diff --git a/web/cashtab/src/hooks/__mocks__/mockReturnGetHydratedUtxoDetailsWithZeroBalance.js b/web/cashtab/src/hooks/__mocks__/mockReturnGetHydratedUtxoDetailsWithZeroBalance.js new file mode 100644 index 000000000..506d5dd18 --- /dev/null +++ b/web/cashtab/src/hooks/__mocks__/mockReturnGetHydratedUtxoDetailsWithZeroBalance.js @@ -0,0 +1,761 @@ +export default { + slpUtxos: [ + { + utxos: [ + { + height: 660869, + tx_hash: + '16b624b60de4a1d8a06baa129e3a88a4becd499e1d5d0d40b9f2ff4d28e3f660', + tx_pos: 1, + value: 546, + txid: + '16b624b60de4a1d8a06baa129e3a88a4becd499e1d5d0d40b9f2ff4d28e3f660', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', + tokenTicker: 'ST', + tokenName: 'ST', + tokenDocumentUrl: 'developer.bitcoin.com', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660869, + tx_hash: + 'c7da9ae6a0ce9d4f2f3345f9f0e5da5371228c8aee72b6eeac1b42871b216e6b', + tx_pos: 1, + value: 546, + txid: + 'c7da9ae6a0ce9d4f2f3345f9f0e5da5371228c8aee72b6eeac1b42871b216e6b', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', + tokenTicker: '🍔', + tokenName: 'Burger', + tokenDocumentUrl: + 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '2', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660869, + tx_hash: + 'dac23f10dd65caa51359c1643ffc93b94d14c05b739590ade85557d338a21040', + tx_pos: 1, + value: 546, + txid: + 'dac23f10dd65caa51359c1643ffc93b94d14c05b739590ade85557d338a21040', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + tokenTicker: 'HONK', + tokenName: 'HONK HONK', + tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660869, + tx_hash: + 'efa6f67078810875513a116b389886610d81ecf6daf97d55dd96d3fdd201dfac', + tx_pos: 1, + value: 546, + txid: + 'efa6f67078810875513a116b389886610d81ecf6daf97d55dd96d3fdd201dfac', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', + tokenTicker: 'TAP', + tokenName: 'Thoughts and Prayers', + tokenDocumentUrl: '', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '2', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660978, + tx_hash: + 'b622b770f74f056e07e5d2ea4d7f8da1c4d865e21e11c31a263602a38d4a2474', + tx_pos: 2, + value: 546, + txid: + 'b622b770f74f056e07e5d2ea4d7f8da1c4d865e21e11c31a263602a38d4a2474', + vout: 2, + utxoType: 'minting-baton', + transactionType: 'mint', + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + tokenType: 1, + tokenTicker: 'CTP', + tokenName: 'Cash Tab Points', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + mintBatonVout: 2, + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660982, + tx_hash: + '67605f3d18135b52d95a4877a427d100c14f2610c63ee84eaf4856f883a0b70e', + tx_pos: 2, + value: 546, + txid: + '67605f3d18135b52d95a4877a427d100c14f2610c63ee84eaf4856f883a0b70e', + vout: 2, + utxoType: 'minting-baton', + transactionType: 'mint', + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + tokenType: 1, + tokenTicker: 'WDT', + tokenName: + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + tokenDocumentUrl: + 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', + tokenDocumentHash: + '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', + decimals: 7, + mintBatonVout: 2, + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 662159, + tx_hash: + '05e90e9f35bc041a2939e0e28cf9c436c9adb0f247a7fb0d1f4abb26d418f096', + tx_pos: 1, + value: 546, + txid: + '05e90e9f35bc041a2939e0e28cf9c436c9adb0f247a7fb0d1f4abb26d418f096', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + tokenTicker: 'CTP', + tokenName: 'Cash Tab Points', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '150', + isValid: false, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 662799, + tx_hash: + 'b399a5ae69e4ac4c96b27c680a541e6b8142006bdc2484a959821858fc0b4ca3', + tx_pos: 2, + value: 546, + txid: + 'b399a5ae69e4ac4c96b27c680a541e6b8142006bdc2484a959821858fc0b4ca3', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + tokenTicker: 'CTP', + tokenName: 'Cash Tab Points', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '310', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 662935, + tx_hash: + 'ec423d0089f5cd85973ff6d875e9507f6b396b3b82bf6e9f5cfb24b7c70273bd', + tx_pos: 1, + value: 546, + txid: + 'ec423d0089f5cd85973ff6d875e9507f6b396b3b82bf6e9f5cfb24b7c70273bd', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', + tokenTicker: 'CTB', + tokenName: 'CashTabBits', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '0', + isValid: false, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 662987, + tx_hash: + '9b4361b24c756ff7a74ea5261be565acade0b246fb85422086ac273c1e4ee7d5', + tx_pos: 1, + value: 546, + txid: + '9b4361b24c756ff7a74ea5261be565acade0b246fb85422086ac273c1e4ee7d5', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '6a4c8bfa2e3ca345795dc3bde84d647390e9e1f2ff96e535cd2754d8ea5a3539', + tokenTicker: 'CTB2', + tokenName: 'CashTabBitsReborn', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '10000000000', + isValid: false, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667561, + tx_hash: + '4ca7f70996c699b0f988597a2dd2700f1f24f305318ddc8fe137d96d5fa96bf5', + tx_pos: 1, + value: 546, + txid: + '4ca7f70996c699b0f988597a2dd2700f1f24f305318ddc8fe137d96d5fa96bf5', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + tokenTicker: 'HONK', + tokenName: 'HONK HONK', + tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667750, + tx_hash: + 'de912bc7a6a1b14abe04960cd9a0ef88a4f59eb04db765f7bc2c0d2c2f997054', + tx_pos: 1, + value: 546, + txid: + 'de912bc7a6a1b14abe04960cd9a0ef88a4f59eb04db765f7bc2c0d2c2f997054', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + tokenTicker: 'WDT', + tokenName: + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + tokenDocumentUrl: + 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', + tokenDocumentHash: + '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', + decimals: 7, + tokenType: 1, + tokenQty: '100.0000001', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667750, + tx_hash: + 'e25cebd4cccbbdd91b36f672d51f5ce978e0817839be9854c3550704aec4359d', + tx_pos: 1, + value: 546, + txid: + 'e25cebd4cccbbdd91b36f672d51f5ce978e0817839be9854c3550704aec4359d', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + tokenTicker: 'HONK', + tokenName: 'HONK HONK', + tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667750, + tx_hash: + 'e9a94cc174839e3659d2fe4d33490528d18ad91404b65eb8cc35d8fa2d3f5096', + tx_pos: 2, + value: 546, + txid: + 'e9a94cc174839e3659d2fe4d33490528d18ad91404b65eb8cc35d8fa2d3f5096', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '5.854300861', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667751, + tx_hash: + '05e87142de1bb8c2a43d22a2e4b97855eb84c3c76f4ea956a654efda8d0557ca', + tx_pos: 1, + value: 546, + txid: + '05e87142de1bb8c2a43d22a2e4b97855eb84c3c76f4ea956a654efda8d0557ca', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '1.14669914', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + ], + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + utxos: [ + { + height: 660971, + tx_hash: + 'aefc3f3c65760d0f0fa716a84d12c4dc76ca7552953d6c7a4358abb6e24c5d7c', + tx_pos: 1, + value: 546, + txid: + 'aefc3f3c65760d0f0fa716a84d12c4dc76ca7552953d6c7a4358abb6e24c5d7c', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + tokenTicker: 'CTP', + tokenName: 'Cash Tab Points', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '0', + isValid: false, + address: + 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + }, + ], + address: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + }, + { + utxos: [ + { + height: 666954, + tx_hash: + '1b19314963be975c57eb37df12b6a8e0598bcb743226cdc684895520f51c4dfe', + tx_pos: 2, + value: 546, + txid: + '1b19314963be975c57eb37df12b6a8e0598bcb743226cdc684895520f51c4dfe', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '4.97', + isValid: false, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 666969, + tx_hash: + '99583b593a3bec993328b076f4988fd77b8423d788183bf2968ed43cec11c454', + tx_pos: 2, + value: 546, + txid: + '99583b593a3bec993328b076f4988fd77b8423d788183bf2968ed43cec11c454', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '9897999873.21001107', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 667560, + tx_hash: + '34002080b2e1b42ff9f33d36dbbd0d8f1aaddc5a00b916054a40c45feebaf548', + tx_pos: 2, + value: 546, + txid: + '34002080b2e1b42ff9f33d36dbbd0d8f1aaddc5a00b916054a40c45feebaf548', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '3.999999998', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 667560, + tx_hash: + '8edccfafe4b002da8f1aa71daae31846c51848968e7d92dcba5f0ff97beb734d', + tx_pos: 2, + value: 546, + txid: + '8edccfafe4b002da8f1aa71daae31846c51848968e7d92dcba5f0ff97beb734d', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + tokenTicker: 'WDT', + tokenName: + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + tokenDocumentUrl: + 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', + tokenDocumentHash: + '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', + decimals: 7, + tokenType: 1, + tokenQty: '523512244.796145', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 667793, + tx_hash: + '9c295332b7bc16758b2e328f21189fa0ea79f71908b47529749b3ba54e523817', + tx_pos: 1, + value: 546, + txid: + '9c295332b7bc16758b2e328f21189fa0ea79f71908b47529749b3ba54e523817', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef', + tokenTicker: 'Sending Token', + tokenName: 'Sending Token', + tokenDocumentUrl: 'developer.bitcoin.com', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '0', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 667909, + tx_hash: + '5bb8f9831d616b3a859ffec4507f66bd5f2f3c057d7f5d5fa5c026216d6c2646', + tx_pos: 1, + value: 546, + txid: + '5bb8f9831d616b3a859ffec4507f66bd5f2f3c057d7f5d5fa5c026216d6c2646', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + tokenTicker: 'CTP', + tokenName: 'Cash Tab Points', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '0', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 667909, + tx_hash: + 'd3183b663a8d67b2b558654896b95102bbe68d164de219da96273ae52de93813', + tx_pos: 1, + value: 546, + txid: + 'd3183b663a8d67b2b558654896b95102bbe68d164de219da96273ae52de93813', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 668550, + tx_hash: + '38a692e3fa974ee186eedc3a03bf3410dd03a5f35bc44f1a07f287b669dce44b', + tx_pos: 2, + value: 546, + txid: + '38a692e3fa974ee186eedc3a03bf3410dd03a5f35bc44f1a07f287b669dce44b', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef', + tokenTicker: 'Sending Token', + tokenName: 'Sending Token', + tokenDocumentUrl: 'developer.bitcoin.com', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '0', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 668779, + tx_hash: + '24604835e5c68aedad2dbf4200ecc1af8c3fa92738445323af86def84d48d572', + tx_pos: 1, + value: 546, + txid: + '24604835e5c68aedad2dbf4200ecc1af8c3fa92738445323af86def84d48d572', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef', + tokenTicker: 'Sending Token', + tokenName: 'Sending Token', + tokenDocumentUrl: 'developer.bitcoin.com', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '0', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 669057, + tx_hash: + 'dd560d87bd632e40c6548021006653a150197ede13fadb5eadfa29abe4400d0e', + tx_pos: 1, + value: 546, + txid: + 'dd560d87bd632e40c6548021006653a150197ede13fadb5eadfa29abe4400d0e', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + tokenTicker: 'NAKAMOTO', + tokenName: 'NAKAMOTO', + tokenDocumentUrl: '', + tokenDocumentHash: '', + decimals: 8, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 669510, + tx_hash: + 'acbb66f826211f40b89e84d9bd2143dfb541d67e1e3c664b17ccd3ba66327a9e', + tx_pos: 1, + value: 546, + txid: + 'acbb66f826211f40b89e84d9bd2143dfb541d67e1e3c664b17ccd3ba66327a9e', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef', + tokenTicker: 'Sending Token', + tokenName: 'Sending Token', + tokenDocumentUrl: 'developer.bitcoin.com', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '0', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 669639, + tx_hash: + '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + tx_pos: 0, + value: 1000, + txid: + '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + vout: 0, + isValid: false, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + '39521b38cd1b6126a57a68b8adfd836020cd53b195f3b4675c58095c5c300ef8', + tx_pos: 0, + value: 700000, + txid: + '39521b38cd1b6126a57a68b8adfd836020cd53b195f3b4675c58095c5c300ef8', + vout: 0, + isValid: false, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + '93949923f02cb5271bd6d0b5a5b937ce5ae22df5bf6117161078f175f0c29d56', + tx_pos: 0, + value: 700000, + txid: + '93949923f02cb5271bd6d0b5a5b937ce5ae22df5bf6117161078f175f0c29d56', + vout: 0, + isValid: false, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + 'ddace66ea968e16e55ebf218814401acc38e0a39150529fa3d1108af04e81373', + tx_pos: 0, + value: 300000, + txid: + 'ddace66ea968e16e55ebf218814401acc38e0a39150529fa3d1108af04e81373', + vout: 0, + isValid: false, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + 'f1147285ac384159b5dfae513bda47a0459f876d046b48f13c8a7ec4f0d20d96', + tx_pos: 0, + value: 700000, + txid: + 'f1147285ac384159b5dfae513bda47a0459f876d046b48f13c8a7ec4f0d20d96', + vout: 0, + isValid: false, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + 'f4ca891d090f2682c7086b27a4d3bc2ed1543fb96123b6649e8f26b644a45b51', + tx_pos: 0, + value: 30000, + txid: + 'f4ca891d090f2682c7086b27a4d3bc2ed1543fb96123b6649e8f26b644a45b51', + vout: 0, + isValid: false, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + ], + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + ], +}; diff --git a/web/cashtab/src/hooks/__mocks__/mockReturnGetSlpBalancesAndUtxosNoZeroBalance.js b/web/cashtab/src/hooks/__mocks__/mockReturnGetSlpBalancesAndUtxosNoZeroBalance.js new file mode 100644 index 000000000..823a4a8a9 --- /dev/null +++ b/web/cashtab/src/hooks/__mocks__/mockReturnGetSlpBalancesAndUtxosNoZeroBalance.js @@ -0,0 +1,733 @@ +import BigNumber from 'bignumber.js'; + +export default { + tokens: [ + { + info: { + height: 660869, + tx_hash: + '16b624b60de4a1d8a06baa129e3a88a4becd499e1d5d0d40b9f2ff4d28e3f660', + tx_pos: 1, + value: 546, + txid: + '16b624b60de4a1d8a06baa129e3a88a4becd499e1d5d0d40b9f2ff4d28e3f660', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', + tokenTicker: 'ST', + tokenName: 'ST', + tokenDocumentUrl: 'developer.bitcoin.com', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + tokenId: + 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', + balance: new BigNumber('1'), + hasBaton: false, + }, + { + info: { + height: 660869, + tx_hash: + 'c7da9ae6a0ce9d4f2f3345f9f0e5da5371228c8aee72b6eeac1b42871b216e6b', + tx_pos: 1, + value: 546, + txid: + 'c7da9ae6a0ce9d4f2f3345f9f0e5da5371228c8aee72b6eeac1b42871b216e6b', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', + tokenTicker: '🍔', + tokenName: 'Burger', + tokenDocumentUrl: + 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '2', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + tokenId: + '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', + balance: new BigNumber('2'), + hasBaton: false, + }, + { + info: { + height: 660869, + tx_hash: + 'dac23f10dd65caa51359c1643ffc93b94d14c05b739590ade85557d338a21040', + tx_pos: 1, + value: 546, + txid: + 'dac23f10dd65caa51359c1643ffc93b94d14c05b739590ade85557d338a21040', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + tokenTicker: 'HONK', + tokenName: 'HONK HONK', + tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + tokenId: + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + balance: new BigNumber('3'), + hasBaton: false, + }, + { + info: { + height: 660869, + tx_hash: + 'efa6f67078810875513a116b389886610d81ecf6daf97d55dd96d3fdd201dfac', + tx_pos: 1, + value: 546, + txid: + 'efa6f67078810875513a116b389886610d81ecf6daf97d55dd96d3fdd201dfac', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', + tokenTicker: 'TAP', + tokenName: 'Thoughts and Prayers', + tokenDocumentUrl: '', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '2', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + tokenId: + 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', + balance: new BigNumber('2'), + hasBaton: false, + }, + { + balance: new BigNumber('310'), + hasBaton: true, + info: { + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + decimals: 9, + height: 660978, + isValid: true, + mintBatonVout: 2, + tokenDocumentHash: '', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + tokenName: 'Cash Tab Points', + tokenTicker: 'CTP', + tokenType: 1, + transactionType: 'mint', + tx_hash: + 'b622b770f74f056e07e5d2ea4d7f8da1c4d865e21e11c31a263602a38d4a2474', + tx_pos: 2, + txid: + 'b622b770f74f056e07e5d2ea4d7f8da1c4d865e21e11c31a263602a38d4a2474', + utxoType: 'minting-baton', + value: 546, + vout: 2, + }, + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + }, + { + balance: new BigNumber('523512344.7961451'), + hasBaton: true, + info: { + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + decimals: 7, + height: 660982, + isValid: true, + mintBatonVout: 2, + tokenDocumentHash: + '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', + tokenDocumentUrl: + 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + tokenName: + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + tokenTicker: 'WDT', + tokenType: 1, + transactionType: 'mint', + tx_hash: + '67605f3d18135b52d95a4877a427d100c14f2610c63ee84eaf4856f883a0b70e', + tx_pos: 2, + txid: + '67605f3d18135b52d95a4877a427d100c14f2610c63ee84eaf4856f883a0b70e', + utxoType: 'minting-baton', + value: 546, + vout: 2, + }, + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + }, + { + info: { + height: 667750, + tx_hash: + 'e9a94cc174839e3659d2fe4d33490528d18ad91404b65eb8cc35d8fa2d3f5096', + tx_pos: 2, + value: 546, + txid: + 'e9a94cc174839e3659d2fe4d33490528d18ad91404b65eb8cc35d8fa2d3f5096', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '5.854300861', + isValid: true, + address: + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + balance: new BigNumber('9897999885.211011069'), + hasBaton: false, + }, + { + info: { + height: 669057, + tx_hash: + 'dd560d87bd632e40c6548021006653a150197ede13fadb5eadfa29abe4400d0e', + tx_pos: 1, + value: 546, + txid: + 'dd560d87bd632e40c6548021006653a150197ede13fadb5eadfa29abe4400d0e', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + tokenTicker: 'NAKAMOTO', + tokenName: 'NAKAMOTO', + tokenDocumentUrl: '', + tokenDocumentHash: '', + decimals: 8, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + tokenId: + 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + balance: new BigNumber('1'), + hasBaton: false, + }, + ], + nonSlpUtxos: [ + { + height: 669639, + tx_hash: + '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + tx_pos: 0, + value: 1000, + txid: + '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + '39521b38cd1b6126a57a68b8adfd836020cd53b195f3b4675c58095c5c300ef8', + tx_pos: 0, + value: 700000, + txid: + '39521b38cd1b6126a57a68b8adfd836020cd53b195f3b4675c58095c5c300ef8', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + '93949923f02cb5271bd6d0b5a5b937ce5ae22df5bf6117161078f175f0c29d56', + tx_pos: 0, + value: 700000, + txid: + '93949923f02cb5271bd6d0b5a5b937ce5ae22df5bf6117161078f175f0c29d56', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + 'ddace66ea968e16e55ebf218814401acc38e0a39150529fa3d1108af04e81373', + tx_pos: 0, + value: 300000, + txid: + 'ddace66ea968e16e55ebf218814401acc38e0a39150529fa3d1108af04e81373', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + 'f1147285ac384159b5dfae513bda47a0459f876d046b48f13c8a7ec4f0d20d96', + tx_pos: 0, + value: 700000, + txid: + 'f1147285ac384159b5dfae513bda47a0459f876d046b48f13c8a7ec4f0d20d96', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + 'f4ca891d090f2682c7086b27a4d3bc2ed1543fb96123b6649e8f26b644a45b51', + tx_pos: 0, + value: 30000, + txid: + 'f4ca891d090f2682c7086b27a4d3bc2ed1543fb96123b6649e8f26b644a45b51', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + ], + slpUtxos: [ + { + height: 660869, + tx_hash: + '16b624b60de4a1d8a06baa129e3a88a4becd499e1d5d0d40b9f2ff4d28e3f660', + tx_pos: 1, + value: 546, + txid: + '16b624b60de4a1d8a06baa129e3a88a4becd499e1d5d0d40b9f2ff4d28e3f660', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', + tokenTicker: 'ST', + tokenName: 'ST', + tokenDocumentUrl: 'developer.bitcoin.com', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660869, + tx_hash: + 'c7da9ae6a0ce9d4f2f3345f9f0e5da5371228c8aee72b6eeac1b42871b216e6b', + tx_pos: 1, + value: 546, + txid: + 'c7da9ae6a0ce9d4f2f3345f9f0e5da5371228c8aee72b6eeac1b42871b216e6b', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', + tokenTicker: '🍔', + tokenName: 'Burger', + tokenDocumentUrl: + 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '2', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660869, + tx_hash: + 'dac23f10dd65caa51359c1643ffc93b94d14c05b739590ade85557d338a21040', + tx_pos: 1, + value: 546, + txid: + 'dac23f10dd65caa51359c1643ffc93b94d14c05b739590ade85557d338a21040', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + tokenTicker: 'HONK', + tokenName: 'HONK HONK', + tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660869, + tx_hash: + 'efa6f67078810875513a116b389886610d81ecf6daf97d55dd96d3fdd201dfac', + tx_pos: 1, + value: 546, + txid: + 'efa6f67078810875513a116b389886610d81ecf6daf97d55dd96d3fdd201dfac', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', + tokenTicker: 'TAP', + tokenName: 'Thoughts and Prayers', + tokenDocumentUrl: '', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '2', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660978, + tx_hash: + 'b622b770f74f056e07e5d2ea4d7f8da1c4d865e21e11c31a263602a38d4a2474', + tx_pos: 2, + value: 546, + txid: + 'b622b770f74f056e07e5d2ea4d7f8da1c4d865e21e11c31a263602a38d4a2474', + vout: 2, + utxoType: 'minting-baton', + transactionType: 'mint', + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + tokenType: 1, + tokenTicker: 'CTP', + tokenName: 'Cash Tab Points', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + mintBatonVout: 2, + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 660982, + tx_hash: + '67605f3d18135b52d95a4877a427d100c14f2610c63ee84eaf4856f883a0b70e', + tx_pos: 2, + value: 546, + txid: + '67605f3d18135b52d95a4877a427d100c14f2610c63ee84eaf4856f883a0b70e', + vout: 2, + utxoType: 'minting-baton', + transactionType: 'mint', + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + tokenType: 1, + tokenTicker: 'WDT', + tokenName: + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + tokenDocumentUrl: + 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', + tokenDocumentHash: + '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', + decimals: 7, + mintBatonVout: 2, + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 662799, + tx_hash: + 'b399a5ae69e4ac4c96b27c680a541e6b8142006bdc2484a959821858fc0b4ca3', + tx_pos: 2, + value: 546, + txid: + 'b399a5ae69e4ac4c96b27c680a541e6b8142006bdc2484a959821858fc0b4ca3', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + tokenTicker: 'CTP', + tokenName: 'Cash Tab Points', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '310', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667561, + tx_hash: + '4ca7f70996c699b0f988597a2dd2700f1f24f305318ddc8fe137d96d5fa96bf5', + tx_pos: 1, + value: 546, + txid: + '4ca7f70996c699b0f988597a2dd2700f1f24f305318ddc8fe137d96d5fa96bf5', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + tokenTicker: 'HONK', + tokenName: 'HONK HONK', + tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667750, + tx_hash: + 'de912bc7a6a1b14abe04960cd9a0ef88a4f59eb04db765f7bc2c0d2c2f997054', + tx_pos: 1, + value: 546, + txid: + 'de912bc7a6a1b14abe04960cd9a0ef88a4f59eb04db765f7bc2c0d2c2f997054', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + tokenTicker: 'WDT', + tokenName: + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + tokenDocumentUrl: + 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', + tokenDocumentHash: + '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', + decimals: 7, + tokenType: 1, + tokenQty: '100.0000001', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667750, + tx_hash: + 'e25cebd4cccbbdd91b36f672d51f5ce978e0817839be9854c3550704aec4359d', + tx_pos: 1, + value: 546, + txid: + 'e25cebd4cccbbdd91b36f672d51f5ce978e0817839be9854c3550704aec4359d', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + tokenTicker: 'HONK', + tokenName: 'HONK HONK', + tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667750, + tx_hash: + 'e9a94cc174839e3659d2fe4d33490528d18ad91404b65eb8cc35d8fa2d3f5096', + tx_pos: 2, + value: 546, + txid: + 'e9a94cc174839e3659d2fe4d33490528d18ad91404b65eb8cc35d8fa2d3f5096', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '5.854300861', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 667751, + tx_hash: + '05e87142de1bb8c2a43d22a2e4b97855eb84c3c76f4ea956a654efda8d0557ca', + tx_pos: 1, + value: 546, + txid: + '05e87142de1bb8c2a43d22a2e4b97855eb84c3c76f4ea956a654efda8d0557ca', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '1.14669914', + isValid: true, + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', + }, + { + height: 666969, + tx_hash: + '99583b593a3bec993328b076f4988fd77b8423d788183bf2968ed43cec11c454', + tx_pos: 2, + value: 546, + txid: + '99583b593a3bec993328b076f4988fd77b8423d788183bf2968ed43cec11c454', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '9897999873.21001107', + isValid: true, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 667560, + tx_hash: + '34002080b2e1b42ff9f33d36dbbd0d8f1aaddc5a00b916054a40c45feebaf548', + tx_pos: 2, + value: 546, + txid: + '34002080b2e1b42ff9f33d36dbbd0d8f1aaddc5a00b916054a40c45feebaf548', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '3.999999998', + isValid: true, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 667560, + tx_hash: + '8edccfafe4b002da8f1aa71daae31846c51848968e7d92dcba5f0ff97beb734d', + tx_pos: 2, + value: 546, + txid: + '8edccfafe4b002da8f1aa71daae31846c51848968e7d92dcba5f0ff97beb734d', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + tokenTicker: 'WDT', + tokenName: + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + tokenDocumentUrl: + 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', + tokenDocumentHash: + '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', + decimals: 7, + tokenType: 1, + tokenQty: '523512244.796145', + isValid: true, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 667909, + tx_hash: + 'd3183b663a8d67b2b558654896b95102bbe68d164de219da96273ae52de93813', + tx_pos: 1, + value: 546, + txid: + 'd3183b663a8d67b2b558654896b95102bbe68d164de219da96273ae52de93813', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + { + height: 669057, + tx_hash: + 'dd560d87bd632e40c6548021006653a150197ede13fadb5eadfa29abe4400d0e', + tx_pos: 1, + value: 546, + txid: + 'dd560d87bd632e40c6548021006653a150197ede13fadb5eadfa29abe4400d0e', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + tokenTicker: 'NAKAMOTO', + tokenName: 'NAKAMOTO', + tokenDocumentUrl: '', + tokenDocumentHash: '', + decimals: 8, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + ], +}; diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js index 5ad0f5ec1..9407486b1 100644 --- a/web/cashtab/src/hooks/__tests__/useBCH.test.js +++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js @@ -1,329 +1,343 @@ /* 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 mockTxHistory from '../__mocks__/mockTxHistory'; import mockFlatTxHistory from '../__mocks__/mockFlatTxHistory'; import mockTxDataWithPassthrough from '../__mocks__/mockTxDataWithPassthrough'; import { tokenSendWdt, tokenReceiveTBS, } from '../__mocks__/mockParseTokenInfoForTxHistory'; import { mockSentCashTx, mockReceivedCashTx, mockSentTokenTx, mockReceivedTokenTx, } 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'; 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 result = await getSlpBalancesAndUtxos( mockReturnGetHydratedUtxoDetails, ); expect(result).toStrictEqual(mockReturnGetSlpBalancesAndUtxos); }); + it(`Ignores SLP utxos with utxo.tokenQty === '0'`, async () => { + const { getSlpBalancesAndUtxos } = useBCH(); + + const result = await getSlpBalancesAndUtxos( + mockReturnGetHydratedUtxoDetailsWithZeroBalance, + ); + + expect(result).toStrictEqual( + mockReturnGetSlpBalancesAndUtxosNoZeroBalance, + ); + }); + it('sends BCH correctly', async () => { const { sendBch } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, utxos, wallet, destinationAddress, sendAmount, } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect( await sendBch( BCH, wallet, utxos, destinationAddress, sendAmount, 1.01, ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( expectedHex, ); }); it('sends BCH correctly with callback', async () => { const { sendBch } = useBCH(); const BCH = new BCHJS(); const callback = jest.fn(); const { expectedTxId, expectedHex, utxos, wallet, destinationAddress, sendAmount, } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect( await sendBch( BCH, wallet, utxos, destinationAddress, sendAmount, 1.01, callback, ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( expectedHex, ); expect(callback).toHaveBeenCalledWith(expectedTxId); }); it(`Throws error if called trying to send one base unit ${currency.ticker} more than available in utxo set`, async () => { const { sendBch } = 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 = sendBch( BCH, wallet, utxos, destinationAddress, oneBaseUnitMoreThanBalance, 1.01, ); expect(failedSendBch).rejects.toThrow(new Error('Insufficient funds')); const nullValuesSendBch = await sendBch( BCH, wallet, utxos, destinationAddress, null, 1.01, ); expect(nullValuesSendBch).toBe(null); }); it('Throws error on attempt to send one satoshi less than backend dust limit', async () => { const { sendBch } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, utxos, wallet, destinationAddress } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); const failedSendBch = sendBch( BCH, wallet, utxos, destinationAddress, new BigNumber(currency.dust) .minus(new BigNumber('0.00000001')) .toString(), 1.01, ); expect(failedSendBch).rejects.toThrow(new Error('dust')); const nullValuesSendBch = await sendBch( BCH, wallet, utxos, destinationAddress, null, 1.01, ); expect(nullValuesSendBch).toBe(null); }); it('receives errors from the network and parses it', async () => { const { sendBch } = 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 = sendBch( BCH, wallet, utxos, destinationAddress, sendAmount, 1.01, ); 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 = sendBch( BCH, wallet, utxos, destinationAddress, sendAmount, 1.01, ); 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 = sendBch( BCH, wallet, utxos, destinationAddress, sendAmount, 1.01, ); 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 = sendBch( BCH, wallet, utxos, destinationAddress, sendAmount, 1.01, ); await expect(tooManyAncestorsMempool).rejects.toThrow( new Error( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)', ), ); }); it('Correctly flattens transaction history', () => { const { flattenTransactions } = useBCH(); expect(flattenTransactions(mockTxHistory, 10)).toStrictEqual( mockFlatTxHistory, ); }); it(`Correctly parses a "send ${currency.ticker}" transaction`, () => { const { parseTxData } = useBCH(); expect(parseTxData([mockTxDataWithPassthrough[0]])).toStrictEqual( mockSentCashTx, ); }); it(`Correctly parses a "receive ${currency.ticker}" transaction`, () => { const { parseTxData } = useBCH(); expect(parseTxData([mockTxDataWithPassthrough[5]])).toStrictEqual( mockReceivedCashTx, ); }); it(`Correctly parses a "send ${currency.tokenTicker}" transaction`, () => { const { parseTxData } = useBCH(); expect(parseTxData([mockTxDataWithPassthrough[1]])).toStrictEqual( mockSentTokenTx, ); }); it(`Correctly parses a "receive ${currency.tokenTicker}" transaction`, () => { const { parseTxData } = useBCH(); expect(parseTxData([mockTxDataWithPassthrough[3]])).toStrictEqual( mockReceivedTokenTx, ); }); it(`Correctly parses a "send ${currency.tokenTicker}" transaction with token details`, () => { const { parseTokenInfoForTxHistory } = useBCH(); expect( parseTokenInfoForTxHistory( 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(); expect( parseTokenInfoForTxHistory( tokenReceiveTBS.parsedTx, tokenReceiveTBS.tokenInfo, ), ).toStrictEqual(tokenReceiveTBS.cashtabTokenInfo); }); }); diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js index 263e3c718..e9528ed0b 100644 --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -1,824 +1,829 @@ import BigNumber from 'bignumber.js'; import { currency } from '@components/Common/Ticker'; import SlpWallet from 'minimal-slp-wallet'; import { toSmallestDenomination } from '@utils/cashMethods'; 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 => { /* Desired output [ { txid: '', type: send, receive receivingAddress: '', quantity: amount bcha token: true/false tokenInfo: { tokenId: tokenQty: txType: mint, send, other } } ] */ 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.confirmations = tx.confirmations; parsedTx.height = tx.height; parsedTx.blocktime = tx.blocktime; let amountSent = 0; let amountReceived = 0; // Assume an incoming transaction let outgoingTx = false; let tokenTx = false; let destinationAddress = tx.address; // If vin includes tx address, this is an outgoing tx // Note that with bch-input data, we do not have input amounts 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; } } // 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, OP_RETURN or token tx if ( !Object.keys(thisOutput.scriptPubKey).includes('addresses') ) { // For now, assume this is a token tx tokenTx = true; continue; } 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]; } } // Construct parsedTx parsedTx.txid = tx.txid; parsedTx.amountSent = amountSent; parsedTx.amountReceived = amountReceived; parsedTx.tokenTx = tokenTx; parsedTx.outgoingTx = outgoingTx; parsedTx.destinationAddress = destinationAddress; 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 const txDataWithPassThrough = await BCH.RawTransactions.getTxData( flatTx.txid, ); txDataWithPassThrough.height = flatTx.height; txDataWithPassThrough.address = flatTx.address; return txDataWithPassThrough; }; const getTxData = async (BCH, txHistory) => { // 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); return parsed; } catch (err) { console.log(`Error in Promise.all(txDataPromises):`); console.log(err); return err; } }; const parseTokenInfoForTxHistory = (parsedTx, tokenInfo) => { // Scan over inputs to find out originating addresses const { sendInputsFull, sendOutputsFull } = tokenInfo; const sendingTokenAddresses = []; 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 continue; } if (parsedTx.outgoingTx) { qtySent = qtySent.plus( new BigNumber(sendOutputsFull[i].amount), ); } else { 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; return cashtabTokenInfo; }; const addTokenTxDataToSingleTx = async (BCH, parsedTx) => { // Accept one parsedTx // If it's a token tx, do an API call to get token info and return it // If it's not a token tx, just return it if (!parsedTx.tokenTx) { return parsedTx; } const tokenData = await BCH.SLP.Utils.txDetails(parsedTx.txid); const { tokenInfo } = tokenData; parsedTx.tokenInfo = parseTokenInfoForTxHistory(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) => { let hydratedUtxoDetails; try { hydratedUtxoDetails = await BCH.SLP.Utils.hydrateUtxos(utxos); return hydratedUtxoDetails; } catch (err) { console.log( `Error in BCH.SLP.Utils.hydrateUtxos(utxosResponse.utxos)`, ); console.log(err); return err; } }; const getSlpBalancesAndUtxos = hydratedUtxoDetails => { const 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); //console.log(`nullUtxos`, nullUtxos); if (nullUtxos.length > 0) { console.log( `${nullUtxos.length} null utxos found, ignoring results`, ); throw new Error('Null utxos found, ignoring results'); } // Prevent app from treating slpUtxos as nonSlpUtxos // Must enforce === false as api will occasionally return utxo.isValid === null // Do not classify utxos with 546 satoshis as nonSlpUtxos as a precaution // Do not classify any utxos that include token information as nonSlpUtxos const nonSlpUtxos = hydratedUtxos.filter( utxo => utxo.isValid === false && utxo.value !== 546 && !utxo.tokenName, ); - const slpUtxos = hydratedUtxos.filter(utxo => utxo.isValid); + // 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 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), 546, ); // Return any token change back to the sender. if (slpSendObj.outputs > 1) { // Try to send this to Path1899 to move all utxos off legacy addresses if (wallet.Path1899.legacyAddress) { transactionBuilder.addOutput( wallet.Path1899.legacyAddress, 546, ); } else { // If you can't, send it back from whence it came transactionBuilder.addOutput( BCH.SLP.Address.toLegacyAddress( tokenUtxosBeingSpent[0].address, ), 546, ); } } // 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 - 546 * 2; if (remainder < 1) { throw new Error('Selected UTXO does not have enough satoshis'); } // Last output: send the BCH change back to the wallet. // If Path1899, send it to Path1899 address if (wallet.Path1899.legacyAddress) { transactionBuilder.addOutput( wallet.Path1899.legacyAddress, remainder, ); } else { // Otherwise 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 sendBch = async ( BCH, wallet, utxos, destinationAddress, sendAmount, feeInSatsPerByte, callbackTxId, encodedOpReturn, ) => { // Note: callbackTxId is a callback function that accepts a txid as its only parameter try { if (!sendAmount) { return null; } const value = new BigNumber(sendAmount); // If user is attempting to send less than minimum accepted by the backend if (value.lt(new BigNumber(currency.dust))) { // Throw the same error given by the backend attempting to broadcast such a tx throw new Error('dust'); } const REMAINDER_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'); 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; } 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 = encodedOpReturn ? calcFee(BCH, inputUtxos, 3, feeInSatsPerByte) : calcFee(BCH, inputUtxos, 2, feeInSatsPerByte); if (originalAmount.minus(satoshisToSend).minus(txFee).gte(0)) { break; } } // 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 (encodedOpReturn) { transactionBuilder.addOutput(encodedOpReturn, 0); } // add output w/ address and amount to send transactionBuilder.addOutput( BCH.Address.toCashAddress(destinationAddress), parseInt(toSmallestDenomination(value)), ); if ( remainder.gte( toSmallestDenomination(new BigNumber(currency.dust)), ) ) { 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 (callbackTxId) { callbackTxId(txidStr); } 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, sendBch, sendToken, }; }