diff --git a/web/cashtab/src/hooks/__mocks__/mockReturnGetHydratedUtxoDetails.js b/web/cashtab/src/hooks/__mocks__/mockReturnGetHydratedUtxoDetails.js new file mode 100644 index 000000000..ea4e2263d --- /dev/null +++ b/web/cashtab/src/hooks/__mocks__/mockReturnGetHydratedUtxoDetails.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: '1e-9', + 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: '1e-9', + 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: '1e-9', + 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: '1e-9', + 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: '1e-9', + 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: '1e-9', + 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: '1e-9', + 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__/mockReturnGetSlpBalancesAndUtxos.js b/web/cashtab/src/hooks/__mocks__/mockReturnGetSlpBalancesAndUtxos.js index e922984be..ea24f505f 100644 --- a/web/cashtab/src/hooks/__mocks__/mockReturnGetSlpBalancesAndUtxos.js +++ b/web/cashtab/src/hooks/__mocks__/mockReturnGetSlpBalancesAndUtxos.js @@ -1,128 +1,878 @@ import BigNumber from 'bignumber.js'; export default { tokens: [ { info: { - height: 659837, + height: 660869, tx_hash: - 'bb055d815e795d7a4fe41fd67288d71886678e7d1e5ba9ddd7f6daffa5f70ceb', - tx_pos: 2, + '16b624b60de4a1d8a06baa129e3a88a4becd499e1d5d0d40b9f2ff4d28e3f660', + tx_pos: 1, value: 546, txid: - 'bb055d815e795d7a4fe41fd67288d71886678e7d1e5ba9ddd7f6daffa5f70ceb', - vout: 2, + '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: - '009ff65ef50632111a9661f67d80025481ebc80ced239d32a92705b04d5df8cc', - tokenTicker: 'TTT', - tokenName: 'TT', - tokenDocumentUrl: 'mint.bitcoin.com', + 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', + tokenTicker: 'TAP', + tokenName: 'Thoughts and Prayers', + tokenDocumentUrl: '', tokenDocumentHash: '', - decimals: 1, + decimals: 0, tokenType: 1, - tokenQty: '7', + tokenQty: '2', isValid: true, address: - 'bitcoincash:qphazxf3vhe4qchvzz2pjempdhplaxcj957xqq8mg2', + 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', }, tokenId: - '009ff65ef50632111a9661f67d80025481ebc80ced239d32a92705b04d5df8cc', - balance: new BigNumber(7), + 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', + balance: new BigNumber('2'), hasBaton: false, }, { info: { - height: 659843, + height: 660978, tx_hash: - '88b7dac07cb30566a6264f330bedda690c8dff151c2307692c79e13dc59ca2ba', + 'b622b770f74f056e07e5d2ea4d7f8da1c4d865e21e11c31a263602a38d4a2474', tx_pos: 2, value: 546, txid: - '88b7dac07cb30566a6264f330bedda690c8dff151c2307692c79e13dc59ca2ba', + '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', + }, + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + balance: new BigNumber('310.000000001'), + hasBaton: true, + }, + { + info: { + 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', + }, + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + balance: new BigNumber('523512344.7961451'), + hasBaton: true, + }, + { + 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: 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: '1e-9', + isValid: true, + address: + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + }, + tokenId: + 'bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef', + balance: new BigNumber('4e-9'), + hasBaton: false, + }, + { + info: { + height: 669057, + tx_hash: + 'dd560d87bd632e40c6548021006653a150197ede13fadb5eadfa29abe4400d0e', + tx_pos: 1, + value: 546, + txid: + 'dd560d87bd632e40c6548021006653a150197ede13fadb5eadfa29abe4400d0e', + vout: 1, utxoType: 'token', transactionType: 'send', tokenId: - '2aef6e63edfded1a299e78b529286deea2a6dd5299b6911778c25632d78a9479', - tokenTicker: 'PTC', - tokenName: 'pitico', - tokenDocumentUrl: 'mint.bitcoin.com', + 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + tokenTicker: 'NAKAMOTO', + tokenName: 'NAKAMOTO', + tokenDocumentUrl: '', tokenDocumentHash: '', - decimals: 5, + decimals: 8, tokenType: 1, - tokenQty: '99.35', + tokenQty: '1', isValid: true, address: - 'bitcoincash:qphazxf3vhe4qchvzz2pjempdhplaxcj957xqq8mg2', + 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, tokenId: - '2aef6e63edfded1a299e78b529286deea2a6dd5299b6911778c25632d78a9479', - balance: new BigNumber(99.35), + 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + balance: new BigNumber('1'), hasBaton: false, }, ], nonSlpUtxos: [ { - height: 660544, + height: 669639, tx_hash: - '5ebb5272b6f875b65f41f2cfca031e6f07f9cf519ee7577f0f63cb79aa86fd0a', - tx_pos: 1, - value: 2196, + '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + tx_pos: 0, + value: 1000, txid: - '5ebb5272b6f875b65f41f2cfca031e6f07f9cf519ee7577f0f63cb79aa86fd0a', - vout: 1, + '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:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05', + 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: 659837, + 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: - 'bb055d815e795d7a4fe41fd67288d71886678e7d1e5ba9ddd7f6daffa5f70ceb', + '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: - 'bb055d815e795d7a4fe41fd67288d71886678e7d1e5ba9ddd7f6daffa5f70ceb', + '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: - '009ff65ef50632111a9661f67d80025481ebc80ced239d32a92705b04d5df8cc', - tokenTicker: 'TTT', - tokenName: 'TT', - tokenDocumentUrl: 'mint.bitcoin.com', + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + tokenTicker: 'HONK', + tokenName: 'HONK HONK', + tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', - decimals: 1, + decimals: 0, tokenType: 1, - tokenQty: '7', + tokenQty: '1', isValid: true, - address: 'bitcoincash:qphazxf3vhe4qchvzz2pjempdhplaxcj957xqq8mg2', + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', }, { - height: 659843, + height: 667750, tx_hash: - '88b7dac07cb30566a6264f330bedda690c8dff151c2307692c79e13dc59ca2ba', + 'e9a94cc174839e3659d2fe4d33490528d18ad91404b65eb8cc35d8fa2d3f5096', tx_pos: 2, value: 546, txid: - '88b7dac07cb30566a6264f330bedda690c8dff151c2307692c79e13dc59ca2ba', + 'e9a94cc174839e3659d2fe4d33490528d18ad91404b65eb8cc35d8fa2d3f5096', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: - '2aef6e63edfded1a299e78b529286deea2a6dd5299b6911778c25632d78a9479', - tokenTicker: 'PTC', - tokenName: 'pitico', - tokenDocumentUrl: 'mint.bitcoin.com', + '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: 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: '1e-9', + 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: '1e-9', + 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: '1e-9', + 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: '1e-9', + 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: 5, + decimals: 9, tokenType: 1, - tokenQty: '99.35', + tokenQty: '1e-9', isValid: true, - address: 'bitcoincash:qphazxf3vhe4qchvzz2pjempdhplaxcj957xqq8mg2', + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, ], }; diff --git a/web/cashtab/src/hooks/__mocks__/mockReturnGetUtxos.js b/web/cashtab/src/hooks/__mocks__/mockReturnGetUtxos.js index 604ce731f..ea8d8312c 100644 --- a/web/cashtab/src/hooks/__mocks__/mockReturnGetUtxos.js +++ b/web/cashtab/src/hooks/__mocks__/mockReturnGetUtxos.js @@ -1,33 +1,248 @@ export default [ { utxos: [ { - height: 659837, + height: 660869, tx_hash: - 'bb055d815e795d7a4fe41fd67288d71886678e7d1e5ba9ddd7f6daffa5f70ceb', + '16b624b60de4a1d8a06baa129e3a88a4becd499e1d5d0d40b9f2ff4d28e3f660', + tx_pos: 1, + value: 546, + }, + { + height: 660869, + tx_hash: + 'c7da9ae6a0ce9d4f2f3345f9f0e5da5371228c8aee72b6eeac1b42871b216e6b', + tx_pos: 1, + value: 546, + }, + { + height: 660869, + tx_hash: + 'dac23f10dd65caa51359c1643ffc93b94d14c05b739590ade85557d338a21040', + tx_pos: 1, + value: 546, + }, + { + height: 660869, + tx_hash: + 'efa6f67078810875513a116b389886610d81ecf6daf97d55dd96d3fdd201dfac', + tx_pos: 1, + value: 546, + }, + { + height: 660978, + tx_hash: + 'b622b770f74f056e07e5d2ea4d7f8da1c4d865e21e11c31a263602a38d4a2474', + tx_pos: 2, + value: 546, + }, + { + height: 660982, + tx_hash: + '67605f3d18135b52d95a4877a427d100c14f2610c63ee84eaf4856f883a0b70e', + tx_pos: 2, + value: 546, + }, + { + height: 662159, + tx_hash: + '05e90e9f35bc041a2939e0e28cf9c436c9adb0f247a7fb0d1f4abb26d418f096', + tx_pos: 1, + value: 546, + }, + { + height: 662799, + tx_hash: + 'b399a5ae69e4ac4c96b27c680a541e6b8142006bdc2484a959821858fc0b4ca3', tx_pos: 2, value: 546, }, { - height: 659843, + height: 662935, + tx_hash: + 'ec423d0089f5cd85973ff6d875e9507f6b396b3b82bf6e9f5cfb24b7c70273bd', + tx_pos: 1, + value: 546, + }, + { + height: 662987, + tx_hash: + '9b4361b24c756ff7a74ea5261be565acade0b246fb85422086ac273c1e4ee7d5', + tx_pos: 1, + value: 546, + }, + { + height: 667561, + tx_hash: + '4ca7f70996c699b0f988597a2dd2700f1f24f305318ddc8fe137d96d5fa96bf5', + tx_pos: 1, + value: 546, + }, + { + height: 667750, + tx_hash: + 'de912bc7a6a1b14abe04960cd9a0ef88a4f59eb04db765f7bc2c0d2c2f997054', + tx_pos: 1, + value: 546, + }, + { + height: 667750, tx_hash: - '88b7dac07cb30566a6264f330bedda690c8dff151c2307692c79e13dc59ca2ba', + 'e25cebd4cccbbdd91b36f672d51f5ce978e0817839be9854c3550704aec4359d', + tx_pos: 1, + value: 546, + }, + { + height: 667750, + tx_hash: + 'e9a94cc174839e3659d2fe4d33490528d18ad91404b65eb8cc35d8fa2d3f5096', tx_pos: 2, value: 546, }, + { + height: 667751, + tx_hash: + '05e87142de1bb8c2a43d22a2e4b97855eb84c3c76f4ea956a654efda8d0557ca', + tx_pos: 1, + value: 546, + }, ], - address: 'bitcoincash:qphazxf3vhe4qchvzz2pjempdhplaxcj957xqq8mg2', + address: 'bitcoincash:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuv44zm0ed', }, { utxos: [ { - height: 660544, + height: 660971, tx_hash: - '5ebb5272b6f875b65f41f2cfca031e6f07f9cf519ee7577f0f63cb79aa86fd0a', + 'aefc3f3c65760d0f0fa716a84d12c4dc76ca7552953d6c7a4358abb6e24c5d7c', tx_pos: 1, - value: 2196, + value: 546, + }, + ], + address: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + }, + { + utxos: [ + { + height: 666954, + tx_hash: + '1b19314963be975c57eb37df12b6a8e0598bcb743226cdc684895520f51c4dfe', + tx_pos: 2, + value: 546, + }, + { + height: 666969, + tx_hash: + '99583b593a3bec993328b076f4988fd77b8423d788183bf2968ed43cec11c454', + tx_pos: 2, + value: 546, + }, + { + height: 667560, + tx_hash: + '34002080b2e1b42ff9f33d36dbbd0d8f1aaddc5a00b916054a40c45feebaf548', + tx_pos: 2, + value: 546, + }, + { + height: 667560, + tx_hash: + '8edccfafe4b002da8f1aa71daae31846c51848968e7d92dcba5f0ff97beb734d', + tx_pos: 2, + value: 546, + }, + { + height: 667793, + tx_hash: + '9c295332b7bc16758b2e328f21189fa0ea79f71908b47529749b3ba54e523817', + tx_pos: 1, + value: 546, + }, + { + height: 667909, + tx_hash: + '5bb8f9831d616b3a859ffec4507f66bd5f2f3c057d7f5d5fa5c026216d6c2646', + tx_pos: 1, + value: 546, + }, + { + height: 667909, + tx_hash: + 'd3183b663a8d67b2b558654896b95102bbe68d164de219da96273ae52de93813', + tx_pos: 1, + value: 546, + }, + { + height: 668550, + tx_hash: + '38a692e3fa974ee186eedc3a03bf3410dd03a5f35bc44f1a07f287b669dce44b', + tx_pos: 2, + value: 546, + }, + { + height: 668779, + tx_hash: + '24604835e5c68aedad2dbf4200ecc1af8c3fa92738445323af86def84d48d572', + tx_pos: 1, + value: 546, + }, + { + height: 669057, + tx_hash: + 'dd560d87bd632e40c6548021006653a150197ede13fadb5eadfa29abe4400d0e', + tx_pos: 1, + value: 546, + }, + { + height: 669510, + tx_hash: + 'acbb66f826211f40b89e84d9bd2143dfb541d67e1e3c664b17ccd3ba66327a9e', + tx_pos: 1, + value: 546, + }, + { + height: 669639, + tx_hash: + '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + tx_pos: 0, + value: 1000, + }, + { + height: 669639, + tx_hash: + '39521b38cd1b6126a57a68b8adfd836020cd53b195f3b4675c58095c5c300ef8', + tx_pos: 0, + value: 700000, + }, + { + height: 669639, + tx_hash: + '93949923f02cb5271bd6d0b5a5b937ce5ae22df5bf6117161078f175f0c29d56', + tx_pos: 0, + value: 700000, + }, + { + height: 669639, + tx_hash: + 'ddace66ea968e16e55ebf218814401acc38e0a39150529fa3d1108af04e81373', + tx_pos: 0, + value: 300000, + }, + { + height: 669639, + tx_hash: + 'f1147285ac384159b5dfae513bda47a0459f876d046b48f13c8a7ec4f0d20d96', + tx_pos: 0, + value: 700000, + }, + { + height: 669639, + tx_hash: + 'f4ca891d090f2682c7086b27a4d3bc2ed1543fb96123b6649e8f26b644a45b51', + tx_pos: 0, + value: 30000, }, ], - address: 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05', + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, ]; diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js index fca5fa05e..e6a84a531 100644 --- a/web/cashtab/src/hooks/__tests__/useBCH.test.js +++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js @@ -1,222 +1,210 @@ /* eslint-disable no-native-reassign */ import useBCH from '../useBCH'; import mockReturnGetUtxos from '../__mocks__/mockReturnGetUtxos'; +import mockReturnGetHydratedUtxoDetails from '../__mocks__/mockReturnGetHydratedUtxoDetails'; import mockReturnGetSlpBalancesAndUtxos from '../__mocks__/mockReturnGetSlpBalancesAndUtxos'; import sendBCHMock from '../__mocks__/sendBCH'; import BCHJS from '@psf/bch-js'; // TODO: should be removed when external lib not needed anymore import { currency } from '../../components/Common/Ticker'; import sendBCH from '../__mocks__/sendBCH'; 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 = [{}, {}]; // For 1.01 sat/byte fee let expectedTxFee = 378; if (currency.defaultFee === 3.01) { expectedTxFee = 1126; } else if (currency.defaultFee === 5.01) { expectedTxFee = 1874; } else if ((currency.defaultFee = 83.3)) { expectedTxFee = 31155; } expect(calcFee(BCH, utxosMock)).toBe(expectedTxFee); }); - it('gets utxos', async () => { - const { getUtxos } = useBCH(); - const BCH = new BCHJS(); - - const addresses = [ - 'bitcoincash:qphazxf3vhe4qchvzz2pjempdhplaxcj957xqq8mg2', - 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05', - ]; - - const result = await getUtxos(BCH, addresses); - expect(result).toStrictEqual(mockReturnGetUtxos); - }); - - it('gets SLP and BCH balances and utxos', async () => { + it('gets SLP and BCH balances and utxos from hydrated utxo details', async () => { const { getSlpBalancesAndUtxos } = useBCH(); - const BCH = new BCHJS(); - const utxos = mockReturnGetUtxos; - const result = await getSlpBalancesAndUtxos(BCH, utxos); + const result = await getSlpBalancesAndUtxos( + mockReturnGetHydratedUtxoDetails, + ); expect(result).toStrictEqual(mockReturnGetSlpBalancesAndUtxos); }); it('sends BCH correctly', async () => { const { sendBch } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, utxos, wallet, addresses, values, } = sendBCHMock; let expectedHexByFee = expectedHex; if (currency.defaultFee === 3.01) { expectedHexByFee = sendBCHMock.expectedHexThreeSatPerByteFee; } else if (currency.defaultFee === 5.01) { expectedHexByFee = sendBCHMock.expectedHexFiveSatPerByteFee; } else if (currency.defaultFee === 83.3) { expectedHexByFee = sendBCHMock.expectedHexEightyThreeSatPerByteFee; } BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect(await sendBch(BCH, wallet, utxos, { addresses, values })).toBe( `${currency.blockExplorerUrl}/tx/${expectedTxId}`, ); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( expectedHexByFee, ); }); it('sends BCH correctly with callback', async () => { const { sendBch } = useBCH(); const BCH = new BCHJS(); const callback = jest.fn(); const { expectedTxId, expectedHex, utxos, wallet, addresses, values, } = sendBCHMock; let expectedHexByFee = expectedHex; if (currency.defaultFee === 3.01) { expectedHexByFee = sendBCHMock.expectedHexThreeSatPerByteFee; } else if (currency.defaultFee === 5.01) { expectedHexByFee = sendBCHMock.expectedHexFiveSatPerByteFee; } else if (currency.defaultFee === 83.3) { expectedHexByFee = sendBCHMock.expectedHexEightyThreeSatPerByteFee; } BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect( await sendBch(BCH, wallet, utxos, { addresses, values }, callback), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( expectedHexByFee, ); expect(callback).toHaveBeenCalledWith(expectedTxId); }); it('sends BCH with less BCH available on balance', async () => { const { sendBch } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, utxos, wallet, addresses, } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); const failedSendBch = sendBch(BCH, wallet, utxos, { addresses, values: [1], }); expect(failedSendBch).rejects.toThrow(new Error('Insufficient funds')); const nullValuesSendBch = await sendBch(BCH, wallet, utxos, { addresses, values: null, }); expect(nullValuesSendBch).toBe(null); }); it('receives errors from the network and parses it', async () => { const { sendBch } = useBCH(); const BCH = new BCHJS(); const { values, utxos, wallet, addresses } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockImplementation(async () => { throw new Error('insufficient priority (code 66)'); }); const insufficientPriority = sendBch(BCH, wallet, utxos, { addresses, values, }); 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, { addresses, values, }); 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, { addresses, values }); 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, { addresses, values, }); await expect(tooManyAncestorsMempool).rejects.toThrow( new Error( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)', ), ); }); }); diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js index 67434a54b..642cd4d3d 100644 --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -1,554 +1,559 @@ import BigNumber from 'bignumber.js'; import { currency } from '@components/Common/Ticker'; import SlpWallet from 'minimal-slp-wallet'; export default function useBCH() { const DUST = 0.000005; const SEND_BCH_ERRORS = { INSUFICIENT_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 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; } }; // 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 getSlpBalancesAndUtxos = async (BCH, utxos) => { + const getHydratedUtxoDetails = async (BCH, utxos) => { let hydratedUtxoDetails; try { hydratedUtxoDetails = await BCH.SLP.Utils.hydrateUtxos(utxos); - //console.log(`hydratedUtxoDetails`, hydratedUtxoDetails); + 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); 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, { addresses, values, encodedOpReturn }, callbackTxId, ) => { // Note: callbackTxId is a callback function that accepts a txid as its only parameter try { if (!values || values.length === 0) { return null; } const value = values.reduce( (previous, current) => new BigNumber(current).plus(previous), new BigNumber(0), ); 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 = BCH.BitcoinCash.toSatoshi(value.toFixed(8)); 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, addresses.length + 2) : calcFee(BCH, inputUtxos, addresses.length + 1); if (originalAmount.minus(satoshisToSend).minus(txFee).gte(0)) { break; } } // amount to send back to the remainder address. const remainder = Math.floor( originalAmount.minus(satoshisToSend).minus(txFee), ); if (remainder < 0) { const error = new Error(`Insufficient funds`); error.code = SEND_BCH_ERRORS.INSUFICIENT_FUNDS; throw error; } if (encodedOpReturn) { transactionBuilder.addOutput(encodedOpReturn, 0); } // add output w/ address and amount to send for (let i = 0; i < addresses.length; i++) { const address = addresses[i]; transactionBuilder.addOutput( BCH.Address.toCashAddress(address), BCH.BitcoinCash.toSatoshi(Number(values[i]).toFixed(8)), ); } if (remainder >= BCH.BitcoinCash.toSatoshi(DUST)) { transactionBuilder.addOutput(REMAINDER_ADDR, 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, getRestUrl, sendBch, sendToken, }; } diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js index e03a9f041..e5f370788 100644 --- a/web/cashtab/src/hooks/useWallet.js +++ b/web/cashtab/src/hooks/useWallet.js @@ -1,1228 +1,1236 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useEffect } from 'react'; import Paragraph from 'antd/lib/typography/Paragraph'; import { notification } from 'antd'; import useAsyncTimeout from '@hooks/useAsyncTimeout'; import usePrevious from '@hooks/usePrevious'; import useBCH from '@hooks/useBCH'; import BigNumber from 'bignumber.js'; import localforage from 'localforage'; import { currency } from '@components/Common/Ticker'; import _ from 'lodash'; const useWallet = () => { const [wallet, setWallet] = useState(false); const [fiatPrice, setFiatPrice] = useState(null); const [ws, setWs] = useState(null); const [apiError, setApiError] = useState(false); const [walletState, setWalletState] = useState({ balances: {}, tokens: [], slpBalancesAndUtxos: [], txHistory: [], }); - const { getBCH, getUtxos, getSlpBalancesAndUtxos, getTxHistory } = useBCH(); + const { + getBCH, + getUtxos, + getHydratedUtxoDetails, + getSlpBalancesAndUtxos, + getTxHistory, + } = useBCH(); const [loading, setLoading] = useState(true); const [apiIndex, setApiIndex] = useState(0); const [BCH, setBCH] = useState(getBCH(apiIndex)); const [utxos, setUtxos] = useState(null); const { balances, tokens, slpBalancesAndUtxos, txHistory } = walletState; const previousBalances = usePrevious(balances); const previousTokens = usePrevious(tokens); const previousWallet = usePrevious(wallet); 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: BCH.BitcoinCash.toBitcoinCash(totalBalanceInSatoshis), }; }; const deriveAccount = async (BCH, { masterHDNode, path }) => { const node = BCH.HDNode.derivePath(masterHDNode, path); const cashAddress = BCH.HDNode.toCashAddress(node); const slpAddress = BCH.SLP.Address.toSLPAddress(cashAddress); return { cashAddress, slpAddress, fundingWif: BCH.HDNode.toWIF(node), fundingAddress: BCH.SLP.Address.toSLPAddress(cashAddress), legacyAddress: BCH.SLP.Address.toLegacyAddress(cashAddress), }; }; const haveUtxosChanged = (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 previousUtxos === 'undefined' || typeof utxos === 'undefined' ) { // Then they have certainly 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; } // Compare utxo sets const utxoArraysUnchanged = _.isEqual(utxos, previousUtxos); // If utxos are not the same as previousUtxos if (utxoArraysUnchanged) { // then utxos have not changed return false; // otherwise, } else { // utxos have changed return true; } }; const update = async ({ wallet, setWalletState }) => { //console.log(`tick()`); //console.time("update"); try { if (!wallet) { return; } const cashAddresses = [ wallet.Path245.cashAddress, wallet.Path145.cashAddress, wallet.Path1899.cashAddress, ]; const utxos = await getUtxos(BCH, cashAddresses); - //console.log(`utxos`, utxos); // 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'); } setUtxos(utxos); const utxosHaveChanged = haveUtxosChanged(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 walletState has not changed and does not need to be updated //console.timeEnd("update"); return; } - // todo: another available optimization, update slpBalancesandUtxos by hydrating only the new utxos - const slpBalancesAndUtxos = await getSlpBalancesAndUtxos( + const hydratedUtxoDetails = await getHydratedUtxoDetails( BCH, utxos, ); + + const slpBalancesAndUtxos = await getSlpBalancesAndUtxos( + hydratedUtxoDetails, + ); const txHistory = await getTxHistory(BCH, cashAddresses); 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.txHistory = txHistory; setWalletState(newState); // If everything executed correctly, remove apiError setApiError(false); } catch (error) { console.log(`Error in update({wallet, setWalletState})`); 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 if (existingWallet && !existingWallet.Path1899) { console.log(`Wallet does not have Path1899`); 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 Path1899 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/1899'/0'/0/0", }); 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 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 = Path145.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 cashtabapp.com/, their active wallet will be migrated to Path1899 by the getWallet function 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 */ // Check savedWallets for currentlyActiveWallet let walletInSavedWallets = false; let walletUnmigrated = false; for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === currentlyActiveWallet.name) { walletInSavedWallets = true; // Check savedWallets for unmigrated currentlyActiveWallet if (!savedWallets[i].Path1899) { // Case 1, described above console.log( `Case 1: Wallet migration in saved wallets still pending, adding Path1899`, ); savedWallets[i].Path1899 = currentlyActiveWallet.Path1899; walletUnmigrated = true; } } } // Case 1 if (walletUnmigrated) { // 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 if (!walletToActivate.Path1899) { // Case 2, described above console.log(`Case 2: Wallet to activate does not have Path1899`); console.log( `Wallet to activate from SavedWallets does not have Path1899`, ); 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; } } 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 => { const wallet = await getWallet(); setWallet(wallet); }; // Parse for incoming BCH transactions // Only notify if websocket is not connected if ( (ws === null || ws.readyState !== 1) && previousBalances && balances && 'totalBalance' in previousBalances && 'totalBalance' in balances && new BigNumber(balances.totalBalance) .minus(previousBalances.totalBalance) .gt(0) ) { notification.success({ message: 'Transaction received', description: ( You received{' '} {Number( balances.totalBalance - previousBalances.totalBalance, ).toFixed(8)}{' '} BCH! ), duration: 3, }); } // Parse for incoming SLP 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) { notification.success({ message: `${currency.tokenTicker} Transaction received: ${receivedSlpTicker}`, description: ( You received {receivedSlpQty} {receivedSlpName} ), duration: 5, }); } // } 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; notification.success({ message: `SLP Transaction received: ${receivedSlpTicker}`, description: ( You received {receivedSlpQty.toString()}{' '} {receivedSlpName} ), duration: 5, }); } } } } // Update price every 1 min useAsyncTimeout(async () => { fetchBchPrice(); }, 60000); // Update wallet every 10s useAsyncTimeout(async () => { const wallet = await getWallet(); update({ wallet, setWalletState, }).finally(() => { setLoading(false); }); }, 10000); const initializeWebsocket = (cashAddress, slpAddress) => { // console.log(`initializeWebsocket(${cashAddress}, ${slpAddress})`); // This function parses 3 cases // 1: edge case, websocket is in state but not properly connected // > Remove it from state and forget about it, fall back to normal notifications // 2: edge-ish case, websocket is in state and connected but user has changed wallet // > Unsubscribe from old addresses and subscribe to new ones // 3: most common: app is opening, creating websocket with existing addresses // If the websocket is already in state but is not properly connected if (ws !== null && ws.readyState !== 1) { // Forget about it and use conventional notifications // Close ws.close(); // Remove from state setWs(null); } // If the websocket is in state and connected else if (ws !== null) { // console.log(`Websocket already in state`); // console.log(`ws,`, ws); // instead of initializing websocket, unsubscribe from old addresses and subscribe to new ones const previousWsCashAddress = previousWallet.Path145.legacyAddress; const previousWsSlpAddress = previousWallet.Path245.legacyAddress; try { // Unsubscribe from previous addresses ws.send( JSON.stringify({ op: 'addr_unsub', addr: previousWsCashAddress, }), ); console.log( `Unsubscribed from BCH address at ${previousWsCashAddress}`, ); ws.send( JSON.stringify({ op: 'addr_unsub', addr: previousWsSlpAddress, }), ); console.log( `Unsubscribed from SLP address at ${previousWsSlpAddress}`, ); // Subscribe to new addresses ws.send( JSON.stringify({ op: 'addr_sub', addr: cashAddress, }), ); console.log(`Subscribed to BCH address at ${cashAddress}`); // Subscribe to SLP address ws.send( JSON.stringify({ op: 'addr_sub', addr: slpAddress, }), ); console.log(`Subscribed to SLP address at ${slpAddress}`); // Reset onmessage; it was previously set with the old addresses // Note this code is exactly identical to lines 431-490 // TODO put in function ws.onmessage = e => { // TODO handle case where receive multiple messages on one incoming transaction //console.log(`ws msg received`); const incomingTx = JSON.parse(e.data); console.log(incomingTx); let bchSatsReceived = 0; // First, check the inputs // If cashAddress or slpAddress are in the inputs, then this is a sent tx and should be ignored for notifications if ( incomingTx && incomingTx.x && incomingTx.x.inputs && incomingTx.x.out ) { const inputs = incomingTx.x.inputs; // Iterate over inputs and see if this transaction was sent by the active wallet for (let i = 0; i < inputs.length; i += 1) { if ( inputs[i].prev_out.addr === cashAddress || inputs[i].prev_out.addr === slpAddress ) { // console.log(`Found a sending tx, not notifying`); // This is a sent transaction and should be ignored by notification handlers return; } } // Iterate over outputs to determine receiving address const outputs = incomingTx.x.out; for (let i = 0; i < outputs.length; i += 1) { if (outputs[i].addr === cashAddress) { // console.log(`BCH transaction received`); bchSatsReceived += outputs[i].value; // handle } if (outputs[i].addr === slpAddress) { console.log(`SLP transaction received`); //handle // you would want to get the slp info using this endpoint: // https://rest.kingbch.com/v3/slp/txDetails/cb39dd04e07e172a37addfcb1d6e167dc52c01867ba21c9bf8b5acf4dd969a3f // But it does not work for unconfirmed txs // Hold off on slp tx notifications for now } } } // parse for receiving address // if received at cashAddress, parse for BCH amount, notify BCH received // if received at slpAddress, parse for token, notify SLP received // if those checks fail, could be from a 'sent' tx, ignore // Note, when you send an SLP tx, you get SLP change to SLP address and BCH change to BCH address // Also note, when you send an SLP tx, you often have inputs from both BCH and SLP addresses // This causes a sent SLP tx to register 4 times from the websocket // Best way to ignore this is to ignore any incoming utx.x with BCH or SLP address in the inputs // Notification for received BCH if (bchSatsReceived > 0) { notification.success({ message: 'Transaction received', description: ( You received {bchSatsReceived / 1e8} BCH! ), duration: 3, }); } }; } catch (err) { console.log( `Error attempting to configure websocket for new wallet`, ); console.log(err); console.log(`Closing connection`); ws.close(); setWs(null); } } else { // If there is no websocket, create one, subscribe to addresses, and add notifications for incoming BCH transactions let newWs = new WebSocket('wss://ws.blockchain.info/bch/inv'); newWs.onopen = () => { console.log(`Connected to bchWs`); // Subscribe to BCH address newWs.send( JSON.stringify({ op: 'addr_sub', addr: cashAddress, }), ); console.log(`Subscribed to BCH address at ${cashAddress}`); // Subscribe to SLP address newWs.send( JSON.stringify({ op: 'addr_sub', addr: slpAddress, }), ); console.log(`Subscribed to SLP address at ${slpAddress}`); }; newWs.onerror = e => { // close and set to null console.log(`Error in websocket connection for ${newWs}`); console.log(e); setWs(null); }; newWs.onclose = () => { console.log(`Websocket connection closed`); // Unsubscribe on close to prevent double subscribing //{"op":"addr_unsub", "addr":"$bitcoin_address"} newWs.send( JSON.stringify({ op: 'addr_unsub', addr: cashAddress, }), ); console.log(`Unsubscribed from BCH address at ${cashAddress}`); newWs.send( JSON.stringify({ op: 'addr_sub', addr: slpAddress, }), ); console.log(`Unsubscribed from SLP address at ${slpAddress}`); }; newWs.onmessage = e => { // TODO handle case where receive multiple messages on one incoming transaction //console.log(`ws msg received`); const incomingTx = JSON.parse(e.data); console.log(incomingTx); let bchSatsReceived = 0; // First, check the inputs // If cashAddress or slpAddress are in the inputs, then this is a sent tx and should be ignored for notifications if ( incomingTx && incomingTx.x && incomingTx.x.inputs && incomingTx.x.out ) { const inputs = incomingTx.x.inputs; // Iterate over inputs and see if this transaction was sent by the active wallet for (let i = 0; i < inputs.length; i += 1) { if ( inputs[i].prev_out.addr === cashAddress || inputs[i].prev_out.addr === slpAddress ) { // console.log(`Found a sending tx, not notifying`); // This is a sent transaction and should be ignored by notification handlers return; } } // Iterate over outputs to determine receiving address const outputs = incomingTx.x.out; for (let i = 0; i < outputs.length; i += 1) { if (outputs[i].addr === cashAddress) { // console.log(`BCH transaction received`); bchSatsReceived += outputs[i].value; // handle } if (outputs[i].addr === slpAddress) { console.log(`SLP transaction received`); //handle // you would want to get the slp info using this endpoint: // https://rest.kingbch.com/v3/slp/txDetails/cb39dd04e07e172a37addfcb1d6e167dc52c01867ba21c9bf8b5acf4dd969a3f // But it does not work for unconfirmed txs // Hold off on slp tx notifications for now } } } // parse for receiving address // if received at cashAddress, parse for BCH amount, notify BCH received // if received at slpAddress, parse for token, notify SLP received // if those checks fail, could be from a 'sent' tx, ignore // Note, when you send an SLP tx, you get SLP change to SLP address and BCH change to BCH address // Also note, when you send an SLP tx, you often have inputs from both BCH and SLP addresses // This causes a sent SLP tx to register 4 times from the websocket // Best way to ignore this is to ignore any incoming utx.x with BCH or SLP address in the inputs // Notification for received BCH if (bchSatsReceived > 0) { notification.success({ message: 'Transaction received', description: ( You received {bchSatsReceived / 1e8} BCH! ), duration: 3, }); } }; setWs(newWs); } }; const fetchBchPrice = async () => { // Split this variable out in case coingecko changes const cryptoId = currency.coingeckoId; // Keep currency as a variable as eventually it will be a user setting const fiatCode = 'usd'; // 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); const bchPriceInFiat = bchPriceJson[cryptoId][fiatCode]; //console.log(`bchPriceInFiat`, bchPriceInFiat); setFiatPrice(bchPriceInFiat); } catch (err) { console.log(`Error parsing price API response to JSON`); console.log(err); } }; useEffect(() => { handleUpdateWallet(setWallet); fetchBchPrice(); }, []); useEffect(() => { if ( wallet && wallet.Path145 && wallet.Path145.cashAddress && wallet.Path245 && wallet.Path245.cashAddress ) { if (currency.useBlockchainWs) { initializeWebsocket( wallet.Path145.legacyAddress, wallet.Path245.legacyAddress, ); } } }, [wallet]); return { BCH, wallet, fiatPrice, slpBalancesAndUtxos, balances, tokens, txHistory, loading, apiError, getActiveWalletFromLocalForage, getWallet, validateMnemonic, getWalletDetails, getSavedWallets, migrateLegacyWallet, update: async () => update({ wallet: await getWallet(), setLoading, setWalletState, }), createWallet: async importMnemonic => { setLoading(true); const newWallet = await createWallet(importMnemonic); setWallet(newWallet); update({ wallet: newWallet, setWalletState, }).finally(() => setLoading(false)); }, activateWallet: async walletToActivate => { setLoading(true); const newWallet = await activateWallet(walletToActivate); setWallet(newWallet); update({ wallet: newWallet, setWalletState, }).finally(() => setLoading(false)); }, addNewSavedWallet, renameWallet, deleteWallet, }; }; export default useWallet;