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.9", + "version": "2.32.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cashtab", - "version": "2.32.9", + "version": "2.32.10", "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.9", + "version": "2.32.10", "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 @@ -10,7 +10,7 @@ isValidTokenTicker, isValidTokenDecimals, isValidTokenMintAmount, - isValidTokenDocumentUrl, + getTokenDocumentUrlError, isProbablyNotAScam, } from 'validation'; import PrimaryButton from 'components/Common/Buttons'; @@ -341,9 +341,7 @@ case 'url': { setFormDataErrors(previous => ({ ...previous, - [name]: isValidTokenDocumentUrl(value) - ? false - : 'Must be a valid URL. Cannot exceed 68 characters.', + [name]: getTokenDocumentUrlError(value), })); break; } diff --git a/cashtab/src/config/token.js b/cashtab/src/config/token.js --- a/cashtab/src/config/token.js +++ b/cashtab/src/config/token.js @@ -8,5 +8,5 @@ tokenIconSubmitApi: 'https://api.etokens.cash/new', tokenIconsUrl: 'https://icons.etokens.cash', tokenDbUrl: 'https://tokendb.kingbch.com', - newTokenDefaultUrl: 'https://cashtab.com/', + newTokenDefaultUrl: 'cashtab.com', }; diff --git a/cashtab/src/validation/__tests__/index.test.js b/cashtab/src/validation/__tests__/index.test.js --- a/cashtab/src/validation/__tests__/index.test.js +++ b/cashtab/src/validation/__tests__/index.test.js @@ -6,7 +6,7 @@ isValidTokenName, isValidTokenTicker, isValidTokenDecimals, - isValidTokenDocumentUrl, + getTokenDocumentUrlError, isValidCashtabSettings, isValidXecSendAmount, isValidTokenId, @@ -31,6 +31,7 @@ getContactNameError, getContactAddressError, getWalletNameError, + TOKEN_DOCUMENT_URL_MAX_CHARACTERS, } from 'validation'; import { validXecAirdropExclusionList, @@ -124,39 +125,41 @@ it(`Rejects tokenDecimals if non-integer`, () => { expect(isValidTokenDecimals('1.7')).toBe(false); }); - it(`Accepts a valid ${appConfig.tokenTicker} token document URL`, () => { - expect(isValidTokenDocumentUrl('cashtabapp.com')).toBe(true); + it(`No error for a valid ${appConfig.tokenTicker} token document URL`, () => { + expect(getTokenDocumentUrlError('cashtabapp.com')).toBe(false); }); - it(`Accepts a valid ${appConfig.tokenTicker} token document URL including special URL characters`, () => { - expect(isValidTokenDocumentUrl('https://cashtabapp.com/')).toBe(true); + it(`No error for a valid ${appConfig.tokenTicker} token document URL including special URL characters`, () => { + expect(getTokenDocumentUrlError('https://cashtabapp.com/')).toBe(false); }); - it(`Accepts a blank string as a valid ${appConfig.tokenTicker} token document URL`, () => { - expect(isValidTokenDocumentUrl('')).toBe(true); + it(`No error for a blank string as a valid ${appConfig.tokenTicker} token document URL`, () => { + expect(getTokenDocumentUrlError('')).toBe(false); }); - it(`Rejects ${appConfig.tokenTicker} token name if longer than 68 characters`, () => { + it(`Expected error for valid url longer than 68 characters`, () => { expect( - isValidTokenDocumentUrl( + getTokenDocumentUrlError( 'http://www.ThisTokenDocumentUrlIsActuallyMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchTooLong.com/', ), - ).toBe(false); + ).toBe( + `URL must be less than ${TOKEN_DOCUMENT_URL_MAX_CHARACTERS} characters.`, + ); }); - it(`Accepts a domain input with https protocol as ${appConfig.tokenTicker} token document URL`, () => { - expect(isValidTokenDocumentUrl('https://google.com')).toBe(true); + it(`No error for a domain input with https protocol as ${appConfig.tokenTicker} token document URL`, () => { + expect(getTokenDocumentUrlError('https://google.com')).toBe(false); }); - it(`Accepts a domain input with http protocol as ${appConfig.tokenTicker} token document URL`, () => { - expect(isValidTokenDocumentUrl('http://test.com')).toBe(true); + it(`No error for a domain input with http protocol as ${appConfig.tokenTicker} token document URL`, () => { + expect(getTokenDocumentUrlError('http://test.com')).toBe(false); }); - it(`Accepts a domain input with a primary and secondary top level domain as ${appConfig.tokenTicker} token document URL`, () => { - expect(isValidTokenDocumentUrl('http://test.co.uk')).toBe(true); + it(`No error for a domain input with a primary and secondary top level domain as ${appConfig.tokenTicker} token document URL`, () => { + expect(getTokenDocumentUrlError('http://test.co.uk')).toBe(false); }); - it(`Accepts a domain input with just a subdomain as ${appConfig.tokenTicker} token document URL`, () => { - expect(isValidTokenDocumentUrl('www.test.co.uk')).toBe(true); + it(`No error for a domain input with just a subdomain as ${appConfig.tokenTicker} token document URL`, () => { + expect(getTokenDocumentUrlError('www.test.co.uk')).toBe(false); }); - it(`Rejects a domain input with no top level domain, protocol or subdomain ${appConfig.tokenTicker} token document URL`, () => { - expect(isValidTokenDocumentUrl('mywebsite')).toBe(false); + it(`Expected error for a domain input with no top level domain, protocol or subdomain ${appConfig.tokenTicker} token document URL`, () => { + expect(getTokenDocumentUrlError('mywebsite')).toBe('Invalid URL'); }); - it(`Rejects a domain input as numbers ${appConfig.tokenTicker} token document URL`, () => { - expect(isValidTokenDocumentUrl(12345)).toBe(false); + it(`Expected error for a domain input as numbers ${appConfig.tokenTicker} token document URL`, () => { + expect(getTokenDocumentUrlError(12345)).toBe('Invalid URL'); }); it(`isValidTokenId accepts valid token ID that is 64 chars in length`, () => { const testValidTokenId = diff --git a/cashtab/src/validation/index.js b/cashtab/src/validation/index.js --- a/cashtab/src/validation/index.js +++ b/cashtab/src/validation/index.js @@ -220,25 +220,41 @@ ); }; -export const isValidTokenDocumentUrl = tokenDocumentUrl => { - const urlPattern = new RegExp( - '^(https?:\\/\\/)?' + // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name - '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string - '(\\#[-a-z\\d_]*)?$', - 'i', - ); // fragment locator - - const urlTestResult = urlPattern.test(tokenDocumentUrl); - return ( - tokenDocumentUrl === '' || - (typeof tokenDocumentUrl === 'string' && - tokenDocumentUrl.length >= 0 && - tokenDocumentUrl.length < 68 && - urlTestResult) - ); +const TOKEN_DOCUMENT_URL_REGEX = new RegExp( + '^(https?:\\/\\/)?' + // protocol (optional) + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', + 'i', +); // fragment locator + +// Spec has no space limitation. Limitation is actually on available bytes in the genesis tx +// This value is chosen arbitrarily to ensure sum of all fields does not break the limit +// Since the URL must pass regex, we cannot have emojis or special characters +// So a max length of 68 corresponds to 34 bytes +export const TOKEN_DOCUMENT_URL_MAX_CHARACTERS = 68; + +/** + * Validate user input for token document URL of genesis tx for SLP1 token + * @param {string} url + * @returns {string | false} error msg as string or false as bool if no error + */ +export const getTokenDocumentUrlError = url => { + // This is an optional input field, so a blank string is valid (no error) + if (url === '') { + return false; + } + const isValidUrl = TOKEN_DOCUMENT_URL_REGEX.test(url); + if (!isValidUrl) { + return `Invalid URL`; + } + if (url.length > TOKEN_DOCUMENT_URL_MAX_CHARACTERS) { + return `URL must be less than ${TOKEN_DOCUMENT_URL_MAX_CHARACTERS} characters.`; + } + // No error + return false; }; export const isValidCashtabSettings = settings => {