diff --git a/cashtab/extension/public/manifest.json b/cashtab/extension/public/manifest.json --- a/cashtab/extension/public/manifest.json +++ b/cashtab/extension/public/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Cashtab", "description": "A browser-integrated eCash wallet from Bitcoin ABC", - "version": "3.26.0", + "version": "3.27.0", "content_scripts": [ { "matches": ["file://*/*", "http://*/*", "https://*/*"], diff --git a/cashtab/package-lock.json b/cashtab/package-lock.json --- a/cashtab/package-lock.json +++ b/cashtab/package-lock.json @@ -1,12 +1,12 @@ { "name": "cashtab", - "version": "2.26.5", + "version": "2.27.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cashtab", - "version": "2.26.5", + "version": "2.27.0", "dependencies": { "@bitgo/utxo-lib": "^9.33.0", "@zxing/browser": "^0.1.4", diff --git a/cashtab/package.json b/cashtab/package.json --- a/cashtab/package.json +++ b/cashtab/package.json @@ -1,6 +1,6 @@ { "name": "cashtab", - "version": "2.26.5", + "version": "2.27.0", "private": true, "scripts": { "start": "node scripts/start.js", diff --git a/cashtab/src/components/App/fixtures/mocks.js b/cashtab/src/components/App/fixtures/mocks.js --- a/cashtab/src/components/App/fixtures/mocks.js +++ b/cashtab/src/components/App/fixtures/mocks.js @@ -2394,3 +2394,107 @@ name: 'gamma', }, ]; + +export const MOCK_CHRONIK_TOKEN_CALL = { + tokenId: '16b12bbacdbb8c8a799adbfd782bfff9843c1f9b0be148eaae02a1a7f74f95c4', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + timefirstSeen: 0, + genesisInfo: { + tokenTicker: 'CGEN', + tokenName: 'Cashtab Genesis', + url: 'https://boomertakes.com/', + decimals: 9, + hash: '', + }, + block: { + height: 684837, + hash: '00000000000000001d065fdd22416c4e8e99803964f4fb9c91af6feb5ead5ff3', + timestamp: 1620082584, + }, +}; + +export const MOCK_CHRONIK_GENESIS_TX_CALL = { + txid: '16b12bbacdbb8c8a799adbfd782bfff9843c1f9b0be148eaae02a1a7f74f95c4', + version: 2, + inputs: [ + { + prevOut: { + txid: '11ae0a8c62deeadbffe82ddea823e731dba7172a672bd98628bf8bd3c0e15b50', + outIdx: 3, + }, + inputScript: + '473044022009777275694aab45f8c5589308b8f525c4b9b7f0b0a4b80b01531988313e92fc02206e7f0afa725f407f59f85482f26ea20a70c5fe533c0592c95733a4418054c025412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', + value: 1497156989, + sequenceNo: 4294967295, + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + }, + ], + outputs: [ + { + value: 0, + outputScript: + '6a04534c500001010747454e45534953044347454e0f436173687461622047656e657369731868747470733a2f2f626f6f6d657274616b65732e636f6d2f4c0001094c000800038d7ea4c68000', + }, + { + value: 546, + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + token: { + tokenId: + '16b12bbacdbb8c8a799adbfd782bfff9843c1f9b0be148eaae02a1a7f74f95c4', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '1000000000000000', + isMintBaton: false, + entryIdx: 0, + }, + spentBy: { + txid: '4f5af8d3dc9d1fb3dc803a80589cab62c78235264aa90e4f8066b7960804cd74', + outIdx: 1, + }, + }, + { + value: 1497155685, + outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + spentBy: { + txid: '0916e71779c9de7ee125741d3f5ab01f556356dbc86fd327a24f1e9e22ebc917', + outIdx: 0, + }, + }, + ], + lockTime: 0, + timeFirstSeen: 0, + size: 311, + isCoinbase: false, + tokenEntries: [ + { + tokenId: + '16b12bbacdbb8c8a799adbfd782bfff9843c1f9b0be148eaae02a1a7f74f95c4', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + txType: 'GENESIS', + isInvalid: false, + burnSummary: '', + failedColorings: [], + actualBurnAmount: '0', + intentionalBurn: '0', + burnsMintBatons: false, + }, + ], + tokenFailedParsings: [], + tokenStatus: 'TOKEN_STATUS_NORMAL', + block: { + height: 684837, + hash: '00000000000000001d065fdd22416c4e8e99803964f4fb9c91af6feb5ead5ff3', + timestamp: 1620082584, + }, +}; diff --git a/cashtab/src/components/Etokens/CreateTokenForm.js b/cashtab/src/components/Etokens/CreateTokenForm.js --- a/cashtab/src/components/Etokens/CreateTokenForm.js +++ b/cashtab/src/components/Etokens/CreateTokenForm.js @@ -2,7 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import styled from 'styled-components'; import Modal from 'components/Common/Modal'; import { WalletContext } from 'wallet/context'; @@ -37,6 +37,7 @@ import { hasEnoughToken } from 'wallet'; import { toast } from 'react-toastify'; import Switch from 'components/Common/Switch'; +import { useNavigate } from 'react-router-dom'; const Form = styled.div` display: flex; @@ -114,6 +115,7 @@ `; const CreateTokenForm = () => { + const navigate = useNavigate(); const { chronik, chaintipBlockheight, cashtabState } = React.useContext(WalletContext); const { settings, wallets } = cashtabState; @@ -125,6 +127,7 @@ // eToken icon adds const [tokenIcon, setTokenIcon] = useState(''); + const [createdTokenId, setCreatedTokenId] = useState(null); const [loading, setLoading] = useState(false); const [fileName, setFileName] = useState(''); const [tokenIconFileName, setTokenIconFileName] = useState(undefined); @@ -140,6 +143,17 @@ const [zoom, setZoom] = useState(1); const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); + useEffect(() => { + // After the user has created a token, we wait until the wallet has updated its balance + // and the page is available, then we navigate to the page + if (createdTokenId === null) { + return; + } + if (typeof tokens.get(createdTokenId) !== 'undefined') { + navigate(`/send-token/${createdTokenId}`); + } + }, [createdTokenId, tokens]); + const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { setCroppedAreaPixels(croppedAreaPixels); }, []); @@ -540,9 +554,12 @@ chaintipBlockheight, ); + const { txid } = response; + setCreatedTokenId(txid); + toast( @@ -555,7 +572,7 @@ // If this eToken has an icon, upload to server if (tokenIcon !== '') { - submitTokenIcon(response.txid); + submitTokenIcon(txid); } } catch (e) { toast.error(`${e}`); diff --git a/cashtab/src/components/Etokens/__tests__/CreateTokenForm.test.js b/cashtab/src/components/Etokens/__tests__/CreateTokenForm.test.js --- a/cashtab/src/components/Etokens/__tests__/CreateTokenForm.test.js +++ b/cashtab/src/components/Etokens/__tests__/CreateTokenForm.test.js @@ -3,7 +3,11 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. import React from 'react'; -import { walletWithXecAndTokens } from 'components/App/fixtures/mocks'; +import { + walletWithXecAndTokens, + MOCK_CHRONIK_TOKEN_CALL, + MOCK_CHRONIK_GENESIS_TX_CALL, +} from 'components/App/fixtures/mocks'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; @@ -135,10 +139,43 @@ ); }); it('User can create a token with a mint baton', async () => { + const createdTokenId = + 'a34382e000eed02f749ab6a9e8de37e25e7565f016707dc81460ce63167ee1c2'; + // Mock a utxo of the not-yet-created token so we can test the redirect + const MOCK_UTXO_FOR_BALANCE = { + token: { + tokenId: createdTokenId, + isMintBaton: false, + amount: '10', + }, + }; const mockedChronik = await initializeCashtabStateForTests( - walletWithXecAndTokens, + { + ...walletWithXecAndTokens, + state: { + ...walletWithXecAndTokens.state, + slpUtxos: [ + ...walletWithXecAndTokens.state.slpUtxos, + MOCK_UTXO_FOR_BALANCE, + ], + }, + }, localforage, ); + + // Mock the not-yet-created token's tokeninfo and utxo calls to test the redirect + mockedChronik.setMock('token', { + input: createdTokenId, + output: MOCK_CHRONIK_TOKEN_CALL, + }); + mockedChronik.setMock('tx', { + input: createdTokenId, + output: MOCK_CHRONIK_GENESIS_TX_CALL, + }); + mockedChronik.setTokenId(createdTokenId); + mockedChronik.setUtxosByTokenId(createdTokenId, { + utxos: [MOCK_UTXO_FOR_BALANCE], + }); // Add tx mock to mockedChronik const hex = '0200000001fe667fba52a1aa603a892126e492717eed3dad43bfea7365a7fdd08e051e8a21020000006a47304402205e54e8ef3c0912b2bbeda364c1b15298e235d91eca3f8e02395fe5f07a406ee70220482d820793356ba8c3012ac2bd6630c8daa88c443111d8f9ee93785937d48f7c4121031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dffffffff040000000000000000466a04534c500001010747454e4553495303544b450a7465737420746f6b656e1768747470733a2f2f7777772e636173687461622e636f6d4c000102010208000000000393870022020000000000001976a9143a5fb236934ec078b4507c303d3afd82067f8fc188ac22020000000000001976a9143a5fb236934ec078b4507c303d3afd82067f8fc188ac227d0e00000000001976a9143a5fb236934ec078b4507c303d3afd82067f8fc188ac00000000'; @@ -197,5 +234,8 @@ 'href', `${explorer.blockExplorerUrl}/tx/${txid}`, ); + + // We are sent to its token-action page + expect(await screen.findByTitle('Token Stats')).toBeInTheDocument(); }); }); diff --git a/cashtab/src/components/Send/SendToken.js b/cashtab/src/components/Send/SendToken.js --- a/cashtab/src/components/Send/SendToken.js +++ b/cashtab/src/components/Send/SendToken.js @@ -716,7 +716,7 @@ ticker={tokenTicker} name={tokenName} /> - + setShowLargeIconModal(true)}