diff --git a/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js b/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js index 73a8c0d9d..5fb55173b 100644 --- a/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js +++ b/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js @@ -1,103 +1,103 @@ // Expected result of applying parseTxData to mockTxDataWityhPassthrough[0] export const mockSentCashTx = [ { amountReceived: 0, amountSent: 0.000042, blocktime: 1614380741, confirmations: 2721, destinationAddress: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', height: 674993, outgoingTx: true, isCashtabMessage: false, opReturnMessage: '', - replyAddress: 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', + replyAddress: null, tokenTx: false, txid: '089f2188d5771a7de0589def2b8d6c1a1f33f45b6de26d9a0ef32782f019ecf1', }, ]; export const mockReceivedCashTx = [ { amountReceived: 3, amountSent: 0, blocktime: 1612567121, confirmations: 5637, destinationAddress: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', height: 672077, outgoingTx: false, isCashtabMessage: false, opReturnMessage: '', - replyAddress: 'ecash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxav9up3h67g', + replyAddress: null, tokenTx: false, txid: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', }, ]; export const mockSentTokenTx = [ { amountReceived: 0, amountSent: 0.00000546, blocktime: 1614027278, confirmations: 3270, destinationAddress: 'bitcoincash:qzj5zu6fgg8v2we82gh76xnrk9njcregluzgaztm45', height: 674444, outgoingTx: true, isCashtabMessage: false, opReturnMessage: '', - replyAddress: 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', + replyAddress: null, tokenTx: true, txid: 'ffe3a7500dbcc98021ad581c98d9947054d1950a7f3416664715066d3d20ad72', }, ]; export const mockReceivedTokenTx = [ { amountReceived: 0.00000546, amountSent: 0, blocktime: 1613859311, confirmations: 3571, destinationAddress: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', height: 674143, outgoingTx: false, isCashtabMessage: false, opReturnMessage: '', - replyAddress: 'ecash:qzudj5fd9t0cknnsc3wzdd4sp46u9r42jc2d89j2kc', + replyAddress: null, tokenTx: true, txid: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', }, ]; export const mockSentOpReturnMessageTx = [ { amountReceived: 0, amountSent: 0, blocktime: 1635507345, confirmations: 59, destinationAddress: undefined, height: undefined, opReturnMessage: new Buffer('bingoelectrum'), - replyAddress: 'ecash:qzekdmmurl75aazj6uj4vc68yrxgws0pmsgztm4atw', + replyAddress: null, outgoingTx: false, tokenTx: false, isCashtabMessage: false, txid: 'dd35690b0cefd24dcc08acba8694ecd49293f365a81372cb66c8f1c1291d97c5', }, ]; export const mockReceivedOpReturnMessageTx = [ { amountReceived: 0, amountSent: 0, blocktime: 1635511136, confirmations: 70, destinationAddress: undefined, height: undefined, opReturnMessage: new Buffer('cashtabular'), replyAddress: 'ecash:qrxkkzsmrxcjmz8x90fx2uztt83cuu0u254w09pq5p', outgoingTx: false, tokenTx: false, isCashtabMessage: true, txid: '5adc33b5c0509b31c6da359177b19467c443bdc4dd37c283c0f87244c0ad63af', }, ]; diff --git a/web/cashtab/src/hooks/__mocks__/mockPublicKeys.js b/web/cashtab/src/hooks/__mocks__/mockPublicKeys.js new file mode 100644 index 000000000..dca7eaa62 --- /dev/null +++ b/web/cashtab/src/hooks/__mocks__/mockPublicKeys.js @@ -0,0 +1,3 @@ +export default [ + '02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', +]; diff --git a/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js b/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js index e7fffc2c7..2c05fec6f 100644 --- a/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js +++ b/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js @@ -1,870 +1,872 @@ export default [ { txid: '089f2188d5771a7de0589def2b8d6c1a1f33f45b6de26d9a0ef32782f019ecf1', hash: '089f2188d5771a7de0589def2b8d6c1a1f33f45b6de26d9a0ef32782f019ecf1', version: 2, size: 225, locktime: 0, vin: [ { txid: 'b96da810b15deb312ad4508a165033ca8ffa282f88e5b7b0e79be09a0b0424f9', vout: 1, scriptSig: { asm: '3044022064084d72b1bb7ca148d1950cf07494ffb397cb3df53b72afa8bd844b80369ecd02203ae21f14ba5019f38bc0b80b99e7c8cc1d5d3360ca7bab56be28ef583fe5c6a6[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '473044022064084d72b1bb7ca148d1950cf07494ffb397cb3df53b72afa8bd844b80369ecd02203ae21f14ba5019f38bc0b80b99e7c8cc1d5d3360ca7bab56be28ef583fe5c6a6412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, - address: - 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 0.000042, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, { value: 0.6244967, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000001f924040b9ae09be7b0b7e5882f28fa8fca3350168a50d42a31eb5db110a86db9010000006a473044022064084d72b1bb7ca148d1950cf07494ffb397cb3df53b72afa8bd844b80369ecd02203ae21f14ba5019f38bc0b80b99e7c8cc1d5d3360ca7bab56be28ef583fe5c6a6412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff0268100000000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac06e8b803000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '000000000000000087dd4ca6308e835edfba871fee36d3e53ad3c9545c4b1719', confirmations: 2721, time: 1614380741, blocktime: 1614380741, height: 674993, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'ffe3a7500dbcc98021ad581c98d9947054d1950a7f3416664715066d3d20ad72', hash: 'ffe3a7500dbcc98021ad581c98d9947054d1950a7f3416664715066d3d20ad72', version: 2, size: 480, locktime: 0, vin: [ { txid: 'b980b35b794ad73d8aae312385e82d9be8086e7b743e1c6a468db8db8ac74bd8', vout: 3, scriptSig: { asm: '30440220538de8f61d716c899e6a2cd78ca46162edaaa5f0d000ebbbc875608e5639170a02206a7fc8f7c16cef1c56667a8da6d5e480f440ecf43238879ad9f8785a0473a72b[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '4730440220538de8f61d716c899e6a2cd78ca46162edaaa5f0d000ebbbc875608e5639170a02206a7fc8f7c16cef1c56667a8da6d5e480f440ecf43238879ad9f8785a0473a72b412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, - address: - 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'b980b35b794ad73d8aae312385e82d9be8086e7b743e1c6a468db8db8ac74bd8', vout: 2, scriptSig: { asm: '3045022100ce03e19bd181b903adc6f192d4ad0900e6816f6e62282cefff05c22cf36a647602202b296a2ed1805f0b0a9aa5f99158685298e7a0aff406fedb8abb8e0afaf48ca4[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '483045022100ce03e19bd181b903adc6f192d4ad0900e6816f6e62282cefff05c22cf36a647602202b296a2ed1805f0b0a9aa5f99158685298e7a0aff406fedb8abb8e0afaf48ca4412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, - address: - 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 5262419 1 1145980243 50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e 0000000000000003 000000000000005e', hex: '6a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e08000000000000000308000000000000005e', type: 'nulldata', }, }, { value: 0.00000546, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 a5417349420ec53b27522fed1a63b1672c0f28ff OP_EQUALVERIFY OP_CHECKSIG', hex: '76a914a5417349420ec53b27522fed1a63b1672c0f28ff88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qzj5zu6fgg8v2we82gh76xnrk9njcregluzgaztm45', ], }, }, { value: 0.00000546, n: 2, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 4.99996074, n: 3, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000002d84bc78adbb88d466a1c3e747b6e08e89b2de8852331ae8a3dd74a795bb380b9030000006a4730440220538de8f61d716c899e6a2cd78ca46162edaaa5f0d000ebbbc875608e5639170a02206a7fc8f7c16cef1c56667a8da6d5e480f440ecf43238879ad9f8785a0473a72b412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffffd84bc78adbb88d466a1c3e747b6e08e89b2de8852331ae8a3dd74a795bb380b9020000006b483045022100ce03e19bd181b903adc6f192d4ad0900e6816f6e62282cefff05c22cf36a647602202b296a2ed1805f0b0a9aa5f99158685298e7a0aff406fedb8abb8e0afaf48ca4412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff040000000000000000406a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e08000000000000000308000000000000005e22020000000000001976a914a5417349420ec53b27522fed1a63b1672c0f28ff88ac22020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acaa55cd1d000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '00000000000000007053867de29516374a23d7adfb08ccb47cfbea0e98a49e5b', confirmations: 3270, time: 1614027278, blocktime: 1614027278, height: 674444, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'b980b35b794ad73d8aae312385e82d9be8086e7b743e1c6a468db8db8ac74bd8', hash: 'b980b35b794ad73d8aae312385e82d9be8086e7b743e1c6a468db8db8ac74bd8', version: 2, size: 479, locktime: 0, vin: [ { txid: 'ec9c20c2c5cd5aa4c9261a9f97e68734b175962c4b3d9edc996dd415dd03c2e7', vout: 0, scriptSig: { asm: '3044022075cb93e60ffb792b2715d96f3d31033e8f385bb9bfeadf99f7b1055d749a33cc022028292ee8ffaed64cbc6f9b680db36e031250672a6b0c5cfd23f9a61977d52ed7[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '473044022075cb93e60ffb792b2715d96f3d31033e8f385bb9bfeadf99f7b1055d749a33cc022028292ee8ffaed64cbc6f9b680db36e031250672a6b0c5cfd23f9a61977d52ed7412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', vout: 1, scriptSig: { asm: '304402203ea0558cd917eb8f6c286e79ffcc5dd1f5accb66c2e5836628d6be6f9d03ca260220120a6da92b6f44bdfcd3ef7b08263d3f73d99ff4b1f83b8f998ff1355f3f0d2e[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '47304402203ea0558cd917eb8f6c286e79ffcc5dd1f5accb66c2e5836628d6be6f9d03ca260220120a6da92b6f44bdfcd3ef7b08263d3f73d99ff4b1f83b8f998ff1355f3f0d2e412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, - address: - 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 5262419 1 1145980243 50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e 0000000000000003 0000000000000061', hex: '6a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e080000000000000003080000000000000061', type: 'nulldata', }, }, { value: 0.00000546, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 d4fa9121bcd065dd93e58831569cf51ef5a74f61 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a914d4fa9121bcd065dd93e58831569cf51ef5a74f6188ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qr204yfphngxthvnukyrz45u7500tf60vyea48xwmd', ], }, }, { value: 0.00000546, n: 2, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 4.99998974, n: 3, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000002e7c203dd15d46d99dc9e3d4b2c9675b13487e6979f1a26c9a45acdc5c2209cec000000006a473044022075cb93e60ffb792b2715d96f3d31033e8f385bb9bfeadf99f7b1055d749a33cc022028292ee8ffaed64cbc6f9b680db36e031250672a6b0c5cfd23f9a61977d52ed7412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffffe6c0ab0db2ba53f1aee96ecd11836fb72bd75d865c51c6345afac5c0d80d8d61010000006a47304402203ea0558cd917eb8f6c286e79ffcc5dd1f5accb66c2e5836628d6be6f9d03ca260220120a6da92b6f44bdfcd3ef7b08263d3f73d99ff4b1f83b8f998ff1355f3f0d2e412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff040000000000000000406a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e08000000000000000308000000000000006122020000000000001976a914d4fa9121bcd065dd93e58831569cf51ef5a74f6188ac22020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acfe60cd1d000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '0000000000000000a9f812d56e2249b7c462ce499a0852bdfe20bb46c1bb9f92', confirmations: 3278, time: 1614021424, blocktime: 1614021424, height: 674436, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', hash: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', version: 2, size: 436, locktime: 0, vin: [ { txid: '50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e', vout: 3, scriptSig: { asm: '30440220664f988b86035ddcdff6e9c3b8e140712eca297750d056e41577a0bf0059e7ff022030982b3fcab1cab5d6086bc935e941e7d22efbb0ad5ccca0268515c5c8306089[ALL|FORKID] 034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', hex: '4730440220664f988b86035ddcdff6e9c3b8e140712eca297750d056e41577a0bf0059e7ff022030982b3fcab1cab5d6086bc935e941e7d22efbb0ad5ccca0268515c5c83060894121034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', }, sequence: 4294967295, - address: - 'bitcoincash:qzudj5fd9t0cknnsc3wzdd4sp46u9r42jcnqnwfss0', }, { txid: '50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e', vout: 1, scriptSig: { asm: '304402203ce88e0a95d5581ad567c0468c87a08027aa5ecdecd614a168d833d7ecc02c1c022013ddd81147b44ad5488107d5c4d535f7f59e9fa46840451d39422aace284b2b7[ALL|FORKID] 034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', hex: '47304402203ce88e0a95d5581ad567c0468c87a08027aa5ecdecd614a168d833d7ecc02c1c022013ddd81147b44ad5488107d5c4d535f7f59e9fa46840451d39422aace284b2b74121034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', }, sequence: 4294967295, - address: - 'bitcoincash:qzudj5fd9t0cknnsc3wzdd4sp46u9r42jcnqnwfss0', }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 5262419 1 1145980243 50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e 0000000000000064', hex: '6a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e080000000000000064', type: 'nulldata', }, }, { value: 0.00000546, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 0.00088064, n: 2, scriptPubKey: { asm: 'OP_DUP OP_HASH160 b8d9512d2adf8b4e70c45c26b6b00d75c28eaa96 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a914b8d9512d2adf8b4e70c45c26b6b00d75c28eaa9688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qzudj5fd9t0cknnsc3wzdd4sp46u9r42jcnqnwfss0', ], }, }, ], hex: '02000000020e41711243954c8ffe409761e994272af43ced6f56c8c6afa7cd55622c29d850030000006a4730440220664f988b86035ddcdff6e9c3b8e140712eca297750d056e41577a0bf0059e7ff022030982b3fcab1cab5d6086bc935e941e7d22efbb0ad5ccca0268515c5c83060894121034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3ffffffff0e41711243954c8ffe409761e994272af43ced6f56c8c6afa7cd55622c29d850010000006a47304402203ce88e0a95d5581ad567c0468c87a08027aa5ecdecd614a168d833d7ecc02c1c022013ddd81147b44ad5488107d5c4d535f7f59e9fa46840451d39422aace284b2b74121034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3ffffffff030000000000000000376a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e08000000000000006422020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00580100000000001976a914b8d9512d2adf8b4e70c45c26b6b00d75c28eaa9688ac00000000', blockhash: '000000000000000034c77993a35c74fe2dddace27198681ca1e89e928d0c2fff', confirmations: 3571, time: 1613859311, blocktime: 1613859311, height: 674143, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'f90631b48521a4147dd9dd7091ce936eddc0c3e6221ec87fa4fabacc453a0b95', hash: 'f90631b48521a4147dd9dd7091ce936eddc0c3e6221ec87fa4fabacc453a0b95', version: 2, size: 437, locktime: 0, vin: [ { txid: 'db464f77ac97deabc28df07a7e4a2e261c854a8ec4dc959b89b10531966f6cbf', vout: 0, scriptSig: { asm: '3044022065622a7aa065f56abe84f3589c983a768e3ef5d72c9352991d6b584a2a16dcb802200c1c0065106207715a024624ed951e851d4f742c55a704e9531bebd2ef84fc14[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '473044022065622a7aa065f56abe84f3589c983a768e3ef5d72c9352991d6b584a2a16dcb802200c1c0065106207715a024624ed951e851d4f742c55a704e9531bebd2ef84fc1441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: 'acbb66f826211f40b89e84d9bd2143dfb541d67e1e3c664b17ccd3ba66327a9e', vout: 1, scriptSig: { asm: '3045022100b475cf7d1eaf37641d2107f13be0ef9acbd17b252ed3f9ae349edfdcd6a97cf402202bf2852dfa905e6d50c96a622d2838408ceb979245a4342d5096acc938135804[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100b475cf7d1eaf37641d2107f13be0ef9acbd17b252ed3f9ae349edfdcd6a97cf402202bf2852dfa905e6d50c96a622d2838408ceb979245a4342d5096acc93813580441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 5262419 1 1145980243 bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef 0000000000000001', hex: '6a04534c500001010453454e4420bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef080000000000000001', type: 'nulldata', }, }, { value: 0.00000546, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 9.99997101, n: 2, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, ], hex: '0200000002bf6c6f963105b1899b95dcc48e4a851c262e4a7e7af08dc2abde97ac774f46db000000006a473044022065622a7aa065f56abe84f3589c983a768e3ef5d72c9352991d6b584a2a16dcb802200c1c0065106207715a024624ed951e851d4f742c55a704e9531bebd2ef84fc1441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff9e7a3266bad3cc174b663c1e7ed641b5df4321bdd9849eb8401f2126f866bbac010000006b483045022100b475cf7d1eaf37641d2107f13be0ef9acbd17b252ed3f9ae349edfdcd6a97cf402202bf2852dfa905e6d50c96a622d2838408ceb979245a4342d5096acc93813580441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff030000000000000000376a04534c500001010453454e4420bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef08000000000000000122020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acadbe9a3b000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac00000000', blockhash: '0000000000000000132378b84a7477b7d601faedec302264bde1e89b1480e364', confirmations: 5013, time: 1612966022, blocktime: 1612966022, height: 672701, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', hash: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', version: 2, size: 226, locktime: 0, vin: [ { txid: '5e0436c6741e226d05c5b7e7e23de8213d3583e2669e50a80b908bf4cb471317', vout: 1, scriptSig: { asm: '3045022100f8a8eca8f5d6149511c518d41015512f8164a5be6f01e9efd609db9a429f4872022059121e122043b43eae77b5e132b8f798a290e6eed8a2026a0656540cd1bd752b[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100f8a8eca8f5d6149511c518d41015512f8164a5be6f01e9efd609db9a429f4872022059121e122043b43eae77b5e132b8f798a290e6eed8a2026a0656540cd1bd752b41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, ], vout: [ { value: 3, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 6.9999586, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, ], hex: '0200000001171347cbf48b900ba8509e66e283353d21e83de2e7b7c5056d221e74c636045e010000006b483045022100f8a8eca8f5d6149511c518d41015512f8164a5be6f01e9efd609db9a429f4872022059121e122043b43eae77b5e132b8f798a290e6eed8a2026a0656540cd1bd752b41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff0200a3e111000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acd416b929000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'b96da810b15deb312ad4508a165033ca8ffa282f88e5b7b0e79be09a0b0424f9', hash: 'b96da810b15deb312ad4508a165033ca8ffa282f88e5b7b0e79be09a0b0424f9', version: 2, size: 226, locktime: 0, vin: [ { txid: '9ad75af97f0617a3729c2bd31bf7c4b380230e661cc921a3c6be0febc75a3e49', vout: 1, scriptSig: { asm: '3045022100d59e6fad4d1d57796f229a7d4aa3b01fc3241132dae9bc406c66fa33d7aef21c022036a5f432d6d99f65848ac12c00bde2b5ba7e63a9f9a74349d9ab8ec39db26f8e[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '483045022100d59e6fad4d1d57796f229a7d4aa3b01fc3241132dae9bc406c66fa33d7aef21c022036a5f432d6d99f65848ac12c00bde2b5ba7e63a9f9a74349d9ab8ec39db26f8e412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, - address: - 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 0.12345, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, { value: 0.62455003, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000001493e5ac7eb0fbec6a321c91c660e2380b3c4f71bd32b9c72a317067ff95ad79a010000006b483045022100d59e6fad4d1d57796f229a7d4aa3b01fc3241132dae9bc406c66fa33d7aef21c022036a5f432d6d99f65848ac12c00bde2b5ba7e63a9f9a74349d9ab8ec39db26f8e412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff02a85ebc00000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88acdbfcb803000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'db464f77ac97deabc28df07a7e4a2e261c854a8ec4dc959b89b10531966f6cbf', hash: 'db464f77ac97deabc28df07a7e4a2e261c854a8ec4dc959b89b10531966f6cbf', version: 2, size: 225, locktime: 0, vin: [ { txid: '1452267e57429edcfdcb1184b24becea6ddf8f8a4f8e130dad6248545d9f8e75', vout: 1, scriptSig: { asm: '30440220184921bfce634a57b5220f06b11b64c0cb7e67ecd9c634335e3e933e35a7a969022038b2074e1d75aa4f6945d150bae5b8a1d426f4284da2b96336fa0fc741eb6de7[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '4730440220184921bfce634a57b5220f06b11b64c0cb7e67ecd9c634335e3e933e35a7a969022038b2074e1d75aa4f6945d150bae5b8a1d426f4284da2b96336fa0fc741eb6de7412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, - address: - 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 10.00000001, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, { value: 2.8830607, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000001758e9f5d544862ad0d138e4f8a8fdf6deaec4bb28411cbfddc9e42577e265214010000006a4730440220184921bfce634a57b5220f06b11b64c0cb7e67ecd9c634335e3e933e35a7a969022038b2074e1d75aa4f6945d150bae5b8a1d426f4284da2b96336fa0fc741eb6de7412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff0201ca9a3b000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac96332f11000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'e32c20137e590f253b8d198608f7fffd428fc0bd7a9a0675bb6af091d1cb2ea4', hash: 'e32c20137e590f253b8d198608f7fffd428fc0bd7a9a0675bb6af091d1cb2ea4', version: 2, size: 373, locktime: 0, vin: [ { txid: 'f63e890423b3bffa6e01be2dcb4942940c2e8a1985926411558a22d1b5dd0e29', vout: 1, scriptSig: { asm: '3045022100c7f51ff0888c182a1a60c08904d8116c9d2e31cb7d2fd5b63c2bf9fd7b246fc102202ee786d2052448621c4a04d18d13c83ac5ee27008dd079e8ba954f8197ff3c6c[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '483045022100c7f51ff0888c182a1a60c08904d8116c9d2e31cb7d2fd5b63c2bf9fd7b246fc102202ee786d2052448621c4a04d18d13c83ac5ee27008dd079e8ba954f8197ff3c6c412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, - address: - 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', vout: 0, scriptSig: { asm: '304402201bbfcd0c120ace9b8c7a6f5e77b61236bb1128e2a757f85ba80101885e9c1212022046fed4006dcd6a236034dede77c566acf74824d14b3ee3da884e9bd93884ff93[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '47304402201bbfcd0c120ace9b8c7a6f5e77b61236bb1128e2a757f85ba80101885e9c1212022046fed4006dcd6a236034dede77c566acf74824d14b3ee3da884e9bd93884ff93412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, - address: - 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 1.8725994, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, { value: 1.49237053, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000002290eddb5d1228a5511649285198a2e0c944249cb2dbe016efabfb32304893ef6010000006b483045022100c7f51ff0888c182a1a60c08904d8116c9d2e31cb7d2fd5b63c2bf9fd7b246fc102202ee786d2052448621c4a04d18d13c83ac5ee27008dd079e8ba954f8197ff3c6c412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffffa9d2061a7b5e12580ac2fed2714df9804bf0fd227b981f69fe408a06be9fd342000000006a47304402201bbfcd0c120ace9b8c7a6f5e77b61236bb1128e2a757f85ba80101885e9c1212022046fed4006dcd6a236034dede77c566acf74824d14b3ee3da884e9bd93884ff93412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff02245c290b000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac3d2de508000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'ec9c20c2c5cd5aa4c9261a9f97e68734b175962c4b3d9edc996dd415dd03c2e7', hash: 'ec9c20c2c5cd5aa4c9261a9f97e68734b175962c4b3d9edc996dd415dd03c2e7', version: 2, size: 1405, locktime: 0, vin: [ { txid: '3507d73b0bb82421d64ae79f469943e56f15d7db954ad235f48ede33c718d860', vout: 0, scriptSig: { asm: '3044022000cc5b79e5da60cf4935f3a172089cd9b631b678462ee29091dc610816d059c4022002e3b6f32e825ac04d2907453d6d647a32a995c798df1c68401cc461f6bfbd3a[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '473044022000cc5b79e5da60cf4935f3a172089cd9b631b678462ee29091dc610816d059c4022002e3b6f32e825ac04d2907453d6d647a32a995c798df1c68401cc461f6bfbd3a41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '8f73a718d907d94e60c5f73f299bd01dc5b1c163c4ebc26b5304e37a1a7f34af', vout: 0, scriptSig: { asm: '3045022100ac50553448f2a5fab1177ed0bc64541b2dba063d04f2d69a8a1d216fb1435e5802202c7f6abd1685a6d81f14ac3bdb0874d214a5f4260719f9c5dc519ac5d8dffd37[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100ac50553448f2a5fab1177ed0bc64541b2dba063d04f2d69a8a1d216fb1435e5802202c7f6abd1685a6d81f14ac3bdb0874d214a5f4260719f9c5dc519ac5d8dffd3741210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: 'd38b76bacd6aa75ad2d6fcfd994533e54d0541435970eace49486fde9d6ee2e3', vout: 0, scriptSig: { asm: '304502210091836c6cb4c786bd3b74b73e579ddf8b843ba51841e5675fa53608449b67371802203de75f32b684cfe2d2e9cd424ea6eb4f49248e6698365c9364ebf84cd6e50eab[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '48304502210091836c6cb4c786bd3b74b73e579ddf8b843ba51841e5675fa53608449b67371802203de75f32b684cfe2d2e9cd424ea6eb4f49248e6698365c9364ebf84cd6e50eab41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: 'd47607a72d6bc093556fa7f2cec9d67719bd627751d5d27bc53c4eb8eb6f54e5', vout: 0, scriptSig: { asm: '3045022100e7727d9d26c645282553aef27947ad6795bc89b505ad089d617b6f696399352802206c736524a1410ed3e30cf1127f7f02c9a249392f8f8e7c670250472909d1c0d6[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100e7727d9d26c645282553aef27947ad6795bc89b505ad089d617b6f696399352802206c736524a1410ed3e30cf1127f7f02c9a249392f8f8e7c670250472909d1c0d641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '33f246811f794c4b64098a64c698ae5811054b13e289256a18e2d142beef57e7', vout: 1, scriptSig: { asm: '304402203fe78ad5aaeefab7b3b2277eefc4a2ace9c2e92694b46bf4a76927bf2b82017102200ded59336aba269a54865d9fdd99e72081c0318ccbc37bc0fc0c72b60ae35382[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '47304402203fe78ad5aaeefab7b3b2277eefc4a2ace9c2e92694b46bf4a76927bf2b82017102200ded59336aba269a54865d9fdd99e72081c0318ccbc37bc0fc0c72b60ae3538241210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '25f915d2912524ad602c882211ccaf479d6bf87ef7c24d1be0f325cec3727257', vout: 0, scriptSig: { asm: '30440220670af03605b9495c8ecee357889ceeb137dadaa1662136fdc55c28fe9434e3c60220285195a62811941745a9f93e136e59c96b81d5b0d9525f3d16d001bc0f6fa9bb[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '4730440220670af03605b9495c8ecee357889ceeb137dadaa1662136fdc55c28fe9434e3c60220285195a62811941745a9f93e136e59c96b81d5b0d9525f3d16d001bc0f6fa9bb41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: 'c9044b4d7438d006a722ef85474c8127265eced4f72c7d71c2f714444bc0e1f2', vout: 0, scriptSig: { asm: '304402203f822a0b207ed49e6918663133a18037c24498c2f770c2649333a32f523e259d02203afc42a79d0da123b67f814effeee7c05c7996ea829b3cfa46c5c2e74209c096[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '47304402203f822a0b207ed49e6918663133a18037c24498c2f770c2649333a32f523e259d02203afc42a79d0da123b67f814effeee7c05c7996ea829b3cfa46c5c2e74209c09641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '045306f0019ae0d977de7ff17dd55e861b3fe94458693ee2b94ce5dd7003aab9', vout: 0, scriptSig: { asm: '3045022100a7a2cf838a13a19f0e443ca35ac5ee3d55f70edca992f98402a84d4ab5ae1ad90220644a02c746eae7b44a4600199ecbf69f3b0f0bdf8479f461c482d67ef4a84e76[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100a7a2cf838a13a19f0e443ca35ac5ee3d55f70edca992f98402a84d4ab5ae1ad90220644a02c746eae7b44a4600199ecbf69f3b0f0bdf8479f461c482d67ef4a84e7641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '1452267e57429edcfdcb1184b24becea6ddf8f8a4f8e130dad6248545d9f8e75', vout: 0, scriptSig: { asm: '30440220290701c797eb52ad6721db615c7d6f623c0200be0e6d6802df68c527655475450220446c4a4da9a0df5efcb57711ad61cf6167dfdda937bd0477189be8afedaedd05[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '4730440220290701c797eb52ad6721db615c7d6f623c0200be0e6d6802df68c527655475450220446c4a4da9a0df5efcb57711ad61cf6167dfdda937bd0477189be8afedaedd0541210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, - address: - 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, ], vout: [ { value: 5.00001874, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 7.52551634, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, ], hex: '020000000960d818c733de8ef435d24a95dbd7156fe54399469fe74ad62124b80b3bd70735000000006a473044022000cc5b79e5da60cf4935f3a172089cd9b631b678462ee29091dc610816d059c4022002e3b6f32e825ac04d2907453d6d647a32a995c798df1c68401cc461f6bfbd3a41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffaf347f1a7ae304536bc2ebc463c1b1c51dd09b293ff7c5604ed907d918a7738f000000006b483045022100ac50553448f2a5fab1177ed0bc64541b2dba063d04f2d69a8a1d216fb1435e5802202c7f6abd1685a6d81f14ac3bdb0874d214a5f4260719f9c5dc519ac5d8dffd3741210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffe3e26e9dde6f4849ceea70594341054de5334599fdfcd6d25aa76acdba768bd3000000006b48304502210091836c6cb4c786bd3b74b73e579ddf8b843ba51841e5675fa53608449b67371802203de75f32b684cfe2d2e9cd424ea6eb4f49248e6698365c9364ebf84cd6e50eab41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffe5546febb84e3cc57bd2d5517762bd1977d6c9cef2a76f5593c06b2da70776d4000000006b483045022100e7727d9d26c645282553aef27947ad6795bc89b505ad089d617b6f696399352802206c736524a1410ed3e30cf1127f7f02c9a249392f8f8e7c670250472909d1c0d641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffe757efbe42d1e2186a2589e2134b051158ae98c6648a09644b4c791f8146f233010000006a47304402203fe78ad5aaeefab7b3b2277eefc4a2ace9c2e92694b46bf4a76927bf2b82017102200ded59336aba269a54865d9fdd99e72081c0318ccbc37bc0fc0c72b60ae3538241210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff577272c3ce25f3e01b4dc2f77ef86b9d47afcc1122882c60ad242591d215f925000000006a4730440220670af03605b9495c8ecee357889ceeb137dadaa1662136fdc55c28fe9434e3c60220285195a62811941745a9f93e136e59c96b81d5b0d9525f3d16d001bc0f6fa9bb41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dfffffffff2e1c04b4414f7c2717d2cf7d4ce5e2627814c4785ef22a706d038744d4b04c9000000006a47304402203f822a0b207ed49e6918663133a18037c24498c2f770c2649333a32f523e259d02203afc42a79d0da123b67f814effeee7c05c7996ea829b3cfa46c5c2e74209c09641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffb9aa0370dde54cb9e23e695844e93f1b865ed57df17fde77d9e09a01f0065304000000006b483045022100a7a2cf838a13a19f0e443ca35ac5ee3d55f70edca992f98402a84d4ab5ae1ad90220644a02c746eae7b44a4600199ecbf69f3b0f0bdf8479f461c482d67ef4a84e7641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff758e9f5d544862ad0d138e4f8a8fdf6deaec4bb28411cbfddc9e42577e265214000000006a4730440220290701c797eb52ad6721db615c7d6f623c0200be0e6d6802df68c527655475450220446c4a4da9a0df5efcb57711ad61cf6167dfdda937bd0477189be8afedaedd0541210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff02526ccd1d000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acd206db2c000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'dd35690b0cefd24dcc08acba8694ecd49293f365a81372cb66c8f1c1291d97c5', hash: 'dd35690b0cefd24dcc08acba8694ecd49293f365a81372cb66c8f1c1291d97c5', version: 2, size: 224, locktime: 0, vin: [ { txid: 'd3e1b8a65f9d50363cad9a496f7cecab59c9415dd9bcfd6f56c0c5dd4dffa7af', vout: 1, scriptSig: { asm: '3045022100fb14c794778e33aa66b861e85650f07e802da8b257cc37ac9dc1ac6346a0171d022051d79d2fc81bcb5bc3c7c7025d4222ecc2060cbdbf71a6fb2c7856b2eeaef7dc[ALL|FORKID] 02e4af47715f4db1d2a8d686be40c42bba5e70d715e470314181730e797be2324b', hex: '483045022100fb14c794778e33aa66b861e85650f07e802da8b257cc37ac9dc1ac6346a0171d022051d79d2fc81bcb5bc3c7c7025d4222ecc2060cbdbf71a6fb2c7856b2eeaef7dc412102e4af47715f4db1d2a8d686be40c42bba5e70d715e470314181730e797be2324b', }, sequence: 4294967295, - address: - 'bitcoincash:qzekdmmurl75aazj6uj4vc68yrxgws0pms30lsw8de', - value: 0.61760311, }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 62696e676f656c65637472756d', hex: '6a0d62696e676f656c65637472756d', type: 'nulldata', }, }, { value: 0.61759811, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 09401a690d52252acd1152c2ddd36c5081dff574 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91409401a690d52252acd1152c2ddd36c5081dff57488ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qqy5qxnfp4fz22kdz9fv9hwnd3ggrhl4wsekqyswf0', ], }, }, ], hex: '0200000001afa7ff4dddc5c0566ffdbcd95d41c959abec7c6f499aad3c36509d5fa6b8e1d3010000006b483045022100fb14c794778e33aa66b861e85650f07e802da8b257cc37ac9dc1ac6346a0171d022051d79d2fc81bcb5bc3c7c7025d4222ecc2060cbdbf71a6fb2c7856b2eeaef7dc412102e4af47715f4db1d2a8d686be40c42bba5e70d715e470314181730e797be2324bffffffff020000000000000000176a026d021274657374696e67206d6573736167652031324361ae03000000001976a91409401a690d52252acd1152c2ddd36c5081dff57488ac00000000', blockhash: '00000000000000000cbd73d616ecdd107a92d33aee5406ce05141231a76d408a', confirmations: 59, time: 1635507345, blocktime: 1635507345, }, { txid: '5adc33b5c0509b31c6da359177b19467c443bdc4dd37c283c0f87244c0ad63af', hash: '5adc33b5c0509b31c6da359177b19467c443bdc4dd37c283c0f87244c0ad63af', version: 2, size: 223, locktime: 0, vin: [ { txid: '2e1c5d1060bf5216678e31ed52d2ca564b81a34ac1a10749c5e124d25ec3c7a2', vout: 0, scriptSig: { asm: '304402205f6f73369ee558a8dd149480dda3d5417aab3c9bc2c4ff97aaebfc2768ceaded022052d7e2cfa0743205db27ac0cd29bcae110f1ca00aeedb6f88694901a7379dc65[ALL|FORKID] 0320b7867e815a2b00fa935a44a4c348299f7171995c8470d8221e6485da521164', hex: '47304402205f6f73369ee558a8dd149480dda3d5417aab3c9bc2c4ff97aaebfc2768ceaded022052d7e2cfa0743205db27ac0cd29bcae110f1ca00aeedb6f88694901a7379dc6541210320b7867e815a2b00fa935a44a4c348299f7171995c8470d8221e6485da521164', }, sequence: 4294967295, - address: - 'bitcoincash:qrxkkzsmrxcjmz8x90fx2uztt83cuu0u25vrmw66jk', - value: 0.00005, }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 1650553856 63617368746162756c6172', hex: '6a04007461620b63617368746162756c6172', type: 'nulldata', }, value: '0', }, { value: 0.000045, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 b366ef7c1ffd4ef452d72556634720cc8741e1dc OP_EQUALVERIFY OP_CHECKSIG', hex: '76a914b366ef7c1ffd4ef452d72556634720cc8741e1dc88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qzekdmmurl75aazj6uj4vc68yrxgws0pms30lsw8de', ], }, }, ], hex: '0200000001a2c7c35ed224e1c54907a1c14aa3814b56cad252ed318e671652bf60105d1c2e000000006a47304402205f6f73369ee558a8dd149480dda3d5417aab3c9bc2c4ff97aaebfc2768ceaded022052d7e2cfa0743205db27ac0cd29bcae110f1ca00aeedb6f88694901a7379dc6541210320b7867e815a2b00fa935a44a4c348299f7171995c8470d8221e6485da521164ffffffff020000000000000000176a026d021274657374696e67206d65737361676520313394110000000000001976a914b366ef7c1ffd4ef452d72556634720cc8741e1dc88ac00000000', blockhash: '000000000000000012c00755aab6cdef0806ebe24da10d78574c67558d3d816b', confirmations: 70, time: 1635511136, blocktime: 1635511136, }, + { + txid: '2e1c5d1060bf5216678e31ed52d2ca564b81a34ac1a10749c5e124d25ec3c7a2', + hash: '2e1c5d1060bf5216678e31ed52d2ca564b81a34ac1a10749c5e124d25ec3c7a2', + version: 2, + size: 225, + locktime: 0, + vin: [ + { + txid: 'cb879adc0a388416eda7289020b9d7930e9df589db375e95947968673edfb780', + vout: 0, + scriptSig: { + asm: '304402202950a75c2833d0400b38d4f47ad4af727388057bdffba90fe8950e2a69a1df8502206955582de9c6c99663895a114634e4fc03e7f2ede79555a4ad747888cdf1250d[ALL|FORKID] 0295236f3c965a760068a021a45a85f551adc2626ddc51d7539497d568affc148a', + hex: '47304402202950a75c2833d0400b38d4f47ad4af727388057bdffba90fe8950e2a69a1df8502206955582de9c6c99663895a114634e4fc03e7f2ede79555a4ad747888cdf1250d41210295236f3c965a760068a021a45a85f551adc2626ddc51d7539497d568affc148a', + }, + sequence: 4294967295, + }, + ], + vout: [ + { + value: 0.00005, + n: 0, + scriptPubKey: { + asm: 'OP_DUP OP_HASH160 cd6b0a1b19b12d88e62bd265704b59e38e71fc55 OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a914cd6b0a1b19b12d88e62bd265704b59e38e71fc5588ac', + reqSigs: 1, + type: 'pubkeyhash', + addresses: [ + 'bitcoincash:qrxkkzsmrxcjmz8x90fx2uztt83cuu0u25vrmw66jk', + ], + }, + }, + { + value: 0.00004545, + n: 1, + scriptPubKey: { + asm: 'OP_DUP OP_HASH160 09401a690d52252acd1152c2ddd36c5081dff574 OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a91409401a690d52252acd1152c2ddd36c5081dff57488ac', + reqSigs: 1, + type: 'pubkeyhash', + addresses: [ + 'bitcoincash:qqy5qxnfp4fz22kdz9fv9hwnd3ggrhl4wsekqyswf0', + ], + }, + }, + ], + hex: '020000000180b7df3e67687994955e37db89f59d0e93d7b9209028a7ed1684380adc9a87cb000000006a47304402202950a75c2833d0400b38d4f47ad4af727388057bdffba90fe8950e2a69a1df8502206955582de9c6c99663895a114634e4fc03e7f2ede79555a4ad747888cdf1250d41210295236f3c965a760068a021a45a85f551adc2626ddc51d7539497d568affc148affffffff0288130000000000001976a914cd6b0a1b19b12d88e62bd265704b59e38e71fc5588acc1110000000000001976a91409401a690d52252acd1152c2ddd36c5081dff57488ac00000000', + blockhash: + '00000000000000000ee5254eb809e121138497bdd9b5053cf2738714220b1ce2', + confirmations: 5625, + time: 1635509252, + blocktime: 1635509252, + }, ]; diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js index 4da183fb1..e50930252 100644 --- a/web/cashtab/src/hooks/__tests__/useBCH.test.js +++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js @@ -1,479 +1,513 @@ /* eslint-disable no-native-reassign */ import useBCH from '../useBCH'; import mockReturnGetHydratedUtxoDetails from '../__mocks__/mockReturnGetHydratedUtxoDetails'; import mockReturnGetSlpBalancesAndUtxos from '../__mocks__/mockReturnGetSlpBalancesAndUtxos'; import mockReturnGetHydratedUtxoDetailsWithZeroBalance from '../__mocks__/mockReturnGetHydratedUtxoDetailsWithZeroBalance'; import mockReturnGetSlpBalancesAndUtxosNoZeroBalance from '../__mocks__/mockReturnGetSlpBalancesAndUtxosNoZeroBalance'; import sendBCHMock from '../__mocks__/sendBCH'; import createTokenMock from '../__mocks__/createToken'; import mockTxHistory from '../__mocks__/mockTxHistory'; import mockFlatTxHistory from '../__mocks__/mockFlatTxHistory'; import mockTxDataWithPassthrough from '../__mocks__/mockTxDataWithPassthrough'; +import mockPublicKeys from '../__mocks__/mockPublicKeys'; import { flattenedHydrateUtxosResponse, legacyHydrateUtxosResponse, } from '../__mocks__/mockHydrateUtxosBatched'; import { tokenSendWdt, tokenReceiveGarmonbozia, tokenReceiveTBS, tokenGenesisCashtabMintAlpha, } from '../__mocks__/mockParseTokenInfoForTxHistory'; import { mockSentCashTx, mockReceivedCashTx, mockSentTokenTx, mockReceivedTokenTx, mockSentOpReturnMessageTx, mockReceivedOpReturnMessageTx, } from '../__mocks__/mockParsedTxs'; import BCHJS from '@psf/bch-js'; // TODO: should be removed when external lib not needed anymore import { currency } from '../../components/Common/Ticker'; import BigNumber from 'bignumber.js'; import { fromSmallestDenomination } from '@utils/cashMethods'; describe('useBCH hook', () => { it('gets Rest Api Url on testnet', () => { process = { env: { REACT_APP_NETWORK: `testnet`, REACT_APP_BCHA_APIS: 'https://rest.kingbch.com/v3/,https://wallet-service-prod.bitframe.org/v3/,notevenaurl,https://rest.kingbch.com/v3/', REACT_APP_BCHA_APIS_TEST: 'https://free-test.fullstack.cash/v3/', }, }; const { getRestUrl } = useBCH(); const expectedApiUrl = `https://free-test.fullstack.cash/v3/`; expect(getRestUrl(0)).toBe(expectedApiUrl); }); it('gets primary Rest API URL on mainnet', () => { process = { env: { REACT_APP_BCHA_APIS: 'https://rest.kingbch.com/v3/,https://wallet-service-prod.bitframe.org/v3/,notevenaurl,https://rest.kingbch.com/v3/', REACT_APP_NETWORK: 'mainnet', }, }; const { getRestUrl } = useBCH(); const expectedApiUrl = `https://rest.kingbch.com/v3/`; expect(getRestUrl(0)).toBe(expectedApiUrl); }); it('calculates fee correctly for 2 P2PKH outputs', () => { const { calcFee } = useBCH(); const BCH = new BCHJS(); const utxosMock = [{}, {}]; expect(calcFee(BCH, utxosMock, 2, 1.01)).toBe(378); }); it('gets SLP and BCH balances and utxos from hydrated utxo details', async () => { const { getSlpBalancesAndUtxos } = useBCH(); const BCH = new BCHJS(); const result = await getSlpBalancesAndUtxos( BCH, mockReturnGetHydratedUtxoDetails, ); expect(result).toStrictEqual(mockReturnGetSlpBalancesAndUtxos); }); it(`Ignores SLP utxos with utxo.tokenQty === '0'`, async () => { const { getSlpBalancesAndUtxos } = useBCH(); const BCH = new BCHJS(); const result = await getSlpBalancesAndUtxos( BCH, mockReturnGetHydratedUtxoDetailsWithZeroBalance, ); expect(result).toStrictEqual( mockReturnGetSlpBalancesAndUtxosNoZeroBalance, ); }); it(`Parses flattened batched hydrateUtxosResponse to yield same result as legacy unbatched hydrateUtxosResponse`, async () => { const { getSlpBalancesAndUtxos } = useBCH(); const BCH = new BCHJS(); const batchedResult = await getSlpBalancesAndUtxos( BCH, flattenedHydrateUtxosResponse, ); const legacyResult = await getSlpBalancesAndUtxos( BCH, legacyHydrateUtxosResponse, ); expect(batchedResult).toStrictEqual(legacyResult); }); it('sends XEC correctly', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, utxos, wallet, destinationAddress, sendAmount, } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect( await sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( expectedHex, ); }); it('sends one to many XEC correctly', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, utxos, wallet, destinationAddress, sendAmount, } = sendBCHMock; const addressAndValueArray = [ 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6', 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6.8', 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,7', 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6', ]; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect( await sendXec( BCH, wallet, utxos, currency.defaultFee, '', true, addressAndValueArray, ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); }); it(`Throws error if called trying to send one base unit ${currency.ticker} more than available in utxo set`, async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, utxos, wallet, destinationAddress } = sendBCHMock; const expectedTxFeeInSats = 229; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); const oneBaseUnitMoreThanBalance = new BigNumber(utxos[0].value) .minus(expectedTxFeeInSats) .plus(1) .div(10 ** currency.cashDecimals) .toString(); const failedSendBch = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, oneBaseUnitMoreThanBalance, ); expect(failedSendBch).rejects.toThrow(new Error('Insufficient funds')); const nullValuesSendBch = await sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, null, ); expect(nullValuesSendBch).toBe(null); }); it('Throws error on attempt to send one satoshi less than backend dust limit', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, utxos, wallet, destinationAddress } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); const failedSendBch = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, new BigNumber( fromSmallestDenomination(currency.dustSats).toString(), ) .minus(new BigNumber('0.00000001')) .toString(), ); expect(failedSendBch).rejects.toThrow(new Error('dust')); const nullValuesSendBch = await sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, null, ); expect(nullValuesSendBch).toBe(null); }); it('receives errors from the network and parses it', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { sendAmount, utxos, wallet, destinationAddress } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockImplementation(async () => { throw new Error('insufficient priority (code 66)'); }); const insufficientPriority = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(insufficientPriority).rejects.toThrow( new Error('insufficient priority (code 66)'), ); BCH.RawTransactions.sendRawTransaction = jest .fn() .mockImplementation(async () => { throw new Error('txn-mempool-conflict (code 18)'); }); const txnMempoolConflict = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(txnMempoolConflict).rejects.toThrow( new Error('txn-mempool-conflict (code 18)'), ); BCH.RawTransactions.sendRawTransaction = jest .fn() .mockImplementation(async () => { throw new Error('Network Error'); }); const networkError = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(networkError).rejects.toThrow(new Error('Network Error')); BCH.RawTransactions.sendRawTransaction = jest .fn() .mockImplementation(async () => { const err = new Error( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)', ); throw err; }); const tooManyAncestorsMempool = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(tooManyAncestorsMempool).rejects.toThrow( new Error( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)', ), ); }); it('creates a token correctly', async () => { const { createToken } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, wallet, configObj } = createTokenMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect(await createToken(BCH, wallet, 5.01, configObj)).toBe( `${currency.tokenExplorerUrl}/tx/${expectedTxId}`, ); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( expectedHex, ); }); it('Throws correct error if user attempts to create a token with an invalid wallet', async () => { const { createToken } = useBCH(); const BCH = new BCHJS(); const { invalidWallet, configObj } = createTokenMock; const invalidWalletTokenCreation = createToken( BCH, invalidWallet, currency.defaultFee, configObj, ); await expect(invalidWalletTokenCreation).rejects.toThrow( new Error('Invalid wallet'), ); }); it('Correctly flattens transaction history', () => { const { flattenTransactions } = useBCH(); expect(flattenTransactions(mockTxHistory, 10)).toStrictEqual( mockFlatTxHistory, ); }); - it(`Correctly parses a "send ${currency.ticker}" transaction`, () => { + it(`Correctly parses a "send ${currency.ticker}" transaction`, async () => { const { parseTxData } = useBCH(); - expect(parseTxData([mockTxDataWithPassthrough[0]])).toStrictEqual( - mockSentCashTx, - ); + const BCH = new BCHJS(); + expect( + await parseTxData( + BCH, + [mockTxDataWithPassthrough[0]], + mockPublicKeys, + ), + ).toStrictEqual(mockSentCashTx); }); - it(`Correctly parses a "receive ${currency.ticker}" transaction`, () => { + it(`Correctly parses a "receive ${currency.ticker}" transaction`, async () => { const { parseTxData } = useBCH(); - expect(parseTxData([mockTxDataWithPassthrough[5]])).toStrictEqual( - mockReceivedCashTx, - ); + const BCH = new BCHJS(); + expect( + await parseTxData( + BCH, + [mockTxDataWithPassthrough[5]], + mockPublicKeys, + ), + ).toStrictEqual(mockReceivedCashTx); }); - it(`Correctly parses a "send ${currency.tokenTicker}" transaction`, () => { + it(`Correctly parses a "send ${currency.tokenTicker}" transaction`, async () => { const { parseTxData } = useBCH(); - expect(parseTxData([mockTxDataWithPassthrough[1]])).toStrictEqual( - mockSentTokenTx, - ); + const BCH = new BCHJS(); + expect( + await parseTxData( + BCH, + [mockTxDataWithPassthrough[1]], + mockPublicKeys, + ), + ).toStrictEqual(mockSentTokenTx); }); - it(`Correctly parses a "receive ${currency.tokenTicker}" transaction`, () => { + it(`Correctly parses a "receive ${currency.tokenTicker}" transaction`, async () => { const { parseTxData } = useBCH(); - expect(parseTxData([mockTxDataWithPassthrough[3]])).toStrictEqual( - mockReceivedTokenTx, - ); + const BCH = new BCHJS(); + expect( + await parseTxData( + BCH, + [mockTxDataWithPassthrough[3]], + mockPublicKeys, + ), + ).toStrictEqual(mockReceivedTokenTx); }); it(`Correctly parses a "send ${currency.tokenTicker}" transaction with token details`, () => { const { parseTokenInfoForTxHistory } = useBCH(); const BCH = new BCHJS(); expect( parseTokenInfoForTxHistory( BCH, tokenSendWdt.parsedTx, tokenSendWdt.tokenInfo, ), ).toStrictEqual(tokenSendWdt.cashtabTokenInfo); }); it(`Correctly parses a "receive ${currency.tokenTicker}" transaction with token details and 9 decimals of precision`, () => { const { parseTokenInfoForTxHistory } = useBCH(); const BCH = new BCHJS(); expect( parseTokenInfoForTxHistory( BCH, tokenReceiveTBS.parsedTx, tokenReceiveTBS.tokenInfo, ), ).toStrictEqual(tokenReceiveTBS.cashtabTokenInfo); }); it(`Correctly parses a "receive ${currency.tokenTicker}" transaction from an HD wallet (change address different from sending address)`, () => { const { parseTokenInfoForTxHistory } = useBCH(); const BCH = new BCHJS(); expect( parseTokenInfoForTxHistory( BCH, tokenReceiveGarmonbozia.parsedTx, tokenReceiveGarmonbozia.tokenInfo, ), ).toStrictEqual(tokenReceiveGarmonbozia.cashtabTokenInfo); }); it(`Correctly parses a "GENESIS ${currency.tokenTicker}" transaction with token details`, () => { const { parseTokenInfoForTxHistory } = useBCH(); const BCH = new BCHJS(); expect( parseTokenInfoForTxHistory( BCH, tokenGenesisCashtabMintAlpha.parsedTx, tokenGenesisCashtabMintAlpha.tokenInfo, ), ).toStrictEqual(tokenGenesisCashtabMintAlpha.cashtabTokenInfo); }); - it(`Correctly parses a "send ${currency.ticker}" transaction with an OP_RETURN message`, () => { + it(`Correctly parses a "send ${currency.ticker}" transaction with an OP_RETURN message`, async () => { const { parseTxData } = useBCH(); - expect(parseTxData([mockTxDataWithPassthrough[10]])).toStrictEqual( - mockSentOpReturnMessageTx, - ); + const BCH = new BCHJS(); + expect( + await parseTxData( + BCH, + [mockTxDataWithPassthrough[10]], + mockPublicKeys, + ), + ).toStrictEqual(mockSentOpReturnMessageTx); }); - it(`Correctly parses a "receive ${currency.ticker}" transaction with an OP_RETURN message`, () => { + it(`Correctly parses a "receive ${currency.ticker}" transaction with an OP_RETURN message`, async () => { const { parseTxData } = useBCH(); - expect(parseTxData([mockTxDataWithPassthrough[11]])).toStrictEqual( - mockReceivedOpReturnMessageTx, - ); + const BCH = new BCHJS(); + BCH.RawTransactions.getRawTransaction = jest + .fn() + .mockResolvedValue(mockTxDataWithPassthrough[12]); + expect( + await parseTxData( + BCH, + [mockTxDataWithPassthrough[11]], + mockPublicKeys, + ), + ).toStrictEqual(mockReceivedOpReturnMessageTx); }); }); diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js index 4baea102e..abdf5a37a 100644 --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -1,1244 +1,1291 @@ import BigNumber from 'bignumber.js'; import { currency, isCashtabOutput, isEtokenOutput, extractCashtabMessage, extractExternalMessage, parseOpReturn, } from '@components/Common/Ticker'; import { isValidTokenStats } from '@utils/validation'; import SlpWallet from 'minimal-slp-wallet'; import { toSmallestDenomination, fromSmallestDenomination, batchArray, flattenBatchedHydratedUtxos, isValidStoredWallet, checkNullUtxosForTokenStatus, confirmNonEtokenUtxos, } from '@utils/cashMethods'; import cashaddr from 'ecashaddrjs'; export default function useBCH() { const SEND_BCH_ERRORS = { INSUFFICIENT_FUNDS: 0, NETWORK_ERROR: 1, INSUFFICIENT_PRIORITY: 66, // ~insufficient fee DOUBLE_SPENDING: 18, MAX_UNCONFIRMED_TXS: 64, }; const getRestUrl = (apiIndex = 0) => { const apiString = process.env.REACT_APP_NETWORK === `mainnet` ? process.env.REACT_APP_BCHA_APIS : process.env.REACT_APP_BCHA_APIS_TEST; const apiArray = apiString.split(','); return apiArray[apiIndex]; }; const flattenTransactions = ( txHistory, txCount = currency.txHistoryCount, ) => { /* Convert txHistory, format [{address: '', transactions: [{height: '', tx_hash: ''}, ...{}]}, {}, {}] to flatTxHistory [{txid: '', blockheight: '', address: ''}] sorted by blockheight, newest transactions to oldest transactions */ let flatTxHistory = []; let includedTxids = []; for (let i = 0; i < txHistory.length; i += 1) { const { address, transactions } = txHistory[i]; for (let j = transactions.length - 1; j >= 0; j -= 1) { let flatTx = {}; flatTx.address = address; // If tx is unconfirmed, give arbitrarily high blockheight flatTx.height = transactions[j].height <= 0 ? 10000000 : transactions[j].height; flatTx.txid = transactions[j].tx_hash; // Only add this tx if the same transaction is not already in the array // This edge case can happen with older wallets, txs can be on multiple paths if (!includedTxids.includes(flatTx.txid)) { includedTxids.push(flatTx.txid); flatTxHistory.push(flatTx); } } } // Sort with most recent transaction at index 0 flatTxHistory.sort((a, b) => b.height - a.height); // Only return 10 return flatTxHistory.splice(0, txCount); }; - const parseTxData = txData => { + const parseTxData = async (BCH, txData, publicKeys) => { /* Desired output [ { txid: '', type: send, receive receivingAddress: '', quantity: amount bcha token: true/false tokenInfo: { tokenId: tokenQty: txType: mint, send, other } opReturnMessage: 'message extracted from asm' or '' } ] */ const parsedTxHistory = []; for (let i = 0; i < txData.length; i += 1) { const tx = txData[i]; const parsedTx = {}; // Move over info that does not need to be calculated parsedTx.txid = tx.txid; parsedTx.height = tx.height; let destinationAddress = tx.address; - // If this tx had too many inputs to be parsed by getTxDataWithPassThrough, skip it - // When this occurs, the tx will only have txid and height + // if there was an error in getting the tx data from api, the tx will only have txid and height // So, it will not have 'vin' if (!Object.keys(tx).includes('vin')) { // Populate as a limited-info tx that can be expanded in a block explorer parsedTxHistory.push(parsedTx); continue; } parsedTx.confirmations = tx.confirmations; parsedTx.blocktime = tx.blocktime; let amountSent = 0; let amountReceived = 0; let opReturnMessage = ''; let isCashtabMessage = false; // Assume an incoming transaction let outgoingTx = false; let tokenTx = false; let substring = ''; - // get the address of the sender for this tx and encode into eCash address - let senderBchAddress = tx.vin[0].address; - const { type, hash } = cashaddr.decode(senderBchAddress); - const senderAddress = cashaddr.encode('ecash', type, hash); - - // If vin includes tx address, this is an outgoing tx - // Note that with bch-input data, we do not have input amounts + // If vin's scriptSig contains one of the publicKeys of this wallet + // This is an outgoing tx for (let j = 0; j < tx.vin.length; j += 1) { - const thisInput = tx.vin[j]; - if (thisInput.address === tx.address) { - // This is an outgoing transaction - outgoingTx = true; + // Since Cashtab only concerns with utxos of Path145, Path245 and Path1899 addresses, + // which are hashes of thier public keys. We can safely assume that Cashtab can only + // consumes utxos of type 'pubkeyhash' + // Therefore, only tx with vin's scriptSig of type 'pubkeyhash' can potentially be an outgoing tx. + // any other scriptSig type indicates that the tx is incoming. + try { + const thisInputScriptSig = tx.vin[j].scriptSig; + let inputPubKey = undefined; + const inputType = BCH.Script.classifyInput( + BCH.Script.decode( + Buffer.from(thisInputScriptSig.hex, 'hex'), + ), + ); + if (inputType === 'pubkeyhash') { + inputPubKey = thisInputScriptSig.hex.substring( + thisInputScriptSig.hex.length - 66, + ); + } + publicKeys.forEach(pubKey => { + if (pubKey === inputPubKey) { + // This is an outgoing transaction + outgoingTx = true; + } + }); + if (outgoingTx === true) break; + } catch (err) { + console.log( + "useBCH.parsedTxHistory() error: in trying to classify Input' scriptSig", + ); } } + // Iterate over vout to find how much was sent or received for (let j = 0; j < tx.vout.length; j += 1) { const thisOutput = tx.vout[j]; // If there is no addresses object in the output, it's either an OP_RETURN msg or token tx if ( !Object.keys(thisOutput.scriptPubKey).includes('addresses') ) { let hex = thisOutput.scriptPubKey.hex; let parsedOpReturnArray = parseOpReturn(hex); if (!parsedOpReturnArray) { console.log( 'useBCH.parsedTxData() error: parsed array is empty', ); break; } let message = ''; let txType = parsedOpReturnArray[0]; if (txType === currency.opReturn.appPrefixesHex.eToken) { // this is an eToken transaction tokenTx = true; } else if ( txType === currency.opReturn.appPrefixesHex.cashtab ) { // this is a Cashtab message try { opReturnMessage = Buffer.from( parsedOpReturnArray[1], 'hex', ); isCashtabMessage = true; } catch (err) { // soft error if an unexpected or invalid cashtab hex is encountered opReturnMessage = ''; console.log( 'useBCH.parsedTxData() error: invalid cashtab msg hex: ' + parsedOpReturnArray[1], ); } } else { // this is an externally generated message message = txType; // index 0 is the message content in this instance // if there are more than one part to the external message const arrayLength = parsedOpReturnArray.length; for (let i = 1; i < arrayLength; i++) { message = message + parsedOpReturnArray[i]; } try { opReturnMessage = Buffer.from(message, 'hex'); } catch (err) { // soft error if an unexpected or invalid cashtab hex is encountered opReturnMessage = ''; console.log( 'useBCH.parsedTxData() error: invalid external msg hex: ' + substring, ); } } continue; // skipping the remainder of tx data parsing logic in both token and OP_RETURN tx cases } if ( thisOutput.scriptPubKey.addresses && thisOutput.scriptPubKey.addresses[0] === tx.address ) { if (outgoingTx) { // This amount is change continue; } amountReceived += thisOutput.value; } else if (outgoingTx) { amountSent += thisOutput.value; // Assume there's only one destination address, i.e. it was sent by a Cashtab wallet destinationAddress = thisOutput.scriptPubKey.addresses[0]; } } + + // If the tx is incoming and have a message attached + // get the address of the sender for this tx and encode into eCash address + let senderAddress = null; + if (!outgoingTx && opReturnMessage !== '') { + const firstVin = tx.vin[0]; + try { + // get the tx that generated the first vin of this tx + const firstVinTxData = + await BCH.RawTransactions.getRawTransaction( + firstVin.txid, + true, + ); + // extract the address of the tx output + let senderBchAddress = + firstVinTxData.vout[firstVin.vout].scriptPubKey + .addresses[0]; + const { prefix, type, hash } = + cashaddr.decode(senderBchAddress); + senderAddress = cashaddr.encode('ecash', type, hash); + } catch (err) { + console.log( + `Error in BCH.RawTransactions.getRawTransaction(${firstVin.txid}, true)`, + ); + } + } // Construct parsedTx parsedTx.amountSent = amountSent; parsedTx.amountReceived = amountReceived; parsedTx.tokenTx = tokenTx; parsedTx.outgoingTx = outgoingTx; parsedTx.replyAddress = senderAddress; parsedTx.destinationAddress = destinationAddress; parsedTx.opReturnMessage = opReturnMessage; parsedTx.isCashtabMessage = isCashtabMessage; parsedTxHistory.push(parsedTx); } return parsedTxHistory; }; const getTxHistory = async (BCH, addresses) => { let txHistoryResponse; try { //console.log(`API Call: BCH.Electrumx.utxo(addresses)`); //console.log(addresses); txHistoryResponse = await BCH.Electrumx.transactions(addresses); //console.log(`BCH.Electrumx.transactions(addresses) succeeded`); //console.log(`txHistoryResponse`, txHistoryResponse); if (txHistoryResponse.success && txHistoryResponse.transactions) { return txHistoryResponse.transactions; } else { // eslint-disable-next-line no-throw-literal throw new Error('Error in getTxHistory'); } } catch (err) { console.log(`Error in BCH.Electrumx.transactions(addresses):`); console.log(err); return err; } }; const getTxDataWithPassThrough = async (BCH, flatTx) => { - // necessary as BCH.RawTransactions.getTxData does not return address or blockheight + // necessary as BCH.RawTransactions.getRawTransaction does not return address or blockheight let txDataWithPassThrough = {}; try { - txDataWithPassThrough = await BCH.RawTransactions.getTxData( + txDataWithPassThrough = await BCH.RawTransactions.getRawTransaction( flatTx.txid, + true, ); } catch (err) { console.log( - `Error in BCH.RawTransactions.getTxData(${flatTx.txid})`, + `Error in BCH.RawTransactions.getRawTransaction(${flatTx.txid}, true)`, ); console.log(err); // Include txid if you don't get it from the attempted response txDataWithPassThrough.txid = flatTx.txid; } txDataWithPassThrough.height = flatTx.height; txDataWithPassThrough.address = flatTx.address; return txDataWithPassThrough; }; - const getTxData = async (BCH, txHistory) => { + const getTxData = async (BCH, txHistory, publicKeys) => { // Flatten tx history let flatTxs = flattenTransactions(txHistory); // Build array of promises to get tx data for all 10 transactions let txDataPromises = []; for (let i = 0; i < flatTxs.length; i += 1) { const txDataPromise = await getTxDataWithPassThrough( BCH, flatTxs[i], ); txDataPromises.push(txDataPromise); } // Get txData for the 10 most recent transactions let txDataPromiseResponse; try { txDataPromiseResponse = await Promise.all(txDataPromises); - const parsed = parseTxData(txDataPromiseResponse); + const parsed = parseTxData(BCH, txDataPromiseResponse, publicKeys); return parsed; } catch (err) { console.log(`Error in Promise.all(txDataPromises):`); console.log(err); return err; } }; const parseTokenInfoForTxHistory = (BCH, parsedTx, tokenInfo) => { // Address at which the eToken was received const { destinationAddress } = parsedTx; // Here in cashtab, destinationAddress is in bitcoincash: format // In the API response of tokenInfo, this will be in simpleledger: format // So, must convert to simpleledger const receivingSlpAddress = BCH.SLP.Address.toSLPAddress(destinationAddress); const { transactionType, sendInputsFull, sendOutputsFull } = tokenInfo; const sendingTokenAddresses = []; // Scan over inputs to find out originating addresses for (let i = 0; i < sendInputsFull.length; i += 1) { const sendingAddress = sendInputsFull[i].address; sendingTokenAddresses.push(sendingAddress); } // Scan over outputs to find out how much was sent let qtySent = new BigNumber(0); let qtyReceived = new BigNumber(0); for (let i = 0; i < sendOutputsFull.length; i += 1) { if (sendingTokenAddresses.includes(sendOutputsFull[i].address)) { // token change and should be ignored, unless it's a genesis transaction // then this is the amount created if (transactionType === 'GENESIS') { qtyReceived = qtyReceived.plus( new BigNumber(sendOutputsFull[i].amount), ); } continue; } if (parsedTx.outgoingTx) { qtySent = qtySent.plus( new BigNumber(sendOutputsFull[i].amount), ); } else { // Only if this matches the receiving address if (sendOutputsFull[i].address === receivingSlpAddress) { qtyReceived = qtyReceived.plus( new BigNumber(sendOutputsFull[i].amount), ); } } } const cashtabTokenInfo = {}; cashtabTokenInfo.qtySent = qtySent.toString(); cashtabTokenInfo.qtyReceived = qtyReceived.toString(); cashtabTokenInfo.tokenId = tokenInfo.tokenIdHex; cashtabTokenInfo.tokenName = tokenInfo.tokenName; cashtabTokenInfo.tokenTicker = tokenInfo.tokenTicker; cashtabTokenInfo.transactionType = transactionType; return cashtabTokenInfo; }; const addTokenTxDataToSingleTx = async (BCH, parsedTx) => { // Accept one parsedTx // If it's not a token tx, just return it as is and do not parse for token data if (!parsedTx.tokenTx) { return parsedTx; } // If it could be a token tx, do an API call to get token info and return it let tokenData; try { tokenData = await BCH.SLP.Utils.txDetails(parsedTx.txid); } catch (err) { console.log( `Error in parsing BCH.SLP.Utils.txDetails(${parsedTx.txid})`, ); console.log(err); // This is not a token tx parsedTx.tokenTx = false; return parsedTx; } const { tokenInfo } = tokenData; parsedTx.tokenInfo = parseTokenInfoForTxHistory( BCH, parsedTx, tokenInfo, ); return parsedTx; }; const addTokenTxData = async (BCH, parsedTxs) => { // Collect all txids for token transactions into array of promises // Promise.all to get their tx history // Add a tokeninfo object to parsedTxs for token txs // Get txData for the 10 most recent transactions // Build array of promises to get tx data for all 10 transactions let tokenTxDataPromises = []; for (let i = 0; i < parsedTxs.length; i += 1) { const txDataPromise = await addTokenTxDataToSingleTx( BCH, parsedTxs[i], ); tokenTxDataPromises.push(txDataPromise); } let tokenTxDataPromiseResponse; try { tokenTxDataPromiseResponse = await Promise.all(tokenTxDataPromises); return tokenTxDataPromiseResponse; } catch (err) { console.log(`Error in Promise.all(tokenTxDataPromises):`); console.log(err); return err; } }; // Split out the BCH.Electrumx.utxo(addresses) call from the getSlpBalancesandUtxos function // If utxo set has not changed, you do not need to hydrate the utxo set // This drastically reduces calls to the API const getUtxos = async (BCH, addresses) => { let utxosResponse; try { //console.log(`API Call: BCH.Electrumx.utxo(addresses)`); //console.log(addresses); utxosResponse = await BCH.Electrumx.utxo(addresses); //console.log(`BCH.Electrumx.utxo(addresses) succeeded`); //console.log(`utxosResponse`, utxosResponse); return utxosResponse.utxos; } catch (err) { console.log(`Error in BCH.Electrumx.utxo(addresses):`); return err; } }; const getHydratedUtxoDetails = async (BCH, utxos) => { const hydrateUtxosPromises = []; for (let i = 0; i < utxos.length; i += 1) { let thisAddress = utxos[i].address; let theseUtxos = utxos[i].utxos; const batchedUtxos = batchArray( theseUtxos, currency.hydrateUtxoBatchSize, ); // Iterate over each utxo in this address field for (let j = 0; j < batchedUtxos.length; j += 1) { const utxoSetForThisPromise = [ { utxos: batchedUtxos[j], address: thisAddress }, ]; const thisPromise = BCH.SLP.Utils.hydrateUtxos( utxoSetForThisPromise, ); hydrateUtxosPromises.push(thisPromise); } } let hydratedUtxoDetails; try { hydratedUtxoDetails = await Promise.all(hydrateUtxosPromises); const flattenedBatchedHydratedUtxos = flattenBatchedHydratedUtxos(hydratedUtxoDetails); return flattenedBatchedHydratedUtxos; } catch (err) { console.log(`Error in Promise.all(hydrateUtxosPromises)`); console.log(err); return err; } }; const fetchTxDataForNullUtxos = async (BCH, nullUtxos) => { // Check nullUtxos. If they aren't eToken txs, count them console.log( `Null utxos found, checking OP_RETURN fields to confirm they are not eToken txs.`, ); const txids = []; for (let i = 0; i < nullUtxos.length; i += 1) { // Batch API call to get their OP_RETURN asm info txids.push(nullUtxos[i].tx_hash); } let nullUtxoTxData; try { nullUtxoTxData = await BCH.Electrumx.txData(txids); console.log(`nullUtxoTxData`, nullUtxoTxData.transactions); // Scan tx data for each utxo to confirm they are not eToken txs const txDataResults = nullUtxoTxData.transactions; const nonEtokenUtxos = checkNullUtxosForTokenStatus(txDataResults); return nonEtokenUtxos; } catch (err) { console.log(`Error in checkNullUtxosForTokenStatus(nullUtxos)`); console.log(`nullUtxos`, nullUtxos); // If error, ignore these utxos, will be updated next utxo set refresh return []; } }; const getSlpBalancesAndUtxos = async (BCH, hydratedUtxoDetails) => { let hydratedUtxos = []; for (let i = 0; i < hydratedUtxoDetails.slpUtxos.length; i += 1) { const hydratedUtxosAtAddress = hydratedUtxoDetails.slpUtxos[i]; for (let j = 0; j < hydratedUtxosAtAddress.utxos.length; j += 1) { const hydratedUtxo = hydratedUtxosAtAddress.utxos[j]; hydratedUtxo.address = hydratedUtxosAtAddress.address; hydratedUtxos.push(hydratedUtxo); } } //console.log(`hydratedUtxos`, hydratedUtxos); // WARNING // If you hit rate limits, your above utxos object will come back with `isValid` as null, but otherwise ok // You need to throw an error before setting nonSlpUtxos and slpUtxos in this case const nullUtxos = hydratedUtxos.filter(utxo => utxo.isValid === null); if (nullUtxos.length > 0) { console.log(`${nullUtxos.length} null utxos found!`); console.log('nullUtxos', nullUtxos); const nullNonEtokenUtxos = await fetchTxDataForNullUtxos( BCH, nullUtxos, ); // Set isValid === false for nullUtxos that are confirmed non-eToken hydratedUtxos = confirmNonEtokenUtxos( hydratedUtxos, nullNonEtokenUtxos, ); } // Prevent app from treating slpUtxos as nonSlpUtxos // Must enforce === false as api will occasionally return utxo.isValid === null // Do not classify any utxos that include token information as nonSlpUtxos const nonSlpUtxos = hydratedUtxos.filter( utxo => utxo.isValid === false && utxo.value !== currency.etokenSats && !utxo.tokenName, ); // To be included in slpUtxos, the utxo must // have utxo.isValid = true // If utxo has a utxo.tokenQty field, i.e. not a minting baton, then utxo.tokenQty !== '0' const slpUtxos = hydratedUtxos.filter( utxo => utxo.isValid && !(utxo.tokenQty === '0'), ); let tokensById = {}; slpUtxos.forEach(slpUtxo => { let token = tokensById[slpUtxo.tokenId]; if (token) { // Minting baton does nto have a slpUtxo.tokenQty type if (slpUtxo.tokenQty) { token.balance = token.balance.plus( new BigNumber(slpUtxo.tokenQty), ); } //token.hasBaton = slpUtxo.transactionType === "genesis"; if (slpUtxo.utxoType && !token.hasBaton) { token.hasBaton = slpUtxo.utxoType === 'minting-baton'; } // Examples of slpUtxo /* Genesis transaction: { address: "bitcoincash:qrhzv5t79e2afc3rdutcu0d3q20gl7ul3ue58whah6" decimals: 9 height: 617564 isValid: true satoshis: 546 tokenDocumentHash: "" tokenDocumentUrl: "developer.bitcoin.com" tokenId: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199" tokenName: "PiticoLaunch" tokenTicker: "PTCL" tokenType: 1 tx_hash: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199" tx_pos: 2 txid: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199" utxoType: "minting-baton" value: 546 vout: 2 } Send transaction: { address: "bitcoincash:qrhzv5t79e2afc3rdutcu0d3q20gl7ul3ue58whah6" decimals: 9 height: 655115 isValid: true satoshis: 546 tokenDocumentHash: "" tokenDocumentUrl: "developer.bitcoin.com" tokenId: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199" tokenName: "PiticoLaunch" tokenQty: 1.123456789 tokenTicker: "PTCL" tokenType: 1 transactionType: "send" tx_hash: "dea400f963bc9f51e010f88533010f8d1f82fc2bcc485ff8500c3a82b25abd9e" tx_pos: 1 txid: "dea400f963bc9f51e010f88533010f8d1f82fc2bcc485ff8500c3a82b25abd9e" utxoType: "token" value: 546 vout: 1 } */ } else { token = {}; token.info = slpUtxo; token.tokenId = slpUtxo.tokenId; if (slpUtxo.tokenQty) { token.balance = new BigNumber(slpUtxo.tokenQty); } else { token.balance = new BigNumber(0); } if (slpUtxo.utxoType) { token.hasBaton = slpUtxo.utxoType === 'minting-baton'; } else { token.hasBaton = false; } tokensById[slpUtxo.tokenId] = token; } }); const tokens = Object.values(tokensById); // console.log(`tokens`, tokens); return { tokens, nonSlpUtxos, slpUtxos, }; }; const calcFee = ( BCH, utxos, p2pkhOutputNumber = 2, satoshisPerByte = currency.defaultFee, ) => { const byteCount = BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, ); const txFee = Math.ceil(satoshisPerByte * byteCount); return txFee; }; const createToken = async (BCH, wallet, feeInSatsPerByte, configObj) => { try { // Throw error if wallet does not have utxo set in state if (!isValidStoredWallet(wallet)) { const walletError = new Error(`Invalid wallet`); throw walletError; } const utxos = wallet.state.slpBalancesAndUtxos.nonSlpUtxos; const CREATION_ADDR = wallet.Path1899.cashAddress; const inputUtxos = []; let transactionBuilder; // instance of transaction builder if (process.env.REACT_APP_NETWORK === `mainnet`) transactionBuilder = new BCH.TransactionBuilder(); else transactionBuilder = new BCH.TransactionBuilder('testnet'); let originalAmount = new BigNumber(0); let txFee = 0; for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i]; originalAmount = originalAmount.plus(new BigNumber(utxo.value)); const vout = utxo.vout; const txid = utxo.txid; // add input with txid and index of vout transactionBuilder.addInput(txid, vout); inputUtxos.push(utxo); txFee = calcFee(BCH, inputUtxos, 3, feeInSatsPerByte); if ( originalAmount .minus(new BigNumber(currency.etokenSats)) .minus(new BigNumber(txFee)) .gte(0) ) { break; } } // amount to send back to the remainder address. const remainder = originalAmount .minus(new BigNumber(currency.etokenSats)) .minus(new BigNumber(txFee)); if (remainder.lt(0)) { const error = new Error(`Insufficient funds`); error.code = SEND_BCH_ERRORS.INSUFFICIENT_FUNDS; throw error; } // Generate the OP_RETURN entry for an SLP GENESIS transaction. const script = BCH.SLP.TokenType1.generateGenesisOpReturn(configObj); // OP_RETURN needs to be the first output in the transaction. transactionBuilder.addOutput(script, 0); // add output w/ address and amount to send transactionBuilder.addOutput(CREATION_ADDR, currency.etokenSats); // Send change to own address if (remainder.gte(new BigNumber(currency.etokenSats))) { transactionBuilder.addOutput( CREATION_ADDR, parseInt(remainder), ); } // Sign the transactions with the HD node. for (let i = 0; i < inputUtxos.length; i++) { const utxo = inputUtxos[i]; transactionBuilder.sign( i, BCH.ECPair.fromWIF(utxo.wif), undefined, transactionBuilder.hashTypes.SIGHASH_ALL, utxo.value, ); } // build tx const tx = transactionBuilder.build(); // output rawhex const hex = tx.toHex(); // Broadcast transaction to the network const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]); if (txidStr && txidStr[0]) { console.log(`${currency.ticker} txid`, txidStr[0]); } let link; if (process.env.REACT_APP_NETWORK === `mainnet`) { link = `${currency.tokenExplorerUrl}/tx/${txidStr}`; } else { link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`; } //console.log(`link`, link); return link; } catch (err) { if (err.error === 'insufficient priority (code 66)') { err.code = SEND_BCH_ERRORS.INSUFFICIENT_PRIORITY; } else if (err.error === 'txn-mempool-conflict (code 18)') { err.code = SEND_BCH_ERRORS.DOUBLE_SPENDING; } else if (err.error === 'Network Error') { err.code = SEND_BCH_ERRORS.NETWORK_ERROR; } else if ( err.error === 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)' ) { err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS; } console.log(`error: `, err); throw err; } }; // No unit tests for this function as it is only an API wrapper // Return false if do not get a valid response const getTokenStats = async (BCH, tokenId) => { let tokenStats; try { tokenStats = await BCH.SLP.Utils.tokenStats(tokenId); if (isValidTokenStats(tokenStats)) { return tokenStats; } } catch (err) { console.log(`Error fetching token stats for tokenId ${tokenId}`); console.log(err); return false; } }; const sendToken = async ( BCH, wallet, slpBalancesAndUtxos, { tokenId, amount, tokenReceiverAddress }, ) => { // Handle error of user having no BCH if (slpBalancesAndUtxos.nonSlpUtxos.length === 0) { throw new Error( `You need some ${currency.ticker} to send ${currency.tokenTicker}`, ); } const largestBchUtxo = slpBalancesAndUtxos.nonSlpUtxos.reduce( (previous, current) => previous.value > current.value ? previous : current, ); const bchECPair = BCH.ECPair.fromWIF(largestBchUtxo.wif); const tokenUtxos = slpBalancesAndUtxos.slpUtxos.filter( (utxo, index) => { if ( utxo && // UTXO is associated with a token. utxo.tokenId === tokenId && // UTXO matches the token ID. utxo.utxoType === 'token' // UTXO is not a minting baton. ) { return true; } return false; }, ); if (tokenUtxos.length === 0) { throw new Error( 'No token UTXOs for the specified token could be found.', ); } // BEGIN transaction construction. // instance of transaction builder let transactionBuilder; if (process.env.REACT_APP_NETWORK === 'mainnet') { transactionBuilder = new BCH.TransactionBuilder(); } else transactionBuilder = new BCH.TransactionBuilder('testnet'); const originalAmount = largestBchUtxo.value; transactionBuilder.addInput( largestBchUtxo.tx_hash, largestBchUtxo.tx_pos, ); let finalTokenAmountSent = new BigNumber(0); let tokenAmountBeingSentToAddress = new BigNumber(amount); let tokenUtxosBeingSpent = []; for (let i = 0; i < tokenUtxos.length; i++) { finalTokenAmountSent = finalTokenAmountSent.plus( new BigNumber(tokenUtxos[i].tokenQty), ); transactionBuilder.addInput( tokenUtxos[i].tx_hash, tokenUtxos[i].tx_pos, ); tokenUtxosBeingSpent.push(tokenUtxos[i]); if (tokenAmountBeingSentToAddress.lte(finalTokenAmountSent)) { break; } } const slpSendObj = BCH.SLP.TokenType1.generateSendOpReturn( tokenUtxosBeingSpent, tokenAmountBeingSentToAddress.toString(), ); const slpData = slpSendObj.script; // Add OP_RETURN as first output. transactionBuilder.addOutput(slpData, 0); // Send dust transaction representing tokens being sent. transactionBuilder.addOutput( BCH.SLP.Address.toLegacyAddress(tokenReceiverAddress), currency.etokenSats, ); // Return any token change back to the sender. if (slpSendObj.outputs > 1) { // Change goes back to where slp utxo came from transactionBuilder.addOutput( BCH.SLP.Address.toLegacyAddress( tokenUtxosBeingSpent[0].address, ), currency.etokenSats, ); } // get byte count to calculate fee. paying 1 sat // Note: This may not be totally accurate. Just guessing on the byteCount size. const txFee = calcFee( BCH, tokenUtxosBeingSpent, 5, 1.1 * currency.defaultFee, ); // amount to send back to the sending address. It's the original amount - 1 sat/byte for tx size const remainder = originalAmount - txFee - currency.etokenSats * 2; if (remainder < 1) { throw new Error('Selected UTXO does not have enough satoshis'); } // Last output: send the BCH change back to the wallet. // Send it back from whence it came transactionBuilder.addOutput( BCH.Address.toLegacyAddress(largestBchUtxo.address), remainder, ); // Sign the transaction with the private key for the BCH UTXO paying the fees. let redeemScript; transactionBuilder.sign( 0, bchECPair, redeemScript, transactionBuilder.hashTypes.SIGHASH_ALL, originalAmount, ); // Sign each token UTXO being consumed. for (let i = 0; i < tokenUtxosBeingSpent.length; i++) { const thisUtxo = tokenUtxosBeingSpent[i]; const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const utxoEcPair = BCH.ECPair.fromWIF( accounts .filter(acc => acc.cashAddress === thisUtxo.address) .pop().fundingWif, ); transactionBuilder.sign( 1 + i, utxoEcPair, redeemScript, transactionBuilder.hashTypes.SIGHASH_ALL, thisUtxo.value, ); } // build tx const tx = transactionBuilder.build(); // output rawhex const hex = tx.toHex(); // console.log(`Transaction raw hex: `, hex); // END transaction construction. const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]); if (txidStr && txidStr[0]) { console.log(`${currency.tokenTicker} txid`, txidStr[0]); } let link; if (process.env.REACT_APP_NETWORK === `mainnet`) { link = `${currency.blockExplorerUrl}/tx/${txidStr}`; } else { link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`; } //console.log(`link`, link); return link; }; const signPkMessage = async (BCH, pk, message) => { try { let signature = await BCH.BitcoinCash.signMessageWithPrivKey( pk, message, ); return signature; } catch (err) { console.log(`useBCH.signPkMessage() error: `, err); throw err; } }; const sendXec = async ( BCH, wallet, utxos, feeInSatsPerByte, optionalOpReturnMsg, isOneToMany, destinationAddressAndValueArray, destinationAddress, sendAmount, ) => { try { let value = new BigNumber(0); if (isOneToMany) { // this is a one to many XEC transaction if ( !destinationAddressAndValueArray || !destinationAddressAndValueArray.length ) { throw new Error('Invalid destinationAddressAndValueArray'); } const arrayLength = destinationAddressAndValueArray.length; for (let i = 0; i < arrayLength; i++) { // add the total value being sent in this array of recipients value = BigNumber.sum( value, new BigNumber( destinationAddressAndValueArray[i].split(',')[1], ), ); } // If user is attempting to send an aggregate value that is less than minimum accepted by the backend if ( value.lt( new BigNumber( fromSmallestDenomination( currency.dustSats, ).toString(), ), ) ) { // Throw the same error given by the backend attempting to broadcast such a tx throw new Error('dust'); } } else { // this is a one to one XEC transaction then check sendAmount // note: one to many transactions won't be sending a single sendAmount if (!sendAmount) { return null; } value = new BigNumber(sendAmount); // If user is attempting to send less than minimum accepted by the backend if ( value.lt( new BigNumber( fromSmallestDenomination( currency.dustSats, ).toString(), ), ) ) { // Throw the same error given by the backend attempting to broadcast such a tx throw new Error('dust'); } } const inputUtxos = []; let transactionBuilder; // instance of transaction builder if (process.env.REACT_APP_NETWORK === `mainnet`) transactionBuilder = new BCH.TransactionBuilder(); else transactionBuilder = new BCH.TransactionBuilder('testnet'); const satoshisToSend = toSmallestDenomination(value); // Throw validation error if toSmallestDenomination returns false if (!satoshisToSend) { const error = new Error( `Invalid decimal places for send amount`, ); throw error; } // Start of building the OP_RETURN output. // only build the OP_RETURN output if the user supplied it if ( typeof optionalOpReturnMsg !== 'undefined' && optionalOpReturnMsg.trim() !== '' ) { const script = [ BCH.Script.opcodes.OP_RETURN, // 6a Buffer.from( currency.opReturn.appPrefixesHex.cashtab, 'hex', ), // 00746162 Buffer.from(optionalOpReturnMsg), ]; const data = BCH.Script.encode(script); transactionBuilder.addOutput(data, 0); } // End of building the OP_RETURN output. let originalAmount = new BigNumber(0); let txFee = 0; for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i]; originalAmount = originalAmount.plus(utxo.value); const vout = utxo.vout; const txid = utxo.txid; // add input with txid and index of vout transactionBuilder.addInput(txid, vout); inputUtxos.push(utxo); txFee = calcFee(BCH, inputUtxos, 2, feeInSatsPerByte); if (originalAmount.minus(satoshisToSend).minus(txFee).gte(0)) { break; } } // Get change address from sending utxos // fall back to what is stored in wallet let REMAINDER_ADDR; // Validate address let isValidChangeAddress; try { REMAINDER_ADDR = inputUtxos[0].address; isValidChangeAddress = BCH.Address.isCashAddress(REMAINDER_ADDR); } catch (err) { isValidChangeAddress = false; } if (!isValidChangeAddress) { REMAINDER_ADDR = wallet.Path1899.cashAddress; } // amount to send back to the remainder address. const remainder = originalAmount.minus(satoshisToSend).minus(txFee); if (remainder.lt(0)) { const error = new Error(`Insufficient funds`); error.code = SEND_BCH_ERRORS.INSUFFICIENT_FUNDS; throw error; } if (isOneToMany) { // for one to many mode, add the multiple outputs from the array let arrayLength = destinationAddressAndValueArray.length; for (let i = 0; i < arrayLength; i++) { // add each send tx from the array as an output let outputAddress = destinationAddressAndValueArray[i].split(',')[0]; let outputValue = new BigNumber( destinationAddressAndValueArray[i].split(',')[1], ); transactionBuilder.addOutput( BCH.Address.toCashAddress(outputAddress), parseInt(toSmallestDenomination(outputValue)), ); } } else { // for one to one mode, add output w/ single address and amount to send transactionBuilder.addOutput( BCH.Address.toCashAddress(destinationAddress), parseInt(toSmallestDenomination(value)), ); } if (remainder.gte(new BigNumber(currency.dustSats))) { transactionBuilder.addOutput( REMAINDER_ADDR, parseInt(remainder), ); } // Sign the transactions with the HD node. for (let i = 0; i < inputUtxos.length; i++) { const utxo = inputUtxos[i]; transactionBuilder.sign( i, BCH.ECPair.fromWIF(utxo.wif), undefined, transactionBuilder.hashTypes.SIGHASH_ALL, utxo.value, ); } // build tx const tx = transactionBuilder.build(); // output rawhex const hex = tx.toHex(); // Broadcast transaction to the network const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]); if (txidStr && txidStr[0]) { console.log(`${currency.ticker} txid`, txidStr[0]); } let link; if (process.env.REACT_APP_NETWORK === `mainnet`) { link = `${currency.blockExplorerUrl}/tx/${txidStr}`; } else { link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`; } //console.log(`link`, link); return link; } catch (err) { if (err.error === 'insufficient priority (code 66)') { err.code = SEND_BCH_ERRORS.INSUFFICIENT_PRIORITY; } else if (err.error === 'txn-mempool-conflict (code 18)') { err.code = SEND_BCH_ERRORS.DOUBLE_SPENDING; } else if (err.error === 'Network Error') { err.code = SEND_BCH_ERRORS.NETWORK_ERROR; } else if ( err.error === 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)' ) { err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS; } console.log(`error: `, err); throw err; } }; const getBCH = (apiIndex = 0) => { let ConstructedSlpWallet; ConstructedSlpWallet = new SlpWallet('', { restURL: getRestUrl(apiIndex), }); return ConstructedSlpWallet.bchjs; }; return { getBCH, calcFee, getUtxos, getHydratedUtxoDetails, getSlpBalancesAndUtxos, getTxHistory, flattenTransactions, parseTxData, addTokenTxData, parseTokenInfoForTxHistory, getTxData, getRestUrl, signPkMessage, sendXec, sendToken, createToken, getTokenStats, }; } diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js index d768ebd4d..d127468e8 100644 --- a/web/cashtab/src/hooks/useWallet.js +++ b/web/cashtab/src/hooks/useWallet.js @@ -1,1129 +1,1137 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { useState, useEffect } from 'react'; import useAsyncTimeout from '@hooks/useAsyncTimeout'; import usePrevious from '@hooks/usePrevious'; import useBCH from '@hooks/useBCH'; import BigNumber from 'bignumber.js'; import { fromSmallestDenomination, loadStoredWallet, isValidStoredWallet, isLegacyMigrationRequired, } from '@utils/cashMethods'; import { isValidCashtabSettings } from '@utils/validation'; import localforage from 'localforage'; import { currency } from '@components/Common/Ticker'; import isEmpty from 'lodash.isempty'; import isEqual from 'lodash.isequal'; import { xecReceivedNotification, eTokenReceivedNotification, } from '@components/Common/Notifications'; const useWallet = () => { const [wallet, setWallet] = useState(false); const [cashtabSettings, setCashtabSettings] = useState(false); const [fiatPrice, setFiatPrice] = useState(null); const [apiError, setApiError] = useState(false); const [checkFiatInterval, setCheckFiatInterval] = useState(null); const { getBCH, getUtxos, getHydratedUtxoDetails, getSlpBalancesAndUtxos, getTxHistory, getTxData, addTokenTxData, } = useBCH(); const [loading, setLoading] = useState(true); const [apiIndex, setApiIndex] = useState(0); const [BCH, setBCH] = useState(getBCH(apiIndex)); const { balances, tokens, utxos } = isValidStoredWallet(wallet) ? wallet.state : { balances: {}, tokens: [], utxos: null, }; const previousBalances = usePrevious(balances); const previousTokens = usePrevious(tokens); const previousUtxos = usePrevious(utxos); // If you catch API errors, call this function const tryNextAPI = () => { let currentApiIndex = apiIndex; // How many APIs do you have? const apiString = process.env.REACT_APP_BCHA_APIS; const apiArray = apiString.split(','); console.log(`You have ${apiArray.length} APIs to choose from`); console.log(`Current selection: ${apiIndex}`); // If only one, exit if (apiArray.length === 0) { console.log( `There are no backup APIs, you are stuck with this error`, ); return; } else if (currentApiIndex < apiArray.length - 1) { currentApiIndex += 1; console.log( `Incrementing API index from ${apiIndex} to ${currentApiIndex}`, ); } else { // Otherwise use the first option again console.log(`Retrying first API index`); currentApiIndex = 0; } //return setApiIndex(currentApiIndex); console.log(`Setting Api Index to ${currentApiIndex}`); setApiIndex(currentApiIndex); return setBCH(getBCH(currentApiIndex)); // If you have more than one, use the next one // If you are at the "end" of the array, use the first one }; const normalizeSlpBalancesAndUtxos = (slpBalancesAndUtxos, wallet) => { const Accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; slpBalancesAndUtxos.nonSlpUtxos.forEach(utxo => { const derivatedAccount = Accounts.find( account => account.cashAddress === utxo.address, ); utxo.wif = derivatedAccount.fundingWif; }); return slpBalancesAndUtxos; }; const normalizeBalance = slpBalancesAndUtxos => { const totalBalanceInSatoshis = slpBalancesAndUtxos.nonSlpUtxos.reduce( (previousBalance, utxo) => previousBalance + utxo.value, 0, ); return { totalBalanceInSatoshis, totalBalance: fromSmallestDenomination(totalBalanceInSatoshis), }; }; const deriveAccount = async (BCH, { masterHDNode, path }) => { const node = BCH.HDNode.derivePath(masterHDNode, path); const publicKey = BCH.HDNode.toPublicKey(node).toString('hex'); const cashAddress = BCH.HDNode.toCashAddress(node); const slpAddress = BCH.SLP.Address.toSLPAddress(cashAddress); return { publicKey, cashAddress, slpAddress, fundingWif: BCH.HDNode.toWIF(node), fundingAddress: BCH.SLP.Address.toSLPAddress(cashAddress), legacyAddress: BCH.SLP.Address.toLegacyAddress(cashAddress), }; }; const loadWalletFromStorageOnStartup = async setWallet => { // get wallet object from localforage const wallet = await getWallet(); // If wallet object in storage is valid, use it to set state on startup if (isValidStoredWallet(wallet)) { // Convert all the token balance figures to big numbers const liveWalletState = loadStoredWallet(wallet.state); wallet.state = liveWalletState; setWallet(wallet); return setLoading(false); } // Loading will remain true until API calls populate this legacy wallet setWallet(wallet); }; const haveUtxosChanged = (wallet, utxos, previousUtxos) => { // Relevant points for this array comparing exercise // https://stackoverflow.com/questions/13757109/triple-equal-signs-return-false-for-arrays-in-javascript-why // https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript // If this is initial state if (utxos === null) { // Then make sure to get slpBalancesAndUtxos return true; } // If this is the first time the wallet received utxos if (typeof utxos === 'undefined') { // Then they have certainly changed return true; } if (typeof previousUtxos === 'undefined') { // Compare to what you have in localStorage on startup // If previousUtxos are undefined, see if you have previousUtxos in wallet state // If you do, and it has everything you need, set wallet state with that instead of calling hydrateUtxos on all utxos if (isValidStoredWallet(wallet)) { // Convert all the token balance figures to big numbers const liveWalletState = loadStoredWallet(wallet.state); wallet.state = liveWalletState; return setWallet(wallet); } // If wallet in storage is a legacy wallet or otherwise does not have all state fields, // then assume utxos have changed return true; } // return true for empty array, since this means you definitely do not want to skip the next API call if (utxos && utxos.length === 0) { return true; } // If wallet is valid, compare what exists in written wallet state instead of former api call let utxosToCompare = previousUtxos; if (isValidStoredWallet(wallet)) { try { utxosToCompare = wallet.state.utxos; } catch (err) { console.log(`Error setting utxos to wallet.state.utxos`, err); console.log(`Wallet at err`, wallet); // If this happens, assume utxo set has changed return true; } } // Compare utxo sets return !isEqual(utxos, utxosToCompare); }; const update = async ({ wallet }) => { //console.log(`tick()`); //console.time("update"); try { if (!wallet) { return; } const cashAddresses = [ wallet.Path245.cashAddress, wallet.Path145.cashAddress, wallet.Path1899.cashAddress, ]; + const publicKeys = [ + wallet.Path145.publicKey, + wallet.Path245.publicKey, + wallet.Path1899.publicKey, + ]; + const utxos = await getUtxos(BCH, cashAddresses); // If an error is returned or utxos from only 1 address are returned if (!utxos || isEmpty(utxos) || utxos.error || utxos.length < 2) { // Throw error here to prevent more attempted api calls // as you are likely already at rate limits throw new Error('Error fetching utxos'); } // Need to call with wallet as a parameter rather than trusting it is in state, otherwise can sometimes get wallet=false from haveUtxosChanged const utxosHaveChanged = haveUtxosChanged( wallet, utxos, previousUtxos, ); // If the utxo set has not changed, if (!utxosHaveChanged) { // remove api error here; otherwise it will remain if recovering from a rate // limit error with an unchanged utxo set setApiError(false); // then wallet.state has not changed and does not need to be updated //console.timeEnd("update"); return; } const hydratedUtxoDetails = await getHydratedUtxoDetails( BCH, utxos, ); const slpBalancesAndUtxos = await getSlpBalancesAndUtxos( BCH, hydratedUtxoDetails, ); const txHistory = await getTxHistory(BCH, cashAddresses); - const parsedTxHistory = await getTxData(BCH, txHistory); + + // public keys are used to determined if a tx is incoming outgoing + const parsedTxHistory = await getTxData(BCH, txHistory, publicKeys); const parsedWithTokens = await addTokenTxData(BCH, parsedTxHistory); console.log(`slpBalancesAndUtxos`, slpBalancesAndUtxos); if (typeof slpBalancesAndUtxos === 'undefined') { console.log(`slpBalancesAndUtxos is undefined`); throw new Error('slpBalancesAndUtxos is undefined'); } const { tokens } = slpBalancesAndUtxos; const newState = { balances: {}, tokens: [], slpBalancesAndUtxos: [], }; newState.slpBalancesAndUtxos = normalizeSlpBalancesAndUtxos( slpBalancesAndUtxos, wallet, ); newState.balances = normalizeBalance(slpBalancesAndUtxos); newState.tokens = tokens; newState.parsedTxHistory = parsedWithTokens; newState.utxos = utxos; newState.hydratedUtxoDetails = hydratedUtxoDetails; // Set wallet with new state field wallet.state = newState; setWallet(wallet); // Write this state to indexedDb using localForage writeWalletState(wallet, newState); // If everything executed correctly, remove apiError setApiError(false); } catch (error) { console.log(`Error in update({wallet})`); console.log(error); // Set this in state so that transactions are disabled until the issue is resolved setApiError(true); //console.timeEnd("update"); // Try another endpoint console.log(`Trying next API...`); tryNextAPI(); } //console.timeEnd("update"); }; const getActiveWalletFromLocalForage = async () => { let wallet; try { wallet = await localforage.getItem('wallet'); } catch (err) { console.log(`Error in getActiveWalletFromLocalForage`, err); wallet = null; } return wallet; }; /* const getSavedWalletsFromLocalForage = async () => { let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log(`Error in getSavedWalletsFromLocalForage`, err); savedWallets = null; } return savedWallets; }; */ const getWallet = async () => { let wallet; let existingWallet; try { existingWallet = await getActiveWalletFromLocalForage(); // existing wallet will be // 1 - the 'wallet' value from localForage, if it exists // 2 - false if it does not exist in localForage // 3 - null if error // If the wallet does not have Path1899, add it // or each Path1899, Path145, Path245 does not have a public key, add them if (existingWallet) { if (isLegacyMigrationRequired(existingWallet)) { console.log( `Wallet does not have Path1899 or does not have public key`, ); existingWallet = await migrateLegacyWallet( BCH, existingWallet, ); } } // If not in localforage then existingWallet = false, check localstorage if (!existingWallet) { console.log(`no existing wallet, checking local storage`); existingWallet = JSON.parse( window.localStorage.getItem('wallet'), ); console.log(`existingWallet from localStorage`, existingWallet); // If you find it here, move it to indexedDb if (existingWallet !== null) { wallet = await getWalletDetails(existingWallet); await localforage.setItem('wallet', wallet); return wallet; } } } catch (err) { console.log(`Error in getWallet()`, err); /* Error here implies problem interacting with localForage or localStorage API Have not seen this error in testing In this case, you still want to return 'wallet' using the logic below based on the determination of 'existingWallet' from the logic above */ } if (existingWallet === null || !existingWallet) { wallet = await getWalletDetails(existingWallet); await localforage.setItem('wallet', wallet); } else { wallet = existingWallet; } return wallet; }; const migrateLegacyWallet = async (BCH, wallet) => { console.log(`migrateLegacyWallet`); console.log(`legacyWallet`, wallet); const NETWORK = process.env.REACT_APP_NETWORK; const mnemonic = wallet.mnemonic; const rootSeedBuffer = await BCH.Mnemonic.toSeed(mnemonic); let masterHDNode; if (NETWORK === `mainnet`) { masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer); } else { masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer, 'testnet'); } const Path245 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/245'/0'/0/0", }); const Path145 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/145'/0'/0/0", }); const Path1899 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/1899'/0'/0/0", }); wallet.Path245 = Path245; wallet.Path145 = Path145; wallet.Path1899 = Path1899; try { await localforage.setItem('wallet', wallet); } catch (err) { console.log( `Error setting wallet to wallet indexedDb in migrateLegacyWallet()`, ); console.log(err); } return wallet; }; const writeWalletState = async (wallet, newState) => { // Add new state as an object on the active wallet wallet.state = newState; try { await localforage.setItem('wallet', wallet); } catch (err) { console.log(`Error in writeWalletState()`); console.log(err); } }; const getWalletDetails = async wallet => { if (!wallet) { return false; } // Since this info is in localforage now, only get the var const NETWORK = process.env.REACT_APP_NETWORK; const mnemonic = wallet.mnemonic; const rootSeedBuffer = await BCH.Mnemonic.toSeed(mnemonic); let masterHDNode; if (NETWORK === `mainnet`) { masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer); } else { masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer, 'testnet'); } const Path245 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/245'/0'/0/0", }); const Path145 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/145'/0'/0/0", }); const Path1899 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/1899'/0'/0/0", }); let name = Path1899.cashAddress.slice(12, 17); // Only set the name if it does not currently exist if (wallet && wallet.name) { name = wallet.name; } return { mnemonic: wallet.mnemonic, name, Path245, Path145, Path1899, }; }; const getSavedWallets = async activeWallet => { let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); if (savedWallets === null) { savedWallets = []; } } catch (err) { console.log(`Error in getSavedWallets`); console.log(err); savedWallets = []; } // Even though the active wallet is still stored in savedWallets, don't return it in this function for (let i = 0; i < savedWallets.length; i += 1) { if ( typeof activeWallet !== 'undefined' && activeWallet.name && savedWallets[i].name === activeWallet.name ) { savedWallets.splice(i, 1); } } return savedWallets; }; const activateWallet = async walletToActivate => { /* If the user is migrating from old version to this version, make sure to save the activeWallet 1 - check savedWallets for the previously active wallet 2 - If not there, add it */ let currentlyActiveWallet; try { currentlyActiveWallet = await localforage.getItem('wallet'); } catch (err) { console.log( `Error in localforage.getItem("wallet") in activateWallet()`, ); return false; } // Get savedwallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log( `Error in localforage.getItem("savedWallets") in activateWallet()`, ); return false; } /* When a legacy user runs cashtab.com/, their active wallet will be migrated to Path1899 by the getWallet function. getWallet function also makes sure that each Path has a public key Wallets in savedWallets are migrated when they are activated, in this function Two cases to handle 1 - currentlyActiveWallet has Path1899, but its stored keyvalue pair in savedWallets does not > Update savedWallets so that Path1899 is added to currentlyActiveWallet 2 - walletToActivate does not have Path1899 > Update walletToActivate with Path1899 before activation NOTE: since publicKey property is added later, wallet without public key in Path1899 is also considered legacy and required migration. */ // Need to handle a similar situation with state // If you find the activeWallet in savedWallets but without state, resave active wallet with state // Note you do not have the Case 2 described above here, as wallet state is added in the update() function of useWallet.js // Also note, since state can be expected to change frequently (unlike path deriv), you will likely save it every time you activate a new wallet // Check savedWallets for currentlyActiveWallet let walletInSavedWallets = false; for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === currentlyActiveWallet.name) { walletInSavedWallets = true; // Check savedWallets for unmigrated currentlyActiveWallet if (isLegacyMigrationRequired(savedWallets[i])) { // Case 1, described above savedWallets[i].Path1899 = currentlyActiveWallet.Path1899; savedWallets[i].Path145 = currentlyActiveWallet.Path145; savedWallets[i].Path245 = currentlyActiveWallet.Path245; } /* Update wallet state Note, this makes previous `walletUnmigrated` variable redundant savedWallets[i] should always be updated, since wallet state can be expected to change most of the time */ savedWallets[i].state = currentlyActiveWallet.state; } } // resave savedWallets try { // Set walletName as the active wallet await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets") in activateWallet() for unmigrated wallet`, ); } if (!walletInSavedWallets) { console.log(`Wallet is not in saved Wallets, adding`); savedWallets.push(currentlyActiveWallet); // resave savedWallets try { // Set walletName as the active wallet await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets") in activateWallet()`, ); } } // If wallet does not have Path1899, add it // or each of the Path1899, Path145, Path245 does not have a public key, add them // by calling migrateLagacyWallet() if (isLegacyMigrationRequired(walletToActivate)) { // Case 2, described above console.log( `Case 2: Wallet to activate does not have Path1899 or does not have public key in each Path`, ); console.log( `Wallet to activate from SavedWallets does not have Path1899 or does not have public key in each Path`, ); console.log(`walletToActivate`, walletToActivate); walletToActivate = await migrateLegacyWallet(BCH, walletToActivate); } else { // Otherwise activate it as normal // Now that we have verified the last wallet was saved, we can activate the new wallet try { await localforage.setItem('wallet', walletToActivate); } catch (err) { console.log( `Error in localforage.setItem("wallet", walletToActivate) in activateWallet()`, ); return false; } } // Make sure stored wallet is in correct format to be used as live wallet if (isValidStoredWallet(walletToActivate)) { // Convert all the token balance figures to big numbers const liveWalletState = loadStoredWallet(walletToActivate.state); walletToActivate.state = liveWalletState; } return walletToActivate; }; const renameWallet = async (oldName, newName) => { // Load savedWallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log( `Error in await localforage.getItem("savedWallets") in renameWallet`, ); console.log(err); return false; } // Verify that no existing wallet has this name for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === newName) { // return an error return false; } } // change name of desired wallet for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === oldName) { // Replace the name of this entry with the new name savedWallets[i].name = newName; } } // resave savedWallets try { // Set walletName as the active wallet await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets", savedWallets) in renameWallet()`, ); return false; } return true; }; const deleteWallet = async walletToBeDeleted => { // delete a wallet // returns true if wallet is successfully deleted // otherwise returns false // Load savedWallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log( `Error in await localforage.getItem("savedWallets") in deleteWallet`, ); console.log(err); return false; } // Iterate over to find the wallet to be deleted // Verify that no existing wallet has this name let walletFoundAndRemoved = false; for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === walletToBeDeleted.name) { // Verify it has the same mnemonic too, that's a better UUID if (savedWallets[i].mnemonic === walletToBeDeleted.mnemonic) { // Delete it savedWallets.splice(i, 1); walletFoundAndRemoved = true; } } } // If you don't find the wallet, return false if (!walletFoundAndRemoved) { return false; } // Resave savedWallets less the deleted wallet try { // Set walletName as the active wallet await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets", savedWallets) in deleteWallet()`, ); return false; } return true; }; const addNewSavedWallet = async importMnemonic => { // Add a new wallet to savedWallets from importMnemonic or just new wallet const lang = 'english'; // create 128 bit BIP39 mnemonic const Bip39128BitMnemonic = importMnemonic ? importMnemonic : BCH.Mnemonic.generate(128, BCH.Mnemonic.wordLists()[lang]); const newSavedWallet = await getWalletDetails({ mnemonic: Bip39128BitMnemonic.toString(), }); // Get saved wallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); // If this doesn't exist yet, savedWallets === null if (savedWallets === null) { savedWallets = []; } } catch (err) { console.log( `Error in savedWallets = await localforage.getItem("savedWallets") in addNewSavedWallet()`, ); console.log(err); console.log(`savedWallets in error state`, savedWallets); } // If this wallet is from an imported mnemonic, make sure it does not already exist in savedWallets if (importMnemonic) { for (let i = 0; i < savedWallets.length; i += 1) { // Check for condition "importing new wallet that is already in savedWallets" if (savedWallets[i].mnemonic === importMnemonic) { // set this as the active wallet to keep name history console.log( `Error: this wallet already exists in savedWallets`, ); console.log(`Wallet not being added.`); return false; } } } // add newSavedWallet savedWallets.push(newSavedWallet); // update savedWallets try { await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets", activeWallet) called in createWallet with ${importMnemonic}`, ); console.log(`savedWallets`, savedWallets); console.log(err); } return true; }; const createWallet = async importMnemonic => { const lang = 'english'; // create 128 bit BIP39 mnemonic const Bip39128BitMnemonic = importMnemonic ? importMnemonic : BCH.Mnemonic.generate(128, BCH.Mnemonic.wordLists()[lang]); const wallet = await getWalletDetails({ mnemonic: Bip39128BitMnemonic.toString(), }); try { await localforage.setItem('wallet', wallet); } catch (err) { console.log( `Error setting wallet to wallet indexedDb in createWallet()`, ); console.log(err); } // Since this function is only called from OnBoarding.js, also add this to the saved wallet try { await localforage.setItem('savedWallets', [wallet]); } catch (err) { console.log( `Error setting wallet to savedWallets indexedDb in createWallet()`, ); console.log(err); } return wallet; }; const validateMnemonic = ( mnemonic, wordlist = BCH.Mnemonic.wordLists().english, ) => { let mnemonicTestOutput; try { mnemonicTestOutput = BCH.Mnemonic.validate(mnemonic, wordlist); if (mnemonicTestOutput === 'Valid mnemonic') { return true; } else { return false; } } catch (err) { console.log(err); return false; } }; const handleUpdateWallet = async setWallet => { await loadWalletFromStorageOnStartup(setWallet); }; const loadCashtabSettings = async () => { // get settings object from localforage let localSettings; try { localSettings = await localforage.getItem('settings'); // If there is no keyvalue pair in localforage with key 'settings' if (localSettings === null) { // Create one with the default settings from Ticker.js localforage.setItem('settings', currency.defaultSettings); // Set state to default settings setCashtabSettings(currency.defaultSettings); return currency.defaultSettings; } } catch (err) { console.log(`Error getting cashtabSettings`, err); // TODO If they do not exist, write them // TODO add function to change them setCashtabSettings(currency.defaultSettings); return currency.defaultSettings; } // If you found an object in localforage at the settings key, make sure it's valid if (isValidCashtabSettings(localSettings)) { setCashtabSettings(localSettings); return localSettings; } // if not valid, also set cashtabSettings to default setCashtabSettings(currency.defaultSettings); return currency.defaultSettings; }; // With different currency selections possible, need unique intervals for price checks // Must be able to end them and set new ones with new currencies const initializeFiatPriceApi = async selectedFiatCurrency => { // Update fiat price and confirm it is set to make sure ap keeps loading state until this is updated await fetchBchPrice(selectedFiatCurrency); // Set interval for updating the price with given currency const thisFiatInterval = setInterval(function () { fetchBchPrice(selectedFiatCurrency); }, 60000); // set interval in state setCheckFiatInterval(thisFiatInterval); }; const clearFiatPriceApi = fiatPriceApi => { // Clear fiat price check interval of previously selected currency clearInterval(fiatPriceApi); }; const changeCashtabSettings = async (key, newValue) => { // Set loading to true as you do not want to display the fiat price of the last currency // loading = true will lock the UI until the fiat price has updated setLoading(true); // Get settings from localforage let currentSettings; let newSettings; try { currentSettings = await localforage.getItem('settings'); } catch (err) { console.log(`Error in changeCashtabSettings`, err); // Set fiat price to null, which disables fiat sends throughout the app setFiatPrice(null); // Unlock the UI setLoading(false); return; } // Make sure function was called with valid params if ( Object.keys(currentSettings).includes(key) && currency.settingsValidation[key].includes(newValue) ) { // Update settings newSettings = currentSettings; newSettings[key] = newValue; } // Set new settings in state so they are available in context throughout the app setCashtabSettings(newSettings); // If this settings change adjusted the fiat currency, update fiat price if (key === 'fiatCurrency') { clearFiatPriceApi(checkFiatInterval); initializeFiatPriceApi(newValue); } // Write new settings in localforage try { await localforage.setItem('settings', newSettings); } catch (err) { console.log( `Error writing newSettings object to localforage in changeCashtabSettings`, err, ); console.log(`newSettings`, newSettings); // do nothing. If this happens, the user will see default currency next time they load the app. } setLoading(false); }; // Parse for incoming XEC transactions if ( previousBalances && balances && 'totalBalance' in previousBalances && 'totalBalance' in balances && new BigNumber(balances.totalBalance) .minus(previousBalances.totalBalance) .gt(0) ) { xecReceivedNotification( balances, previousBalances, cashtabSettings, fiatPrice, ); } // Parse for incoming eToken transactions if ( tokens && tokens[0] && tokens[0].balance && previousTokens && previousTokens[0] && previousTokens[0].balance ) { // If tokens length is greater than previousTokens length, a new token has been received // Note, a user could receive a new token, AND more of existing tokens in between app updates // In this case, the app will only notify about the new token // TODO better handling for all possible cases to cover this // TODO handle with websockets for better response time, less complicated calc if (tokens.length > previousTokens.length) { // Find the new token const tokenIds = tokens.map(({ tokenId }) => tokenId); const previousTokenIds = previousTokens.map( ({ tokenId }) => tokenId, ); //console.log(`tokenIds`, tokenIds); //console.log(`previousTokenIds`, previousTokenIds); // An array with the new token Id const newTokenIdArr = tokenIds.filter( tokenId => !previousTokenIds.includes(tokenId), ); // It's possible that 2 new tokens were received // To do, handle this case const newTokenId = newTokenIdArr[0]; //console.log(newTokenId); // How much of this tokenId did you get? // would be at // Find where the newTokenId is const receivedTokenObjectIndex = tokens.findIndex( x => x.tokenId === newTokenId, ); //console.log(`receivedTokenObjectIndex`, receivedTokenObjectIndex); // Calculate amount received //console.log(`receivedTokenObject:`, tokens[receivedTokenObjectIndex]); const receivedSlpQty = tokens[receivedTokenObjectIndex].balance.toString(); const receivedSlpTicker = tokens[receivedTokenObjectIndex].info.tokenTicker; const receivedSlpName = tokens[receivedTokenObjectIndex].info.tokenName; //console.log(`receivedSlpQty`, receivedSlpQty); // Notification if you received SLP if (receivedSlpQty > 0) { eTokenReceivedNotification( currency, receivedSlpTicker, receivedSlpQty, receivedSlpName, ); } // } else { // If tokens[i].balance > previousTokens[i].balance, a new SLP tx of an existing token has been received // Note that tokens[i].balance is of type BigNumber for (let i = 0; i < tokens.length; i += 1) { if (tokens[i].balance.gt(previousTokens[i].balance)) { // Received this token // console.log(`previousTokenId`, previousTokens[i].tokenId); // console.log(`currentTokenId`, tokens[i].tokenId); if (previousTokens[i].tokenId !== tokens[i].tokenId) { console.log( `TokenIds do not match, breaking from SLP notifications`, ); // Then don't send the notification // Also don't 'continue' ; this means you have sent a token, just stop iterating through break; } const receivedSlpQty = tokens[i].balance.minus( previousTokens[i].balance, ); const receivedSlpTicker = tokens[i].info.tokenTicker; const receivedSlpName = tokens[i].info.tokenName; eTokenReceivedNotification( currency, receivedSlpTicker, receivedSlpQty, receivedSlpName, ); } } } } // Update wallet every 10s useAsyncTimeout(async () => { const wallet = await getWallet(); update({ wallet, }).finally(() => { setLoading(false); }); }, 10000); const fetchBchPrice = async ( fiatCode = cashtabSettings ? cashtabSettings.fiatCurrency : 'usd', ) => { // Split this variable out in case coingecko changes const cryptoId = currency.coingeckoId; // Keep this in the code, because different URLs will have different outputs require different parsing const priceApiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${cryptoId}&vs_currencies=${fiatCode}&include_last_updated_at=true`; let bchPrice; let bchPriceJson; try { bchPrice = await fetch(priceApiUrl); //console.log(`bchPrice`, bchPrice); } catch (err) { console.log(`Error fetching BCH Price`); console.log(err); } try { bchPriceJson = await bchPrice.json(); //console.log(`bchPriceJson`, bchPriceJson); let bchPriceInFiat = bchPriceJson[cryptoId][fiatCode]; const validEcashPrice = typeof bchPriceInFiat === 'number'; if (validEcashPrice) { setFiatPrice(bchPriceInFiat); } else { // If API price looks fishy, do not allow app to send using fiat settings setFiatPrice(null); } } catch (err) { console.log(`Error parsing price API response to JSON`); console.log(err); } }; useEffect(async () => { handleUpdateWallet(setWallet); const initialSettings = await loadCashtabSettings(); initializeFiatPriceApi(initialSettings.fiatCurrency); }, []); return { BCH, wallet, fiatPrice, loading, apiError, cashtabSettings, changeCashtabSettings, getActiveWalletFromLocalForage, getWallet, validateMnemonic, getWalletDetails, getSavedWallets, migrateLegacyWallet, createWallet: async importMnemonic => { setLoading(true); const newWallet = await createWallet(importMnemonic); setWallet(newWallet); update({ wallet: newWallet, }).finally(() => setLoading(false)); }, activateWallet: async walletToActivate => { setLoading(true); const newWallet = await activateWallet(walletToActivate); setWallet(newWallet); if (isValidStoredWallet(walletToActivate)) { // If you have all state parameters needed in storage, immediately load the wallet setLoading(false); } else { // If the wallet is missing state parameters in storage, wait for API info // This handles case of unmigrated legacy wallet update({ wallet: newWallet, }).finally(() => setLoading(false)); } }, addNewSavedWallet, renameWallet, deleteWallet, }; }; export default useWallet;