Page MenuHomePhabricator

No OneTemporary

diff --git a/cashtab/src/assets/airdrop-icon.svg b/cashtab/src/assets/airdrop-icon.svg
index ad707930c..0684644d8 100644
--- a/cashtab/src/assets/airdrop-icon.svg
+++ b/cashtab/src/assets/airdrop-icon.svg
@@ -1,34 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
- width="249.553px" height="309.122px" viewBox="0 0 249.553 309.122" enable-background="new 0 0 249.553 309.122"
- xml:space="preserve">
-<g id="VM3YWc.tif">
-
- <image overflow="visible" width="1024" height="1024" id="Layer_0_1_" xlink:href="BAE1798302874AFB.png" transform="matrix(0.4382 0 0 0.4382 -947.2231 -73.2114)">
- </image>
-</g>
-<g id="YpnODz.tif">
-
- <image overflow="visible" width="420" height="512" id="Layer_0_2_" xlink:href="BAE1798302874AF9.png" transform="matrix(1 0 0 1 -847.0186 -560.8555)">
- </image>
-</g>
-<path d="M248.686,139.521c8.057-68.52-40.959-130.598-109.478-138.654C70.688-7.19,8.611,41.824,0.555,110.345
- c-0.235,1.993-0.416,3.98-0.555,5.961l89.803,78.08l-27.413-3.224L49.979,296.71l105.548,12.412l12.41-105.549l-25.021-2.942
- l105.354-57.929C248.421,141.646,248.56,140.586,248.686,139.521z M140.004,289.461l-70.366-8.273l8.274-70.365l70.366,8.273
- L140.004,289.461z M136.603,17.93c21.269,2.501,17.834,92.839,17.834,92.839s-23.843-21.37-58.193-6.842
- C96.244,103.927,115.334,15.43,136.603,17.93z M152.213,129.674l-35.235,55.153l-20.89-61.752
- C96.089,123.075,123.072,104.985,152.213,129.674z M108.419,18.21c-17.002,18.204-29.308,83.702-29.308,83.702
- c-27.629-19.72-57.724-8.285-57.724-8.285C42.808,26.068,108.419,18.21,108.419,18.21z M77.914,124.832l17.049,51.12L23.96,112.498
- C23.96,112.498,53.394,96.493,77.914,124.832z M139.862,181.23l28.772-46.031c0,0,26.341-20.262,55.639,0.553L139.862,181.23z
- M172.751,112.922c0,0,5.105-48.514-8.208-88.113c0,0,57.69,18.763,66.278,93.444C230.822,118.253,207.258,99.61,172.751,112.922z"
- />
-<path d="M-169.542-694.849c-115.98,0-210,94.02-210,210c0,3.374,0.087,6.727,0.245,10.062l189.755,129.438l49-5L40.39-479.456
- c0.045-1.792,0.068-3.59,0.068-5.393C40.458-600.829-53.562-694.849-169.542-694.849z M-170.542-665.849c36,0,48,151.5,48,151.5
- s-44-31-98.5,0C-221.042-514.349-206.542-665.849-170.542-665.849z M-217.542-659.849c-24.813,33.73-32.5,145.5-32.5,145.5
- c-50-27.5-98-2.5-98-2.5C-325.542-633.849-217.542-659.849-217.542-659.849z M-340.042-485.849c0,0,46-32.5,92.5,10l38.5,82
- L-340.042-485.849z M-170.542-383.349l-47-99c0,0,41.5-35.5,95,0L-170.542-383.349z M-133.042-393.849l39-82.5c0,0,40-39,93-10
- L-133.042-393.849z M-91.542-514.349c0,0-1-82-31-145.5c0,0,100,20,129,143C6.458-516.849-36.542-543.349-91.542-514.349z"/>
-<path d="M-260.556-361.878v179h179v-179H-260.556z M-111.556-212.878h-119v-120h119V-212.878z"/>
-</svg>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="128pt" height="128pt" version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
+ <path d="m58.504 74.941-33.625-36.773c-0.59375-0.65234-0.76562-1.543-0.52734-2.3281 2.6289-8.7344 7.9844-16.094 14.957-21.27 6.9727-5.1758 15.566-8.168 24.688-8.168s17.715 2.9922 24.688 8.168c6.9727 5.1758 12.324 12.535 14.969 21.266 0.28125 0.92969-0.023438 1.8945-0.69531 2.5039l-33.469 36.605h7.5195c2.8398 0 5.4219 1.1602 7.2891 3.0312 1.8672 1.8711 3.0312 4.4531 3.0312 7.2891v26.02c0 2.8398-1.1641 5.4219-3.0312 7.2891-1.8672 1.8672-4.4492 3.0312-7.2891 3.0312h-26.016c-2.8359 0-5.418-1.1641-7.2891-3.0312-1.8672-1.8672-3.0312-4.4492-3.0312-7.2891v-26.02c0-2.8359 1.1602-5.418 3.0312-7.2891 1.8711-1.8672 4.4531-3.0312 7.2891-3.0312h7.5195zm12.801 18.211c-0.10938-1.5195-0.69141-2.9297-1.6094-4.0703-0.83984-1.0391-1.9648-1.8516-3.2734-2.3086v-0.69531c0-1.3398-1.0859-2.4258-2.4258-2.4258s-2.4258 1.0859-2.4258 2.4258l-0.019531 0.70703c-1.3477 0.48438-2.543 1.3477-3.4258 2.5352-0.91016 1.2305-1.3945 2.6836-1.4336 4.1445-0.046875 1.457 0.34766 2.9297 1.1797 4.207 1.5273 2.3398 3.6406 2.6641 5.7539 2.9922 1.043 0.16016 2.0938 0.32422 2.4375 0.85156 0.28516 0.4375 0.42188 0.94531 0.40625 1.4375-0.011719 0.48828-0.17969 0.98047-0.48828 1.3945-0.30859 0.41406-0.73047 0.71484-1.1992 0.87109-0.47266 0.16016-0.99219 0.17578-1.4922 0.035156-0.49219-0.14453-0.92187-0.43359-1.2305-0.81641s-0.50781-0.86719-0.54297-1.3828c-0.09375-1.332-1.2539-2.3398-2.5859-2.2461-1.332 0.09375-2.3398 1.2539-2.2461 2.5859 0.10938 1.5234 0.6875 2.9375 1.6055 4.0703 0.83594 1.0391 1.9648 1.8516 3.2734 2.3086v0.69531c0 1.3398 1.0859 2.4258 2.4258 2.4258 1.3398 0 2.4258-1.0859 2.4258-2.4258v-0.69922c1.3477-0.47656 2.5586-1.3438 3.4453-2.543 0.91016-1.2266 1.3906-2.6797 1.4375-4.1445 0.039063-1.457-0.34766-2.9297-1.1836-4.207-1.5273-2.3359-3.6445-2.6641-5.7539-2.9922-1.043-0.16016-2.0938-0.32422-2.4375-0.85156-0.28516-0.4375-0.41797-0.94531-0.40625-1.4375 0.015625-0.48828 0.17969-0.98047 0.48828-1.3945 0.29297-0.39453 0.6875-0.68359 1.1641-0.85938 0.51172-0.17188 1.0312-0.1875 1.5234-0.046875 0.49219 0.14062 0.92188 0.43359 1.2383 0.82812 0.30469 0.37891 0.5 0.85547 0.53516 1.3711 0.09375 1.332 1.2539 2.3398 2.5859 2.2461s2.3398-1.2539 2.2461-2.5859zm-9.7344-28.566v-26.652c-1.9688-0.875-4.0898-1.3125-6.2148-1.3125-1.7617 0-3.5195 0.30078-5.1914 0.90234zm4.8516-26.652v26.652l11.406-27.062c-1.6719-0.60156-3.4297-0.90234-5.1914-0.90234-2.125 0-4.2422 0.4375-6.2148 1.3125zm16.273 0.49219-11.664 27.668 25.555-27.949c-2.0977-1.0156-4.3828-1.5234-6.668-1.5234-2.4922 0-4.9766 0.60156-7.2266 1.8047zm-25.73 27.672-11.664-27.672c-2.25-1.2031-4.7344-1.8047-7.2266-1.8047-2.2891 0-4.5703 0.50781-6.668 1.5234z" fill="#00abe6"/>
+</svg>
diff --git a/cashtab/src/components/Home/Tx/__tests__/index.test.js b/cashtab/src/components/Home/Tx/__tests__/index.test.js
index 58cebaae7..8e880c47e 100644
--- a/cashtab/src/components/Home/Tx/__tests__/index.test.js
+++ b/cashtab/src/components/Home/Tx/__tests__/index.test.js
@@ -1,1969 +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('<Tx />', () => {
it('Incoming XEC-only', async () => {
render(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={incomingXec.tx}
hashes={getHashes(mockParseTxWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={new CashtabState()}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={outgoingXec.tx}
hashes={getHashes(mockParseTxWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={new CashtabState()}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={stakingRwd.tx}
hashes={getHashes(mockParseTxWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={new CashtabState()}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={aliasRegistration.tx}
hashes={getHashes(mockAliasWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={new CashtabState()}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={{
...aliasRegistration.tx,
outputs: [
{
...aliasRegistration.tx.outputs[0],
outputScript:
'6a042e786563010104627567321500dc1147663948f0dcfb00cc407eda41b121713ad3',
},
...aliasRegistration.tx.outputs.slice(1),
],
}}
hashes={getHashes(mockAliasWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={new CashtabState()}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={{
...aliasRegistration.tx,
outputs: [
{
...aliasRegistration.tx.outputs[0],
outputScript:
'6a042e7865630d616e64616e6f746865726f6e65',
},
...aliasRegistration.tx.outputs.slice(1),
],
}}
hashes={getHashes(mockAliasWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={new CashtabState()}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={incomingEtoken.tx}
hashes={getHashes(mockParseTxWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: { tokens: mockParseTxTokenCache },
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={incomingEtoken.tx}
hashes={getHashes(mockParseTxWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={outgoingEtoken.tx}
hashes={getHashes(mockParseTxWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: { tokens: mockParseTxTokenCache },
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={outgoingEtoken.tx}
hashes={getHashes(mockParseTxWallet)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={genesisTx.tx}
hashes={getHashes(mockParseTxWalletAirdrop)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: { tokens: mockParseTxTokenCache },
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={genesisTx.tx}
hashes={getHashes(mockParseTxWalletAirdrop)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={incomingEtokenNineDecimals.tx}
hashes={getHashes(mockParseTxWalletAirdrop)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: { tokens: mockParseTxTokenCache },
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={incomingEtokenNineDecimals.tx}
hashes={getHashes(mockParseTxWalletAirdrop)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={legacyAirdropTx.tx}
hashes={getHashes(mockParseTxWalletAirdrop)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: { tokens: mockParseTxTokenCache },
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={onSpecAirdropTxNoMsg.tx}
hashes={['2a96944d06700882bbd984761d9c9e4215f2d78e']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: { tokens: mockLargeTokenCache },
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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();
+ expect(screen.getByText('Airdrop (XEC)')).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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={{
...onSpecAirdropTxNoMsg.tx,
outputs: [
{
...onSpecAirdropTxNoMsg.tx.outputs[0],
outputScript:
'6a0464726f701ffb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87',
},
...onSpecAirdropTxNoMsg.tx.outputs.slice(1),
],
}}
hashes={['2a96944d06700882bbd984761d9c9e4215f2d78e']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: { tokens: mockLargeTokenCache },
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={outgoingEncryptedMsg.tx}
hashes={getHashes(mockParseTxWalletEncryptedMsg)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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.getByTitle('tx-encrypted-msg')).toBeInTheDocument();
// We see expected text msg
expect(screen.getByText('Encrypted Cashtab Msg')).toBeInTheDocument();
});
it('Received encrypted msg', async () => {
render(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={incomingEncryptedMsg.tx}
hashes={getHashes(mockParseTxWalletEncryptedMsg)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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.getByTitle('tx-encrypted-msg')).toBeInTheDocument();
// We see expected text msg
expect(screen.getByText('Encrypted Cashtab Msg')).toBeInTheDocument();
});
it('Burn slpv1 fungible token', async () => {
render(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={tokenBurn.tx}
hashes={getHashes(mockParseTxWalletAirdrop)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: { tokens: mockParseTxTokenCache },
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={tokenBurn.tx}
hashes={getHashes(mockParseTxWalletAirdrop)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={tokenBurnDecimals.tx}
hashes={getHashes(mockParseTxWalletAirdrop)}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: { tokens: mockParseTxTokenCache },
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={swapTx.tx}
hashes={['93472d56ba91581ed473225a765dd14a2db5d9d8']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={PayButtonNoDataYesNonce.tx}
hashes={['f66d2760b20dc7a47d9cf1a2b2f49749bf7093f6']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={PayButtonYesDataYesNonce.tx}
hashes={['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={PayButtonEmpty.tx}
hashes={['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={PayButtonYesDataNoNonce.tx}
hashes={['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={PayButtonOffSpec.tx}
hashes={['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={PayButtonBadVersion.tx}
hashes={['e628f12f1e911c9f20ec2eeb1847e3a2ffad5fcc']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={MsgFromEcashChat.tx}
hashes={['0b7d35fda03544a08e65464d54cfae4257eb6db7']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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.getByTitle('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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={{
...MsgFromEcashChat.tx,
outputs: [
{
...MsgFromEcashChat.tx.outputs[0],
outputScript: '6a0463686174',
},
...MsgFromEcashChat.tx.outputs.slice(1),
],
}}
hashes={['0b7d35fda03544a08e65464d54cfae4257eb6db7']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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.getByTitle('tx-chat')).toBeInTheDocument();
// We see expected protocol label
expect(screen.getByText('Invalid eCash Chat')).toBeInTheDocument();
});
it('slpv1 fungible token MINT', async () => {
render(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={SlpV1Mint.tx}
hashes={['95e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: {
tokens: mockTxHistorySupportingTokenCache,
},
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={SlpV1Mint.tx}
hashes={['95e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={MsgFromElectrum.tx}
hashes={['4e532257c01b310b3b5c1fd947c79a72addf8523']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={unknownAppTx.tx}
hashes={['d18b7b500f17c5db64303fec630f9dbb85aa9596']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={AlpTx.tx}
hashes={['dee50f576362377dd2f031453c0bb09009acaf81']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
cashtabCache: {
tokens: mockTxHistorySupportingTokenCache,
},
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={AlpTx.tx}
hashes={['dee50f576362377dd2f031453c0bb09009acaf81']}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={CashtabMsg.tx}
hashes={[CashtabMsg.tx.outputs[1].outputScript]}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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(
<MemoryRouter>
<ThemeProvider theme={theme}>
<Tx
tx={{
...CashtabMsg.tx,
outputs: [
{
...CashtabMsg.tx.outputs[0],
outputScript: '6a0400746162',
},
...CashtabMsg.tx.outputs.slice(1),
],
}}
hashes={[CashtabMsg.tx.outputs[1].outputScript]}
fiatPrice={0.00003}
fiatCurrency="usd"
cashtabState={{
...new CashtabState(),
}}
/>
,
</ThemeProvider>
</MemoryRouter>,
);
// 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
index d9c7a434b..987e816a3 100644
--- a/cashtab/src/components/Home/Tx/index.js
+++ b/cashtab/src/components/Home/Tx/index.js
@@ -1,897 +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(
<IconAndLabel>
<AliasIconTx />
<AppDescLabel>
Invalid alias registration
</AppDescLabel>
</IconAndLabel>,
);
break;
}
const aliasAddress = cashaddr.encode(
'ecash',
addressType,
stackArray[3].slice(1),
);
const aliasAddrPreview = `${aliasAddress.slice(
6,
9,
)}...${aliasAddress.slice(-3)}`;
appActions.push(
<>
<IconAndLabel>
<AliasIconTx />
<AppDescLabel>Alias Registration</AppDescLabel>
</IconAndLabel>
<AppDescMsg>
{`${Buffer.from(stackArray[2], 'hex').toString(
'utf8',
)} to ${aliasAddrPreview}`}
</AppDescMsg>
</>,
);
break;
}
appActions.push(
<IconAndLabel>
<AliasIconTx />
<AppDescLabel>Invalid alias registration</AppDescLabel>
</IconAndLabel>,
);
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(
<>
<IconAndLabel>
<AirdropIcon />
- <AppDescLabel>Airdrop</AppDescLabel>
+ <AppDescLabel>Airdrop (XEC)</AppDescLabel>
</IconAndLabel>
<TokenIcon
size={32}
tokenId={airdroppedTokenId}
/>
<TokenInfoCol>
<TokenType>{parsedTokenType}</TokenType>
<TokenName>
<ActionLink
href={`${explorer.blockExplorerUrl}/tx/${airdroppedTokenId}`}
target="_blank"
rel="noreferrer"
>
{tokenName}
</ActionLink>
</TokenName>
<TokenTicker>({tokenTicker})</TokenTicker>
</TokenInfoCol>
{airdropMsg !== '' && (
<AppDescMsg>{airdropMsg}</AppDescMsg>
)}
</>,
);
break;
}
appActions.push(
<>
<IconAndLabel>
<AirdropIcon />
<AppDescLabel>
Airdrop to holders of{' '}
<ActionLink
href={`${explorer.blockExplorerUrl}/tx/${airdroppedTokenId}`}
target="_blank"
rel="noreferrer"
>
{airdroppedTokenId}
</ActionLink>
</AppDescLabel>
</IconAndLabel>
<TokenIcon size={32} tokenId={airdroppedTokenId} />
{airdropMsg !== '' && (
<AppDescMsg>{airdropMsg}</AppDescMsg>
)}
</>,
);
break;
}
// off-spec airdrop
appActions.push(
<>
<IconAndLabel>
<AirdropIcon />
<AppDescLabel>
Off-spec airdrop: tokenId unavailable
</AppDescLabel>
</IconAndLabel>
</>,
);
break;
}
case opReturn.appPrefixesHex.cashtabEncrypted: {
// Deprecated, we do not parse
// Just render the lokad
// <lokad> <encrypted utf8>
appActions.push(
<IconAndLabel>
<EncryptedMsgIcon />
<AppDescLabel>Encrypted Cashtab Msg</AppDescLabel>
</IconAndLabel>,
);
break;
}
case opReturn.appPrefixesHex.swap: {
// Limited parse support for SWaP txs, which are not expected in Cashtab
// Just print the type
appActions.push(
<IconAndLabel>
<SwapIcon />
<AppDescLabel>SWaP</AppDescLabel>
</IconAndLabel>,
);
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(
<>
<IconAndLabel>
<PayButtonIcon />
</IconAndLabel>
{stackArray[2] !== '00' && (
<AppDescMsg>
{Buffer.from(stackArray[2], 'hex').toString(
'utf8',
)}
</AppDescMsg>
)}
{stackArray[3] !== '00' && (
<AppDescMsg>{stackArray[3]}</AppDescMsg>
)}
</>,
);
} else {
appActions.push(
<IconAndLabel>
<PayButtonIcon />
<AppDescLabel>(Invalid)</AppDescLabel>
</IconAndLabel>,
);
}
break;
}
case opReturn.appPrefixesHex.eCashChat: {
if (typeof stackArray[1] !== 'undefined') {
appActions.push(
<>
<IconAndLabel>
<ChatIcon />
<AppDescLabel>eCash Chat</AppDescLabel>
</IconAndLabel>
<AppDescMsg>
{Buffer.from(stackArray[1], 'hex').toString(
'utf8',
)}
</AppDescMsg>
{xecTxType === 'Received' &&
typeof replyAddress !== 'undefined' && (
<PanelLink
to="/send"
state={{
replyAddress: replyAddress,
}}
>
<ReplyIcon />
</PanelLink>
)}
</>,
);
} else {
appActions.push(
<IconAndLabel>
<ChatIcon />
<AppDescLabel>Invalid eCash Chat</AppDescLabel>
</IconAndLabel>,
);
}
break;
}
case opReturn.appPrefixesHex.cashtab: {
if (typeof stackArray[1] !== 'undefined') {
appActions.push(
<>
<IconAndLabel>
<CashtabMsgIcon />
<AppDescLabel>Cashtab Msg</AppDescLabel>
</IconAndLabel>
<AppDescMsg>
{Buffer.from(stackArray[1], 'hex').toString(
'utf8',
)}
</AppDescMsg>
{xecTxType === 'Received' &&
typeof replyAddress !== 'undefined' && (
<ReplyLink
to="/send"
state={{
replyAddress: replyAddress,
}}
>
<ReplyIcon />
</ReplyLink>
)}
</>,
);
} else {
appActions.push(
<>
<IconAndLabel>
<CashtabMsgIcon />
<AppDescLabel>Invalid Cashtab Msg</AppDescLabel>
</IconAndLabel>
</>,
);
}
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(
<>
<IconAndLabel>
<UnknownIcon />
<AppDescLabel>Unknown App</AppDescLabel>
</IconAndLabel>
<UnknownMsgColumn>
<AppDescMsg>{stackArray.join(' ')}</AppDescMsg>
<AppDescMsg>{decodedTest.join(' ')}</AppDescMsg>
</UnknownMsgColumn>
</>,
);
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 = <TokenIcon size={32} tokenId={tokenId} />;
// 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(
<TokenAction>
{tokenIcon}
<TokenType>{parsedTokenType}</TokenType>
<TokenDesc>Unknown token action</TokenDesc>
</TokenAction>,
);
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(
<TokenAction tokenTxType={renderedTxType}>
<IconAndLabel>
{txType === 'GENESIS' && <GenesisIcon />}
{txType === 'MINT' && <MintIcon />}
{(isUnintentionalBurn || txType === 'BURN') && (
<TokenBurnIcon />
)}
{tokenIcon}
<TokenInfoCol>
<TokenType>{parsedTokenType}</TokenType>
<TokenType>{txType}</TokenType>
</TokenInfoCol>
</IconAndLabel>
<TokenDesc>{renderedTxType}</TokenDesc>
</TokenAction>,
);
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(
<TokenAction tokenTxType={renderedTxType}>
<IconAndLabel>
{txType === 'GENESIS' && <GenesisIcon />}
{txType === 'MINT' && <MintIcon />}
{(isUnintentionalBurn || txType === 'BURN') && (
<TokenBurnIcon />
)}
{tokenIcon}
<TokenInfoCol>
<TokenType>{parsedTokenType}</TokenType>
<TokenType>{txType}</TokenType>
</TokenInfoCol>
</IconAndLabel>
<TokenInfoCol>
<TokenName>{tokenName}</TokenName>
<TokenTicker>({tokenTicker})</TokenTicker>
</TokenInfoCol>
<TokenDesc>
{renderedTxType} {formattedAmount} {tokenTicker}
</TokenDesc>
</TokenAction>,
);
}
}
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 && (
<Modal
height={180}
title={`Add new contact`}
handleOk={
xecTxType === 'Sent' &&
typeof recipients[0] !== 'undefined'
? () => addNewContact(recipients[0])
: () => addNewContact(replyAddress)
}
handleCancel={() => setShowAddNewContactModal(false)}
showCancelButton
>
<ModalInput
placeholder="Enter new contact name"
name="newContactName"
value={formData.newContactName}
error={formDataErrors.newContactName}
handleInput={handleInput}
/>
</Modal>
)}
<TxWrapper>
{/* We always display the MainRow, which is the XEC tx summary*/}
<Collapse onClick={() => setShowPanel(!showPanel)}>
<MainRow type={xecTxType}>
<MainRowLeft>
{xecTxType === 'Received' ? (
<ReceiveIcon />
) : xecTxType === 'Sent' ? (
<SendIcon />
) : (
<MinedIcon />
)}
<TxDescCol>
<TxDesc>
{xecTxType}
{typeof replyAddress === 'string' ? (
<>
{' from'}
<AddressLink
href={`${explorer.blockExplorerUrl}/address/${replyAddress}`}
target="_blank"
rel="noreferrer"
>
{typeof knownSender ===
'undefined'
? replyAddressPreview
: knownSender.name}
</AddressLink>
</>
) : xecTxType === 'Sent' &&
typeof recipients[0] !== 'undefined' ? (
<>
{' to'}
<AddressLink
href={`${explorer.blockExplorerUrl}/address/${recipients[0]}`}
target="_blank"
rel="noreferrer"
>
{renderedRecipient}
</AddressLink>
{renderedOtherRecipients !== '' && (
<AddressLink
href={`${explorer.blockExplorerUrl}/tx/${txid}`}
target="_blank"
rel="noreferrer"
>
{renderedOtherRecipients}
</AddressLink>
)}
</>
) : xecTxType === 'Sent' ||
xecTxType === 'Received' ? (
' to self'
) : (
''
)}
</TxDesc>
<Timestamp>{renderedTimestamp}</Timestamp>
</TxDescCol>
</MainRowLeft>
<AmountCol>
<AmountTop>
{xecTxType === 'Sent' ? '-' : ''}
{toFormattedXec(satoshisSent, userLocale)} XEC
</AmountTop>
<AmountBottom>
{xecTxType === 'Sent' ? '-' : ''}
{supportedFiatCurrencies[fiatCurrency].symbol}
{(
fiatPrice * toXec(satoshisSent)
).toLocaleString(userLocale, {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
})}
</AmountBottom>
</AmountCol>
</MainRow>
{appActions.map((action, index) => {
return <AppAction key={index}>{action}</AppAction>;
})}
{tokenActions.map((action, index) => {
return (
<TokenActionHolder key={index}>
{action}
</TokenActionHolder>
);
})}
</Collapse>
<Expand showPanel={showPanel}>
<CopyToClipboard
data={txid}
showToast
customMsg={`Txid "${txid}" copied to clipboard`}
>
<PanelButton>
<CopyPasteIcon />
</PanelButton>
</CopyToClipboard>
<PanelLink
to={`${explorer.blockExplorerUrl}/tx/${txid}`}
target="_blank"
rel="noreferrer"
>
<ThemedLinkSolid />
</PanelLink>
<PanelLink
to={`${explorer.pdfReceiptUrl}/${txid}.pdf`}
target="_blank"
rel="noreferrer"
>
<ThemedPdfSolid />
</PanelLink>
{senderOrRecipientNotInContacts && (
<PanelButton
onClick={() => {
setShowAddNewContactModal(true);
}}
>
<AddContactIcon />
</PanelButton>
)}
</Expand>
</TxWrapper>
</>
);
};
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;

File Metadata

Mime Type
image/svg+xml
Expires
Mon, Dec 2, 21:52 (2 d)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4665493
Default Alt Text
(115 KB)

Event Timeline