From 9f2f61ce9fcf56da439a1160e7cd8005a0092b30 Mon Sep 17 00:00:00 2001 From: Aiwe Date: Fri, 16 Oct 2020 14:42:41 +0300 Subject: [PATCH] Overhaul of blockchain API, massive sync speedup (#3) - Use new faster CN crypto - Use new Karbo daemon's JSON RPC methods made especially for this wallet to speedup sync --- .gitignore | 2 - Deploy.md | 7 +- debug-router.php | 30 - src/api.html | 2 +- src/config.ts | 1 + src/d/config.d.ts | 1 + src/d/nacl.d.ts | 18 +- src/filters/Filters.ts | 80 + src/model/AppState.ts | 92 +- src/model/Cn.ts | 2331 +++++++++++++++++ src/model/CnUtilNative.ts | 114 - src/model/CryptoUtils.ts | 241 -- src/model/Functions.ts | 35 + src/model/KeysRepository.ts | 8 +- src/model/Transaction.ts | 66 +- src/model/TransactionsExplorer.ts | 453 ++-- src/model/Wallet.ts | 8 +- src/model/WalletWatchdog.ts | 330 +++ src/model/blockchain/BlockchainExplorer.ts | 62 +- .../blockchain/BlockchainExplorerRPCDaemon.ts | 396 +++ .../blockchain/BlockchainExplorerRpc2.ts | 545 ---- src/pages/changeWalletPassword.ts | 5 +- src/pages/createWallet.ts | 13 +- src/pages/importFromFile.ts | 4 +- src/pages/importFromKeys.ts | 16 +- src/pages/importFromMnemonic.ts | 93 +- src/pages/importFromQr.ts | 12 +- src/pages/network.ts | 49 +- src/pages/receive.ts | 5 +- src/pages/send.ts | 17 +- src/pages/settings.html | 4 +- src/pages/settings.ts | 18 +- src/pages/support.ts | 9 +- src/providers/BlockchainExplorerProvider.ts | 13 +- src/workers/TransferProcessing.ts | 70 +- src/workers/TransferProcessingEntrypoint.ts | 6 +- src_api/blockchain.php | 366 --- src_api/config.php | 7 - src_api/getTransactionPool.php | 80 - src_api/getheight.php | 31 - src_api/network.php | 46 - src_api/openAlias.php | 60 - src_api/sendrawtransaction.php | 29 - 43 files changed, 3824 insertions(+), 1951 deletions(-) delete mode 100644 debug-router.php create mode 100644 src/filters/Filters.ts create mode 100644 src/model/Cn.ts delete mode 100644 src/model/CnUtilNative.ts delete mode 100644 src/model/CryptoUtils.ts create mode 100644 src/model/Functions.ts create mode 100644 src/model/WalletWatchdog.ts create mode 100644 src/model/blockchain/BlockchainExplorerRPCDaemon.ts delete mode 100644 src/model/blockchain/BlockchainExplorerRpc2.ts delete mode 100644 src_api/blockchain.php delete mode 100644 src_api/config.php delete mode 100644 src_api/getTransactionPool.php delete mode 100644 src_api/getheight.php delete mode 100644 src_api/network.php delete mode 100644 src_api/openAlias.php delete mode 100644 src_api/sendrawtransaction.php diff --git a/.gitignore b/.gitignore index 34d52de6..20682bdb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ .idea/ *.iml node_modules -src_api/cache* -src_api/lastRun.txt src/**/*.js src/**/*.js.map !src/lib/*.js diff --git a/Deploy.md b/Deploy.md index 227a4035..e2e8d85d 100644 --- a/Deploy.md +++ b/Deploy.md @@ -7,8 +7,8 @@ You also need to build some files that are dynamically generated like the manife This task is doable with : ``` npm install -nodejs ./node_modules/typescript/bin/tsc --project tsconfig.prod.json -nodejs build.js +node ./node_modules/typescript/bin/tsc --project tsconfig.prod.json +node build.js ``` The first task install dependencies (typescript) and the text one compile the typescript code. We are using a custom tsconfig file which is optimized for production. @@ -21,9 +21,6 @@ That's all # Deploy All the content of the src directory needs to be exposed with a web-server. -You will also need to expose the content of the src_api content to an endpoint which can interpret PHP. -By default the configuration looks at domainname.com/api/ - # Permissions The API stores precomputed data for performances in a directory called cache/ in the same directory of the API code (PHP code). diff --git a/debug-router.php b/debug-router.php deleted file mode 100644 index f97863c1..00000000 --- a/debug-router.php +++ /dev/null @@ -1,30 +0,0 @@ -Karbo Wallet API relay - + diff --git a/src/config.ts b/src/config.ts index 78494106..043780b0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -32,6 +32,7 @@ global.config = { idleTimeout: 30, idleWarningDuration: 20, + syncBlockCount: 50, coinSymbol: 'KRB', openAliasPrefix: "krb", diff --git a/src/d/config.d.ts b/src/d/config.d.ts index 4c1c7a2f..a7444376 100644 --- a/src/d/config.d.ts +++ b/src/d/config.d.ts @@ -28,6 +28,7 @@ declare var config : { txChargeAddress: string, idleTimeout: number, idleWarningDuration: number, + syncBlockCount: number, maxBlockNumber: number, avgBlockTime: number, }; diff --git a/src/d/nacl.d.ts b/src/d/nacl.d.ts index d335a42a..85f1d3e5 100644 --- a/src/d/nacl.d.ts +++ b/src/d/nacl.d.ts @@ -1 +1,17 @@ -declare var nacl : any; \ No newline at end of file +declare var nacl : { + ll:{ + ge_scalarmult:(a : Uint8Array, b : Uint8Array)=>Uint8Array, + ge_double_scalarmult_base_vartime:(a : Uint8Array, b : Uint8Array, c : Uint8Array)=>Uint8Array, + ge_double_scalarmult_postcomp_vartime:(a : Uint8Array, b : Uint8Array, c : Uint8Array, d : Uint8Array)=>Uint8Array, + ge_add:(a : Uint8Array, b : Uint8Array)=>Uint8Array, + ge_scalarmult_base:(a : Uint8Array)=>Uint8Array + }, + secretbox:any, + //open:(encrypted:Uint8Array, nonce:Uint8Array, privKey:Uint8Array)=>Uint8Array; + util:{ + encodeBase64:(value : Uint8Array)=>string, + }, + randomBytes:(bits : number) => Uint8Array +}; + +declare function keccak_256(bin : Uint8Array) : string; diff --git a/src/filters/Filters.ts b/src/filters/Filters.ts new file mode 100644 index 00000000..9c36b59f --- /dev/null +++ b/src/filters/Filters.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2018-2020, ExploShot + * Copyright (c) 2018-2020, The Qwertycoin Project + * Copyright (c) 2020, The Masari Project + * + * All rights reserved. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * ==> Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * ==> Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * ==> Neither the name of Qwertycoin nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function VueFilterSatoshis(value: number) { + return '₿ ' + value.toFixed(8) +} + +export function VueFilterPiconero(value: number) { + return value.toFixed(12) +} + +export function VueFilterFiat(value: number, currency: string) { + if (currency == 'usd' || currency == 'aud' || currency == 'cad' || currency == 'nzd') { + return '$ ' + value.toFixed(2); + } + if (currency == 'eur') { + return '€ ' + value.toFixed(2); + } + if (currency == 'jpy') { + return '¥ ' + value.toFixed(2); + } + if (currency == 'gbp') { + return '£ ' + value.toFixed(2); + } + if (currency == 'chf') { + return 'Fr. ' + value.toFixed(2); + } + if (currency == 'sek') { + return 'kr ' + value.toFixed(2); + } + if (currency == 'czk') { + return 'CZK ' + value.toFixed(2); + } + if (currency == 'eth') { + return 'Ξ ' + value.toFixed(2); + } + if (currency == 'ltc') { + return 'Ł ' + value.toFixed(2); + } +} + +export function VueFilterHashrate(hashrate: number) { + let i = 0; + let byteUnits = ['H', 'kH', 'MH', 'GH', 'TH', 'PH', 'EH', 'ZH', 'YH']; + + while (hashrate > 1000) { + hashrate = hashrate / 1000; + i++; + } + + return hashrate.toFixed(2) + byteUnits[i]; +} \ No newline at end of file diff --git a/src/model/AppState.ts b/src/model/AppState.ts index f1c97df8..9f1144c4 100644 --- a/src/model/AppState.ts +++ b/src/model/AppState.ts @@ -14,28 +14,27 @@ */ import {DependencyInjectorInstance} from "../lib/numbersLab/DependencyInjector"; -import {BlockchainExplorerRpc2, WalletWatchdog} from "./blockchain/BlockchainExplorerRpc2"; import {Wallet} from "./Wallet"; import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; import {Observable} from "../lib/numbersLab/Observable"; import {WalletRepository} from "./WalletRepository"; -import {BlockchainExplorer} from "./blockchain/BlockchainExplorer"; -import {Constants} from "./Constants"; +import {BlockchainExplorer, RawDaemon_Transaction} from "./blockchain/BlockchainExplorer"; import {TransactionsExplorer} from "./TransactionsExplorer"; +import {WalletWatchdog} from "./WalletWatchdog"; -export class WalletWorker{ - wallet : Wallet; - password : string; +export class WalletWorker { + wallet: Wallet; + password: string; intervalSave = 0; - constructor(wallet: Wallet, password:string) { + constructor(wallet: Wallet, password: string) { this.wallet = wallet; this.password = password; let self = this; - wallet.addObserver(Observable.EVENT_MODIFIED, function(){ - if(self.intervalSave === 0) - self.intervalSave = setTimeout(function(){ + wallet.addObserver(Observable.EVENT_MODIFIED, function () { + if (self.intervalSave === 0) + self.intervalSave = setTimeout(function () { self.save(); self.intervalSave = 0; }, 1000); @@ -44,65 +43,68 @@ export class WalletWorker{ this.save(); } - save(){ + save() { WalletRepository.save(this.wallet, this.password); } } -export class AppState{ +export class AppState { - static openWallet(wallet : Wallet, password:string){ + static openWallet(wallet: Wallet, password: string) { let walletWorker = new WalletWorker(wallet, password); - DependencyInjectorInstance().register(Wallet.name,wallet); + DependencyInjectorInstance().register(Wallet.name, wallet); let watchdog = BlockchainExplorerProvider.getInstance().watchdog(wallet); - DependencyInjectorInstance().register(WalletWatchdog.name,watchdog); - DependencyInjectorInstance().register(WalletWorker.name,walletWorker); + DependencyInjectorInstance().register(WalletWatchdog.name, watchdog); + DependencyInjectorInstance().register(WalletWorker.name, walletWorker); $('body').addClass('connected'); - if(wallet.isViewOnly()) + if (wallet.isViewOnly()) $('body').addClass('viewOnlyWallet'); } - static disconnect(){ - let wallet : Wallet = DependencyInjectorInstance().getInstance(Wallet.name,'default', false); - let walletWorker : WalletWorker = DependencyInjectorInstance().getInstance(WalletWorker.name,'default', false); - let walletWatchdog : WalletWatchdog = DependencyInjectorInstance().getInstance(WalletWatchdog.name,'default', false); - if(walletWatchdog !== null) + static disconnect() { + let wallet: Wallet = DependencyInjectorInstance().getInstance(Wallet.name, 'default', false); + let walletWorker: WalletWorker = DependencyInjectorInstance().getInstance(WalletWorker.name, 'default', false); + let walletWatchdog: WalletWatchdog = DependencyInjectorInstance().getInstance(WalletWatchdog.name, 'default', false); + if (walletWatchdog !== null) walletWatchdog.stop(); - DependencyInjectorInstance().register(Wallet.name,undefined,'default'); - DependencyInjectorInstance().register(WalletWorker.name,undefined,'default'); - DependencyInjectorInstance().register(WalletWatchdog.name,undefined,'default'); + DependencyInjectorInstance().register(Wallet.name, undefined, 'default'); + DependencyInjectorInstance().register(WalletWorker.name, undefined, 'default'); + DependencyInjectorInstance().register(WalletWatchdog.name, undefined, 'default'); $('body').removeClass('connected'); $('body').removeClass('viewOnlyWallet'); } private static leftMenuEnabled = false; - static enableLeftMenu(){ - if(!this.leftMenuEnabled) { + + static enableLeftMenu() { + if (!this.leftMenuEnabled) { this.leftMenuEnabled = true; $('body').removeClass('menuDisabled'); } } - static disableLeftMenu(){ - if(this.leftMenuEnabled) { + + static disableLeftMenu() { + if (this.leftMenuEnabled) { this.leftMenuEnabled = false; $('body').addClass('menuDisabled'); } } - static askUserOpenWallet(redirectToHome:boolean=true){ + static askUserOpenWallet(redirectToHome: boolean = true) { let self = this; return new Promise(function (resolve, reject) { + swal({ title: i18n.t('global.openWalletModal.title'), input: 'password', showCancelButton: true, confirmButtonText: i18n.t('global.openWalletModal.confirmText'), cancelButtonText: i18n.t('global.openWalletModal.cancelText'), - }).then((result:any) => { - setTimeout(function(){//for async + }).then((result: any) => { + setTimeout(function () { //for async if (result.value) { swal({ type: 'info', @@ -115,8 +117,8 @@ export class AppState{ let savePassword = result.value; // let password = prompt(); let memoryWallet = DependencyInjectorInstance().getInstance(Wallet.name, 'default', false); - if(memoryWallet === null){ - WalletRepository.getLocalWalletWithPassword(savePassword).then((wallet : Wallet|null) => { + if (memoryWallet === null) { + WalletRepository.getLocalWalletWithPassword(savePassword).then((wallet: Wallet | null) => { //console.log(wallet); if (wallet !== null) { wallet.recalculateIfNotViewOnly(); @@ -130,19 +132,23 @@ export class AppState{ } let blockchainHeightToRescan = Object.keys(blockchainHeightToRescanObj); if (blockchainHeightToRescan.length > 0) { - let blockchainExplorer: BlockchainExplorerRpc2 = BlockchainExplorerProvider.getInstance(); + let blockchainExplorer: BlockchainExplorer = BlockchainExplorerProvider.getInstance(); let promisesBlocks = []; for (let height of blockchainHeightToRescan) { - promisesBlocks.push(blockchainExplorer.getTransactionsForBlocks(parseInt(height))); + promisesBlocks.push(blockchainExplorer.getTransactionsForBlocks(parseInt(height), parseInt(height), wallet.options.checkMinerTx)); + //console.log(`promisesBlocks.length: ${promisesBlocks.length}`); } - Promise.all(promisesBlocks).then(function (arrayOfTxs: Array) { + + Promise.all(promisesBlocks).then(function (arrayOfTxs: Array) { for (let txs of arrayOfTxs) { for (let rawTx of txs) { if (wallet !== null) { let tx = TransactionsExplorer.parse(rawTx, wallet); - if (tx !== null) + if (tx !== null) { + console.log(`Added new Tx ${tx.hash} to wallet`); wallet.addNew(tx); + } } } } @@ -167,16 +173,14 @@ export class AppState{ reject(); } }); - }else { + } else { swal.close(); window.location.href = '#account'; } - }else + } else reject(); - },1); + }, 1); }); }); } - - -} \ No newline at end of file +} diff --git a/src/model/Cn.ts b/src/model/Cn.ts new file mode 100644 index 00000000..73ff11a1 --- /dev/null +++ b/src/model/Cn.ts @@ -0,0 +1,2331 @@ +/** + * Copyright (c) 2018, Gnock + * Copyright (c) 2018-2020, ExploShot + * Copyright (c) 2018-2020, The Qwertycoin Project + * Copyright (c) 2018-2020, The Masari Project + * Copyright (c) 2014-2018, MyMonero.com + * + * All rights reserved. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * ==> Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * ==> Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * ==> Neither the name of Qwertycoin nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {Mnemonic} from "./Mnemonic"; +import {Constants} from "./Constants"; + +declare let Module : any; + +let HASH_STATE_BYTES = 200; +let HASH_SIZE = 32; +let ADDRESS_CHECKSUM_SIZE = 4; +let INTEGRATED_ID_SIZE = 8; +let ENCRYPTED_PAYMENT_ID_TAIL = 141; +let CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = config.addressPrefix; +let CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = config.integratedAddressPrefix; +let CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = config.subAddressPrefix; +if (config.testnet === true) +{ + CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = config.addressPrefixTestnet; + CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = config.integratedAddressPrefixTestnet; + CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = config.subAddressPrefixTestnet; +} +let UINT64_MAX = new JSBigInt(2).pow(64); +let CURRENT_TX_VERSION = 1; +let OLD_TX_VERSION = 1; +let TX_EXTRA_NONCE_MAX_COUNT = 255; +let TX_EXTRA_TAGS = { + PADDING: '00', + PUBKEY: '01', + NONCE: '02', + MERGE_MINING: '03', + ADDITIONAL_PUBKEY: '04' +}; +let TX_EXTRA_NONCE_TAGS = { + PAYMENT_ID: '00', + ENCRYPTED_PAYMENT_ID: '01' +}; +let KEY_SIZE = 32; +let STRUCT_SIZES = { + GE_P3: 160, + GE_P2: 120, + GE_P1P1: 160, + GE_CACHED: 160, + EC_SCALAR: 32, + EC_POINT: 32, + KEY_IMAGE: 32, + GE_DSMP: 160 * 8, // ge_cached * 8 + SIGNATURE: 64 // ec_scalar * 2 +}; + +export namespace CnVars{ + export enum RCT_TYPE{ + Null = 0, + Full = 1, + Simple = 2, + FullBulletproof = 3, + SimpleBulletproof = 4, + } + + export let H = "8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94"; //base H for amounts + export let l = JSBigInt("7237005577332262213973186563042994240857116359379907606001950938285454250989"); //curve order (not RCT specific) + export let I = "0100000000000000000000000000000000000000000000000000000000000000"; //identity element + export let Z = "0000000000000000000000000000000000000000000000000000000000000000"; //zero scalar + //H2 object to speed up some operations + export let H2 = ["8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94", "8faa448ae4b3e2bb3d4d130909f55fcd79711c1c83cdbccadd42cbe1515e8712", + "12a7d62c7791654a57f3e67694ed50b49a7d9e3fc1e4c7a0bde29d187e9cc71d", "789ab9934b49c4f9e6785c6d57a498b3ead443f04f13df110c5427b4f214c739", + "771e9299d94f02ac72e38e44de568ac1dcb2edc6edb61f83ca418e1077ce3de8", "73b96db43039819bdaf5680e5c32d741488884d18d93866d4074a849182a8a64", + "8d458e1c2f68ebebccd2fd5d379f5e58f8134df3e0e88cad3d46701063a8d412", "09551edbe494418e81284455d64b35ee8ac093068a5f161fa6637559177ef404", + "d05a8866f4df8cee1e268b1d23a4c58c92e760309786cdac0feda1d247a9c9a7", "55cdaad518bd871dd1eb7bc7023e1dc0fdf3339864f88fdd2de269fe9ee1832d", + "e7697e951a98cfd5712b84bbe5f34ed733e9473fcb68eda66e3788df1958c306", "f92a970bae72782989bfc83adfaa92a4f49c7e95918b3bba3cdc7fe88acc8d47", + "1f66c2d491d75af915c8db6a6d1cb0cd4f7ddcd5e63d3ba9b83c866c39ef3a2b", "3eec9884b43f58e93ef8deea260004efea2a46344fc5965b1a7dd5d18997efa7", + "b29f8f0ccb96977fe777d489d6be9e7ebc19c409b5103568f277611d7ea84894", "56b1f51265b9559876d58d249d0c146d69a103636699874d3f90473550fe3f2c", + "1d7a36575e22f5d139ff9cc510fa138505576b63815a94e4b012bfd457caaada", "d0ac507a864ecd0593fa67be7d23134392d00e4007e2534878d9b242e10d7620", + "f6c6840b9cf145bb2dccf86e940be0fc098e32e31099d56f7fe087bd5deb5094", "28831a3340070eb1db87c12e05980d5f33e9ef90f83a4817c9f4a0a33227e197", + "87632273d629ccb7e1ed1a768fa2ebd51760f32e1c0b867a5d368d5271055c6e", "5c7b29424347964d04275517c5ae14b6b5ea2798b573fc94e6e44a5321600cfb", + "e6945042d78bc2c3bd6ec58c511a9fe859c0ad63fde494f5039e0e8232612bd5", "36d56907e2ec745db6e54f0b2e1b2300abcb422e712da588a40d3f1ebbbe02f6", + "34db6ee4d0608e5f783650495a3b2f5273c5134e5284e4fdf96627bb16e31e6b", "8e7659fb45a3787d674ae86731faa2538ec0fdf442ab26e9c791fada089467e9", + "3006cf198b24f31bb4c7e6346000abc701e827cfbb5df52dcfa42e9ca9ff0802", "f5fd403cb6e8be21472e377ffd805a8c6083ea4803b8485389cc3ebc215f002a", + "3731b260eb3f9482e45f1c3f3b9dcf834b75e6eef8c40f461ea27e8b6ed9473d", "9f9dab09c3f5e42855c2de971b659328a2dbc454845f396ffc053f0bb192f8c3", + "5e055d25f85fdb98f273e4afe08464c003b70f1ef0677bb5e25706400be620a5", "868bcf3679cb6b500b94418c0b8925f9865530303ae4e4b262591865666a4590", + "b3db6bd3897afbd1df3f9644ab21c8050e1f0038a52f7ca95ac0c3de7558cb7a", "8119b3a059ff2cac483e69bcd41d6d27149447914288bbeaee3413e6dcc6d1eb", + "10fc58f35fc7fe7ae875524bb5850003005b7f978c0c65e2a965464b6d00819c", "5acd94eb3c578379c1ea58a343ec4fcff962776fe35521e475a0e06d887b2db9", + "33daf3a214d6e0d42d2300a7b44b39290db8989b427974cd865db011055a2901", "cfc6572f29afd164a494e64e6f1aeb820c3e7da355144e5124a391d06e9f95ea", + "d5312a4b0ef615a331f6352c2ed21dac9e7c36398b939aec901c257f6cbc9e8e", "551d67fefc7b5b9f9fdbf6af57c96c8a74d7e45a002078a7b5ba45c6fde93e33", + "d50ac7bd5ca593c656928f38428017fc7ba502854c43d8414950e96ecb405dc3", "0773e18ea1be44fe1a97e239573cfae3e4e95ef9aa9faabeac1274d3ad261604", + "e9af0e7ca89330d2b8615d1b4137ca617e21297f2f0ded8e31b7d2ead8714660", "7b124583097f1029a0c74191fe7378c9105acc706695ed1493bb76034226a57b", + "ec40057b995476650b3db98e9db75738a8cd2f94d863b906150c56aac19caa6b", "01d9ff729efd39d83784c0fe59c4ae81a67034cb53c943fb818b9d8ae7fc33e5", + "00dfb3c696328c76424519a7befe8e0f6c76f947b52767916d24823f735baf2e", "461b799b4d9ceea8d580dcb76d11150d535e1639d16003c3fb7e9d1fd13083a8", + "ee03039479e5228fdc551cbde7079d3412ea186a517ccc63e46e9fcce4fe3a6c", "a8cfb543524e7f02b9f045acd543c21c373b4c9b98ac20cec417a6ddb5744e94", + "932b794bf89c6edaf5d0650c7c4bad9242b25626e37ead5aa75ec8c64e09dd4f", "16b10c779ce5cfef59c7710d2e68441ea6facb68e9b5f7d533ae0bb78e28bf57", + "0f77c76743e7396f9910139f4937d837ae54e21038ac5c0b3fd6ef171a28a7e4", "d7e574b7b952f293e80dde905eb509373f3f6cd109a02208b3c1e924080a20ca", + "45666f8c381e3da675563ff8ba23f83bfac30c34abdde6e5c0975ef9fd700cb9", "b24612e454607eb1aba447f816d1a4551ef95fa7247fb7c1f503020a7177f0dd", + "7e208861856da42c8bb46a7567f8121362d9fb2496f131a4aa9017cf366cdfce", "5b646bff6ad1100165037a055601ea02358c0f41050f9dfe3c95dccbd3087be0", + "746d1dccfed2f0ff1e13c51e2d50d5324375fbd5bf7ca82a8931828d801d43ab", "cb98110d4a6bb97d22feadbc6c0d8930c5f8fc508b2fc5b35328d26b88db19ae", + "60b626a033b55f27d7676c4095eababc7a2c7ede2624b472e97f64f96b8cfc0e", "e5b52bc927468df71893eb8197ef820cf76cb0aaf6e8e4fe93ad62d803983104", + "056541ae5da9961be2b0a5e895e5c5ba153cbb62dd561a427bad0ffd41923199", "f8fef05a3fa5c9f3eba41638b247b711a99f960fe73aa2f90136aeb20329b888"]; +} + +export namespace CnRandom{ + // Generate a 256-bit / 64-char / 32-byte crypto random + export function rand_32() { + return Mnemonic.mn_random(256); + } + + // Generate a 128-bit / 32-char / 16-byte crypto random + export function rand_16() { + return Mnemonic.mn_random(128); + } + + // Generate a 64-bit / 16-char / 8-byte crypto random + export function rand_8() { + return Mnemonic.mn_random(64); + } + + export function random_scalar() { + //let rand = this.sc_reduce(mn_random(64 * 8)); + //return rand.slice(0, STRUCT_SIZES.EC_SCALAR * 2); + return CnNativeBride.sc_reduce32(CnRandom.rand_32()); + } +} + +export namespace CnUtils{ + + export function hextobin(hex : string) : Uint8Array{ + if (hex.length % 2 !== 0) throw "Hex string has invalid length!"; + let res = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length / 2; ++i) { + res[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return res; + } + + export function bintohex(bin : Uint8Array|string) : string { + let out = []; + if(typeof bin === 'string'){ + for (let i = 0; i < bin.length; ++i) { + out.push(("0" + bin[i].charCodeAt(0).toString(16)).slice(-2)); + } + }else { + for (let i = 0; i < bin.length; ++i) { + out.push(("0" + bin[i].toString(16)).slice(-2)); + } + } + return out.join(""); + } + + //switch byte order for hex string + export function swapEndian(hex : string){ + if (hex.length % 2 !== 0){return "length must be a multiple of 2!";} + let data = ""; + for (let i=1; i <= hex.length / 2; i++){ + data += hex.substr(0 - 2 * i, 2); + } + return data; + } + + //switch byte order charwise + export function swapEndianC(string : string) : string{ + let data = ""; + for (let i=1; i <= string.length; i++){ + data += string.substr(0 - i, 1); + } + return data; + } + +//for most uses you'll also want to swapEndian after conversion + //mainly to convert integer "scalars" to usable hexadecimal strings + export function d2h(integer : number|string){ + if (typeof integer !== "string" && integer.toString().length > 15){throw "integer should be entered as a string for precision";} + let padding = ""; + for (let i = 0; i < 63; i++){ + padding += "0"; + } + return (padding + JSBigInt(integer).toString(16).toLowerCase()).slice(-64); + } + + //integer (string) to scalar + export function d2s(integer : number|string){ + if (typeof integer === "string") { + return CnUtils.swapEndian(CnUtils.d2h(integer)); + } else { + return CnUtils.swapEndian(CnUtils.d2h(integer.toString())); + } + + } + + // hexadecimal to integer + export function h2d(hex : any) { + /*let vali = 0; + for (let j = 7; j >= 0; j--) { + vali = (vali * 256 + test[j].charCodeAt(0)); + } + return vali;*/ + // return JSBigInt.parse(test,16); + // let bytes = Crypto.hextobin(test); + // console.log('bytes',bytes, test,swapEndianC(test)); + // console.log(JSBigInt.parse(swapEndianC(test),16).valueOf()); + // console.log(JSBigInt.parse(test.substr(0,12),16).valueOf()); + let vali = 0; + for (let j = 7; j >= 0; j--) { + // console.log(vali,vali*256,bytes[j]); + vali = (vali * 256 + parseInt(hex.slice(j*2, j*2+2), 16)); + } + return vali; + } + + export function d2b(integer : number) : string{ + let integerStr = integer.toString(); + if (typeof integer !== "string" && integerStr.length > 15){throw "integer should be entered as a string for precision";} + let padding = ""; + for (let i = 0; i < 63; i++){ + padding += "0"; + } + let a = new JSBigInt(integerStr); + if (a.toString(2).length > 64){throw "amount overflows uint64!";} + return CnUtils.swapEndianC((padding + a.toString(2)).slice(-64)); + } + + export function ge_scalarmult(pub : string, sec : string) { + if (pub.length !== 64 || sec.length !== 64) { + throw "Invalid input length"; + } + return CnUtils.bintohex(nacl.ll.ge_scalarmult(CnUtils.hextobin(pub), CnUtils.hextobin(sec))); + } + + export function ge_add(p1 : string, p2 : string) { + if (p1.length !== 64 || p2.length !== 64) { + throw "Invalid input length!"; + } + return bintohex(nacl.ll.ge_add(hextobin(p1), hextobin(p2))); + } + + //curve and scalar functions; split out to make their host functions cleaner and more readable + //inverts X coordinate -- this seems correct ^_^ -luigi1111 + export function ge_neg(point : string) { + if (point.length !== 64){ + throw "expected 64 char hex string"; + } + return point.slice(0,62) + ((parseInt(point.slice(62,63), 16) + 8) % 16).toString(16) + point.slice(63,64); + } + + //order matters + export function ge_sub(point1 : string, point2 : string) { + let point2n = CnUtils.ge_neg(point2); + return CnUtils.ge_add(point1, point2n); + } + + export function sec_key_to_pub(sec : string) : string { + if (sec.length !== 64) { + throw "Invalid sec length"; + } + return CnUtils.bintohex(nacl.ll.ge_scalarmult_base(hextobin(sec))); + } + + export function valid_hex(hex : string) { + let exp = new RegExp("[0-9a-fA-F]{" + hex.length + "}"); + return exp.test(hex); + } + + export function ge_scalarmult_base(sec : string) : string{ + return CnUtils.sec_key_to_pub(sec); + } + + export function derivation_to_scalar(derivation : string, output_index : number) { + let buf = ""; + if (derivation.length !== (STRUCT_SIZES.EC_POINT * 2)) { + throw "Invalid derivation length!"; + } + buf += derivation; + let enc = CnUtils.encode_varint(output_index); + if (enc.length > 10 * 2) { + throw "output_index didn't fit in 64-bit varint"; + } + buf += enc; + return Cn.hash_to_scalar(buf); + } + + export function encode_varint(i : number|string) { + let j = new JSBigInt(i); + let out = ''; + // While i >= b10000000 + while (j.compare(0x80) >= 0) { + // out.append i & b01111111 | b10000000 + out += ("0" + ((j.lowVal() & 0x7f) | 0x80).toString(16)).slice(-2); + j = j.divide(new JSBigInt(2).pow(7)); + } + out += ("0" + j.toJSValue().toString(16)).slice(-2); + return out; + } + + export function cn_fast_hash(input : string) { + if (input.length % 2 !== 0 || !CnUtils.valid_hex(input)) { + throw "Input invalid"; + } + //update to use new keccak impl (approx 45x faster) + //let state = this.keccak(input, inlen, HASH_STATE_BYTES); + //return state.substr(0, HASH_SIZE * 2); + return keccak_256(CnUtils.hextobin(input)); + } + + export function hex_xor(hex1 : string, hex2 : string) { + if (!hex1 || !hex2 || hex1.length !== hex2.length || hex1.length % 2 !== 0 || hex2.length % 2 !== 0){throw "Hex string(s) is/are invalid!";} + let bin1 = hextobin(hex1); + let bin2 = hextobin(hex2); + let xor = new Uint8Array(bin1.length); + for (let i = 0; i < xor.length; i++){ + xor[i] = bin1[i] ^ bin2[i]; + } + return bintohex(xor); + } + + export function trimRight(str : string, char : string) { + while (str[str.length - 1] == char) str = str.slice(0, -1); + return str; + } + + export function padLeft(str : string, len : number, char : string) { + while (str.length < len) { + str = char + str; + } + return str; + } + + export function ge_double_scalarmult_base_vartime(c : string, P : string, r : string) : string{ + if (c.length !== 64 || P.length !== 64 || r.length !== 64) { + throw "Invalid input length!"; + } + return bintohex(nacl.ll.ge_double_scalarmult_base_vartime(hextobin(c), hextobin(P), hextobin(r))); + } + + export function ge_double_scalarmult_postcomp_vartime(r : string, P : string, c : string, I : string) { + if (c.length !== 64 || P.length !== 64 || r.length !== 64 || I.length !== 64) { + throw "Invalid input length!"; + } + let Pb = CnNativeBride.hash_to_ec_2(P); + return bintohex(nacl.ll.ge_double_scalarmult_postcomp_vartime(hextobin(r), hextobin(Pb), hextobin(c), hextobin(I))); + } + + export function decompose_amount_into_digits(amount : number|string) { + amount = amount.toString(); + let ret = []; + while (amount.length > 0) { + //check so we don't create 0s + if (amount[0] !== "0"){ + let digit = amount[0]; + while (digit.length < amount.length) { + digit += "0"; + } + ret.push(new JSBigInt(digit)); + } + amount = amount.slice(1); + } + return ret; + } + + export function decode_rct_ecdh(ecdh : {mask:string, amount:string}, key : string) { + let first = Cn.hash_to_scalar(key); + let second = Cn.hash_to_scalar(first); + return { + mask: CnNativeBride.sc_sub(ecdh.mask, first), + amount: CnNativeBride.sc_sub(ecdh.amount, second), + }; + } + + export function encode_rct_ecdh(ecdh : {mask:string, amount:string}, key : string) { + let first = Cn.hash_to_scalar(key); + let second = Cn.hash_to_scalar(first); + return { + mask: CnNativeBride.sc_add(ecdh.mask, first), + amount: CnNativeBride.sc_add(ecdh.amount, second), + }; + } +} + +export namespace CnNativeBride{ + export function sc_reduce32(hex : string) { + let input = CnUtils.hextobin(hex); + if (input.length !== 32) { + throw "Invalid input length"; + } + let mem = Module._malloc(32); + Module.HEAPU8.set(input, mem); + Module.ccall('sc_reduce32', 'void', ['number'], [mem]); + let output = Module.HEAPU8.subarray(mem, mem + 32); + Module._free(mem); + return CnUtils.bintohex(output); + } + + export function derive_secret_key(derivation : string, out_index : number, sec : string) { + if (derivation.length !== 64 || sec.length !== 64) { + throw "Invalid input length!"; + } + let scalar_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + let scalar_b = CnUtils.hextobin(CnUtils.derivation_to_scalar(derivation, out_index)); + Module.HEAPU8.set(scalar_b, scalar_m); + let base_m = Module._malloc(KEY_SIZE); + Module.HEAPU8.set(CnUtils.hextobin(sec), base_m); + let derived_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + Module.ccall("sc_add", "void", ["number", "number", "number"], [derived_m, base_m, scalar_m]); + let res = Module.HEAPU8.subarray(derived_m, derived_m + STRUCT_SIZES.EC_SCALAR); + Module._free(scalar_m); + Module._free(base_m); + Module._free(derived_m); + return CnUtils.bintohex(res); + } + + export function hash_to_ec(key : string) { + if (key.length !== (KEY_SIZE * 2)) { + throw "Invalid input length"; + } + let h_m = Module._malloc(HASH_SIZE); + let point_m = Module._malloc(STRUCT_SIZES.GE_P2); + let point2_m = Module._malloc(STRUCT_SIZES.GE_P1P1); + let res_m = Module._malloc(STRUCT_SIZES.GE_P3); + let hash = CnUtils.hextobin(CnUtils.cn_fast_hash(key)); + Module.HEAPU8.set(hash, h_m); + Module.ccall("ge_fromfe_frombytes_vartime", "void", ["number", "number"], [point_m, h_m]); + Module.ccall("ge_mul8", "void", ["number", "number"], [point2_m, point_m]); + Module.ccall("ge_p1p1_to_p3", "void", ["number", "number"], [res_m, point2_m]); + let res = Module.HEAPU8.subarray(res_m, res_m + STRUCT_SIZES.GE_P3); + Module._free(h_m); + Module._free(point_m); + Module._free(point2_m); + Module._free(res_m); + return CnUtils.bintohex(res); + } + + //returns a 32 byte point via "ge_p3_tobytes" rather than a 160 byte "p3", otherwise same as above; + export function hash_to_ec_2(key : string) { + if (key.length !== (KEY_SIZE * 2)) { + throw "Invalid input length"; + } + let h_m = Module._malloc(HASH_SIZE); + let point_m = Module._malloc(STRUCT_SIZES.GE_P2); + let point2_m = Module._malloc(STRUCT_SIZES.GE_P1P1); + let res_m = Module._malloc(STRUCT_SIZES.GE_P3); + let hash = CnUtils.hextobin(CnUtils.cn_fast_hash(key)); + let res2_m = Module._malloc(KEY_SIZE); + Module.HEAPU8.set(hash, h_m); + Module.ccall("ge_fromfe_frombytes_vartime", "void", ["number", "number"], [point_m, h_m]); + Module.ccall("ge_mul8", "void", ["number", "number"], [point2_m, point_m]); + Module.ccall("ge_p1p1_to_p3", "void", ["number", "number"], [res_m, point2_m]); + Module.ccall("ge_p3_tobytes", "void", ["number", "number"], [res2_m, res_m]); + let res = Module.HEAPU8.subarray(res2_m, res2_m + KEY_SIZE); + Module._free(h_m); + Module._free(point_m); + Module._free(point2_m); + Module._free(res_m); + Module._free(res2_m); + return CnUtils.bintohex(res); + } + + export function generate_key_image_2(pub : string, sec : string) { + if (!pub || !sec || pub.length !== 64 || sec.length !== 64) { + throw "Invalid input length"; + } + let pub_m = Module._malloc(KEY_SIZE); + let sec_m = Module._malloc(KEY_SIZE); + Module.HEAPU8.set(CnUtils.hextobin(pub), pub_m); + Module.HEAPU8.set(CnUtils.hextobin(sec), sec_m); + if (Module.ccall("sc_check", "number", ["number"], [sec_m]) !== 0) { + throw "sc_check(sec) != 0"; + } + let point_m = Module._malloc(STRUCT_SIZES.GE_P3); + let point2_m = Module._malloc(STRUCT_SIZES.GE_P2); + let point_b = CnUtils.hextobin(CnNativeBride.hash_to_ec(pub)); + Module.HEAPU8.set(point_b, point_m); + let image_m = Module._malloc(STRUCT_SIZES.KEY_IMAGE); + Module.ccall("ge_scalarmult", "void", ["number", "number", "number"], [point2_m, sec_m, point_m]); + Module.ccall("ge_tobytes", "void", ["number", "number"], [image_m, point2_m]); + let res = Module.HEAPU8.subarray(image_m, image_m + STRUCT_SIZES.KEY_IMAGE); + Module._free(pub_m); + Module._free(sec_m); + Module._free(point_m); + Module._free(point2_m); + Module._free(image_m); + return CnUtils.bintohex(res); + } + + //adds two scalars together + export function sc_add(scalar1 : string, scalar2 : string) { + if (scalar1.length !== 64 || scalar2.length !== 64) { + throw "Invalid input length!"; + } + let scalar1_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + let scalar2_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + Module.HEAPU8.set(CnUtils.hextobin(scalar1), scalar1_m); + Module.HEAPU8.set(CnUtils.hextobin(scalar2), scalar2_m); + let derived_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + Module.ccall("sc_add", "void", ["number", "number", "number"], [derived_m, scalar1_m, scalar2_m]); + let res = Module.HEAPU8.subarray(derived_m, derived_m + STRUCT_SIZES.EC_SCALAR); + Module._free(scalar1_m); + Module._free(scalar2_m); + Module._free(derived_m); + return CnUtils.bintohex(res); + } + + //subtracts one scalar from another + export function sc_sub(scalar1 : string, scalar2 : string) { + if (scalar1.length !== 64 || scalar2.length !== 64) { + throw "Invalid input length!"; + } + let scalar1_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + let scalar2_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + Module.HEAPU8.set(CnUtils.hextobin(scalar1), scalar1_m); + Module.HEAPU8.set(CnUtils.hextobin(scalar2), scalar2_m); + let derived_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + Module.ccall("sc_sub", "void", ["number", "number", "number"], [derived_m, scalar1_m, scalar2_m]); + let res = Module.HEAPU8.subarray(derived_m, derived_m + STRUCT_SIZES.EC_SCALAR); + Module._free(scalar1_m); + Module._free(scalar2_m); + Module._free(derived_m); + return CnUtils.bintohex(res); + } + + //res = c - (ab) mod l; argument names copied from the signature implementation + export function sc_mulsub(sigc : string, sec : string, k : string) { + if (k.length !== KEY_SIZE * 2 || sigc.length !== KEY_SIZE * 2 || sec.length !== KEY_SIZE * 2 || !CnUtils.valid_hex(k) || !CnUtils.valid_hex(sigc) || !CnUtils.valid_hex(sec)) { + throw "bad scalar"; + } + let sec_m = Module._malloc(KEY_SIZE); + Module.HEAPU8.set(CnUtils.hextobin(sec), sec_m); + let sigc_m = Module._malloc(KEY_SIZE); + Module.HEAPU8.set(CnUtils.hextobin(sigc), sigc_m); + let k_m = Module._malloc(KEY_SIZE); + Module.HEAPU8.set(CnUtils.hextobin(k), k_m); + let res_m = Module._malloc(KEY_SIZE); + + Module.ccall("sc_mulsub", "void", ["number", "number", "number", "number"], [res_m, sigc_m, sec_m, k_m]); + let res = Module.HEAPU8.subarray(res_m, res_m + KEY_SIZE); + Module._free(k_m); + Module._free(sec_m); + Module._free(sigc_m); + Module._free(res_m); + return CnUtils.bintohex(res); + } + + + + export function generate_ring_signature(prefix_hash : string, k_image : string, keys : string[], sec : string, real_index : number) { + if (k_image.length !== STRUCT_SIZES.KEY_IMAGE * 2) { + throw "invalid key image length"; + } + if (sec.length !== KEY_SIZE * 2) { + throw "Invalid secret key length"; + } + if (prefix_hash.length !== HASH_SIZE * 2 || !CnUtils.valid_hex(prefix_hash)) { + throw "Invalid prefix hash"; + } + if (real_index >= keys.length || real_index < 0) { + throw "real_index is invalid"; + } + let _ge_tobytes = Module.cwrap("ge_tobytes", "void", ["number", "number"]); + let _ge_p3_tobytes = Module.cwrap("ge_p3_tobytes", "void", ["number", "number"]); + let _ge_scalarmult_base = Module.cwrap("ge_scalarmult_base", "void", ["number", "number"]); + let _ge_scalarmult = Module.cwrap("ge_scalarmult", "void", ["number", "number", "number"]); + let _sc_add = Module.cwrap("sc_add", "void", ["number", "number", "number"]); + let _sc_sub = Module.cwrap("sc_sub", "void", ["number", "number", "number"]); + let _sc_mulsub = Module.cwrap("sc_mulsub", "void", ["number", "number", "number", "number"]); + let _sc_0 = Module.cwrap("sc_0", "void", ["number"]); + let _ge_double_scalarmult_base_vartime = Module.cwrap("ge_double_scalarmult_base_vartime", "void", ["number", "number", "number", "number"]); + let _ge_double_scalarmult_precomp_vartime = Module.cwrap("ge_double_scalarmult_precomp_vartime", "void", ["number", "number", "number", "number", "number"]); + let _ge_frombytes_vartime = Module.cwrap("ge_frombytes_vartime", "number", ["number", "number"]); + let _ge_dsm_precomp = Module.cwrap("ge_dsm_precomp", "void", ["number", "number"]); + + let buf_size = STRUCT_SIZES.EC_POINT * 2 * keys.length; + let buf_m = Module._malloc(buf_size); + let sig_size = STRUCT_SIZES.SIGNATURE * keys.length; + let sig_m = Module._malloc(sig_size); + + // Struct pointer helper functions + function buf_a(i : number) { + return buf_m + STRUCT_SIZES.EC_POINT * (2 * i); + } + function buf_b(i : number) { + return buf_m + STRUCT_SIZES.EC_POINT * (2 * i + 1); + } + function sig_c(i : number) { + return sig_m + STRUCT_SIZES.EC_SCALAR * (2 * i); + } + function sig_r(i : number) { + return sig_m + STRUCT_SIZES.EC_SCALAR * (2 * i + 1); + } + let image_m = Module._malloc(STRUCT_SIZES.KEY_IMAGE); + Module.HEAPU8.set(CnUtils.hextobin(k_image), image_m); + let i; + let image_unp_m = Module._malloc(STRUCT_SIZES.GE_P3); + let image_pre_m = Module._malloc(STRUCT_SIZES.GE_DSMP); + let sum_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + let k_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + let h_m = Module._malloc(STRUCT_SIZES.EC_SCALAR); + let tmp2_m = Module._malloc(STRUCT_SIZES.GE_P2); + let tmp3_m = Module._malloc(STRUCT_SIZES.GE_P3); + let pub_m = Module._malloc(KEY_SIZE); + let sec_m = Module._malloc(KEY_SIZE); + Module.HEAPU8.set(CnUtils.hextobin(sec), sec_m); + if (_ge_frombytes_vartime(image_unp_m, image_m) != 0) { + throw "failed to call ge_frombytes_vartime"; + } + _ge_dsm_precomp(image_pre_m, image_unp_m); + _sc_0(sum_m); + for (i = 0; i < keys.length; i++) { + if (i === real_index) { + // Real key + let rand = CnRandom.random_scalar(); + Module.HEAPU8.set(CnUtils.hextobin(rand), k_m); + _ge_scalarmult_base(tmp3_m, k_m); + _ge_p3_tobytes(buf_a(i), tmp3_m); + let ec = CnNativeBride.hash_to_ec(keys[i]); + Module.HEAPU8.set(CnUtils.hextobin(ec), tmp3_m); + _ge_scalarmult(tmp2_m, k_m, tmp3_m); + _ge_tobytes(buf_b(i), tmp2_m); + } else { + Module.HEAPU8.set(CnUtils.hextobin(CnRandom.random_scalar()), sig_c(i)); + Module.HEAPU8.set(CnUtils.hextobin(CnRandom.random_scalar()), sig_r(i)); + Module.HEAPU8.set(CnUtils.hextobin(keys[i]), pub_m); + if (Module.ccall("ge_frombytes_vartime", "void", ["number", "number"], [tmp3_m, pub_m]) !== 0) { + throw "Failed to call ge_frombytes_vartime"; + } + _ge_double_scalarmult_base_vartime(tmp2_m, sig_c(i), tmp3_m, sig_r(i)); + _ge_tobytes(buf_a(i), tmp2_m); + let ec = CnNativeBride.hash_to_ec(keys[i]); + Module.HEAPU8.set(CnUtils.hextobin(ec), tmp3_m); + _ge_double_scalarmult_precomp_vartime(tmp2_m, sig_r(i), tmp3_m, sig_c(i), image_pre_m); + _ge_tobytes(buf_b(i), tmp2_m); + _sc_add(sum_m, sum_m, sig_c(i)); + } + } + let buf_bin = Module.HEAPU8.subarray(buf_m, buf_m + buf_size); + let scalar = Cn.hash_to_scalar(prefix_hash + CnUtils.bintohex(buf_bin)); + Module.HEAPU8.set(CnUtils.hextobin(scalar), h_m); + _sc_sub(sig_c(real_index), h_m, sum_m); + _sc_mulsub(sig_r(real_index), sig_c(real_index), sec_m, k_m); + let sig_data = CnUtils.bintohex(Module.HEAPU8.subarray(sig_m, sig_m + sig_size)); + let sigs = []; + for (let k = 0; k < keys.length; k++) { + sigs.push(sig_data.slice(STRUCT_SIZES.SIGNATURE * 2 * k, STRUCT_SIZES.SIGNATURE * 2 * (k + 1))); + } + Module._free(image_m); + Module._free(image_unp_m); + Module._free(image_pre_m); + Module._free(sum_m); + Module._free(k_m); + Module._free(h_m); + Module._free(tmp2_m); + Module._free(tmp3_m); + Module._free(buf_m); + Module._free(sig_m); + Module._free(pub_m); + Module._free(sec_m); + return sigs; + } + + export function generate_key_derivation(pub : any, sec : any){ + let generate_key_derivation_bind = (self).Module_native.cwrap('generate_key_derivation', null, ['number', 'number', 'number']); + + let pub_b = CnUtils.hextobin(pub); + let sec_b = CnUtils.hextobin(sec); + let Module_native = (self).Module_native; + + let pub_m = Module_native._malloc(KEY_SIZE); + Module_native.HEAPU8.set(pub_b, pub_m); + + let sec_m = Module_native._malloc(KEY_SIZE); + Module_native.HEAPU8.set(sec_b, sec_m); + + let derivation_m = Module_native._malloc(KEY_SIZE); + let r = generate_key_derivation_bind(pub_m,sec_m,derivation_m); + + Module_native._free(pub_m); + Module_native._free(sec_m); + + let res = Module_native.HEAPU8.subarray(derivation_m, derivation_m + KEY_SIZE); + Module_native._free(derivation_m); + + return CnUtils.bintohex(res); + } + + export function derive_public_key(derivation : string, + output_idx_in_tx : number, + pubSpend : string){ + let derive_public_key_bind = (self).Module_native.cwrap('derive_public_key', null, ['number', 'number', 'number', 'number']); + + let derivation_b = CnUtils.hextobin(derivation); + let pub_spend_b = CnUtils.hextobin(pubSpend); + + + let Module_native = (self).Module_native; + + let derivation_m = Module_native._malloc(KEY_SIZE); + Module_native.HEAPU8.set(derivation_b, derivation_m); + + let pub_spend_m = Module_native._malloc(KEY_SIZE); + Module_native.HEAPU8.set(pub_spend_b, pub_spend_m); + + let derived_key_m = Module_native._malloc(KEY_SIZE); + let r = derive_public_key_bind(derivation_m, output_idx_in_tx, pub_spend_m, derived_key_m); + + Module_native._free(derivation_m); + Module_native._free(pub_spend_m); + + let res = Module_native.HEAPU8.subarray(derived_key_m, derived_key_m + KEY_SIZE); + Module_native._free(derived_key_m); + + return CnUtils.bintohex(res); + } +} + +export namespace Cn{ + + export function hash_to_scalar(buf : string) : string{ + let hash = CnUtils.cn_fast_hash(buf); + let scalar = CnNativeBride.sc_reduce32(hash); + return scalar; + } + + export function array_hash_to_scalar(array : string[]) : string{ + let buf = ""; + for (let i = 0; i < array.length; i++){ + if (typeof array[i] !== "string"){throw "unexpected array element";} + buf += array[i]; + } + return hash_to_scalar(buf); + } + + /** + * @deprecated CnNativeBride has a much faster version + * @param pub + * @param sec + */ + export function generate_key_derivation(pub : string, sec : string) { + if (pub.length !== 64 || sec.length !== 64) { + throw "Invalid input length"; + } + let P = CnUtils.ge_scalarmult(pub, sec); + return CnUtils.ge_scalarmult(P, CnUtils.d2s(8)); //mul8 to ensure group + } + + /** + * @deprecated CnNativeBride has a much faster version + * @param derivation + * @param out_index + * @param pub + */ + export function derive_public_key(derivation : string, out_index : number, pub : string) { + if (derivation.length !== 64 || pub.length !== 64) { + throw "Invalid input length!"; + } + let s = CnUtils.derivation_to_scalar(derivation, out_index); + return CnUtils.bintohex(nacl.ll.ge_add(CnUtils.hextobin(pub), CnUtils.hextobin(CnUtils.ge_scalarmult_base(s)))); + } + + export function generate_keys(seed : string) : {sec:string, pub:string}{ + if (seed.length !== 64) throw "Invalid input length!"; + let sec = CnNativeBride.sc_reduce32(seed); + let pub = CnUtils.sec_key_to_pub(sec); + return { + sec: sec, + pub: pub + }; + } + + export function random_keypair() { + return Cn.generate_keys(CnRandom.rand_32()); + } + + export function pubkeys_to_string(spend : string, view : string) { + let prefix = CnUtils.encode_varint(CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX); + let data = prefix + spend + view; + let checksum = CnUtils.cn_fast_hash(data); + return cnBase58.encode(data + checksum.slice(0, ADDRESS_CHECKSUM_SIZE * 2)); + } + + export function create_address(seed : string) : { + spend:{ + sec:string, + pub:string + }, + view:{ + sec:string, + pub:string + }, + public_addr:string + }{ + let keys = { + spend:{ + sec:'', + pub:'' + }, + view:{ + sec:'', + pub:'' + }, + public_addr:'' + }; + let first; + if (seed.length !== 64) { + first = CnUtils.cn_fast_hash(seed); + } else { + first = seed; //only input reduced seeds or this will not give you the result you want + } + + keys.spend = Cn.generate_keys(first); + let second = seed.length !== 64 ? CnUtils.cn_fast_hash(first) : CnUtils.cn_fast_hash(keys.spend.sec); + keys.view = Cn.generate_keys(second); + keys.public_addr = Cn.pubkeys_to_string(keys.spend.pub, keys.view.pub); + return keys; + } + + export function decode_address(address : string) : { + spend: string, + view: string, + intPaymentId: string|null + }{ + let dec = cnBase58.decode(address); + console.log(dec,CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX,CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX); + let expectedPrefix = CnUtils.encode_varint(CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX); + let expectedPrefixInt = CnUtils.encode_varint(CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX); + let expectedPrefixSub = CnUtils.encode_varint(CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX); + let prefix = dec.slice(0, expectedPrefix.length); + console.log(prefix,expectedPrefixInt,expectedPrefix); + if (prefix !== expectedPrefix && prefix !== expectedPrefixInt && prefix !== expectedPrefixSub) { + throw "Invalid address prefix"; + } + dec = dec.slice(expectedPrefix.length); + let spend = dec.slice(0, 64); + let view = dec.slice(64, 128); + let checksum : string|null = null; + let expectedChecksum : string|null = null; + let intPaymentId : string|null = null; + if (prefix === expectedPrefixInt){ + intPaymentId = dec.slice(128, 128 + (INTEGRATED_ID_SIZE * 2)); + checksum = dec.slice(128 + (INTEGRATED_ID_SIZE * 2), 128 + (INTEGRATED_ID_SIZE * 2) + (ADDRESS_CHECKSUM_SIZE * 2)); + expectedChecksum = CnUtils.cn_fast_hash(prefix + spend + view + intPaymentId).slice(0, ADDRESS_CHECKSUM_SIZE * 2); + } else { + checksum = dec.slice(128, 128 + (ADDRESS_CHECKSUM_SIZE * 2)); + expectedChecksum = CnUtils.cn_fast_hash(prefix + spend + view).slice(0, ADDRESS_CHECKSUM_SIZE * 2); + } + if (checksum !== expectedChecksum) { + throw "Invalid checksum"; + } + + return { + spend: spend, + view: view, + intPaymentId: intPaymentId + }; + } + + export function is_subaddress(addr : string) { + let decoded = cnBase58.decode(addr); + let subaddressPrefix = CnUtils.encode_varint(CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX); + let prefix = decoded.slice(0, subaddressPrefix.length); + + return (prefix === subaddressPrefix); + } + + export function valid_keys(view_pub : string, view_sec : string, spend_pub : string, spend_sec : string) { + let expected_view_pub = CnUtils.sec_key_to_pub(view_sec); + let expected_spend_pub = CnUtils.sec_key_to_pub(spend_sec); + return (expected_spend_pub === spend_pub) && (expected_view_pub === view_pub); + } + + export function decrypt_payment_id(payment_id8 : string, tx_public_key : string, acc_prv_view_key : string) { + if (payment_id8.length !== 16) throw "Invalid input length2!"; + + let key_derivation = Cn.generate_key_derivation(tx_public_key, acc_prv_view_key); + + let pid_key = CnUtils.cn_fast_hash(key_derivation + ENCRYPTED_PAYMENT_ID_TAIL.toString(16)).slice(0, INTEGRATED_ID_SIZE * 2); + + let decrypted_payment_id = CnUtils.hex_xor(payment_id8, pid_key); + + return decrypted_payment_id; + } + + export function get_account_integrated_address(address : string, payment_id8 : string) { + let decoded_address = decode_address(address); + + let prefix = CnUtils.encode_varint(CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX); + let data = prefix + decoded_address.spend + decoded_address.view + payment_id8; + + let checksum = CnUtils.cn_fast_hash(data); + + return cnBase58.encode(data + checksum.slice(0, ADDRESS_CHECKSUM_SIZE * 2)); + } + + export function formatMoneyFull(units : number|string) { + let unitsStr = (units).toString(); + let symbol = unitsStr[0] === '-' ? '-' : ''; + if (symbol === '-') { + unitsStr = unitsStr.slice(1); + } + let decimal; + if (unitsStr.length >= config.coinUnitPlaces) { + decimal = unitsStr.substr(unitsStr.length - config.coinUnitPlaces, config.coinUnitPlaces); + } else { + decimal = CnUtils.padLeft(unitsStr, config.coinUnitPlaces, '0'); + } + return symbol + (unitsStr.substr(0, unitsStr.length - config.coinUnitPlaces) || '0') + '.' + decimal; + } + + export function formatMoneyFullSymbol(units : number|string) { + return Cn.formatMoneyFull(units) + ' ' + config.coinSymbol; + } + + export function formatMoney(units : number|string) { + let f = CnUtils.trimRight(Cn.formatMoneyFull(units), '0'); + if (f[f.length - 1] === '.') { + return f.slice(0, f.length - 1); + } + return f; + } + + export function formatMoneySymbol(units : number|string) { + return Cn.formatMoney(units) + ' ' + config.coinSymbol; + } + +} + +export namespace CnTransactions{ + + export function commit(amount : string, mask : string){ + if (!CnUtils.valid_hex(mask) || mask.length !== 64 || !CnUtils.valid_hex(amount) || amount.length !== 64){ + throw "invalid amount or mask!"; + } + let C = CnUtils.ge_double_scalarmult_base_vartime(amount, CnVars.H, mask); + return C; + } + + export function zeroCommit(amount : string){ + if (!CnUtils.valid_hex(amount) || amount.length !== 64){ + throw "invalid amount!"; + } + let C = CnUtils.ge_double_scalarmult_base_vartime(amount, CnVars.H, CnVars.I); + return C; + } + + export function decodeRctSimple(rv : any, sk :any, i : number, mask : any, hwdev : any=null) { + // CHECK_AND_ASSERT_MES(rv.type == RCTTypeSimple || rv.type == RCTTypeSimpleBulletproof, false, "decodeRct called on non simple rctSig"); + // CHECK_AND_ASSERT_THROW_MES(i < rv.ecdhInfo.size(), "Bad index"); + // CHECK_AND_ASSERT_THROW_MES(rv.outPk.size() == rv.ecdhInfo.size(), "Mismatched sizes of rv.outPk and rv.ecdhInfo"); +// console.log(i < rv.ecdhInfo.length ? undefined : 'Bad index'); +// console.log(rv.outPk.length == rv.ecdhInfo.length ? undefined : 'Mismatched sizes of rv.outPk and rv.ecdhInfo'); + + //mask amount and mask + // console.log('decode',rv.ecdhInfo[i], sk, h2d(rv.ecdhInfo[i].amount)); + let ecdh_info = CnUtils.decode_rct_ecdh(rv.ecdhInfo[i], sk); + // console.log('ecdh_info',ecdh_info); + // mask = ecdh_info.mask; + let amount = ecdh_info.amount; + let C = rv.outPk[i].mask; + + // console.log('amount', amount); + // console.log('C', C); + // DP("C"); + // DP(C); + // key Ctmp; + // addKeys2(Ctmp, mask, amount, H); + // DP("Ctmp"); + // DP(Ctmp); + // if (equalKeys(C, Ctmp) == false) { + // CHECK_AND_ASSERT_THROW_MES(false, "warning, amount decoded incorrectly, will be unable to spend"); + // } + + return CnUtils.h2d(amount); + } + + export function decode_ringct(rv:any, + pub : any, + sec : any, + i : number, + mask : any, + amount : any, + derivation : string|null) : number|false + { + if(derivation===null) + derivation = CnNativeBride.generate_key_derivation(pub, sec);//[10;11]ms + + let scalar1 = CnUtils.derivation_to_scalar(derivation, i);//[0.2ms;1ms] + + try + { + // console.log(rv.type,'RCTTypeSimple='+RCTTypeSimple,'RCTTypeFull='+RCTTypeFull); + switch (rv.type) + { + case CnVars.RCT_TYPE.Simple: + amount = CnTransactions.decodeRctSimple(rv, + scalar1, + i, + mask);//[5;10]ms + break; + case CnVars.RCT_TYPE.Full: + amount = CnTransactions.decodeRctSimple(rv, + scalar1, + i, + mask); + break; + case CnVars.RCT_TYPE.SimpleBulletproof: + amount = CnTransactions.decodeRctSimple(rv, + scalar1, + i, + mask); + break; + case CnVars.RCT_TYPE.FullBulletproof: + amount = CnTransactions.decodeRctSimple(rv, + scalar1, + i, + mask); + break; + default: + console.log('Unsupported rc type', rv.type); + // cerr << "Unsupported rct type: " << rv.type << endl; + return false; + } + } + catch (e) + { + console.error(e); + console.log("Failed to decode input " +i); + return false; + } + + return amount; + } + + export function generate_key_image_helper(ack:{view_secret_key:any,spend_secret_key:string, public_spend_key:string}, tx_public_key:any, real_output_index:any,recv_derivation:string|null) + { + if(recv_derivation === null) + recv_derivation = CnNativeBride.generate_key_derivation(tx_public_key, ack.view_secret_key); + // recv_derivation = CnUtilNative.generate_key_derivation(tx_public_key, ack.view_secret_key); + // console.log('recv_derivation', recv_derivation); + + // CHECK_AND_ASSERT_MES(r, false, "key image helper: failed to generate_key_derivation(" << tx_public_key << ", " << ack.m_view_secret_key << ")"); + // + + // let start = Date.now(); + + let in_ephemeral_pub = CnNativeBride.derive_public_key(recv_derivation, real_output_index, ack.public_spend_key); + // let in_ephemeral_pub = CnUtilNative.derive_public_key(recv_derivation, real_output_index, ack.public_spend_key); + // console.log('in_ephemeral_pub',in_ephemeral_pub); + + + // CHECK_AND_ASSERT_MES(r, false, "key image helper: failed to derive_public_key(" << recv_derivation << ", " << real_output_index << ", " << ack.m_account_address.m_spend_public_key << ")"); + // + let in_ephemeral_sec = CnNativeBride.derive_secret_key(recv_derivation, real_output_index, ack.spend_secret_key); + // let in_ephemeral_sec = CnNativeBride.derive_secret_key(recv_derivation, real_output_index, ack.spend_secret_key); + // console.log('in_ephemeral_sec',in_ephemeral_sec); + + + + // let ki = CnNativeBride.generate_key_image_2(in_ephemeral_pub, in_ephemeral_sec); + let ki = CnNativeBride.generate_key_image_2(in_ephemeral_pub, in_ephemeral_sec); + + // let end = Date.now(); + // console.log(end-start); + + return { + ephemeral_pub:in_ephemeral_pub, + ephemeral_sec:in_ephemeral_sec, + key_image:ki + }; + } + + //TODO duplicate above + export function generate_key_image_helper_rct(keys : {view:{sec:string}, spend:{pub:string,sec:string}}, tx_pub_key : string, out_index : number, enc_mask : string | null) { + let recv_derivation = CnNativeBride.generate_key_derivation(tx_pub_key, keys.view.sec); + if (!recv_derivation) throw "Failed to generate key image"; + + let mask; + + if (enc_mask === CnVars.I) + { + // this is for ringct coinbase txs (rct type 0). they are ringct tx that have identity mask + mask = enc_mask; // enc_mask is idenity mask returned by backend. + } + else + { + // for other ringct types or for non-ringct txs to this. + let temp0 = CnUtils.derivation_to_scalar(recv_derivation, out_index); + let temp1 = Cn.hash_to_scalar(temp0); + + mask = enc_mask ? CnNativeBride.sc_sub(enc_mask, temp1) : CnVars.I; //decode mask, or d2s(1) if no mask + } + + let ephemeral_pub = CnNativeBride.derive_public_key(recv_derivation, out_index, keys.spend.pub); + if (!ephemeral_pub) throw "Failed to generate key image"; + let ephemeral_sec = CnNativeBride.derive_secret_key(recv_derivation, out_index, keys.spend.sec); + let image = CnNativeBride.generate_key_image_2(ephemeral_pub, ephemeral_sec); + return { + in_ephemeral: { + pub: ephemeral_pub, + sec: ephemeral_sec, + mask: mask + }, + image: image + }; + } + + export function estimateRctSize(inputs : number, mixin : number, outputs : number) { + let size = 0; + size += outputs * 6306; + size += ((mixin + 1) * 4 + 32 + 8) * inputs; //key offsets + key image + amount + size += 64 * (mixin + 1) * inputs + 64 * inputs; //signature + pseudoOuts/cc + size += 74; //extra + whatever, assume long payment ID + return size; + } + + export function decompose_tx_destinations(dsts : {address:string, amount:number}[], rct : boolean) : {address:string, amount:number}[] { + let out = []; + if (rct) { + for (let i = 0; i < dsts.length; i++) { + out.push({ + address: dsts[i].address, + amount: dsts[i].amount + }); + } + } else { + for (let i = 0; i < dsts.length; i++) { + let digits = CnUtils.decompose_amount_into_digits(dsts[i].amount); + for (let j = 0; j < digits.length; j++) { + if (digits[j].compare(0) > 0) { + out.push({ + address: dsts[i].address, + amount: digits[j] + }); + } + } + } + } + return out.sort(function(a,b){ + return a["amount"] - b["amount"]; + }); + } + + export function get_payment_id_nonce(payment_id : string, pid_encrypt : boolean) { + if (payment_id.length !== 64 && payment_id.length !== 16) { + throw "Invalid payment id"; + } + let res = ''; + if (pid_encrypt) { + res += TX_EXTRA_NONCE_TAGS.ENCRYPTED_PAYMENT_ID; + } else { + res += TX_EXTRA_NONCE_TAGS.PAYMENT_ID; + } + res += payment_id; + return res; + } + + export function abs_to_rel_offsets(offsets : number[]) { + if (offsets.length === 0) return offsets; + for (let i = offsets.length - 1; i >= 1; --i) { + offsets[i] = new JSBigInt(offsets[i]).subtract(offsets[i - 1]).toString(); + } + return offsets; + } + + //TODO merge + export function add_pub_key_to_extra(extra : string, pubkey : string) { + if (pubkey.length !== 64) throw "Invalid pubkey length"; + // Append pubkey tag and pubkey + extra += TX_EXTRA_TAGS.PUBKEY + pubkey; + return extra; + } + + //TODO merge + export function add_additionnal_pub_keys_to_extra(extra : string, keys : string[]){ + //do not add if there is no additional keys + console.log('Add additionnal keys to extra', keys); + if(keys.length === 0)return extra; + + extra += TX_EXTRA_TAGS.ADDITIONAL_PUBKEY; + // Encode count of keys + extra += ('0' + (keys.length).toString(16)).slice(-2); + for(let key of keys){ + if (key.length !== 64) throw "Invalid pubkey length"; + extra += key; + } + return extra; + } + + //TODO merge + export function add_nonce_to_extra(extra : string, nonce : string) { + // Append extra nonce + if ((nonce.length % 2) !== 0) { + throw "Invalid extra nonce"; + } + if ((nonce.length / 2) > TX_EXTRA_NONCE_MAX_COUNT) { + throw "Extra nonce must be at most " + TX_EXTRA_NONCE_MAX_COUNT + " bytes"; + } + // Add nonce tag + extra += TX_EXTRA_TAGS.NONCE; + // Encode length of nonce + extra += ('0' + (nonce.length / 2).toString(16)).slice(-2); + // Write nonce + extra += nonce; + return extra; + } + + export type Ephemeral = { + pub: string, + sec: string, + mask: string + }; + + export type Output = { + index:string, + key:string, + commit:string, + }; + + export type Source = { + outputs:CnTransactions.Output[], + amount:'', + real_out_tx_key:string, + real_out:number, + real_out_in_tx:number, + mask:string|null, + key_image:string, + in_ephemeral:CnTransactions.Ephemeral, + }; + + export type Destination = {address:string,amount:number}; + + export type Vin = { + type:string, + amount:string, + k_image:string, + key_offsets:any[] + }; + + export type Vout = { + amount: number, + target:{ + type: string, + data: { + key: string + } + } + }; + + export type EcdhInfo = { + mask: string, + amount: string + } + + export type RangeProveSignature = { + Ci:string[], + bsig:{ + s: string[][], + ee:string + } + }; + + export type key = string;//32characters + export type keyV = key[]; //vector of keys + export type keyM = keyV[]; //matrix of keys (indexed by column first) + + export type RangeProveBulletproofSignature = { + V : CnTransactions.keyV, + + A : CnTransactions.key, + S : CnTransactions.key, + T1 : CnTransactions.key, + T2 : CnTransactions.key, + + taux : CnTransactions.key, + mu : CnTransactions.key, + + L : CnTransactions.keyV, + R : CnTransactions.keyV, + + a : CnTransactions.key, + b : CnTransactions.key, + t : CnTransactions.key; + }; + + export type MG_Signature = { + ss: string[][], + cc: string + }; + + export type RctSignature = { + ecdhInfo:EcdhInfo[] + outPk:string[], + pseudoOuts:string[], + txnFee:string, + type:number, + message?: string, + p?: { + rangeSigs: RangeProveSignature[], + bulletproofs: RangeProveBulletproofSignature[], + MGs: MG_Signature[] + }, + } + + export type Transaction = { + unlock_time: number, + version: number, + extra: string, + prvkey: string, + vin: Vin[], + vout: Vout[], + rct_signatures:RctSignature, + signatures:any[], + }; + + export function serialize_tx(tx : CnTransactions.Transaction, headeronly : boolean = false) { + //tx: { + // version: uint64, + // unlock_time: uint64, + // extra: hex, + // vin: [{amount: uint64, k_image: hex, key_offsets: [uint64,..]},...], + // vout: [{amount: uint64, target: {key: hex}},...], + // signatures: [[s,s,...],...] + //} + console.log('serialize tx ', JSON.parse(JSON.stringify(tx))); + let buf = ""; + buf += CnUtils.encode_varint(tx.version); + buf += CnUtils.encode_varint(tx.unlock_time); + buf += CnUtils.encode_varint(tx.vin.length); + let i, j; + for (i = 0; i < tx.vin.length; i++) { + let vin = tx.vin[i]; + console.log('start vin', vin); + switch (vin.type) { + case "input_to_key": + buf += "02"; + buf += CnUtils.encode_varint(vin.amount); + buf += CnUtils.encode_varint(vin.key_offsets.length); + console.log(vin.key_offsets,vin.key_offsets.length); + for (j = 0; j < vin.key_offsets.length; j++) { + console.log(j, vin.key_offsets[j]); + buf += CnUtils.encode_varint(vin.key_offsets[j]); + } + buf += vin.k_image; + break; + default: + throw "Unhandled vin type: " + vin.type; + } + console.log('end vin', vin); + } + console.log('serialize tx ', tx); + buf += CnUtils.encode_varint(tx.vout.length); + for (i = 0; i < tx.vout.length; i++) { + let vout = tx.vout[i]; + buf += CnUtils.encode_varint(vout.amount); + switch (vout.target.type) { + case "txout_to_key": + buf += "02"; + buf += vout.target.data.key; + break; + default: + throw "Unhandled txout target type: " + vout.target.type; + } + } + console.log('serialize tx ', tx); + + if (!CnUtils.valid_hex(tx.extra)) { + throw "Tx extra has invalid hex"; + } + console.log('serialize tx ', tx); + + buf += CnUtils.encode_varint(tx.extra.length / 2); + buf += tx.extra; + if (!headeronly) { + if (tx.vin.length !== tx.signatures.length) { + throw "Signatures length != vin length"; + } + for (i = 0; i < tx.vin.length; i++) { + for (j = 0; j < tx.signatures[i].length; j++) { + buf += tx.signatures[i][j]; + } + } + } + console.log('serialize tx ', buf); + return buf; + } + + export function serialize_tx_with_hash (tx : CnTransactions.Transaction | any) { + var hashes = ""; + var buf = ""; + buf += CnTransactions.serialize_tx(tx, false); + hashes += CnUtils.cn_fast_hash(buf); + + return { + raw: buf, + hash: hashes, + prvkey: tx.prvkey + }; + }; + + export function serialize_rct_tx_with_hash(tx : CnTransactions.Transaction) { + let hashes = ""; + let buf = ""; + buf += CnTransactions.serialize_tx(tx, true); + hashes += CnUtils.cn_fast_hash(buf); + let buf2 = CnTransactions.serialize_rct_base(tx.rct_signatures); + hashes += CnUtils.cn_fast_hash(buf2); + buf += buf2; + let buf3 = serializeRangeProofs(tx.rct_signatures); + //add MGs + let p = tx.rct_signatures.p; + if(p) + for (let i = 0; i < p.MGs.length; i++) { + for (let j = 0; j < p.MGs[i].ss.length; j++) { + buf3 += p.MGs[i].ss[j][0]; + buf3 += p.MGs[i].ss[j][1]; + } + buf3 += p.MGs[i].cc; + } + + hashes += CnUtils.cn_fast_hash(buf3); + buf += buf3; + let hash = CnUtils.cn_fast_hash(hashes); + return { + raw: buf, + hash: hash, + prvkey: tx.prvkey + }; + } + + export function get_tx_prefix_hash(tx : CnTransactions.Transaction) { + let prefix = CnTransactions.serialize_tx(tx, true); + return CnUtils.cn_fast_hash(prefix); + } + + //xv: vector of secret keys, 1 per ring (nrings) + //pm: matrix of pubkeys, indexed by size first + //iv: vector of indexes, 1 per ring (nrings), can be a string + //size: ring size + //nrings: number of rings + //extensible borromean signatures + export function genBorromean(xv : string[], pm : string[][], iv : string, size : number, nrings : number){ + if (xv.length !== nrings){ + throw "wrong xv length " + xv.length; + } + if (pm.length !== size){ + throw "wrong pm size " + pm.length; + } + for (let i = 0; i < pm.length; i++){ + if (pm[i].length !== nrings){ + throw "wrong pm[" + i + "] length " + pm[i].length; + } + } + if (iv.length !== nrings){ + throw "wrong iv length " + iv.length; + } + for (let i = 0; i < iv.length; i++){ + if (parseInt(iv[i]) >= size){ + throw "bad indices value at: " + i + ": " + iv[i]; + } + } + //signature struct + let bb : { + s: string[][], + ee:string + } = { + s: [], + ee: "" + }; + //signature pubkey matrix + let L : string[][] = []; + //add needed sub vectors (1 per ring size) + for (let i = 0; i < size; i++){ + bb.s[i] = []; + L[i] = []; + } + //compute starting at the secret index to the last row + let index; + let alpha = []; + for (let i = 0; i < nrings; i++){ + index = parseInt(''+iv[i]); + alpha[i] = CnRandom.random_scalar(); + L[index][i] = CnUtils.ge_scalarmult_base(alpha[i]); + for (let j = index + 1; j < size; j++){ + bb.s[j][i] = CnRandom.random_scalar(); + let c = Cn.hash_to_scalar(L[j-1][i]); + L[j][i] = CnUtils.ge_double_scalarmult_base_vartime(c, pm[j][i], bb.s[j][i]); + } + } + //hash last row to create ee + let ltemp = ""; + for (let i = 0; i < nrings; i++){ + ltemp += L[size-1][i]; + } + bb.ee = Cn.hash_to_scalar(ltemp); + //compute the rest from 0 to secret index + for (let i = 0; i < nrings; i++){ + let cc = bb.ee; + let j = 0; + for (j = 0; j < parseInt(iv[i]); j++){ + bb.s[j][i] = CnRandom.random_scalar(); + let LL = CnUtils.ge_double_scalarmult_base_vartime(cc, pm[j][i], bb.s[j][i]); + cc = Cn.hash_to_scalar(LL); + } + bb.s[j][i] = CnNativeBride.sc_mulsub(xv[i], cc, alpha[i]); + } + return bb; + } + + //proveRange gives C, and mask such that \sumCi = C + // c.f. http://eprint.iacr.org/2015/1098 section 5.1 + // and Ci is a commitment to either 0 or s^i, i=0,...,n + // thus this proves that "amount" is in [0, s^n] (we assume s to be 4) (2 for now with v2 txes) + // mask is a such that C = aG + bH, and b = amount + //commitMaskObj = {C: commit, mask: mask} + export function proveRange(commitMaskObj : {C:string,mask:string}, amount : number, nrings : number, enc_seed : number, exponent : number){ + let size = 2; + let C = CnVars.I; //identity + let mask = CnVars.Z; //zero scalar + let indices = CnUtils.d2b(amount); //base 2 for now + let sig : RangeProveSignature = { + Ci: [], + bsig:{ + s:[], + ee:'' + } + //exp: exponent //doesn't exist for now + }; + /*payload stuff - ignore for now + seeds = new Array(3); + for (let i = 0; i < seeds.length; i++){ + seeds[i] = new Array(1); + } + genSeeds(seeds, enc_seed); + */ + let ai = []; + let PM : string[][]= []; + for (let i = 0; i < size; i++){ + PM[i] = []; + } + //start at index and fill PM left and right -- PM[0] holds Ci + for (let i = 0; i < nrings; i++){ + ai[i] = CnRandom.random_scalar(); + let j : number = parseInt(indices[i]); + PM[j][i] = CnUtils.ge_scalarmult_base(ai[i]); + while (j > 0){ + j--; + PM[j][i] = CnUtils.ge_add(PM[j+1][i], CnVars.H2[i]); //will need to use i*2 for base 4 (or different object) + } + j = parseInt(indices[i]); + while (j < size - 1){ + j++; + PM[j][i] = CnUtils.ge_sub(PM[j-1][i], CnVars.H2[i]); //will need to use i*2 for base 4 (or different object) + } + mask = CnNativeBride.sc_add(mask, ai[i]); + } + /* + * some more payload stuff here + */ + //copy commitments to sig and sum them to commitment + for (let i = 0; i < nrings; i++){ + //if (i < nrings - 1) //for later version + sig.Ci[i] = PM[0][i]; + C = CnUtils.ge_add(C, PM[0][i]); + } + /* exponent stuff - ignore for now + if (exponent){ + n = JSBigInt(10); + n = n.pow(exponent).toString(); + mask = sc_mul(mask, d2s(n)); //new sum + } + */ + sig.bsig = CnTransactions.genBorromean(ai, PM, indices, size, nrings); + commitMaskObj.C = C; + commitMaskObj.mask = mask; + return sig; + } + + /*export function proveRangeBulletproof(commitMaskObj : {C:string,mask:string}, amount : string, nrings : number, enc_seed : number, exponent : number) : CnTransactions.RangeProveBulletproofSignature{ + let mask = CnRandom.random_scalar(); + + let proof : CnTransactions.RangeProveBulletproofSignature = bulletproof_PROVE(amount, mask); + + CHECK_AND_ASSERT_THROW_MES(proof.V.length == 1, "V has not exactly one element"); + commitMaskObj.C = proof.V[0]; + commitMaskObj.mask = mask; + return proof; + } + export function verBulletproof(proof : CnTransactions.RangeProveBulletproofSignature) : boolean{ + try { return bulletproof_VERIFY(proof); } + // we can get deep throws from ge_frombytes_vartime if input isn't valid + catch (e) { return false; } + }*/ + + // Gen creates a signature which proves that for some column in the keymatrix "pk" + // the signer knows a secret key for each row in that column + // we presently only support matrices of 2 rows (pubkey, commitment) + // this is a simplied MLSAG_Gen function to reflect that + // because we don't want to force same secret column for all inputs + export function MLSAG_Gen(message : string, pk : string[][], xx : string[], kimg : string, index : number){ + let cols = pk.length; //ring size + if (index >= cols){throw "index out of range";} + let rows = pk[0].length; //number of signature rows (always 2) + if (rows !== 2){throw "wrong row count";} + for (let i = 0; i < cols; i++){ + if (pk[i].length !== rows){throw "pk is not rectangular";} + } + if (xx.length !== rows){throw "bad xx size";} + + let c_old = ""; + let alpha = []; + + let rv : MG_Signature = { + ss: [], + cc: '' + }; + for (let i = 0; i < cols; i++){ + rv.ss[i] = []; + } + let toHash = []; //holds 6 elements: message, pubkey, dsRow L, dsRow R, commitment, ndsRow L + toHash[0] = message; + + //secret index (pubkey section) + alpha[0] = CnRandom.random_scalar(); //need to save alphas for later + toHash[1] = pk[index][0]; //secret index pubkey + toHash[2] = CnUtils.ge_scalarmult_base(alpha[0]); //dsRow L + toHash[3] = CnNativeBride.generate_key_image_2(pk[index][0], alpha[0]); //dsRow R (key image check) + //secret index (commitment section) + alpha[1] = CnRandom.random_scalar(); + toHash[4] = pk[index][1]; //secret index commitment + toHash[5] = CnUtils.ge_scalarmult_base(alpha[1]); //ndsRow L + + c_old = Cn.array_hash_to_scalar(toHash); + + let i = (index + 1) % cols; + if (i === 0){ + rv.cc = c_old; + } + while (i != index){ + rv.ss[i][0] = CnRandom.random_scalar(); //dsRow ss + rv.ss[i][1] = CnRandom.random_scalar(); //ndsRow ss + + //!secret index (pubkey section) + toHash[1] = pk[i][0]; + toHash[2] = CnUtils.ge_double_scalarmult_base_vartime(c_old, pk[i][0], rv.ss[i][0]); + toHash[3] = CnUtils.ge_double_scalarmult_postcomp_vartime(rv.ss[i][0], pk[i][0], c_old, kimg); + //!secret index (commitment section) + toHash[4] = pk[i][1]; + toHash[5] = CnUtils.ge_double_scalarmult_base_vartime(c_old, pk[i][1], rv.ss[i][1]); + c_old = Cn.array_hash_to_scalar(toHash); //hash to get next column c + i = (i + 1) % cols; + if (i === 0){ + rv.cc = c_old; + } + } + for (i = 0; i < rows; i++){ + rv.ss[index][i] = CnNativeBride.sc_mulsub(c_old, xx[i], alpha[i]); + } + return rv; + } + + //prepares for MLSAG_Gen + export function proveRctMG(message : string, pubs : {dest:string, mask:string}[], inSk : {a:string, x:string}, kimg : string, mask : string, Cout : string, index : number){ + let cols = pubs.length; + if (cols < 3){throw "cols must be > 2 (mixin)";} + let xx : string[] = []; + let PK : string[][] = []; + //fill pubkey matrix (copy destination, subtract commitments) + for (let i = 0; i < cols; i++){ + PK[i] = []; + PK[i][0] = pubs[i].dest; + PK[i][1] = CnUtils.ge_sub(pubs[i].mask, Cout); + } + xx[0] = inSk.x; + xx[1] = CnNativeBride.sc_sub(inSk.a, mask); + return CnTransactions.MLSAG_Gen(message, PK, xx, kimg, index); + } + + export function serialize_rct_base(rv : RctSignature) { + let buf = ""; + buf += CnUtils.encode_varint(rv.type); + buf += CnUtils.encode_varint(rv.txnFee); + if (rv.type === 2) { + for (let i = 0; i < rv.pseudoOuts.length; i++) { + buf += rv.pseudoOuts[i]; + } + } + if (rv.ecdhInfo.length !== rv.outPk.length) { + throw "mismatched outPk/ecdhInfo!"; + } + for (let i = 0; i < rv.ecdhInfo.length; i++) { + buf += rv.ecdhInfo[i].mask; + buf += rv.ecdhInfo[i].amount; + } + for (let i = 0; i < rv.outPk.length; i++) { + buf += rv.outPk[i]; + } + return buf; + } + + export function serializeRangeProofs(rv : RctSignature) : string { + let buf = ""; + let p = rv.p; + if(p){ + if(p.rangeSigs.length) + return CnTransactions.serializeRangeProofsClassic(rv); + else if(p.bulletproofs.length) + return CnTransactions.serializeRangeProofsBulletproof(rv); + else + throw new Error(' missing range proof or bulletproof range proof'); + } + else + throw new Error('invalid p signature'); + return buf; + } + + export function serializeRangeProofsClassic(rv : RctSignature) : string { + let buf = ""; + let p = rv.p; + if(p && p.rangeSigs.length) + for (let i = 0; i < p.rangeSigs.length; i++) { + for (let j = 0; j < p.rangeSigs[i].bsig.s.length; j++) { + for (let l = 0; l < p.rangeSigs[i].bsig.s[j].length; l++) { + buf += p.rangeSigs[i].bsig.s[j][l]; + } + } + buf += p.rangeSigs[i].bsig.ee; + for (let j = 0; j < p.rangeSigs[i].Ci.length; j++) { + buf += p.rangeSigs[i].Ci[j]; + } + } + else + throw new Error('invalid p signature. missing range proof'); + return buf; + } + + export function serializeRangeProofsBulletproof(rv : RctSignature) : string { + let buf = ""; + let p = rv.p; + if(p) + for (let i = 0; i < p.bulletproofs.length; i++) { + throw new Error('bulletproof serialization not implemented'); + } + else + throw new Error('invalid p signature. missing bulletproof range proof'); + + return buf; + } + + export function get_pre_mlsag_hash(rv : RctSignature) { + let hashes = ""; + hashes += rv.message; + hashes += CnUtils.cn_fast_hash(CnTransactions.serialize_rct_base(rv)); + let buf = CnTransactions.serializeRangeProofs(rv); + hashes += CnUtils.cn_fast_hash(buf); + return CnUtils.cn_fast_hash(hashes); + } + + //message is normal prefix hash + //inSk is vector of x,a + //kimg is vector of kimg + //destinations is vector of pubkeys (we skip and proxy outAmounts instead) + //inAmounts is vector of strings + //outAmounts is vector of strings + //mixRing is matrix of pubkey, commit (dest, mask) + //amountKeys is vector of scalars + //indices is vector + //txnFee is string + export function genRct( + message : string, + inSk : {x:string,a:string}[], + kimg : string[], + /*destinations, */inAmounts : string[], + outAmounts : number[], + mixRing : {dest:string, mask:string}[][], + amountKeys : string[], + indices : number[], + txnFee : string, + bulletproof : boolean = false + ){ + console.log('MIXIN:', mixRing); + if (outAmounts.length !== amountKeys.length ){throw "different number of amounts/amount_keys";} + for (let i = 0; i < mixRing.length; i++){ + if (mixRing[i].length <= indices[i]){throw "bad mixRing/index size";} + } + if (mixRing.length !== inSk.length){throw "mismatched mixRing/inSk";} + if (inAmounts.length !== inSk.length){throw "mismatched inAmounts/inSk";} + if (indices.length !== inSk.length){throw "mismatched indices/inSk";} + + console.log('======t'); + + let rv : RctSignature = { + type: inSk.length === 1 ? CnVars.RCT_TYPE.Full : CnVars.RCT_TYPE.Simple, + message: message, + outPk: [], + p: { + rangeSigs: [], + bulletproofs: [], + MGs: [] + }, + ecdhInfo: [], + txnFee: txnFee.toString(), + pseudoOuts: [] + }; + + let sumout = CnVars.Z; + let cmObj = { + C: '', + mask: '' + }; + + console.log('====a'); + + let p = rv.p; + if(p) { + let nrings = 64; //for base 2/current + //compute range proofs, etc + for (let i = 0; i < outAmounts.length; i++) { + let teststart = new Date().getTime(); + if(!bulletproof) + p.rangeSigs[i] = CnTransactions.proveRange(cmObj, outAmounts[i], nrings, 0, 0); + // else + // p.bulletproofs[i] = CnTransactions.proveRangeBulletproof(cmObj, outAmounts[i], nrings, 0, 0); + + let testfinish = new Date().getTime() - teststart; + console.log("Time take for range proof " + i + ": " + testfinish); + rv.outPk[i] = cmObj.C; + sumout = CnNativeBride.sc_add(sumout, cmObj.mask); + rv.ecdhInfo[i] = CnUtils.encode_rct_ecdh({mask: cmObj.mask, amount: CnUtils.d2s(outAmounts[i])}, amountKeys[i]); + } + console.log('====a'); + + //simple + console.log('-----------rv type', rv.type); + if (rv.type === CnVars.RCT_TYPE.Simple) { + let ai = []; + let sumpouts = CnVars.Z; + //create pseudoOuts + let i = 0; + for (; i < inAmounts.length - 1; i++) { + ai[i] = CnRandom.random_scalar(); + sumpouts = CnNativeBride.sc_add(sumpouts, ai[i]); + rv.pseudoOuts[i] = commit(CnUtils.d2s(inAmounts[i]), ai[i]); + } + ai[i] = CnNativeBride.sc_sub(sumout, sumpouts); + rv.pseudoOuts[i] = commit(CnUtils.d2s(inAmounts[i]), ai[i]); + let full_message = CnTransactions.get_pre_mlsag_hash(rv); + for (let i = 0; i < inAmounts.length; i++) { + p.MGs.push(CnTransactions.proveRctMG(full_message, mixRing[i], inSk[i], kimg[i], ai[i], rv.pseudoOuts[i], indices[i])); + } + } else { + let sumC = CnVars.I; + //get sum of output commitments to use in MLSAG + for (let i = 0; i < rv.outPk.length; i++) { + sumC = CnUtils.ge_add(sumC, rv.outPk[i]); + } + sumC = CnUtils.ge_add(sumC, CnUtils.ge_scalarmult(CnVars.H, CnUtils.d2s(rv.txnFee))); + let full_message = CnTransactions.get_pre_mlsag_hash(rv); + p.MGs.push(CnTransactions.proveRctMG(full_message, mixRing[0], inSk[0], kimg[0], sumout, sumC, indices[0])); + } + } + + return rv; + } + + export function construct_tx( + keys : { + view: { + pub: string, + sec: string + }, + spend: { + pub: string, + sec: string + } + }, + sources : CnTransactions.Source[], + dsts : CnTransactions.Destination[], + fee_amount : any/*JSBigInt*/, + payment_id : string, + pid_encrypt : boolean, + realDestViewKey : string|undefined, + unlock_time : number = 0, + rct:boolean + ){ + //we move payment ID stuff here, because we need txkey to encrypt + let txkey = Cn.random_keypair(); + console.log(txkey); + let extra = ''; + if (payment_id) { + if (pid_encrypt && payment_id.length !== INTEGRATED_ID_SIZE * 2) { + throw "payment ID must be " + INTEGRATED_ID_SIZE + " bytes to be encrypted!"; + } + console.log("Adding payment id: " + payment_id); + if (pid_encrypt && realDestViewKey) { //get the derivation from our passed viewkey, then hash that + tail to get encryption key + let pid_key = CnUtils.cn_fast_hash(Cn.generate_key_derivation(realDestViewKey, txkey.sec) + ENCRYPTED_PAYMENT_ID_TAIL.toString(16)).slice(0, INTEGRATED_ID_SIZE * 2); + console.log("Txkeys:", txkey, "Payment ID key:", pid_key); + payment_id = CnUtils.hex_xor(payment_id, pid_key); + } + let nonce = CnTransactions.get_payment_id_nonce(payment_id, pid_encrypt); + console.log("Extra nonce: " + nonce); + extra = CnTransactions.add_nonce_to_extra(extra, nonce); + } + let tx : CnTransactions.Transaction = { + unlock_time: unlock_time, + version: rct ? CURRENT_TX_VERSION : OLD_TX_VERSION, + extra: extra, + prvkey: '', + vin: [], + vout: [], + rct_signatures:{ + ecdhInfo:[], + outPk:[], + pseudoOuts:[], + txnFee:'', + type:0, + }, + signatures:[] + }; + + if (rct) { + tx.rct_signatures = {ecdhInfo: [], outPk: [], pseudoOuts: [], txnFee: "", type: 0}; + } else { + tx.signatures = []; + } + + tx.prvkey = txkey.sec; + + let in_contexts = []; + let inputs_money = JSBigInt.ZERO; + let i, j; + + console.log('Sources: '); + //run the for loop twice to sort ins by key image + //first generate key image and other construction data to sort it all in one go + for (i = 0; i < sources.length; i++) { + console.log(i + ': ' + Cn.formatMoneyFull(sources[i].amount)); + if (sources[i].real_out >= sources[i].outputs.length) { + throw "real index >= outputs.length"; + } + // inputs_money = inputs_money.add(sources[i].amount); + + // sets res.mask among other things. mask is identity for non-rct transactions + // and for coinbase ringct (type = 0) txs. + let res = CnTransactions.generate_key_image_helper_rct(keys, sources[i].real_out_tx_key, sources[i].real_out_in_tx, sources[i].mask); //mask will be undefined for non-rct + // in_contexts.push(res.in_ephemeral); + + // now we mark if this is ringct coinbase txs. such transactions + // will have identity mask. Non-ringct txs will have sources[i].mask set to null. + // this only works if beckend will produce masks in get_unspent_outs for + // coinbaser ringct txs. + //is_rct_coinbases.push((sources[i].mask ? sources[i].mask === I : 0)); + + console.log('res.in_ephemeral.pub', res, res.in_ephemeral.pub, sources, i); + if (res.in_ephemeral.pub !== sources[i].outputs[sources[i].real_out].key) { + throw "in_ephemeral.pub != source.real_out.key"; + } + sources[i].key_image = res.image; + sources[i].in_ephemeral = res.in_ephemeral; + } + //sort ins + sources.sort(function(a,b){ + return JSBigInt.parse(a.key_image, 16).compare(JSBigInt.parse(b.key_image, 16)) * -1 ; + }); + //copy the sorted sources data to tx + for (i = 0; i < sources.length; i++) { + inputs_money = inputs_money.add(sources[i].amount); + in_contexts.push(sources[i].in_ephemeral); + let input_to_key : CnTransactions.Vin = { + type:"input_to_key", + amount:sources[i].amount, + k_image:sources[i].key_image, + key_offsets:[], + }; + for (j = 0; j < sources[i].outputs.length; ++j) { + console.log('add to key offsets',sources[i].outputs[j].index, j, sources[i].outputs); + input_to_key.key_offsets.push(sources[i].outputs[j].index); + } + console.log('key offsets before abs',input_to_key.key_offsets); + input_to_key.key_offsets = CnTransactions.abs_to_rel_offsets(input_to_key.key_offsets); + console.log('key offsets after abs',input_to_key.key_offsets); + tx.vin.push(input_to_key); + } + let outputs_money = JSBigInt.ZERO; + let out_index = 0; + let amountKeys = []; //rct only + + let num_stdaddresses = 0; + let num_subaddresses = 0; + let single_dest_subaddress : string = ''; + + let unique_dst_addresses : {[key : string] : number} = {}; + + for (i = 0; i < dsts.length; ++i) { + if (new JSBigInt(dsts[i].amount).compare(0) < 0) { + throw "dst.amount < 0"; //amount can be zero if no change + } + let destKeys = Cn.decode_address(dsts[i].address); + + if(destKeys.view === keys.view.pub)//change address + continue; + + if(typeof unique_dst_addresses[dsts[i].address] === 'undefined'){ + unique_dst_addresses[dsts[i].address] = 1; + + if(Cn.is_subaddress(dsts[i].address)){ + ++num_subaddresses; + single_dest_subaddress = dsts[i].address; + }else{ + ++num_stdaddresses; + } + } + } + + console.log('Destinations resume:', unique_dst_addresses, num_stdaddresses, num_subaddresses ); + + if (num_stdaddresses == 0 && num_subaddresses == 1) { + let uniqueSubaddressDecoded = Cn.decode_address(single_dest_subaddress); + txkey.pub = CnUtils.ge_scalarmult(uniqueSubaddressDecoded.spend, txkey.sec); + } + + let additional_tx_keys : string[] = []; + let additional_tx_public_keys : string[] = []; + let need_additional_txkeys : boolean = num_subaddresses > 0 && (num_stdaddresses > 0 || num_subaddresses > 1); + + + for (i = 0; i < dsts.length; ++i) { + let destKeys = Cn.decode_address(dsts[i].address); + + let additional_txkey : {sec:string, pub:string} = {sec:'', pub:''}; + if(need_additional_txkeys){ + additional_txkey = Cn.random_keypair(); + if(Cn.is_subaddress(dsts[i].address)) { + // R = rD for subaddresses + additional_txkey.pub = CnUtils.ge_scalarmult(destKeys.spend, additional_txkey.sec); + }else + additional_txkey.pub = CnUtils.ge_scalarmult_base(additional_txkey.sec); + } + let out_derivation; + if(destKeys.view === keys.view.pub) { + out_derivation = Cn.generate_key_derivation(txkey.pub, keys.view.sec); + } else { + if(Cn.is_subaddress(dsts[i].address) && need_additional_txkeys) + out_derivation = Cn.generate_key_derivation(destKeys.view, additional_txkey.sec); + else + out_derivation = Cn.generate_key_derivation(destKeys.view, txkey.sec); + } + + if (need_additional_txkeys){ + additional_tx_public_keys.push(additional_txkey.pub); + additional_tx_keys.push(additional_txkey.sec); + } + + if (rct) { + amountKeys.push(CnUtils.derivation_to_scalar(out_derivation, out_index)); + } + let out_ephemeral_pub = Cn.derive_public_key(out_derivation, out_index, destKeys.spend); + let out : CnTransactions.Vout = { + amount: dsts[i].amount, + target:{ + type: "txout_to_key", + data: { + key: out_ephemeral_pub + } + } + }; + // txout_to_key + tx.vout.push(out); + ++out_index; + outputs_money = outputs_money.add(dsts[i].amount); + } + + // add pub key to extra after we know whether to use R = rG or R = rD + tx.extra = CnTransactions.add_pub_key_to_extra(tx.extra, txkey.pub); + tx.extra = CnTransactions.add_additionnal_pub_keys_to_extra(tx.extra, additional_tx_public_keys); + + if (outputs_money.add(fee_amount).compare(inputs_money) > 0) { + throw "outputs money (" + Cn.formatMoneyFull(outputs_money) + ") + fee (" + Cn.formatMoneyFull(fee_amount) + ") > inputs money (" + Cn.formatMoneyFull(inputs_money) + ")"; + } + if (!rct) { + for (i = 0; i < sources.length; ++i) { + let src_keys : string[] = []; + for (j = 0; j < sources[i].outputs.length; ++j) { + src_keys.push(sources[i].outputs[j].key); + } + let sigs = CnNativeBride.generate_ring_signature(CnTransactions.get_tx_prefix_hash(tx), tx.vin[i].k_image, src_keys, + in_contexts[i].sec, sources[i].real_out); + tx.signatures.push(sigs); + } + } else { //rct + let txnFee = fee_amount; + let keyimages = []; + let inSk = []; + let inAmounts = []; + let mixRing : {dest:string, mask:string}[][] = []; + let indices = []; + for (i = 0; i < tx.vin.length; i++) { + keyimages.push(tx.vin[i].k_image); + inSk.push({ + x: in_contexts[i].sec, + a: in_contexts[i].mask, + }); + inAmounts.push(tx.vin[i].amount); + if (in_contexts[i].mask !== CnVars.I) { + //if input is rct (has a valid mask), 0 out amount + tx.vin[i].amount = "0"; + } + mixRing[i] = []; + for (j = 0; j < sources[i].outputs.length; j++) { + mixRing[i].push({ + dest: sources[i].outputs[j].key, + mask: sources[i].outputs[j].commit, + }); + } + indices.push(sources[i].real_out); + } + let outAmounts: number[] = []; + for (i = 0; i < tx.vout.length; i++) { + outAmounts.push(tx.vout[i].amount); + tx.vout[i].amount = 0; //zero out all rct outputs + } + console.log('rc signature----'); + let tx_prefix_hash = CnTransactions.get_tx_prefix_hash(tx); + console.log('rc signature----'); + tx.rct_signatures = CnTransactions.genRct(tx_prefix_hash, inSk, keyimages, /*destinations, */inAmounts, outAmounts, mixRing, amountKeys, indices, txnFee); + + } + console.log(tx); + return tx; + } + + export function create_transaction(pub_keys:{spend:string,view:string}, + sec_keys:{spend:string,view:string}, + dsts : CnTransactions.Destination[], + outputs : { + amount:number, + public_key:string, + index:number, + global_index:number, + tx_pub_key:string, + }[], + mix_outs:{ + outputs:{ + public_key:string, + global_index:number + }[], + amount:0 + }[] = [], + fake_outputs_count:number, + fee_amount : any/*JSBigInt*/, + payment_id : string, + pid_encrypt : boolean, + realDestViewKey : string|undefined, + unlock_time : number = 0, + rct:boolean + ) : CnTransactions.Transaction{ + let i, j; + if (dsts.length === 0) { + throw 'Destinations empty'; + } + if (mix_outs.length !== outputs.length && fake_outputs_count !== 0) { + throw 'Wrong number of mix outs provided (' + outputs.length + ' outputs, ' + mix_outs.length + ' mix outs)'; + } + for (i = 0; i < mix_outs.length; i++) { + if ((mix_outs[i].outputs || []).length < fake_outputs_count) { + throw 'Not enough outputs to mix with'; + } + } + let keys = { + view: { + pub: pub_keys.view, + sec: sec_keys.view + }, + spend: { + pub: pub_keys.spend, + sec: sec_keys.spend + } + }; + if (!Cn.valid_keys(keys.view.pub, keys.view.sec, keys.spend.pub, keys.spend.sec)) { + throw "Invalid secret keys!"; + } + let needed_money = JSBigInt.ZERO; + for (i = 0; i < dsts.length; ++i) { + needed_money = needed_money.add(dsts[i].amount); + if (needed_money.compare(UINT64_MAX) !== -1) { + throw "Output overflow!"; + } + } + let found_money = JSBigInt.ZERO; + let sources : CnTransactions.Source[] = []; + console.log('Selected transfers: ', outputs); + for (i = 0; i < outputs.length; ++i) { + found_money = found_money.add(outputs[i].amount); + if (found_money.compare(UINT64_MAX) !== -1) { + throw "Input overflow!"; + } + let src : CnTransactions.Source = { + outputs: [], + amount: '', + real_out_tx_key:'', + real_out:0, + real_out_in_tx:0, + mask:null, + key_image:'', + in_ephemeral:{ + pub: '', + sec: '', + mask: '' + } + }; + src.amount = new JSBigInt(outputs[i].amount).toString(); + if (mix_outs.length !== 0) { + // Sort fake outputs by global index + console.log('mix outs before sort',mix_outs[i].outputs); + mix_outs[i].outputs.sort(function(a, b) { + return new JSBigInt(a.global_index).compare(b.global_index); + }); + j = 0; + + console.log('mix outs sorted',mix_outs[i].outputs); + + while ((src.outputs.length < fake_outputs_count) && (j < mix_outs[i].outputs.length)) { + let out = mix_outs[i].outputs[j]; + console.log('chekcing mixin',out, outputs[i]); + if (out.global_index === outputs[i].global_index) { + console.log('got mixin the same as output, skipping'); + j++; + continue; + } + let oe : Output = { + index:out.global_index.toString(), + key:out.public_key, + commit:'' + }; + /* + if (rct){ + if (out.rct){ + oe.commit = out.rct.slice(0,64); //add commitment from rct mix outs + } else { + if (outputs[i]['rct']) {throw "mix rct outs missing commit";} + oe.commit = zeroCommit(CnUtils.d2s(src.amount)); //create identity-masked commitment for non-rct mix input + } + } + + */ + src.outputs.push(oe); + j++; + } + } + let real_oe = { + index:new JSBigInt(outputs[i].global_index || 0).toString(), + key:outputs[i].public_key, + commit:'', + }; + console.log('OUT FOR REAL:',outputs[i].global_index); + /* + if (rct){ + if (outputs[i].rct) { + real_oe.commit = outputs[i].rct.slice(0,64); //add commitment for real input + } else { + console.log('ZERO COMMIT'); + real_oe.commit = zeroCommit(CnUtils.d2s(src.amount)); //create identity-masked commitment for non-rct input + } + } + + */ + let real_index = src.outputs.length; + for (j = 0; j < src.outputs.length; j++) { + if (new JSBigInt(real_oe.index).compare(src.outputs[j].index) < 0) { + real_index = j; + break; + } + } + // Add real_oe to outputs + console.log('inserting real ouput at index', real_index, real_oe, outputs[i], i); + src.outputs.splice(real_index, 0, real_oe); + src.real_out_tx_key = outputs[i].tx_pub_key; + // Real output entry index + src.real_out = real_index; + src.real_out_in_tx = outputs[i].index; + console.log('check mask', outputs, rct, i); + /* + if (rct){ + if (outputs[i].rct) { + src.mask = outputs[i].rct.slice(64,128); //encrypted or idenity mask for coinbase txs. + } else { + console.log('NULL MASK'); + src.mask = null; //will be set by generate_key_image_helper_rct + } + } + + */ + sources.push(src); + } + console.log('sources: ', sources); + let change = { + amount: JSBigInt.ZERO + }; + let cmp = needed_money.compare(found_money); + if (cmp < 0) { + change.amount = found_money.subtract(needed_money); + if (change.amount.compare(fee_amount) !== 0) { + throw "early fee calculation != later"; + } + } else if (cmp > 0) { + throw "Need more money than found! (have: " + Cn.formatMoney(found_money) + " need: " + Cn.formatMoney(needed_money) + ")"; + } + return CnTransactions.construct_tx(keys, sources, dsts, fee_amount, payment_id, pid_encrypt, realDestViewKey, unlock_time, rct); + } +} diff --git a/src/model/CnUtilNative.ts b/src/model/CnUtilNative.ts deleted file mode 100644 index df95fbec..00000000 --- a/src/model/CnUtilNative.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2018, Gnock - * Copyright (c) 2018, The Masari Project - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -let KEY_SIZE = 32; -let STRUCT_SIZES = { - GE_P3: 160, - GE_P2: 120, - GE_P1P1: 160, - GE_CACHED: 160, - EC_SCALAR: 32, - EC_POINT: 32, - KEY_IMAGE: 32, - GE_DSMP: 160 * 8, // ge_cached * 8 - SIGNATURE: 64 // ec_scalar * 2 -}; - -let generate_key_derivation_bind : any = null; -let derive_public_key_bind : any = null; - -export class CnUtilNative{ - static generate_key_derivation : (pub : any, sec : any) => string; - static derive_public_key : (derivation : string,output_idx_in_tx : number,pubSpend : string) => string; - - static hextobin(hex : any) { - if (hex.length % 2 !== 0) throw "Hex string has invalid length!"; - let res = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length / 2; ++i) { - res[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); - } - return res; - } - - static bintohex(bin : any) { - let out = []; - for (let i = 0; i < bin.length; ++i) { - out.push(("0" + bin[i].toString(16)).slice(-2)); - } - return out.join(""); - } - - static generate_key_derivation_native(pub : any, sec : any){ - - let pub_b = CnUtilNative.hextobin(pub); - let sec_b = CnUtilNative.hextobin(sec); - let Module_native = (self).Module_native; - - let pub_m = Module_native._malloc(KEY_SIZE); - Module_native.HEAPU8.set(pub_b, pub_m); - - let sec_m = Module_native._malloc(KEY_SIZE); - Module_native.HEAPU8.set(sec_b, sec_m); - - let derivation_m = Module_native._malloc(KEY_SIZE); - let r = generate_key_derivation_bind(pub_m,sec_m,derivation_m); - - Module_native._free(pub_m); - Module_native._free(sec_m); - - let res = Module_native.HEAPU8.subarray(derivation_m, derivation_m + KEY_SIZE); - Module_native._free(derivation_m); - - return CnUtilNative.bintohex(res); - } - - static derive_public_key_native(derivation : string, - output_idx_in_tx : number, - pubSpend : string){ - - let derivation_b = CnUtilNative.hextobin(derivation); - let pub_spend_b = CnUtilNative.hextobin(pubSpend); - - - let Module_native = (self).Module_native; - - let derivation_m = Module_native._malloc(KEY_SIZE); - Module_native.HEAPU8.set(derivation_b, derivation_m); - - let pub_spend_m = Module_native._malloc(KEY_SIZE); - Module_native.HEAPU8.set(pub_spend_b, pub_spend_m); - - let derived_key_m = Module_native._malloc(KEY_SIZE); - let r = derive_public_key_bind(derivation_m, output_idx_in_tx, pub_spend_m, derived_key_m); - - Module_native._free(derivation_m); - Module_native._free(pub_spend_m); - - let res = Module_native.HEAPU8.subarray(derived_key_m, derived_key_m + KEY_SIZE); - Module_native._free(derived_key_m); - - return CnUtilNative.bintohex(res); - } -} - -if(typeof (self).Module_native !== 'undefined') { - generate_key_derivation_bind = (self).Module_native.cwrap('generate_key_derivation', null, ['number', 'number', 'number']); - derive_public_key_bind = (self).Module_native.cwrap('derive_public_key', null, ['number', 'number', 'number', 'number']); - CnUtilNative.generate_key_derivation = CnUtilNative.generate_key_derivation_native; - CnUtilNative.derive_public_key = CnUtilNative.derive_public_key_native; -}else{ - CnUtilNative.generate_key_derivation = function(pub : any, sec : any){return cnUtil.generate_key_derivation(pub, sec)}; - CnUtilNative.derive_public_key = function(derivation : string,output_idx_in_tx : number,pubSpend : string){return cnUtil.derive_public_key(derivation, output_idx_in_tx, pubSpend)} -} \ No newline at end of file diff --git a/src/model/CryptoUtils.ts b/src/model/CryptoUtils.ts deleted file mode 100644 index 9cfef63d..00000000 --- a/src/model/CryptoUtils.ts +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright (c) 2018, Gnock - * Copyright (c) 2018, The Masari Project - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import {CnUtilNative} from "./CnUtilNative"; - -export class CryptoUtils{ - - - static bintohex(bin : any) { - let out = []; - for (let i = 0; i < bin.length; ++i) { - out.push(("0" + bin[i].charCodeAt(0).toString(16)).slice(-2)); - } - return out.join(""); - } - -//addKeys2 -//aGbB = aG + bB where a, b are scalars, G is the basepoint and B is a point - static addKeys2(aGbB : any, a : any, b : any, B : any) { - // ge_p2 rv; - // ge_p3 B2; - // CHECK_AND_ASSERT_THROW_MES_L1(ge_frombytes_vartime(&B2, B.bytes) == 0, "ge_frombytes_vartime failed at "+boost::lexical_cast(__LINE__)); - // ge_double_scalarmult_base_vartime(&rv, b.bytes, &B2, a.bytes); - // ge_tobytes(aGbB.bytes, &rv); - } - - static hextobin(hex:string) { - if (hex.length % 2 !== 0) throw "Hex string has invalid length!"; - let res = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length / 2; ++i) { - res[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); - //console.log(hex.slice(i * 2, i * 2 + 2), res[i]); - } - return res; - } - - static swapEndian(hex:any){ - if (hex.length % 2 !== 0){return "length must be a multiple of 2!";} - let data = ""; - for (let i=1; i <= hex.length / 2; i++){ - data += hex.substr(0 - 2 * i, 2); - } - return data; - } - -//switch byte order charwise - static swapEndianC(string:any){ - let data = ""; - for (let i=1; i <= string.length; i++){ - data += string.substr(0 - i, 1); - } - return data; - } - -//for most uses you'll also want to swapEndian after conversion -//mainly to convert integer "scalars" to usable hexadecimal strings - static d2h(integer:any){ - if (typeof integer !== "string" && integer.toString().length > 15){throw "integer should be entered as a string for precision";} - let padding = ""; - for (let i = 0; i < 63; i++){ - padding += "0"; - } - return (padding + JSBigInt(integer).toString(16).toLowerCase()).slice(-64); - } - -// hexadecimal to integer - static h2d(test : any) { - /*let vali = 0; - for (let j = 7; j >= 0; j--) { - vali = (vali * 256 + test[j].charCodeAt(0)); - } - return vali;*/ - // return JSBigInt.parse(test,16); - // let bytes = Crypto.hextobin(test); - //console.log('bytes',bytes, test,swapEndianC(test)); - //console.log(JSBigInt.parse(swapEndianC(test),16).valueOf()); - //console.log(JSBigInt.parse(test.substr(0,12),16).valueOf()); - let vali = 0; - for (let j = 7; j >= 0; j--) { - //console.log(vali,vali*256,bytes[j]); - vali = (vali * 256 + parseInt(test.slice(j*2, j*2+2), 16)); - } - return vali; - } - - static decodeRctSimple(rv : any, sk :any, i : number, mask : any, hwdev : any=null) { - // CHECK_AND_ASSERT_MES(rv.type == RCTTypeSimple || rv.type == RCTTypeSimpleBulletproof, false, "decodeRct called on non simple rctSig"); - // CHECK_AND_ASSERT_THROW_MES(i < rv.ecdhInfo.size(), "Bad index"); - // CHECK_AND_ASSERT_THROW_MES(rv.outPk.size() == rv.ecdhInfo.size(), "Mismatched sizes of rv.outPk and rv.ecdhInfo"); -//console.log(i < rv.ecdhInfo.length ? undefined : 'Bad index'); -//console.log(rv.outPk.length == rv.ecdhInfo.length ? undefined : 'Mismatched sizes of rv.outPk and rv.ecdhInfo'); - - //mask amount and mask - //console.log('decode',rv.ecdhInfo[i], sk, h2d(rv.ecdhInfo[i].amount)); - let ecdh_info = cnUtil.decode_rct_ecdh(rv.ecdhInfo[i], sk); - //console.log('ecdh_info',ecdh_info); - // mask = ecdh_info.mask; - let amount = ecdh_info.amount; - let C = rv.outPk[i].mask; - - //console.log('amount', amount); - //console.log('C', C); - // DP("C"); - // DP(C); - // key Ctmp; - // addKeys2(Ctmp, mask, amount, H); - // DP("Ctmp"); - // DP(Ctmp); - // if (equalKeys(C, Ctmp) == false) { - // CHECK_AND_ASSERT_THROW_MES(false, "warning, amount decoded incorrectly, will be unable to spend"); - // } - - return CryptoUtils.h2d(amount); - } - - - static RCTTypeFull = 1; - static RCTTypeSimple = 2; - - static decode_ringct(rv:any, - pub : any, - sec : any, - i : number, - mask : any, - amount : any, - derivation : string|null) - { - if(derivation===null) - derivation = cnUtil.generate_key_derivation(pub, sec);//[10;11]ms - - let scalar1 = cnUtil.derivation_to_scalar(derivation, i);//[0.2ms;1ms] - - try - { - //console.log(rv.type,'RCTTypeSimple='+RCTTypeSimple,'RCTTypeFull='+RCTTypeFull); - switch (rv.type) - { - case CryptoUtils.RCTTypeSimple: - //console.log('RCTTypeSimple'); - let realAmount = amount; - // for(let i = 0; i < 1000; ++i) - amount = CryptoUtils.decodeRctSimple(rv, - scalar1, - i, - mask);//[5;10]ms - - - break; - case CryptoUtils.RCTTypeFull: - //console.log('RCTTypeSimple'); - amount = CryptoUtils.decodeRctSimple(rv, - scalar1, - i, - mask); - break; - // case RCTTypeFull: - // //console.log('RCTTypeFull'); - // amount = decodeRct(rv, - // rct::sk2rct(scalar1), - // i, - // mask); - // break; - default: - //console.log('Unsupported rc type', rv.type); - // cerr << "Unsupported rct type: " << rv.type << endl; - return false; - } - } - catch (e) - { - console.error(e); - //console.log("Failed to decode input " +i); - return false; - } - - return amount; - } - - static relative_output_offsets_to_absolute(offsets : Array){ - let res : Array = offsets.slice(); - for(let i = 1; i < res.length; i++) - res[i] += res[i-1]; - return res; - } - - static get_output_keys(amount:number,absolute_offsets:Array){ - - } - -//CNutil.generate_key_image alternative ?????? - static generate_key_image_helper(ack:{view_secret_key:any,spend_secret_key:string, public_spend_key:string}, tx_public_key:any, real_output_index:any,recv_derivation:string|null) - { - if(recv_derivation === null) - // recv_derivation = cnUtil.generate_key_derivation(tx_public_key, ack.view_secret_key); - recv_derivation = CnUtilNative.generate_key_derivation(tx_public_key, ack.view_secret_key); - //console.log('recv_derivation', recv_derivation); - - // CHECK_AND_ASSERT_MES(r, false, "key image helper: failed to generate_key_derivation(" << tx_public_key << ", " << ack.m_view_secret_key << ")"); - // - - // let start = Date.now(); - - // let in_ephemeral_pub = cnUtil.derive_public_key(recv_derivation, real_output_index, ack.public_spend_key); - let in_ephemeral_pub = CnUtilNative.derive_public_key(recv_derivation, real_output_index, ack.public_spend_key); - //console.log('in_ephemeral_pub',in_ephemeral_pub); - - - // CHECK_AND_ASSERT_MES(r, false, "key image helper: failed to derive_public_key(" << recv_derivation << ", " << real_output_index << ", " << ack.m_account_address.m_spend_public_key << ")"); - // - // let in_ephemeral_sec = cnUtil.derive_secret_key(recv_derivation, real_output_index, ack.spend_secret_key); - let in_ephemeral_sec = cnUtil.derive_secret_key(recv_derivation, real_output_index, ack.spend_secret_key); - //console.log('in_ephemeral_sec',in_ephemeral_sec); - - - - let ki = cnUtil.generate_key_image_2(in_ephemeral_pub, in_ephemeral_sec); - - // let end = Date.now(); - //console.log(end-start); - - return { - ephemeral_pub:in_ephemeral_pub, - ephemeral_sec:in_ephemeral_sec, - key_image:ki - }; - } - - -} \ No newline at end of file diff --git a/src/model/Functions.ts b/src/model/Functions.ts new file mode 100644 index 00000000..e43fa8c8 --- /dev/null +++ b/src/model/Functions.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2018-2020, ExploShot + * Copyright (c) 2018-2020, The Qwertycoin Project + * + * All rights reserved. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * ==> Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * ==> Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * ==> Neither the name of Qwertycoin nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export class Functions { + public static randInt() { + return Math.floor(Math.random() * Math.floor(config.apiUrl.length)); + } +} diff --git a/src/model/KeysRepository.ts b/src/model/KeysRepository.ts index eb5a84f5..7fe71e18 100644 --- a/src/model/KeysRepository.ts +++ b/src/model/KeysRepository.ts @@ -13,6 +13,8 @@ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import {CnUtils} from "./Cn"; + export type UserKeys = { pub:{ view:string, @@ -27,8 +29,8 @@ export type UserKeys = { export class KeysRepository{ static fromPriv(spend : string, view : string) : UserKeys{ - let pubView = cnUtil.sec_key_to_pub(view); - let pubSpend = cnUtil.sec_key_to_pub(spend); + let pubView = CnUtils.sec_key_to_pub(view); + let pubSpend = CnUtils.sec_key_to_pub(spend); return { pub:{ view:pubView, @@ -41,4 +43,4 @@ export class KeysRepository{ } } -} \ No newline at end of file +} diff --git a/src/model/Transaction.ts b/src/model/Transaction.ts index d6e84ca7..d528cf70 100644 --- a/src/model/Transaction.ts +++ b/src/model/Transaction.ts @@ -1,17 +1,34 @@ - -/* - * Copyright (c) 2018, Gnock - * Copyright (c) 2018, The Masari Project - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +/** + * Copyright (c) 2018, Gnock + * Copyright (c) 2018-2020, ExploShot + * Copyright (c) 2018-2020, The Qwertycoin Project + * Copyright (c) 2018-2020, The Masari Project + * Copyright (c) 2014-2018, MyMonero.com * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * All rights reserved. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: * - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * ==> Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * ==> Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * ==> Neither the name of Qwertycoin nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ export class TransactionOut { @@ -19,8 +36,12 @@ export class TransactionOut { keyImage: string = ''; outputIdx: number = 0; globalIndex: number = 0; - ephemeralPub:string=''; + + ephemeralPub: string = ''; pubKey: string = ''; + rtcOutPk: string = ''; + rtcMask: string = ''; + rtcAmount: string = ''; static fromRaw(raw: any) { let nout = new TransactionOut(); @@ -28,8 +49,13 @@ export class TransactionOut { nout.outputIdx = raw.outputIdx; nout.globalIndex = raw.globalIndex; nout.amount = raw.amount; + + if (typeof raw.ephemeralPub !== 'undefined') nout.ephemeralPub = raw.ephemeralPub; if (typeof raw.pubKey !== 'undefined') nout.pubKey = raw.pubKey; - if(typeof raw.ephemeralPub !== 'undefined') nout.ephemeralPub = raw.ephemeralPub; + if (typeof raw.rtcOutPk !== 'undefined') nout.rtcOutPk = raw.rtcOutPk; + if (typeof raw.rtcMask !== 'undefined') nout.rtcMask = raw.rtcMask; + if (typeof raw.rtcAmount !== 'undefined') nout.rtcAmount = raw.rtcAmount; + return nout; } @@ -40,6 +66,10 @@ export class TransactionOut { globalIndex: this.globalIndex, amount: this.amount, }; + if (this.rtcOutPk !== '') data.rtcOutPk = this.rtcOutPk; + if (this.rtcMask !== '') data.rtcMask = this.rtcMask; + if (this.rtcAmount !== '') data.rtcAmount = this.rtcAmount; + if (this.ephemeralPub !== '') data.ephemeralPub = this.ephemeralPub; if (this.pubKey !== '') data.pubKey = this.pubKey; return data; @@ -140,8 +170,14 @@ export class Transaction { return amount; } + isCoinbase() { + return this.outs.length == 1 && this.outs[0].rtcAmount === ''; + } + isConfirmed(blockchainHeight: number) { - if (this.blockHeight + config.txMinConfirms < blockchainHeight) { + if (this.isCoinbase() && this.blockHeight + config.txCoinbaseMinConfirms < blockchainHeight) { + return true; + } else if (!this.isCoinbase() && this.blockHeight + config.txMinConfirms < blockchainHeight) { return true; } return false; @@ -155,4 +191,4 @@ export class Transaction { } return true; } -} \ No newline at end of file +} diff --git a/src/model/TransactionsExplorer.ts b/src/model/TransactionsExplorer.ts index e2ce01aa..b308549d 100644 --- a/src/model/TransactionsExplorer.ts +++ b/src/model/TransactionsExplorer.ts @@ -1,23 +1,42 @@ -/* - * Copyright (c) 2018, Gnock - * Copyright (c) 2018, The Masari Project +/** + * Copyright (c) 2018, Gnock + * Copyright (c) 2018-2020, ExploShot + * Copyright (c) 2018-2020, The Qwertycoin Project + * Copyright (c) 2018-2020, The Masari Project + * Copyright (c) 2014-2018, MyMonero.com * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * All rights reserved. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: * - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * ==> Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * ==> Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * ==> Neither the name of Qwertycoin nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import {Transaction, TransactionIn, TransactionOut} from "./Transaction"; -import {CryptoUtils} from "./CryptoUtils"; import {Wallet} from "./Wallet"; import {MathUtil} from "./MathUtil"; -import {CnUtilNative} from "./CnUtilNative"; +import {Cn, CnNativeBride, CnRandom, CnTransactions, CnUtils} from "./Cn"; +import {RawDaemon_Transaction} from "./blockchain/BlockchainExplorer"; +import hextobin = CnUtils.hextobin; export const TX_EXTRA_PADDING_MAX_COUNT = 255; export const TX_EXTRA_NONCE_MAX_COUNT = 255; @@ -38,8 +57,8 @@ type RawOutForTx = { amount: number, public_key: string, index: number, - global_index: number, - tx_pub_key: string + global_index: number, + tx_pub_key: string }; type TxExtra = { @@ -49,78 +68,194 @@ type TxExtra = { export class TransactionsExplorer { - static parse(rawTransaction: RawDaemonTransaction, wallet: Wallet): Transaction | null { + static parseExtra(oExtra: number[]): TxExtra[] { + let extra = oExtra.slice(); + let extras: TxExtra[] = []; + let hasFoundPubKey = false; + + while (extra.length > 0) { + let extraSize = 0; + let startOffset = 0; + + if (extra[0] === TX_EXTRA_NONCE || + extra[0] === TX_EXTRA_MERGE_MINING_TAG || + extra[0] === TX_EXTRA_MYSTERIOUS_MINERGATE_TAG) { + + extraSize = extra[1]; + startOffset = 2; + } else if (extra[0] === TX_EXTRA_TAG_PUBKEY) { + extraSize = 32; + startOffset = 1; + hasFoundPubKey = true; + } else if (extra[0] === TX_EXTRA_TAG_ADDITIONAL_PUBKEYS) { + extraSize = extra[1] * 32; + startOffset = 2; + } else if (extra[0] === TX_EXTRA_TAG_PADDING) { + + // this tag has to be the last in extra + // we do nothing with it + + /* + + let iExtra = 2; + let fExtras = { + type: extra[0], + data: [extra[1]] + }; + + while (extra.length > iExtra && extra[iExtra++] == 0) { + fExtras.data.push(0); + } + + continue; + */ + } + + if (extraSize === 0) { + if (!hasFoundPubKey) { + throw 'Invalid extra size' + extra[0]; + } + + break; + } + + let data = extra.slice(startOffset, startOffset + extraSize); + extras.push({ + type: extra[0], + data: data + }); + extra = extra.slice(startOffset + extraSize); + } + + return extras; + } + + static isMinerTx(rawTransaction: RawDaemon_Transaction) { + if (!Array.isArray(rawTransaction.vout) || rawTransaction.vin.length > 0) { + return false; + } + + if (!Array.isArray(rawTransaction.vout) || rawTransaction.vout.length === 0) { + console.error('Weird tx !', rawTransaction); + + return false; + } + + try { + return rawTransaction.vout[0].amount !== 0; + } catch (err) { + return false; + } + } + + static parse(rawTransaction: RawDaemon_Transaction, wallet: Wallet): Transaction | null { let transaction: Transaction | null = null; let tx_pub_key = ''; let paymentId: string | null = null; - tx_pub_key = rawTransaction.extra.publicKey; - paymentId = rawTransaction.paymentId; + let txExtras = []; + try { + let hexExtra: number[] = []; + let uint8Array = hextobin(rawTransaction.extra); + + for (let i = 0; i < uint8Array.byteLength; i++) { + hexExtra[i] = uint8Array[i]; + } + + txExtras = this.parseExtra(hexExtra); + } catch (e) { + console.error(e); + console.log('Error when scanning transaction on block ' + rawTransaction.height, rawTransaction); + + return null; + } + + for (let extra of txExtras) { + if (extra.type === TX_EXTRA_TAG_PUBKEY) { + for (let i = 0; i < 32; ++i) { + tx_pub_key += String.fromCharCode(extra.data[i]); + } + break; + } + } + + if (tx_pub_key === '') { + console.log(`tx_pub_key === null`); + return null; + } + + tx_pub_key = CnUtils.bintohex(tx_pub_key); + let encryptedPaymentId: string | null = null; + + for (let extra of txExtras) { + if (extra.type === TX_EXTRA_NONCE) { + if (extra.data[0] === TX_EXTRA_NONCE_PAYMENT_ID) { + paymentId = ''; + for (let i = 1; i < extra.data.length; ++i) { + paymentId += String.fromCharCode(extra.data[i]); + } + paymentId = CnUtils.bintohex(paymentId); + break; + } else if (extra.data[0] === TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID) { + encryptedPaymentId = ''; + for (let i = 1; i < extra.data.length; ++i) { + encryptedPaymentId += String.fromCharCode(extra.data[i]); + } + encryptedPaymentId = CnUtils.bintohex(encryptedPaymentId); + break; + } + } + } let derivation = null; try { - derivation = CnUtilNative.generate_key_derivation(tx_pub_key, wallet.keys.priv.view); + derivation = CnNativeBride.generate_key_derivation(tx_pub_key, wallet.keys.priv.view); } catch (e) { - //console.log('UNABLE TO CREATE DERIVATION', e); + console.log('UNABLE TO CREATE DERIVATION', e); return null; } let outs: TransactionOut[] = []; let ins: TransactionIn[] = []; - for (let iOut = 0; iOut < rawTransaction.outputs.length; ++iOut) { - let out = rawTransaction.outputs[iOut]; - let txout_k = out.output.target.data; - let amount = out.output.amount; + for (let iOut = 0; iOut < rawTransaction.vout.length; ++iOut) { + let out = rawTransaction.vout[iOut]; + let txout_k = out.target.data; + let amount: number = 0; + try { + amount = out.amount; + } catch (e) { + console.error(e); + continue; + } + let output_idx_in_tx = iOut; - //let generated_tx_pubkey = cnUtil.derive_public_key(derivation,output_idx_in_tx,wallet.keys.pub.spend);//5.5ms - let generated_tx_pubkey = CnUtilNative.derive_public_key(derivation,output_idx_in_tx,wallet.keys.pub.spend);//5.5ms + let generated_tx_pubkey = CnNativeBride.derive_public_key(derivation, output_idx_in_tx, wallet.keys.pub.spend); // check if generated public key matches the current output's key let mine_output = (txout_k.key == generated_tx_pubkey); - if (mine_output) { - - //let minerTx = false; - - //if (amount !== 0) {//miner tx - // minerTx = true; - //} else { - // let mask = rawTransaction.rct_signatures.ecdhInfo[output_idx_in_tx].mask; - // let r = CryptoUtils.decode_ringct(rawTransaction.rct_signatures, - // tx_pub_key, - // wallet.keys.priv.view, - // output_idx_in_tx, - // mask, - // amount, - // derivation); - - // if (r === false) - // console.error("Cant decode ringCT!"); - // else - // amount = r; - //} - + if (mine_output) { let transactionOut = new TransactionOut(); - if (typeof out.globalIndex !== 'undefined') - transactionOut.globalIndex = out.globalIndex; + if (typeof rawTransaction.global_index_start !== 'undefined') + transactionOut.globalIndex = rawTransaction.output_indexes[output_idx_in_tx]; else transactionOut.globalIndex = output_idx_in_tx; - transactionOut.amount = amount; + transactionOut.amount = amount; transactionOut.pubKey = txout_k.key; transactionOut.outputIdx = output_idx_in_tx; - - //if (!minerTx) { - // transactionOut.rtcOutPk = rawTransaction.rct_signatures.outPk[output_idx_in_tx]; - // transactionOut.rtcMask = rawTransaction.rct_signatures.ecdhInfo[output_idx_in_tx].mask; - // transactionOut.rtcAmount = rawTransaction.rct_signatures.ecdhInfo[output_idx_in_tx].amount; - //} - //THIS is super slow and causing issues + /* + if (!minerTx) { + transactionOut.rtcOutPk = rawTransaction.rct_signatures.outPk[output_idx_in_tx]; + transactionOut.rtcMask = rawTransaction.rct_signatures.ecdhInfo[output_idx_in_tx].mask; + transactionOut.rtcAmount = rawTransaction.rct_signatures.ecdhInfo[output_idx_in_tx].amount; + } + */ if (wallet.keys.priv.spend !== null && wallet.keys.priv.spend !== '') { - let m_key_image = CryptoUtils.generate_key_image_helper({ + let m_key_image = CnTransactions.generate_key_image_helper({ view_secret_key: wallet.keys.priv.view, spend_secret_key: wallet.keys.priv.spend, public_spend_key: wallet.keys.pub.spend, @@ -129,33 +264,32 @@ export class TransactionsExplorer { transactionOut.keyImage = m_key_image.key_image; transactionOut.ephemeralPub = m_key_image.ephemeral_pub; } - if (transactionOut.amount !== 0) { //fusion - outs.push(transactionOut); - } + + outs.push(transactionOut); //if (minerTx) - // break; + // break; } // if (mine_output) } //check if no read only wallet if (wallet.keys.priv.spend !== null && wallet.keys.priv.spend !== '') { let keyImages = wallet.getTransactionKeyImages(); - for (let iIn = 0; iIn < rawTransaction.inputs.length; ++iIn) { - let input = rawTransaction.inputs[iIn]; - if (keyImages.indexOf(input.data.input.k_image) != -1) { + for (let iIn = 0; iIn < rawTransaction.vin.length; ++iIn) { + let vin = rawTransaction.vin[iIn]; + if (vin.value && keyImages.indexOf(vin.value.k_image) !== -1) { //console.log('found in', vin); let walletOuts = wallet.getAllOuts(); for (let ut of walletOuts) { - if (ut.keyImage == input.data.input.k_image) { + if (ut.keyImage == vin.value.k_image) { // ins.push(vin.key.k_image); // sumIns += ut.amount; let transactionIn = new TransactionIn(); transactionIn.amount = ut.amount; - transactionIn.keyImage = ut.keyImage; - ins.push(transactionIn); - //console.log(ut); + transactionIn.keyImage = ut.keyImage; + ins.push(transactionIn); + // console.log(ut); break; } } @@ -163,10 +297,12 @@ export class TransactionsExplorer { } } else { let txOutIndexes = wallet.getTransactionOutIndexes(); - for (let iIn = 0; iIn < rawTransaction.inputs.length; ++iIn) { - let input = rawTransaction.inputs[iIn]; + for (let iIn = 0; iIn < rawTransaction.vin.length; ++iIn) { + let vin = rawTransaction.vin[iIn]; + + if (!vin.value) continue; - let absoluteOffets = input.data.input.key_offsets.slice(); + let absoluteOffets = vin.value.key_offsets.slice(); for (let i = 1; i < absoluteOffets.length; ++i) { absoluteOffets[i] += absoluteOffets[i - 1]; } @@ -185,21 +321,33 @@ export class TransactionsExplorer { let transactionIn = new TransactionIn(); transactionIn.amount = -txOut.amount; transactionIn.keyImage = txOut.keyImage; - ins.push(transactionIn); + ins.push(transactionIn); } } } } - if (outs.length > 0 || ins.length > 0) { + if (outs.length > 0 || ins.length) { transaction = new Transaction(); - if (typeof rawTransaction.blockIndex !== 'undefined') transaction.blockHeight = rawTransaction.blockIndex; - if (typeof rawTransaction.timestamp !== 'undefined') transaction.timestamp = rawTransaction.timestamp; - if (typeof rawTransaction.hash !== 'undefined') transaction.hash = rawTransaction.hash; + + if (typeof rawTransaction.height !== 'undefined') transaction.blockHeight = rawTransaction.height; + if (typeof rawTransaction.ts !== 'undefined') transaction.timestamp = rawTransaction.ts; + if (typeof rawTransaction.hash !== 'undefined') transaction.hash = rawTransaction.hash; + transaction.txPubKey = tx_pub_key; - if (paymentId !== null && paymentId != '0000000000000000000000000000000000000000000000000000000000000000') + + if (paymentId !== null) transaction.paymentId = paymentId; - transaction.fees = rawTransaction.fee; + if (encryptedPaymentId !== null) { + transaction.paymentId = Cn.decrypt_payment_id(encryptedPaymentId, tx_pub_key, wallet.keys.priv.view); + } + + if (rawTransaction.vin[0].type === 'ff') { + transaction.fees = 0; + } else { + transaction.fees = rawTransaction.fee; + } + transaction.outs = outs; transaction.ins = ins; } @@ -212,7 +360,20 @@ export class TransactionsExplorer { static formatWalletOutsForTx(wallet: Wallet, blockchainHeight: number): RawOutForTx[] { let unspentOuts = []; - //console.log(wallet.getAll()); + //rct=rct_outpk + rct_mask + rct_amount + // {"amount" , out.amount}, + // {"public_key" , out.out_pub_key}, + // {"index" , out.out_index}, + // {"global_index" , out.global_index}, + // {"rct" , rct}, + // {"tx_id" , out.tx_id}, + // {"tx_hash" , tx.hash}, + // {"tx_prefix_hash" , tx.prefix_hash}, + // {"tx_pub_key" , tx.tx_pub_key}, + // {"timestamp" , static_cast(out.timestamp)}, + // {"height" , tx.height}, + // {"spend_key_images", json::array()} + for (let tr of wallet.getAll()) { //todo improve to take into account miner tx //only add outs unlocked @@ -222,7 +383,12 @@ export class TransactionsExplorer { for (let out of tr.outs) { - //console.log("out.globalIndex: " + out.globalIndex); + let rct = ''; + if (out.rtcAmount !== '') { + rct = out.rtcOutPk + out.rtcMask + out.rtcAmount; + } else { + rct = CnTransactions.zeroCommit(CnUtils.d2s(out.amount)); + } unspentOuts.push({ keyImage: out.keyImage, @@ -263,19 +429,19 @@ export class TransactionsExplorer { mixin: number, neededFee: number, payment_id: string - ): Promise<{ raw: { hash: string, prvKey: string, raw: string }, signed: any }> { - return new Promise<{ raw: { hash: string, prvKey: string, raw: string }, signed: any }>(function (resolve, reject) { + ): Promise<{ raw: { hash: string, prvkey: string, raw: string }, signed: any }> { + return new Promise<{ raw: { hash: string, prvkey: string, raw: string }, signed: any }>(function (resolve, reject) { let signed; try { //console.log('Destinations: '); //need to get viewkey for encrypting here, because of splitting and sorting let realDestViewKey = undefined; if (pid_encrypt) { - realDestViewKey = cnUtil.decode_address(dsts[0].address).view; + realDestViewKey = Cn.decode_address(dsts[0].address).view; } - let splittedDsts = cnUtil.decompose_tx_destinations(dsts, rct); - signed = cnUtil.create_transaction( + let splittedDsts = CnTransactions.decompose_tx_destinations(dsts, rct); + signed = CnTransactions.create_transaction( { spend: wallet.keys.pub.spend, view: wallet.keys.pub.view @@ -286,15 +452,16 @@ export class TransactionsExplorer { splittedDsts, usingOuts, mix_outs, mixin, neededFee, payment_id, pid_encrypt, - realDestViewKey, 0); + realDestViewKey, 0, rct); + + console.log("signed tx: ", signed); + let raw_tx_and_hash = CnTransactions.serialize_tx_with_hash(signed); + resolve({raw: raw_tx_and_hash, signed: signed}); } catch (e) { reject("Failed to create transaction: " + e); } - //console.log("signed tx: ", signed); - //let raw_tx_and_hash = cnUtil.serialize_rct_tx_with_hash(signed); - let raw_tx_and_hash = cnUtil.serialize_tx_with_hash(signed); - resolve({raw: raw_tx_and_hash, signed: signed}); + }); } @@ -305,16 +472,12 @@ export class TransactionsExplorer { blockchainHeight: number, obtainMixOutsCallback: (quantity: number) => Promise, confirmCallback: (amount: number, feesAmount: number) => Promise, - mixin : number = config.defaultMixin): - Promise<{ raw: { hash: string, prvKey: string, raw: string }, signed: any }> { - return new Promise<{ raw: { hash: string, prvKey: string, raw: string }, signed: any }>(function (resolve, reject) { - // few multiplayers based on uint64_t wallet2::get_fee_multiplier - //let fee_multiplayers = [1, 4, 20, 166]; - //let default_priority = 2; - //let feePerKB = new JSBigInt((window).config.feePerKB); - //let priority = default_priority; - //let fee_multiplayer = fee_multiplayers[priority - 1]; - let neededFee = new JSBigInt((window).config.coinFee); // feePerKB.multiply(13).multiply(fee_multiplayer); + mixin: number = config.defaultMixin): + Promise<{ raw: { hash: string, prvkey: string, raw: string }, signed: any }> { + return new Promise<{ raw: { hash: string, prvkey: string, raw: string }, signed: any }>(function (resolve, reject) { + + let neededFee = new JSBigInt((window).config.coinFee); + let pid_encrypt = false; //don't encrypt payment ID unless we find an integrated one let totalAmountWithoutFee = new JSBigInt(0); @@ -325,13 +488,17 @@ export class TransactionsExplorer { for (let dest of userDestinations) { totalAmountWithoutFee = totalAmountWithoutFee.add(dest.amount); - let target = cnUtil.decode_address(dest.address); - if (typeof target.intPaymentId !== 'undefined') { + let target = Cn.decode_address(dest.address); + if (target.intPaymentId !== null) { ++paymentIdIncluded; paymentId = target.intPaymentId; pid_encrypt = true; } - dsts.push(dest); + + dsts.push({ + address: dest.address, + amount: new JSBigInt(dest.amount) + }); } if (paymentIdIncluded > 1) { @@ -397,60 +564,41 @@ export class TransactionsExplorer { return kB * fee_per_kb * fee_multiplier; }; - //console.log("Selected outs:", usingOuts); - //if (usingOuts.length > 1) { - // let newNeededFee = 10000000; //JSBigInt(Math.ceil(cnUtil.estimateRctSize(usingOuts.length, mixin, 2) / 1024)).multiply(feePerKB).multiply(fee_multiplayer); - // totalAmount = totalAmountWithoutFee.add(newNeededFee); - //add outputs 1 at a time till we either have them all or can meet the fee - // while (usingOuts_amount.compare(totalAmount) < 0 && unusedOuts.length > 0) { - // let out = pop_random_value(unusedOuts); - // usingOuts.push(out); - // usingOuts_amount = usingOuts_amount.add(out.amount); - // console.log("Using output: " + cnUtil.formatMoney(out.amount) + " - " + JSON.stringify(out)); - // newNeededFee = JSBigInt(Math.ceil((usingOuts.length, mixin, 2) / 1024)).multiply(feePerKB).multiply(fee_multiplayer); - // totalAmount = totalAmountWithoutFee.add(newNeededFee); - // } - // console.log("New fee: " + cnUtil.formatMoneySymbol(newNeededFee) + " for " + usingOuts.length + " inputs"); - // neededFee = newNeededFee; - //} - - if (neededFee < 10000000) { - neededFee = 10000000; - } - - // neededFee = neededFee / 3 * 2; - - //console.log('using amount of ' + usingOuts_amount + ' for sending ' + totalAmountWithoutFee + ' with fees of ' + (neededFee / Math.pow(10, config.coinUnitPlaces))); + console.log("Selected outs:", usingOuts); + + if (neededFee < 10000000) { + neededFee = 10000000; + } + + console.log('using amount of ' + usingOuts_amount + ' for sending ' + totalAmountWithoutFee + ' with fees of ' + (neededFee / Math.pow(10, config.coinUnitPlaces)) + ' QWC'); confirmCallback(totalAmountWithoutFee, neededFee).then(function () { if (usingOuts_amount.compare(totalAmount) < 0) { - //console.log("Not enough spendable outputs / balance too low (have " - // + cnUtil.formatMoneyFull(usingOuts_amount) + " but need " - // + cnUtil.formatMoneyFull(totalAmount) - // + " (estimated fee " + cnUtil.formatMoneyFull(neededFee) + " included)"); + console.log("Not enough spendable outputs / balance too low (have " + + Cn.formatMoneyFull(usingOuts_amount) + " but need " + + Cn.formatMoneyFull(totalAmount) + + " (estimated fee " + Cn.formatMoneyFull(neededFee) + " QWC included)"); // return; reject({error: 'balance_too_low'}); return; - } - else if (usingOuts_amount.compare(totalAmount) > 0) { + } else if (usingOuts_amount.compare(totalAmount) > 0) { let changeAmount = usingOuts_amount.subtract(totalAmount); //add entire change for rct - //console.log("1) Sending change of " + cnUtil.formatMoneySymbol(changeAmount) - // + " to " /*+ AccountService.getAddress()*/); + console.log("1) Sending change of " + Cn.formatMoneySymbol(changeAmount) + + " to " /*+ AccountService.getAddress()*/); dsts.push({ address: wallet.getPublicAddress(), amount: changeAmount }); - } - else if (usingOuts_amount.compare(totalAmount) === 0) { + } else if (usingOuts_amount.compare(totalAmount) === 0) { //create random destination to keep 2 outputs always in case of 0 change - //let fakeAddress = cnUtil.create_address(cnUtil.random_scalar()).public_addr; - //console.log("Sending 0 KRB to a fake address to keep tx uniform (no change exists): " + fakeAddress); - //dsts.push({ - // address: fakeAddress, - // amount: 0 - //}); + let fakeAddress = Cn.create_address(CnRandom.random_scalar()).public_addr; + console.log("Sending 0 QWC to a fake address to keep tx uniform (no change exists): " + fakeAddress); + dsts.push({ + address: fakeAddress, + amount: 0 + }); } - //console.log('destinations', dsts); + console.log('destinations', dsts); let amounts: string[] = []; for (let l = 0; l < usingOuts.length; l++) { @@ -458,9 +606,9 @@ export class TransactionsExplorer { } obtainMixOutsCallback(amounts.length * (mixin + 1)).then(function (lotsMixOuts: any[]) { - //console.log('------------------------------mix_outs', lotsMixOuts); - //console.log('amounts', amounts); - //console.log('lots_mix_outs', lotsMixOuts); + console.log('------------------------------mix_outs', lotsMixOuts); + console.log('amounts', amounts); + console.log('lots_mix_outs', lotsMixOuts); let mix_outs = []; let iMixOutsIndexes = 0; @@ -476,9 +624,9 @@ export class TransactionsExplorer { amount: 0 }); } - //console.log('mix_outs', mix_outs); + console.log('mix_outs', mix_outs); - TransactionsExplorer.createRawTx(dsts, wallet, false, usingOuts, pid_encrypt, mix_outs, mixin, neededFee, paymentId).then(function (data: { raw: { hash: string, prvKey: string, raw: string }, signed: any }) { + TransactionsExplorer.createRawTx(dsts, wallet, false, usingOuts, pid_encrypt, mix_outs, mixin, neededFee, paymentId).then(function (data: { raw: { hash: string, prvkey: string, raw: string }, signed: any }) { resolve(data); }).catch(function (e) { reject(e); @@ -487,11 +635,8 @@ export class TransactionsExplorer { //https://github.com/moneroexamples/openmonero/blob/ebf282faa8d385ef3cf97e6561bd1136c01cf210/README.md //https://github.com/moneroexamples/openmonero/blob/95bc207e1dd3881ba0795c02c06493861de8c705/src/YourMoneroRequests.cpp - }); - }); } - - } + diff --git a/src/model/Wallet.ts b/src/model/Wallet.ts index da235e01..0b282cf2 100644 --- a/src/model/Wallet.ts +++ b/src/model/Wallet.ts @@ -16,7 +16,7 @@ import {Transaction, TransactionIn, TransactionOut} from "./Transaction"; import {KeysRepository, UserKeys} from "./KeysRepository"; import {Observable} from "../lib/numbersLab/Observable"; -import {CryptoUtils} from "./CryptoUtils"; +import {Cn, CnNativeBride, CnTransactions} from "./Cn"; export type RawWalletOptions = { checkMinerTx?:boolean, @@ -318,7 +318,7 @@ export class Wallet extends Observable{ } getPublicAddress(){ - return cnUtil.pubkeys_to_string(this.keys.pub.spend,this.keys.pub.view); + return Cn.pubkeys_to_string(this.keys.pub.spend, this.keys.pub.view); } recalculateIfNotViewOnly(){ @@ -335,13 +335,13 @@ export class Wallet extends Observable{ if(needDerivation) { let derivation = ''; try { - derivation = cnUtil.generate_key_derivation(tx.txPubKey, this.keys.priv.view);//9.7ms + derivation = CnNativeBride.generate_key_derivation(tx.txPubKey, this.keys.priv.view); } catch (e) { continue; } for (let out of tx.outs) { if (out.keyImage === '') { - let m_key_image = CryptoUtils.generate_key_image_helper({ + let m_key_image = CnTransactions.generate_key_image_helper({ view_secret_key: this.keys.priv.view, spend_secret_key: this.keys.priv.spend, public_spend_key: this.keys.pub.spend, diff --git a/src/model/WalletWatchdog.ts b/src/model/WalletWatchdog.ts new file mode 100644 index 00000000..3e5795aa --- /dev/null +++ b/src/model/WalletWatchdog.ts @@ -0,0 +1,330 @@ +/** + * Copyright (c) 2018-2020, ExploShot + * Copyright (c) 2018-2020, The Qwertycoin Project + * Copyright (c) 2018-2020, The Karbo + * + * All rights reserved. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * ==> Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * ==> Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * ==> Neither the name of Qwertycoin nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {Wallet} from "./Wallet"; +import {BlockchainExplorer, RawDaemon_Transaction} from "./blockchain/BlockchainExplorer"; +import {Transaction} from "./Transaction"; +import {TransactionsExplorer} from "./TransactionsExplorer"; + +export class WalletWatchdog { + + wallet: Wallet; + explorer: BlockchainExplorer; + + constructor(wallet: Wallet, explorer: BlockchainExplorer) { + this.wallet = wallet; + this.explorer = explorer; + + this.initWorker(); + this.initMempool(); + } + + initWorker() { + let self = this; + + if (this.wallet.options.customNode) { + config.nodeUrl = this.wallet.options.nodeUrl; + } else { + let randNodeInt:number = Math.floor(Math.random() * Math.floor(config.nodeList.length)); + config.nodeUrl = config.nodeList[randNodeInt]; + } + + this.workerProcessing = new Worker('./workers/TransferProcessingEntrypoint.js'); + this.workerProcessing.onmessage = function (data: MessageEvent) { + let message: string | any = data.data; + //console.log("InitWorker message: "); + //console.log(message); + if (message === 'ready') { + //console.info('worker ready'); + self.signalWalletUpdate(); + } else if (message === 'readyWallet') { + self.workerProcessingReady = true; + } else if (message.type) { + if (message.type === 'processed') { + let transactions = message.transactions; + if (transactions.length > 0) { + for (let tx of transactions) + self.wallet.addNew(Transaction.fromRaw(tx)); + self.signalWalletUpdate(); + } + if (self.workerCurrentProcessing.length > 0) { + let transactionHeight = self.workerCurrentProcessing[self.workerCurrentProcessing.length - 1].height; + if (typeof transactionHeight !== 'undefined') + self.wallet.lastHeight = transactionHeight; + } + + self.workerProcessingWorking = false; + } + } + }; + } + + signalWalletUpdate() { + let self = this; + //console.log('wallet update'); + this.lastBlockLoading = -1;//reset scanning + + if (this.wallet.options.customNode) { + config.nodeUrl = this.wallet.options.nodeUrl; + } else { + let randNodeInt:number = Math.floor(Math.random() * Math.floor(config.nodeList.length)); + config.nodeUrl = config.nodeList[randNodeInt]; + } + + this.workerProcessing.postMessage({ + type: 'initWallet', + wallet: this.wallet.exportToRaw() + }); + clearInterval(this.intervalTransactionsProcess); + this.intervalTransactionsProcess = setInterval(function () { + self.checkTransactionsInterval(); + }, this.wallet.options.readSpeed); + + //force mempool update after a wallet update (new tx, ...) + self.checkMempool(); + } + + intervalMempool = 0; + + initMempool(force: boolean = false) { + let self = this; + if (this.intervalMempool === 0 || force) { + if (force && this.intervalMempool !== 0) { + clearInterval(this.intervalMempool); + } + + this.intervalMempool = setInterval(function () { + self.checkMempool(); + }, config.avgBlockTime / 2 * 1000); + } + self.checkMempool(); + } + + stopped: boolean = false; + + stop() { + clearInterval(this.intervalTransactionsProcess); + this.transactionsToProcess = []; + clearInterval(this.intervalMempool); + this.stopped = true; + } + + checkMempool(): boolean { + let self = this; + if (this.lastMaximumHeight - this.lastBlockLoading > 1) { //only check memory pool if the user is up to date to ensure outs & ins will be found in the wallet + return false; + } + + this.wallet.txsMem = []; + this.explorer.getTransactionPool().then(function (pool: any) { + if (typeof pool !== 'undefined') + for (let rawTx of pool) { + let tx = TransactionsExplorer.parse(rawTx, self.wallet); + if (tx !== null) { + self.wallet.txsMem.push(tx); + } + } + }).catch(function () { + }); + return true; + } + + terminateWorker() { + this.workerProcessing.terminate(); + this.workerProcessingReady = false; + this.workerCurrentProcessing = []; + this.workerProcessingWorking = false; + this.workerCountProcessed = 0; + } + + transactionsToProcess: RawDaemon_Transaction[] = []; + intervalTransactionsProcess = 0; + + workerProcessing !: Worker; + workerProcessingReady = false; + workerProcessingWorking = false; + workerCurrentProcessing: RawDaemon_Transaction[] = []; + workerCountProcessed = 0; + + checkTransactions(rawTransactions: RawDaemon_Transaction[]) { + for (let rawTransaction of rawTransactions) { + let height = rawTransaction.height; + if (typeof height !== 'undefined') { + let transaction = TransactionsExplorer.parse(rawTransaction, this.wallet); + if (transaction !== null) { + this.wallet.addNew(transaction); + } + if (height - this.wallet.lastHeight >= 2) { + this.wallet.lastHeight = height - 1; + } + } + } + if (this.transactionsToProcess.length == 0) { + this.wallet.lastHeight = this.lastBlockLoading; + } + } + + checkTransactionsInterval() { + + //somehow we're repeating and regressing back to re-process Tx's + //loadHistory getting into a stack overflow ? + //need to work out timinings and ensure process does not reload when it's already running... + + if (this.workerProcessingWorking || !this.workerProcessingReady) { + return; + } + + //we destroy the worker in charge of decoding the transactions every 5k transactions to ensure the memory is not corrupted + //cnUtil bug, see https://github.com/mymonero/mymonero-core-js/issues/8 + if (this.workerCountProcessed >= 5 * 1000) { + //console.log('Recreate worker..'); + this.terminateWorker(); + this.initWorker(); + return; + } + + let transactionsToProcess: RawDaemon_Transaction[] = this.transactionsToProcess.splice(0, 25); //process 25 tx's at a time + if (transactionsToProcess.length > 0) { + this.workerCurrentProcessing = transactionsToProcess; + this.workerProcessing.postMessage({ + type: 'process', + transactions: transactionsToProcess + }); + //this.workerCountProcessed += this.transactionsToProcess.length; + ++this.workerCountProcessed; + this.workerProcessingWorking = true; + } else { + clearInterval(this.intervalTransactionsProcess); + this.intervalTransactionsProcess = 0; + } + } + + processTransactions(transactions: RawDaemon_Transaction[]) { + let transactionsToAdd = []; + + for (let tr of transactions) { + if (typeof tr.height !== 'undefined') + if (tr.height > this.wallet.lastHeight) { + transactionsToAdd.push(tr); + } + } + + this.transactionsToProcess.push.apply(this.transactionsToProcess, transactionsToAdd); + if (this.intervalTransactionsProcess === 0) { + let self = this; + this.intervalTransactionsProcess = setInterval(function () { + self.checkTransactionsInterval(); + }, this.wallet.options.readSpeed); + } + } + + + lastBlockLoading = -1; + lastMaximumHeight = 0; + + loadHistory() { + if (this.stopped) return; + + let self = this; + + if (this.lastBlockLoading === -1) this.lastBlockLoading = this.wallet.lastHeight; + + //don't reload until it's finished processing the last batch of transactions + if (this.workerProcessingWorking || !this.workerProcessingReady) { + setTimeout(function () { + self.loadHistory(); + }, 100); + return; + } + if (this.transactionsToProcess.length > 500) { + //to ensure no pile explosion + setTimeout(function () { + self.loadHistory(); + }, 2 * 1000); + return; + } + + // console.log('checking'); + this.explorer.getHeight().then(function (height) { + if (height > self.lastMaximumHeight) self.lastMaximumHeight = height; + + if (self.lastBlockLoading !== height) { + let previousStartBlock = Number(self.lastBlockLoading); + let endBlock = previousStartBlock + config.syncBlockCount; + + if (previousStartBlock > self.lastMaximumHeight) previousStartBlock = self.lastMaximumHeight; + if (endBlock > self.lastMaximumHeight) endBlock = self.lastMaximumHeight; + + self.explorer.getTransactionsForBlocks(previousStartBlock, endBlock, self.wallet.options.checkMinerTx).then(function (transactions: any) { + //to ensure no pile explosion + if (transactions === 'OK') { + self.lastBlockLoading = endBlock; + self.wallet.lastHeight = endBlock; + setTimeout(function () { + self.loadHistory(); + }, 1); + } else if (transactions.length > 0) { + let lastTx = transactions[transactions.length - 1]; + if (typeof lastTx.height !== 'undefined') { + self.lastBlockLoading = lastTx.height + 1; + } + self.processTransactions(transactions); + setTimeout(function () { + self.loadHistory(); + }, 1); + } else { + + self.lastBlockLoading = endBlock; + self.wallet.lastHeight = endBlock; + + setTimeout(function () { + self.loadHistory(); + }, 30 * 1000); + } + }).catch(function () { + setTimeout(function () { + self.loadHistory(); + }, 30 * 1000);//retry 30s later if an error occurred + }); + } else { + setTimeout(function () { + self.loadHistory(); + }, 30 * 1000); + } + }).catch(function () { + setTimeout(function () { + self.loadHistory(); + }, 30 * 1000);//retry 30s later if an error occurred + }); + } + + +} diff --git a/src/model/blockchain/BlockchainExplorer.ts b/src/model/blockchain/BlockchainExplorer.ts index 6827ec2a..922ed34a 100644 --- a/src/model/blockchain/BlockchainExplorer.ts +++ b/src/model/blockchain/BlockchainExplorer.ts @@ -14,9 +14,61 @@ */ import {Wallet} from "../Wallet"; +import {CnTransactions} from "../Cn"; -export interface BlockchainExplorer{ - getHeight() : Promise; - getScannedHeight() : number; - watchdog(wallet : Wallet) : void; -} \ No newline at end of file +export type RawDaemon_Transaction = { + extra: string, + vout: CnTransactions.Vout[], + vin: { + type: string, + value?: CnTransactions.Vin, + gen?: { height: number }, + }[], + rct_signatures: CnTransactions.RctSignature, + unlock_time: number, + version: number, + ctsig_prunable: any, + global_index_start?: number, + output_indexes: number[], + height?: number, + ts?: number,//timestamp + hash?: string, + fee: number +}; + +export type NetworkInfo = { + node: string, + major_version: number, + hash: string, + reward: number, + height: number, + timestamp: number, + difficulty: number, +}; + +export type RemoteNodeInformation = { + fee_address: string, + status: string +}; + +export interface BlockchainExplorer { + resolveOpenAlias(str: string): Promise<{ address: string, name: string | null }>; + + getHeight(): Promise; + + getScannedHeight(): number; + + watchdog(wallet: Wallet): void; + + getTransactionPool(): Promise; + + getTransactionsForBlocks(startBlock: number, endBlock: number, includeMinerTx: boolean): Promise; + + sendRawTx(rawTx: string): Promise; + + getRandomOuts(numberOuts: number): Promise; + + getNetworkInfo(): Promise; + + getRemoteNodeInformation(): Promise; +} diff --git a/src/model/blockchain/BlockchainExplorerRPCDaemon.ts b/src/model/blockchain/BlockchainExplorerRPCDaemon.ts new file mode 100644 index 00000000..85591ca9 --- /dev/null +++ b/src/model/blockchain/BlockchainExplorerRPCDaemon.ts @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2018, Gnock + * Copyright (c) 2018, The Masari Project + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {BlockchainExplorer, NetworkInfo, RawDaemon_Transaction, RemoteNodeInformation} from "./BlockchainExplorer"; +import {Wallet} from "../Wallet"; +import {MathUtil} from "../MathUtil"; +import {CnTransactions, CnUtils} from "../Cn"; +import {Transaction} from "../Transaction"; +import {WalletWatchdog} from "../WalletWatchdog"; + +export type DaemonResponseGetInfo = { + "already_generated_coins": number, + "block_major_version": number, + "contact": string, + "cumulative_difficulty": number, + "difficulty": number, + "fee_address": string, + "grey_peerlist_size": number, + "height": number, + "height_without_bootstrap": number, + "is_synchronized": boolean, + "incoming_connections_count": number, + "outgoing_connections_count": number, + "last_known_block_index": number, + "min_fee": number, + "next_reward": number, + "rpc_connections_count": number, + "start_time": number, + "status": "OK" | string, + "target": number, + "top_block_hash": string, + "transactions_count": number, + "transactions_pool_size": number, + "white_peerlist_size": number +} + +export type DaemonResponseGetNodeFeeInfo = { + fee_address: string, + fee_amount: number, + status: "OK" | string +} + +export class BlockchainExplorerRpcDaemon implements BlockchainExplorer { + //daemonAddress = config.nodeList[Math.floor(Math.random() * Math.floor(config.nodeList.length))]; + daemonAddress = config.nodeUrl; + phpProxy: boolean = false; + + constructor(daemonAddress: string | null = null) { + if (daemonAddress !== null && daemonAddress.trim() !== '') { + this.daemonAddress = daemonAddress; + } + } + + protected makeRpcRequest(method: string, params: any = {}): Promise { + return new Promise((resolve, reject) => { + $.ajax({ + url: config.nodeUrl + 'json_rpc', + method: 'POST', + data: JSON.stringify({ + jsonrpc: '2.0', + method: method, + params: params, + id: 0 + }), + contentType: 'application/json' + }).done(function (raw: any) { + if ( + typeof raw.id === 'undefined' || + typeof raw.jsonrpc === 'undefined' || + raw.jsonrpc !== '2.0' || + typeof raw.result !== 'object' + ) + reject('Daemon response is not properly formatted'); + else + resolve(raw.result); + }).fail(function (data: any) { + reject(data); + }); + }); + } + + protected makeRequest(method: 'GET' | 'POST', url: string, body: any = undefined): Promise { + return new Promise((resolve, reject) => { + $.ajax({ + url: config.nodeUrl + url, + method: method, + data: typeof body === 'string' ? body : JSON.stringify(body) + }).done(function (raw: any) { + resolve(raw); + }).fail(function (data: any) { + reject(data); + }); + }); + } + + cacheInfo: any = null; + cacheHeight: number = 0; + lastTimeRetrieveInfo = 0; + + getInfo(): Promise { + if (Date.now() - this.lastTimeRetrieveInfo < 20 * 1000 && this.cacheInfo !== null) { + return Promise.resolve(this.cacheInfo); + } + + this.lastTimeRetrieveInfo = Date.now(); + return this.makeRequest('GET', 'getinfo').then((data: DaemonResponseGetInfo) => { + this.cacheInfo = data; + console.log(`GetInfo: `) + return data; + }) + } + + getHeight(): Promise { + if (Date.now() - this.lastTimeRetrieveInfo < 20 * 1000 && this.cacheHeight !== 0) { + return Promise.resolve(this.cacheHeight); + } + + this.lastTimeRetrieveInfo = Date.now(); + return this.makeRequest('GET', 'getheight').then((data: any) => { + let height = parseInt(data.height); + this.cacheHeight = height; + return height; + }) + } + + scannedHeight: number = 0; + + getScannedHeight(): number { + return this.scannedHeight; + } + + watchdog(wallet: Wallet): WalletWatchdog { + let watchdog = new WalletWatchdog(wallet, this); + watchdog.loadHistory(); + return watchdog; + } + + /** + * Returns an array containing all numbers like [start;end] + * @param start + * @param end + */ + range(start: number, end: number) { + let numbers: number[] = []; + for (let i = start; i <= end; ++i) { + numbers.push(i); + } + + return numbers; + } + + getTransactionsForBlocks(startBlock: number, endBlock: number, includeMinerTxs: boolean): Promise { + let tempStartBlock; + if (startBlock === 0) { + tempStartBlock = 1; + } else { + tempStartBlock = startBlock; + } + + return this.makeRequest('POST', 'get_raw_transactions_by_heights', { + heights: [tempStartBlock, endBlock], + include_miner_txs: includeMinerTxs, + range: true + }).then((response: { + status: 'OK' | 'string', + transactions: { transaction: any, timestamp: number, output_indexes: number[], height: number, block_hash: string, hash: string, fee: number }[] + }) => { + let formatted: RawDaemon_Transaction[] = []; + + if (response.status !== 'OK') throw 'invalid_transaction_answer'; + + if (response.transactions.length > 0) { + for (let rawTx of response.transactions) { + let tx: RawDaemon_Transaction | null = null; + try { + tx = rawTx.transaction; + } catch (e) { + try { + //compat for some invalid endpoints + tx = rawTx.transaction; + } catch (e) { + } + } + if (tx !== null) { + tx.ts = rawTx.timestamp; + tx.height = rawTx.height; + tx.hash = rawTx.hash; + if (rawTx.output_indexes.length > 0) + tx.global_index_start = rawTx.output_indexes[0]; + tx.output_indexes = rawTx.output_indexes; + formatted.push(tx); + } + } + + return formatted; + } else { + return response.status; + } + }); + } + + getTransactionPool(): Promise { + return this.makeRequest('GET', 'getrawtransactionspool').then( + (response: { + status: 'OK' | 'string', + transactions: { transaction: any, timestamp: number, output_indexes: number[], height: number, block_hash: string, hash: string, fee: number }[] + }) => { + + let formatted: RawDaemon_Transaction[] = []; + + for (let rawTx of response.transactions) { + let tx: RawDaemon_Transaction | null = null; + try { + tx = rawTx.transaction; + } catch (e) { + try { + //compat for some invalid endpoints + tx = rawTx.transaction; + } catch (e) { + } + } + if (tx !== null) { + tx.ts = rawTx.timestamp; + tx.height = rawTx.height; + tx.hash = rawTx.hash; + if (rawTx.output_indexes.length > 0) { + tx.global_index_start = rawTx.output_indexes[0]; + tx.output_indexes = rawTx.output_indexes; + } + formatted.push(tx); + } + } + + return formatted; + }); + } + + existingOuts: any[] = []; + + getRandomOuts(nbOutsNeeded: number, initialCall = true): Promise { + let self = this; + if (initialCall) { + self.existingOuts = []; + } + + return this.getHeight().then(function (height: number) { + let txs: RawDaemon_Transaction[] = []; + let promiseGetCompressedBlocks: Promise = Promise.resolve(); + + let randomBlocksIndexesToGet: number[] = []; + let numOuts = height; + + let compressedBlocksToGet: { [key: string]: boolean } = {}; + + console.log('Requires ' + nbOutsNeeded + ' outs'); + + //select blocks for the final mixin. selection is made with a triangular selection + for (let i = 0; i < nbOutsNeeded; ++i) { + let selectedIndex: number = -1; + do { + selectedIndex = MathUtil.randomTriangularSimplified(numOuts); + if (selectedIndex >= height - config.txCoinbaseMinConfirms) + selectedIndex = -1; + } while (selectedIndex === -1 || randomBlocksIndexesToGet.indexOf(selectedIndex) !== -1); + randomBlocksIndexesToGet.push(selectedIndex); + + compressedBlocksToGet[Math.floor(selectedIndex / 100) * 100] = true; + } + + console.log('Random blocks required: ', randomBlocksIndexesToGet); + console.log('Blocks to get for outputs selections:', compressedBlocksToGet); + + //load compressed blocks (100 blocks) containing the blocks referred by their index + for (let compressedBlock in compressedBlocksToGet) { + promiseGetCompressedBlocks = promiseGetCompressedBlocks.then(() => { + return self.getTransactionsForBlocks(parseInt(compressedBlock), Math.min(parseInt(compressedBlock) + 99, height - config.txCoinbaseMinConfirms), false).then(function (rawTransactions: RawDaemon_Transaction[]) { + txs.push.apply(txs, rawTransactions); + }); + }); + } + + return promiseGetCompressedBlocks.then(function () { + console.log('txs selected for outputs: ', txs); + let txCandidates: any = {}; + for (let iOut = 0; iOut < txs.length; ++iOut) { + let tx = txs[iOut]; + + if ( + (typeof tx.height !== 'undefined' && randomBlocksIndexesToGet.indexOf(tx.height) === -1) || + typeof tx.height === 'undefined' + ) { + continue; + } + + for (let output_idx_in_tx = 0; output_idx_in_tx < tx.vout.length; ++output_idx_in_tx) { + let rct = null; + let globalIndex = output_idx_in_tx; + if (typeof tx.global_index_start !== 'undefined' && typeof tx.output_indexes !== 'undefined') { + globalIndex = tx.output_indexes[output_idx_in_tx]; + } + if (tx.vout[output_idx_in_tx].amount !== 0) {//check if miner tx + rct = CnTransactions.zeroCommit(CnUtils.d2s(tx.vout[output_idx_in_tx].amount)); + } else { + let rtcOutPk = tx.rct_signatures.outPk[output_idx_in_tx]; + let rtcMask = tx.rct_signatures.ecdhInfo[output_idx_in_tx].mask; + let rtcAmount = tx.rct_signatures.ecdhInfo[output_idx_in_tx].amount; + rct = rtcOutPk + rtcMask + rtcAmount; + } + + let newOut = { + rct: rct, + public_key: tx.vout[output_idx_in_tx].target.data.key, + global_index: globalIndex, + // global_index: count, + }; + if (typeof txCandidates[tx.height] === 'undefined') txCandidates[tx.height] = []; + txCandidates[tx.height].push(newOut); + + } + } + + console.log(txCandidates); + + let selectedOuts = []; + for (let txsOutsHeight in txCandidates) { + let outIndexSelect = MathUtil.getRandomInt(0, txCandidates[txsOutsHeight].length - 1); + console.log('select ' + outIndexSelect + ' for ' + txsOutsHeight + ' with length of ' + txCandidates[txsOutsHeight].length); + selectedOuts.push(txCandidates[txsOutsHeight][outIndexSelect]); + } + + console.log(selectedOuts); + + return selectedOuts; + }); + }); + } + + sendRawTx(rawTx: string) { + return this.makeRequest('POST', 'sendrawtransaction', { + tx_as_hex: rawTx, + do_not_relay: false + }).then((transactions: any) => { + if (!transactions.status || transactions.status !== 'OK') + throw transactions; + }); + } + + resolveOpenAlias(domain: string): Promise<{ address: string, name: string | null }> { + return this.makeRpcRequest('resolve_open_alias', {url: domain}).then(function (response: { + addresses?: string[], + status: 'OK' | string + }) { + if (response.addresses && response.addresses.length > 0) + return {address: response.addresses[0], name: null}; + throw 'not_found'; + }); + } + + getNetworkInfo(): Promise { + return this.makeRpcRequest('getlastblockheader').then((raw: any) => { + console.log(raw); + return { + 'node': config.nodeUrl.split(':')[1].replace(/[-[\]\/{}()*+?\\^$|#\s]/g, ''), + 'major_version': raw.block_header['major_version'], + 'hash': raw.block_header['hash'], + 'reward': raw.block_header['reward'], + 'height': raw.block_header['height'], + 'timestamp': raw.block_header['timestamp'], + 'difficulty': raw.block_header['difficulty'] + } + }); + } + + getRemoteNodeInformation(): Promise { + // TODO change to /feeaddress + return this.getInfo().then((info: DaemonResponseGetInfo) => { + return { + 'fee_address': info['fee_address'], + 'status': info['status'] + } + }); + } + +} diff --git a/src/model/blockchain/BlockchainExplorerRpc2.ts b/src/model/blockchain/BlockchainExplorerRpc2.ts deleted file mode 100644 index dbe18745..00000000 --- a/src/model/blockchain/BlockchainExplorerRpc2.ts +++ /dev/null @@ -1,545 +0,0 @@ -/* - * Copyright (c) 2018, Gnock - * Copyright (c) 2018, The Masari Project - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import { BlockchainExplorer } from "./BlockchainExplorer"; -import { Wallet } from "../Wallet"; -import { TransactionsExplorer, TX_EXTRA_TAG_PUBKEY } from "../TransactionsExplorer"; -import { CryptoUtils } from "../CryptoUtils"; -import { Transaction } from "../Transaction"; -import { MathUtil } from "../MathUtil"; - -export class WalletWatchdog { - - wallet: Wallet; - explorer: BlockchainExplorerRpc2; - - constructor(wallet: Wallet, explorer: BlockchainExplorerRpc2) { - this.wallet = wallet; - this.explorer = explorer; - - this.initWorker(); - this.initMempool(); - } - - initWorker() { - let self = this; - if (this.wallet.options.customNode) { - config.nodeUrl = this.wallet.options.nodeUrl; - } else { - let randNodeInt:number = Math.floor(Math.random() * Math.floor(config.nodeList.length)); - config.nodeUrl = config.nodeList[randNodeInt]; - } - - this.workerProcessing = new Worker('./workers/TransferProcessingEntrypoint.js'); - this.workerProcessing.onmessage = function (data: MessageEvent) { - let message: string | any = data.data; - //console.log("InitWorker message: "); - //console.log(message); - if (message === 'ready') { - self.signalWalletUpdate(); - } else if (message === 'readyWallet') { - self.workerProcessingReady = true; - } else if (message.type) { - if (message.type === 'processed') { - let transactions = message.transactions; - if (transactions.length > 0) { - for (let tx of transactions) - self.wallet.addNew(Transaction.fromRaw(tx)); - self.signalWalletUpdate(); - } - if (self.workerCurrentProcessing.length > 0) { - let transactionHeight = self.workerCurrentProcessing[self.workerCurrentProcessing.length - 1].blockIndex; - if (typeof transactionHeight !== 'undefined') - self.wallet.lastHeight = transactionHeight; - } - - self.workerProcessingWorking = false; - } - } - }; - } - - signalWalletUpdate() { - let self = this; - this.lastBlockLoading = -1;//reset scanning - - if (this.wallet.options.customNode) { - config.nodeUrl = this.wallet.options.nodeUrl; - } else { - let randNodeInt:number = Math.floor(Math.random() * Math.floor(config.nodeList.length)); - config.nodeUrl = config.nodeList[randNodeInt]; - } - - this.workerProcessing.postMessage({ - type: 'initWallet', - wallet:this.wallet.exportToRaw() - }); - clearInterval(this.intervalTransactionsProcess); - this.intervalTransactionsProcess = setInterval(function () { - self.checkTransactionsInterval(); - }, this.wallet.options.readSpeed); - } - - intervalMempool = 0; - initMempool() { - let self = this; - if (this.intervalMempool === 0) { - this.intervalMempool = setInterval(function () { - self.checkMempool(); - }, 30 * 1000); - } - self.checkMempool(); - } - - stopped: boolean = false; - - stop() { - clearInterval(this.intervalTransactionsProcess); - this.transactionsToProcess = []; - clearInterval(this.intervalMempool); - this.stopped = true; - } - - checkMempool(): boolean { - let self = this; - if (this.lastMaximumHeight - this.lastBlockLoading > 1) {//only check memory pool if the user is up to date to ensure outs & ins will be found in the wallet - return false; - } - - this.wallet.txsMem = []; - this.explorer.getTransactionPool().then(function (pool: any) { - if (typeof pool !== 'undefined') - for (let rawTx of pool) { - let tx = TransactionsExplorer.parse(rawTx, self.wallet); - if (tx !== null) { - self.wallet.txsMem.push(tx); - } - } - }).catch(function () { }); - return true; - } - - terminateWorker() { - this.workerProcessing.terminate(); - this.workerProcessingReady = false; - this.workerCurrentProcessing = []; - this.workerProcessingWorking = false; - this.workerCountProcessed = 0; - } - - transactionsToProcess: RawDaemonTransaction[] = []; - intervalTransactionsProcess = 0; - - workerProcessing !: Worker; - workerProcessingReady = false; - workerProcessingWorking = false; - workerCurrentProcessing: RawDaemonTransaction[] = []; - workerCountProcessed = 0; - - checkTransactions(rawTransactions: RawDaemonTransaction[]) { - for (let rawTransaction of rawTransactions) { - let height = rawTransaction.blockIndex; - if (typeof height !== 'undefined') { - let transaction = TransactionsExplorer.parse(rawTransaction, this.wallet); - if (transaction !== null) { - this.wallet.addNew(transaction); - } - if (height - this.wallet.lastHeight >= 2) { - this.wallet.lastHeight = height - 1; - } - } - } - if (this.transactionsToProcess.length == 0) { - this.wallet.lastHeight = this.lastBlockLoading; - } - } - - checkTransactionsInterval() { - - //somehow we're repeating and regressing back to re-process Tx's - //loadHistory getting into a stack overflow ? - //need to work out timinings and ensure process does not reload when it's already running... - - if (this.workerProcessingWorking || !this.workerProcessingReady) { - return; - } - - //we destroy the worker in charge of decoding the transactions every 250 transactions to ensure the memory is not corrupted - //cnUtil bug, see https://github.com/mymonero/mymonero-core-js/issues/8 - if (this.workerCountProcessed >= 100) { - //console.log('Recreate worker..'); - this.terminateWorker(); - this.initWorker(); - return; - } - - let transactionsToProcess: RawDaemonTransaction[] = this.transactionsToProcess.splice(0, 25); //process 25 tx's at a time - if (transactionsToProcess.length > 0) { - this.workerCurrentProcessing = transactionsToProcess; - this.workerProcessing.postMessage({ - type: 'process', - transactions: transactionsToProcess - }); - ++this.workerCountProcessed; - this.workerProcessingWorking = true; - } else { - clearInterval(this.intervalTransactionsProcess); - this.intervalTransactionsProcess = 0; - } - } - - processTransactions(transactions: RawDaemonTransaction[]) { - let transactionsToAdd = []; - - for (let tr of transactions) { - if (typeof tr.blockIndex !== 'undefined') - if (tr.blockIndex > this.wallet.lastHeight) { - transactionsToAdd.push(tr); - } - } - - this.transactionsToProcess.push.apply(this.transactionsToProcess, transactionsToAdd); - if (this.intervalTransactionsProcess === 0) { - let self = this; - this.intervalTransactionsProcess = setInterval(function () { - self.checkTransactionsInterval(); - }, this.wallet.options.readSpeed); - } - - } - - - lastBlockLoading = -1; - lastMaximumHeight = 0; - - loadHistory() { - if (this.stopped) return; - - if (this.lastBlockLoading === -1) this.lastBlockLoading = this.wallet.lastHeight; - let self = this; - //don't reload until it's finished processing the last batch of transactions - if (this.workerProcessingWorking || !this.workerProcessingReady) { - setTimeout(function () { - self.loadHistory(); - }, 100); - return; - } - if (this.transactionsToProcess.length > 100) { - //to ensure no pile explosion - setTimeout(function () { - self.loadHistory(); - }, 2 * 1000); - return; - } - - //console.log('checking'); - this.explorer.getHeight().then(function (height) { - //console.log(self.lastBlockLoading,height); - if (height > self.lastMaximumHeight) self.lastMaximumHeight = height; - - if (self.lastBlockLoading !== height) { - let previousStartBlock = Number(self.lastBlockLoading); - let startBlock = Math.floor(self.lastBlockLoading / 100) * 100; - //console.log('=>',self.lastBlockLoading, endBlock, height, startBlock, self.lastBlockLoading); - //console.log('load block from ' + startBlock); - self.explorer.getTransactionsForBlocks(previousStartBlock).then(function (transactions: RawDaemonTransaction[]) { - //to ensure no pile explosion - if (transactions.length > 0) { - let lastTx = transactions[transactions.length - 1]; - if (typeof lastTx.blockIndex !== 'undefined') { - self.lastBlockLoading = lastTx.blockIndex + 1; - } - } - self.processTransactions(transactions); - - setTimeout(function () { - self.loadHistory(); - }, 1); - }).catch(function () { - setTimeout(function () { - self.loadHistory(); - }, 30 * 1000);//retry 30s later if an error occurred - }); - } else { - setTimeout(function () { - self.loadHistory(); - }, 30 * 1000); - } - }).catch(function () { - setTimeout(function () { - self.loadHistory(); - }, 30 * 1000);//retry 30s later if an error occurred - }); - } - - -} - -export class BlockchainExplorerRpc2 implements BlockchainExplorer { - - // testnet : boolean = true; - randInt = Math.floor(Math.random() * Math.floor(config.apiUrl.length)); - serverAddress = config.apiUrl[this.randInt]; - - heightCache = 0; - heightLastTimeRetrieve = 0; - getHeight(): Promise { - if (Date.now() - this.heightLastTimeRetrieve < 20 * 1000 && this.heightCache !== 0) { - return Promise.resolve(this.heightCache); - } - let self = this; - this.heightLastTimeRetrieve = Date.now(); - return new Promise(function (resolve, reject) { - $.ajax({ - url: config.nodeUrl + 'getheight', - method: 'POST', - data: JSON.stringify({ - }) - }).done(function (raw: any) { - self.heightCache = parseInt(raw.height); - resolve(self.heightCache); - }).fail(function (data: any) { - reject(data); - }); - }); - } - - scannedHeight: number = 0; - - getScannedHeight(): number { - return this.scannedHeight; - } - - watchdog(wallet: Wallet): WalletWatchdog { - let watchdog = new WalletWatchdog(wallet, this); - watchdog.loadHistory(); - return watchdog; - } - - getTransactionsForBlocks(start_block: number): Promise { - let self = this; - let transactions: RawDaemonTransaction[] = []; - let startBlock = Number(start_block); - return new Promise(function (resolve, reject) { - let tempHeight; - let operator = 10; - if (self.heightCache - startBlock > operator) { - tempHeight = startBlock + operator; - } else { - tempHeight = self.heightCache; - } - - let blockHeights: number[] = []; - let c = tempHeight - startBlock + 1, th = tempHeight; - while ( c-- ) { - blockHeights[c] = th-- - } - - self.postData(config.nodeUrl + 'json_rpc', { - "jsonrpc": "2.0", - "id": 0, - "method": "getblocksbyheights", - "params": { - "blockHeights": blockHeights - } - }).then(data => { - for (let i = 0; i < data.result.blocks.length; i++) { - let finalTxs: any[] = data.result.blocks[i].transactions; - for (let j = 0; j < finalTxs.length; j++) { - let finalTx = finalTxs[j]; - transactions.push(finalTx); - } - } - resolve(transactions); - }).catch(error => { - console.log('REJECT'); - try { - console.log(JSON.parse(error.responseText)); - } catch (e) { - console.log(e); - } - reject(error); - }); - - }); - } - - getTransactionPool(): Promise { - let self = this; - return new Promise(function (resolve, reject) { - self.postData(config.nodeUrl + 'json_rpc', { - 'jsonrpc': '2.0', - 'id': 0, - 'method': 'gettransactionspool', - 'params': '' - }).then(data => { - let rawTxs = data.result.transactions; - let txHashes: any[] = []; - - for (let iTx = 0; iTx < rawTxs.length; iTx++) { - txHashes.push(rawTxs[iTx].hash); - } - - self.postData(config.nodeUrl + 'json_rpc', { - "jsonrpc": "2.0", - "id": 0, - "method": "gettransactionsbyhashes", - "params": { - "transactionHashes": txHashes - } - }).then(detailTx => { - let response = detailTx.transactions; - if (response !== null) { - resolve(response); - } - }).catch(error => { - console.log('REJECT'); - try { - console.log(JSON.parse(error.responseText)); - } catch (e) { - console.log(e); - } - reject(error); - }); - }); - }); - } - - existingOuts: any[] = []; - getRandomOuts(nbOutsNeeded: number, initialCall = true): Promise { - let self = this; - if (initialCall) { - self.existingOuts = []; - } - - return this.getHeight().then(function (height: number) { - let txs: RawDaemonTransaction[] = []; - let promises = []; - - let randomBlocksIndexesToGet: number[] = []; - let numOuts = height; - - for (let i = 0; i < nbOutsNeeded; ++i) { - let selectedIndex: number = -1; - do { - selectedIndex = MathUtil.randomTriangularSimplified(numOuts); - if (selectedIndex >= height - config.txCoinbaseMinConfirms) - selectedIndex = -1; - } while (selectedIndex === -1 || randomBlocksIndexesToGet.indexOf(selectedIndex) !== -1); - randomBlocksIndexesToGet.push(selectedIndex); - - let promise = self.getTransactionsForBlocks(Math.floor(selectedIndex / 100) * 100).then(function (rawTransactions: RawDaemonTransaction[]) { - txs.push.apply(txs, rawTransactions); - }); - promises.push(promise); - } - - return Promise.all(promises).then(function () { - let txCandidates: any = {}; - for (let iOut = 0; iOut < txs.length; ++iOut) { - let tx = txs[iOut]; - - if ( - (typeof tx.blockIndex !== 'undefined' && randomBlocksIndexesToGet.indexOf(tx.blockIndex) === -1) || - typeof tx.blockIndex === 'undefined' - ) { - continue; - } - - for (let output_idx_in_tx = 0; output_idx_in_tx < tx.outputs.length; ++output_idx_in_tx) { - //let globalIndex = output_idx_in_tx; - //if (typeof tx.global_index_start !== 'undefined') - // globalIndex += tx.global_index_start; - let globalIndex = tx.outputs[output_idx_in_tx].globalIndex; - - let newOut = { - public_key: tx.outputs[output_idx_in_tx].output.target.data.key, - global_index: globalIndex, - // global_index: count, - }; - if (typeof txCandidates[tx.blockIndex] === 'undefined') txCandidates[tx.blockIndex] = []; - txCandidates[tx.blockIndex].push(newOut); - } - } - - //console.log(txCandidates); - - let selectedOuts = []; - for (let txsOutsHeight in txCandidates) { - let outIndexSelect = MathUtil.getRandomInt(0, txCandidates[txsOutsHeight].length - 1); - //console.log('select ' + outIndexSelect + ' for ' + txsOutsHeight + ' with length of ' + txCandidates[txsOutsHeight].length); - selectedOuts.push(txCandidates[txsOutsHeight][outIndexSelect]); - } - - //console.log(selectedOuts); - - return selectedOuts; - }); - }); - } - - sendRawTx(rawTx: string) { - let self = this; - return new Promise(function (resolve, reject) { - self.postData(config.nodeUrl + 'sendrawtransaction', { - tx_as_hex: rawTx, - do_not_relay: false - }).then(transactions => { - if (transactions.status && transactions.status == 'OK') { - resolve(transactions); - } else { - reject(transactions); - } - }).catch(error => { - reject(error); - }); - }); - } - - resolveOpenAlias(domain: string): Promise<{ address: string, name: string | null }> { - let self = this; - return new Promise(function (resolve, reject) { - self.postData(config.nodeUrl + 'json_rpc', { - "jsonrpc": "2.0", - "id": 0, - "method": "resolveopenalias", - "params": { - "url": domain - } - }).then(data => { - resolve(data.result); - }).catch(error => { - console.log('REJECT'); - try { - console.log(JSON.parse(error.responseText)); - } catch (e) { - console.log(e); - } - reject(error); - }); - }); - } - - async postData(url: string, data: any) { - const response = await fetch(url, { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(data) - }); - - return response.json(); - } - -} diff --git a/src/pages/changeWalletPassword.ts b/src/pages/changeWalletPassword.ts index e02f676e..2a6e3c0e 100644 --- a/src/pages/changeWalletPassword.ts +++ b/src/pages/changeWalletPassword.ts @@ -17,16 +17,17 @@ import {DestructableView} from "../lib/numbersLab/DestructableView"; import {VueVar, VueWatched} from "../lib/numbersLab/VueAnnotate"; import {TransactionsExplorer} from "../model/TransactionsExplorer"; import {WalletRepository} from "../model/WalletRepository"; -import {BlockchainExplorerRpc2, WalletWatchdog} from "../model/blockchain/BlockchainExplorerRpc2"; import {DependencyInjectorInstance} from "../lib/numbersLab/DependencyInjector"; import {Constants} from "../model/Constants"; import {Wallet} from "../model/Wallet"; import {AppState, WalletWorker} from "../model/AppState"; import {Password} from "../model/Password"; import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; +import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; +import {WalletWatchdog} from "../model/WalletWatchdog"; let wallet : Wallet = DependencyInjectorInstance().getInstance(Wallet.name, 'default', false); -let blockchainExplorer : BlockchainExplorerRpc2 = BlockchainExplorerProvider.getInstance(); +let blockchainExplorer : BlockchainExplorer = BlockchainExplorerProvider.getInstance(); let walletWatchdog : WalletWatchdog = DependencyInjectorInstance().getInstance(WalletWatchdog.name,'default', false); class ChangeWalletPasswordView extends DestructableView{ diff --git a/src/pages/createWallet.ts b/src/pages/createWallet.ts index 570868d2..0fd910aa 100644 --- a/src/pages/createWallet.ts +++ b/src/pages/createWallet.ts @@ -17,15 +17,16 @@ import {DestructableView} from "../lib/numbersLab/DestructableView"; import {KeysRepository} from "../model/KeysRepository"; import {Wallet} from "../model/Wallet"; import {Password} from "../model/Password"; -import {BlockchainExplorerRpc2} from "../model/blockchain/BlockchainExplorerRpc2"; import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; import {Mnemonic} from "../model/Mnemonic"; import {AppState} from "../model/AppState"; import {WalletRepository} from "../model/WalletRepository"; import {Translations} from "../model/Translations"; import {MnemonicLang} from "../model/MnemonicLang"; +import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; +import {Cn, CnNativeBride, CnRandom} from "../model/Cn"; -let blockchainExplorer : BlockchainExplorerRpc2 = BlockchainExplorerProvider.getInstance(); +let blockchainExplorer : BlockchainExplorer = BlockchainExplorerProvider.getInstance(); class CreateViewWallet extends DestructableView{ @VueVar(0) step !: number; @@ -54,8 +55,8 @@ class CreateViewWallet extends DestructableView{ let self = this; setTimeout(function(){ blockchainExplorer.getHeight().then(function(currentHeight){ - let seed = cnUtil.sc_reduce32(cnUtil.rand_32()); - let keys = cnUtil.create_address(seed); + let seed = CnNativeBride.sc_reduce32(CnRandom.rand_32()); + let keys = Cn.create_address(seed); let newWallet = new Wallet(); newWallet.keys = KeysRepository.fromPriv(keys.spend.sec, keys.view.sec); @@ -111,8 +112,8 @@ class CreateViewWallet extends DestructableView{ confirmButtonText: 'Yes' }).then((result:{value:boolean}) => { if (result.value) {*/ - self.forceInsecurePassword = true; - // } + self.forceInsecurePassword = true; + // } // }); } diff --git a/src/pages/importFromFile.ts b/src/pages/importFromFile.ts index f8a6646a..2b17852b 100644 --- a/src/pages/importFromFile.ts +++ b/src/pages/importFromFile.ts @@ -21,14 +21,14 @@ import {Password} from "../model/Password"; import {Wallet} from "../model/Wallet"; import {KeysRepository} from "../model/KeysRepository"; import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; -import {BlockchainExplorerRpc2} from "../model/blockchain/BlockchainExplorerRpc2"; import {Mnemonic} from "../model/Mnemonic"; import {MnemonicLang} from "../model/MnemonicLang"; import {WalletRepository} from "../model/WalletRepository"; +import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; AppState.enableLeftMenu(); -let blockchainExplorer : BlockchainExplorerRpc2 = BlockchainExplorerProvider.getInstance(); +let blockchainExplorer : BlockchainExplorer = BlockchainExplorerProvider.getInstance(); class ImportView extends DestructableView{ @VueVar('') password !: string; diff --git a/src/pages/importFromKeys.ts b/src/pages/importFromKeys.ts index 97abcfa3..acb30ab3 100644 --- a/src/pages/importFromKeys.ts +++ b/src/pages/importFromKeys.ts @@ -21,11 +21,13 @@ import {Password} from "../model/Password"; import {Wallet} from "../model/Wallet"; import {KeysRepository} from "../model/KeysRepository"; import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; -import {BlockchainExplorerRpc2} from "../model/blockchain/BlockchainExplorerRpc2"; +import {Constants} from "../model/Constants"; +import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; +import {Cn, CnUtils} from "../model/Cn"; AppState.enableLeftMenu(); -let blockchainExplorer : BlockchainExplorerRpc2 = BlockchainExplorerProvider.getInstance(); +let blockchainExplorer : BlockchainExplorer = BlockchainExplorerProvider.getInstance(); class ImportView extends DestructableView{ @VueVar(false) viewOnly !: boolean; @@ -67,7 +69,7 @@ class ImportView extends DestructableView{ blockchainExplorer.getHeight().then(function(currentHeight){ let newWallet = new Wallet(); if(self.viewOnly){ - let decodedPublic = cnUtil.decode_address(self.publicAddress.trim()); + let decodedPublic = Cn.decode_address(self.publicAddress.trim()); newWallet.keys = { priv:{ spend:'', @@ -78,11 +80,11 @@ class ImportView extends DestructableView{ view:decodedPublic.view, } }; - }else { + } else { //console.log(1); let viewkey = self.privateViewKey.trim(); - if(viewkey === ''){ - viewkey = cnUtil.generate_keys(cnUtil.cn_fast_hash(self.privateSpendKey.trim())).sec; + if(viewkey === '') { + viewkey = Cn.generate_keys(CnUtils.cn_fast_hash(self.privateSpendKey.trim())).sec; } //console.log(1, viewkey); newWallet.keys = KeysRepository.fromPriv(self.privateSpendKey.trim(), viewkey); @@ -135,7 +137,7 @@ class ImportView extends DestructableView{ @VueWatched() publicAddressWatch(){ try{ - cnUtil.decode_address(this.publicAddress.trim()); + Cn.decode_address(this.publicAddress.trim()); this.validPublicAddress = true; }catch(e){ this.validPublicAddress = false; diff --git a/src/pages/importFromMnemonic.ts b/src/pages/importFromMnemonic.ts index c7e45ec6..e74a1cda 100644 --- a/src/pages/importFromMnemonic.ts +++ b/src/pages/importFromMnemonic.ts @@ -21,15 +21,15 @@ import {Password} from "../model/Password"; import {Wallet} from "../model/Wallet"; import {KeysRepository} from "../model/KeysRepository"; import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; -import {BlockchainExplorerRpc2} from "../model/blockchain/BlockchainExplorerRpc2"; import {Mnemonic} from "../model/Mnemonic"; -import {MnemonicLang} from "../model/MnemonicLang"; +import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; +import {Cn} from "../model/Cn"; AppState.enableLeftMenu(); -let blockchainExplorer : BlockchainExplorerRpc2 = BlockchainExplorerProvider.getInstance(); +let blockchainExplorer: BlockchainExplorer = BlockchainExplorerProvider.getInstance(); -class ImportView extends DestructableView{ +class ImportView extends DestructableView { @VueVar('') password !: string; @VueVar('') password2 !: string; @VueVar(false) insecurePassword !: boolean; @@ -39,73 +39,72 @@ class ImportView extends DestructableView{ @VueVar('') mnemonicPhrase !: string; @VueVar('') validMnemonicPhrase !: boolean; @VueVar('') language !: string; - @VueVar([]) languages !: {key:string,name:string}[]; + @VueVar([]) languages !: { key: string, name: string }[]; - constructor(container : string){ + constructor(container: string) { super(container); - this.languages.push({key:'auto',name:'Detect automatically'}); - this.languages.push({key:'english',name:'English'}); - this.languages.push({key:'chinese',name:'Chinese (simplified)'}); - this.languages.push({key:'dutch',name:'Dutch'}); - this.languages.push({key:'electrum',name:'Electrum'}); - this.languages.push({key:'esperanto',name:'Esperanto'}); - this.languages.push({key:'french',name:'French'}); - this.languages.push({key:'italian',name:'Italian'}); - this.languages.push({key:'japanese',name:'Japanese'}); - this.languages.push({key:'lojban',name:'Lojban'}); - this.languages.push({key:'portuguese',name:'Portuguese'}); - this.languages.push({key:'russian',name:'Russian'}); - this.languages.push({key:'spanish',name:'Spanish'}); + this.languages.push({key: 'auto', name: 'Detect automatically'}); + this.languages.push({key: 'english', name: 'English'}); + this.languages.push({key: 'chinese', name: 'Chinese (simplified)'}); + this.languages.push({key: 'dutch', name: 'Dutch'}); + this.languages.push({key: 'electrum', name: 'Electrum'}); + this.languages.push({key: 'esperanto', name: 'Esperanto'}); + this.languages.push({key: 'french', name: 'French'}); + this.languages.push({key: 'italian', name: 'Italian'}); + this.languages.push({key: 'japanese', name: 'Japanese'}); + this.languages.push({key: 'lojban', name: 'Lojban'}); + this.languages.push({key: 'portuguese', name: 'Portuguese'}); + this.languages.push({key: 'russian', name: 'Russian'}); + this.languages.push({key: 'spanish', name: 'Spanish'}); this.language = 'auto'; } - formValid(){ - if(this.password != this.password2) + formValid() { + if (this.password != this.password2) return false; - if(!(this.password !== '' && (!this.insecurePassword || this.forceInsecurePassword))) + if (!(this.password !== '' && (!this.insecurePassword || this.forceInsecurePassword))) return false; - if(!this.validMnemonicPhrase) + if (!this.validMnemonicPhrase) return false; return true; } - importWallet(){ + importWallet() { let self = this; - blockchainExplorer.getHeight().then(function(currentHeight){ + blockchainExplorer.getHeight().then(function (currentHeight) { let newWallet = new Wallet(); let mnemonic = self.mnemonicPhrase.trim(); // let current_lang = 'english'; let current_lang = 'english'; - if(self.language === 'auto') { + if (self.language === 'auto') { let detectedLang = Mnemonic.detectLang(self.mnemonicPhrase.trim()); - if(detectedLang !== null) + if (detectedLang !== null) current_lang = detectedLang; - } - else + } else current_lang = self.language; let mnemonic_decoded = Mnemonic.mn_decode(mnemonic, current_lang); - if(mnemonic_decoded !== null) { - let keys = cnUtil.create_address(mnemonic_decoded); + if (mnemonic_decoded !== null) { + let keys = Cn.create_address(mnemonic_decoded); let newWallet = new Wallet(); newWallet.keys = KeysRepository.fromPriv(keys.spend.sec, keys.view.sec); let height = self.importHeight - 10; if (height < 0) height = 0; - if(height > currentHeight)height = currentHeight; + if (height > currentHeight) height = currentHeight; newWallet.lastHeight = height; newWallet.creationHeight = newWallet.lastHeight; AppState.openWallet(newWallet, self.password); window.location.href = '#account'; - }else{ + } else { swal({ type: 'error', title: i18n.t('global.invalidMnemonicModal.title'), @@ -118,46 +117,46 @@ class ImportView extends DestructableView{ } @VueWatched() - passwordWatch(){ - if(!Password.checkPasswordConstraints(this.password, false)){ + passwordWatch() { + if (!Password.checkPasswordConstraints(this.password, false)) { this.insecurePassword = true; - }else + } else this.insecurePassword = false; } @VueWatched() - importHeightWatch(){ - if((this.importHeight) === '')this.importHeight = 0; - if(this.importHeight < 0){ + importHeightWatch() { + if ((this.importHeight) === '') this.importHeight = 0; + if (this.importHeight < 0) { this.importHeight = 0; } - this.importHeight = parseInt(''+this.importHeight); + this.importHeight = parseInt('' + this.importHeight); } @VueWatched() - mnemonicPhraseWatch(){ + mnemonicPhraseWatch() { this.checkMnemonicValidity(); } @VueWatched() - languageWatch(){ + languageWatch() { this.checkMnemonicValidity(); } - checkMnemonicValidity(){ + checkMnemonicValidity() { let splitted = this.mnemonicPhrase.trim().split(' '); - if(splitted.length != 25){ + if (splitted.length != 25) { this.validMnemonicPhrase = false; - }else { + } else { let detected = Mnemonic.detectLang(this.mnemonicPhrase.trim()); - if(this.language === 'auto') + if (this.language === 'auto') this.validMnemonicPhrase = detected !== null; else this.validMnemonicPhrase = detected === this.language; } } - forceInsecurePasswordCheck(){ + forceInsecurePasswordCheck() { let self = this; self.forceInsecurePassword = true; } diff --git a/src/pages/importFromQr.ts b/src/pages/importFromQr.ts index df2ce5e7..e0ba52a8 100644 --- a/src/pages/importFromQr.ts +++ b/src/pages/importFromQr.ts @@ -21,14 +21,16 @@ import {Password} from "../model/Password"; import {Wallet} from "../model/Wallet"; import {KeysRepository} from "../model/KeysRepository"; import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; -import {BlockchainExplorerRpc2} from "../model/blockchain/BlockchainExplorerRpc2"; import {QRReader} from "../model/QRReader"; import {CoinUri} from "../model/CoinUri"; import {Mnemonic} from "../model/Mnemonic"; +import {Constants} from "../model/Constants"; +import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; +import {Cn, CnUtils} from "../model/Cn"; AppState.enableLeftMenu(); -let blockchainExplorer : BlockchainExplorerRpc2 = BlockchainExplorerProvider.getInstance(); +let blockchainExplorer : BlockchainExplorer = BlockchainExplorerProvider.getInstance(); class ImportView extends DestructableView{ @VueVar('') password !: string; @@ -70,7 +72,7 @@ class ImportView extends DestructableView{ if(detectedMnemonicLang !== null){ let mnemonic_decoded = Mnemonic.mn_decode(self.mnemonicSeed, detectedMnemonicLang); if(mnemonic_decoded !== null) { - let keys = cnUtil.create_address(mnemonic_decoded); + let keys = Cn.create_address(mnemonic_decoded); newWallet.keys = KeysRepository.fromPriv(keys.spend.sec, keys.view.sec); }else{ swal({ @@ -93,12 +95,12 @@ class ImportView extends DestructableView{ }else if(self.privateSpendKey !== null){ let viewkey = self.privateViewKey !== null ? self.privateViewKey : ''; if(viewkey === ''){ - viewkey = cnUtil.generate_keys(cnUtil.cn_fast_hash(self.privateSpendKey)).sec; + viewkey = Cn.generate_keys(CnUtils.cn_fast_hash(self.privateSpendKey)).sec; } newWallet.keys = KeysRepository.fromPriv(self.privateSpendKey, viewkey); }else if(self.privateSpendKey === null && self.privateViewKey !== null && self.publicAddress !== null){ - let decodedPublic = cnUtil.decode_address(self.publicAddress); + let decodedPublic = Cn.decode_address(self.publicAddress); newWallet.keys = { priv:{ spend:'', diff --git a/src/pages/network.ts b/src/pages/network.ts index 2854e5d8..76e47471 100644 --- a/src/pages/network.ts +++ b/src/pages/network.ts @@ -14,18 +14,20 @@ */ import {DestructableView} from "../lib/numbersLab/DestructableView"; -import {VueVar} from "../lib/numbersLab/VueAnnotate"; -import {TransactionsExplorer} from "../model/TransactionsExplorer"; -import {WalletRepository} from "../model/WalletRepository"; -import {BlockchainExplorerRpc2} from "../model/blockchain/BlockchainExplorerRpc2"; -import {DependencyInjectorInstance} from "../lib/numbersLab/DependencyInjector"; +import {VueVar, VueRequireFilter} from "../lib/numbersLab/VueAnnotate"; import {Constants} from "../model/Constants"; import {Wallet} from "../model/Wallet"; import {AppState} from "../model/AppState"; +import {BlockchainExplorer, NetworkInfo} from "../model/blockchain/BlockchainExplorer"; +import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; +import {VueFilterHashrate} from "../filters/Filters"; AppState.enableLeftMenu(); +let blockchainExplorer: BlockchainExplorer = BlockchainExplorerProvider.getInstance(); -class NetworkView extends DestructableView{ +@VueRequireFilter('hashrate', VueFilterHashrate) + +class NetworkView extends DestructableView { @VueVar(0) networkHashrate !: number; @VueVar(0) blockchainHeight !: number; @VueVar(0) networkDifficulty !: number; @@ -35,13 +37,13 @@ class NetworkView extends DestructableView{ private intervalRefreshStat = 0; - constructor(container : string){ + constructor(container: string) { super(container); let self = this; - this.intervalRefreshStat = setInterval(function(){ + this.intervalRefreshStat = setInterval(function () { self.refreshStats(); - }, 30*1000); + }, 30 * 1000); this.refreshStats(); } @@ -51,29 +53,16 @@ class NetworkView extends DestructableView{ } refreshStats() { - let self = this; - let randInt = Math.floor(Math.random() * Math.floor(config.nodeList.length)); - $.ajax({ - url:config.nodeUrl+'json_rpc', - method: 'POST', - data: JSON.stringify( - { - "jsonrpc": "2.0", - "id": 0, - "method": "getlastblockheader", - "params": {} - } - ) - }).done(function(data : any){ - self.networkDifficulty = data['result']['block_header'].difficulty; - self.networkHashrate = parseFloat((data['result']['block_header'].difficulty/config.avgBlockTime/1000000).toFixed(2)); - self.blockchainHeight = data['result']['block_header'].height; - self.lastReward = data['result']['block_header'].reward/Math.pow(10, config.coinUnitPlaces); - self.lastBlockFound = parseInt(data['result']['block_header'].timestamp); - self.connectedNode = config.nodeUrl; + blockchainExplorer.getNetworkInfo().then((info: NetworkInfo) => { + //console.log(info); + this.connectedNode = info.node; + this.networkDifficulty = info.difficulty; + this.networkHashrate = info.difficulty / config.avgBlockTime; + this.blockchainHeight = info.height; + this.lastReward = info.reward / Math.pow(10, config.coinUnitPlaces); + this.lastBlockFound = info.timestamp; }); } - } new NetworkView('#app'); diff --git a/src/pages/receive.ts b/src/pages/receive.ts index fc2c53bf..4fc7242d 100644 --- a/src/pages/receive.ts +++ b/src/pages/receive.ts @@ -20,6 +20,7 @@ import {Constants} from "../model/Constants"; import {VueVar, VueWatched} from "../lib/numbersLab/VueAnnotate"; import {CoinUri} from "../model/CoinUri"; import {Nfc} from "../model/Nfc"; +import {Cn} from "../model/Cn"; let wallet : Wallet = DependencyInjectorInstance().getInstance(Wallet.name,'default', false); @@ -91,7 +92,7 @@ class AccountView extends DestructableView{ if(this.paymentId !== '' && this.paymentId.length <= 8) { let paymentId8 = ('00000000'+this.stringToHex(this.paymentId)).slice(-16); //console.log(paymentId8+'==>'+this.stringToHex(this.paymentId)); - this.address = cnUtil.get_account_integrated_address(wallet.getPublicAddress(), paymentId8); + this.address = Cn.get_account_integrated_address(wallet.getPublicAddress(), paymentId8); }else this.address = wallet.getPublicAddress(); } @@ -197,4 +198,4 @@ class AccountView extends DestructableView{ if(wallet !== null) new AccountView('#app'); else - window.location.href = '#index'; \ No newline at end of file + window.location.href = '#index'; diff --git a/src/pages/send.ts b/src/pages/send.ts index e38fb72f..f3d3c6b0 100644 --- a/src/pages/send.ts +++ b/src/pages/send.ts @@ -16,21 +16,20 @@ import {DestructableView} from "../lib/numbersLab/DestructableView"; import {VueRequireFilter, VueVar, VueWatched} from "../lib/numbersLab/VueAnnotate"; import {TransactionsExplorer} from "../model/TransactionsExplorer"; -import {WalletRepository} from "../model/WalletRepository"; -import {BlockchainExplorerRpc2, WalletWatchdog} from "../model/blockchain/BlockchainExplorerRpc2"; import {Autowire, DependencyInjectorInstance} from "../lib/numbersLab/DependencyInjector"; -import {Constants} from "../model/Constants"; import {Wallet} from "../model/Wallet"; -import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; import {Url} from "../utils/Url"; import {CoinUri} from "../model/CoinUri"; import {QRReader} from "../model/QRReader"; import {AppState} from "../model/AppState"; import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; import {NdefMessage, Nfc} from "../model/Nfc"; +import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; +import {Cn} from "../model/Cn"; +import {WalletWatchdog} from "../model/WalletWatchdog"; let wallet: Wallet = DependencyInjectorInstance().getInstance(Wallet.name, 'default', false); -let blockchainExplorer: BlockchainExplorerRpc2 = BlockchainExplorerProvider.getInstance(); +let blockchainExplorer: BlockchainExplorer = BlockchainExplorerProvider.getInstance(); AppState.enableLeftMenu(); @@ -291,10 +290,10 @@ class SendView extends DestructableView { }).catch(reject); }, 1); }); - }).then(function (rawTxData: { raw: { hash: string, prvKey: string, raw: string }, signed: any }) { + }).then(function (rawTxData: { raw: { hash: string, prvkey: string, raw: string }, signed: any }) { blockchainExplorer.sendRawTx(rawTxData.raw.raw).then(function () { //save the tx private key - wallet.addTxPrivateKeyWithTxHash(rawTxData.raw.hash, rawTxData.raw.prvKey); + wallet.addTxPrivateKeyWithTxHash(rawTxData.raw.hash, rawTxData.raw.prvkey); //force a mempool check so the user is up to date let watchdog: WalletWatchdog = DependencyInjectorInstance().getInstance(WalletWatchdog.name); @@ -376,7 +375,7 @@ class SendView extends DestructableView { this.timeoutResolveAlias = setTimeout(function () { blockchainExplorer.resolveOpenAlias(self.destinationAddressUser).then(function (data: { address: string, name: string | null }) { try { - // cnUtil.decode_address(data.address); + Cn.decode_address(data.address); self.txDestinationName = data.name; self.destinationAddress = data.address; self.domainAliasAddress = data.address; @@ -395,7 +394,7 @@ class SendView extends DestructableView { } else { this.openAliasValid = true; try { - cnUtil.decode_address(this.destinationAddressUser); + Cn.decode_address(this.destinationAddressUser); this.destinationAddressValid = true; this.destinationAddress = this.destinationAddressUser; } catch (e) { diff --git a/src/pages/settings.html b/src/pages/settings.html index 199d59e5..b601726b 100644 --- a/src/pages/settings.html +++ b/src/pages/settings.html @@ -73,10 +73,10 @@ - + diff --git a/src/pages/settings.ts b/src/pages/settings.ts index 7db5c0bf..46588ad4 100644 --- a/src/pages/settings.ts +++ b/src/pages/settings.ts @@ -17,7 +17,6 @@ import {DestructableView} from "../lib/numbersLab/DestructableView"; import {VueVar, VueWatched} from "../lib/numbersLab/VueAnnotate"; import {TransactionsExplorer} from "../model/TransactionsExplorer"; import {WalletRepository} from "../model/WalletRepository"; -import {BlockchainExplorerRpc2, WalletWatchdog} from "../model/blockchain/BlockchainExplorerRpc2"; import {DependencyInjectorInstance} from "../lib/numbersLab/DependencyInjector"; import {Constants} from "../model/Constants"; import {Wallet} from "../model/Wallet"; @@ -25,9 +24,11 @@ import {AppState} from "../model/AppState"; import {Storage} from "../model/Storage"; import {Translations} from "../model/Translations"; import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; +import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; +import {WalletWatchdog} from "../model/WalletWatchdog"; let wallet : Wallet = DependencyInjectorInstance().getInstance(Wallet.name, 'default', false); -let blockchainExplorer : BlockchainExplorerRpc2 = BlockchainExplorerProvider.getInstance(); +let blockchainExplorer: BlockchainExplorer = BlockchainExplorerProvider.getInstance(); let walletWatchdog : WalletWatchdog = DependencyInjectorInstance().getInstance(WalletWatchdog.name,'default', false); class SendView extends DestructableView{ @@ -46,7 +47,7 @@ class SendView extends DestructableView{ @VueVar(0) nativeVersionCode !: number; @VueVar('') nativeVersionNumber !: string; - constructor(container : string){ + constructor(container : string) { super(container); let self = this; this.readSpeed = wallet.options.readSpeed; @@ -103,16 +104,16 @@ class SendView extends DestructableView{ @VueWatched() checkMinerTxWatch(){this.updateWalletOptions();} @VueWatched() customNodeWatch(){this.updateWalletOptions();} - @VueWatched() creationHeightWatch(){ + @VueWatched() creationHeightWatch() { if(this.creationHeight < 0)this.creationHeight = 0; if(this.creationHeight > this.maxHeight && this.maxHeight !== -1)this.creationHeight = this.maxHeight; } - @VueWatched() scanHeightWatch(){ + @VueWatched() scanHeightWatch() { if(this.scanHeight < 0)this.scanHeight = 0; if(this.scanHeight > this.maxHeight && this.maxHeight !== -1)this.scanHeight = this.maxHeight; } - private updateWalletOptions(){ + private updateWalletOptions() { let options = wallet.options; options.readSpeed = this.readSpeed; options.checkMinerTx = this.checkMinerTx; @@ -122,16 +123,17 @@ class SendView extends DestructableView{ walletWatchdog.signalWalletUpdate(); } - updateWalletSettings(){ + updateWalletSettings() { wallet.creationHeight = this.creationHeight; wallet.lastHeight = this.scanHeight; walletWatchdog.signalWalletUpdate(); } - updateConnectionSettings(){ + updateConnectionSettings() { let options = wallet.options; options.customNode = this.customNode; options.nodeUrl = this.nodeUrl; + config.nodeUrl = this.nodeUrl; wallet.options = options; walletWatchdog.signalWalletUpdate(); } diff --git a/src/pages/support.ts b/src/pages/support.ts index 57fe8ec0..7323ed75 100644 --- a/src/pages/support.ts +++ b/src/pages/support.ts @@ -12,14 +12,7 @@ * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import {VueVar, VueWatched} from "../lib/numbersLab/VueAnnotate"; import {DestructableView} from "../lib/numbersLab/DestructableView"; -import {KeysRepository} from "../model/KeysRepository"; -import {Wallet} from "../model/Wallet"; -import {Password} from "../model/Password"; -import {BlockchainExplorerRpc2} from "../model/blockchain/BlockchainExplorerRpc2"; -import {BlockchainExplorerProvider} from "../providers/BlockchainExplorerProvider"; -import {Mnemonic} from "../model/Mnemonic"; import {AppState} from "../model/AppState"; class SupportView extends DestructableView{ @@ -31,4 +24,4 @@ class SupportView extends DestructableView{ } -new SupportView('#app'); \ No newline at end of file +new SupportView('#app'); diff --git a/src/providers/BlockchainExplorerProvider.ts b/src/providers/BlockchainExplorerProvider.ts index 8869c31c..930a7ed8 100644 --- a/src/providers/BlockchainExplorerProvider.ts +++ b/src/providers/BlockchainExplorerProvider.ts @@ -15,17 +15,18 @@ import {Constants} from "../model/Constants"; import {DependencyInjectorInstance} from "../lib/numbersLab/DependencyInjector"; -import {BlockchainExplorerRpc2} from "../model/blockchain/BlockchainExplorerRpc2"; +import {BlockchainExplorer} from "../model/blockchain/BlockchainExplorer"; +import {BlockchainExplorerRpcDaemon} from "../model/blockchain/BlockchainExplorerRPCDaemon"; -export class BlockchainExplorerProvider{ +export class BlockchainExplorerProvider { - static getInstance() : BlockchainExplorerRpc2{ - let blockchainExplorer : BlockchainExplorerRpc2 = DependencyInjectorInstance().getInstance(Constants.BLOCKCHAIN_EXPLORER); + static getInstance() : BlockchainExplorer { + let blockchainExplorer : BlockchainExplorer = DependencyInjectorInstance().getInstance(Constants.BLOCKCHAIN_EXPLORER); if(blockchainExplorer === null) { - blockchainExplorer = new BlockchainExplorerRpc2(); + blockchainExplorer = new BlockchainExplorerRpcDaemon(); DependencyInjectorInstance().register(Constants.BLOCKCHAIN_EXPLORER, blockchainExplorer); } return blockchainExplorer; } -} \ No newline at end of file +} diff --git a/src/workers/TransferProcessing.ts b/src/workers/TransferProcessing.ts index dc90f8f2..0aa90b40 100644 --- a/src/workers/TransferProcessing.ts +++ b/src/workers/TransferProcessing.ts @@ -2,53 +2,63 @@ import {TransactionsExplorer} from "../model/TransactionsExplorer"; import {Wallet, WalletOptions} from "../model/Wallet"; import {Mnemonic} from "../model/Mnemonic"; import {Transaction} from "../model/Transaction"; +import {Constants} from "../model/Constants"; +import {RawDaemon_Transaction} from "../model/blockchain/BlockchainExplorer"; //bridge for cnUtil with the new mnemonic class (self).mn_random = Mnemonic.mn_random; (self).mn_decode = Mnemonic.mn_decode; (self).mn_encode = Mnemonic.mn_encode; -let currentWallet : Wallet|null = null; +let currentWallet: Wallet | null = null; -onmessage = function(data : MessageEvent){ +onmessage = function (data: MessageEvent) { // if(data.isTrusted){ - let event : any = data.data; - if(event.type === 'initWallet'){ + let event: any = data.data; + if (event.type === 'initWallet') { + currentWallet = Wallet.loadFromRaw(event.wallet); + postMessage('readyWallet'); + } else if (event.type === 'process') { + if (typeof event.wallet !== 'undefined') { currentWallet = Wallet.loadFromRaw(event.wallet); - postMessage('readyWallet'); - }else if (event.type === 'process'){ - if(typeof event.wallet !== 'undefined'){ - currentWallet = Wallet.loadFromRaw(event.wallet); - } + } + + if (currentWallet === null) { + postMessage('missing_wallet'); + return; + } - if(currentWallet === null){ - postMessage('missing_wallet'); - return; + let readMinersTx = typeof currentWallet.options.checkMinerTx !== 'undefined' && currentWallet.options.checkMinerTx; + + let rawTransactions: RawDaemon_Transaction[] = event.transactions; + let transactions: any[] = []; + + for (let rawTransaction of rawTransactions) { + if (!readMinersTx && TransactionsExplorer.isMinerTx(rawTransaction)) { + continue; } - let rawTransactions : RawDaemonTransaction[] = event.transactions; - let transactions: any[] = []; - - for(let rawTransaction of rawTransactions){ - let transaction = TransactionsExplorer.parse(rawTransaction, currentWallet); - if(transaction !== null){ - currentWallet.addNew(transaction); - //if (transaction.getAmount() !== 0) //fusion - //{ - transactions.push(transaction.export()); - //} - } + let transaction = TransactionsExplorer.parse(rawTransaction, currentWallet); + if (transaction !== null) { + //console.log(`parsed tx ${transaction['hash']} from rawTransaction`); } + if (transaction !== null) { + currentWallet.addNew(transaction); + //console.log(`Added tx ${transaction.hash} to currentWallet`); - postMessage({ - type:'processed', - transactions:transactions - }); + transactions.push(transaction.export()); + //console.log(`pushed tx ${transaction.hash} to transactions[]`); + } } - // let transaction = TransactionsExplorer.parse(rawTransaction, height, this.wallet); + postMessage({ + type: 'processed', + transactions: transactions + }); + } + // let transaction = TransactionsExplorer.parse(rawTransaction, height, this.wallet); // }else { // console.warn('Non trusted data', data.data, JSON.stringify(data.data)); // } }; -postMessage('ready'); \ No newline at end of file +postMessage('ready'); diff --git a/src/workers/TransferProcessingEntrypoint.ts b/src/workers/TransferProcessingEntrypoint.ts index 047ea152..a8630da4 100644 --- a/src/workers/TransferProcessingEntrypoint.ts +++ b/src/workers/TransferProcessingEntrypoint.ts @@ -7,9 +7,7 @@ importScripts('../lib/require.js'); importScripts('../lib/biginteger.js'); importScripts('../config.js'); importScripts('../lib/base58.js'); -importScripts('../lib/cn_utils.js'); importScripts('../lib/crypto.js'); -// importScripts('../lib/mnemonic.js'); importScripts('../lib/nacl-fast.js'); importScripts('../lib/nacl-util.min.js'); importScripts('../lib/sha3.js'); @@ -19,8 +17,8 @@ try { (self).Module_native['onRuntimeInitialized'] = function () { requirejs(['./TransferProcessing.js'], function (App) {}); }; -}catch(e){ +}catch(e) { setTimeout(function(){//wait 5s due to crypto //TODO find a better fix requirejs(['./TransferProcessing.js'], function (App) {}); }, 5*1000); -} \ No newline at end of file +} diff --git a/src_api/blockchain.php b/src_api/blockchain.php deleted file mode 100644 index eef32c12..00000000 --- a/src_api/blockchain.php +++ /dev/null @@ -1,366 +0,0 @@ -$txHashes, - 'decode_as_json'=>true - )); - curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/gettransactions', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body)); - - $resp = curl_exec($curl); - curl_close($curl); - $array = json_decode($resp, true); - - return $array; -} - -function getBlockchainHeight(){ - global $rpcPort; - global $daemonAddress; - $curl = curl_init(); - - curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/getheight', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => '')); - - $resp = curl_exec($curl); - curl_close($curl); - $array = json_decode($resp, true); - return $array['height']; -} - - -$outCount = 0;//to start at 0 - -function createOptimizedBock($startHeight, $endHeight){ - global $outCount; - global $rpcPort; - global $daemonAddress; - $txHashesPerBlock = array(); - $txHashes = array(); - $txHashesMap = array(); - $txOutCountMap = array(); - - $finalTransactions = array(); - $curl = curl_init(); - - $minerTxs = []; - - $blockTimes = array(); - - for($height = $startHeight; $height <= $endHeight; ++$height){ - //get the block hash - $body = json_encode(array("jsonrpc" => "2.0", "id" => "0", "method" => "getblockhash", "params" => array($height))); - curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/json_rpc', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body)); - $resp = curl_exec($curl); - $array = json_decode($resp, true); - $hash = $array["result"]; - //get the block details - $body = json_encode(array("jsonrpc" => "2.0", "id" => "0", "method" => "getblockbyhash", "params" => array("hash" => $hash))); - curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/json_rpc', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body)); - $resp = curl_exec($curl); - $array = json_decode($resp, true); - $blockJson = $array["result"]["block"]; - - // var_dump($array); - //$blockJson = json_decode($array['result']['json'], true); - /* - $minerTx = $blockJson['transactions'][0]; - $minerTx['height'] = $height; - $minerTx['vin'] = []; - $minerTxs[] = $minerTx; - */ - $blockTxHashes = array(); - $blockTimes[$height] = $blockJson['timestamp']; - $txs = $blockJson['transactions']; - foreach($txs as $tx){ - $blockTxHashes[] = $tx["hash"]; - //$tx["block_timestamp"] = $blockJson['timestamp']; - } - $txHashesPerBlock[$height] = $blockTxHashes; - - foreach($blockTxHashes as $txHash){ - $txHashesMap[$txHash] = $height; - $txHashes[] = $txHash; - $txOutCountMap[$txHash] = $outCount; - } - - } - - - for($height = $startHeight; $height <= $endHeight; ++$height){ - /*foreach($minerTxs as $minerTx){ - if($minerTx['height'] === $height){ - $minerTx['global_index_start'] = $outCount; - $minerTx['ts'] = $blockTimes[$height]; - $finalTransactions[] = $minerTx; - ++$outCount; - break; - } - } - */ - $body = json_encode(array( - 'transactionHashes'=>$txHashesPerBlock[$height] - )); - - curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/get_transaction_details_by_hashes', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body)); - - $resp = curl_exec($curl); - - $decodedJson = json_decode($resp, true); - - if(!isset($decodedJson['transactions'])){ - $rawTransactions = []; - }else{ - $rawTransactions = $decodedJson['transactions']; - } - -// var_dump($decodedJson['txs']); -// var_dump($rawTransactions); - - for($iTransaction = 0; $iTransaction < count($rawTransactions); ++$iTransaction){ - //$rawTransactionJson = $rawTransactionsJson[$iTransaction]; - $rawTransaction = $rawTransactions[$iTransaction]; - // var_dump($txHashesMap[$txHashes[$iTransaction]].'<=>'.$height.'=>'.count($rawTransactions)); -// if($txHashesMap[$txHashes[$iTransaction]] === $height){ - // ++$outCount; - $finalTransaction = $rawTransaction; - unset($finalTransaction['signatures']); - unset($finalTransaction['ts']); - unset($finalTransaction['unlockTime']); - unset($finalTransaction['signaturesSize']); - $finalTransaction['global_index_start'] = $outCount; - $finalTransaction['ts'] = $blockJson['timestamp']; //$rawTransaction['block_timestamp']; - $finalTransaction['height'] = $height; - $finalTransaction['hash'] = $rawTransaction['hash']; - // var_dump('-->'.$txHashesMap[$txHashes[$iTransaction]]); - $finalTransactions[] = $finalTransaction; - - - $voutCount = count($finalTransaction['outputs']); - //var_dump('vout of ' . $voutCount . ' at height ' . $finalTransaction["height"]); - $outCount += $voutCount; -// } - } - // var_dump($outCount); - } - - curl_close($curl); - - // return array_merge($finalTransactions,$minerTxs); - return $finalTransactions; -} - -/* -function createOptimizedBock2($startHeight, $endHeight){ - global $rpcPort; - global $outCount; - $txHashesPerBlock = array(); - $txHashes = array(); - $txHashesMap = array(); - - $finalTransactions = array(); - - for($height = $startHeight; $height < $endHeight; ++$height){ - $body = json_encode(array("jsonrpc" => "2.0", "id" => "0", "method" => "getblock", "params" => array("height" => $height))); - - $curl = curl_init(); - - curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/json_rpc', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body)); - - $resp = curl_exec($curl); - curl_close($curl); - $array = json_decode($resp, true); - - // var_dump($array); - $blockJson = json_decode($array['result']['json'], true); - $blockTxHashes = ($blockJson['tx_hashes']); - - $txHashesPerBlock[$height] = $blockTxHashes; - foreach($blockTxHashes as $txHash){ - $txHashesMap[$txHash] = $height; - $txHashes[] = $txHash; - } - - ++$outCount;//minx tx - - if(count($txHashesPerBlock[$height]) > 0){ - $rawTransactions = getTxWithHashes($txHashesPerBlock[$height])['txs_as_json']; - $iTransaction = 0; - foreach($rawTransactions as $rawTransaction){ - ++$outCount; - $finalTransaction = json_decode($rawTransaction, true); - unset($finalTransaction['rctsig_prunable']); - $finalTransaction['height'] = $txHashesPerBlock[$height]; - $finalTransaction['global_index_start'] = $outCount; - $finalTransactions[] = $finalTransaction; - ++$iTransaction; - $outCount+=count($finalTransaction['vout'])-1; - } - } - } - - return $finalTransactions; -} -*/ - -function retrieveCache($startHeight, $endHeight, $decoded=true){ - global $cacheLocation; - $content = @file_get_contents($cacheLocation.'/'.$startHeight.'-'.$endHeight); - if($content === false) - return null; - if($decoded) - $content = json_decode($content, true); - return $content; -} - -function saveCache($startHeight, $endHeight, $content){ - global $cacheLocation; - file_put_contents($cacheLocation.'/'.$startHeight.'-'.$endHeight, json_encode($content)); -} - -//if(getenv('generate') !== 'true'){ -if($_ENV['generate'] !== 'true' || getenv('generate') !== 'true'){ - if(!is_int($_GET['height']+0)){ - http_response_code(400); - exit; - } - $startHeight = (int)$_GET['height']; - $realStartHeight = $startHeight; - $startHeight = floor($startHeight/100)*100; - $endHeight = $startHeight + 100; - if($startHeight < 0) $startHeight = 0; - - $blockchainHeight = getBlockchainHeight(); - if($blockchainHeight === null) $blockchainHeight = $endHeight+100; - if($endHeight > $blockchainHeight){ - $endHeight = $blockchainHeight; - } - - // var_dump($startHeight, $endHeight); - // exit; - $cacheContent = retrieveCache($startHeight, $endHeight, false); - if($cacheContent === null){ - http_response_code(400); - }else{ - $cacheContent = json_decode($cacheContent, true); - $txForUser = []; - foreach($cacheContent as $tx){ - if($tx['height'] >= $realStartHeight){ - $txForUser[] = $tx; - } - } - - header('Content-Type: application/json'); - echo json_encode($txForUser); - } -}else{ - $lastRunStored = @file_get_contents('./lastRun.txt'); - if($lastRunStored===false) - $lastRunStored = 0; - else - $lastRunStored = (int)$lastRunStored; - - if($lastRunStored+1/**60*/ >= time())//concurrent run, 1min lock - exit; - file_put_contents('./lastRun.txt', time()); - - $lastScanHeight = 0; - $timeStart = time(); - $lastOutCount = 0; - while(time() - $timeStart < 59*60){ - $blockchainHeight = getBlockchainHeight(); - $lastBlockCacheContent = null; - for($startHeight = $lastScanHeight; $startHeight <= $blockchainHeight; $startHeight += 100){ - - $endHeight = $startHeight + 100; - $realStartHeight = $startHeight; - // if($realStartHeight < 1) $realStartHeight = 1; - if($endHeight > $blockchainHeight){ - $endHeight = $blockchainHeight; - } - - echo 'scanning ' . $startHeight . ' to ' . $endHeight . "
"; - - $cacheContent = retrieveCache($realStartHeight, $endHeight, false); - // var_dump('==>',$lastBlockCacheContent,$cacheContent); - if($cacheContent === null){ - if($realStartHeight > 1){ - $lastBlockCacheContent = retrieveCache($realStartHeight-100, $realStartHeight, false); - $decodedContent = json_decode($lastBlockCacheContent, true); - if(count($decodedContent) > 0){ - $lastTr = $decodedContent[count($decodedContent) - 1]; - $outCount = $lastTr['global_index_start'] + count($lastTr['outputs']); - //var_dump('out count='.$outCount.' '.$lastTr['global_index_start'].' '.count($lastTr['outputs'])); - }else{ - var_dump('Missing compacted block file. Weird case'); - exit; - } - $lastBlockCacheContent = null; - } - - //var_dump("generating..."); - $cacheContent = createOptimizedBock($realStartHeight, $endHeight); - saveCache($realStartHeight, $endHeight, $cacheContent); - $cacheContent = json_encode($cacheContent); - }else{ -// if($cacheContent !== '[]' && $cacheContent !== null){ -// $lastBlockCacheContent = $cacheContent; -// } - } - - var_dump($outCount); - } - - $lastOutCount = $outCount; - - //var_dump('cleaning ...'); - - $allBlocksFiles = scandir($cacheLocation); - foreach($allBlocksFiles as $filename){ - if($filename !== '.' && $filename !== '..'){ - $blocksNumbers = explode('-', $filename); - if(count($blocksNumbers) === 2 && $blocksNumbers[1] % 100 !== 0){ - if($blocksNumbers[1]+1 < $blockchainHeight){//to be sure if other client are using the last one - unlink($cacheLocation . '/' . $filename); - } - } - } - } - - $lastScanHeight = floor($blockchainHeight/100)*100; - - file_put_contents('./lastRun.txt', time()); - sleep(10); - } -} - -//$finalTransactions = createOptimizedBock($startHeight, $endHeight); -//ini_set('zlib.output_compression_level', 1); -//if (extension_loaded('zlib') && !ini_get('zlib.output_compression')){ -// header('Content-Encoding: gzip'); -// ob_start('ob_gzhandler'); -//} -//ob_start("ob_gzhandler"); -//$data = gzcompress($cacheContent,9); - -//ob_end_clean(); -//echo strlen($data); -//echo '|'; -//echo strlen($cacheContent); -//ob_end_flush(); diff --git a/src_api/config.php b/src_api/config.php deleted file mode 100644 index b4435786..00000000 --- a/src_api/config.php +++ /dev/null @@ -1,7 +0,0 @@ - "2.0", "id" => "0", "method" => "gettransactionspool", "params" => '')); -curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/json_rpc', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body)); -$resp = curl_exec($curl); - -//now get the Tx details -$jsonMempool = json_decode($resp, true); -$rawTransactions = $jsonMempool["result"]["transactions"]; -$txHashes = array(); -for($iTransaction = 0; $iTransaction < count($rawTransactions); ++$iTransaction){ - $txHashes[] = $rawTransactions[$iTransaction]["hash"]; -} - -$body = json_encode(array( - 'transactionHashes'=>$txHashes -)); -curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/get_transaction_details_by_hashes', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body)); -$resp = curl_exec($curl); -$decodedJson = json_decode($resp, true); -curl_close($curl); - -/* -$blobString1 = '"tx_blob": "'; -$blobString2 = '"tx_blob":"'; -$posTxBlob = 0; -$searchTxBlock1 = 0; -$searchTxBlock2 = 0; -while(($searchTxBlock1 = strpos($resp,$blobString1)) !== false || ($searchTxBlock2 = strpos($resp,$blobString2)) !== false ){ -// var_dump($searchTxBlock1.' '.$searchTxBlock2); - if($searchTxBlock1 !== false){ - $posTxBlob = $searchTxBlock1; - $posEndTxBlock = $posTxBlob + strlen($blobString1); - } - else if($searchTxBlock2 !== false){ - $posTxBlob = $searchTxBlock2; - $posEndTxBlock = $posTxBlob + strlen($blobString2); - } - - $i = 0; - do{ - ++$posEndTxBlock; - $posEndTxBlock = strpos($resp, '"', $posEndTxBlock); - ++$i; - }while($posEndTxBlock !== false && $resp[$posEndTxBlock-1] === '\\'); - - $resp = substr($resp, 0, $posTxBlob).substr($resp, $posEndTxBlock+2); -} - -$jsonMempool = json_decode($resp, true); -if(isset($jsonMempool["result"]['transactions'])){ -// var_dump('isset'); - foreach($jsonMempool['transactions'] as $key=>$tx){ - unset($tx['tx_blob']); - $tx['tx_json'] = json_decode($tx['tx_json']); - $jsonMempool['transactions'][$key] = $tx; - } -} -*/ -$jsonMempool = json_decode($resp, true); -header('Content-Type: application/json'); -echo json_encode($jsonMempool['transactions']); - diff --git a/src_api/getheight.php b/src_api/getheight.php deleted file mode 100644 index d344fd31..00000000 --- a/src_api/getheight.php +++ /dev/null @@ -1,31 +0,0 @@ - 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/getheight')); -$resp = curl_exec($curl); -curl_close($curl); -//var_dump($resp); -$array = json_decode($resp, true); -//var_dump($array); -if($array === null) - http_response_code(400); -else - echo $array['height']; \ No newline at end of file diff --git a/src_api/network.php b/src_api/network.php deleted file mode 100644 index a7eb5ed9..00000000 --- a/src_api/network.php +++ /dev/null @@ -1,46 +0,0 @@ - "2.0", "id" => "0", "method" => "getlastblockheader")); -curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/json_rpc', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body)); - -$resp = curl_exec($curl); -curl_close($curl); -//var_dump($resp); -$array = json_decode($resp, true); -//var_dump($array); -if($array === null) - http_response_code(400); -else{ - $blockHeader = $array['result']['block_header']; - header('Content-Type: application/json'); - echo json_encode(array( - 'major_version'=>$blockHeader['major_version'], - 'hash'=>$blockHeader['hash'], - 'reward'=>$blockHeader['reward'], - 'height'=>$blockHeader['height'], - 'timestamp'=>$blockHeader['timestamp'], - 'difficulty'=>$blockHeader['difficulty'], - 'hashrate'=>$blockHeader['difficulty']*60*2, - )); -} - - - diff --git a/src_api/openAlias.php b/src_api/openAlias.php deleted file mode 100644 index bf9a831b..00000000 --- a/src_api/openAlias.php +++ /dev/null @@ -1,60 +0,0 @@ -= 1){ - if(trim($subparts[0]) === 'recipient_address'){ - $recipient_address = trim($subparts[1]); - }else if(trim($subparts[0]) === 'recipient_name'){ - $recipient_name = trim($subparts[1]); - } - } - } - } - } - } - -} -if($recipient_address !== null){ - header('Content-Type: application/json'); - echo json_encode(array( - 'address'=>$recipient_address, - 'name'=>$recipient_name, - )); -}else{ - http_response_code(404); -} \ No newline at end of file diff --git a/src_api/sendrawtransaction.php b/src_api/sendrawtransaction.php deleted file mode 100644 index 4cdaa1fd..00000000 --- a/src_api/sendrawtransaction.php +++ /dev/null @@ -1,29 +0,0 @@ - 1, CURLOPT_URL => 'http://'.$daemonAddress.':'.$rpcPort.'/sendrawtransaction', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body)); - -$resp = curl_exec($curl); -curl_close($curl); - -header('Content-Type: application/json'); -echo $resp; \ No newline at end of file