diff --git a/cashtab/src/components/Etokens/CreateTokenForm/index.tsx b/cashtab/src/components/Etokens/CreateTokenForm/index.tsx --- a/cashtab/src/components/Etokens/CreateTokenForm/index.tsx +++ b/cashtab/src/components/Etokens/CreateTokenForm/index.tsx @@ -37,7 +37,10 @@ getNftParentGenesisTargetOutputs, getNftChildGenesisTargetOutputs, } from 'token-protocols/slpv1'; -import { getAlpGenesisTargetOutputs } from 'token-protocols/alp'; +import { + getAlpGenesisTargetOutputs, + getMaxDecimalizedAlpQty, +} from 'token-protocols/alp'; import { sendXec } from 'transactions'; import { TokenNotificationIcon } from 'components/Common/CustomIcons'; import { explorer } from 'config/explorer'; @@ -217,6 +220,30 @@ } }, [createNftCollection]); + useEffect(() => { + if (formData.genesisQty === '') { + // genesisQty is required and will be validated when the user inputs + // Do not handle here + return; + } + // Update validation of genesis qty field when user toggles between ALP and SLP + const isValidOrStringErrorMsg = isValidTokenMintAmount( + formData.genesisQty, + // Note that, in this code block, value is formData.decimals + formData.decimals === '' + ? 0 + : (parseInt(formData.decimals) as SlpDecimals), + tokenTypeSwitches.alp === true ? 'ALP' : 'SLP', + ); + setFormDataErrors(previous => ({ + ...previous, + genesisQty: + typeof isValidOrStringErrorMsg === 'string' + ? isValidOrStringErrorMsg + : false, + })); + }, [tokenTypeSwitches.alp]); + const onCropComplete = useCallback((_: Area, croppedAreaPixels: Area) => { setCroppedAreaPixels(croppedAreaPixels); }, []); @@ -450,6 +477,7 @@ formData.genesisQty, // Note that, in this code block, value is formData.decimals parseInt(value) as SlpDecimals, + tokenTypeSwitches.alp === true ? 'ALP' : 'SLP', ); setFormDataErrors(previous => ({ ...previous, @@ -468,6 +496,7 @@ formData.decimals === '' ? 0 : (parseInt(formData.decimals) as SlpDecimals), + tokenTypeSwitches.alp === true ? 'ALP' : 'SLP', ); setFormDataErrors(previous => ({ ...previous, @@ -505,8 +534,10 @@ formData.decimals === '' ? 0 : (parseInt(formData.decimals) as SlpDecimals); - const maxGenesisAmount = getMaxDecimalizedSlpQty(usedDecimals); - + const maxGenesisAmount = + tokenTypeSwitches.slp === true + ? getMaxDecimalizedSlpQty(usedDecimals) + : getMaxDecimalizedAlpQty(usedDecimals); handleInput({ target: { name: 'genesisQty', 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 @@ -457,4 +457,95 @@ // We are sent to its token-action page expect(await screen.findByTitle('Token Stats')).toBeInTheDocument(); }); + it('Validation works as expected for ALP and SLP max supply', async () => { + const createdTokenId = + '75883c5ebc2c3b375ae6e25dcc845dfbc6b34ae6c1319fb840e7dcba1f8135e7'; + // 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, + 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.setToken(createdTokenId, MOCK_CHRONIK_TOKEN_CALL); + mockedChronik.setTx(createdTokenId, MOCK_CHRONIK_GENESIS_TX_CALL); + mockedChronik.setUtxosByTokenId(createdTokenId, [ + MOCK_UTXO_FOR_BALANCE, + ]); + + render( + <CashtabTestWrapper + chronik={mockedChronik} + ecc={ecc} + route="/create-token" + />, + ); + + // Wait for Cashtab to load + await waitFor(() => + expect( + screen.queryByTitle('Cashtab Loading'), + ).not.toBeInTheDocument(), + ); + + // On load, the SLP switch is selected by default + expect(screen.getByTitle('Create SLP')).toBeChecked(); + + const tokenGenesisQtyInput = screen.getByPlaceholderText( + 'Enter initial token supply', + ); + + // Max out SLP supply + await user.click(screen.getByText('max')); + + // We see max SLP supply for 0 decimals + expect(tokenGenesisQtyInput).toHaveValue(18446744073709552000); + + // Select ALP + await user.click(screen.getByTitle('Create ALP')); + + // Expect validation error bc SLP max supply is > ALP max supply + expect( + screen.getByText( + 'Amount 18446744073709551615 exceeds max mint amount for this token (281474976710655)', + ), + ).toBeInTheDocument(); + + // Max out ALP supply + await user.click(screen.getByText('max')); + + // We see max ALP supply for 0 decimals + expect(tokenGenesisQtyInput).toHaveValue(281474976710655); + + // Increase the decimals so that this supply is invalid + await user.type( + await screen.findByPlaceholderText( + 'Enter number of decimal places', + ), + '2', + ); + + // Expect validation error bc ALP max supply is lower if you have 2 decimals + expect( + screen.getByText( + 'Amount 281474976710655 exceeds max mint amount for this token (2814749767106.55)', + ), + ).toBeInTheDocument(); + }); });