diff --git a/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap b/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap --- a/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap +++ b/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap @@ -3,13 +3,13 @@ exports[`Wallet with BCH balances 1`] = ` Array [

MigrationTestAlpha

@@ -18,14 +18,14 @@ onClick={[Function]} > edit.svg
You currently have 0 XEC @@ -35,7 +35,7 @@
,
,
XEC Airdrop Calculator
@@ -117,13 +117,13 @@ exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

@@ -132,14 +132,14 @@ onClick={[Function]} > edit.svg
You currently have 0 XEC @@ -149,7 +149,7 @@
,
,
XEC Airdrop Calculator
@@ -231,13 +231,13 @@ exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

@@ -246,14 +246,14 @@ onClick={[Function]} > edit.svg
0.06 @@ -262,7 +262,7 @@
,
,
XEC Airdrop Calculator
@@ -344,13 +344,13 @@ exports[`Wallet without BCH balance 1`] = ` Array [

MigrationTestAlpha

@@ -359,14 +359,14 @@ onClick={[Function]} > edit.svg
You currently have 0 XEC @@ -376,7 +376,7 @@
,
,
XEC Airdrop Calculator
@@ -458,24 +458,24 @@ exports[`Without wallet defined 1`] = ` Array [
You currently have 0 XEC @@ -485,7 +485,7 @@
,
,
XEC Airdrop Calculator
diff --git a/web/cashtab/src/components/Common/CustomIcons.js b/web/cashtab/src/components/Common/CustomIcons.js --- a/web/cashtab/src/components/Common/CustomIcons.js +++ b/web/cashtab/src/components/Common/CustomIcons.js @@ -9,6 +9,7 @@ SettingOutlined, LockOutlined, ContactsOutlined, + FireOutlined, } from '@ant-design/icons'; import { Image } from 'antd'; import { currency } from 'components/Common/Ticker'; @@ -50,6 +51,9 @@ preview={false} /> ); +export const ThemedBurnOutlined = styled(FireOutlined)` + color: ${props => props.theme.eCashPurple} !important; +`; export const ThemedCopyOutlined = styled(CopyOutlined)` color: ${props => props.theme.icons.outlined} !important; `; diff --git a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap --- a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap +++ b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap @@ -2,15 +2,15 @@ exports[`Wallet with BCH balances and tokens 1`] = `

@@ -101,13 +101,13 @@ exports[`Wallet with BCH balances and tokens 1`] = ` Array [ ,
@@ -199,13 +199,13 @@ exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [ ,
@@ -290,13 +290,13 @@ onClick={[Function]} >

TBS @@ -316,13 +316,13 @@ exports[`Wallet without BCH balance 1`] = ` Array [ ,
@@ -413,18 +413,18 @@ exports[`Without wallet defined 1`] = `
XEC
eToken @@ -125,7 +125,7 @@ exports[`Wallet with BCH balances and tokens 1`] = `

Receive @@ -153,7 +153,7 @@ } >
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd @@ -228,16 +228,16 @@

XEC
eToken @@ -248,7 +248,7 @@ exports[`Wallet with BCH balances and tokens and state field 1`] = `

Receive @@ -276,7 +276,7 @@ } >
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd @@ -351,16 +351,16 @@

XEC
eToken @@ -371,7 +371,7 @@ exports[`Wallet without BCH balance 1`] = `

Receive @@ -399,7 +399,7 @@ } >
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd @@ -474,16 +474,16 @@

XEC
eToken @@ -494,18 +494,18 @@ exports[`Without wallet defined 1`] = `

Welcome to Cashtab!

Cashtab is an

,

0 XEC

= @@ -378,16 +378,16 @@ } >
Advanced
@@ -449,10 +449,10 @@
Sign Message
@@ -511,10 +511,10 @@
Verify Message
@@ -579,13 +579,13 @@ exports[`Wallet with BCH balances and tokens 1`] = ` Array [
,

0 XEC

= @@ -954,16 +954,16 @@ } >
Advanced
@@ -1025,10 +1025,10 @@
Sign Message
@@ -1087,10 +1087,10 @@
Verify Message
@@ -1155,13 +1155,13 @@ exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
,

0 XEC

= @@ -1529,17 +1529,17 @@ } >
Advanced
@@ -1601,10 +1601,10 @@
Sign Message
@@ -1663,10 +1663,10 @@
Verify Message
@@ -1731,13 +1731,13 @@ exports[`Wallet without BCH balance 1`] = ` Array [
,

0 XEC

= @@ -2106,16 +2106,16 @@ } >
Advanced
@@ -2177,10 +2177,10 @@
Sign Message
@@ -2239,10 +2239,10 @@
Verify Message
@@ -2307,24 +2307,24 @@ exports[`Without wallet defined 1`] = ` Array [
,

0 XEC

= @@ -2677,16 +2677,16 @@ } >
Advanced
@@ -2748,10 +2748,10 @@
Sign Message
@@ -2810,10 +2810,10 @@
Verify Message
diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap @@ -4,10 +4,10 @@ exports[`Wallet with BCH balances and tokens and state field 1`] = `
6.001 @@ -33,7 +33,7 @@ } >
@@ -222,7 +222,7 @@ } >
@@ -135,7 +135,7 @@
@@ -173,7 +173,7 @@
@@ -211,7 +211,7 @@
@@ -317,7 +317,7 @@
,

Create a Token

You need at least @@ -64,13 +64,13 @@ exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

@@ -79,14 +79,14 @@ onClick={[Function]} > edit.svg
0 @@ -94,17 +94,17 @@
,

Create a Token

You need at least @@ -125,13 +125,13 @@ exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

@@ -140,14 +140,14 @@ onClick={[Function]} > edit.svg
0.06 @@ -155,16 +155,16 @@
,

Create a Token

@@ -215,7 +215,7 @@
@@ -253,7 +253,7 @@
@@ -291,7 +291,7 @@
@@ -329,7 +329,7 @@
@@ -367,7 +367,7 @@
@@ -473,7 +473,7 @@
,

Create a Token

You need at least @@ -575,24 +575,24 @@ exports[`Without wallet defined 1`] = ` Array [

0 @@ -600,17 +600,17 @@
,

Create a Token

You need at least diff --git a/web/cashtab/src/utils/__mocks__/chronikTxHistory.js b/web/cashtab/src/utils/__mocks__/chronikTxHistory.js --- a/web/cashtab/src/utils/__mocks__/chronikTxHistory.js +++ b/web/cashtab/src/utils/__mocks__/chronikTxHistory.js @@ -7234,3 +7234,155 @@ isCoinbase: false, network: 'XEC', }; +export const mockTokenBurnTx = { + txid: '312553668f596bfd61287aec1b7f0f035afb5ddadf40b6f9d1ffcec5b7d4b684', + version: 2, + inputs: [ + { + prevOut: { + txid: '842dd09e723d664d7647bc49f911c88b60f0450e646fedb461f319dadb867934', + outIdx: 0, + }, + inputScript: + '473044022025c68cf0ab9c1a4d6b35b2b58f7e397722f469412841eb09d38d1973dc5ef7120220712e1f3c8740fff2af75c1062a773eef167550ee008deaef9089537cd17c35f0412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + value: '2300', + sequenceNo: 4294967295, + }, + { + prevOut: { + txid: '1efe359a0bfa83c409433c487b025fb446a3a9bfa51a718c8dd9a56401656e33', + outIdx: 2, + }, + inputScript: + '47304402206a2f53497eb734ea94ca158951aa005f6569c184675a497d33d061b78c66c25b02201f826fa71be5943ce63740d92a278123974e44846c3766c5cb58ef5ad307ba36412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + value: '546', + sequenceNo: 4294967295, + slpToken: { + amount: '2', + isMintBaton: false, + }, + }, + { + prevOut: { + txid: '49f825370128056333af945eb4f4d9712171c9e88954deb189ca6f479564f2ee', + outIdx: 2, + }, + inputScript: + '483045022100efa3c767b749abb2dc958932348e2b19b845964e581c9f6de706cd43dac3f087022059afad6ff3c1e49cc0320499381e78eab922f18b00e0409228ad417e0220bf5d412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + value: '546', + sequenceNo: 4294967295, + slpBurn: { + token: { + amount: '12', + isMintBaton: false, + }, + tokenId: + '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', + }, + slpToken: { + amount: '999875', + isMintBaton: false, + }, + }, + ], + outputs: [ + { + value: '0', + outputScript: + '6a04534c500001010453454e44204db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c8750800000000000f41b9', + }, + { + value: '546', + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + slpToken: { + amount: '999865', + isMintBaton: false, + }, + }, + ], + lockTime: 0, + slpTxData: { + slpMeta: { + tokenType: 'FUNGIBLE', + txType: 'SEND', + tokenId: + '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', + }, + }, + timeFirstSeen: '1664919857', + size: 550, + isCoinbase: false, + network: 'XEC', +}; + +export const mockTokenBurnWithDecimalsTx = { + txid: 'dacd4bacb46caa3af4a57ac0449b2cb82c8a32c64645cd6a64041287d1ced556', + version: 2, + inputs: [ + { + prevOut: { + txid: 'eb79e90e3b5a0b6766cbfab3efd9c52f831bef62f9f27c2aa925ee81e43b843f', + outIdx: 0, + }, + inputScript: + '47304402207122751937862fad68c3e293982cf7afb91967d20da63a0c23bf0565b625b775022054f39f41a43438a0df7fbe6a78521f572613bc08d6a43b6d248bcb6a434e2b52412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + value: '2200', + sequenceNo: 4294967295, + }, + { + prevOut: { + txid: '905cc5662cad77df56c3770863634ce498dde9d4772dc494d33b7ce3f36fa66c', + outIdx: 2, + }, + inputScript: + '483045022100dce5b3b516bfebd40bd8d4b4ff9c43c685d3c9dde1def0cc0667389ac522cf2502202651f95638e48c210a04082e6053457a539aef0f65a2e9c2f61e3faf96c1dfd8412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + value: '546', + sequenceNo: 4294967295, + slpBurn: { + token: { + amount: '1234567', + isMintBaton: false, + }, + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + }, + slpToken: { + amount: '5235120760000000', + isMintBaton: false, + }, + }, + ], + outputs: [ + { + value: '0', + outputScript: + '6a04534c500001010453454e44207443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d0800129950892eb779', + }, + { + value: '546', + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + slpToken: { + amount: '5235120758765433', + isMintBaton: false, + }, + }, + ], + lockTime: 0, + slpTxData: { + slpMeta: { + tokenType: 'FUNGIBLE', + txType: 'SEND', + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + }, + }, + timeFirstSeen: '1664923127', + size: 403, + isCoinbase: false, + network: 'XEC', +}; diff --git a/web/cashtab/src/utils/__tests__/chronik.test.js b/web/cashtab/src/utils/__tests__/chronik.test.js --- a/web/cashtab/src/utils/__tests__/chronik.test.js +++ b/web/cashtab/src/utils/__tests__/chronik.test.js @@ -50,6 +50,8 @@ mockWalletWithPrivateKeys, mockSentEncryptedTx, mockReceivedEncryptedTx, + mockTokenBurnTx, + mockTokenBurnWithDecimalsTx, } from '../__mocks__/chronikTxHistory'; import { ChronikClient } from 'chronik-client'; import { when } from 'jest-when'; @@ -312,6 +314,7 @@ incoming: true, xecAmount: '5.46', isEtokenTx: true, + isTokenBurn: false, originatingHash160: '4e532257c01b310b3b5c1fd947c79a72addf8523', slpMeta: { tokenId: @@ -367,6 +370,7 @@ incoming: false, xecAmount: '5.46', isEtokenTx: true, + isTokenBurn: false, originatingHash160: '76458db0ed96fe9863fc1ccec9fa2cfab884b0f6', slpMeta: { tokenId: @@ -423,6 +427,7 @@ xecAmount: '0', originatingHash160: '95e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d', isEtokenTx: true, + isTokenBurn: false, etokenAmount: '777.7777777', slpMeta: { tokenType: 'FUNGIBLE', @@ -477,6 +482,7 @@ xecAmount: '5.46', originatingHash160: '4e532257c01b310b3b5c1fd947c79a72addf8523', isEtokenTx: true, + isTokenBurn: false, etokenAmount: '0.123456789', slpMeta: { tokenType: 'FUNGIBLE', @@ -623,3 +629,117 @@ }, }); }); + +it(`Correctly parses a token burn transaction`, () => { + const BCH = new BCHJS({ + restURL: 'https://FakeBchApiUrlToEnsureMocksOnly.com', + }); + // This function needs to be mocked as bch-js functions that require Buffer types do not work in jest environment + BCH.Address.hash160ToCash = jest + .fn() + .mockReturnValue( + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + ); + expect( + parseChronikTx( + BCH, + mockTokenBurnTx, + anotherMockParseTxWallet, + txHistoryTokenInfoById, + ), + ).toStrictEqual({ + incoming: false, + xecAmount: '0', + originatingHash160: '95e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d', + isEtokenTx: true, + isTokenBurn: true, + etokenAmount: '12', + slpMeta: { + tokenType: 'FUNGIBLE', + txType: 'SEND', + tokenId: + '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', + }, + genesisInfo: { + tokenTicker: 'LVV', + tokenName: 'Lambda Variant Variants', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 0, + tokenId: + '4db25a4b2f0b57415ce25fab6d9cb3ac2bbb444ff493dc16d0615a11ad06c875', + success: true, + }, + legacy: { + amountSent: '0', + amountReceived: 0, + outgoingTx: true, + tokenTx: true, + airdropFlag: false, + airdropTokenId: '', + opReturnMessage: '', + isCashtabMessage: false, + isEncryptedMessage: false, + decryptionSuccess: false, + replyAddress: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + }, + }); +}); +it(`Correctly parses a token burn transaction with decimal places`, () => { + const BCH = new BCHJS({ + restURL: 'https://FakeBchApiUrlToEnsureMocksOnly.com', + }); + // This function needs to be mocked as bch-js functions that require Buffer types do not work in jest environment + BCH.Address.hash160ToCash = jest + .fn() + .mockReturnValue( + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + ); + expect( + parseChronikTx( + BCH, + mockTokenBurnWithDecimalsTx, + anotherMockParseTxWallet, + txHistoryTokenInfoById, + ), + ).toStrictEqual({ + incoming: false, + xecAmount: '0', + originatingHash160: '95e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d', + isEtokenTx: true, + etokenAmount: '0.1234567', + isTokenBurn: true, + slpMeta: { + tokenType: 'FUNGIBLE', + txType: 'SEND', + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + }, + genesisInfo: { + tokenTicker: 'WDT', + tokenName: + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + tokenDocumentUrl: + 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', + tokenDocumentHash: + '85b591c15c9f49531e39fcfeb2a5a26b2bd0f7c018fb9cd71b5d92dfb732d5cc', + decimals: 7, + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + success: true, + }, + legacy: { + amountSent: '0', + amountReceived: 0, + outgoingTx: true, + tokenTx: true, + airdropFlag: false, + airdropTokenId: '', + opReturnMessage: '', + isCashtabMessage: false, + isEncryptedMessage: false, + decryptionSuccess: false, + replyAddress: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + }, + }); +}); diff --git a/web/cashtab/src/utils/chronik.js b/web/cashtab/src/utils/chronik.js --- a/web/cashtab/src/utils/chronik.js +++ b/web/cashtab/src/utils/chronik.js @@ -432,6 +432,7 @@ let xecAmount = new BigNumber(0); let originatingHash160 = ''; let etokenAmount = new BigNumber(0); + let isTokenBurn = false; const isEtokenTx = 'slpTxData' in tx && typeof tx.slpTxData !== 'undefined'; const isGenesisTx = isEtokenTx && @@ -456,6 +457,23 @@ for (let i = 0; i < inputs.length; i += 1) { const thisInput = inputs[i]; const thisInputSendingHash160 = thisInput.outputScript; + // If this is an etoken tx, check for token burn + if (isEtokenTx && typeof thisInput.slpBurn !== 'undefined') { + console.log(`Token burn at ${tx.txid}`); + // Assume that any eToken tx with a burn is a burn tx + isTokenBurn = true; + try { + const thisEtokenBurnAmount = new BigNumber( + thisInput.slpBurn.token.amount, + ); + // Need to know the total output amount to compare to total input amount and tell if this is a burn transaction + etokenAmount = etokenAmount.plus(thisEtokenBurnAmount); + } catch (err) { + // do nothing + // If this happens, the burn amount will render wrong in tx history because we don't have the info in chronik + // This is acceptable + } + } /* Assume the first input is the originating address @@ -637,7 +655,7 @@ // 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) { + if (isEtokenTx && !isTokenBurn) { try { const thisEtokenAmount = new BigNumber( thisOutput.slpToken.amount, @@ -659,7 +677,7 @@ if (!incoming) { const thisOutputAmount = new BigNumber(thisOutput.value); xecAmount = xecAmount.plus(thisOutputAmount); - if (isEtokenTx && !isGenesisTx) { + if (isEtokenTx && !isGenesisTx && !isTokenBurn) { try { const thisEtokenAmount = new BigNumber( thisOutput.slpToken.amount, @@ -681,6 +699,7 @@ // Get decimal info for correct etokenAmount let genesisInfo = {}; + if (isEtokenTx) { // Get token genesis info from cache @@ -703,6 +722,9 @@ } } etokenAmount = etokenAmount.toString(); + if (isTokenBurn) { + console.log(`${etokenAmount} of ${genesisInfo.tokenName} burned`); + } // Convert opReturnMessage to string opReturnMessage = Buffer.from(opReturnMessage).toString(); @@ -716,6 +738,7 @@ originatingHash160, isEtokenTx, etokenAmount, + isTokenBurn, slpMeta, genesisInfo, legacy: {