diff --git a/apps/token-server/scripts/sendTgIconNotice.ts b/apps/token-server/scripts/sendTgIconNotice.ts --- a/apps/token-server/scripts/sendTgIconNotice.ts +++ b/apps/token-server/scripts/sendTgIconNotice.ts @@ -21,7 +21,7 @@ ticker: 'TEST', decimals: 0, url: 'https://cashtab.com/', - initialQty: '1000000000', + genesisQty: '1000000000', tokenId: '1111111111111111111111111111111111111111111111111111111111111111', }; diff --git a/apps/token-server/src/telegram.ts b/apps/token-server/src/telegram.ts --- a/apps/token-server/src/telegram.ts +++ b/apps/token-server/src/telegram.ts @@ -143,7 +143,7 @@ ticker: string; decimals: number; url: string; - initialQty: string; + genesisQty: string; tokenId: string; } 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.32.7", + "version": "2.32.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cashtab", - "version": "2.32.7", + "version": "2.32.8", "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.32.7", + "version": "2.32.8", "private": true, "scripts": { "start": "node scripts/start.js", diff --git a/cashtab/src/components/Etokens/CreateTokenForm/index.js b/cashtab/src/components/Etokens/CreateTokenForm/index.js --- a/cashtab/src/components/Etokens/CreateTokenForm/index.js +++ b/cashtab/src/components/Etokens/CreateTokenForm/index.js @@ -75,14 +75,39 @@ const [imageUrl, setImageUrl] = useState(false); const [showCropModal, setShowCropModal] = useState(false); const [roundSelection, setRoundSelection] = useState(true); - const [createWithMintBatonAtIndexTwo, setCreateWithMintBatonAtIndexTwo] = - useState(false); - const [crop, setCrop] = useState({ x: 0, y: 0 }); const [rotation, setRotation] = useState(0); const [zoom, setZoom] = useState(1); const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); + // Modal settings + const [showConfirmCreateToken, setShowConfirmCreateToken] = useState(false); + + // Token form items + const emptyFormData = { + name: '', + ticker: '', + decimals: '', + genesisQty: '', + url: '', + createWithMintBatonAtIndexTwo: false, + }; + const initialFormDataErrors = { + name: false, + ticker: false, + decimals: false, + genesisQty: false, + url: false, + }; + const [formData, setFormData] = useState(emptyFormData); + const [formDataErrors, setFormDataErrors] = useState(initialFormDataErrors); + // This switch is form data, but since it is a bool and not a string, keep it with its own state field + const [createWithMintBatonAtIndexTwo, setCreateWithMintBatonAtIndexTwo] = + useState(false); + + // Note: We do not include a UI input for token document hash + // Questionable value to casual users and requires significant complication + 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 @@ -240,182 +265,136 @@ } }; - // Token name - const [name, setName] = useState(''); - const [newTokenNameIsValid, setNewTokenNameIsValid] = useState(null); - const [newTokenNameIsProbablyNotAScam, setNewTokenNameIsProbablyNotAScam] = - useState(null); - const [tokenNameError, setTokenNameError] = useState(false); - const handleNewTokenNameInput = e => { - const { value } = e.target; - // validation - const validTokenName = isValidTokenName(value); - const probablyNotScam = isProbablyNotAScam(value); - - setNewTokenNameIsValid(validTokenName); - setNewTokenNameIsProbablyNotAScam(probablyNotScam); - - if (!validTokenName) { - setTokenNameError( - 'Token name must be a valid string between 1 and 68 characters long.', - ); - } - if (!probablyNotScam) { - setTokenNameError( - 'Token name must not conflict with existing crypto or fiat', - ); - } - if (validTokenName && probablyNotScam) { - setTokenNameError(false); - } - - setName(value); - }; - - // New Token Ticker - const [ticker, setTicker] = useState(''); - const [newTokenTickerIsValid, setNewTokenTickerIsValid] = useState(null); - const [ - newTokenTickerIsProbablyNotAScam, - setNewTokenTickerIsProbablyNotAScam, - ] = useState(null); - const [tokenTickerError, setTokenTickerError] = useState(false); - const handleNewTokenTickerInput = e => { - const { value } = e.target; - // validation - const validTokenTicker = isValidTokenTicker(value); - const probablyNotScamTicker = isProbablyNotAScam(value); - setNewTokenTickerIsValid(validTokenTicker); - setNewTokenTickerIsProbablyNotAScam(probablyNotScamTicker); - - if (!validTokenTicker) { - setTokenTickerError( - 'Ticker must be a valid string between 1 and 12 characters long', - ); - } - if (!probablyNotScamTicker) { - setTokenTickerError( - 'Token ticker must not conflict with existing crypto or fiat', - ); - } - if (validTokenTicker && probablyNotScamTicker) { - setTokenTickerError(false); - } - - setTicker(value); - }; - - // New Token Decimals - const [decimals, setDecimals] = useState(''); - const [decimalsError, setDecimalsError] = useState(false); - const handleNewTokenDecimalsInput = e => { - const { value } = e.target; - // validation - setDecimalsError( - isValidTokenDecimals(value) - ? false - : 'Token decimals must be an integer between 0 and 9', - ); - - // Also validate the supply here if the form has been touched - // Supply validation may change when decimals changes - if (initialQty !== '') { - const isValidOrErrorMsg = isValidTokenMintAmount( - initialQty, - parseInt(value), - ); - setGenesisSupplyError( - typeof isValidOrErrorMsg === 'string' - ? isValidOrErrorMsg - : false, - ); + const handleInput = e => { + const { name, value } = e.target; + switch (name) { + case 'name': { + // Handle validation and state updates for new token name + // validation + const validTokenName = isValidTokenName(value); + const probablyNotScam = isProbablyNotAScam(value); + setFormDataErrors(previous => ({ + ...previous, + [name]: !validTokenName + ? `Token name must be a valid string between 1 and 68 characters long.` + : !probablyNotScam + ? 'Token name must not conflict with existing crypto or fiat' + : false, + })); + break; + } + case 'ticker': { + // validation + const validTokenTicker = isValidTokenTicker(value); + const probablyNotScamTicker = isProbablyNotAScam(value); + setFormDataErrors(previous => ({ + ...previous, + [name]: !validTokenTicker + ? `Token ticker must be a valid string between 1 and 12 characters long` + : !probablyNotScamTicker + ? 'Token ticker must not conflict with existing crypto or fiat' + : false, + })); + break; + } + case 'decimals': { + setFormDataErrors(previous => ({ + ...previous, + [name]: isValidTokenDecimals(value) + ? false + : 'Token decimals must be an integer between 0 and 9', + })); + + // Also validate the supply here if the form has been touched + // Supply validation may change when decimals changes + if (formData.genesisQty !== '') { + const isValidOrStringErrorMsg = isValidTokenMintAmount( + formData.genesisQty, + // Note that, in this code block, value is formData.decimals + parseInt(value), + ); + setFormDataErrors(previous => ({ + ...previous, + genesisQty: + typeof isValidOrStringErrorMsg === 'string' + ? isValidOrStringErrorMsg + : false, + })); + } + break; + } + case 'genesisQty': { + const isValidOrStringErrorMsg = isValidTokenMintAmount( + value, + // If user has not yet input decimals, assume 0 decimals + formData.decimals === '' ? 0 : parseInt(formData.decimals), + ); + setFormDataErrors(previous => ({ + ...previous, + [name]: + typeof isValidOrStringErrorMsg === 'string' + ? isValidOrStringErrorMsg + : false, + })); + break; + } + case 'url': { + setFormDataErrors(previous => ({ + ...previous, + [name]: isValidTokenDocumentUrl(value) + ? false + : 'Must be a valid URL. Cannot exceed 68 characters.', + })); + break; + } + default: + break; } - - setDecimals(value); + setFormData(previous => ({ + ...previous, + [name]: value, + })); }; const onMaxGenesis = () => { // Use 0 for decimals if user has not input decimals yet - const usedDecimals = decimals === '' ? 0 : parseInt(decimals); + const usedDecimals = + formData.decimals === '' ? 0 : parseInt(formData.decimals); const maxGenesisAmount = getMaxMintAmount(usedDecimals); - handleNewTokenInitialQtyInput({ + handleInput({ target: { - name: 'initialQty', + name: 'genesisQty', value: maxGenesisAmount, }, }); }; - // New Token Initial Quantity - const [initialQty, setInitialQty] = useState(''); - const [genesisSupplyError, setGenesisSupplyError] = useState(null); - const handleNewTokenInitialQtyInput = e => { - const { value } = e.target; - // If user has not yet input decimals, assume 0 decimals - const usedDecimalsValue = decimals === '' ? 0 : parseInt(decimals); - - // validation - const isValidOrErrorMsg = isValidTokenMintAmount( - value, - usedDecimalsValue, - ); - setGenesisSupplyError( - typeof isValidOrErrorMsg === 'string' ? isValidOrErrorMsg : false, - ); - setInitialQty(value); - }; - // New Token document URL - const [url, setUrl] = useState(''); - const [urlError, setUrlError] = useState(false); - - const handleNewTokenDocumentUrlInput = e => { - const { value } = e.target; - // validation - setUrlError( - isValidTokenDocumentUrl(value) - ? false - : 'Must be a valid URL. Cannot exceed 68 characters.', - ); - setUrl(value); - }; - - // New Token fixed supply - // Only allow creation of fixed supply tokens until Minting support is added - - // New Token document hash - // Do not include this; questionable value to casual users and requires significant complication - // Only enable CreateToken button if all form entries are valid let tokenGenesisDataIsValid = - newTokenNameIsValid && - newTokenTickerIsValid && - !decimalsError && - !genesisSupplyError && - !urlError && - newTokenNameIsProbablyNotAScam && - newTokenTickerIsProbablyNotAScam; - - // Modal settings - const [showConfirmCreateToken, setShowConfirmCreateToken] = useState(false); + formDataErrors.name === false && + formDataErrors.ticker === false && + formDataErrors.decimals === false && + formDataErrors.genesisQty === false && + formDataErrors.url === false; const submitTokenIcon = async tokenId => { - let formData = new FormData(); + let submittedFormData = new FormData(); const data = { - name, - ticker, - decimals, - url, - initialQty, + name: formData.name, + ticker: formData.ticker, + decimals: formData.decimals, + url: formData.url, + genesisQty: formData.genesisQty, tokenIcon, }; for (let key in data) { - formData.append(key, data[key]); + submittedFormData.append(key, data[key]); } // This function is called after the genesis tx is broadcast, using tokenId as a calling param - formData.append('tokenId', tokenId); + submittedFormData.append('tokenId', tokenId); try { const tokenIconApprovalResponse = await fetch( @@ -426,7 +405,7 @@ headers: { Accept: 'application/json', }, - body: formData, + body: submittedFormData, }, ); @@ -463,11 +442,14 @@ // data must be valid and user reviewed to get here const configObj = { - name, - ticker, - documentUrl: url === '' ? tokenConfig.newTokenDefaultUrl : url, - decimals, - initialQty, + name: formData.name, + ticker: formData.ticker, + decimals: formData.decimals, + documentUrl: + formData.url === '' + ? tokenConfig.newTokenDefaultUrl + : formData.url, + genesisQty: formData.genesisQty, documentHash: '', mintBatonVout: createWithMintBatonAtIndexTwo ? 2 : null, }; @@ -530,26 +512,26 @@ Name: - {name} + {formData.name} Ticker:{' '} - {ticker} + {formData.ticker} Decimals: - {decimals} + {formData.decimals} Supply: - {initialQty} + {formData.genesisQty} URL: - {url === '' + {formData.url === '' ? tokenConfig.newTokenDefaultUrl - : url} + : formData.url} @@ -561,41 +543,41 @@