diff --git a/cashtab/extension/public/manifest.json b/cashtab/extension/public/manifest.json --- a/cashtab/extension/public/manifest.json +++ b/cashtab/extension/public/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Cashtab", "description": "A browser-integrated eCash wallet from Bitcoin ABC", - "version": "3.24.0", + "version": "3.25.0", "content_scripts": [ { "matches": ["file://*/*", "http://*/*", "https://*/*"], diff --git a/cashtab/package-lock.json b/cashtab/package-lock.json --- a/cashtab/package-lock.json +++ b/cashtab/package-lock.json @@ -1,12 +1,12 @@ { "name": "cashtab", - "version": "2.24.2", + "version": "2.25.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cashtab", - "version": "2.24.2", + "version": "2.25.0", "dependencies": { "@ant-design/icons": "^5.3.0", "@bitgo/utxo-lib": "^9.33.0", diff --git a/cashtab/package.json b/cashtab/package.json --- a/cashtab/package.json +++ b/cashtab/package.json @@ -1,6 +1,6 @@ { "name": "cashtab", - "version": "2.24.2", + "version": "2.25.0", "private": true, "scripts": { "start": "node scripts/start.js", diff --git a/cashtab/src/assets/addcontact.svg b/cashtab/src/assets/addcontact.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/addcontact.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cashtab/src/assets/alias.svg b/cashtab/src/assets/alias.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/alias.svg @@ -0,0 +1,4 @@ + + + + diff --git a/cashtab/src/assets/cashtab-encrypted.svg b/cashtab/src/assets/cashtab-encrypted.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/cashtab-encrypted.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cashtab/src/assets/cashtab-msg.svg b/cashtab/src/assets/cashtab-msg.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/cashtab-msg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/cashtab/src/assets/chat.svg b/cashtab/src/assets/chat.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/chat.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cashtab/src/assets/copypaste.svg b/cashtab/src/assets/copypaste.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/copypaste.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cashtab/src/assets/mint.svg b/cashtab/src/assets/mint.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/mint.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/cashtab/src/assets/paybutton.webp b/cashtab/src/assets/paybutton.webp new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ + + + diff --git a/cashtab/src/assets/reply.svg b/cashtab/src/assets/reply.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/reply.svg @@ -0,0 +1,4 @@ + + + + diff --git a/cashtab/src/assets/styles/theme.js b/cashtab/src/assets/styles/theme.js --- a/cashtab/src/assets/styles/theme.js +++ b/cashtab/src/assets/styles/theme.js @@ -17,6 +17,7 @@ encryptionRed: '#DC143C', genesisGreen: '#00e781', aliasGreen: '#0FFF50', + separator: 'rgba(255, 255, 255, 0.12)', receivedMessage: 'rgba(0,171,231,0.2)', sentMessage: 'rgba(255, 255, 255, 0.1)', lightWhite: 'rgba(255,255,255,0.4)', diff --git a/cashtab/src/assets/swap.svg b/cashtab/src/assets/swap.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/swap.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cashtab/src/assets/tokenburn.svg b/cashtab/src/assets/tokenburn.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/tokenburn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/cashtab/src/assets/unknown.svg b/cashtab/src/assets/unknown.svg new file mode 100644 --- /dev/null +++ b/cashtab/src/assets/unknown.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cashtab/src/chronik/__tests__/index.test.js b/cashtab/src/chronik/__tests__/index.test.js --- a/cashtab/src/chronik/__tests__/index.test.js +++ b/cashtab/src/chronik/__tests__/index.test.js @@ -118,10 +118,9 @@ describe('Parses supported tx types', () => { const { expectedReturns } = vectors.parseTx; expectedReturns.forEach(expectedReturn => { - const { description, tx, wallet, cachedTokens, parsed } = - expectedReturn; + const { description, tx, hashes, parsed } = expectedReturn; it(`parseTx: ${description}`, () => { - expect(parseTx(tx, wallet, cachedTokens)).toStrictEqual(parsed); + expect(parseTx(tx, hashes)).toStrictEqual(parsed); }); }); }); diff --git a/cashtab/src/chronik/fixtures/mocks.js b/cashtab/src/chronik/fixtures/mocks.js --- a/cashtab/src/chronik/fixtures/mocks.js +++ b/cashtab/src/chronik/fixtures/mocks.js @@ -7272,17 +7272,13 @@ }, }, parsed: { - incoming: true, - xecAmount: 625008.97, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - opReturnMessage: '', - replyAddress: 'N/A', + satoshisSent: 62500897, + stackArray: [], + xecTxType: 'Staking Reward', + recipients: [ + 'ecash:qr689ree3wukyetgqv6xld9vghthvpq69cg04xjp57', + 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + ], }, }; @@ -7338,17 +7334,10 @@ }, }, parsed: { - incoming: true, - xecAmount: 42, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - opReturnMessage: '', - replyAddress: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6', + satoshisSent: 4200, + stackArray: [], + xecTxType: 'Received', + recipients: ['ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6'], }, }; @@ -7404,17 +7393,10 @@ }, }, parsed: { - incoming: false, - xecAmount: 222, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - opReturnMessage: '', - replyAddress: 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', + satoshisSent: 22200, + stackArray: [], + xecTxType: 'Sent', + recipients: ['ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6'], }, }; @@ -7474,17 +7456,15 @@ }, }, parsed: { - incoming: false, - xecAmount: 5.55, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - aliasFlag: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - opReturnMessage: 'bug2', - replyAddress: 'ecash:qrwpz3mx89y0ph8mqrxyqlk6gxcjzuf66vc4ajscad', + satoshisSent: 555, + stackArray: [ + '2e786563', + '00', + '62756732', + '00dc1147663948f0dcfb00cc407eda41b121713ad3', + ], + xecTxType: 'Sent', + recipients: ['ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07'], }, }; @@ -7619,37 +7599,17 @@ }, }, parsed: { - incoming: true, - xecAmount: 5.46, - isEtokenTx: true, - isTokenBurn: false, - tokenEntries: [ - { - tokenId: - '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'SEND', - isInvalid: false, - burnSummary: '', - failedColorings: [], - actualBurnAmount: '0', - intentionalBurn: '0', - burnsMintBatons: false, - }, - ], - assumedTokenDecimals: false, - etokenAmount: '12', - airdropFlag: false, - airdropTokenId: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - opReturnMessage: '', - replyAddress: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6', + satoshisSent: 546, + stackArray: [ + '534c5000', + '01', + '53454e44', + '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + '000000000000000c', + '00000000000000e4', + ], + xecTxType: 'Received', + recipients: ['ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6'], }, }; @@ -7784,37 +7744,17 @@ }, }, parsed: { - incoming: false, - xecAmount: 5.46, - isEtokenTx: true, - isTokenBurn: false, - tokenEntries: [ - { - tokenId: - '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'SEND', - isInvalid: false, - burnSummary: '', - failedColorings: [], - actualBurnAmount: '0', - intentionalBurn: '0', - burnsMintBatons: false, - }, - ], - assumedTokenDecimals: false, - etokenAmount: '17', - airdropFlag: false, - airdropTokenId: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - opReturnMessage: '', - replyAddress: 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', + satoshisSent: 546, + stackArray: [ + '534c5000', + '01', + '53454e44', + '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + '0000000000000011', + '0000000000000034', + ], + xecTxType: 'Sent', + recipients: ['ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6'], }, }; @@ -7891,37 +7831,21 @@ }, }, parsed: { - incoming: false, - xecAmount: 0, - isEtokenTx: true, - isTokenBurn: false, - etokenAmount: '777.7777777', - tokenEntries: [ - { - tokenId: - 'cf601c56b58bc05a39a95374a4a865f0a8b56544ea937b30fb46315441717c50', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'GENESIS', - isInvalid: false, - burnSummary: '', - failedColorings: [], - actualBurnAmount: '0', - intentionalBurn: '0', - burnsMintBatons: false, - }, - ], - assumedTokenDecimals: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + satoshisSent: 546, + stackArray: [ + '534c5000', + '01', + '47454e45534953', + '554454', + '55706461746554657374', + '68747470733a2f2f636173687461622e636f6d2f', + '', + '07', + '', + '00000001cf977871', + ], + xecTxType: 'Sent', + recipients: [], }, }; @@ -8048,37 +7972,17 @@ }, }, parsed: { - incoming: true, - xecAmount: 5.46, - isEtokenTx: true, - isTokenBurn: false, - etokenAmount: '0.123456789', - tokenEntries: [ - { - tokenId: - 'acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'SEND', - isInvalid: false, - burnSummary: '', - failedColorings: [], - actualBurnAmount: '0', - intentionalBurn: '0', - burnsMintBatons: false, - }, - ], - assumedTokenDecimals: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6', + satoshisSent: 546, + stackArray: [ + '534c5000', + '01', + '53454e44', + 'acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', + '00000000075bcd15', + '000000024554499f', + ], + xecTxType: 'Received', + recipients: ['ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6'], }, }; @@ -8600,504 +8504,893 @@ }, }, parsed: { - incoming: true, - xecAmount: 5.69, - isEtokenTx: false, - aliasFlag: false, - airdropFlag: true, - airdropTokenId: + satoshisSent: 569, + stackArray: [ + '64726f70', 'bdb3b4215ca0622e0c4c07655522c376eaa891838a82f0217fa453bb0595a37c', - opReturnMessage: 'evc token service holders air dropπŸ₯‡πŸŒπŸ₯‡β€πŸ‘ŒπŸ›¬πŸ›¬πŸ—πŸ€΄', - isCashtabMessage: true, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qp36z7k8xt7k4l5xnxeypg5mfqeyvvyduu04m37fwd', + '00746162', + '65766320746f6b656e207365727669636520686f6c64657273206169722064726f70f09fa587f09f8c90f09fa587e29da4f09f918cf09f9bacf09f9bacf09f8d97f09fa4b4', + ], + xecTxType: 'Received', + recipients: [ + 'ecash:qqp49ckzgmar3ljh7egymnlk9z32hpwf4qf8m8l9gc', + 'ecash:qp7j4nzkraqhhuext4r9l0tkk7tke566m5phzhkktx', + 'ecash:qpu29ydpjdr3vxjn9ucu462afykv27t9aq4t0vgc3v', + 'ecash:qpuvcexsnsk9tr3v0ud6733lfc4xy3j4nqq3chf5l9', + 'ecash:qpc4xc6q5kknr8ey4epn6l92g36a6606as2zhlv5c3', + 'ecash:qpjfhctcr7tzc485wfeatr33gw0mg54e9y4kuan3jl', + 'ecash:qzlreeyeuv0taqx842lkwwkds4xgj6wacssxeqqpz4', + 'ecash:qr5g7wfc83xjv3qs7vxjk2xd4em4cel23cmmek9thu', + 'ecash:qp0me6v4nnnmwy3exyu2austqy7452qzuc2j22e0nt', + 'ecash:qpg0xh3cv8tqj300e54statzalc562xmzqwuzjntyl', + 'ecash:qzrxakyh8ezy68m9x043skx29p9dtzdury4xkq4azv', + 'ecash:qqyy3mss5vmthgnu0m5sm39pcfq8z799kun7jjcf72', + 'ecash:qzt4pnwah9mts3nxdznnkkxq5xhm6m6dhq77227t0f', + 'ecash:qz8wz5dlputr0nwjux6pa5kdx2cd7z5ny5x4tw2zpk', + 'ecash:qzl8jth497mtckku404cadsylwanm3rfxslzu6ufgg', + 'ecash:qq4djmjx07f4f7rwpsg6el0r2yv55xpaequ4h6axa6', + 'ecash:qzhay3c0yep99uf4n4acpyll7n7azgx9lykz945tm5', + 'ecash:qz9gaysz88a4e3j8s4wp6c6tpwlycjm8qy0t5wv0r6', + 'ecash:qqf0sn65lt2xj5ep7cwrz0fwx2s237qgvsntr84uqt', + 'ecash:qzzzk9f2pw75v3a04m8wezn2l25sv688cuy23pmxch', + 'ecash:qrlfw84jjcx7ln5n2q79vsw4f64d92m2q5z7v4d2da', + 'ecash:qp59aqjevxm8g4h5gr92424sl9xtxd2twgl620v7ph', + 'ecash:qpmtg3r6xct7jxxsxfsn20shnfvrlpwjccn0x53qp0', + 'ecash:pqvtknma3zqur5290se6dtuwtyml0amk4q7apqmnwx', + 'ecash:qzekdmmurl75aazj6uj4vc68yrxgws0pmsgztm4atw', + 'ecash:qr67stwqz9cdnxskh7tppk588h68lq420gvwpush28', + 'ecash:qqhddqwu2ssa6js99aymmf26ns69lvp9uqhgstr4vr', + 'ecash:qzu863zm9ka6vhz6twmejkd5fsj9jdgclqcwvvhlkq', + 'ecash:pp7erhrc87cutdljf0hajthderdtl29t0cckdef6dm', + 'ecash:prmj9lywy0zuydnr4ge88az9k7ztyga2k5zqm3zchg', + 'ecash:qz2qssp3rjlxqyl9ntlh98lurkgzl46dry8x8j7ftm', + 'ecash:qrfef5yyvpaua9l6fenpkmev05hjxlyfac99kea3re', + 'ecash:qpcwrv6v28x4xxw9ef2d49u2vs3xqhnt8c7qtax842', + 'ecash:qpqwavpkm8ttcuwdvku3addmlfwcpzq9egtnc3rwvz', + 'ecash:qpx4tamfec20638zkc6spk2sz6pc5hgnp5yhp72z28', + 'ecash:qzsha6zk9m0f3hlfe5q007zdwnzvn3vwuuzel2lfzv', + 'ecash:qzsnlsmy95089yltfw030mqmdakhaf92avqgmfwnsn', + 'ecash:qp3wjpa3tjlj042z2wv7hahsldgwhwy0rquas9fmzn', + 'ecash:qzr2jy0x2afmx7thg3yzxrn73aaw4w86tcv87dlc9m', + 'ecash:qr5nvnzhwpu0zmhzkfljc4c2fezsm4fw0gxf008tgt', + 'ecash:qrkez7h6j6pnc8l2v78zxd6v24lds0lkluen22kx9g', + 'ecash:qzpv7j9wlnvqquh0y8j2v80w3skhp59ukvfejazgkn', + 'ecash:qpzwswytm4jvraneq5neqehsg33c6rskdy0hunmn7m', + 'ecash:qrtzu6z98d6e8pskkafsnsecr52dv89e5sqggy7w5p', + 'ecash:qqjmr545vy9kmmkcu0f2cah57yfgsvfxus3qv9h336', + 'ecash:qptyydu4msh6sharjvwdl8jc7nuxv8ptysp2d46z2p', + 'ecash:qrsrm989nwespwt94s355f6trn6pc09d6uvu4zsdp8', + 'ecash:qq0q665w7ty2pakw4n5x2czea2wmavgmmgz0n7r63a', + 'ecash:qrmv6mh3h4ad6v20mxc3tsadph88s3ynpshjfcac55', + 'ecash:qzy0k220s7c0qklkahwp66lau2ar4paum549mlx7vn', + 'ecash:qzs4fuqzyarkajt5rfqka94kjemlmh6tr5sqsrh0tz', + 'ecash:qqmz5dmn7459ez0yhqqwf38ejfwm9mqmtswnrhjrcl', + 'ecash:qpnhp9v93qzf50ees28pmhzh70whwgn6zyzefhg5vl', + 'ecash:qzcrzd696hmus5xfdqkzwydk598jmwf8dvcneehcza', + 'ecash:qrl89x4yqaulsg4gcjvg7jdpzhy240qvcuwluwhshd', + 'ecash:qrkw7qql8sfhezq0s2xcg0m4fgyzadfedv6jdz8zgs', + 'ecash:qp370xkalsadx0gyecry4hsz60yv4j52l5dcy6vscq', + 'ecash:qzy6dks7mpkgjelsx6g6mxhcmy7xykgnwvm2ka7y4n', + 'ecash:qz06z7pkpj43wru5yv3r5kckv9cl2n2mcyvh00d7l3', + 'ecash:qz7r06eys9aggs4j8t56qmxyqhy0mu08cspyq02pq4', + 'ecash:qrnc6vzxxfyfhgjqk2vcdln2l5evw74pvv9ruy8fga', + 'ecash:qzvnu6lw7a85a5xrleg6lz27gakwxlpk9v55jr2p46', + 'ecash:qzugyr9xh88tpa2xu9pdmkzh5jt5fqm3ngjykg3vgy', + 'ecash:qr9f38l560030ljdcm4nxz6xn0tdt4ypfcxrgleaav', + 'ecash:qzkjnnwwygmlw854lmj4ruzyyhmskljvn52clpf7gl', + 'ecash:qq840pewqms4ty7g52y0edmpky722uwh3qvx936rje', + 'ecash:qq4fd9zdqecq3q4mmxz8v8vunepptukh3czav3gjyt', + 'ecash:qq0rwc6wv6f7y2yqrsv5c3tsr4y6r5fw9squdmajds', + 'ecash:qp36z7k8xt7k4l5xnxeypg5mfqeyvvyduu04m37fwd', + ], }, }; - -export const outgoingEncryptedMsg = { +export const onSpecAirdropTxNoMsg = { tx: { - txid: '7ac10096c8a7b32fe338dc938bcf2e1341b99f841687e690d88241107ce4b84b', + txid: '298c3d1a5bd00bd86d92d48ec5695c25a0a86093964d9f53eb19b46dc472b9f5', version: 2, inputs: [ { prevOut: { - txid: '45411aa786288b679d1c1874f7b126d5ea0c83380304950d364b5b8279a460de', - outIdx: 1, + txid: '1238b76f12c0a4e2c54f5f80951464396f40685256f0ffc3e30a450995e5da43', + outIdx: 2, }, inputScript: - '483045022100d4a93c615a7af48f422c273a530ac7f2b78d31a2d4515f11b2f416fce4f4f380022075c22c73190a7de805f219ca8d294777440b558551fea6b59c6c84ec529b16f94121038c4c26730d97cdeb18e69dff6c47cebb23e6f305c950923cd6110f35ab9006d0', - value: 48445, + '483045022100a6886a347a977b31fb3cf4a0b0ef85e58bd60d7af9db27d4d260f71c9b5f22c30220436ceaca789bc8ab631633434eb0b64b93ae6ebeac94d3ddbd12d3916a57fc8441210343b0a63fb80795016f064481f0380836adf7cde6ad32a662ddf551876b303a93', + value: 16194930, sequenceNo: 4294967295, outputScript: - '76a914ee6dc9d40f95d8e106a63385c6fa882991b9e84e88ac', + '76a9142a96944d06700882bbd984761d9c9e4215f2d78e88ac', }, ], outputs: [ { value: 0, outputScript: - '6a04657461624ca1040f3cc3bc507126c239cde840befd974bdac054f9b9f2bfd4ff32b5f59ca554c4f3fb2d11d30eae3e5d3f61625ff7812ba14f8c901c30ee7e03dea57681a8f7ab8c64d42ce505921b4d67507452537cbe7525281714857c75d7a441b65030b7ea646b59ed0c34adc9f739661620cf7678963db3cac78afd7f49ad0d63aad404b07730255ded82ea3a939c63ee040ae9fac9336bb8d84d7b3380665ffa514a45f4', + '6a0464726f7020fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa4cad415454454e54494f4e204752554d50592050454f504c452120f09f98be20596f752063616e206e6f77206465706f736974202447525020746f207468652065546f6b656e20626f7420617420742e6d652f6543617368506c617920746f20746f7020757020796f757220436173696e6f20437265646974732120316d2024475250203d2031204372656469742e20506c617920436173696e6f2067616d657320616e642077696e205845432120', }, { - value: 1200, + value: 2105, outputScript: - '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', - spentBy: { - txid: 'aca8ec27a6fc4dc45b1c2e2a6175e84d81ffdd54c7f97711654a100ade4e80bc', - outIdx: 0, - }, + '76a9145561e7d054bb4d81d862fdc674525c2dc337ac6d88ac', }, { - value: 46790, + value: 2105, outputScript: - '76a914ee6dc9d40f95d8e106a63385c6fa882991b9e84e88ac', - spentBy: { - txid: '610f8a6f8e7266af18feda7a5672d379314eb05cb7ce6690a1f1d5bff1051dad', - outIdx: 1, - }, + '76a91417b83cbad4814a5c6400e418ec69f29963a2805888ac', }, - ], - lockTime: 0, - timeFirstSeen: 0, - size: 404, - isCoinbase: false, - tokenEntries: [], - tokenFailedParsings: [], - tokenStatus: 'TOKEN_STATUS_NON_TOKEN', - block: { - height: 760192, - hash: '0000000000000000085f5e0372ca7d42c37e5f93db753440331b3cfc1be23052', - timestamp: 1664910499, - }, - }, - parsed: { - incoming: false, - xecAmount: 12, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - opReturnMessage: 'Encrypted Cashtab Msg', - isCashtabMessage: true, - isEcashChatMessage: false, - isEncryptedMessage: true, - replyAddress: 'ecash:qrhxmjw5p72a3cgx5cect3h63q5erw0gfcvjnyv7xt', - }, -}; - -export const incomingEncryptedMsg = { - tx: { - txid: '66974f4a22ca1a4aa36c932b4effafcb9dd8a32b8766dfc7644ba5922252c4c6', - version: 2, - inputs: [ { - prevOut: { - txid: 'fec829a1ff34a9f84058cdd8bf795c114a8fcb3bcc6c3ca9ea8b9ae68420dd9a', - outIdx: 1, - }, - inputScript: - '483045022100e9fce8984a9f0cb76642c6df63a83150aa31d1071b62debe89ecadd4d45e727e02205a87fcaad0dd188860db8053caf7d6a21ed7807dbcd1560c251f9a91a4f36815412103318d0e1109f32debc66952d0e3ec21b1cf96575ea4c2a97a6535628f7f8b10e6', - value: 36207, - sequenceNo: 4294967295, + value: 2105, outputScript: - '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + '76a9142e153c4fc63dcabf0e8949b20ddab2c3df7704ed88ac', }, - ], - outputs: [ { - value: 0, + value: 2105, outputScript: - '6a04657461624c9104eaa5cbe6e13db7d91f35dca5d270c944a9a3e8c7738c56d12069312f589c7f193e67ea3d2f6d1f300f404c33c19e48dc3ac35145c8152624b7a8e22278e9133862425da2cc44f7297c8618ffa78dd09054a4a5490afd2b62139f19fa7b8516cbae692488fa50e79101d55e7582b3a662c3a5cc737044ef392f8c1fde63b8385886aed37d1b68e887284262f298fe74c0', + '76a9149966d31280b53f1c2b85f975918eb3023b281f8688ac', }, { - value: 1100, + value: 2105, outputScript: - '76a914ee6dc9d40f95d8e106a63385c6fa882991b9e84e88ac', - spentBy: { - txid: '610f8a6f8e7266af18feda7a5672d379314eb05cb7ce6690a1f1d5bff1051dad', - outIdx: 0, - }, + '76a914bfd3a8b912a7988809090de56651d47451528ba188ac', }, { - value: 34652, + value: 2105, outputScript: - '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', - spentBy: { - txid: '3efa1835682ecc60d2476f1c608eb6f5ae9040610193111a2c312453cd7db4ef', - outIdx: 0, - }, + '76a9147ff46e0807b0d3a5797dd65beae6cfcc2d01e56e88ac', }, - ], - lockTime: 0, - timeFirstSeen: 0, - size: 388, - isCoinbase: false, - tokenEntries: [], - tokenFailedParsings: [], - tokenStatus: 'TOKEN_STATUS_NON_TOKEN', - block: { - height: 760192, - hash: '0000000000000000085f5e0372ca7d42c37e5f93db753440331b3cfc1be23052', - timestamp: 1664910499, - }, - }, - parsed: { - incoming: true, - xecAmount: 11, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - opReturnMessage: 'Encrypted Cashtab Msg', - isCashtabMessage: true, - isEcashChatMessage: false, - isEncryptedMessage: true, - replyAddress: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6', - }, -}; - -export const tokenBurn = { - tx: { - txid: '312553668f596bfd61287aec1b7f0f035afb5ddadf40b6f9d1ffcec5b7d4b684', - version: 2, - inputs: [ { - prevOut: { - txid: '842dd09e723d664d7647bc49f911c88b60f0450e646fedb461f319dadb867934', - outIdx: 0, - }, - inputScript: - '473044022025c68cf0ab9c1a4d6b35b2b58f7e397722f469412841eb09d38d1973dc5ef7120220712e1f3c8740fff2af75c1062a773eef167550ee008deaef9089537cd17c35f0412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', - value: 2300, - sequenceNo: 4294967295, + value: 2105, outputScript: - '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + '76a914585ecb807269977bc21a1a2cd5d7c4ff1150e94988ac', }, { - prevOut: { - txid: '1efe359a0bfa83c409433c487b025fb446a3a9bfa51a718c8dd9a56401656e33', - outIdx: 2, - }, - inputScript: - '47304402206a2f53497eb734ea94ca158951aa005f6569c184675a497d33d061b78c66c25b02201f826fa71be5943ce63740d92a278123974e44846c3766c5cb58ef5ad307ba36412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', - value: 546, - sequenceNo: 4294967295, - token: { - tokenId: - '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - amount: '2', - isMintBaton: false, - entryIdx: 0, - }, + value: 2105, outputScript: - '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + '76a9140db07d6b795f5fe5f47e53aab25aac078d229f8188ac', }, { - prevOut: { - txid: '49f825370128056333af945eb4f4d9712171c9e88954deb189ca6f479564f2ee', - outIdx: 2, - }, - inputScript: - '483045022100efa3c767b749abb2dc958932348e2b19b845964e581c9f6de706cd43dac3f087022059afad6ff3c1e49cc0320499381e78eab922f18b00e0409228ad417e0220bf5d412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', - value: 546, - sequenceNo: 4294967295, - token: { - tokenId: - '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - amount: '999875', - isMintBaton: false, - entryIdx: 0, - }, + value: 2105, outputScript: - '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + '76a9143f23e265a57078ac8e675f78bd552a95943ca7e888ac', }, - ], - outputs: [ { - value: 0, + value: 2105, outputScript: - '6a04534c500001010453454e44204db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c8750800000000000f41b9', + '76a914a33b3460d43b9e27a165185b2863b1f64d418d8288ac', }, { - value: 546, + value: 2105, outputScript: - '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', - token: { - tokenId: - '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - amount: '999865', - isMintBaton: false, - entryIdx: 0, - }, - spentBy: { - txid: '657646f7a4e7237fca4ed8231c27d95afc8086f678244d5560be2230d920ff70', - outIdx: 1, - }, + '76a914e006de0508dcbf24ae0455ca3b5665ed0545553c88ac', }, - ], - lockTime: 0, - timeFirstSeen: 0, - size: 550, - isCoinbase: false, - tokenEntries: [ { - tokenId: - '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'SEND', - isInvalid: false, - burnSummary: 'Unexpected burn: Burns 12 base tokens', - failedColorings: [], - actualBurnAmount: '12', - intentionalBurn: '0', - burnsMintBatons: false, + value: 2105, + outputScript: + '76a9145f1ec6dd2d02c1fa32b184818be5611ae674df0388ac', + }, + { + value: 2105, + outputScript: + '76a914445c0c740419357dd03c93a351c69eedf433be4688ac', + }, + { + value: 2105, + outputScript: + '76a914eafcfdecd98cde993e3be01a9ad1158fd3eb773988ac', + }, + { + value: 2105, + outputScript: + '76a914023a7087ee9bfabc77548fc5f0a359ae9bacf7bc88ac', + }, + { + value: 2105, + outputScript: + '76a914eded3588169234400f7556a40baf808e1ec8ebf588ac', + }, + { + value: 2105, + outputScript: + '76a9141a33684209d978e8bc143c6fcdb7f56e3243dcee88ac', + }, + { + value: 2105, + outputScript: + '76a914486cbf0bfba3b7d0aae10bec7f0d4226e6e10f9688ac', + }, + { + value: 2105, + outputScript: + '76a91480c72defab2c99cf19341cc0e2992c659d42198788ac', + spentBy: { + txid: '46eed31f3d61c5a0a7023c4626c061afc158de9d3855ab304abefd7bb4f7de0d', + outIdx: 109, + }, + }, + { + value: 2105, + outputScript: + '76a914a15db8a24f9b3740383927a1d787ba77b34b63a888ac', + }, + { + value: 2105, + outputScript: + '76a91423a1340dbbe6dedf1cd31cdf11f85b3442cfd82888ac', + }, + { + value: 2105, + outputScript: + '76a9141e464c8d283976ddc13fa6756736f8f3a0069f7888ac', + }, + { + value: 2105, + outputScript: + '76a914f2d85c4f3fb78c1d9727dea73690c72815756e2b88ac', + }, + { + value: 2105, + outputScript: + '76a914d8723ad3becc44356267e8d0313692c493fe2bdf88ac', + }, + { + value: 2105, + outputScript: + '76a914161cae938ec121bd9970304766865991fe80a63088ac', + }, + { + value: 2105, + outputScript: + '76a91490ec469ca54ce9616282dea980a39f0e4b9a6ab988ac', + }, + { + value: 2105, + outputScript: + '76a914b386b1f59b5f03b45471df214d47f7ab5d48003088ac', + }, + { + value: 2105, + outputScript: + '76a914d2ea9ba1a091c2adf0116da4d2c3ddc3cf7124a988ac', + }, + { + value: 2105, + outputScript: + '76a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac', + }, + { + value: 2105, + outputScript: + '76a91451379ab611287658c9e1c0f98f0929addd5a2f1d88ac', + }, + { + value: 2105, + outputScript: + '76a91432fc3341b83f902a360cbbf91a08ea99c293733d88ac', + }, + { + value: 2105, + outputScript: + '76a91459f93839ba24abd6996b75a39486691dd40660be88ac', + }, + { + value: 2105, + outputScript: + '76a91473ef17c5b9f551eae3f3b4fadf61f93cae5e6aea88ac', + }, + { + value: 2105, + outputScript: + '76a91469003998c2c32ac81951b88416a9a15df3a1992988ac', + }, + { + value: 2105, + outputScript: + '76a9145dd0411fa601ab82fcd68894c95934619a49920688ac', + }, + { + value: 2105, + outputScript: + '76a914534c4407eeea7e4b8c3ed7dae5cb4a2539beed9988ac', + }, + { + value: 2105, + outputScript: + '76a91447f86f44721c8d0bac263602717fc10b0da49b9f88ac', + }, + { + value: 2105, + outputScript: + '76a91485149cd55457401ad4645c54b86caa0ce0d4f05f88ac', + }, + { + value: 2105, + outputScript: + '76a914f0af3a1411ed4989bf5c44641c3a86d473afe45188ac', + }, + { + value: 2105, + outputScript: + '76a914973d953e15d62383b24ebe3d73d01e7b83bd989788ac', + }, + { + value: 2105, + outputScript: + '76a914dadf34cde9c774fdd6340cd2916a9b9c5d57cf4388ac', + }, + { + value: 2105, + outputScript: + '76a9144de2fb39f09d14492f4d40e0fb670a42af505c6b88ac', + spentBy: { + txid: '11a58a92afc39a6d7bd413a11864d0d34f21ab72b63028293d1004ff76e74950', + outIdx: 0, + }, + }, + { + value: 2105, + outputScript: + '76a914fd7f54f496cadb0b6d3cc206ee098bab29bd5bbf88ac', + spentBy: { + txid: '51518eb20ca45eaa07925e6d502da8b5be5ad411272863be9a2280e46d6505f7', + outIdx: 11, + }, + }, + { + value: 2105, + outputScript: + '76a914b55d27e509500af85243622343ca9e3d54a0438a88ac', + }, + { + value: 2105, + outputScript: + '76a914547abbaaa1c5e92ecde551c1bfdb9a2e5454b83088ac', + }, + { + value: 2105, + outputScript: + '76a9140f69a9314698156aee8bdb96a36f1e08f1ba168d88ac', + }, + { + value: 2105, + outputScript: + '76a91468506abb4ce69c0e596c80bebe456f3be7f904fd88ac', + spentBy: { + txid: '39ef9f76d052d4b3fa5f4aa19b5597b1b55ab71e361c665eef371a501a1282be', + outIdx: 9, + }, + }, + { + value: 2105, + outputScript: + '76a91447ab6772a47d55b7649b83f105fd5cdc3eaa22a988ac', + }, + { + value: 2105, + outputScript: + '76a91463a17ac732fd6afe8699b240a29b483246308de788ac', + }, + { + value: 2105, + outputScript: + '76a914b74cc1418fad22fe0eb0bef57082d9836a29340c88ac', + }, + { + value: 2105, + outputScript: + '76a914e33f7fa6b1c03d68a28758c1ef3a5fa7322cafbb88ac', + }, + { + value: 2105, + outputScript: + '76a914a30153ad73ba57b6f37c210435e407bb7a368a5d88ac', + }, + { + value: 2105, + outputScript: + '76a9141404dc7b54ab7768837729e2efe052105a4c405988ac', + spentBy: { + txid: '6ab45eb0770ca387bcd76e3ffc0439958dc2bb7b87437234c722a3c615ca2071', + outIdx: 0, + }, + }, + { + value: 2105, + outputScript: + '76a91496cdae0c820426ae831216d629383dda7ee5adab88ac', + }, + { + value: 2105, + outputScript: + '76a91412c4c82aac6896d96ede38eb916b5819a46c803a88ac', + }, + { + value: 2105, + outputScript: + '76a9147bdf4e819215ccfe937a633ae28ae2e9d3aadc0688ac', + }, + { + value: 2105, + outputScript: + '76a9146e2b68c87b86ed79b86a09c62c4762d7e431bcca88ac', + spentBy: { + txid: 'c95c7f6f4baa7d91fd3aa24f9b73b4e04ef840ac970ae82e2d386f81eeec2cf0', + outIdx: 22, + }, + }, + { + value: 2105, + outputScript: + '76a914e7a5f062e50a35d639fc1773738839119e61475d88ac', + }, + { + value: 2105, + outputScript: + '76a914a34960963da7e02e1f0357325985475bda969def88ac', + }, + { + value: 2105, + outputScript: + '76a9149d8689dc0813da4f520225eebb8b80c8352ec4a588ac', + }, + { + value: 2105, + outputScript: + '76a914bd4cdb9bc9dbe21e2b9bdd3395be350d8abbe16d88ac', + }, + { + value: 2105, + outputScript: + '76a9142d755595516b0f625c51d223bc84a5adfc77b20688ac', + }, + { + value: 2105, + outputScript: + '76a914dd01dbc55b0fe9e33ceb700b4c4452010bdb5a1688ac', + }, + { + value: 2105, + outputScript: + '76a9140839c285a8d5b52934c28d4a45a1835dd45f0a5388ac', + spentBy: { + txid: 'cd68979654b1ecee37a33c321b6cb7f2966be6af02232855f46c2aef231463cb', + outIdx: 1, + }, + }, + { + value: 2105, + outputScript: + '76a9143dd8ebcdf0e4d65712a723f2235675316687716388ac', + spentBy: { + txid: 'f8123bf1175047b9d5ebd5d7cacbb378aaa8f58a55ed400893f192f28606a0d3', + outIdx: 3, + }, + }, + { + value: 2105, + outputScript: + '76a914974a7bb26ac2f62bf60a675f5f0024a689c03d7d88ac', + }, + { + value: 2105, + outputScript: + '76a914f5f740bc76e56b77bcab8b4d7f888167f416fc6888ac', + spentBy: { + txid: '27f2d0454f78b90be92eea7d557486ebc07d7ea1004fa7dbc0e7f89e835a4c6e', + outIdx: 3, + }, + }, + { + value: 2105, + outputScript: + '76a914dceb306a73582e52c43025f7eed5827a6d9e92e088ac', + }, + { + value: 2105, + outputScript: + '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', + spentBy: { + txid: 'c62c16c68df7d69d5d1524ac250e30473dacdbf13c131fb1978911674f045665', + outIdx: 17, + }, + }, + { + value: 2105, + outputScript: + '76a9140620a7df2e0637bc8d3dfa663c979c15a671dfe488ac', + }, + { + value: 2105, + outputScript: + '76a9147847fe7070bec8567b3e810f543f2f80cc3e03be88ac', + }, + { + value: 2105, + outputScript: + '76a914837604effd470faaba3e044e0a7c4e6a8a7ee8c688ac', + }, + { + value: 2105, + outputScript: + '76a9142def2114338f0be9a26956378efce60e17b580b388ac', + }, + { + value: 2105, + outputScript: + '76a91482b15f681a94fe9f6ac29ddee214d3dd88f55bfc88ac', + spentBy: { + txid: '920853c238299614bc03270839f1b815c9763385485e04be18a861039c07b606', + outIdx: 14, + }, + }, + { + value: 2105, + outputScript: + '76a91478f43ee6b1e577329c0fc9cb47f7435954eae81f88ac', + }, + { + value: 2105, + outputScript: + '76a914c1a7d12dddc6a3072df09cf5e0a00ece198cc8c188ac', + }, + { + value: 2105, + outputScript: + '76a914bccecb7e3e5d3fccbee3211494ad3214f91cc74f88ac', + }, + { + value: 2105, + outputScript: + '76a914fbc9461beec0d783052c20c994ffb44e46041d5188ac', + }, + { + value: 2105, + outputScript: + '76a914a94b8176d28cb5b5c301f10bb45bdb3d6e0c277d88ac', + spentBy: { + txid: 'b2c0183a724aa141568e9c116b684eb94e8be326c92cf588d83296916974017f', + outIdx: 1, + }, + }, + { + value: 2105, + outputScript: + '76a914993e6beef74f4ed0c3fe51af895e476ce37c362b88ac', + }, + { + value: 2105, + outputScript: + '76a9142ec5281864fc989dab543b054631c9703809689e88ac', + spentBy: { + txid: '962f5149fbca6c739886cb839901b0de5926119430ce268b7aa1be0c073ad84c', + outIdx: 6, + }, + }, + { + value: 2105, + outputScript: + '76a914581bd5bc835cc788bd90a4f6f0c9c21eb173572e88ac', + }, + { + value: 2105, + outputScript: + '76a91428cabb69be3e20707574d7a0ddc65a801b6ae59988ac', + }, + { + value: 2105, + outputScript: + '76a914b8b3c22d82784c27e0224fd8a8ff549a67e955a388ac', + }, + { + value: 2105, + outputScript: + '76a914b8af3f36894ee7e6563c672714f9eb47cc83a9e188ac', + }, + { + value: 2105, + outputScript: + '76a914fa49f98fb25e8b84ce210d06f052aed88c2c4f9888ac', + }, + { + value: 2105, + outputScript: + '76a9146134463df4436bf8c662b64917610f63fa5d89ef88ac', + spentBy: { + txid: 'd8a9729473589d3c30d26e672c2c49e1a58fc380765ddbcde8628ab93293af11', + outIdx: 3, + }, + }, + { + value: 2105, + outputScript: + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + spentBy: { + txid: '3c844ed9f76207027a47dd2170a590a1f8d8a8ff9b797da4f050ad6394adf52a', + outIdx: 1, + }, + }, + { + value: 2105, + outputScript: + '76a914b3f80c88220f138201702a4d0c033b248059fcdc88ac', + spentBy: { + txid: '7d5cf7814e3225587e522e03da0589b806de0498a779e8b0d1cb273c9f257b87', + outIdx: 0, + }, + }, + { + value: 2105, + outputScript: + '76a914ad7eb2c8b88fa2e3f5158b398a49bf277401984e88ac', + spentBy: { + txid: '50974e99e87dec3b575497b9592a89d9ae0f2dc129f26d567582e4d0aaf27741', + outIdx: 0, + }, + }, + { + value: 2105, + outputScript: + '76a9146b475c3b68ff8411e5c43271edc4e4f26dfc802a88ac', }, - ], - tokenFailedParsings: [], - tokenStatus: 'TOKEN_STATUS_NOT_NORMAL', - block: { - height: 760213, - hash: '000000000000000010150c61dcde7dffb6af223a7f3f45be599d43ae972cbf67', - timestamp: 1664921460, - }, - }, - parsed: { - incoming: false, - xecAmount: 0, - isEtokenTx: true, - isTokenBurn: true, - etokenAmount: '12', - tokenEntries: [ { - tokenId: - '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, + value: 2105, + outputScript: + '76a914ee8cbaa5642d1c5d1af1503edda6a55044e8106e88ac', + spentBy: { + txid: 'ca0229e4287f534526e811545e43c01bc011d2451acebd18aacbb74fe8d055ea', + outIdx: 2, + }, + }, + { + value: 2105, + outputScript: + '76a9147e3f074aae3cc99a6f48b928008eb9458615b6a988ac', + }, + { + value: 2105, + outputScript: + '76a9140816fa82ce5021871afb6fbdc9470714fbf7c7ed88ac', + }, + { + value: 2105, + outputScript: + '76a91412e01685eea02225ae3d3d528b184ae0db52314388ac', + spentBy: { + txid: 'fc4013c0a37cde3de2238f61c5212a7d115382aae5e0cb28b80c1d935e9233f5', + outIdx: 0, + }, + }, + { + value: 15987628, + outputScript: + '76a9142a96944d06700882bbd984761d9c9e4215f2d78e88ac', + spentBy: { + txid: '96f072b8db666b8eb59c0f43373b65c50fd5ac5042ea1e7d822161b45c2219a1', + outIdx: 0, }, - txType: 'SEND', - isInvalid: false, - burnSummary: 'Unexpected burn: Burns 12 base tokens', - failedColorings: [], - actualBurnAmount: '12', - intentionalBurn: '0', - burnsMintBatons: false, }, ], - assumedTokenDecimals: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + lockTime: 0, + timeFirstSeen: 1711102052, + size: 3645, + isCoinbase: false, + tokenEntries: [], + tokenFailedParsings: [], + tokenStatus: 'TOKEN_STATUS_NON_TOKEN', + block: { + height: 836935, + hash: '000000000000000008e74d35ca49974c15ca67e1209fa7e23bea15450dd64336', + timestamp: 1711102691, + }, }, + parsed: {}, }; -export const tokenBurnDecimals = { +export const outgoingEncryptedMsg = { tx: { - txid: 'dacd4bacb46caa3af4a57ac0449b2cb82c8a32c64645cd6a64041287d1ced556', + txid: '7ac10096c8a7b32fe338dc938bcf2e1341b99f841687e690d88241107ce4b84b', version: 2, inputs: [ { prevOut: { - txid: 'eb79e90e3b5a0b6766cbfab3efd9c52f831bef62f9f27c2aa925ee81e43b843f', - outIdx: 0, + txid: '45411aa786288b679d1c1874f7b126d5ea0c83380304950d364b5b8279a460de', + outIdx: 1, }, inputScript: - '47304402207122751937862fad68c3e293982cf7afb91967d20da63a0c23bf0565b625b775022054f39f41a43438a0df7fbe6a78521f572613bc08d6a43b6d248bcb6a434e2b52412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', - value: 2200, + '483045022100d4a93c615a7af48f422c273a530ac7f2b78d31a2d4515f11b2f416fce4f4f380022075c22c73190a7de805f219ca8d294777440b558551fea6b59c6c84ec529b16f94121038c4c26730d97cdeb18e69dff6c47cebb23e6f305c950923cd6110f35ab9006d0', + value: 48445, sequenceNo: 4294967295, outputScript: - '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + '76a914ee6dc9d40f95d8e106a63385c6fa882991b9e84e88ac', + }, + ], + outputs: [ + { + value: 0, + outputScript: + '6a04657461624ca1040f3cc3bc507126c239cde840befd974bdac054f9b9f2bfd4ff32b5f59ca554c4f3fb2d11d30eae3e5d3f61625ff7812ba14f8c901c30ee7e03dea57681a8f7ab8c64d42ce505921b4d67507452537cbe7525281714857c75d7a441b65030b7ea646b59ed0c34adc9f739661620cf7678963db3cac78afd7f49ad0d63aad404b07730255ded82ea3a939c63ee040ae9fac9336bb8d84d7b3380665ffa514a45f4', + }, + { + value: 1200, + outputScript: + '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + spentBy: { + txid: 'aca8ec27a6fc4dc45b1c2e2a6175e84d81ffdd54c7f97711654a100ade4e80bc', + outIdx: 0, + }, + }, + { + value: 46790, + outputScript: + '76a914ee6dc9d40f95d8e106a63385c6fa882991b9e84e88ac', + spentBy: { + txid: '610f8a6f8e7266af18feda7a5672d379314eb05cb7ce6690a1f1d5bff1051dad', + outIdx: 1, + }, }, + ], + lockTime: 0, + timeFirstSeen: 0, + size: 404, + isCoinbase: false, + tokenEntries: [], + tokenFailedParsings: [], + tokenStatus: 'TOKEN_STATUS_NON_TOKEN', + block: { + height: 760192, + hash: '0000000000000000085f5e0372ca7d42c37e5f93db753440331b3cfc1be23052', + timestamp: 1664910499, + }, + }, + parsed: { + satoshisSent: 1200, + stackArray: [ + '65746162', + '040f3cc3bc507126c239cde840befd974bdac054f9b9f2bfd4ff32b5f59ca554c4f3fb2d11d30eae3e5d3f61625ff7812ba14f8c901c30ee7e03dea57681a8f7ab8c64d42ce505921b4d67507452537cbe7525281714857c75d7a441b65030b7ea646b59ed0c34adc9f739661620cf7678963db3cac78afd7f49ad0d63aad404b07730255ded82ea3a939c63ee040ae9fac9336bb8d84d7b3380665ffa514a45f4', + ], + xecTxType: 'Sent', + recipients: ['ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6'], + }, +}; + +export const incomingEncryptedMsg = { + tx: { + txid: '66974f4a22ca1a4aa36c932b4effafcb9dd8a32b8766dfc7644ba5922252c4c6', + version: 2, + inputs: [ { prevOut: { - txid: '905cc5662cad77df56c3770863634ce498dde9d4772dc494d33b7ce3f36fa66c', - outIdx: 2, + txid: 'fec829a1ff34a9f84058cdd8bf795c114a8fcb3bcc6c3ca9ea8b9ae68420dd9a', + outIdx: 1, }, inputScript: - '483045022100dce5b3b516bfebd40bd8d4b4ff9c43c685d3c9dde1def0cc0667389ac522cf2502202651f95638e48c210a04082e6053457a539aef0f65a2e9c2f61e3faf96c1dfd8412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', - value: 546, + '483045022100e9fce8984a9f0cb76642c6df63a83150aa31d1071b62debe89ecadd4d45e727e02205a87fcaad0dd188860db8053caf7d6a21ed7807dbcd1560c251f9a91a4f36815412103318d0e1109f32debc66952d0e3ec21b1cf96575ea4c2a97a6535628f7f8b10e6', + value: 36207, sequenceNo: 4294967295, - token: { - tokenId: - '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - amount: '5235120760000000', - isMintBaton: false, - entryIdx: 0, - }, outputScript: - '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', }, ], outputs: [ { value: 0, outputScript: - '6a04534c500001010453454e44207443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d0800129950892eb779', + '6a04657461624c9104eaa5cbe6e13db7d91f35dca5d270c944a9a3e8c7738c56d12069312f589c7f193e67ea3d2f6d1f300f404c33c19e48dc3ac35145c8152624b7a8e22278e9133862425da2cc44f7297c8618ffa78dd09054a4a5490afd2b62139f19fa7b8516cbae692488fa50e79101d55e7582b3a662c3a5cc737044ef392f8c1fde63b8385886aed37d1b68e887284262f298fe74c0', }, { - value: 546, + value: 1100, outputScript: - '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', - token: { - tokenId: - '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - amount: '5235120758765433', - isMintBaton: false, - entryIdx: 0, + '76a914ee6dc9d40f95d8e106a63385c6fa882991b9e84e88ac', + spentBy: { + txid: '610f8a6f8e7266af18feda7a5672d379314eb05cb7ce6690a1f1d5bff1051dad', + outIdx: 0, }, + }, + { + value: 34652, + outputScript: + '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', spentBy: { - txid: '9c0c01c1e8cc3c6d816a3b41d09d65fda69de082b74b6ede7832ed05527ec744', - outIdx: 1, + txid: '3efa1835682ecc60d2476f1c608eb6f5ae9040610193111a2c312453cd7db4ef', + outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, - size: 403, + size: 388, isCoinbase: false, - tokenEntries: [ - { - tokenId: - '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'SEND', - isInvalid: false, - burnSummary: 'Unexpected burn: Burns 1234567 base tokens', - failedColorings: [], - actualBurnAmount: '1234567', - intentionalBurn: '0', - burnsMintBatons: false, - }, - ], + tokenEntries: [], tokenFailedParsings: [], - tokenStatus: 'TOKEN_STATUS_NOT_NORMAL', + tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { - height: 760216, - hash: '00000000000000000446cfe07eb99bca0ba33a23465e1b0248be96efed74c89d', - timestamp: 1664923585, + height: 760192, + hash: '0000000000000000085f5e0372ca7d42c37e5f93db753440331b3cfc1be23052', + timestamp: 1664910499, }, }, parsed: { - incoming: false, - xecAmount: 0, - isEtokenTx: true, - etokenAmount: '0.1234567', - isTokenBurn: true, - tokenEntries: [ - { - tokenId: - '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'SEND', - isInvalid: false, - burnSummary: 'Unexpected burn: Burns 1234567 base tokens', - failedColorings: [], - actualBurnAmount: '1234567', - intentionalBurn: '0', - burnsMintBatons: false, - }, + satoshisSent: 1100, + stackArray: [ + '65746162', + '04eaa5cbe6e13db7d91f35dca5d270c944a9a3e8c7738c56d12069312f589c7f193e67ea3d2f6d1f300f404c33c19e48dc3ac35145c8152624b7a8e22278e9133862425da2cc44f7297c8618ffa78dd09054a4a5490afd2b62139f19fa7b8516cbae692488fa50e79101d55e7582b3a662c3a5cc737044ef392f8c1fde63b8385886aed37d1b68e887284262f298fe74c0', ], - assumedTokenDecimals: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + xecTxType: 'Received', + recipients: ['ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6'], }, }; -export const incomingEtokenTwo = { +export const tokenBurn = { tx: { - txid: 'b808f6a831dcdfda2bd4c5f857f94e1a746a4effeda6a5ad742be6137884a4fb', + txid: '312553668f596bfd61287aec1b7f0f035afb5ddadf40b6f9d1ffcec5b7d4b684', version: 2, inputs: [ { prevOut: { - txid: 'c638754cb7707edd4faad89bdfee899aa7acbbc61f66e21f8faf60bdbb34fd65', - outIdx: 3, + txid: '842dd09e723d664d7647bc49f911c88b60f0450e646fedb461f319dadb867934', + outIdx: 0, }, inputScript: - '4830450221009d649476ad963306a5210d9df2dfd7e2bb604be43d6cdfe359638d96239973eb02200ac6e71575f0f111dad2fbbeb2712490cc709ffe03eda7de33acc8614b2c0979412103318d0e1109f32debc66952d0e3ec21b1cf96575ea4c2a97a6535628f7f8b10e6', - value: 3503, + '473044022025c68cf0ab9c1a4d6b35b2b58f7e397722f469412841eb09d38d1973dc5ef7120220712e1f3c8740fff2af75c1062a773eef167550ee008deaef9089537cd17c35f0412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + value: 2300, sequenceNo: 4294967295, outputScript: - '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, { prevOut: { - txid: '82d8dc652779f8d6c8453d2ba5aefec91f5247489246e5672cf3c5986fa3d235', + txid: '1efe359a0bfa83c409433c487b025fb446a3a9bfa51a718c8dd9a56401656e33', outIdx: 2, }, inputScript: - '483045022100b7bec6d09e71bc4c124886e5953f6e7a7845c920f66feac2e9e5d16fc58a649a0220689d617c11ef0bd63dbb7ea0fa5c0d3419d6500535bda8f7a7fc3e27f27c3de6412103318d0e1109f32debc66952d0e3ec21b1cf96575ea4c2a97a6535628f7f8b10e6', + '47304402206a2f53497eb734ea94ca158951aa005f6569c184675a497d33d061b78c66c25b02201f826fa71be5943ce63740d92a278123974e44846c3766c5cb58ef5ad307ba36412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', value: 546, sequenceNo: 4294967295, token: { tokenId: - 'acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', + '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '2', + isMintBaton: false, + entryIdx: 0, + }, + outputScript: + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + }, + { + prevOut: { + txid: '49f825370128056333af945eb4f4d9712171c9e88954deb189ca6f479564f2ee', + outIdx: 2, + }, + inputScript: + '483045022100efa3c767b749abb2dc958932348e2b19b845964e581c9f6de706cd43dac3f087022059afad6ff3c1e49cc0320499381e78eab922f18b00e0409228ad417e0220bf5d412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + value: 546, + sequenceNo: 4294967295, + token: { + tokenId: + '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, - amount: '9876543156', + amount: '999875', isMintBaton: false, entryIdx: 0, }, outputScript: - '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, ], outputs: [ { value: 0, outputScript: - '6a04534c500001010453454e4420acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f550800000000075bcd1508000000024554499f', + '6a04534c500001010453454e44204db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c8750800000000000f41b9', }, { value: 546, @@ -9105,52 +9398,30 @@ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', token: { tokenId: - 'acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - amount: '123456789', - isMintBaton: false, - entryIdx: 0, - }, - }, - { - value: 546, - outputScript: - '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', - token: { - tokenId: - 'acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', + '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, - amount: '9753086367', + amount: '999865', isMintBaton: false, entryIdx: 0, }, - }, - { - value: 1685, - outputScript: - '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', spentBy: { - txid: '04b16fa516fbdd64d51b8aa1a752855beb4250d99199322d89d9c4c6172a1b9f', - outIdx: 4, + txid: '657646f7a4e7237fca4ed8231c27d95afc8086f678244d5560be2230d920ff70', + outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 0, - size: 481, + size: 550, isCoinbase: false, tokenEntries: [ { tokenId: - 'acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', + '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', @@ -9158,164 +9429,113 @@ }, txType: 'SEND', isInvalid: false, - burnSummary: '', + burnSummary: 'Unexpected burn: Burns 12 base tokens', failedColorings: [], - actualBurnAmount: '0', + actualBurnAmount: '12', intentionalBurn: '0', burnsMintBatons: false, }, ], tokenFailedParsings: [], - tokenStatus: 'TOKEN_STATUS_NORMAL', + tokenStatus: 'TOKEN_STATUS_NOT_NORMAL', block: { - height: 760076, - hash: '00000000000000000bf1ee10a21cc4b784ea48840fa00237e41f69a027c6a86c', - timestamp: 1664840266, + height: 760213, + hash: '000000000000000010150c61dcde7dffb6af223a7f3f45be599d43ae972cbf67', + timestamp: 1664921460, }, }, parsed: { - incoming: true, - xecAmount: 5.46, - isEtokenTx: true, - etokenAmount: '0.123456789', - isTokenBurn: false, - tokenEntries: [ - { - tokenId: - 'acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'SEND', - isInvalid: false, - burnSummary: '', - failedColorings: [], - actualBurnAmount: '0', - intentionalBurn: '0', - burnsMintBatons: false, - }, + satoshisSent: 546, + stackArray: [ + '534c5000', + '01', + '53454e44', + '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', + '00000000000f41b9', ], - assumedTokenDecimals: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6', + xecTxType: 'Sent', + recipients: [], }, }; -export const swapTx = { +export const tokenBurnDecimals = { tx: { - txid: '2f030de7c8f80a1ecac3645092dd22f0943c34d54cb734e12d7dfda0641fdfcf', - version: 1, + txid: 'dacd4bacb46caa3af4a57ac0449b2cb82c8a32c64645cd6a64041287d1ced556', + version: 2, inputs: [ { prevOut: { - txid: '4e771bc4bbd377f05b467b0e070ff330f03112b9effb61af5568e174850afa1b', - outIdx: 1, + txid: 'eb79e90e3b5a0b6766cbfab3efd9c52f831bef62f9f27c2aa925ee81e43b843f', + outIdx: 0, }, inputScript: - '41ff62002b741b8b4831484f9a214c72972965765dc398cccb2f9756a910415f89a28c3560b772a73cb6f987057a7204105cb8afab30a46e74308a134d15ceb48b4121038a124bbf306b5bd19e8d10a396a96ae18abe79229820f30e81989fd645cf0525', - value: 546, + '47304402207122751937862fad68c3e293982cf7afb91967d20da63a0c23bf0565b625b775022054f39f41a43438a0df7fbe6a78521f572613bc08d6a43b6d248bcb6a434e2b52412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + value: 2200, sequenceNo: 4294967295, outputScript: - '76a91480ad93eff2bd02e6383ba62476ffd729d1b2660d88ac', + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, { prevOut: { - txid: 'ffbe78a817d157a0debf3c6ee5e14cea8a2bd1cd0feaf8c368292b694110d7f4', - outIdx: 1, + txid: '905cc5662cad77df56c3770863634ce498dde9d4772dc494d33b7ce3f36fa66c', + outIdx: 2, }, inputScript: - '41a112ff6b2b9d288f507b48e042390b8b285bf761e617885eb9a536259c1bd1bec673325cebbf913d90ad0ec3237eac29e6592198cb52dcd6cf6786f784f5889e41210247295c2401b8846ddd915ba9808e0962241003baecd0242b3888d1b3182c2154', + '483045022100dce5b3b516bfebd40bd8d4b4ff9c43c685d3c9dde1def0cc0667389ac522cf2502202651f95638e48c210a04082e6053457a539aef0f65a2e9c2f61e3faf96c1dfd8412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', value: 546, sequenceNo: 4294967295, token: { tokenId: - '54dc2ecd5251f8dfda4c4f15ce05272116b01326076240e2b9cc0104d33b1484', + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, - amount: '10000', + amount: '5235120760000000', isMintBaton: false, entryIdx: 0, }, outputScript: - '76a91475c5980aa6eeada103b45f82e37163e9047903af88ac', - }, - { - prevOut: { - txid: '6684cb754ec82e4d9b9b068ab2191af8cfd0998da9f753c16fabb293664e45af', - outIdx: 0, - }, - inputScript: - '41bb8866a6cd6975ec9fdd8c45860c6cee5f83c52c801f830b3a97a69b6a02762c73a71ef91ce519224eb7e62fc4eb895587231a258a8f368f007c6377e7ca0028412102744cf89c996b8ec7ea887a1c4d0e0f98a2c82f8a1e4956ed12d8c8dc8bb2f6e4', - value: 101670, - sequenceNo: 4294967295, - outputScript: - '76a914205c792fff2ffc891e986246760ee1079fa5a36988ac', + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, ], outputs: [ { value: 0, outputScript: - '6a04534c500001010453454e442054dc2ecd5251f8dfda4c4f15ce05272116b01326076240e2b9cc0104d33b1484080000000000002710', + '6a04534c500001010453454e44207443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d0800129950892eb779', }, { value: 546, outputScript: - '76a914205c792fff2ffc891e986246760ee1079fa5a36988ac', + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', token: { tokenId: - '54dc2ecd5251f8dfda4c4f15ce05272116b01326076240e2b9cc0104d33b1484', + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, - amount: '10000', + amount: '5235120758765433', isMintBaton: false, entryIdx: 0, }, spentBy: { - txid: '7d3e61946d7573ee58d8d2d1d05366604b7eb5db64d8a1e22201f12f5836f864', - outIdx: 1, - }, - }, - { - value: 100546, - outputScript: - '76a91480ad93eff2bd02e6383ba62476ffd729d1b2660d88ac', - spentBy: { - txid: '561d26cc733822b2c518f6917bc84eeb78c505dc07e4f86379e93518f2c63514', - outIdx: 0, - }, - }, - { - value: 1000, - outputScript: - '76a914a7d744e1246a20f26238e0510fb82d8df84cc82d88ac', - spentBy: { - txid: 'b1db3cfab9ae782c9d85a52ea3271109d7270dbaa589b6cc19c72b9f7d23840b', + txid: '9c0c01c1e8cc3c6d816a3b41d09d65fda69de082b74b6ede7832ed05527ec744', outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 0, - size: 599, + size: 403, isCoinbase: false, tokenEntries: [ { tokenId: - '54dc2ecd5251f8dfda4c4f15ce05272116b01326076240e2b9cc0104d33b1484', + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', @@ -9323,103 +9543,111 @@ }, txType: 'SEND', isInvalid: false, - burnSummary: '', + burnSummary: 'Unexpected burn: Burns 1234567 base tokens', failedColorings: [], - actualBurnAmount: '0', + actualBurnAmount: '1234567', intentionalBurn: '0', burnsMintBatons: false, }, ], tokenFailedParsings: [], - tokenStatus: 'TOKEN_STATUS_NORMAL', + tokenStatus: 'TOKEN_STATUS_NOT_NORMAL', block: { - height: 767064, - hash: '0000000000000000018dacde348577244cca129a8787f1594ef3e2dff9831153', - timestamp: 1669029608, + height: 760216, + hash: '00000000000000000446cfe07eb99bca0ba33a23465e1b0248be96efed74c89d', + timestamp: 1664923585, }, }, parsed: { - incoming: true, - xecAmount: 10, - isEtokenTx: false, - aliasFlag: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qzq2myl0727s9e3c8wnzgahl6u5arvnxp5fs9sem4x', + satoshisSent: 546, + stackArray: [ + '534c5000', + '01', + '53454e44', + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + '00129950892eb779', + ], + xecTxType: 'Sent', + recipients: [], }, }; -export const aliasOffSpec = { +export const swapTx = { tx: { - txid: '7b265a49e0bd5fe0c5e4b4aec634a25dd85656766a035b6e436c415538c43d90', - version: 2, + txid: 'baed6358b9ea2e354e384d2e31a576ffa25fcceaf796e711e8306f9c8086b00f', + version: 1, inputs: [ { prevOut: { - txid: '1be4bb9f820d60a82f6eb86a32ca9442700f180fc94469bca2ded9129f5dce88', + txid: '8b55a382501b538296cd13269b341f7a964366a705a45f89f56e0d783240f3a4', outIdx: 2, }, inputScript: - '47304402205af9cf7ddb8412803b8e884dbd5cb02535ffc266fd5c6afb3e48e7425e7b215b0220799688d330130e4c7c7ffa33d9310e0bbc6fd820bbe26f7f47f52c17d79d6d4d4121022658400e1f93f3f491b6b8e98c0af1f45e30dd6a328894b7ea0569e0182c1e77', - value: 3962985, - sequenceNo: 4294967294, + '41256f3c091df7dea2bb9d74241b47116364d7b0035dfe1c5d1d398d8e92e99f4d5f3dd747f8e81ca99ddaf5630399ef18e26b6a3bf9b763cdd25225e68f7bbd2d41210304222c88e9936a195762fc4ee41a082e906a0e8434df43a03bfcdf1f9d2c1b8d', + value: 546, + sequenceNo: 4294967295, + outputScript: + '76a91493472d56ba91581ed473225a765dd14a2db5d9d888ac', + }, + { + prevOut: { + txid: '8b55a382501b538296cd13269b341f7a964366a705a45f89f56e0d783240f3a4', + outIdx: 3, + }, + inputScript: + '418ab02f08273afd67c4db840f09429d7c76c0a71b28dbaef5c63f277944a168819d72bedd14e78b327a237f6070b0519ef8456efbfe206bae0c60d3b5f328faea412103df543832906a1f5fc8f201bb99454f350b1906375d522f735bd357cbda11ab5b', + value: 2565, + sequenceNo: 4294967295, outputScript: - '76a914bc4932372bf33d57b3a21b2b2636919bc83a87a788ac', + '76a9149ea00e6c2ef24026719421e4790e1a694c94381b88ac', }, ], outputs: [ { value: 0, - outputScript: '6a042e7865630d616e64616e6f746865726f6e65', - }, - { - value: 551, outputScript: - '76a914638568e36d0b5d7d49a6e99854caa27d9772b09388ac', - spentBy: { - txid: '33805053250ab648e231ea61a70fc4027765c184c112cc0b83f05f7c9db6a4c5', - outIdx: 12, - }, + '6a045357500001010101209e0a9d4720782cf661beaea6c5513f1972e0f3b1541ba4c83f4c87ef65f843dc0453454c4c0631323831323301002039c6db26912f34352d50fdfd8d75d1c16cb8a669f3ae05000a6c8c74d14839a50101063132383132330437383035', }, { - value: 3961979, + value: 2656, outputScript: - '76a914bc4932372bf33d57b3a21b2b2636919bc83a87a788ac', + '76a91493472d56ba91581ed473225a765dd14a2db5d9d888ac', spentBy: { - txid: 'f299dfce0030f9a0cf6d104b95182d973cf46111cfb3daaebb62b44c25d3f134', - outIdx: 0, + txid: '47f7a2189eb65e9a2288f81640351cc80ada49288b09973bcaa7aef1e423faa8', + outIdx: 1, }, }, ], lockTime: 0, - timeFirstSeen: 0, - size: 254, + timeFirstSeen: 1712535539, + size: 439, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { - height: 778616, - hash: '00000000000000000fc2761e52b21752aee12a0f36b339f669a195b00a4a172e', - timestamp: 1675967591, + height: 839523, + hash: '00000000000000000c61b358a9681170b9387790370bf3ca18a402bc50264fc0', + timestamp: 1712536759, }, }, parsed: { - airdropFlag: false, - airdropTokenId: '', - aliasFlag: true, - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: false, - opReturnMessage: 'off-spec alias registration', - replyAddress: 'ecash:qz7yjv3h90en64an5gdjkf3kjxdusw585u9j5rqxcg', - xecAmount: 0, + satoshisSent: 0, + stackArray: [ + '53575000', + '01', + '01', + '9e0a9d4720782cf661beaea6c5513f1972e0f3b1541ba4c83f4c87ef65f843dc', + '53454c4c', + '313238313233', + '00', + '39c6db26912f34352d50fdfd8d75d1c16cb8a669f3ae05000a6c8c74d14839a5', + '01', + '313238313233', + '37383035', + ], + xecTxType: 'Received', + recipients: ['ecash:qzf5wt2kh2g4s8k5wv395aja699zmdwemq05vg6h92'], }, }; @@ -9475,17 +9703,10 @@ }, }, parsed: { - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: false, - opReturnMessage: 'PayButton (d980190d13019567)', - replyAddress: 'ecash:qqpuv0f62tx7zdk63pvwn58l42qsednrjgnt0czndd', - xecAmount: 0, + satoshisSent: 1800, + stackArray: ['50415900', '00', '00', 'd980190d13019567'], + xecTxType: 'Received', + recipients: ['ecash:qqqmlnj07demzz9avk6d5zx7vgddapddk5k05jys53'], }, }; @@ -9546,17 +9767,13 @@ }, }, parsed: { - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: false, - opReturnMessage: 'PayButton (69860643e4dc4c88): πŸ˜‚πŸ‘', - replyAddress: 'ecash:qrnz3uf0r6g3e8eqashwkxz8uw30lt2les5yk8l5d7', - xecAmount: 0, + satoshisSent: 3401592, + stackArray: ['50415900', '00', 'f09f9882f09f918d', '69860643e4dc4c88'], + xecTxType: 'Sent', + recipients: [ + 'ecash:qrjh8hvf5c0cmt44d06mt76a0nvxuvdt9cmj39zxwm', + 'ecash:qp5h4eetqcj407nfl82dpyvz22w6x69tdyxpprn8zg', + ], }, }; @@ -9569,17 +9786,13 @@ export const PayButtonEmpty = { tx: PayButtonEmptyTx, parsed: { - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: false, - opReturnMessage: 'PayButton', - replyAddress: 'ecash:qrnz3uf0r6g3e8eqashwkxz8uw30lt2les5yk8l5d7', - xecAmount: 0, + satoshisSent: 3401592, + stackArray: ['50415900', '00', '00', '00'], + xecTxType: 'Sent', + recipients: [ + 'ecash:qrjh8hvf5c0cmt44d06mt76a0nvxuvdt9cmj39zxwm', + 'ecash:qp5h4eetqcj407nfl82dpyvz22w6x69tdyxpprn8zg', + ], }, }; // data and no payment id @@ -9592,17 +9805,13 @@ export const PayButtonYesDataNoNonce = { tx: PayButtonYesDataNoNonceTx, parsed: { - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: false, - opReturnMessage: 'PayButton: only data here', - replyAddress: 'ecash:qrnz3uf0r6g3e8eqashwkxz8uw30lt2les5yk8l5d7', - xecAmount: 0, + satoshisSent: 3401592, + stackArray: ['50415900', '00', '6f6e6c7920646174612068657265', '00'], + xecTxType: 'Sent', + recipients: [ + 'ecash:qrjh8hvf5c0cmt44d06mt76a0nvxuvdt9cmj39zxwm', + 'ecash:qp5h4eetqcj407nfl82dpyvz22w6x69tdyxpprn8zg', + ], }, }; @@ -9615,17 +9824,13 @@ export const PayButtonOffSpec = { tx: PayButtonOffSpecTx, parsed: { - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: false, - opReturnMessage: 'off-spec PayButton tx', - replyAddress: 'ecash:qrnz3uf0r6g3e8eqashwkxz8uw30lt2les5yk8l5d7', - xecAmount: 0, + satoshisSent: 3401592, + stackArray: ['50415900', '00', 'f09f9882f09f918d'], + xecTxType: 'Sent', + recipients: [ + 'ecash:qrjh8hvf5c0cmt44d06mt76a0nvxuvdt9cmj39zxwm', + 'ecash:qp5h4eetqcj407nfl82dpyvz22w6x69tdyxpprn8zg', + ], }, }; @@ -9639,17 +9844,13 @@ export const PayButtonBadVersion = { tx: PayButtonBadVersionTx, parsed: { - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: false, - opReturnMessage: 'Unsupported version PayButton tx: 01', - replyAddress: 'ecash:qrnz3uf0r6g3e8eqashwkxz8uw30lt2les5yk8l5d7', - xecAmount: 0, + satoshisSent: 3401592, + stackArray: ['50415900', '01', 'f09f9882f09f918d', '69860643e4dc4c88'], + xecTxType: 'Sent', + recipients: [ + 'ecash:qrjh8hvf5c0cmt44d06mt76a0nvxuvdt9cmj39zxwm', + 'ecash:qp5h4eetqcj407nfl82dpyvz22w6x69tdyxpprn8zg', + ], }, }; @@ -9697,18 +9898,64 @@ tokenStatus: 'TOKEN_STATUS_NON_TOKEN', }, parsed: { - incoming: true, - xecAmount: 0, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: 'hello from eCash Chat πŸ‘', - isCashtabMessage: false, - isEcashChatMessage: true, - isEncryptedMessage: false, - replyAddress: 'ecash:qqznd7vug3avk24jdwgakaqewkmwp0vczu5u9man9y', - aliasFlag: false, + satoshisSent: 1000, + xecTxType: 'Received', + stackArray: [ + '63686174', + '68656c6c6f2066726f6d206543617368204368617420f09f918d', + ], + recipients: ['ecash:qqznd7vug3avk24jdwgakaqewkmwp0vczu5u9man9y'], + }, +}; +export const CashtabMsg = { + tx: { + txid: '1ce6c307b4083fcfc065287a00f0a582cf88bf33de34845db4c49387d4532b8a', + version: 2, + inputs: [ + { + prevOut: { + txid: '01d4b064a4e17f77e5712cb13b488e65d39b33b54475b78debee1fe1d9d9acb1', + outIdx: 1, + }, + inputScript: + '483045022100eccfc2e23d49fb7e72a35123c807f4feef2f379313673295f36611d725e877b002207b1df4c142c590a54d371fe2f04c05769ecf778e0d28fc50a671e5c5d8b277854121028c1fc90b3fa6e5be985032b061b5ca6db41a6878a9c8b442747b820ca74010db', + value: 3001592, + sequenceNo: 4294967295, + outputScript: + '76a914e6309418b6e60b8119928ec45b8ba87de8e735f788ac', + }, + ], + outputs: [ + { + value: 0, + outputScript: + '6a04007461624cbe4d6572636920706f7572206c65207072697820657420626f6e6e6520636f6e74696e756174696f6e2064616e7320766f732070726f6a6574732064652064c3a976656c6f70706575722e2e2e204a27616920c3a974c3a92063656e737572c3a92073c3bb722074c3a96cc3a96772616d6d65206a7573717527617520313520417672696c20323032342e2052c3a97061726572206c6520627567206f6273657276c3a920737572206c6120706167652065546f6b656e204661756365743f', + }, + { + value: 550, + outputScript: + '76a9143c28745097b1e32b343c50a8d4a7697fe7ad8aff88ac', + }, + { + value: 3000609, + outputScript: + '76a914e6309418b6e60b8119928ec45b8ba87de8e735f788ac', + }, + ], + lockTime: 0, + timeFirstSeen: 1712616513, + size: 433, + isCoinbase: false, + tokenEntries: [], + tokenFailedParsings: [], + tokenStatus: 'TOKEN_STATUS_NON_TOKEN', + block: { + height: 839618, + hash: '00000000000000000e63e39951cc745db046aa7f57f811b68846ade8ad100293', + timestamp: 1712616969, + }, }, + parsed: {}, }; export const SlpV1Mint = { @@ -9834,37 +10081,17 @@ }, }, parsed: { - airdropFlag: false, - airdropTokenId: '', - assumedTokenDecimals: true, - etokenAmount: '100', - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: true, - isTokenBurn: false, - opReturnMessage: '', - replyAddress: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', - tokenEntries: [ - { - actualBurnAmount: '0', - burnSummary: '', - burnsMintBatons: false, - failedColorings: [], - intentionalBurn: '0', - isInvalid: false, - tokenId: - 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', - tokenType: { - number: 1, - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - }, - txType: 'MINT', - }, - ], - xecAmount: 33.72, + satoshisSent: 3372, + stackArray: [ + '534c5000', + '01', + '4d494e54', + 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + '02', + '0000000000000064', + ], + xecTxType: 'Sent', + recipients: [], }, }; @@ -9895,50 +10122,102 @@ { value: 80213, outputScript: - '76a914731fbd873b3603e8dafd62923b954d38571e10fc88ac', - spentBy: { - txid: 'b817870c8ae5ec94d639089e37763daee271f412ab478705a29b036ba0b00f3d', - outIdx: 55, - }, + '76a914731fbd873b3603e8dafd62923b954d38571e10fc88ac', + spentBy: { + txid: 'b817870c8ae5ec94d639089e37763daee271f412ab478705a29b036ba0b00f3d', + outIdx: 55, + }, + }, + { + value: 600, + outputScript: + '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + spentBy: { + txid: 'dc06ab36c9a7e365f319c0e918324af9778cb29b82c07ff87e2ec80eb6e4e6fe', + outIdx: 9, + }, + }, + ], + lockTime: 0, + timeFirstSeen: 1709353270, + size: 253, + isCoinbase: false, + tokenEntries: [], + tokenFailedParsings: [], + tokenStatus: 'TOKEN_STATUS_NON_TOKEN', + block: { + height: 833968, + hash: '000000000000000020f276cf59fc4e53672500ca5b5896502d0a50500174c27c', + timestamp: 1709354653, + }, + }, + parsed: { + satoshisSent: 600, + stackArray: ['74657374696e672061206d736720666f72206572726f72'], + xecTxType: 'Received', + recipients: ['ecash:qpe3l0v88vmq86x6l43fywu4f5u9w8sslsga0tcn4t'], + }, +}; + +export const unknownAppTx = { + tx: { + txid: '4cd528a95263714b8f748d58df30c44956158825924e3385b5c5c511129d1b3a', + version: 2, + inputs: [ + { + prevOut: { + txid: '9ca28926f8ec125dce0b7084468bd595b27bd73991b48461ac994cacff47a21d', + outIdx: 1, + }, + inputScript: + '483045022100b50fac4b810ac6b10ce35f25fcc1a6b1f87b1209e8ee5973732d983395199de102204f860238b12ba3e7adfc432e331405f751fef1aa494c2d0122b7aaa522158933412102188904278ebf33059093f596a2697cf3668b3bec9a3a0c6408a455147ab3db93', + value: 3725, + sequenceNo: 4294967295, + outputScript: + '76a914d18b7b500f17c5db64303fec630f9dbb85aa959688ac', + }, + ], + outputs: [ + { + value: 0, + outputScript: + '6a4cd43336616533642d4d45524f4e2d57494e227d2c7b226e616d65223a2277616c61222c226d657373616765223a223635396661313133373065333136663265613336616533642d57414c412d57494e227d5d2c227465726d73223a5b7b226e616d65223a22726566657265655075624b6579222c2274797065223a226279746573222c2276616c7565223a22303231383839303432373865626633333035393039336635393661323639376366333636386233626563396133613063363430386134353531343761623364623933227d5d7d7d7d7d', }, { - value: 600, + value: 3308, outputScript: - '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + '76a914d18b7b500f17c5db64303fec630f9dbb85aa959688ac', spentBy: { - txid: 'dc06ab36c9a7e365f319c0e918324af9778cb29b82c07ff87e2ec80eb6e4e6fe', - outIdx: 9, + txid: 'e5b4912fa19d93db9b6b9586ad9ab3a7f9bc3514325c71e36816e4b047a9f6b8', + outIdx: 0, }, }, ], lockTime: 0, - timeFirstSeen: 1709353270, - size: 253, + timeFirstSeen: 0, + size: 416, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { - height: 833968, - hash: '000000000000000020f276cf59fc4e53672500ca5b5896502d0a50500174c27c', - timestamp: 1709354653, + height: 826662, + hash: '00000000000000001d45441094ec7a93f42f3beb564684aba68250b016feefb4', + timestamp: 1704961725, }, }, parsed: { - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: false, - opReturnMessage: 'testing a msg for error', - replyAddress: 'ecash:qrhlng96s3awja5h48uhcpvg02azksgxpce6nvshln', - xecAmount: 0, + satoshisSent: 3308, + stackArray: [ + '3336616533642d4d45524f4e2d57494e227d2c7b226e616d65223a2277616c61222c226d657373616765223a223635396661313133373065333136663265613336616533642d57414c412d57494e227d5d2c227465726d73223a5b7b226e616d65223a22726566657265655075624b6579222c2274797065223a226279746573222c2276616c7565223a22303231383839303432373865626633333035393039336635393661323639376366333636386233626563396133613063363430386134353531343761623364623933227d5d7d7d7d7d', + ], + xecTxType: 'Sent', + recipients: [], }, }; +// TODO you are here. Get the parseTx unit tests to match mocks by updating mocks. +// You will need integration tests for rendering of different OP_RETURN types export const AlpTx = { tx: { txid: '791c460c6d5b513283b98b92b83f0e6fa662fc279f39fd00bd27047370ba4647', @@ -10105,17 +10384,16 @@ }, }, parsed: { - airdropFlag: false, - airdropTokenId: '', - aliasFlag: false, - incoming: true, - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - isEtokenTx: false, - opReturnMessage: '', - replyAddress: 'ecash:qpt4z9kg4h6czlyel3da4jxmrrgscfts859gzp2zuu', - xecAmount: 0, + satoshisSent: 546, + stackArray: [ + '50', + '534c5032000453454e4445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd038a02000000003e3000000000948f00000000', + ], + xecTxType: 'Received', + recipients: [ + 'ecash:pzctlwr4prjjqwqrfyxz7wy36pq0wu46pud7n9ffz3', + 'ecash:qpt4z9kg4h6czlyel3da4jxmrrgscfts859gzp2zuu', + ], }, }; @@ -17084,6 +17362,268 @@ } export const mockLargeTokenCache = largeTokenCache; +const txHistorySupportingTokenCache = { + aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1: { + token: { + tokenId: + 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + timeFirstSeen: 1711776546, + genesisInfo: { + tokenTicker: 'CACHET', + tokenName: 'Cachet', + url: 'https://cashtab.com/', + decimals: 2, + hash: '', + }, + block: { + height: 838192, + hash: '0000000000000000132232769161d6211f7e6e20cf63b26e5148890aacd26962', + timestamp: 1711779364, + }, + }, + tx: { + txid: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + version: 2, + inputs: [ + { + prevOut: { + txid: 'dd3eafefb1941fd67d8a29b7dd057ac48ec11712887e2ae7c008a7c72d0cd9fc', + outIdx: 0, + }, + inputScript: + '4830450221009bb1fb7d49d9ac64b79ea041be2e2efa5a8709a470930b04c27c9fc46ed1906302206a0a9daf5e64e934a3467951dd2da37405969d4434d4006ddfea3ed39ff4e0ae412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + value: 2200, + sequenceNo: 4294967295, + outputScript: + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + }, + ], + outputs: [ + { + value: 0, + outputScript: + '6a04534c500001010747454e4553495306434143484554064361636865741468747470733a2f2f636173687461622e636f6d2f4c0001020102080000000000989680', + }, + { + value: 546, + outputScript: + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + token: { + tokenId: + 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '10000000', + isMintBaton: false, + entryIdx: 0, + }, + }, + { + value: 546, + outputScript: + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + token: { + tokenId: + 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '0', + isMintBaton: true, + entryIdx: 0, + }, + spentBy: { + txid: '4b5b2a0f8bcacf6bccc7ef49e7f82a894c9c599589450eaeaf423e0f5926c38e', + outIdx: 0, + }, + }, + { + value: 773, + outputScript: + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + spentBy: { + txid: '343356b9d4acd59065f90b1ace647c1f714f1fd4c411e2cf77081a0246c7416d', + outIdx: 3, + }, + }, + ], + lockTime: 0, + timeFirstSeen: 1711776546, + size: 335, + isCoinbase: false, + tokenEntries: [ + { + tokenId: + 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + txType: 'GENESIS', + isInvalid: false, + burnSummary: '', + failedColorings: [], + actualBurnAmount: '0', + intentionalBurn: '0', + burnsMintBatons: false, + }, + ], + tokenFailedParsings: [], + tokenStatus: 'TOKEN_STATUS_NORMAL', + block: { + height: 838192, + hash: '0000000000000000132232769161d6211f7e6e20cf63b26e5148890aacd26962', + timestamp: 1711779364, + }, + }, + calculated: { + genesisSupply: '100000.00', + genesisOutputScripts: [ + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + ], + genesisMintBatons: 1, + }, + }, + cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145: { + token: { + tokenId: + 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', + tokenType: { + protocol: 'ALP', + type: 'ALP_TOKEN_TYPE_STANDARD', + number: 0, + }, + timeFirstSeen: 0, + genesisInfo: { + tokenTicker: 'CRD', + tokenName: 'Credo In Unum Deo', + url: 'https://crd.network/token', + decimals: 4, + data: {}, + authPubkey: + '0334b744e6338ad438c92900c0ed1869c3fd2c0f35a4a9b97a88447b6e2b145f10', + }, + block: { + height: 795680, + hash: '00000000000000000b7e89959ee52ca1cd691e1fc3b4891c1888f84261c83e73', + timestamp: 1686305735, + }, + }, + tx: { + txid: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', + version: 1, + inputs: [ + { + prevOut: { + txid: 'dd2020be54ad3dccf98548512e6f735cac002434bbddb61f19cbe6f3f1de04da', + outIdx: 0, + }, + inputScript: + '4130ef71df9d2daacf48d05a0361e103e087b636f4d68af8decd769227caf198003991629bf7057fa1572fc0dd3581115a1b06b5c0eafc88555e58521956fe5cbc410768999600fc71a024752102d8cb55aaf01f84335130bf7b3751267e5cf3398a60e5162ff93ec8d77f14850fac', + value: 4000, + sequenceNo: 4294967295, + outputScript: + 'a91464275fca443d169d23d077c85ad1bb7a31b6e05987', + }, + ], + outputs: [ + { + value: 0, + outputScript: + '6a504c63534c5032000747454e455349530343524411437265646f20496e20556e756d2044656f1968747470733a2f2f6372642e6e6574776f726b2f746f6b656e00210334b744e6338ad438c92900c0ed1869c3fd2c0f35a4a9b97a88447b6e2b145f10040001', + }, + { + value: 546, + outputScript: + '76a914bbb6c4fecc56ecce35958f87c2367cd3f5e88c2788ac', + token: { + tokenId: + 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', + tokenType: { + protocol: 'ALP', + type: 'ALP_TOKEN_TYPE_STANDARD', + number: 0, + }, + amount: '0', + isMintBaton: true, + entryIdx: 0, + }, + spentBy: { + txid: 'ff06c312bef229f6f27989326d9be7e0e142aaa84538967b104b262af69f7f00', + outIdx: 0, + }, + }, + ], + lockTime: 777777, + timeFirstSeen: 0, + size: 308, + isCoinbase: false, + tokenEntries: [ + { + tokenId: + 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', + tokenType: { + protocol: 'ALP', + type: 'ALP_TOKEN_TYPE_STANDARD', + number: 0, + }, + txType: 'GENESIS', + isInvalid: false, + burnSummary: '', + failedColorings: [], + actualBurnAmount: '0', + intentionalBurn: '0', + burnsMintBatons: false, + }, + ], + tokenFailedParsings: [], + tokenStatus: 'TOKEN_STATUS_NORMAL', + block: { + height: 795680, + hash: '00000000000000000b7e89959ee52ca1cd691e1fc3b4891c1888f84261c83e73', + timestamp: 1686305735, + }, + }, + calculated: { + genesisSupply: '0.0000', + genesisOutputScripts: [], + genesisMintBatons: 1, + }, + }, +}; +// Build a mock token cache from these chronik mocks +const supportingTokenCache = new CashtabCache().tokens; +for (const tokenId of Object.keys(txHistorySupportingTokenCache)) { + const { token, calculated } = txHistorySupportingTokenCache[tokenId]; + const { timeFirstSeen, genesisInfo, tokenType } = token; + const { genesisSupply, genesisOutputScripts, genesisMintBatons } = + calculated; + const cachedInfo = { + timeFirstSeen, + genesisInfo, + tokenType, + genesisSupply, + genesisOutputScripts, + genesisMintBatons, + }; + if ('block' in token) { + cachedInfo.block = token.block; + } + supportingTokenCache.set(tokenId, cachedInfo); +} +export const mockTxHistorySupportingTokenCache = supportingTokenCache; + /** * getTxHistory mocks * Mock a wallet with tx history at two different paths to confirm expected behavior @@ -18153,17 +18693,10 @@ timestamp: 1710799378, }, parsed: { - incoming: false, - xecAmount: 11, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', - aliasFlag: false, + recipients: ['ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj'], + satoshisSent: 1100, + stackArray: [], + xecTxType: 'Sent', }, }, { @@ -18212,17 +18745,10 @@ timestamp: 1710799378, }, parsed: { - incoming: true, - xecAmount: 33, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', - aliasFlag: false, + recipients: ['ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj'], + satoshisSent: 3300, + stackArray: [], + xecTxType: 'Received', }, }, { @@ -18271,17 +18797,10 @@ timestamp: 1710799378, }, parsed: { - incoming: false, - xecAmount: 11, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', - aliasFlag: false, + recipients: ['ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj'], + satoshisSent: 1100, + stackArray: [], + xecTxType: 'Sent', }, }, { @@ -18330,17 +18849,10 @@ timestamp: 1710799378, }, parsed: { - incoming: true, - xecAmount: 11, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', - aliasFlag: false, + recipients: ['ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj'], + satoshisSent: 1100, + stackArray: [], + xecTxType: 'Received', }, }, { @@ -18480,37 +18992,17 @@ timestamp: 1710799378, }, parsed: { - incoming: true, - xecAmount: 5.46, - isEtokenTx: true, - etokenAmount: '1', - isTokenBurn: false, - tokenEntries: [ - { - tokenId: - '3fee3384150b030490b7bee095a63900f66a45f2d8e3002ae2cf17ce3ef4d109', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'SEND', - isInvalid: false, - burnSummary: '', - failedColorings: [], - actualBurnAmount: '0', - intentionalBurn: '0', - burnsMintBatons: false, - }, + recipients: ['ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj'], + satoshisSent: 546, + stackArray: [ + '534c5000', + '01', + '53454e44', + '3fee3384150b030490b7bee095a63900f66a45f2d8e3002ae2cf17ce3ef4d109', + '0000000000000001', + '0000000000000008', ], - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', - assumedTokenDecimals: false, + xecTxType: 'Received', }, }, { @@ -18634,37 +19126,17 @@ timestamp: 1708821393, }, parsed: { - incoming: false, - xecAmount: 5.46, - isEtokenTx: true, - etokenAmount: '33', - isTokenBurn: false, - tokenEntries: [ - { - tokenId: - '20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8', - tokenType: { - protocol: 'SLP', - type: 'SLP_TOKEN_TYPE_FUNGIBLE', - number: 1, - }, - txType: 'SEND', - isInvalid: false, - burnSummary: '', - failedColorings: [], - actualBurnAmount: '0', - intentionalBurn: '0', - burnsMintBatons: false, - }, + recipients: ['ecash:qphlhe78677sz227k83hrh542qeehh8el5lcjwk72y'], + satoshisSent: 546, + stackArray: [ + '534c5000', + '01', + '53454e44', + '20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8', + '00000007aef40a00', + '00038d5fad5b8e00', ], - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qqxefwshnmppcsjp0fc6w7rnkdsexc7cagdus7ugd0', - assumedTokenDecimals: false, + xecTxType: 'Sent', }, }, { @@ -18713,17 +19185,10 @@ timestamp: 1705492712, }, parsed: { - incoming: false, - xecAmount: 1003.83, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qqxefwshnmppcsjp0fc6w7rnkdsexc7cagdus7ugd0', - aliasFlag: false, + recipients: ['ecash:qr6mxvfp2hlr0qg5phhqapqz8ajv7uaxk55z9332rl'], + satoshisSent: 100383, + stackArray: [], + xecTxType: 'Sent', }, }, { @@ -18772,17 +19237,10 @@ timestamp: 1705365441, }, parsed: { - incoming: false, - xecAmount: 1010.53, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qqxefwshnmppcsjp0fc6w7rnkdsexc7cagdus7ugd0', - aliasFlag: false, + recipients: ['ecash:qpp6zklxvwrqynkhlp75qszgcw0mdu8uuu55gjkvax'], + satoshisSent: 101053, + stackArray: [], + xecTxType: 'Sent', }, }, { @@ -18831,17 +19289,10 @@ timestamp: 1705365441, }, parsed: { - incoming: false, - xecAmount: 1011.32, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qqxefwshnmppcsjp0fc6w7rnkdsexc7cagdus7ugd0', - aliasFlag: false, + recipients: ['ecash:qpp6zklxvwrqynkhlp75qszgcw0mdu8uuu55gjkvax'], + satoshisSent: 101132, + stackArray: [], + xecTxType: 'Sent', }, }, { @@ -18890,17 +19341,10 @@ timestamp: 1705365441, }, parsed: { - incoming: false, - xecAmount: 1012.15, - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: '', - isCashtabMessage: false, - isEcashChatMessage: false, - isEncryptedMessage: false, - replyAddress: 'ecash:qqxefwshnmppcsjp0fc6w7rnkdsexc7cagdus7ugd0', - aliasFlag: false, + recipients: ['ecash:qpp6zklxvwrqynkhlp75qszgcw0mdu8uuu55gjkvax'], + satoshisSent: 101215, + stackArray: [], + xecTxType: 'Sent', }, }, ]; @@ -18908,15 +19352,5 @@ const tokenInfoErrorParsedTxHistory = JSON.parse( JSON.stringify(expectedParsedTxHistory), ); -for (const tx of tokenInfoErrorParsedTxHistory) { - if ('assumedTokenDecimals' in tx.parsed) { - // If we had cached token info before, we do not have it now - tx.parsed.assumedTokenDecimals = true; - } - if (tx.parsed.etokenAmount === '33') { - // Update amount for the one assumed tx where decimals is not really 0 - tx.parsed.etokenAmount = '33000000000'; - } -} export const noCachedInfoParsedTxHistory = tokenInfoErrorParsedTxHistory; diff --git a/cashtab/src/chronik/fixtures/vectors.js b/cashtab/src/chronik/fixtures/vectors.js --- a/cashtab/src/chronik/fixtures/vectors.js +++ b/cashtab/src/chronik/fixtures/vectors.js @@ -20,10 +20,8 @@ incomingEncryptedMsg, tokenBurn, tokenBurnDecimals, - incomingEtokenTwo, swapTx, mockSwapWallet, - aliasOffSpec, PayButtonNoDataYesNonce, PayButtonYesDataYesNonce, PayButtonBadVersion, @@ -32,6 +30,7 @@ PayButtonYesDataNoNonce, MsgFromElectrum, MsgFromEcashChat, + unknownAppTx, mockFlatTxHistoryNoUnconfirmed, mockSortedTxHistoryNoUnconfirmed, mockFlatTxHistoryWithUnconfirmed, @@ -39,10 +38,10 @@ mockFlatTxHistoryWithAllUnconfirmed, mockSortedFlatTxHistoryWithAllUnconfirmed, AlpTx, - mockParseTxTokenCache, SlpV1Mint, } from './mocks'; import { mockChronikUtxos, mockOrganizedUtxosByType } from './chronikUtxos'; +import { getHashes } from 'wallet'; export default { parseTx: { @@ -50,208 +49,153 @@ { description: 'Staking rewards coinbase tx', tx: stakingRwd.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWallet), parsed: stakingRwd.parsed, }, { description: 'Incoming XEC tx', tx: incomingXec.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWallet), parsed: incomingXec.parsed, }, { description: 'Outgoing XEC tx', tx: outgoingXec.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWallet), parsed: outgoingXec.parsed, }, { description: 'Alias registration', tx: aliasRegistration.tx, - wallet: mockAliasWallet, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockAliasWallet), parsed: aliasRegistration.parsed, }, { description: 'Incoming eToken', tx: incomingEtoken.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWallet), parsed: incomingEtoken.parsed, }, - { - description: - 'Incoming eToken that for some reason does not get cached', - tx: incomingEtoken.tx, - wallet: mockParseTxWallet, - cachedTokens: new Map(), - parsed: { - ...incomingEtoken.parsed, - assumedTokenDecimals: true, - }, - }, { description: 'Outgoing eToken', tx: outgoingEtoken.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWallet), parsed: outgoingEtoken.parsed, }, { description: 'Genesis tx', tx: genesisTx.tx, - wallet: mockParseTxWalletAirdrop, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWalletAirdrop), parsed: genesisTx.parsed, }, { description: 'Incoming eToken tx with 9 decimals', tx: incomingEtokenNineDecimals.tx, - wallet: mockParseTxWalletAirdrop, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWalletAirdrop), parsed: incomingEtokenNineDecimals.parsed, }, { description: 'Legacy airdrop tx', tx: legacyAirdropTx.tx, - wallet: mockParseTxWalletAirdrop, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWalletAirdrop), parsed: legacyAirdropTx.parsed, }, { description: 'Outgoing encrypted msg (deprecated)', tx: outgoingEncryptedMsg.tx, - wallet: mockParseTxWalletEncryptedMsg, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWalletEncryptedMsg), parsed: outgoingEncryptedMsg.parsed, }, { description: 'Incoming encrypted msg (deprecated)', tx: incomingEncryptedMsg.tx, - wallet: mockParseTxWalletEncryptedMsg, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWalletEncryptedMsg), parsed: incomingEncryptedMsg.parsed, }, { description: 'Token burn tx', tx: tokenBurn.tx, - wallet: mockParseTxWalletAirdrop, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWalletAirdrop), parsed: tokenBurn.parsed, }, { description: 'Token burn tx with decimals', tx: tokenBurnDecimals.tx, - wallet: mockParseTxWalletAirdrop, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockParseTxWalletAirdrop), parsed: tokenBurnDecimals.parsed, }, - { - description: 'Incoming eToken tx less than zero with decimals', - tx: incomingEtokenTwo.tx, - wallet: mockParseTxWalletAirdrop, - cachedTokens: mockParseTxTokenCache, - parsed: incomingEtokenTwo.parsed, - }, { description: 'SWaP tx', tx: swapTx.tx, - wallet: mockSwapWallet, - cachedTokens: mockParseTxTokenCache, + hashes: getHashes(mockSwapWallet), parsed: swapTx.parsed, }, - { - description: 'Pre-spec alias registration (now off spec)', - tx: aliasOffSpec.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, - parsed: aliasOffSpec.parsed, - }, { description: 'PayButton tx with no data and payment id', tx: PayButtonNoDataYesNonce.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: ['f66d2760b20dc7a47d9cf1a2b2f49749bf7093f6'], parsed: PayButtonNoDataYesNonce.parsed, }, { description: 'PayButton tx with data and payment id', tx: PayButtonYesDataYesNonce.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: ['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc'], parsed: PayButtonYesDataYesNonce.parsed, }, { description: 'PayButton tx with no data and no payment id', tx: PayButtonEmpty.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: ['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc'], parsed: PayButtonEmpty.parsed, }, { description: 'PayButton tx with data and no payment id', tx: PayButtonYesDataNoNonce.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: ['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc'], parsed: PayButtonYesDataNoNonce.parsed, }, { description: 'PayButton tx with unsupported version number', tx: PayButtonBadVersion.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: ['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc'], parsed: PayButtonBadVersion.parsed, }, { description: 'Paybutton tx that does not have spec number of pushes', tx: PayButtonOffSpec.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: ['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc'], parsed: PayButtonOffSpec.parsed, }, { description: 'External msg received from Electrum', tx: MsgFromElectrum.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: ['4e532257c01b310b3b5c1fd947c79a72addf8523'], parsed: MsgFromElectrum.parsed, }, { - description: - 'Before adding support for tokens other than SLPV1, an ALP tx is parsed as an eCash tx', + description: 'Unknown app tx', + tx: unknownAppTx.tx, + hashes: ['d18b7b500f17c5db64303fec630f9dbb85aa9596'], + parsed: unknownAppTx.parsed, + }, + { + description: 'We can parse a received ALP tx', tx: AlpTx.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + // Mock this as a received tx + hashes: [AlpTx.tx.outputs[1].outputScript], parsed: AlpTx.parsed, }, { description: 'External msg received from eCash Chat', tx: MsgFromEcashChat.tx, - wallet: mockParseTxWallet, - cachedTokens: mockParseTxTokenCache, + hashes: ['0b7d35fda03544a08e65464d54cfae4257eb6db7'], parsed: MsgFromEcashChat.parsed, }, { description: 'slp v1 mint tx', tx: SlpV1Mint.tx, - wallet: { - ...mockParseTxWallet, - paths: new Map([ - [ - 1899, - { - address: - 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', - hash: '95e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d', - }, - ], - ]), - }, - cachedTokens: mockParseTxTokenCache, + hashes: ['95e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d'], parsed: SlpV1Mint.parsed, }, ], diff --git a/cashtab/src/chronik/index.js b/cashtab/src/chronik/index.js --- a/cashtab/src/chronik/index.js +++ b/cashtab/src/chronik/index.js @@ -2,13 +2,15 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -import { BN } from 'slp-mdm'; -import { getHashArrayFromWallet } from 'utils/cashMethods'; import { opReturn as opreturnConfig } from 'config/opreturn'; import { chronik as chronikConfig } from 'config/chronik'; import { getStackArray } from 'ecash-script'; import cashaddr from 'ecashaddrjs'; -import { toXec, decimalizeTokenAmount, undecimalizeTokenAmount } from 'wallet'; +import { + getHashes, + decimalizeTokenAmount, + undecimalizeTokenAmount, +} from 'wallet'; export const getTxHistoryPage = async (chronik, hash160, page = 0) => { let txHistoryPage; @@ -195,373 +197,122 @@ * @param {Map} cachedTokens * @returns */ -export const parseTx = (tx, wallet, cachedTokens) => { - const walletHash160s = getHashArrayFromWallet(wallet); - const { inputs, outputs, tokenEntries } = tx; +export const parseTx = (tx, hashes) => { + const { inputs, outputs, isCoinbase } = tx; // Assign defaults let incoming = true; - let satoshis = 0; - let etokenAmount = new BN(0); - let isTokenBurn = false; - - let isSlpV1 = false; - let isUnsupportedTokenTx = false; - let isGenesisTx = false; - let isMintTx = false; - - // For now, we only support one token per tx - // Get the tokenId if this is an slpv1 tx - // TODO support multiple token actions in a tx - let tokenId = ''; - - // Iterate over tokenEntries to parse for token status - for (const entry of tokenEntries) { - if ( - entry.tokenType?.protocol === 'SLP' && - entry.tokenType?.number === 1 && - typeof entry.tokenId !== 'undefined' - ) { - isSlpV1 = true; - tokenId = entry.tokenId; - // Check for token burn - if (entry.burnSummary !== '' && entry.actualBurnAmount !== '') { - isTokenBurn = true; - etokenAmount = new BN(entry.actualBurnAmount); - } - } - if (entry.txType === 'GENESIS') { - isGenesisTx = true; - } - if (entry.txType === 'MINT') { - isMintTx = true; - } - } + let stackArray = []; - // We might lose this variable. Cashtab only supports SLP type 1 txs for now. - // Will be easy to tell if it's "any token" by modifying this definition, e.g. isSlpV1 | isAlp - let isEtokenTx = isSlpV1; - isUnsupportedTokenTx = tokenEntries.length > 0 && !isEtokenTx; - - // Initialize required variables - let airdropFlag = false; - let airdropTokenId = ''; - let opReturnMessage = ''; - let isCashtabMessage = false; - let isEncryptedMessage = false; - let isEcashChatMessage = false; - let replyAddress = ''; - let aliasFlag = false; - - if (tx.isCoinbase) { - // Note that coinbase inputs have `undefined` for `thisInput.outputScript` - incoming = true; - replyAddress = 'N/A'; - } else { - // Iterate over inputs to see if this is an incoming tx (incoming === true) - for (let i = 0; i < inputs.length; i += 1) { - const thisInput = inputs[i]; - - /* - - Assume the first input is the originating address - - https://en.bitcoin.it/wiki/Script for reference - - Assume standard pay-to-pubkey-hash tx - scriptPubKey: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG - 76 + a9 + 14 = OP_DUP + OP_HASH160 + 14 Bytes to push - 88 + ac = OP_EQUALVERIFY + OP_CHECKSIG - - So, the hash160 we want will be in between '76a914' and '88ac' - ...most of the time ;) - */ - - // Since you may have more than one address in inputs, assume the first one is the replyAddress - if (i === 0) { - try { - replyAddress = cashaddr.encodeOutputScript( - thisInput.outputScript, - ); - } catch (err) { - console.error( - `Error from cashaddr.encodeOutputScript(${thisInput.outputScript})`, - err, - ); - // If the transaction is nonstandard, don't worry about a reply address for now - replyAddress = 'N/A'; - } - } + // If it is not an incoming tx, make an educated guess about what addresses were sent to + const destinationAddresses = new Set(); - for (let j = 0; j < walletHash160s.length; j += 1) { - const thisWalletHash160 = walletHash160s[j]; - if ( - typeof thisInput.outputScript !== 'undefined' && - thisInput.outputScript.includes(thisWalletHash160) && - !isMintTx - ) { - // Then this is an outgoing tx - // TODO not really, it could be a self-send tx like a genesis or mint tx - // Parse better in refactor - incoming = false; - // Break out of this for loop once you know this is an outgoing tx - break; - } + // Iterate over inputs to see if this is an incoming tx (incoming === true) + for (const input of inputs) { + for (const hash of hashes) { + if ( + typeof input.outputScript !== 'undefined' && + input.outputScript.includes(hash) + ) { + // Then this is an outgoing tx + // Note: if the outputs also only send to inputs, then it is actually an incoming "self-send" tx + // For the purposes of rendering Cashtab tx history, self send txs are considered !incoming + incoming = false; + + // Break out of this for loop once you know this is (probably) an outgoing tx + break; } } } // Iterate over outputs to get the amount sent - for (let i = 0; i < outputs.length; i += 1) { - const thisOutput = outputs[i]; - const thisOutputReceivedAtHash160 = thisOutput.outputScript; - - if ( - thisOutputReceivedAtHash160.startsWith( - opreturnConfig.opReturnPrefixHex, - ) && - !isUnsupportedTokenTx - ) { - // If this is an OP_RETURN output, parse it - const stackArray = getStackArray(thisOutputReceivedAtHash160); - - const lokad = stackArray[0]; - switch (lokad) { - case opreturnConfig.appPrefixesHex.eToken: { - // Do not set opReturnMsg for etoken txs - break; - } - case opreturnConfig.appPrefixesHex.airdrop: { - // this is to facilitate special Cashtab-specific cases of airdrop txs, both with and without msgs - // The UI via Tx.js can check this airdropFlag attribute in the parsedTx object to conditionally render airdrop-specific formatting if it's true - airdropFlag = true; - // index 0 is drop prefix, 1 is the token Id, 2 is msg prefix, 3 is msg - airdropTokenId = - stackArray.length >= 2 ? stackArray[1] : 'N/A'; - - // Legacy airdrops used to add the Cashtab Msg lokad before a msg - if (stackArray.length >= 3) { - // If there are pushes beyond the token id, we have a msg - isCashtabMessage = true; - if ( - stackArray[2] === - opreturnConfig.appPrefixesHex.cashtab && - stackArray.length >= 4 - ) { - // Legacy airdrops also pushed hte cashtab msg lokad before the msg - opReturnMessage = Buffer.from( - stackArray[3], - 'hex', - ).toString(); - } else { - opReturnMessage = Buffer.from( - stackArray[2], - 'hex', - ).toString(); - } - } - break; - } - case opreturnConfig.appPrefixesHex.cashtab: { - isCashtabMessage = true; - if (stackArray.length >= 2) { - opReturnMessage = Buffer.from( - stackArray[1], - 'hex', - ).toString(); - } else { - opReturnMessage = 'off-spec Cashtab Msg'; - } - break; - } - case opreturnConfig.appPrefixesHex.cashtabEncrypted: { - // Encrypted Cashtab msgs are deprecated, set a standard msg - isCashtabMessage = true; - isEncryptedMessage = true; - opReturnMessage = 'Encrypted Cashtab Msg'; - break; - } - case opreturnConfig.appPrefixesHex.aliasRegistration: { - aliasFlag = true; - if (stackArray.length >= 3) { - opReturnMessage = Buffer.from( - stackArray[2], - 'hex', - ).toString(); - } else { - opReturnMessage = 'off-spec alias registration'; - } - break; - } - case opreturnConfig.appPrefixesHex.paybutton: { - // Paybutton tx - // For now, Cashtab only supports version 0 PayButton txs - // ref doc/standards/paybutton.md - // https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/doc/standards/paybutton.md - - // - - if (stackArray.length !== 4) { - opReturnMessage = 'off-spec PayButton tx'; - break; - } - if (stackArray[1] !== '00') { - opReturnMessage = `Unsupported version PayButton tx: ${stackArray[1]}`; - break; - } - const dataHex = stackArray[2]; - const nonceHex = stackArray[3]; - - opReturnMessage = `PayButton${ - nonceHex !== '00' ? ` (${nonceHex})` : '' - }${ - dataHex !== '00' - ? `: ${Buffer.from(dataHex, 'hex').toString()}` - : '' - }`; - break; - } - case opreturnConfig.appPrefixesHex.eCashChat: { - isEcashChatMessage = true; - if (stackArray.length >= 2) { - opReturnMessage = Buffer.from( - stackArray[1], - 'hex', - ).toString(); - } else { - opReturnMessage = 'off-spec eCash Chat Msg'; - } - break; - } - default: { - // Unrecognized lokad - // In this case, utf8 decode the stack array - - const decodedStackArray = []; - for (const hexStr of stackArray) { - decodedStackArray.push( - Buffer.from(hexStr, 'hex').toString(), - ); - } - - // join with space - opReturnMessage = decodedStackArray.join(' '); - - break; - } - } - // Continue to the next output, we do not need to parse values for OP_RETURN outputs + let change = 0; + let outputSatoshis = 0; + let receivedSatoshis = 0; + // A selfSendTx only sends to outputs in the wallet + let selfSendTx = true; + for (const output of outputs) { + const { outputScript, value } = output; + // outputSatoshis will have the total satoshis of all outputs + outputSatoshis += value; + if (outputScript.startsWith(opreturnConfig.opReturnPrefixHex)) { + // If this is an OP_RETURN output, get stackArray to store in parsed + // Note: we are assuming the tx only has one OP_RETURN output + // If it has more than one, then we will only parse the stackArray of the highest-index OP_RETURN + // For now, there is no need to handle the edge case of multiple OP_RETURNS in tx parsing + stackArray = getStackArray(outputScript); + + // Continue to the next output, we do not parse value for OP_RETURN outputs continue; } // Find amounts at your wallet's addresses - for (let j = 0; j < walletHash160s.length; j += 1) { - const thisWalletHash160 = walletHash160s[j]; - if (thisOutputReceivedAtHash160.includes(thisWalletHash160)) { + let walletIncludesThisOutputScript = false; + for (const hash of hashes) { + if (outputScript.includes(hash)) { + walletIncludesThisOutputScript = true; // If incoming tx, this is amount received by the user's wallet // if outgoing tx (incoming === false), then this is a change amount - const thisOutputAmount = thisOutput.value; - satoshis = incoming - ? satoshis + thisOutputAmount - : satoshis - thisOutputAmount; - - // Parse token qty if token tx - // Note: edge case this is a token tx that sends XEC to Cashtab recipient but token somewhere else - if (isEtokenTx && !isTokenBurn) { - try { - const thisEtokenAmount = new BN( - thisOutput.token.amount, - ); - - etokenAmount = - incoming || isGenesisTx - ? etokenAmount.plus(thisEtokenAmount) - : etokenAmount.minus(thisEtokenAmount); - } catch (err) { - // edge case described above; in this case there is zero eToken value for this Cashtab recipient in this output, so add 0 - etokenAmount.plus(new BN(0)); - } - } + change += value; + receivedSatoshis += value; } } - // Output amounts not at your wallet are sent amounts if !incoming - // Exception for eToken genesis transactions - if (!incoming) { - const thisOutputAmount = thisOutput.value; - satoshis = satoshis + thisOutputAmount; - if (isEtokenTx && !isGenesisTx && !isTokenBurn) { - try { - const thisEtokenAmount = new BN(thisOutput.token.amount); - etokenAmount = etokenAmount.plus(thisEtokenAmount); - } catch (err) { - // NB the edge case described above cannot exist in an outgoing tx - // because the eTokens sent originated from this wallet - } + if (!walletIncludesThisOutputScript) { + // See if this output script is a p2pkh or p2sh ecash address + try { + const destinationAddress = + cashaddr.encodeOutputScript(outputScript); + destinationAddresses.add(destinationAddress); + } catch (err) { + // Do not render non-address recipients in tx history } - } - } - /* If it's an eToken tx that - - did not send any eTokens to the receiving Cashtab wallet - - did send XEC to the receiving Cashtab wallet - Parse it as an XEC received tx - This type of tx is created by this swap wallet. More detailed parsing to be added later as use case is better understood - https://www.youtube.com/watch?v=5EFWXHPwzRk - */ - if (isEtokenTx && etokenAmount.isEqualTo(0)) { - isEtokenTx = false; - opReturnMessage = ''; + // If any output is at an outputScript that is not included in this wallet + // Then it is not a self-send tx + selfSendTx = false; + } } - // Convert from sats to XEC - const xecAmount = toXec(satoshis); - - // Get decimal info for correct etokenAmount - let assumedTokenDecimals = false; - if (isEtokenTx) { - // Parse with decimals = 0 if you do not have this token cached for some reason - // Acceptable error rendering in tx history - let decimals = 0; - const cachedTokenInfo = cachedTokens.get(tokenId); - if (typeof cachedTokenInfo !== 'undefined') { - decimals = cachedTokenInfo.genesisInfo.decimals; + const satoshisSent = selfSendTx + ? outputSatoshis + : isCoinbase + ? change + : incoming + ? receivedSatoshis + : outputSatoshis - change; + + let xecTxType = incoming ? 'Received' : 'Sent'; + + // Parse for tx label + if (isCoinbase) { + // Note, staking rewards activated at blockheight 818670 + // For now, we assume Cashtab is parsing txs after this height + const STAKING_REWARDS_FACTOR = 0.1; // i.e. 10% + // In practice, the staking reward will almost always be the one that is exactly 10% of totalCoinbaseSats + // Use a STAKING_REWARDS_PADDING range to exclude miner and ifp outputs + const STAKING_REWARDS_PADDING = 0.01; + if ( + satoshisSent >= + Math.floor( + (STAKING_REWARDS_FACTOR - STAKING_REWARDS_PADDING) * + outputSatoshis, + ) && + satoshisSent <= + Math.floor( + (STAKING_REWARDS_FACTOR + STAKING_REWARDS_PADDING) * + outputSatoshis, + ) + ) { + xecTxType = 'Staking Reward'; } else { - assumedTokenDecimals = true; + // We do not specifically parse for IFP reward vs miner reward + xecTxType = 'Coinbase Reward'; } - etokenAmount = etokenAmount.shiftedBy(-1 * decimals); - } - etokenAmount = etokenAmount.toString(); - - // Return eToken specific fields if eToken tx - if (isEtokenTx) { - return { - incoming, - xecAmount, - isEtokenTx, - etokenAmount, - isTokenBurn, - tokenEntries: tx.tokenEntries, - airdropFlag, - airdropTokenId, - opReturnMessage: '', - isCashtabMessage, - isEcashChatMessage, - isEncryptedMessage, - replyAddress, - assumedTokenDecimals, - }; } - // Otherwise do not include these fields + return { - incoming, - xecAmount, - isEtokenTx, - airdropFlag, - airdropTokenId, - opReturnMessage, - isCashtabMessage, - isEcashChatMessage, - isEncryptedMessage, - replyAddress, - aliasFlag, + xecTxType, + satoshisSent, + stackArray, + recipients: Array.from(destinationAddresses), }; }; @@ -627,7 +378,7 @@ } } - tx.parsed = parseTx(tx, wallet, cachedTokens); + tx.parsed = parseTx(tx, getHashes(wallet)); history.push(tx); } diff --git a/cashtab/src/components/App/App.js b/cashtab/src/components/App/App.js --- a/cashtab/src/components/App/App.js +++ b/cashtab/src/components/App/App.js @@ -321,6 +321,9 @@ height: auto; flex: 1; } + g { + fill: ${props => props.theme.contrast}; + } p { flex: 2; margin: 0; @@ -457,6 +460,12 @@ height: 33px; width: 30px; } + g { + fill: ${props => props.theme.navActive}; + } + path { + fill: ${props => props.theme.navActive}; + } `; const App = () => { diff --git a/cashtab/src/components/App/__tests__/App.test.js b/cashtab/src/components/App/__tests__/App.test.js --- a/cashtab/src/components/App/__tests__/App.test.js +++ b/cashtab/src/components/App/__tests__/App.test.js @@ -325,26 +325,16 @@ // We see the home container await screen.findByTestId('tx-history-ctn'); - // Open the collapse of this tx in tx history - await user.click( - await screen.findByRole('button', { - name: /Warning: This sender is not in your contact list. Beware of scams./, - }), - { - // https://github.com/testing-library/user-event/issues/922 - pointerEventsCheck: PointerEventsCheckLevel.Never, - }, - ); - - // Get the "Add to contacts" button of tx - const addToContactsBtn = screen.getByTestId('add-to-contacts-btn'); + // Open the collapse of this tx in tx history to see the panel options + // We can click anywhere on this tx + await user.click(screen.getByText('Cashtab Msg')); // Confirm expected initial state of localforage const storedContacts = await localforage.getItem('contactList'); expect(storedContacts).toStrictEqual(null); - // Click the button - await user.click(addToContactsBtn); + // Click the add to contacts button + await user.click(screen.getByTitle('add-contact')); // We see the add contact from tx history modal, prompting for name only input await user.type( @@ -396,26 +386,16 @@ // We see the home container await screen.findByTestId('tx-history-ctn'); - // Open the collapse of this tx in tx history - await user.click( - await screen.findByRole('button', { - name: /Warning: This sender is not in your contact list. Beware of scams./, - }), - { - // https://github.com/testing-library/user-event/issues/922 - pointerEventsCheck: PointerEventsCheckLevel.Never, - }, - ); - - // Get the "Add to contacts" button of tx - const addToContactsBtn = screen.getByTestId('add-to-contacts-btn'); + // Open the collapse of this tx in tx history to see the panel options + // We can click anywhere on this tx + await user.click(screen.getByText('Cashtab Msg')); // Confirm expected initial state of localforage const storedContacts = await localforage.getItem('contactList'); expect(storedContacts).toEqual(initialContactList); - // Click the button - await user.click(addToContactsBtn); + // Click the add to contacts button + await user.click(screen.getByTitle('add-contact')); // We see the add contact from tx history modal, prompting for name only input await user.type( @@ -488,7 +468,7 @@ ).not.toBeInTheDocument(), ); - await user.click(screen.getByTestId('cashtab-msg-reply')); + await user.click(screen.getByTitle('reply')); // Now we see the Send screen expect( diff --git a/cashtab/src/components/App/fixtures/helpers.js b/cashtab/src/components/App/fixtures/helpers.js --- a/cashtab/src/components/App/fixtures/helpers.js +++ b/cashtab/src/components/App/fixtures/helpers.js @@ -319,11 +319,11 @@ tokenIdsToMock.add(utxo.token.tokenId); } for (const tx of wallet.state.parsedTxHistory) { - if (tx.parsed.isEtokenTx) { + if (tx.parsed?.isEtokenTx || tx?.tokenEntries.length > 0) { const tokenId = - 'tokenEntries' in tx.parsed - ? tx.parsed.tokenEntries[0].tokenId - : tx.parsed.slpMeta.tokenId; + 'tokenEntries' in tx + ? tx.tokenEntries[0].tokenId + : tx.slpMeta.tokenId; tokenIdsToMock.add(tokenId); } } diff --git a/cashtab/src/components/App/fixtures/mocks.js b/cashtab/src/components/App/fixtures/mocks.js --- a/cashtab/src/components/App/fixtures/mocks.js +++ b/cashtab/src/components/App/fixtures/mocks.js @@ -1008,10 +1008,10 @@ }, inputScript: '483045022100d8350abb126e2ff6c841dcfb3902b175d46b59f141a23c40deeb7dcac1f219e7022072ee779da16bf15a8032093f03693ea98f2bbc6557dca7b48cf1f308ffb8173a4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1020,10 +1020,10 @@ }, inputScript: '483045022100989f2cd7b8994a0af144a5033d6959779bd7466226901656a35aac231ceb53f602202606fa0f2de1d82abcfac3180c7b111529792243eff23fc6455a29c92532552e4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1100, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1100', - sequenceNo: 4294967295, }, { prevOut: { @@ -1032,10 +1032,10 @@ }, inputScript: '483045022100e09950e7c9a956dd3125f741164802ef7de74a4c8c3c617f1eacdeefd9527ae502202c0cb5f8839ddfb88bf11e5f337cb0220e0228a5a663eb10b22e5567bddb2e404121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 169505, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '169505', - sequenceNo: 4294967295, }, { prevOut: { @@ -1044,10 +1044,10 @@ }, inputScript: '47304402204d2a2e8aec45ba90295fd09661d1504a8f3f0fe2be42b450c67be34212cfacb402201a438dbee9e1da4712885558925a5d49ce600da2ec25fc55240059e65ce193494121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 5500, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '5500', - sequenceNo: 4294967295, }, { prevOut: { @@ -1056,10 +1056,10 @@ }, inputScript: '473044022074585d51f69f3a1afd5df2de6c4d630b6d89861454274909373dfbf5f5a63bc0022010abb2dc05e79505dc054f87467a494c28320a2ae4338f30c10e8f645b06c3784121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1068,10 +1068,10 @@ }, inputScript: '47304402201bb538c5da8e8f4f5fbf5d98b4d8dc8f6870d05e93fed6eead7a535f6b59338302204e0b18a4136fe6842ee01a9439aa8e6b9a089e4116aa4381f0d0281bc853348f4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1080,10 +1080,10 @@ }, inputScript: '483045022100a9c9c91b48f7fdd781cd132e36c84767541ef738e405ea04e71e2f2d4b54165f02206524b3cb82a2f738906dc91a367c994122dff98c1f187e32ec676989ab3a85874121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1092,10 +1092,10 @@ }, inputScript: '473044022048e22c07d4f68235ca8e129c74fabbad7c9b5e17ce36d76d312f4fb08d1496c30220639024f26673e0b46247b419f1605ffea2a195eaa4123b2ab5e3360d926469164121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1104,10 +1104,10 @@ }, inputScript: '47304402200a629b21dbba359dfa5376bc0c64c984041389e2a64c248d4bfe06f3634143860220331d15a53b4313b19c9ad717c33d7f1a7fbb82768545fc225b9bef2d541685084121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 5500, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '5500', - sequenceNo: 4294967295, }, { prevOut: { @@ -1116,10 +1116,10 @@ }, inputScript: '47304402204f6ac84bfb08b9d194c9cffaa1d28cf62d4003c4f2db08c8d9dc23cd2ba2d53b0220605658e5e00499894b74aa6de304c76a6906c66a687541f33f0205403967e04a4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1128,10 +1128,10 @@ }, inputScript: '47304402205126713ba8abf10b9ef384262dc808c23b072df9051b1bd266ead92571a32c7002201a20d649eec51d1f859bd843e3afee4bd15a52c42ae497bb161c34fef6301b174121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 600, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '600', - sequenceNo: 4294967295, }, { prevOut: { @@ -1140,10 +1140,10 @@ }, inputScript: '483045022100a4f9b2e2175b0bbae6c3de2f89b2585ac6c82b63ac55e2bebb6d9b70a72daf1102204c00d2a9b6ced89eadde34980444ffb755b494082c9b11fc6d21c821ee8ba0b04121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1152,10 +1152,10 @@ }, inputScript: '4730440220009194b5caf2d28216f69c0e98b1ee106779a6de485a1ba69dd5fd635d2a23e4022002cfb688783df6b4988309c55eaf31ce346b4b5db9330335f7a3acd3b6f85f514121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1164,10 +1164,10 @@ }, inputScript: '483045022100c4654eada3fdde6a3a3e12c09f40ab30c587ed5daf533a550ac8adba5a3f3089022039c6d938474e81fff50f677a3444f87ef35140ce0d09e7a8ba02ee5b08c667174121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1700, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1700', - sequenceNo: 4294967295, }, { prevOut: { @@ -1176,10 +1176,10 @@ }, inputScript: '4830450221008d98777056f9c8b1bb4e3f2fefdbeca1ac9d65d738276e271334ead8f088de060220606388b42a5e7c3eb590ad840e6596b5e48cf5b418bdfaee47c7b2b87b4082af4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1188,10 +1188,10 @@ }, inputScript: '4730440220468ea9ee32fe1400fffcb6c1a934c3789213e46d16747dc8746f4b9465b11e7c022003192f77afec1bcecf7ad46ddde813301b383b07177440786e9b1bae9946fb3c4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1200,10 +1200,10 @@ }, inputScript: '483045022100ed8bdeedb7decc83d0cf71212a4fe4c21cb4ec091040b27ef15ca716aeb66957022006d12aeb15fc76f15642853dde0b35ae2a5a3efe24cfa275e049fba17690a3914121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1100, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1100', - sequenceNo: 4294967295, }, { prevOut: { @@ -1212,10 +1212,10 @@ }, inputScript: '483045022100a940abcc3bfd26eff3b6d254750ac19361d734a42282164c432d66b62139e8c502207a28c61e435ee42dae5b1964cc457aeeb316717b51ec0cf1af0e1b4e0a5b5a474121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1000, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1000', - sequenceNo: 4294967295, }, { prevOut: { @@ -1224,10 +1224,10 @@ }, inputScript: '47304402204dd6870b836e9482a7b68decdc9447f50a2f9df4f9c4d0b6446e28878386f2c60220654b4131261d41fb5b931a89be777c2716d32b77f0b85ce662ce2cade32ac3264121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1100, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1100', - sequenceNo: 4294967295, }, { prevOut: { @@ -1236,10 +1236,10 @@ }, inputScript: '483045022100f36c72b34d0350bf1bee5acea403981d41307e0da65a7405501c6631e24abc510220389ecf6b00d46e9af27d2e2463d74a2453d14da076ec94df83e2f63bbb430f0d4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1100, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1100', - sequenceNo: 4294967295, }, { prevOut: { @@ -1248,10 +1248,10 @@ }, inputScript: '473044022070d354ca81b378bb1408102755ee19f112dc84e7c97dc44224ce0ec61cf9da5e02200e601eff33e36fc5e96aef74f28d6329201cb8670250fd09501221c67a5eeb034121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1100, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1100', - sequenceNo: 4294967295, }, { prevOut: { @@ -1260,10 +1260,10 @@ }, inputScript: '47304402205ae9a030a48fad096f465f9a2d564d5f88a83b2207e2bcf9f96522a4eeca3927022050fb4fb6ca418b3559dde7cf0f6412f82dbcee7710de2c895b5989fdca75a8f84121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 900, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '900', - sequenceNo: 4294967295, }, { prevOut: { @@ -1272,10 +1272,10 @@ }, inputScript: '47304402207a47b86a75c91022a5f54f2f39d1d2952a8623b8cbdad40ca1d8d25e13f1027502204b5b021a65276886e4f31ce5dfab2eb7979595aecd2b244ca88047453f57e04e4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1100, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1100', - sequenceNo: 4294967295, }, { prevOut: { @@ -1284,10 +1284,10 @@ }, inputScript: '483045022100bf4d11242dd28d5ec4de42388d8fafc6bcaa812a20a728f0e86e5b5e51c0005e022041b6bf9034412e9e2377ebf1ea61ef358c05be1a42829b08032bf03be28c9fa04121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 497990, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '497990', - sequenceNo: 4294967295, }, { prevOut: { @@ -1296,10 +1296,10 @@ }, inputScript: '47304402204f6c5b9e7a8b610bce1eb32c2362fb428137f6797a0a8f463a1043eab51c3dfe02203f5f0c7816573ca5266fb4a5fddb2d29e76d8c79cde8ab8198acaab9ac206dbf4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 700, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '700', - sequenceNo: 4294967295, }, { prevOut: { @@ -1308,10 +1308,10 @@ }, inputScript: '483045022100b13a13af99fcb0946431f7407b35d2ec513c2fc100c6ea5b9c787cd21513461b02207a6549f50789f5027fbef437d7cd74785fbc01f6976eee5b99a60b0515a325ab4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1100, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1100', - sequenceNo: 4294967295, }, { prevOut: { @@ -1320,10 +1320,10 @@ }, inputScript: '473044022029e166c7c52719ae6dd4b0e54102f796041c3dec3edb61a87d6cca8acb31b67302206696df121a4b71251c99c1ad9d80f4acb302d44be2ecd860e76f6241675c50264121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2500, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2500', - sequenceNo: 4294967295, }, { prevOut: { @@ -1332,10 +1332,10 @@ }, inputScript: '483045022100cd99ca822e8cceedff64c67577c4ba55ae1db654c28b9982926269a7b2e1847c022020054e903352d1079c19513d990da774ab12d98860c241c50066bc4f6b37357f4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 5500, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '5500', - sequenceNo: 4294967295, }, { prevOut: { @@ -1344,10 +1344,10 @@ }, inputScript: '47304402200b570f4c81ef54cfc9d77d1fcd615b90063243f99aa665a53a4fa1b6204fb83802200d83088d8dc9690932d4a22e33556221ad1f44dc596b3d31cbff38b2c6a29c0a4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 2200, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '2200', - sequenceNo: 4294967295, }, { prevOut: { @@ -1356,47 +1356,44 @@ }, inputScript: '483045022100dc7147775fd80ccb6e75710ae2f226249d3a99017ddae7a0163900595967765f02205d84c411885d90a31b41360e8b0a8aa0a70be079c5c5a643db238a52d978835e4121024781b5971a20049fa211c364a868d2fa8f258c31bb3738e01957400067eeee0f', + value: 1000000, + sequenceNo: 4294967295, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', - value: '1000000', - sequenceNo: 4294967295, }, ], outputs: [ { - value: '0', + value: 0, outputScript: '6a04007461623a6865726520697320612043617368746162204d736720666f722075736520696e204361736874616220696e746567726174696f6e207465737473', }, { - value: '1000000', + value: 1000000, outputScript: '76a914d32616c8f849d159b0225f36966ccb85d425e68388ac', }, { - value: '713065', + value: 713065, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', + spentBy: { + txid: '4ca77715698d7f5acab6fcd3703cc07d6a760b4cab064a87c03a755bc734e501', + outIdx: 41, + }, }, ], lockTime: 0, - timeFirstSeen: '1707161386', + timeFirstSeen: 0, size: 4576, isCoinbase: false, - network: 'XEC', - parsed: { - incoming: true, - xecAmount: '10000', - isEtokenTx: false, - airdropFlag: false, - airdropTokenId: '', - opReturnMessage: - 'here is a Cashtab Msg for use in Cashtab integration tests', - isCashtabMessage: true, - isEncryptedMessage: false, - replyAddress: - 'ecash:qphlhe78677sz227k83hrh542qeehh8el5lcjwk72y', - aliasFlag: false, + tokenEntries: [], + tokenFailedParsings: [], + tokenStatus: 'TOKEN_STATUS_NON_TOKEN', + block: { + height: 830308, + hash: '000000000000000007f40dd57695bb3e592e9769bd74fbe0fe9c8c09db32da61', + timestamp: 1707162498, }, }, ], diff --git a/cashtab/src/components/Common/CustomIcons.js b/cashtab/src/components/Common/CustomIcons.js --- a/cashtab/src/components/Common/CustomIcons.js +++ b/cashtab/src/components/Common/CustomIcons.js @@ -16,13 +16,25 @@ FireOutlined, UserAddOutlined, WarningOutlined, - SwapOutlined, AppstoreAddOutlined, GithubOutlined, } from '@ant-design/icons'; import { Image } from 'antd'; +import PayButton from 'assets/paybutton.webp'; import { ReactComponent as QRCode } from 'assets/qrcode.svg'; import { ReactComponent as Send } from 'assets/send.svg'; +import { ReactComponent as CopyPaste } from 'assets/copypaste.svg'; +import { ReactComponent as AddContact } from 'assets/addcontact.svg'; +import { ReactComponent as Unknown } from 'assets/unknown.svg'; +import { ReactComponent as Reply } from 'assets/reply.svg'; +import { ReactComponent as Mint } from 'assets/mint.svg'; +import { ReactComponent as CashtabMsg } from 'assets/cashtab-msg.svg'; +import { ReactComponent as Chat } from 'assets/chat.svg'; +import { ReactComponent as Mined } from 'assets/pickaxe.svg'; +import { ReactComponent as CashtabEncrypted } from 'assets/cashtab-encrypted.svg'; +import { ReactComponent as TokenBurn } from 'assets/tokenburn.svg'; +import { ReactComponent as Swap } from 'assets/swap.svg'; +import { ReactComponent as Alias } from 'assets/alias.svg'; import { ReactComponent as Receive } from 'assets/receive.svg'; import { ReactComponent as Genesis } from 'assets/flask.svg'; import { ReactComponent as Unparsed } from 'assets/alert-circle.svg'; @@ -228,23 +240,56 @@ ); +const Rotate = styled.div` + transform: rotate(-45deg); +`; +const MineRotate = styled.div` + transform: rotate(45deg); +`; +export const SendIcon = () => ( + + + +); + +export const MinedIcon = () => ( + + + +); +const PayButtonImg = styled.img` + color: transparent; + filter: brightness(0) invert(1); +`; +export const EncryptedMsgIcon = () => ( + tx-encrypted-msg +); +export const TokenBurnIcon = () => ; +export const PayButtonIcon = () => ( + +); +export const ChatIcon = () => tx-chat; +export const MintIcon = () => ; +export const CopyPasteIcon = () => ; +export const AddContactIcon = () => ; +export const ReplyIcon = () => ; +export const UnknownIcon = () => ; +export const CashtabMsgIcon = () => ; -export const ReceiveIcon = () => ; +export const AliasIconTx = () => ; +export const GenesisIcon = () => ; +export const ReceiveIcon = () => ; +export const AirdropIcon = () => ; +export const SwapIcon = () => ; export const QRCodeIcon = () => ; -export const GenesisIcon = () => ; export const UnparsedIcon = () => ; export const HomeIcon = () => ; export const SettingsIcon = () => ; export const WarningIcon = () => ; -export const AirdropIcon = () => ; -export const SwapIcon = () => ; export const EtokensIcon = () => ( ); -export const SendIcon = styled(Send)` - transform: rotate(-35deg); -`; export const AliasRegisterIcon = () => ( ); diff --git a/cashtab/src/components/Configure/Configure.js b/cashtab/src/components/Configure/Configure.js --- a/cashtab/src/components/Configure/Configure.js +++ b/cashtab/src/components/Configure/Configure.js @@ -110,12 +110,6 @@ autoCameraOn: e.target.checked, }); }; - const handleUnknownSenderMsg = e => { - updateCashtabState('settings', { - ...settings, - hideMessagesFromUnknownSenders: e.target.checked, - }); - }; return ( @@ -179,16 +173,6 @@ /> )} - - - Hide msgs from unknown sender - - - {hasEnoughToken( diff --git a/cashtab/src/components/Etokens/TokenIcon.js b/cashtab/src/components/Etokens/TokenIcon.js --- a/cashtab/src/components/Etokens/TokenIcon.js +++ b/cashtab/src/components/Etokens/TokenIcon.js @@ -12,6 +12,7 @@ src={`${tokenConfig.tokenIconsUrl}/${size}/${tokenId}.png`} width={size} height={size} + alt={`icon for ${tokenId}`} /> ); }; diff --git a/cashtab/src/components/Home/Home.js b/cashtab/src/components/Home/Home.js --- a/cashtab/src/components/Home/Home.js +++ b/cashtab/src/components/Home/Home.js @@ -11,6 +11,8 @@ import { getWalletState } from 'utils/cashMethods'; import Receive from 'components/Receive/Receive'; import { Alert } from 'components/Common/Atoms'; +import { getUserLocale } from 'helpers'; +import { getHashes } from 'wallet'; export const Tabs = styled.div` margin: auto; @@ -52,6 +54,9 @@ `; export const TxHistoryCtn = styled.div` + display: flex; + flex-direction: column; + gap: 12px; color: ${props => props.theme.contrast}; margin-top: 24px; `; @@ -120,16 +125,20 @@ ContextValue; const { settings, wallets } = cashtabState; const wallet = wallets.length > 0 ? wallets[0] : false; + const hashes = getHashes(wallet); const walletState = getWalletState(wallet); const { parsedTxHistory } = walletState; const hasHistory = parsedTxHistory && parsedTxHistory.length > 0; + const userLocale = getUserLocale(navigator); + return ( <> {apiError && } {!hasHistory && ( <> diff --git a/cashtab/src/components/Home/Tx.js b/cashtab/src/components/Home/Tx.js deleted file mode 100644 --- a/cashtab/src/components/Home/Tx.js +++ /dev/null @@ -1,1201 +0,0 @@ -// Copyright (c) 2024 The Bitcoin developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; -import PropTypes from 'prop-types'; -import styled from 'styled-components'; -import { - SendIcon, - ReceiveIcon, - GenesisIcon, - UnparsedIcon, - ThemedContactsOutlined, - ThemedBurnOutlined, - AirdropIcon, - AliasIcon, -} from 'components/Common/CustomIcons'; -import { formatBalance, formatDate } from 'utils/formatting'; -import TokenIcon from 'components/Etokens/TokenIcon'; -import { Collapse } from 'antd'; -import CopyToClipboard from 'components/Common/CopyToClipboard'; -import { - ThemedCopySolid, - ThemedLinkSolid, - ThemedPdfSolid, -} from 'components/Common/CustomIcons'; -import { explorer } from 'config/explorer'; -import { supportedFiatCurrencies } from 'config/cashtabSettings'; -import appConfig from 'config/app'; -import Modal from 'components/Common/Modal'; -import { ModalInput } from 'components/Common/Inputs'; -import { toast } from 'react-toastify'; -import { getContactNameError } from 'validation'; - -const TxIcon = styled.div` - svg { - width: 20px; - height: 20px; - } - height: 40px; - width: 40px; - border: 1px solid #fff; - display: flex; - align-items: center; - justify-content: center; - border-radius: 100px; -`; - -const AddToContacts = styled.span` - max-height: 200px; - text-align: left; -`; - -const SentTx = styled(TxIcon)` - svg { - margin-right: -3px; - } - fill: ${props => props.theme.contrast}; -`; -const BurnedTx = styled(TxIcon)` - svg { - margin-right: -3px; - } - border-color: ${props => props.theme.eCashPurple}; -`; -const ReceivedTx = styled(TxIcon)` - svg { - fill: ${props => props.theme.eCashBlue}; - } - border-color: ${props => props.theme.eCashBlue}; -`; - -const AirdropSentTx = styled(TxIcon)` - fill: ${props => props.theme.contrast}; -`; - -const AirdropReceivedTx = styled(TxIcon)` - svg { - fill: ${props => props.theme.eCashBlue}; - } - border-color: ${props => props.theme.eCashBlue}; -`; - -const AirdropTokenInfoCtn = styled.div` - flex-grow: 3; - - h4 { - font-size: 10px; - color: ${props => props.theme.lightWhite}; - margin: 0; - margin-top: 5px; - } -`; - -const GenesisTx = styled(TxIcon)` - border-color: ${props => props.theme.genesisGreen}; - svg { - fill: ${props => props.theme.genesisGreen}; - } -`; -const AliasRegistrationTx = styled(TxIcon)` - border-color: ${props => props.theme.genesisGreen}; - svg { - fill: ${props => props.theme.genesisGreen}; - } -`; -const UnparsedTx = styled(TxIcon)` - color: ${props => props.theme.eCashBlue} !important; -`; -const DateType = styled.div` - text-align: left; - padding: 12px; - @media screen and (max-width: 500px) { - font-size: 0.8rem; - } -`; - -const GenesisHeader = styled.h3``; -const ReceivedHeader = styled.h3``; - -const LeftTextCtn = styled.div` - text-align: left; - display: flex; - align-items: left; - flex-direction: column; - margin-left: 10px; - h3 { - color: ${props => props.theme.contrast}; - font-size: 14px; - font-weight: 700; - margin: 0; - } - ${GenesisHeader} { - color: ${props => props.theme.genesisGreen}; - } - ${ReceivedHeader} { - color: ${props => props.theme.eCashBlue}; - justify-content: left; - } - h4 { - font-size: 12px; - color: ${props => props.theme.lightWhite}; - margin: 0; - } -`; - -const RightTextCtn = styled.div` - text-align: right; - display: flex; - align-items: left; - flex-direction: column; - margin-left: 10px; - h3 { - color: ${props => props.theme.contrast}; - font-size: 14px; - font-weight: 700; - margin: 0; - } - h4 { - font-size: 12px; - color: ${props => props.theme.lightWhite}; - margin: 0; - } -`; - -const OpReturnType = styled.div` - text-align: right; - width: 100%; - padding: 10px; - border-radius: 5px; - background: ${props => props.theme.sentMessage}; - margin-top: 15px; - transition: max-height 500ms cubic-bezier(0, 1, 0, 1); - ${({ hideMessagesFromUnknownSenders }) => - hideMessagesFromUnknownSenders && - ` - max-height: auto; - &[aria-expanded='true'] { - max-height: 5000px; - transition: max-height 500ms ease-in; - } - - &[aria-expanded='false'] { - transition: max-height 200ms ease-in; - max-height: 6rem; - } - `} - - h4 { - color: ${props => props.theme.lightWhite}; - margin: 0; - font-size: 12px; - display: inline-block; - } - p { - color: ${props => props.theme.contrast}; - margin: 0; - font-size: 14px; - width: 100%; - margin-bottom: 10px; - word-break: break-word; - } - a { - color: ${props => props.theme.contrast}; - margin: 0px 0px 0px 5px; - font-size: 10px; - border: 1px solid ${props => props.theme.contrast}; - border-radius: 5px; - padding: 2px 10px; - opacity: 0.6; - } - a:hover { - opacity: 1; - border-color: ${props => props.theme.eCashBlue}; - color: ${props => props.theme.contrast}; - background: ${props => props.theme.eCashBlue}; - } - ${({ received, ...props }) => - received && - ` - text-align: left; - background: ${props.theme.receivedMessage}; - `} -`; - -const ShowHideMessageButton = styled.button` - color: ${props => props.theme.contrast}; - background-color: transparent; - margin: 0px 0px 0px 5px; - font-size: 10px; - border: 1px solid ${props => props.theme.contrast}; - border-radius: 5px; - padding: 1.6px 10px; - opacity: 0.6; - &:hover { - opacity: 1; - border-color: ${props => props.theme.eCashBlue}; - color: ${props => props.theme.contrast}; - background: ${props => props.theme.eCashBlue}; - } -`; -const ReceivedLabel = styled.span` - font-weight: bold; - color: ${props => props.theme.eCashBlue} !important; -`; - -const EncryptionMessageLabel = styled.span` - font-weight: bold; - font-size: 12px; - color: ${props => props.theme.encryptionRed}; - white-space: nowrap; -`; - -const TxInfo = styled.div` - text-align: right; - display: flex; - align-items: left; - flex-direction: column; - margin-left: 10px; - flex-grow: 1; - h3 { - color: ${props => props.theme.contrast}; - font-size: 14px; - font-weight: 700; - margin: 0; - } - - h4 { - font-size: 12px; - color: ${props => props.theme.lightWhite}; - margin: 0; - } - - @media screen and (max-width: 500px) { - font-size: 0.8rem; - } -`; - -const TokenInfo = styled.div` - display: flex; - flex-grow: 1; - justify-content: flex-end; - - color: ${props => - props.outgoing ? props.theme.secondary : props.theme.eCashBlue}; - - @media screen and (max-width: 500px) { - font-size: 0.8rem; - grid-template-columns: 16px auto; - } -`; -const TxTokenIcon = styled.div` - img { - height: 24px; - width: 24px; - } - @media screen and (max-width: 500px) { - img { - height: 16px; - width: 16px; - } - } - grid-column-start: 1; - grid-column-end: span 1; - grid-row-start: 1; - grid-row-end: span 2; - align-self: center; -`; -const TokenTxAmt = styled.h3` - text-align: right; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - -const TokenTxAmtGenesis = styled(TokenTxAmt)` - color: ${props => props.theme.genesisGreen} !important; -`; -const TokenTxAmtReceived = styled(TokenTxAmt)` - color: ${props => props.theme.eCashBlue} !important; -`; - -const TokenName = styled.div` - text-align: right; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 12px; - color: rgba(255, 255, 255, 0.4); - margin: 0px; -`; - -const TxWrapper = styled.div` - display: flex; - align-items: center; - border-top: 1px solid rgba(255, 255, 255, 0.12); - color: ${props => props.theme.contrast}; - padding: 10px 0; - flex-wrap: wrap; - width: 100%; -`; - -const AntdContextCollapseWrapper = styled.div` - .ant-collapse { - border: none !important; - background-color: transparent !important; - } - .ant-collapse-item { - border: none !important; - } - .ant-collapse-header { - padding: 0 !important; - color: ${props => props.theme.forms.text} !important; - } - border-radius: 16px; - .ant-collapse-content-box { - padding-right: 0 !important; - } - - @media screen and (max-width: 500px) { - grid-template-columns: 24px 30% 50%; - } -`; - -const Panel = Collapse.Panel; - -const DropdownIconWrapper = styled.div` - display: flex; - align-items: center; - gap: 4px; -`; - -const TextLayer = styled.div` - font-size: 12px; - color: ${props => props.theme.contrast}; - white-space: nowrap; -`; - -const DropdownButton = styled.button` - display: flex; - justify-content: flex-end; - position: relative; - background-color: ${props => props.theme.walletBackground}; - border: none; - cursor: pointer; - padding: 0; - &:hover { - div { - color: ${props => props.theme.eCashBlue}!important; - } - svg { - fill: ${props => props.theme.eCashBlue}!important; - } - } -`; - -const PanelCtn = styled.div` - display: flex; - align-items: center; - justify-content: flex-end; - right: 0; - gap: 8px; - @media (max-width: 500px) { - flex-wrap: wrap; - } -`; - -const TxLink = styled.a` - color: ${props => props.theme.primary}; -`; - -const NotInContactsAlert = styled.div` - color: ${props => props.theme.forms.error} !important; - font-style: italic; -`; - -const ReceivedFromCtn = styled.div` - display: flex; - align-items: center; - justify-content: left; - gap: 2px; - h4 { - margin-top: 2.5px; - } -`; - -const Tx = ({ - data, - fiatPrice, - fiatCurrency, - cashtabState, - updateCashtabState, -}) => { - const { contactList, settings, cashtabCache } = cashtabState; - const { tokens } = cashtabCache; - - // For now, we only parse tokenEntries[0] - let tokenId, cachedTokenInfo; - if ( - data.parsed.isEtokenTx && - Array.isArray(data.tokenEntries) && - data.tokenEntries.length > 0 - ) { - tokenId = data.parsed.tokenEntries[0].tokenId; - cachedTokenInfo = tokens.get(tokenId); - } - - const [displayedMessage, setDisplayedMessage] = useState(false); - const [showAddNewContactModal, setShowAddNewContactModal] = useState(false); - const emptyFormData = { - newContactName: '', - }; - const emptyFormDataErrors = { - newContactName: false, - }; - - // State variables - const [formData, setFormData] = useState(emptyFormData); - const [formDataErrors, setFormDataErrors] = useState(emptyFormDataErrors); - - /** - * Update formData with user input - * @param {Event} e js input event - * e.target.value will be input value - * e.target.name will be name of originating input field - */ - const handleInput = e => { - const { name, value } = e.target; - - if (name === 'newContactName') { - const contactNameError = getContactNameError(value, contactList); - setFormDataErrors(previous => ({ - ...previous, - [name]: contactNameError, - })); - } - setFormData(previous => ({ - ...previous, - [name]: value, - })); - }; - - const addNewContact = async () => { - const addressToAdd = data.parsed.replyAddress; - - // Check to see if the contact exists - const contactExists = contactList.find( - contact => contact.address === addressToAdd, - ); - - if (typeof contactExists !== 'undefined') { - // Contact exists - // Not expected to ever happen from Tx.js as user should not see option to - // add an existing contact - toast.error(`${addressToAdd} already exists in Contacts`); - } else { - contactList.push({ - name: formData.newContactName, - address: addressToAdd, - }); - // update localforage and state - await updateCashtabState('contactList', contactList); - toast.success( - `${formData.newContactName} (${addressToAdd}) added to Contact List`, - ); - } - - // Reset relevant state fields - setShowAddNewContactModal(false); - - // Clear new contact formData - setFormData(previous => ({ - ...previous, - newContactName: '', - })); - }; - - const handleShowMessage = () => { - setDisplayedMessage(!displayedMessage); - }; - let txDate, txTime; - if (data.timeFirstSeen === 0) { - // If chronik does not have a timeFirstSeen for this tx - if (!('block' in data)) { - // If it is also unconfirmed, we have nothing to go on here - // Do not render txDate or txTime - txDate = false; - txTime = false; - } else { - // If it is confirmed, use the block timestamp - txDate = formatDate(data.block.timestamp, navigator.language); - txTime = new Date( - parseInt(`${data.block.timestamp}000`), - ).toLocaleTimeString(); - } - } else { - // If it is unconfirmed and we have data.timeFirstSeen, use that - txDate = formatDate(data.timeFirstSeen, navigator.language); - txTime = new Date( - parseInt(`${data.timeFirstSeen}000`), - ).toLocaleTimeString(); - } - - const knownContact = contactList.find( - contact => contact.address === data.parsed.replyAddress, - ); - const fromKnownSender = typeof knownContact !== 'undefined'; - - // A wallet migrating from bch-api tx history to chronik will get caught here for one update cycle - let unparsedTx = false; - if (!Object.keys(data).includes('parsed')) { - unparsedTx = true; - } - return ( - <> - {showAddNewContactModal && ( - setShowAddNewContactModal(false)} - showCancelButton - > - - - )} - {unparsedTx ? ( - - - - - - - Unparsed -
- {txDate} - {typeof txTime === 'string' && ` at ${txTime}`} -
- Open in Explorer -
- ) : ( - - - - - {!data.parsed.incoming ? ( - <> - {data.parsed.isEtokenTx && - data.tokenEntries[0].txType === - 'GENESIS' ? ( - - - - ) : data.parsed.isTokenBurn ? ( - - - - ) : ( - <> - {data.parsed - .airdropFlag ? ( - - - - ) : data.parsed - .aliasFlag ? ( - - - - ) : ( - - - - )} - - )} - - ) : ( - <> - {data.parsed.airdropFlag ? ( - - - - ) : data.parsed.isEtokenTx && - data.tokenEntries[0] - .txType === 'MINT' ? ( - - - - ) : ( - - - - )} - - )} - - - {!data.parsed.incoming ? ( - <> - {data.parsed.isEtokenTx && - data.tokenEntries[0] - .txType === - 'GENESIS' ? ( - - Genesis - - ) : ( -

- {data.parsed - .isTokenBurn - ? 'Burned' - : data.parsed - .airdropFlag - ? 'Airdrop' - : data.parsed - .aliasFlag - ? 'Alias' - : 'Sent'} -

- )} - - ) : data.parsed.isEtokenTx && - data.tokenEntries[0].txType === - 'MINT' ? ( - - Mint - - ) : ( - - - {data.parsed.airdropFlag - ? 'Airdrop' - : 'Received'} - - - {fromKnownSender && ( -

- from{' '} - {knownContact.name} -

- )} -
- )} -

- {txDate} - {typeof txTime === 'string' && - ` at ${txTime}`} -

-
- {data.parsed.isEtokenTx ? ( - - {data.parsed.isEtokenTx && - Array.isArray( - data.tokenEntries, - ) && - data.tokenEntries.length > 0 ? ( - <> - - - - {!data.parsed - .incoming ? ( - - {data - .tokenEntries[0] - .txType === - 'GENESIS' ? ( - <> - - +{' '} - {data.parsed.etokenAmount.toString()} -   - {typeof cachedTokenInfo !== - 'undefined' && - cachedTokenInfo - .genesisInfo - .tokenTicker} - - - {typeof cachedTokenInfo !== - 'undefined' && - cachedTokenInfo - .genesisInfo - .tokenName} - - - ) : ( - <> - - -{' '} - {data.parsed.etokenAmount.toString()} -   - {typeof cachedTokenInfo !== - 'undefined' && - cachedTokenInfo - .genesisInfo - .tokenTicker} - - - {typeof cachedTokenInfo !== - 'undefined' && - cachedTokenInfo - .genesisInfo - .tokenName} - - - )} - - ) : ( - - - +{' '} - {data.parsed.etokenAmount.toString()} -   - {typeof cachedTokenInfo !== - 'undefined' && - cachedTokenInfo - .genesisInfo - .tokenTicker} - - - {typeof cachedTokenInfo !== - 'undefined' && - cachedTokenInfo - .genesisInfo - .tokenName} - - - )} - - ) : ( - Token Tx - )} - - ) : ( - <> - {data.parsed.airdropFlag && ( - - - {tokens.has( - data.parsed - .airdropTokenId, - ) && ( -

- { - tokens.get( - data - .parsed - .airdropTokenId, - )[ - 'tokenName' - ] - } -

- )} -
- )} - - {!data.parsed.incoming ? ( - <> -

- - - {formatBalance( - data.parsed - .xecAmount, - )}{' '} - { - appConfig.ticker - } -

- {fiatPrice !== - null && - !isNaN( - data.parsed - .xecAmount, - ) && ( -

- - - { - supportedFiatCurrencies[ - fiatCurrency - ] - .symbol - } - {( - data - .parsed - .xecAmount * - fiatPrice - ).toFixed( - 2, - )}{' '} - { - supportedFiatCurrencies.fiatCurrency - } -

- )} - - ) : ( - <> - - + - {formatBalance( - data.parsed - .xecAmount, - )}{' '} - { - appConfig.ticker - } - - {fiatPrice !== - null && - !isNaN( - data.parsed - .xecAmount, - ) && ( -

- + - { - supportedFiatCurrencies[ - fiatCurrency - ] - .symbol - } - {( - data - .parsed - .xecAmount * - fiatPrice - ).toFixed( - 2, - )}{' '} - { - supportedFiatCurrencies.fiatCurrency - } -

- )} - - )} -
- - )} - {data.parsed.opReturnMessage && ( - <> - - {data.parsed.incoming && - !fromKnownSender && ( - - Warning: This - sender is not in - your contact - list. Beware of - scams. - - )} - {data.parsed - .isCashtabMessage ? ( -

- Cashtab Message{' '} -

- ) : data.parsed - .aliasFlag ? ( -

- Alias Registration -

- ) : data.parsed - .isEcashChatMessage ? ( -

- eCash Chat Message{' '} -

- ) : ( -

- External Message -

- )} - {data.parsed - .isEncryptedMessage ? ( - -  - Encrypted - - ) : ( - '' - )} -
- {settings.hideMessagesFromUnknownSenders ? ( - <> - {/*unencrypted OP_RETURN Message*/} - {data.parsed - .opReturnMessage && - !data.parsed - .isEncryptedMessage && ( - <> - {!displayedMessage && - data - .parsed - .incoming && - !fromKnownSender ? ( - { - e.stopPropagation(); - handleShowMessage(); - }} - > - Show - - ) : ( - <> -

- {' '} - { - data - .parsed - .opReturnMessage - } -

- {!fromKnownSender && - data - .parsed - .incoming && ( - { - e.stopPropagation(); - handleShowMessage(); - }} - > - Hide - - )} - - )} - - )} - {data.parsed - .opReturnMessage && - data.parsed - .isEncryptedMessage && ( - <> - {!displayedMessage && - data - .parsed - .incoming && - !fromKnownSender ? ( - { - e.stopPropagation(); - handleShowMessage(); - }} - > - Show - - ) : ( - <> -

- { - data - .parsed - .opReturnMessage - } -

- - )} - - )} - - ) : ( - <> - {/*unencrypted OP_RETURN Message*/} - {data.parsed - .opReturnMessage && - !data.parsed - .isEncryptedMessage ? ( -

- { - data - .parsed - .opReturnMessage - } -

- ) : ( - '' - )} - - )} - {(!settings.hideMessagesFromUnknownSenders && - data.parsed.incoming && - data.parsed - .replyAddress) || - (settings.hideMessagesFromUnknownSenders && - displayedMessage) || - (fromKnownSender && - data.parsed.incoming && - data.parsed - .replyAddress) ? ( - - Reply - - ) : ( - '' - )} -
- - )} -
- - } - > - - - - - Txid - - - - - - {data.parsed.opReturnMessage && ( - - - - Msg - - - - - )} - - - - - View on e.cash - - - - - - - - - Receipt - - - - - {!!data.parsed.incoming && - data.parsed.replyAddress && - !fromKnownSender && ( - - { - setShowAddNewContactModal( - true, - ); - }} - > - - - Add to contacts - - - - - - )} - -
-
-
- )} - - ); -}; - -Tx.propTypes = { - data: PropTypes.object, - fiatPrice: PropTypes.number, - fiatCurrency: PropTypes.string, - cashtabState: PropTypes.shape({ - contactList: PropTypes.arrayOf( - PropTypes.shape({ - address: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }), - ), - settings: PropTypes.shape({ - fiatCurrency: PropTypes.string.isRequired, - sendModal: PropTypes.bool.isRequired, - autoCameraOn: PropTypes.bool.isRequired, - hideMessagesFromUnknownSenders: PropTypes.bool.isRequired, - balanceVisible: PropTypes.bool.isRequired, - minFeeSends: PropTypes.bool.isRequired, - }), - cashtabCache: PropTypes.shape({ - tokens: PropTypes.object.isRequired, - }), - }), - updateCashtabState: PropTypes.func, -}; - -export default Tx; diff --git a/cashtab/src/components/Home/Tx/__tests__/index.test.js b/cashtab/src/components/Home/Tx/__tests__/index.test.js new file mode 100644 --- /dev/null +++ b/cashtab/src/components/Home/Tx/__tests__/index.test.js @@ -0,0 +1,1969 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { theme } from 'assets/styles/theme'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Tx from 'components/Home/Tx'; +import { + incomingXec, + outgoingXec, + stakingRwd, + aliasRegistration, + mockParseTxWallet, + mockAliasWallet, + incomingEtoken, + outgoingEtoken, + genesisTx, + incomingEtokenNineDecimals, + legacyAirdropTx, + onSpecAirdropTxNoMsg, + outgoingEncryptedMsg, + incomingEncryptedMsg, + tokenBurn, + tokenBurnDecimals, + swapTx, + PayButtonNoDataYesNonce, + PayButtonYesDataYesNonce, + PayButtonEmpty, + PayButtonYesDataNoNonce, + PayButtonOffSpec, + PayButtonBadVersion, + MsgFromEcashChat, + SlpV1Mint, + MsgFromElectrum, + unknownAppTx, + AlpTx, + CashtabMsg, + mockParseTxWalletAirdrop, + mockParseTxWalletEncryptedMsg, + mockParseTxTokenCache, + mockLargeTokenCache, + mockTxHistorySupportingTokenCache, +} from 'chronik/fixtures/mocks'; +import CashtabState from 'config/CashtabState'; +import { MemoryRouter } from 'react-router-dom'; +import { getHashes } from 'wallet'; + +// https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +// https://stackoverflow.com/questions/64813447/cannot-read-property-addlistener-of-undefined-react-testing-library +window.matchMedia = query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), +}); + +describe('', () => { + it('Incoming XEC-only', async () => { + render( + + + + , + + , + ); + + // We see the tx received icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see the tx received label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // For a tx with timeFirstSeen of 0, we render the block timestamp + expect(screen.getByText('May 17, 2022, 18:35:28')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('42 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + }); + it('Outgoing XEC-only', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // For a tx with timeFirstSeen of 0, we render the block timestamp + expect(screen.getByText('May 17, 2022, 21:46:58')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-222 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.01')).toBeInTheDocument(); + }); + it('Staking reward received by Cashtab wallet', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-mined')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText('Staking Reward')).toBeInTheDocument(); + + // Coinbase txs have timeFirstSeen of 0 + // For a tx with timeFirstSeen of 0, we render the block timestamp + expect(screen.getByText('Nov 15, 2023, 15:37:13')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('625.01k XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$18.75')).toBeInTheDocument(); + }); + it('Alias registration (v0)', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Oct 3, 2023, 12:14:36')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.55 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // Alias registration app action + // We see the alias registration icon + expect(screen.getByTitle('tx-alias-registration')).toBeInTheDocument(); + // We see the alias registration description + expect(screen.getByText('bug2 to qqx...kqz')).toBeInTheDocument(); + }); + it('Invalid alias registration (v0)', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // Coinbase txs have timeFirstSeen of 0 + // For a tx with timeFirstSeen of 0, we render the block timestamp + expect(screen.getByText('Oct 3, 2023, 12:14:36')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.55 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // Alias registration app action + // We see the alias registration icon + expect(screen.getByTitle('tx-alias-registration')).toBeInTheDocument(); + // We see the alias registration description + expect( + screen.getByText('Invalid alias registration'), + ).toBeInTheDocument(); + }); + it('Another invalid alias registration (v0)', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // Coinbase txs have timeFirstSeen of 0 + // For a tx with timeFirstSeen of 0, we render the block timestamp + expect(screen.getByText('Oct 3, 2023, 12:14:36')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.55 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // Alias registration app action + // We see the alias registration icon + expect(screen.getByTitle('tx-alias-registration')).toBeInTheDocument(); + // We see the alias registration description + expect( + screen.getByText('Invalid alias registration'), + ).toBeInTheDocument(); + }); + it('Received slpv1 fungible token', async () => { + render( + + + + , + + , + ); + + // We see the tx received icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see the tx received label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // For a tx with timeFirstSeen of 0, we render the block timestamp + expect(screen.getByText('May 17, 2022, 21:17:04')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for 4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + ), + ).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We see the token name + expect( + screen.getByText('Covid19 Lifetime Immunity'), + ).toBeInTheDocument(); + + // We see the token ticker in parenthesis in the summary column + expect(screen.getByText('(NOCOVID)')).toBeInTheDocument(); + + // We see the expected token action text for a received SLPv1 fungible token tx + expect(screen.getByText('Received 12 NOCOVID')).toBeInTheDocument(); + }); + it('Received slpv1 fungible token with no token info in cache', async () => { + render( + + + + , + + , + ); + + // We see the tx received icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see the tx received label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // For a tx with timeFirstSeen of 0, we render the block timestamp + expect(screen.getByText('May 17, 2022, 21:17:04')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for 4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + ), + ).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We do not see the token name + expect( + screen.queryByText('Covid19 Lifetime Immunity'), + ).not.toBeInTheDocument(); + + // We do not see the token ticker in parenthesis in the summary column + expect(screen.queryByText('(NOCOVID)')).not.toBeInTheDocument(); + + // We see the expected token action with no quantity + expect(screen.getByText('Received')).toBeInTheDocument(); + }); + it('Sent slpv1 fungible token', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('May 17, 2022, 21:46:58')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for 4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + ), + ).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We see the token name + expect( + screen.getByText('Covid19 Lifetime Immunity'), + ).toBeInTheDocument(); + + // We see the token ticker in parenthesis in the summary column + expect(screen.getByText('(NOCOVID)')).toBeInTheDocument(); + + // We see the expected token action text for a received SLPv1 fungible token tx + expect(screen.getByText('Sent 17 NOCOVID')).toBeInTheDocument(); + }); + it('Sent slpv1 fungible token with no token info in cache', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('May 17, 2022, 21:46:58')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for 4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + ), + ).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We do not see the token name + expect( + screen.queryByText('Covid19 Lifetime Immunity'), + ).not.toBeInTheDocument(); + + // We do not see the token ticker in parenthesis in the summary column + expect(screen.queryByText('(NOCOVID)')).not.toBeInTheDocument(); + + // We see the expected token action with no amount + expect(screen.getByText('Sent')).toBeInTheDocument(); + }); + it('slpv1 fungible token GENESIS', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + // Note: slpv1 genesis is a "self send" tx + // Subject how we want to present this to the user + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx received label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Sep 26, 2022, 21:11:49')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for cf601c56b58bc05a39a95374a4a865f0a8b56544ea937b30fb46315441717c50', + ), + ).toBeInTheDocument(); + + // We see the genesis icon + expect(screen.getByTitle('tx-genesis')).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We see the token name + expect(screen.getByText('UpdateTest')).toBeInTheDocument(); + + // We see the token ticker in parenthesis in the summary column + expect(screen.getByText('(UDT)')).toBeInTheDocument(); + + // We see the expected token action text for a received SLPv1 fungible token tx + expect(screen.getByText('Created 777.7777777 UDT')).toBeInTheDocument(); + }); + it('slpv1 fungible token GENESIS with no token info in cache', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + // Note: slpv1 genesis is a "self send" tx + // Subject how we want to present this to the user + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx received label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Sep 26, 2022, 21:11:49')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for cf601c56b58bc05a39a95374a4a865f0a8b56544ea937b30fb46315441717c50', + ), + ).toBeInTheDocument(); + + // We see the genesis icon + expect(screen.getByTitle('tx-genesis')).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We do not see the token name + expect(screen.queryByText('UpdateTest')).not.toBeInTheDocument(); + + // We do not see the token ticker in parenthesis in the summary column + expect(screen.queryByText('(UDT)')).not.toBeInTheDocument(); + + // We see the expected token action presented without quantity or other token info + expect(screen.getByText('Created')).toBeInTheDocument(); + }); + it('Received slpv1 fungible token amount less than 1 with 9 decimals', async () => { + render( + + + + , + + , + ); + + // We see the tx received icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see the tx received label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Oct 3, 2022, 23:37:46')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', + ), + ).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We see the token name + expect(screen.getByText('CashTabBits')).toBeInTheDocument(); + + // We see the token ticker in parenthesis in the summary column + expect(screen.getByText('(CTB)')).toBeInTheDocument(); + + // We see the expected token action text for a received SLPv1 fungible token tx + expect(screen.getByText('Received .123456789 CTB')).toBeInTheDocument(); + }); + it('Received slpv1 fungible token with 9 decimals with no token info in cache', async () => { + render( + + + + , + + , + ); + + // We see the tx received icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see the tx received label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Oct 3, 2022, 23:37:46')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for acba1d7f354c6d4d001eb99d31de174e5cea8a31d692afd6e7eb8474ad541f55', + ), + ).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We do not see the token name + expect(screen.queryByText('CashTabBits')).not.toBeInTheDocument(); + + // We do not see the token ticker in parenthesis in the summary column + expect(screen.queryByText('(CTB)')).not.toBeInTheDocument(); + + // We see the expected token action text without quantity or ticker + expect(screen.getByText('Received')).toBeInTheDocument(); + }); + it('Received airdrop with msg (legacy push) with no token info in cache', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Oct 1, 2022, 23:36:08')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('5.69 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // Airdrop app action + // We see the airdrop icon + expect(screen.getByTitle('tx-airdrop')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for bdb3b4215ca0622e0c4c07655522c376eaa891838a82f0217fa453bb0595a37c', + ), + ).toBeInTheDocument(); + + // We see airdrop msg + expect(screen.getByText('Airdrop to holders of')).toBeInTheDocument(); + // We see the airdrop msg + expect( + screen.getByText( + 'evc token service holders air dropπŸ₯‡πŸŒπŸ₯‡β€πŸ‘ŒπŸ›¬πŸ›¬πŸ—πŸ€΄', + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + 'bdb3b4215ca0622e0c4c07655522c376eaa891838a82f0217fa453bb0595a37c', + ), + ).toBeInTheDocument(); + }); + it('Sent airdrop with msg with token info in cache', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Mar 22, 2024, 10:07:32')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-2k XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.06')).toBeInTheDocument(); + + // Airdrop app action + // We see the airdrop icon + expect(screen.getByTitle('tx-airdrop')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', + ), + ).toBeInTheDocument(); + + // We see token info in column + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + expect(screen.getByText('GRUMPY')).toBeInTheDocument(); + expect(screen.getByText('(GRP)')).toBeInTheDocument(); + + // We see airdrop label + expect(screen.getByText('Airdrop')).toBeInTheDocument(); + // We see the airdrop msg + expect( + screen.getByText( + 'ATTENTION GRUMPY PEOPLE! 😾 You can now deposit $GRP to the eToken bot at t.me/eCashPlay to top up your Casino Credits! 1m $GRP = 1 Credit. Play Casino games and win XEC!', + ), + ).toBeInTheDocument(); + }); + it('Off-spec airdrop tx', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Mar 22, 2024, 10:07:32')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-2k XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.06')).toBeInTheDocument(); + + // Airdrop app action + // We see the airdrop icon + expect(screen.getByTitle('tx-airdrop')).toBeInTheDocument(); + + // We get the off-spec airdrop action + expect( + screen.getByText('Off-spec airdrop: tokenId unavailable'), + ).toBeInTheDocument(); + }); + it('Sent encrypted msg', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Oct 4, 2022, 19:08:19')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-12 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // Encrypted msg app action + // We see the encrypted msg icon + expect(screen.getByAltText('tx-encrypted-msg')).toBeInTheDocument(); + + // We see expected text msg + expect(screen.getByText('Encrypted Cashtab Msg')).toBeInTheDocument(); + }); + it('Received encrypted msg', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Oct 4, 2022, 19:08:19')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('11 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // Encrypted msg app action + // We see the encrypted msg icon + expect(screen.getByAltText('tx-encrypted-msg')).toBeInTheDocument(); + + // We see expected text msg + expect(screen.getByText('Encrypted Cashtab Msg')).toBeInTheDocument(); + }); + it('Burn slpv1 fungible token', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Oct 4, 2022, 22:11:00')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for 4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', + ), + ).toBeInTheDocument(); + + // We see the token burn icon + expect(screen.getByTitle('tx-token-burn')).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We see the token name + expect(screen.getByText('Lambda Variant Variants')).toBeInTheDocument(); + + // We see the token ticker in parenthesis in the summary column + expect(screen.getByText('(LVV)')).toBeInTheDocument(); + + // We see the expected token action text for a received SLPv1 fungible token tx + expect(screen.getByText('Burned 12 LVV')).toBeInTheDocument(); + }); + it('Burn slpv1 fungible token with no token info in cache', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Oct 4, 2022, 22:11:00')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for 4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', + ), + ).toBeInTheDocument(); + + // We see the token burn icon + expect(screen.getByTitle('tx-token-burn')).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We do not see the token name + expect( + screen.queryByText('Lambda Variant Variants'), + ).not.toBeInTheDocument(); + + // We do notsee the token ticker in parenthesis in the summary column + expect(screen.queryByText('(LVV)')).not.toBeInTheDocument(); + + // We see the expected token action text for a received SLPv1 fungible token tx + expect(screen.getByText('Burned')).toBeInTheDocument(); + }); + it('Burn slpv1 fungible token with 9 decimals', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx sent label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Oct 4, 2022, 22:46:25')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for 7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + ), + ).toBeInTheDocument(); + + // We see the token burn icon + expect(screen.getByTitle('tx-token-burn')).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We see the token name + expect( + screen.getByText( + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + ), + ).toBeInTheDocument(); + + // We see the token ticker in parenthesis in the summary column + expect(screen.getByText('(WDT)')).toBeInTheDocument(); + + // We see the expected token action text for a received SLPv1 fungible token tx + expect(screen.getByText('Burned .1234567 WDT')).toBeInTheDocument(); + }); + it('Swap tx', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Apr 8, 2024, 24:18:59')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-26.56 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // SWaP msg app action + // We see the SWaP icon + expect(screen.getByTitle('swap')).toBeInTheDocument(); + + // We see expected text msg + expect(screen.getByText('SWaP')).toBeInTheDocument(); + }); + it('Received PayButton tx with nonce and no data', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Jan 27, 2024, 02:42:14')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('18 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // PayButton msg app action + // We see the PayButton logo + expect(screen.getByAltText('tx-paybutton')).toBeInTheDocument(); + + // We see the nonce + expect(screen.getByText('d980190d13019567')).toBeInTheDocument(); + }); + it('Sent PayButton tx with nonce and data', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Jan 27, 2024, 02:40:34')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-34.02k XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$1.02')).toBeInTheDocument(); + + // PayButton msg app action + // We see the PayButton logo + expect(screen.getByAltText('tx-paybutton')).toBeInTheDocument(); + + // We see the data + expect(screen.getByText('πŸ˜‚πŸ‘')).toBeInTheDocument(); + + // We see the nonce + expect(screen.getByText('69860643e4dc4c88')).toBeInTheDocument(); + }); + it('Sent empty PayButton tx (no nonce, no data)', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Jan 27, 2024, 02:40:34')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-34.02k XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$1.02')).toBeInTheDocument(); + + // PayButton msg app action + // We see the PayButton logo + expect(screen.getByAltText('tx-paybutton')).toBeInTheDocument(); + + // We do not see the data + expect(screen.queryByText('πŸ˜‚πŸ‘')).not.toBeInTheDocument(); + + // We do not see the nonce + expect(screen.queryByText('69860643e4dc4c88')).not.toBeInTheDocument(); + }); + it('Sent PayButton tx with data but no nonce', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Jan 27, 2024, 02:40:34')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-34.02k XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$1.02')).toBeInTheDocument(); + + // PayButton msg app action + // We see the PayButton logo + expect(screen.getByAltText('tx-paybutton')).toBeInTheDocument(); + + // We see the data + expect(screen.getByText('only data here')).toBeInTheDocument(); + + // We do not see the nonce + expect(screen.queryByText('69860643e4dc4c88')).not.toBeInTheDocument(); + }); + it('Off-spec PayButton tx', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Jan 27, 2024, 02:40:34')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-34.02k XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$1.02')).toBeInTheDocument(); + + // PayButton msg app action + // We see the PayButton logo + expect(screen.getByAltText('tx-paybutton')).toBeInTheDocument(); + + // We see expected protocol label + expect(screen.getByText('(Invalid)')).toBeInTheDocument(); + }); + it('Unsupported version PayButton tx', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Jan 27, 2024, 02:40:34')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-34.02k XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$1.02')).toBeInTheDocument(); + + // PayButton msg app action + // We see the PayButton logo + expect(screen.getByAltText('tx-paybutton')).toBeInTheDocument(); + + // We see expected protocol label + expect(screen.getByText('(Invalid)')).toBeInTheDocument(); + }); + it('eCash chat tx', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Mar 30, 2024, 08:54:10')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('10 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // eCash chat msg app action + // We see the eCash Chat logo + expect(screen.getByAltText('tx-chat')).toBeInTheDocument(); + + // We see expected protocol label + expect(screen.getByText('eCash Chat')).toBeInTheDocument(); + + // We see expected chat msg + expect( + screen.getByText('hello from eCash Chat πŸ‘'), + ).toBeInTheDocument(); + }); + it('off-spec eCash chat tx', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Mar 30, 2024, 08:54:10')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('10 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // eCash chat msg app action + // We see the eCash Chat logo + expect(screen.getByAltText('tx-chat')).toBeInTheDocument(); + + // We see expected protocol label + expect(screen.getByText('Invalid eCash Chat')).toBeInTheDocument(); + }); + it('slpv1 fungible token MINT', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + // Note: slpv1 mint is a "self send" tx + // Subject how we want to present this to the user + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx received label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Mar 31, 2024, 05:10:19')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-33.72 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + ), + ).toBeInTheDocument(); + + // We see the mint icon + expect(screen.getByTitle('tx-mint')).toBeInTheDocument(); + + // We see the token type + expect(screen.getAllByText('SLP 1')[0]).toBeInTheDocument(); + + // We see the token name + expect(screen.getByText('Cachet')).toBeInTheDocument(); + + // We see the token ticker in parenthesis in the summary column + expect(screen.getByText('(CACHET)')).toBeInTheDocument(); + + // We see the expected token action text for a received SLPv1 fungible token tx + expect(screen.getByText('Minted 1.00 CACHET')).toBeInTheDocument(); + }); + it('slpv1 fungible token MINT with no token info in cache', async () => { + render( + + + + , + + , + ); + + // We see the tx sent icon + // Note: slpv1 mint is a "self send" tx + // Subject how we want to present this to the user + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see the tx received label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Mar 31, 2024, 05:10:19')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-33.72 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + ), + ).toBeInTheDocument(); + + // We see the mint icon + expect(screen.getByTitle('tx-mint')).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('SLP 1')).toBeInTheDocument(); + + // We do not see the token name + expect(screen.queryByText('Cachet')).not.toBeInTheDocument(); + + // We do not see the token ticker in parenthesis in the summary column + expect(screen.queryByText('(CACHET)')).not.toBeInTheDocument(); + + // We see the expected token action text for a received SLPv1 fungible token tx + expect(screen.getByText('Minted')).toBeInTheDocument(); + }); + it('Msg sent by ElectrumABC', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Mar 2, 2024, 04:21:10')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('6 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // Unknown msg app action + // We see the Unknown logo + expect(screen.getByTitle('tx-unknown')).toBeInTheDocument(); + + // We see expected protocol label + expect(screen.getByText('Unknown App')).toBeInTheDocument(); + + // We see raw hex + expect( + screen.getByText('74657374696e672061206d736720666f72206572726f72'), + ).toBeInTheDocument(); + + // We see attempted utf8 parsing of the msg + expect(screen.getByText('testing a msg for error')).toBeInTheDocument(); + }); + it('Unknown App tx with long push in stackArray', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-sent')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Sent to/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Jan 11, 2024, 08:28:45')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('-33.08 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('-$0.00')).toBeInTheDocument(); + + // Unknown msg app action + // We see the Unknown logo + expect(screen.getByTitle('tx-unknown')).toBeInTheDocument(); + + // We see expected protocol label + expect(screen.getByText('Unknown App')).toBeInTheDocument(); + + // We see raw hex + expect( + screen.getByText( + '3336616533642d4d45524f4e2d57494e227d2c7b226e616d65223a2277616c61222c226d657373616765223a223635396661313133373065333136663265613336616533642d57414c412d57494e227d5d2c227465726d73223a5b7b226e616d65223a22726566657265655075624b6579222c2274797065223a226279746573222c2276616c7565223a22303231383839303432373865626633333035393039336635393661323639376366333636386233626563396133613063363430386134353531343761623364623933227d5d7d7d7d7d', + ), + ).toBeInTheDocument(); + + // We see attempted utf8 parsing of the msg + expect( + screen.getByText( + `36ae3d-MERON-WIN"},{"name":"wala","message":"659fa11370e316f2ea36ae3d-WALA-WIN"}],"terms":[{"name":"refereePubKey","type":"bytes","value":"02188904278ebf33059093f596a2697cf3668b3bec9a3a0c6408a455147ab3db93"}]}}}}`, + ), + ).toBeInTheDocument(); + }); + it('Received ALP tx', async () => { + render( + + + + , + + , + ); + + // We see expected send/receive label icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see the expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Mar 14, 2024, 17:59:21')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', + ), + ).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('ALP')).toBeInTheDocument(); + // We see the token name + expect(screen.getByText('Credo In Unum Deo')).toBeInTheDocument(); + // We see the token ticker + expect(screen.getByText('(CRD)')).toBeInTheDocument(); + + // We see the expected token action text for a received ALP fungible token tx + expect(screen.getByText('Received 0.0650 CRD')).toBeInTheDocument(); + }); + it('Received ALP tx with token not in cache', async () => { + render( + + + + , + + , + ); + + // We see expected send/receive label icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see the expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Mar 14, 2024, 17:59:21')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('5.46 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // We see the token icon + expect( + screen.getByAltText( + 'icon for cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', + ), + ).toBeInTheDocument(); + + // We see the token type + expect(screen.getByText('ALP')).toBeInTheDocument(); + + // We see the expected token action text for a received ALP fungible token tx + expect(screen.getByText('Received')).toBeInTheDocument(); + }); + it('Received Cashtab message', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Apr 8, 2024, 22:48:33')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('5.5 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // Cashtab msg app action + // We see the Cashtab Msg logo + expect(screen.getByTitle('tx-cashtab-msg')).toBeInTheDocument(); + + // We see expected protocol label + expect(screen.getByText('Cashtab Msg')).toBeInTheDocument(); + + // We see expected Cashtab msg + expect( + screen.getByText( + `Merci pour le prix et bonne continuation dans vos projets de dΓ©veloppeur... J'ai Γ©tΓ© censurΓ© sΓ»r tΓ©lΓ©gramme jusqu'au 15 Avril 2024. RΓ©parer le bug observΓ© sur la page eToken Faucet?`, + ), + ).toBeInTheDocument(); + }); + it('off-spec Cashtab msg', async () => { + render( + + + + , + + , + ); + + // We see expected icon + expect(screen.getByTitle('tx-received')).toBeInTheDocument(); + + // We see expected label + expect(screen.getByText(/Received from/)).toBeInTheDocument(); + + // We render the timestamp + expect(screen.getByText('Apr 8, 2024, 22:48:33')).toBeInTheDocument(); + + // We see the formatted XEC amount + expect(screen.getByText('5.5 XEC')).toBeInTheDocument(); + + // We see the formatted fiat amount + expect(screen.getByText('$0.00')).toBeInTheDocument(); + + // Cashtab msg app action + // We see the Cashtab msg logo + expect(screen.getByTitle('tx-cashtab-msg')).toBeInTheDocument(); + + // We see expected protocol label + expect(screen.getByText('Invalid Cashtab Msg')).toBeInTheDocument(); + }); +}); diff --git a/cashtab/src/components/Home/Tx/index.js b/cashtab/src/components/Home/Tx/index.js new file mode 100644 --- /dev/null +++ b/cashtab/src/components/Home/Tx/index.js @@ -0,0 +1,897 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import React, { useState } from 'react'; +import { + TxWrapper, + MainRow, + TxDescCol, + TxDesc, + Timestamp, + AmountCol, + AmountTop, + AmountBottom, + Expand, + MainRowLeft, + PanelButton, + Collapse, + PanelLink, + ReplyLink, + AppAction, + AppDescLabel, + AppDescMsg, + TokenActionHolder, + TokenAction, + TokenDesc, + TokenType, + TokenName, + TokenTicker, + TokenInfoCol, + UnknownMsgColumn, + ActionLink, + IconAndLabel, + AddressLink, +} from 'components/Home/Tx/styles'; +import { + SendIcon, + ReceiveIcon, + MinedIcon, + AliasIconTx, + GenesisIcon, + AirdropIcon, + EncryptedMsgIcon, + TokenBurnIcon, + SwapIcon, + PayButtonIcon, + ChatIcon, + MintIcon, + UnknownIcon, + CashtabMsgIcon, + CopyPasteIcon, + ThemedPdfSolid, + ThemedLinkSolid, + AddContactIcon, + ReplyIcon, +} from 'components/Common/CustomIcons'; +import PropTypes from 'prop-types'; +import { supportedFiatCurrencies } from 'config/cashtabSettings'; +import CopyToClipboard from 'components/Common/CopyToClipboard'; +import { explorer } from 'config/explorer'; +import { parseTx } from 'chronik'; +import { + toFormattedXec, + decimalizedTokenQtyToLocaleFormat, +} from 'utils/formatting'; +import { toXec, decimalizeTokenAmount } from 'wallet'; +import { opReturn } from 'config/opreturn'; +import cashaddr from 'ecashaddrjs'; +import TokenIcon from 'components/Etokens/TokenIcon'; +import Modal from 'components/Common/Modal'; +import { ModalInput } from 'components/Common/Inputs'; +import { toast } from 'react-toastify'; +import { getContactNameError } from 'validation'; + +const Tx = ({ + tx, + hashes, + fiatPrice, + fiatCurrency, + cashtabState, + updateCashtabState, + userLocale = 'en-US', +}) => { + const { txid, timeFirstSeen, block, tokenEntries, inputs, outputs } = tx; + const { satoshisSent, xecTxType, stackArray, recipients } = parseTx( + tx, + hashes, + ); + const { cashtabCache, contactList } = cashtabState; + + let replyAddress, replyAddressPreview, knownSender; + if (xecTxType === 'Received') { + // If Sent from Cashtab, then the sender will be the outputScript at the 0-index input + // If Received, we assume that it is "from" the outputScript of the 0-index input + // We only render these previews in history if this outputScript + // can be encoded as a p2sh or p2pkh address + try { + replyAddress = cashaddr.encodeOutputScript(inputs[0].outputScript); + replyAddressPreview = `${replyAddress.slice( + 6, + 9, + )}...${replyAddress.slice(-3)}`; + knownSender = contactList.find( + contact => contact.address === replyAddress, + ); + } catch (err) { + // Leave replyAddress as false so we do not try to render it + } + } + + let knownRecipient, renderedRecipient, renderedOtherRecipients; + if (xecTxType === 'Sent' && typeof recipients[0] !== 'undefined') { + const recipientPreview = `${recipients[0].slice( + 6, + 9, + )}...${recipients[0].slice(-3)}`; + knownRecipient = contactList.find( + contact => contact.address === recipients[0], + ); + + renderedRecipient = `${ + typeof knownRecipient !== 'undefined' + ? knownRecipient.name + : recipientPreview + }`; + renderedOtherRecipients = + recipients.length > 1 + ? `and ${recipients.length - 1} other${ + recipients.length - 1 > 1 ? 's' : '' + }` + : ''; + } + + // Parse for app actions + // For now, these are broadly of two types + // token actions, which are summarized in the tokenEntries key of tx + // app actions, which are OP_RETURN txs that may or may not follow spec + // https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/doc/standards/op_return-prefix-guideline.md + // With EMPP, a single TX may have several app actions + // For now, we parse only token actions and non-EMPP OP_RETURN + // TODO parse EMPP + + let appActions = []; + if (stackArray.length !== 0) { + // If we have an OP_RETURN output + switch (stackArray[0]) { + case opReturn.appPrefixesHex.eToken: { + // slpv1 + // Do nothing, handle this in token actions + break; + } + case opReturn.opReserved: { + // EMPP + // For now, do nothing + // ALP will be parsed by tokenEntries below + // TODO parse EMPP app actions + break; + } + case opReturn.appPrefixesHex.aliasRegistration: { + // Magic numbers per spec + // https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/doc/standards/ecash-alias.md + if ( + stackArray[1] === '00' && + typeof stackArray[2] !== 'undefined' && + typeof stackArray[3] !== 'undefined' && + stackArray[3].length === 42 + ) { + const addressTypeByte = stackArray[3].slice(0, 2); + let addressType; + if (addressTypeByte === '00') { + addressType = 'p2pkh'; + } else if (addressTypeByte === '08') { + addressType = 'p2sh'; + } else { + appActions.push( + + + + Invalid alias registration + + , + ); + break; + } + const aliasAddress = cashaddr.encode( + 'ecash', + addressType, + stackArray[3].slice(1), + ); + const aliasAddrPreview = `${aliasAddress.slice( + 6, + 9, + )}...${aliasAddress.slice(-3)}`; + appActions.push( + <> + + + Alias Registration + + + {`${Buffer.from(stackArray[2], 'hex').toString( + 'utf8', + )} to ${aliasAddrPreview}`} + + , + ); + break; + } + appActions.push( + + + Invalid alias registration + , + ); + break; + } + case opReturn.appPrefixesHex.airdrop: { + if ( + typeof stackArray[1] !== 'undefined' && + stackArray[1].length === 64 + ) { + // We have an on-spec airdrop tx if OP_RETURN prefix and tokenID at first push after prefix + const airdroppedTokenId = stackArray[1]; + let airdropMsg = ''; + if (typeof stackArray[2] !== 'undefined') { + // Legacy airdrop msg would be at [3] after cashtab msg prefix push + // on-spec airdrop msg would be at [2] + airdropMsg = + stackArray[2] === opReturn.appPrefixesHex.cashtab && + typeof stackArray[3] !== 'undefined' + ? Buffer.from(stackArray[3], 'hex').toString( + 'utf8', + ) + : Buffer.from(stackArray[2], 'hex').toString( + 'utf8', + ); + } + // Add token info if we have it in cache + const airdroppedTokenInfo = + cashtabCache.tokens.get(airdroppedTokenId); + if (typeof airdroppedTokenInfo !== 'undefined') { + const { genesisInfo, tokenType } = airdroppedTokenInfo; + const { tokenName, tokenTicker } = genesisInfo; + const { protocol, number } = tokenType; + const parsedTokenType = `${protocol}${ + protocol !== 'ALP' ? ` ${number}` : '' + }`; + appActions.push( + <> + + + Airdrop + + + + {parsedTokenType} + + + {tokenName} + + + ({tokenTicker}) + + {airdropMsg !== '' && ( + {airdropMsg} + )} + , + ); + break; + } + appActions.push( + <> + + + + Airdrop to holders of{' '} + + {airdroppedTokenId} + + + + + {airdropMsg !== '' && ( + {airdropMsg} + )} + , + ); + break; + } + // off-spec airdrop + appActions.push( + <> + + + + Off-spec airdrop: tokenId unavailable + + + , + ); + break; + } + case opReturn.appPrefixesHex.cashtabEncrypted: { + // Deprecated, we do not parse + // Just render the lokad + // + appActions.push( + + + Encrypted Cashtab Msg + , + ); + break; + } + case opReturn.appPrefixesHex.swap: { + // Limited parse support for SWaP txs, which are not expected in Cashtab + // Just print the type + appActions.push( + + + SWaP + , + ); + break; + } + case opReturn.appPrefixesHex.paybutton: { + // PayButton tx + // https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/doc/standards/paybutton.md + if ( + stackArray[1] === '00' && + typeof stackArray[2] !== 'undefined' && + typeof stackArray[3] !== 'undefined' + ) { + // Valid PayButtonTx + appActions.push( + <> + + + + {stackArray[2] !== '00' && ( + + {Buffer.from(stackArray[2], 'hex').toString( + 'utf8', + )} + + )} + {stackArray[3] !== '00' && ( + {stackArray[3]} + )} + , + ); + } else { + appActions.push( + + + (Invalid) + , + ); + } + break; + } + case opReturn.appPrefixesHex.eCashChat: { + if (typeof stackArray[1] !== 'undefined') { + appActions.push( + <> + + + eCash Chat + + + {Buffer.from(stackArray[1], 'hex').toString( + 'utf8', + )} + + {xecTxType === 'Received' && + typeof replyAddress !== 'undefined' && ( + + + + )} + , + ); + } else { + appActions.push( + + + Invalid eCash Chat + , + ); + } + break; + } + case opReturn.appPrefixesHex.cashtab: { + if (typeof stackArray[1] !== 'undefined') { + appActions.push( + <> + + + Cashtab Msg + + + {Buffer.from(stackArray[1], 'hex').toString( + 'utf8', + )} + + {xecTxType === 'Received' && + typeof replyAddress !== 'undefined' && ( + + + + )} + , + ); + } else { + appActions.push( + <> + + + Invalid Cashtab Msg + + , + ); + } + break; + } + default: { + // Unsupported lokad prefixes or misc OP_RETURN msgs + // e.g. a msg sent by ElectrumABC + // Attempt to utf8 decode each push + const decodedTest = []; + for (const el of stackArray) { + decodedTest.push(Buffer.from(el, 'hex').toString('utf8')); + } + appActions.push( + <> + + + Unknown App + + + {stackArray.join(' ')} + {decodedTest.join(' ')} + + , + ); + break; + } + } + } + let tokenActions = []; + for (let i = 0; i < tokenEntries.length; i += 1) { + // Each entry will get a parsed token action row + const entry = tokenEntries[i]; + + const { tokenId, tokenType, txType, burnSummary, actualBurnAmount } = + entry; + const { protocol, number } = tokenType; + const isUnintentionalBurn = + burnSummary !== '' && actualBurnAmount !== '0'; + + // Every token entry has a token icon + const tokenIcon = ; + + // Every token entry has a token type + // We parse it to be more human friendly (...less dev friendly πŸ€”) in tx history + const parsedTokenType = `${protocol}${ + protocol !== 'ALP' ? ` ${number}` : '' + }`; + + // Ref TokenTxType in ChronikClientNode + if (txType === 'NONE' || txType === 'UNKNOWN') { + // Render a token entry row for this + // Exercise for the user to figure out what is going on with this tx + tokenActions.push( + + {tokenIcon} + {parsedTokenType} + Unknown token action + , + ); + continue; + } + + // Other txTypes have an associated quantity + // We will render this if we can get the token's decimals from cache + + const cachedTokenInfo = cashtabCache.tokens.get(tokenId); + const renderedTxType = + txType === 'SEND' && !isUnintentionalBurn + ? xecTxType + : txType === 'GENESIS' + ? 'Created' + : isUnintentionalBurn || txType === 'BURN' + ? 'Burned' + : txType === 'MINT' + ? 'Minted' + : txType; + if (typeof cachedTokenInfo === 'undefined') { + tokenActions.push( + + + {txType === 'GENESIS' && } + {txType === 'MINT' && } + {(isUnintentionalBurn || txType === 'BURN') && ( + + )} + {tokenIcon} + + {parsedTokenType} + {txType} + + + {renderedTxType} + , + ); + continue; + } else { + const { decimals, tokenName, tokenTicker } = + cachedTokenInfo.genesisInfo; + let amountTotal = BigInt(0); + let amountThisWallet = BigInt(0); + // For unexpected burns, we do not need to iterate over outputs + // Enough info is available in the burnSummary key + if (isUnintentionalBurn) { + amountTotal = BigInt(actualBurnAmount); + } else { + for (const output of outputs) { + if ( + typeof output.token !== 'undefined' && + typeof output.token.entryIdx !== 'undefined' && + output.token.entryIdx === i + ) { + // Get the amount associated with this token entry + // Per ChronikClientNode, we will always have amount as a string in + // the token key of an output, see type Token_InNode + amountTotal += BigInt(output.token.amount); + for (const hash of hashes) { + if (output.outputScript.includes(hash)) { + amountThisWallet += BigInt(output.token.amount); + } + } + } + } + } + + // Calculate amount to render in tx history based on tx type + // For a received tx, we are only interested in amountThisWallet + // For a genesis tx -- cashtab will only create outputs at this wallet. So, just render this. + // For a sent tx, we want amountTotal - amountThisWallet (amountThisWallet is change) + const renderedTokenAmount = + renderedTxType === 'Received' + ? amountThisWallet + : renderedTxType === 'Created' || + renderedTxType === 'Minted' + ? amountTotal + : amountTotal - amountThisWallet; + + const decimalizedAmount = decimalizeTokenAmount( + renderedTokenAmount.toString(), + decimals, + ); + const formattedAmount = decimalizedTokenQtyToLocaleFormat( + decimalizedAmount, + userLocale, + ); + tokenActions.push( + + + {txType === 'GENESIS' && } + {txType === 'MINT' && } + {(isUnintentionalBurn || txType === 'BURN') && ( + + )} + {tokenIcon} + + {parsedTokenType} + {txType} + + + + + {tokenName} + ({tokenTicker}) + + + {renderedTxType} {formattedAmount} {tokenTicker} + + , + ); + } + } + + const [showPanel, setShowPanel] = useState(false); + const [showAddNewContactModal, setShowAddNewContactModal] = useState(false); + const emptyFormData = { + newContactName: '', + }; + const emptyFormDataErrors = { + newContactName: false, + }; + const [formData, setFormData] = useState(emptyFormData); + const [formDataErrors, setFormDataErrors] = useState(emptyFormDataErrors); + + const addNewContact = async addressToAdd => { + // Check to see if the contact exists + const contactExists = contactList.find( + contact => contact.address === addressToAdd, + ); + + if (typeof contactExists !== 'undefined') { + // Contact exists + // Not expected to ever happen from Tx.js as user should not see option to + // add an existing contact + toast.error(`${addressToAdd} already exists in Contacts`); + } else { + contactList.push({ + name: formData.newContactName, + address: addressToAdd, + }); + // update localforage and state + await updateCashtabState('contactList', contactList); + toast.success( + `${formData.newContactName} (${addressToAdd}) added to Contact List`, + ); + } + + // Reset relevant state fields + setShowAddNewContactModal(false); + + // Clear new contact formData + setFormData(previous => ({ + ...previous, + newContactName: '', + })); + }; + + /** + * Update formData with user input + * @param {Event} e js input event + * e.target.value will be input value + * e.target.name will be name of originating input field + */ + const handleInput = e => { + const { name, value } = e.target; + + if (name === 'newContactName') { + const contactNameError = getContactNameError(value, contactList); + setFormDataErrors(previous => ({ + ...previous, + [name]: contactNameError, + })); + } + setFormData(previous => ({ + ...previous, + [name]: value, + })); + }; + + /** + * We use timeFirstSeen if it is not 0 + * We use block.timestamp if timeFirstSeen is unavailable + * We use Date.now() if timeFirstSeen is 0 and we do not have a block timestamp + * This is an edge case that is unlikely to ever be seen -- requires the chronik node to have just + * become indexed after a tx was broadcast but before it is confirmed + */ + const timestamp = + timeFirstSeen !== 0 || typeof block?.timestamp !== 'undefined' + ? new Date( + parseInt( + `${ + timeFirstSeen !== 0 ? timeFirstSeen : block.timestamp + }000`, + ), + ) + : new Date(); + + const renderedTimestamp = timestamp.toLocaleTimeString(userLocale, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour12: false, + }); + + const senderOrRecipientNotInContacts = + (xecTxType === 'Received' && typeof knownSender === 'undefined') || + (xecTxType === 'Sent' && + typeof knownRecipient === 'undefined' && + typeof recipients[0] !== 'undefined'); + + return ( + <> + {showAddNewContactModal && ( + addNewContact(recipients[0]) + : () => addNewContact(replyAddress) + } + handleCancel={() => setShowAddNewContactModal(false)} + showCancelButton + > + + + )} + + {/* We always display the MainRow, which is the XEC tx summary*/} + setShowPanel(!showPanel)}> + + + {xecTxType === 'Received' ? ( + + ) : xecTxType === 'Sent' ? ( + + ) : ( + + )} + + + {xecTxType} + {typeof replyAddress === 'string' ? ( + <> + {' from'} + + {typeof knownSender === + 'undefined' + ? replyAddressPreview + : knownSender.name} + + + ) : xecTxType === 'Sent' && + typeof recipients[0] !== 'undefined' ? ( + <> + {' to'} + + {renderedRecipient} + + {renderedOtherRecipients !== '' && ( + + {renderedOtherRecipients} + + )} + + ) : xecTxType === 'Sent' || + xecTxType === 'Received' ? ( + ' to self' + ) : ( + '' + )} + + {renderedTimestamp} + + + + + {xecTxType === 'Sent' ? '-' : ''} + {toFormattedXec(satoshisSent, userLocale)} XEC + + + {xecTxType === 'Sent' ? '-' : ''} + {supportedFiatCurrencies[fiatCurrency].symbol} + {( + fiatPrice * toXec(satoshisSent) + ).toLocaleString(userLocale, { + maximumFractionDigits: 2, + minimumFractionDigits: 2, + })} + + + + {appActions.map((action, index) => { + return {action}; + })} + {tokenActions.map((action, index) => { + return ( + + {action} + + ); + })} + + + + + + + + + + + + + + {senderOrRecipientNotInContacts && ( + { + setShowAddNewContactModal(true); + }} + > + + + )} + + + + ); +}; + +Tx.propTypes = { + tx: PropTypes.shape({ + txid: PropTypes.string, + timeFirstSeen: PropTypes.number, + block: PropTypes.shape({ timestamp: PropTypes.number }), + inputs: PropTypes.array, + outputs: PropTypes.array, + tokenEntries: PropTypes.array, + }), + hashes: PropTypes.arrayOf(PropTypes.string), + fiatPrice: PropTypes.number, + fiatCurrency: PropTypes.string, + cashtabState: PropTypes.shape({ + contactList: PropTypes.arrayOf( + PropTypes.shape({ + address: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }), + ), + settings: PropTypes.shape({ + fiatCurrency: PropTypes.string.isRequired, + sendModal: PropTypes.bool.isRequired, + autoCameraOn: PropTypes.bool.isRequired, + hideMessagesFromUnknownSenders: PropTypes.bool.isRequired, + balanceVisible: PropTypes.bool.isRequired, + minFeeSends: PropTypes.bool.isRequired, + }), + cashtabCache: PropTypes.shape({ + tokens: PropTypes.object.isRequired, + }), + }), + updateCashtabState: PropTypes.func, + userLocale: PropTypes.string, +}; + +export default Tx; diff --git a/cashtab/src/components/Home/Tx/styles.js b/cashtab/src/components/Home/Tx/styles.js new file mode 100644 --- /dev/null +++ b/cashtab/src/components/Home/Tx/styles.js @@ -0,0 +1,208 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import styled, { css } from 'styled-components'; +import { Link } from 'react-router-dom'; + +export const TxWrapper = styled.div` + border-bottom: 0.5px solid ${props => props.theme.separator}; + display: flex; + flex-direction: column; + gap: 12px; + svg { + width: 33px; + height: 33px; + } + img { + height: 33px; + } + box-sizing: border-box; + *, + *:before, + *:after { + box-sizing: inherit; + } +`; +export const Collapse = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + cursor: pointer; +`; +const Incoming = css` + color: ${props => props.theme.eCashBlue}; + fill: ${props => props.theme.eCashBlue}; +`; +const Genesis = css` + color: ${props => props.theme.genesisGreen}; + fill: ${props => props.theme.genesisGreen}; + svg { + fill: ${props => props.theme.genesisGreen}; + } + path { + fill: ${props => props.theme.genesisGreen}; + } + g { + fill: ${props => props.theme.genesisGreen}; + } +`; +const Burn = css` + color: ${props => props.theme.eCashPurple}; + fill: ${props => props.theme.eCashPurple}; + svg { + fill: ${props => props.theme.eCashPurple}; + } + path { + fill: ${props => props.theme.eCashPurple}; + } + g { + fill: ${props => props.theme.eCashPurple}; + } +`; +export const MainRow = styled.div` + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: center; + gap: 12px; + width: 100%; + color: ${props => props.theme.contrast}; + fill: ${props => props.theme.contrast}; + ${props => + (props.type === 'Received' || + props.type === 'Staking Reward' || + props.type === 'Coinbase Reward') && + Incoming} +`; +export const MainRowLeft = styled.div` + display: flex; + align-items: center; + gap: 12px; +`; +export const AppTxIcon = styled.div``; +export const TxDescCol = styled.div` + flex-direction: row; +`; +// Top row of TxDescCol +export const TxDesc = styled.div` + display: inline-block; + text-align: left; + width: 100%; +`; +// Bottom row of TxDescCol +export const Timestamp = styled.div` + display: flex; + width: 100%; + color: ${props => props.theme.lightWhite}; +`; +export const AmountCol = styled.div` + flex-direction: row; + justify-content: flex-end; +`; +// Top row of TxAmountCol +export const AmountTop = styled.div` + display: flex; + width: 100%; + justify-content: flex-end; +`; +export const AmountBottom = styled.div` + display: flex; + width: 100%; + color: ${props => props.theme.lightWhite}; + justify-content: flex-end; +`; +export const CashtabMsg = styled.div` + display: flex; + width: 100%; +`; +export const TokenEntry = styled.div` + display: flex; + width: 100%; +`; +// Button panel for actions on each tx +export const Expand = styled.div` + display: flex; + overflow: hidden; + height: ${props => (props.showPanel ? '36px' : '0px')}; + visibility: ${props => (props.showPanel ? 'visible' : 'collapse')}; + transition: all 0.5s ease-out; + justify-content: flex-end; + align-items: center; + gap: 12px; + svg { + height: 33px; + width: 33px; + fill: ${props => props.theme.contrast}; + } + path { + fill: ${props => props.theme.contrast}; + } + g { + fill: ${props => props.theme.contrast}; + } +`; +export const PanelButton = styled.button` + border: none; + background: none; + cursor: pointer; +`; +export const PanelLink = styled(Link)` + border: none; + background: none; + cursor: pointer; +`; +export const ReplyLink = styled(PanelLink)` + margin-left: auto; +`; +export const AddressLink = styled.a` + padding: 0 3px; +`; +export const AppAction = styled.div` + display: flex; + gap: 12px; + width: 100%; + align-items: center; + justify-content: space-between; + padding: 3px 12px; + ${props => props.type === 'Received' && Incoming} + border-radius: 9px; + background-color: ${props => props.theme.panel}; + flex-wrap: wrap; +`; +export const AppDescLabel = styled.div` + font-weight: bold; +`; +export const IconAndLabel = styled.div` + display: flex; + gap: 6px; + align-items: center; +`; +export const AppDescMsg = styled.div` + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-all; + text-align: left; +`; +export const TokenAction = styled(AppAction)` + ${props => props.tokenTxType === 'Received' && Incoming} + ${props => + (props.tokenTxType === 'Created' || props.tokenTxType === 'Minted') && + Genesis} + ${props => props.tokenTxType === 'Burned' && Burn} +`; +export const TokenActionHolder = styled.div``; +export const TokenInfoCol = styled.div` + display: flex; + flex-direction: column; +`; +export const UnknownMsgColumn = styled.div` + display: flex; + flex-direction: column; + text-align: left; +`; +export const TokenType = styled.div``; +export const TokenName = styled.div``; +export const TokenTicker = styled.div``; +export const TokenDesc = styled.div``; +export const ActionLink = styled.a``; diff --git a/cashtab/src/components/Home/TxHistory.js b/cashtab/src/components/Home/TxHistory.js --- a/cashtab/src/components/Home/TxHistory.js +++ b/cashtab/src/components/Home/TxHistory.js @@ -4,33 +4,38 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Tx from './Tx'; +import Tx from 'components/Home/Tx'; const TxHistory = ({ txs, + hashes, fiatPrice, fiatCurrency, cashtabState, updateCashtabState, + userLocale = 'en-US', }) => { return ( -
+ <> {txs.map(tx => ( ))} -
+ ); }; TxHistory.propTypes = { txs: PropTypes.array, + hashes: PropTypes.arrayOf(PropTypes.string), fiatPrice: PropTypes.number, fiatCurrency: PropTypes.string, cashtabState: PropTypes.shape({ @@ -53,6 +58,7 @@ }), }), updateCashtabState: PropTypes.func, + userLocale: PropTypes.string, }; export default TxHistory; diff --git a/cashtab/src/components/Home/__tests__/Tx.test.js b/cashtab/src/components/Home/__tests__/Tx.test.js deleted file mode 100644 --- a/cashtab/src/components/Home/__tests__/Tx.test.js +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2024 The Bitcoin developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { theme } from 'assets/styles/theme'; -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import Tx from 'components/Home/Tx'; -import { mockReceivedTxData } from 'components/Home/fixtures/mocks'; -import CashtabState from 'config/CashtabState'; - -// https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function -Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), -}); - -// https://stackoverflow.com/questions/64813447/cannot-read-property-addlistener-of-undefined-react-testing-library -window.matchMedia = query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), // deprecated - removeListener: jest.fn(), // deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), -}); - -describe('', () => { - it('Renders the timestamp if timeFirstSeen !== 0', async () => { - render( - - - , - , - ); - - // leftTxtCtn is rendered - const leftTxtCtn = screen.queryByTestId('left-txt-ctn'); - expect(leftTxtCtn).toBeInTheDocument(); - - // Expected timestamp is displayed - expect(leftTxtCtn).toHaveTextContent( - 'ReceivedJan 14, 2024 at 4:40:11 AM', - ); - }); - it('Renders the timestamp as block timestamp timeFirstSeen === 0', async () => { - render( - - - , - ); - - // leftTxtCtn is rendered - const leftTxtCtn = screen.queryByTestId('left-txt-ctn'); - expect(leftTxtCtn).toBeInTheDocument(); - - // Expected timestamp is displayed - expect(leftTxtCtn).toHaveTextContent( - 'ReceivedJan 14, 2024 at 4:46:35 AM', - ); - }); - it(`Does not render a timestamp for an unconfirmed tx with timeFirstSeen === '0'`, async () => { - const noTimeFirstSeenUnconfirmedMock = JSON.parse( - JSON.stringify(mockReceivedTxData), - ); - noTimeFirstSeenUnconfirmedMock.timeFirstSeen = '0'; - delete noTimeFirstSeenUnconfirmedMock['block']; - render( - - - , - ); - - // leftTxtCtn is rendered - const leftTxtCtn = screen.queryByTestId('left-txt-ctn'); - expect(leftTxtCtn).toBeInTheDocument(); - - // No timestamp is rendered as we have no timestamp to go off of in this case - expect(leftTxtCtn).toHaveTextContent('Received'); - }); - it('Renders from contact name if a tx is from an address in contact list', async () => { - render( - - - , - , - ); - - expect(screen.getByText('from inTheList')).toBeInTheDocument(); - }); - it('Does not render from contact name if a tx is not from an address in contact list', async () => { - render( - - - , - , - ); - - expect(screen.queryByText('from inTheList')).not.toBeInTheDocument(); - }); -}); diff --git a/cashtab/src/components/Send/SendXec.js b/cashtab/src/components/Send/SendXec.js --- a/cashtab/src/components/Send/SendXec.js +++ b/cashtab/src/components/Send/SendXec.js @@ -263,10 +263,14 @@ // if this was routed from Wallet screen's Reply to message link then prepopulate the address and value field if (location && location.state && location.state.replyAddress) { + // Populate a dust tx to the reply address setFormData({ + ...formData, address: location.state.replyAddress, amount: `${toXec(appConfig.dustSats)}`, }); + // Turn on the Cashtab Msg switch + setSendWithCashtabMsg(true); } // if this was routed from the Contact List diff --git a/cashtab/src/components/Wallets/index.js b/cashtab/src/components/Wallets/index.js --- a/cashtab/src/components/Wallets/index.js +++ b/cashtab/src/components/Wallets/index.js @@ -35,7 +35,7 @@ } from 'wallet'; import { getUserLocale } from 'helpers'; import { Event } from 'components/Common/GoogleAnalytics'; -import { formatXecBalance } from 'utils/formatting'; +import { toFormattedXec } from 'utils/formatting'; const Wallets = () => { const ContextValue = React.useContext(WalletContext); @@ -392,7 +392,7 @@ {wallet?.state?.balanceSats !== 0 - ? formatXecBalance( + ? toFormattedXec( wallet.state.balanceSats, userLocale, ) diff --git a/cashtab/src/config/opreturn.js b/cashtab/src/config/opreturn.js --- a/cashtab/src/config/opreturn.js +++ b/cashtab/src/config/opreturn.js @@ -5,6 +5,7 @@ 'use strict'; export const opReturn = { + opReserved: '50', opReturnPrefixHex: '6a', opReturnPrefixDec: '106', opPushDataOne: '4c', @@ -16,6 +17,7 @@ aliasRegistration: '2e786563', paybutton: '50415900', eCashChat: '63686174', + swap: '53575000', }, /* The max payload per spec is 220 bytes (or 223 bytes including +1 for OP_RETURN and +2 for pushdata opcodes) Within this 223 bytes, transaction building will take up 8 bytes, hence cashtabMsgByteLimit is set to 215 bytes diff --git a/cashtab/src/utils/__tests__/cashMethods.test.js b/cashtab/src/utils/__tests__/cashMethods.test.js --- a/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/cashtab/src/utils/__tests__/cashMethods.test.js @@ -2,8 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -import { getHashArrayFromWallet, sumOneToManyXec } from 'utils/cashMethods'; -import { walletWithXecAndTokens } from 'components/App/fixtures/mocks'; +import { sumOneToManyXec } from 'utils/cashMethods'; it(`sumOneToManyXec() correctly parses the value for a valid one to many send XEC transaction`, () => { const destinationAddressAndValueArray = [ @@ -30,13 +29,3 @@ ]; expect(sumOneToManyXec(destinationAddressAndValueArray)).toStrictEqual(NaN); }); - -describe('Correctly executes cash utility functions', () => { - it(`Successfully extracts a hash160 array from valid cashtab wallet`, () => { - expect(getHashArrayFromWallet(walletWithXecAndTokens)).toStrictEqual([ - '3a5fb236934ec078b4507c303d3afd82067f8fc1', - 'a28f8852f868f88e71ec666c632d6f86e978f046', - '600efb12a6f813eccf13171a8bc62055212d8d6c', - ]); - }); -}); diff --git a/cashtab/src/utils/__tests__/formatting.test.js b/cashtab/src/utils/__tests__/formatting.test.js --- a/cashtab/src/utils/__tests__/formatting.test.js +++ b/cashtab/src/utils/__tests__/formatting.test.js @@ -7,7 +7,7 @@ formatFiatBalance, formatBalance, decimalizedTokenQtyToLocaleFormat, - formatXecBalance, + toFormattedXec, } from 'utils/formatting'; import vectors from 'utils/fixtures/vectors'; @@ -124,13 +124,11 @@ }); }); describe('We can format an XEC balance', () => { - const { expectedReturns } = vectors.formatXecBalance; + const { expectedReturns } = vectors.toFormattedXec; expectedReturns.forEach(vector => { - const { description, balanceSats, userLocale, returned } = vector; - it(`formatXecBalance: ${description}`, () => { - expect(formatXecBalance(balanceSats, userLocale)).toBe( - returned, - ); + const { description, satoshis, userLocale, returned } = vector; + it(`toFormattedXec: ${description}`, () => { + expect(toFormattedXec(satoshis, userLocale)).toBe(returned); }); }); }); diff --git a/cashtab/src/utils/cashMethods.js b/cashtab/src/utils/cashMethods.js --- a/cashtab/src/utils/cashMethods.js +++ b/cashtab/src/utils/cashMethods.js @@ -31,16 +31,3 @@ return wallet.state; }; - -/** - * Get hash values to use for chronik calls - * @param {object} wallet valid cashtab wallet - * @returns {string[]} array of hashes of all addresses in wallet - */ -export const getHashArrayFromWallet = wallet => { - const hashArray = []; - wallet.paths.forEach(pathInfo => { - hashArray.push(pathInfo.hash); - }); - return hashArray; -}; diff --git a/cashtab/src/utils/fixtures/vectors.js b/cashtab/src/utils/fixtures/vectors.js --- a/cashtab/src/utils/fixtures/vectors.js +++ b/cashtab/src/utils/fixtures/vectors.js @@ -78,71 +78,71 @@ }, ], }, - formatXecBalance: { + toFormattedXec: { expectedReturns: [ { description: 'Balance over 1 trillion XEC (10 trillion)', - balanceSats: 1000000000000000, + satoshis: 1000000000000000, userLocale: 'en-US', returned: '10T', }, { description: 'Balance of exactly 1 trillion XEC', - balanceSats: 100000000000000, + satoshis: 100000000000000, userLocale: 'en-US', returned: '1T', }, { description: 'Balance exceeding 1 billion XEC (10 billion)', - balanceSats: 1000000000000, + satoshis: 1000000000000, userLocale: 'en-US', returned: '10B', }, { description: 'Balance exactly 1 billion XEC', - balanceSats: 100000000000, + satoshis: 100000000000, userLocale: 'en-US', returned: '1B', }, { description: 'Balance exceeding 1 million XEC (10 million)', - balanceSats: 1000000000, + satoshis: 1000000000, userLocale: 'en-US', returned: '10M', }, { description: 'Balance of exactly 1 million XEC', - balanceSats: 100000000, + satoshis: 100000000, userLocale: 'en-US', returned: '1M', }, { description: 'Balance exceeding 1 thousand XEC (10 thousand)', - balanceSats: 1000000, + satoshis: 1000000, userLocale: 'en-US', returned: '10k', }, { description: 'Balance of exactly 1 thousand XEC', - balanceSats: 100000, + satoshis: 100000, userLocale: 'en-US', returned: '1k', }, { description: 'Balance less than 1 thousand XEC', - balanceSats: 99999, + satoshis: 99999, userLocale: 'en-US', returned: '999.99', }, { description: 'Balance less than 1 thousand XEC, but french', - balanceSats: 99999, + satoshis: 99999, userLocale: 'fr-FR', returned: '999,99', }, { description: 'Balance less than 1 thousand XEC, but arabic', - balanceSats: 99999, + satoshis: 99999, userLocale: 'ar', returned: 'Ω©Ω©Ω©Ω«Ω©Ω©', }, diff --git a/cashtab/src/utils/formatting.js b/cashtab/src/utils/formatting.js --- a/cashtab/src/utils/formatting.js +++ b/cashtab/src/utils/formatting.js @@ -104,30 +104,30 @@ return localeTokenString; }; -export const formatXecBalance = (balanceSats, userLocale) => { +export const toFormattedXec = (satoshis, userLocale) => { // Get XEC balance - let balanceXec = toXec(balanceSats); + let xecAmount = toXec(satoshis); // Format up to max supply const trillion = 1e12; const billion = 1e9; const million = 1e6; const thousand = 1e3; let units = 'T'; - if (balanceXec >= trillion) { - balanceXec = balanceXec / trillion; - } else if (balanceXec >= billion) { - balanceXec = balanceXec / billion; + if (xecAmount >= trillion) { + xecAmount = xecAmount / trillion; + } else if (xecAmount >= billion) { + xecAmount = xecAmount / billion; units = 'B'; - } else if (balanceXec >= million) { - balanceXec = balanceXec / million; + } else if (xecAmount >= million) { + xecAmount = xecAmount / million; units = 'M'; - } else if (balanceXec >= thousand) { - balanceXec = balanceXec / thousand; + } else if (xecAmount >= thousand) { + xecAmount = xecAmount / thousand; units = 'k'; } else { units = ''; } - return `${balanceXec.toLocaleString(userLocale, { + return `${xecAmount.toLocaleString(userLocale, { maximumFractionDigits: 2, })}${units}`; }; diff --git a/cashtab/src/wallet/__tests__/index.test.js b/cashtab/src/wallet/__tests__/index.test.js --- a/cashtab/src/wallet/__tests__/index.test.js +++ b/cashtab/src/wallet/__tests__/index.test.js @@ -14,8 +14,10 @@ decimalizeTokenAmount, undecimalizeTokenAmount, removeLeadingZeros, + getHashes, } from 'wallet'; import { isValidCashtabWallet } from 'validation'; +import { walletWithXecAndTokens } from 'components/App/fixtures/mocks'; import vectors from '../fixtures/vectors'; describe('Cashtab wallet methods', () => { @@ -194,4 +196,11 @@ }); }); }); + it(`Successfully extracts a hash160 array from valid cashtab wallet`, () => { + expect(getHashes(walletWithXecAndTokens)).toStrictEqual([ + '3a5fb236934ec078b4507c303d3afd82067f8fc1', + 'a28f8852f868f88e71ec666c632d6f86e978f046', + '600efb12a6f813eccf13171a8bc62055212d8d6c', + ]); + }); }); diff --git a/cashtab/src/wallet/index.js b/cashtab/src/wallet/index.js --- a/cashtab/src/wallet/index.js +++ b/cashtab/src/wallet/index.js @@ -372,3 +372,16 @@ } return givenString.slice(leadingZeroCount, givenString.length); }; + +/** + * Get hash values to use for chronik calls and parsing tx history + * @param {object} wallet valid cashtab wallet + * @returns {string[]} array of hashes of all addresses in wallet + */ +export const getHashes = wallet => { + const hashArray = []; + wallet.paths.forEach(pathInfo => { + hashArray.push(pathInfo.hash); + }); + return hashArray; +}; diff --git a/cashtab/src/wallet/useWallet.js b/cashtab/src/wallet/useWallet.js --- a/cashtab/src/wallet/useWallet.js +++ b/cashtab/src/wallet/useWallet.js @@ -3,7 +3,6 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. import React, { useState, useEffect, useRef } from 'react'; -import { getHashArrayFromWallet } from 'utils/cashMethods'; import { isValidCashtabSettings, isValidCashtabCache, @@ -30,7 +29,13 @@ cashtabWalletsFromJSON, cashtabWalletsToJSON, } from 'helpers'; -import { createCashtabWallet, getLegacyPaths, getBalanceSats } from 'wallet'; +import { + createCashtabWallet, + getLegacyPaths, + getBalanceSats, + getHashes, + toXec, +} from 'wallet'; import { toast } from 'react-toastify'; import CashtabState from 'config/CashtabState'; import TokenIcon from 'components/Etokens/TokenIcon'; @@ -41,6 +46,7 @@ BlockNotificationDesc, } from 'components/Common/Atoms'; import { explorer } from 'config/explorer'; +import { toFormattedXec } from 'utils/formatting'; const useWallet = chronik => { const [cashtabLoaded, setCashtabLoaded] = useState(false); @@ -451,9 +457,7 @@ if (cashtabState.wallets.length > 0) { // Subscribe to addresses of current wallet, if you have one - const hash160Array = getHashArrayFromWallet( - cashtabState.wallets[0], - ); + const hash160Array = getHashes(cashtabState.wallets[0]); for (const hash of hash160Array) { ws.subscribeToScript('p2pkh', hash); } @@ -500,7 +504,7 @@ } let subscriptionUpdateRequired = false; - const hash160Array = getHashArrayFromWallet(cashtabState.wallets[0]); + const hash160Array = getHashes(cashtabState.wallets[0]); if (scripts.length !== hash160Array.length) { // If the websocket is not subscribed to the same amount of addresses as the wallet, // we need to update subscriptions @@ -627,6 +631,7 @@ let tokenCacheForParsingThisTx = cashtabCache.tokens; let thisTokenCachedInfo; + let tokenId; if ( incomingTxDetails.tokenStatus !== 'TOKEN_STATUS_NON_TOKEN' && incomingTxDetails.tokenEntries.length > 0 @@ -634,7 +639,7 @@ // If this is a token tx with at least one tokenId that is NOT cached, get token info // TODO we must get token info for multiple token IDs when we start supporting // token types other than slpv1 - const tokenId = incomingTxDetails.tokenEntries[0].tokenId; + tokenId = incomingTxDetails.tokenEntries[0].tokenId; thisTokenCachedInfo = cashtabCache.tokens.get(tokenId); if (typeof thisTokenCachedInfo === 'undefined') { // If we do not have this token cached @@ -662,38 +667,46 @@ // parse tx for notification const parsedTx = parseTx( incomingTxDetails, - cashtabState.wallets[0], - tokenCacheForParsingThisTx, + getHashes(cashtabState.wallets[0]), ); - if (parsedTx.incoming) { - if (parsedTx.isEtokenTx) { - let eTokenAmountReceived = parsedTx.etokenAmount; - const eTokenReceivedString = `Received ${ - parsedTx.assumedTokenDecimals ? '' : eTokenAmountReceived - } ${ - typeof thisTokenCachedInfo !== 'undefined' - ? `${thisTokenCachedInfo.genesisInfo.tokenTicker} (${thisTokenCachedInfo.genesisInfo.tokenName})` - : '' - }`; + if (parsedTx.xecTxType === 'Received') { + if ( + incomingTxDetails.tokenEntries.length > 0 && + incomingTxDetails.tokenEntries[0].txType === 'SEND' && + incomingTxDetails.tokenEntries[0].burnSummary === '' && + incomingTxDetails.tokenEntries[0].actualBurnAmount === '0' + ) { + // For now, we only parse the first tokenEntry for an incoming tx notification + // Only parse incoming SEND txs + let eTokenReceivedString = ''; + if (typeof thisTokenCachedInfo === 'undefined') { + // If we do not have token name, ticker, or decimals, show a generic notification + eTokenReceivedString = `Received ${tokenId.slice( + 0, + 3, + )}...${tokenId.slice(-3)}`; + } else { + const { tokenTicker, tokenName } = + thisTokenCachedInfo.genesisInfo; + eTokenReceivedString = `Received ${tokenName} (${tokenTicker})`; + // TODO calculate and format decimalized quantity + // TODO test this feature of parseChronikWsMsg, e.g. add a helper function + // getNotification(Tx_InNode) that can be easily tested and called here + } toast(eTokenReceivedString, { - icon: ( - - ), + icon: , }); } else { - const xecAmount = parsedTx.xecAmount; - const xecReceivedString = `Received ${xecAmount.toLocaleString( + const xecReceivedString = `Received ${toFormattedXec( + parsedTx.satoshisSent, locale, )} ${appConfig.ticker}${ settings && typeof settings.fiatCurrency !== 'undefined' ? ` (${ supportedFiatCurrencies[settings.fiatCurrency] .symbol - }${(xecAmount * fiatPrice).toFixed( + }${(toXec(parsedTx.satoshisSent) * fiatPrice).toFixed( appConfig.cashDecimals, )} ${settings.fiatCurrency.toUpperCase()})` : ''