diff --git a/web/cashtab/src/utils/__tests__/validation.test.js b/web/cashtab/src/utils/__tests__/validation.test.js index cc619248e..7067fb31c 100644 --- a/web/cashtab/src/utils/__tests__/validation.test.js +++ b/web/cashtab/src/utils/__tests__/validation.test.js @@ -1,687 +1,736 @@ import { shouldRejectAmountInput, fiatToCrypto, isValidTokenName, isValidTokenTicker, isValidTokenDecimals, isValidTokenInitialQty, isValidTokenDocumentUrl, isValidCashtabSettings, isValidXecAddress, + isValidBchAddress, isValidNewWalletNameLength, isValidEtokenAddress, isValidXecSendAmount, isValidSendToMany, isValidEtokenBurnAmount, isValidTokenId, isValidXecAirdrop, isValidAirdropOutputsArray, isValidAirdropExclusionArray, isValidContactList, parseInvalidSettingsForMigration, isValidCashtabCache, } from '../validation'; import { currency } from 'components/Common/Ticker.js'; import { fromSatoshisToXec } from 'utils/cashMethods'; import { stStatsValid, noCovidStatsValid, noCovidStatsInvalid, cGenStatsValid, } from '../__mocks__/mockTokenStats'; import { validXecAirdropList, invalidXecAirdropList, invalidXecAirdropListMultipleInvalidValues, invalidXecAirdropListMultipleValidValues, } from '../__mocks__/mockXecAirdropRecipients'; import { validXecAirdropExclusionList, invalidXecAirdropExclusionList, } from '../__mocks__/mockXecAirdropExclusionList'; import { validCashtabCache, cashtabCacheWithOneBadTokenId, cashtabCacheWithDecimalNotNumber, cashtabCacheWithTokenNameNotString, cashtabCacheWithMissingTokenName, } from 'utils/__mocks__/mockCashtabCache'; describe('Validation utils', () => { it(`Returns 'false' if ${currency.ticker} send amount is a valid send amount`, () => { expect(shouldRejectAmountInput('10', currency.ticker, 20.0, 300)).toBe( false, ); }); it(`Returns 'false' if ${currency.ticker} send amount is a valid send amount in USD`, () => { // Here, user is trying to send $170 USD, where 1 BCHA = $20 USD, and the user has a balance of 15 BCHA or $300 expect(shouldRejectAmountInput('170', 'USD', 20.0, 15)).toBe(false); }); it(`Returns not a number if ${currency.ticker} send amount is not a number`, () => { const expectedValidationError = `Amount must be a number`; expect( shouldRejectAmountInput('Not a number', currency.ticker, 20.0, 3), ).toBe(expectedValidationError); }); it(`Returns amount must be greater than 0 if ${currency.ticker} send amount is 0`, () => { const expectedValidationError = `Amount must be greater than 0`; expect(shouldRejectAmountInput('0', currency.ticker, 20.0, 3)).toBe( expectedValidationError, ); }); it(`Returns amount must be greater than 0 if ${currency.ticker} send amount is less than 0`, () => { const expectedValidationError = `Amount must be greater than 0`; expect( shouldRejectAmountInput('-0.031', currency.ticker, 20.0, 3), ).toBe(expectedValidationError); }); it(`Returns balance error if ${currency.ticker} send amount is greater than user balance`, () => { const expectedValidationError = `Amount cannot exceed your ${currency.ticker} balance`; expect(shouldRejectAmountInput('17', currency.ticker, 20.0, 3)).toBe( expectedValidationError, ); }); it(`Returns balance error if ${currency.ticker} send amount is greater than user balance`, () => { const expectedValidationError = `Amount cannot exceed your ${currency.ticker} balance`; expect(shouldRejectAmountInput('17', currency.ticker, 20.0, 3)).toBe( expectedValidationError, ); }); it(`Returns error if ${ currency.ticker } send amount is less than ${fromSatoshisToXec( currency.dustSats, ).toString()} minimum`, () => { const expectedValidationError = `Send amount must be at least ${fromSatoshisToXec( currency.dustSats, ).toString()} ${currency.ticker}`; expect( shouldRejectAmountInput( ( fromSatoshisToXec(currency.dustSats).toString() - 0.00000001 ).toString(), currency.ticker, 20.0, 3, ), ).toBe(expectedValidationError); }); it(`Returns error if ${ currency.ticker } send amount is less than ${fromSatoshisToXec( currency.dustSats, ).toString()} minimum in fiat currency`, () => { const expectedValidationError = `Send amount must be at least ${fromSatoshisToXec( currency.dustSats, ).toString()} ${currency.ticker}`; expect( shouldRejectAmountInput('0.0000005', 'USD', 0.00005, 1000000), ).toBe(expectedValidationError); }); it(`Returns balance error if ${currency.ticker} send amount is greater than user balance with fiat currency selected`, () => { const expectedValidationError = `Amount cannot exceed your ${currency.ticker} balance`; // Here, user is trying to send $170 USD, where 1 BCHA = $20 USD, and the user has a balance of 5 BCHA or $100 expect(shouldRejectAmountInput('170', 'USD', 20.0, 5)).toBe( expectedValidationError, ); }); it(`Returns precision error if ${currency.ticker} send amount has more than ${currency.cashDecimals} decimal places`, () => { const expectedValidationError = `${currency.ticker} transactions do not support more than ${currency.cashDecimals} decimal places`; expect( shouldRejectAmountInput('17.123456789', currency.ticker, 20.0, 35), ).toBe(expectedValidationError); }); it(`Returns expected crypto amount with ${currency.cashDecimals} decimals of precision even if inputs have higher precision`, () => { expect(fiatToCrypto('10.97231694823432', 20.3231342349234234, 8)).toBe( '0.53989295', ); }); it(`Returns expected crypto amount with ${currency.cashDecimals} decimals of precision even if inputs have higher precision`, () => { expect(fiatToCrypto('10.97231694823432', 20.3231342349234234, 2)).toBe( '0.54', ); }); it(`Returns expected crypto amount with ${currency.cashDecimals} decimals of precision even if inputs have lower precision`, () => { expect(fiatToCrypto('10.94', 10, 8)).toBe('1.09400000'); }); it(`Accepts a valid ${currency.tokenTicker} token name`, () => { expect(isValidTokenName('Valid token name')).toBe(true); }); it(`Accepts a valid ${currency.tokenTicker} token name that is a stringified number`, () => { expect(isValidTokenName('123456789')).toBe(true); }); it(`Rejects ${currency.tokenTicker} token name if longer than 68 characters`, () => { expect( isValidTokenName( 'This token name is not valid because it is longer than 68 characters which is really pretty long for a token name when you think about it and all', ), ).toBe(false); }); it(`Rejects ${currency.tokenTicker} token name if empty string`, () => { expect(isValidTokenName('')).toBe(false); }); it(`Accepts a 4-char ${currency.tokenTicker} token ticker`, () => { expect(isValidTokenTicker('DOGG')).toBe(true); }); it(`Accepts a 12-char ${currency.tokenTicker} token ticker`, () => { expect(isValidTokenTicker('123456789123')).toBe(true); }); it(`Rejects ${currency.tokenTicker} token ticker if empty string`, () => { expect(isValidTokenTicker('')).toBe(false); }); it(`Rejects ${currency.tokenTicker} token ticker if > 12 chars`, () => { expect(isValidTokenTicker('1234567891234')).toBe(false); }); it(`Accepts ${currency.tokenDecimals} if zero`, () => { expect(isValidTokenDecimals('0')).toBe(true); }); it(`Accepts ${currency.tokenDecimals} if between 0 and 9 inclusive`, () => { expect(isValidTokenDecimals('9')).toBe(true); }); it(`Rejects ${currency.tokenDecimals} if empty string`, () => { expect(isValidTokenDecimals('')).toBe(false); }); it(`Rejects ${currency.tokenDecimals} if non-integer`, () => { expect(isValidTokenDecimals('1.7')).toBe(false); }); it(`Accepts ${currency.tokenDecimals} initial genesis quantity at minimum amount for 3 decimal places`, () => { expect(isValidTokenInitialQty('0.001', '3')).toBe(true); }); it(`Accepts ${currency.tokenDecimals} initial genesis quantity at minimum amount for 9 decimal places`, () => { expect(isValidTokenInitialQty('0.000000001', '9')).toBe(true); }); it(`Accepts ${currency.tokenDecimals} initial genesis quantity at amount below 100 billion`, () => { expect(isValidTokenInitialQty('1000', '0')).toBe(true); }); it(`Accepts highest possible ${currency.tokenDecimals} initial genesis quantity at amount below 100 billion`, () => { expect(isValidTokenInitialQty('99999999999.999999999', '9')).toBe(true); }); it(`Accepts ${currency.tokenDecimals} initial genesis quantity if decimal places equal tokenDecimals`, () => { expect(isValidTokenInitialQty('0.123', '3')).toBe(true); }); it(`Accepts ${currency.tokenDecimals} initial genesis quantity if decimal places are less than tokenDecimals`, () => { expect(isValidTokenInitialQty('0.12345', '9')).toBe(true); }); it(`Rejects ${currency.tokenDecimals} initial genesis quantity of zero`, () => { expect(isValidTokenInitialQty('0', '9')).toBe(false); }); it(`Rejects ${currency.tokenDecimals} initial genesis quantity if tokenDecimals is not valid`, () => { expect(isValidTokenInitialQty('0', '')).toBe(false); }); it(`Rejects ${currency.tokenDecimals} initial genesis quantity if 100 billion or higher`, () => { expect(isValidTokenInitialQty('100000000000', '0')).toBe(false); }); it(`Rejects ${currency.tokenDecimals} initial genesis quantity if it has more decimal places than tokenDecimals`, () => { expect(isValidTokenInitialQty('1.5', '0')).toBe(false); }); it(`Accepts a valid ${currency.tokenTicker} token document URL`, () => { expect(isValidTokenDocumentUrl('cashtabapp.com')).toBe(true); }); it(`Accepts a valid ${currency.tokenTicker} token document URL including special URL characters`, () => { expect(isValidTokenDocumentUrl('https://cashtabapp.com/')).toBe(true); }); it(`Accepts a blank string as a valid ${currency.tokenTicker} token document URL`, () => { expect(isValidTokenDocumentUrl('')).toBe(true); }); it(`Rejects ${currency.tokenTicker} token name if longer than 68 characters`, () => { expect( isValidTokenDocumentUrl( 'http://www.ThisTokenDocumentUrlIsActuallyMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchTooLong.com/', ), ).toBe(false); }); it(`Accepts a domain input with https protocol as ${currency.tokenTicker} token document URL`, () => { expect(isValidTokenDocumentUrl('https://google.com')).toBe(true); }); it(`Accepts a domain input with http protocol as ${currency.tokenTicker} token document URL`, () => { expect(isValidTokenDocumentUrl('http://test.com')).toBe(true); }); it(`Accepts a domain input with a primary and secondary top level domain as ${currency.tokenTicker} token document URL`, () => { expect(isValidTokenDocumentUrl('http://test.co.uk')).toBe(true); }); it(`Accepts a domain input with just a subdomain as ${currency.tokenTicker} token document URL`, () => { expect(isValidTokenDocumentUrl('www.test.co.uk')).toBe(true); }); it(`Rejects a domain input with no top level domain, protocol or subdomain ${currency.tokenTicker} token document URL`, () => { expect(isValidTokenDocumentUrl('mywebsite')).toBe(false); }); it(`Rejects a domain input as numbers ${currency.tokenTicker} token document URL`, () => { expect(isValidTokenDocumentUrl(12345)).toBe(false); }); it(`Recognizes the default cashtabCache object as valid`, () => { expect(isValidCashtabCache(currency.defaultCashtabCache)).toBe(true); }); it(`Recognizes a valid cashtabCache object`, () => { expect(isValidCashtabCache(validCashtabCache)).toBe(true); }); it(`Rejects a cashtabCache object if one token id is invalid`, () => { expect(isValidCashtabCache(cashtabCacheWithOneBadTokenId)).toBe(false); }); it(`Rejects a cashtabCache object if decimals is not a number`, () => { expect(isValidCashtabCache(cashtabCacheWithDecimalNotNumber)).toBe( false, ); }); it(`Rejects a cashtabCache object if tokenName is not a string`, () => { expect(isValidCashtabCache(cashtabCacheWithTokenNameNotString)).toBe( false, ); }); it(`Rejects a cashtabCache object if tokenName is missing`, () => { expect(isValidCashtabCache(cashtabCacheWithMissingTokenName)).toBe( false, ); }); it(`Recognizes a valid cashtab settings object`, () => { expect( isValidCashtabSettings({ fiatCurrency: 'usd', sendModal: false, autoCameraOn: true, hideMessagesFromUnknownSenders: true, balanceVisible: false, }), ).toBe(true); }); it(`Rejects a cashtab settings object for an unsupported currency`, () => { expect( isValidCashtabSettings({ fiatCurrency: 'xau', sendModal: false }), ).toBe(false); }); it(`Rejects a corrupted cashtab settings object for an unsupported currency`, () => { expect( isValidCashtabSettings({ fiatCurrencyWrongLabel: 'usd', sendModal: false, }), ).toBe(false); }); it(`Rejects a valid fiatCurrency setting but undefined sendModal setting`, () => { expect(isValidCashtabSettings({ fiatCurrency: 'usd' })).toBe(false); }); it(`Rejects a valid fiatCurrency setting but invalid sendModal setting`, () => { expect( isValidCashtabSettings({ fiatCurrency: 'usd', sendModal: 'NOTVALID', }), ).toBe(false); }); it(`isValidXecAddress correctly validates a valid XEC address with ecash: prefix`, () => { const addr = 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'; expect(isValidXecAddress(addr)).toBe(true); }); it(`isValidXecAddress correctly validates a valid XEC address without ecash: prefix`, () => { const addr = 'qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'; expect(isValidXecAddress(addr)).toBe(true); }); it(`isValidXecAddress rejects a valid legacy address`, () => { const addr = '1Efd9z9GRVJK2r73nUpFmBnsKUmfXNm2y2'; expect(isValidXecAddress(addr)).toBe(false); }); it(`isValidXecAddress rejects a valid bitcoincash: address`, () => { const addr = 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr'; expect(isValidXecAddress(addr)).toBe(false); }); it(`isValidXecAddress rejects a valid etoken: address with prefix`, () => { const addr = 'etoken:qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'; expect(isValidXecAddress(addr)).toBe(false); }); it(`isValidXecAddress rejects a valid etoken: address without prefix`, () => { const addr = 'qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'; expect(isValidXecAddress(addr)).toBe(false); }); it(`isValidXecAddress rejects a valid simpleledger: address with prefix`, () => { const addr = 'simpleledger:qrujw0wrzncyxw8q3d0xkfet4jafrqhk6csev0v6y3'; expect(isValidXecAddress(addr)).toBe(false); }); it(`isValidXecAddress rejects a valid simpleledger: address without prefix`, () => { const addr = 'qrujw0wrzncyxw8q3d0xkfet4jafrqhk6csev0v6y3'; expect(isValidXecAddress(addr)).toBe(false); }); it(`isValidXecAddress rejects an invalid address`, () => { const addr = 'wtf is this definitely not an address'; expect(isValidXecAddress(addr)).toBe(false); }); it(`isValidXecAddress rejects a null input`, () => { const addr = null; expect(isValidXecAddress(addr)).toBe(false); }); it(`isValidXecAddress rejects an empty string input`, () => { const addr = ''; expect(isValidXecAddress(addr)).toBe(false); }); + it(`isValidBchAddress correctly validates a valid BCH address with bitcoincash: prefix`, () => { + const addr = 'bitcoincash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqqkm80dnl6'; + expect(isValidBchAddress(addr)).toBe(true); + }); + it(`isValidBchAddress correctly validates a valid BCH address without bitcoincash: prefix`, () => { + const addr = 'qzvydd4n3lm3xv62cx078nu9rg0e3srmqqkm80dnl6'; + expect(isValidBchAddress(addr)).toBe(true); + }); + it(`isValidBchAddress rejects a valid legacy address`, () => { + const addr = '1Efd9z9GRVJK2r73nUpFmBnsKUmfXNm2y2'; + expect(isValidBchAddress(addr)).toBe(false); + }); + it(`isValidBchAddress rejects a valid ecash: address`, () => { + const addr = 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'; + expect(isValidBchAddress(addr)).toBe(false); + }); + it(`isValidBchAddress rejects a valid ecash: address without the ecash prefix`, () => { + const addr = 'qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'; + expect(isValidBchAddress(addr)).toBe(false); + }); + it(`isValidBchAddress rejects a valid etoken: address with prefix`, () => { + const addr = 'etoken:qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'; + expect(isValidBchAddress(addr)).toBe(false); + }); + it(`isValidBchAddress rejects a valid etoken: address without prefix`, () => { + const addr = 'qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'; + expect(isValidBchAddress(addr)).toBe(false); + }); + it(`isValidBchAddress rejects a valid simpleledger: address with prefix`, () => { + const addr = 'simpleledger:qrujw0wrzncyxw8q3d0xkfet4jafrqhk6csev0v6y3'; + expect(isValidBchAddress(addr)).toBe(false); + }); + it(`isValidBchAddress rejects a valid simpleledger: address without prefix`, () => { + const addr = 'qrujw0wrzncyxw8q3d0xkfet4jafrqhk6csev0v6y3'; + expect(isValidBchAddress(addr)).toBe(false); + }); + it(`isValidBchAddress rejects an invalid address`, () => { + const addr = 'wtf is this definitely not an address'; + expect(isValidBchAddress(addr)).toBe(false); + }); + it(`isValidBchAddress rejects a null input`, () => { + const addr = null; + expect(isValidBchAddress(addr)).toBe(false); + }); + it(`isValidBchAddress rejects an empty string input`, () => { + const addr = ''; + expect(isValidBchAddress(addr)).toBe(false); + }); it(`isValidEtokenAddress rejects a valid XEC address with ecash: prefix`, () => { const addr = 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'; expect(isValidEtokenAddress(addr)).toBe(false); }); it(`isValidEtokenAddress rejects a valid XEC address without ecash: prefix`, () => { const addr = 'qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'; expect(isValidEtokenAddress(addr)).toBe(false); }); it(`isValidEtokenAddress rejects a valid legacy address`, () => { const addr = '1Efd9z9GRVJK2r73nUpFmBnsKUmfXNm2y2'; expect(isValidEtokenAddress(addr)).toBe(false); }); it(`isValidEtokenAddress rejects a valid bitcoincash: address`, () => { const addr = 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr'; expect(isValidEtokenAddress(addr)).toBe(false); }); it(`isValidEtokenAddress correctly validates a valid etoken: address with prefix`, () => { const addr = 'etoken:qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'; expect(isValidEtokenAddress(addr)).toBe(true); }); it(`isValidEtokenAddress correctly validates a valid etoken: address without prefix`, () => { const addr = 'qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'; expect(isValidEtokenAddress(addr)).toBe(true); }); it(`isValidEtokenAddress rejects a valid simpleledger: address with prefix`, () => { const addr = 'simpleledger:qrujw0wrzncyxw8q3d0xkfet4jafrqhk6csev0v6y3'; expect(isValidEtokenAddress(addr)).toBe(false); }); it(`isValidEtokenAddress rejects a valid simpleledger: address without prefix`, () => { const addr = 'qrujw0wrzncyxw8q3d0xkfet4jafrqhk6csev0v6y3'; expect(isValidEtokenAddress(addr)).toBe(false); }); it(`isValidEtokenAddress rejects an invalid address`, () => { const addr = 'wtf is this definitely not an address'; expect(isValidEtokenAddress(addr)).toBe(false); }); it(`isValidEtokenAddress rejects a null input`, () => { const addr = null; expect(isValidEtokenAddress(addr)).toBe(false); }); it(`isValidEtokenAddress rejects an empty string input`, () => { const addr = ''; expect(isValidEtokenAddress(addr)).toBe(false); }); it(`isValidXecSendAmount accepts the dust minimum`, () => { const testXecSendAmount = fromSatoshisToXec(currency.dustSats); expect(isValidXecSendAmount(testXecSendAmount)).toBe(true); }); it(`isValidXecSendAmount accepts arbitrary number above dust minimum`, () => { const testXecSendAmount = fromSatoshisToXec(currency.dustSats) + 1.75; expect(isValidXecSendAmount(testXecSendAmount)).toBe(true); }); it(`isValidXecSendAmount rejects zero`, () => { const testXecSendAmount = 0; expect(isValidXecSendAmount(testXecSendAmount)).toBe(false); }); it(`isValidXecSendAmount rejects a non-number string`, () => { const testXecSendAmount = 'not a number'; expect(isValidXecSendAmount(testXecSendAmount)).toBe(false); }); it(`isValidXecSendAmount accepts arbitrary number above dust minimum as a string`, () => { const testXecSendAmount = `${ fromSatoshisToXec(currency.dustSats) + 1.75 }`; expect(isValidXecSendAmount(testXecSendAmount)).toBe(true); }); it(`isValidXecSendAmount rejects null`, () => { const testXecSendAmount = null; expect(isValidXecSendAmount(testXecSendAmount)).toBe(false); }); it(`isValidXecSendAmount rejects undefined`, () => { const testXecSendAmount = undefined; expect(isValidXecSendAmount(testXecSendAmount)).toBe(false); }); it(`isValidEtokenBurnAmount rejects null`, () => { const testEtokenBurnAmount = null; expect(isValidEtokenBurnAmount(testEtokenBurnAmount)).toBe(false); }); it(`isValidEtokenBurnAmount rejects undefined`, () => { const testEtokenBurnAmount = undefined; expect(isValidEtokenBurnAmount(testEtokenBurnAmount)).toBe(false); }); it(`isValidEtokenBurnAmount rejects a burn amount that is 0`, () => { const testEtokenBurnAmount = 0; expect(isValidEtokenBurnAmount(testEtokenBurnAmount, 100)).toBe(false); }); it(`isValidEtokenBurnAmount rejects a burn amount that is negative`, () => { const testEtokenBurnAmount = -50; expect(isValidEtokenBurnAmount(testEtokenBurnAmount, 100)).toBe(false); }); it(`isValidEtokenBurnAmount rejects a burn amount that is more than the maxAmount param`, () => { const testEtokenBurnAmount = 1000; expect(isValidEtokenBurnAmount(testEtokenBurnAmount, 100)).toBe(false); }); it(`isValidEtokenBurnAmount accepts a valid burn amount`, () => { const testEtokenBurnAmount = 50; expect(isValidEtokenBurnAmount(testEtokenBurnAmount, 100)).toBe(true); }); it(`isValidEtokenBurnAmount accepts a valid burn amount with decimal points`, () => { const testEtokenBurnAmount = 10.545454; expect(isValidEtokenBurnAmount(testEtokenBurnAmount, 100)).toBe(true); }); it(`isValidEtokenBurnAmount accepts a valid burn amount that is the same as the maxAmount`, () => { const testEtokenBurnAmount = 100; expect(isValidEtokenBurnAmount(testEtokenBurnAmount, 100)).toBe(true); }); it(`isValidTokenId accepts valid token ID that is 64 chars in length`, () => { const testValidTokenId = '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; expect(isValidTokenId(testValidTokenId)).toBe(true); }); it(`isValidTokenId rejects a token ID that is less than 64 chars in length`, () => { const testValidTokenId = '111111thisisaninvalidtokenid'; expect(isValidTokenId(testValidTokenId)).toBe(false); }); it(`isValidTokenId rejects a token ID that is more than 64 chars in length`, () => { const testValidTokenId = '111111111c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; expect(isValidTokenId(testValidTokenId)).toBe(false); }); it(`isValidTokenId rejects a token ID number that is 64 digits in length`, () => { const testValidTokenId = 8912345678912345678912345678912345678912345678912345678912345679; expect(isValidTokenId(testValidTokenId)).toBe(false); }); it(`isValidTokenId rejects null`, () => { const testValidTokenId = null; expect(isValidTokenId(testValidTokenId)).toBe(false); }); it(`isValidTokenId rejects undefined`, () => { const testValidTokenId = undefined; expect(isValidTokenId(testValidTokenId)).toBe(false); }); it(`isValidTokenId rejects empty string`, () => { const testValidTokenId = ''; expect(isValidTokenId(testValidTokenId)).toBe(false); }); it(`isValidTokenId rejects special character input`, () => { const testValidTokenId = '^&$%@&^$@&%$!'; expect(isValidTokenId(testValidTokenId)).toBe(false); }); it(`isValidTokenId rejects non-alphanumeric input`, () => { const testValidTokenId = 99999999999; expect(isValidTokenId(testValidTokenId)).toBe(false); }); it(`isValidXecAirdrop accepts valid Total Airdrop Amount`, () => { const testAirdropTotal = '1000000'; expect(isValidXecAirdrop(testAirdropTotal)).toBe(true); }); it(`isValidXecAirdrop rejects null`, () => { const testAirdropTotal = null; expect(isValidXecAirdrop(testAirdropTotal)).toBe(false); }); it(`isValidXecAirdrop rejects undefined`, () => { const testAirdropTotal = undefined; expect(isValidXecAirdrop(testAirdropTotal)).toBe(false); }); it(`isValidXecAirdrop rejects empty string`, () => { const testAirdropTotal = ''; expect(isValidXecAirdrop(testAirdropTotal)).toBe(false); }); it(`isValidTokenId rejects an alphanumeric input`, () => { const testAirdropTotal = 'a73hsyujs3737'; expect(isValidXecAirdrop(testAirdropTotal)).toBe(false); }); it(`isValidTokenId rejects a number !> 0 in string format`, () => { const testAirdropTotal = '0'; expect(isValidXecAirdrop(testAirdropTotal)).toBe(false); }); it(`isValidAirdropOutputsArray accepts an airdrop list with valid XEC values`, () => { // Tools.js logic removes the EOF newline before validation const outputArray = validXecAirdropList.substring( 0, validXecAirdropList.length - 1, ); expect(isValidAirdropOutputsArray(outputArray)).toBe(true); }); it(`isValidAirdropOutputsArray rejects an airdrop list with invalid XEC values`, () => { // Tools.js logic removes the EOF newline before validation const outputArray = invalidXecAirdropList.substring( 0, invalidXecAirdropList.length - 1, ); expect(isValidAirdropOutputsArray(outputArray)).toBe(false); }); it(`isValidAirdropOutputsArray rejects null`, () => { const testAirdropListValues = null; expect(isValidAirdropOutputsArray(testAirdropListValues)).toBe(false); }); it(`isValidAirdropOutputsArray rejects undefined`, () => { const testAirdropListValues = undefined; expect(isValidAirdropOutputsArray(testAirdropListValues)).toBe(false); }); it(`isValidAirdropOutputsArray rejects empty string`, () => { const testAirdropListValues = ''; expect(isValidAirdropOutputsArray(testAirdropListValues)).toBe(false); }); it(`isValidAirdropOutputsArray rejects an airdrop list with multiple invalid XEC values per row`, () => { // Tools.js logic removes the EOF newline before validation const addressStringArray = invalidXecAirdropListMultipleInvalidValues.substring( 0, invalidXecAirdropListMultipleInvalidValues.length - 1, ); expect(isValidAirdropOutputsArray(addressStringArray)).toBe(false); }); it(`isValidAirdropOutputsArray rejects an airdrop list with multiple valid XEC values per row`, () => { // Tools.js logic removes the EOF newline before validation const addressStringArray = invalidXecAirdropListMultipleValidValues.substring( 0, invalidXecAirdropListMultipleValidValues.length - 1, ); expect(isValidAirdropOutputsArray(addressStringArray)).toBe(false); }); it(`isValidAirdropExclusionArray accepts a valid airdrop exclusion list`, () => { expect(isValidAirdropExclusionArray(validXecAirdropExclusionList)).toBe( true, ); }); it(`isValidAirdropExclusionArray rejects an invalid airdrop exclusion list`, () => { expect( isValidAirdropExclusionArray(invalidXecAirdropExclusionList), ).toBe(false); }); it(`isValidAirdropExclusionArray rejects an empty airdrop exclusion list`, () => { expect(isValidAirdropExclusionArray([])).toBe(false); }); it(`isValidAirdropExclusionArray rejects a null airdrop exclusion list`, () => { expect(isValidAirdropExclusionArray(null)).toBe(false); }); it(`isValidContactList accepts default empty contactList`, () => expect(isValidContactList([{}])).toBe(true)); it(`isValidContactList rejects array of more than one empty object`, () => expect(isValidContactList([{}, {}])).toBe(false)); it(`isValidContactList accepts a contact list of length 1 with valid XEC address and name`, () => expect( isValidContactList([ { address: 'ecash:qphlhe78677sz227k83hrh542qeehh8el5lcjwk72y', name: 'Alpha', }, ]), ).toBe(true)); it(`isValidContactList accepts a contact list of length > 1 with valid XEC addresses and names`, () => expect( isValidContactList([ { address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr', name: 'Alpha', }, { address: 'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2', name: 'Beta', }, { address: 'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82', name: 'Gamma', }, ]), ).toBe(true)); it(`isValidContactList rejects a contact list of length > 1 with valid XEC addresses and names but an empty object included`, () => expect( isValidContactList([ {}, { address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr', name: 'Alpha', }, { address: 'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2', name: 'Beta', }, { address: 'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82', name: 'Gamma', }, ]), ).toBe(false)); it('updates an invalid settings object and keeps existing valid settings intact', () => expect( parseInvalidSettingsForMigration({ fiatCurrency: 'gbp', }), ).toStrictEqual({ fiatCurrency: 'gbp', sendModal: false, autoCameraOn: true, hideMessagesFromUnknownSenders: false, balanceVisible: true, })); it('sets settings object with no exsting valid settings to default values', () => expect(parseInvalidSettingsForMigration({})).toStrictEqual({ fiatCurrency: 'usd', sendModal: false, autoCameraOn: true, hideMessagesFromUnknownSenders: false, balanceVisible: true, })); it('does nothing if valid settings object is present in localStorage', () => expect( parseInvalidSettingsForMigration({ fiatCurrency: 'brl', sendModal: true, autoCameraOn: true, hideMessagesFromUnknownSenders: false, balanceVisible: true, }), ).toStrictEqual({ fiatCurrency: 'brl', sendModal: true, autoCameraOn: true, hideMessagesFromUnknownSenders: false, balanceVisible: true, })); it(`accepts a valid wallet name`, () => { expect(isValidNewWalletNameLength('Apollo')).toBe(true); }); it(`rejects wallet name that is too long`, () => { expect( isValidNewWalletNameLength( 'this string is far too long to be used as a wallet name...', ), ).toBe(false); }); it(`rejects blank string as new wallet name`, () => { expect(isValidNewWalletNameLength('')).toBe(false); }); it(`rejects wallet name of the wrong type`, () => { expect(isValidNewWalletNameLength(['newWalletName'])).toBe(false); }); }); diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js index 01dd3d8ca..97a948de7 100644 --- a/web/cashtab/src/utils/cashMethods.js +++ b/web/cashtab/src/utils/cashMethods.js @@ -1,1093 +1,1096 @@ import { currency } from 'components/Common/Ticker'; import { isValidXecAddress, isValidEtokenAddress, isValidContactList, + isValidBchAddress, } from 'utils/validation'; import BigNumber from 'bignumber.js'; import cashaddr from 'ecashaddrjs'; import bs58 from 'bs58'; export const getUtxoWif = (utxo, wallet) => { if (!wallet) { throw new Error('Invalid wallet parameter'); } const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const wif = accounts .filter(acc => acc.cashAddress === utxo.address) .pop().fundingWif; return wif; }; export const signUtxosByAddress = (BCH, inputUtxos, wallet, txBuilder) => { for (let i = 0; i < inputUtxos.length; i++) { const utxo = inputUtxos[i]; const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const utxoEcPair = BCH.ECPair.fromWIF( accounts.filter(acc => acc.cashAddress === utxo.address).pop() .fundingWif, ); txBuilder.sign( i, utxoEcPair, undefined, txBuilder.hashTypes.SIGHASH_ALL, parseInt(utxo.value), ); } return txBuilder; }; export const getCashtabByteCount = (p2pkhInputCount, p2pkhOutputCount) => { // Simplifying bch-js function for P2PKH txs only, as this is all Cashtab supports for now // https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/src/bitcoincash.js#L408 /* const types = { inputs: { 'P2PKH': 148 * 4, }, outputs: { P2PKH: 34 * 4, }, }; */ const inputCount = new BigNumber(p2pkhInputCount); const outputCount = new BigNumber(p2pkhOutputCount); const inputWeight = new BigNumber(148 * 4); const outputWeight = new BigNumber(34 * 4); const nonSegwitWeightConstant = new BigNumber(10 * 4); let totalWeight = new BigNumber(0); totalWeight = totalWeight .plus(inputCount.times(inputWeight)) .plus(outputCount.times(outputWeight)) .plus(nonSegwitWeightConstant); const byteCount = totalWeight.div(4).integerValue(BigNumber.ROUND_CEIL); return Number(byteCount); }; export const calcFee = ( utxos, p2pkhOutputNumber = 2, satoshisPerByte = currency.defaultFee, ) => { const byteCount = getCashtabByteCount(utxos.length, p2pkhOutputNumber); const txFee = Math.ceil(satoshisPerByte * byteCount); return txFee; }; export const generateTokenTxOutput = ( BCH, txBuilder, tokenAction, legacyCashOriginAddress, tokenUtxosBeingSpent = [], // optional - send or burn tx only remainderXecValue = new BigNumber(0), // optional - only if > dust tokenConfigObj = {}, // optional - genesis only tokenRecipientAddress = false, // optional - send tx only tokenAmount = false, // optional - send or burn amount for send/burn tx only ) => { try { if (!BCH || !tokenAction || !legacyCashOriginAddress || !txBuilder) { throw new Error('Invalid token tx output parameter'); } let script, opReturnObj, destinationAddress; switch (tokenAction) { case 'GENESIS': script = BCH.SLP.TokenType1.generateGenesisOpReturn(tokenConfigObj); destinationAddress = legacyCashOriginAddress; break; case 'SEND': opReturnObj = BCH.SLP.TokenType1.generateSendOpReturn( tokenUtxosBeingSpent, tokenAmount.toString(), ); script = opReturnObj.script; destinationAddress = BCH.SLP.Address.toLegacyAddress( tokenRecipientAddress, ); break; case 'BURN': script = BCH.SLP.TokenType1.generateBurnOpReturn( tokenUtxosBeingSpent, tokenAmount, ); destinationAddress = BCH.SLP.Address.toLegacyAddress( legacyCashOriginAddress, ); break; default: throw new Error('Invalid token transaction type'); } // OP_RETURN needs to be the first output in the transaction. txBuilder.addOutput(script, 0); // add XEC dust output as fee for genesis, send or burn token output txBuilder.addOutput(destinationAddress, parseInt(currency.etokenSats)); // Return any token change back to the sender for send and burn txs if ( tokenAction !== 'GENESIS' || (opReturnObj && opReturnObj.outputs > 1) ) { // add XEC dust output as fee txBuilder.addOutput( tokenUtxosBeingSpent[0].address, // etoken address parseInt(currency.etokenSats), ); } // Send xec change to own address if (remainderXecValue.gte(new BigNumber(currency.dustSats))) { txBuilder.addOutput( legacyCashOriginAddress, parseInt(remainderXecValue), ); } } catch (err) { console.log(`generateTokenTxOutput() error: ` + err); throw err; } return txBuilder; }; export const generateTxInput = ( BCH, isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, ) => { let txInputObj = {}; const inputUtxos = []; let txFee = 0; let totalInputUtxoValue = new BigNumber(0); try { if ( !BCH || (isOneToMany && !destinationAddressAndValueArray) || !utxos || !txBuilder || !satoshisToSend || !feeInSatsPerByte ) { throw new Error('Invalid tx input parameter'); } // A normal tx will have 2 outputs, destination and change // A one to many tx will have n outputs + 1 change output, where n is the number of recipients const txOutputs = isOneToMany ? destinationAddressAndValueArray.length + 1 : 2; for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i]; totalInputUtxoValue = totalInputUtxoValue.plus(utxo.value); const vout = utxo.outpoint.outIdx; const txid = utxo.outpoint.txid; // add input with txid and index of vout txBuilder.addInput(txid, vout); inputUtxos.push(utxo); txFee = calcFee(inputUtxos, txOutputs, feeInSatsPerByte); if (totalInputUtxoValue.minus(satoshisToSend).minus(txFee).gte(0)) { break; } } } catch (err) { console.log(`generateTxInput() error: ` + err); throw err; } txInputObj.txBuilder = txBuilder; txInputObj.totalInputUtxoValue = totalInputUtxoValue; txInputObj.inputUtxos = inputUtxos; txInputObj.txFee = txFee; return txInputObj; }; export const generateTokenTxInput = ( BCH, tokenAction, // GENESIS, SEND or BURN totalXecUtxos, totalTokenUtxos, tokenId, tokenAmount, // optional - only for sending or burning feeInSatsPerByte, txBuilder, ) => { let totalXecInputUtxoValue = new BigNumber(0); let remainderXecValue = new BigNumber(0); let remainderTokenValue = new BigNumber(0); let totalXecInputUtxos = []; let txFee = 0; let tokenUtxosBeingSpent = []; try { if ( !BCH || !tokenAction || !totalXecUtxos || (tokenAction !== 'GENESIS' && !tokenId) || !feeInSatsPerByte || !txBuilder ) { throw new Error('Invalid token tx input parameter'); } // collate XEC UTXOs for this token tx const txOutputs = tokenAction === 'GENESIS' ? 2 // one for genesis OP_RETURN output and one for change : 4; // for SEND/BURN token txs see T2645 on why this is not dynamically generated for (let i = 0; i < totalXecUtxos.length; i++) { const thisXecUtxo = totalXecUtxos[i]; totalXecInputUtxoValue = totalXecInputUtxoValue.plus( new BigNumber(thisXecUtxo.value), ); const vout = thisXecUtxo.outpoint.outIdx; const txid = thisXecUtxo.outpoint.txid; // add input with txid and index of vout txBuilder.addInput(txid, vout); totalXecInputUtxos.push(thisXecUtxo); txFee = calcFee(totalXecInputUtxos, txOutputs, feeInSatsPerByte); remainderXecValue = tokenAction === 'GENESIS' ? totalXecInputUtxoValue .minus(new BigNumber(currency.etokenSats)) .minus(new BigNumber(txFee)) : totalXecInputUtxoValue .minus(new BigNumber(currency.etokenSats * 2)) // one for token send/burn output, one for token change .minus(new BigNumber(txFee)); if (remainderXecValue.gte(0)) { break; } } if (remainderXecValue.lt(0)) { throw new Error(`Insufficient funds`); } let filteredTokenInputUtxos = []; let finalTokenAmountSpent = new BigNumber(0); let tokenAmountBeingSpent = new BigNumber(tokenAmount); if (tokenAction === 'SEND' || tokenAction === 'BURN') { // filter for token UTXOs matching the token being sent/burnt filteredTokenInputUtxos = totalTokenUtxos.filter(utxo => { if ( utxo && // UTXO is associated with a token. utxo.slpMeta.tokenId === tokenId && // UTXO matches the token ID. !utxo.slpToken.isMintBaton // UTXO is not a minting baton. ) { return true; } return false; }); if (filteredTokenInputUtxos.length === 0) { throw new Error( 'No token UTXOs for the specified token could be found.', ); } // collate token UTXOs to cover the token amount being sent/burnt for (let i = 0; i < filteredTokenInputUtxos.length; i++) { finalTokenAmountSpent = finalTokenAmountSpent.plus( new BigNumber(filteredTokenInputUtxos[i].tokenQty), ); txBuilder.addInput( filteredTokenInputUtxos[i].outpoint.txid, filteredTokenInputUtxos[i].outpoint.outIdx, ); tokenUtxosBeingSpent.push(filteredTokenInputUtxos[i]); if (tokenAmountBeingSpent.lte(finalTokenAmountSpent)) { break; } } // calculate token change remainderTokenValue = finalTokenAmountSpent.minus( new BigNumber(tokenAmount), ); if (remainderTokenValue.lt(0)) { throw new Error( 'Insufficient token UTXOs for the specified token amount.', ); } } } catch (err) { console.log(`generateTokenTxInput() error: ` + err); throw err; } return { txBuilder: txBuilder, inputXecUtxos: totalXecInputUtxos, inputTokenUtxos: tokenUtxosBeingSpent, remainderXecValue: remainderXecValue, remainderTokenValue: remainderTokenValue, }; }; export const getChangeAddressFromInputUtxos = (BCH, inputUtxos, wallet) => { if (!BCH || !inputUtxos || !wallet) { throw new Error('Invalid getChangeAddressFromWallet input parameter'); } // Assume change address is input address of utxo at index 0 let changeAddress; // Validate address try { changeAddress = inputUtxos[0].address; - BCH.Address.isCashAddress(changeAddress); + if (!isValidBchAddress(changeAddress)) { + throw new Error('Invalid change address'); + } } catch (err) { throw new Error('Invalid input utxo'); } return changeAddress; }; /* * Parse the total value of a send XEC tx and checks whether it is more than dust * One to many: isOneToMany is true, singleSendValue is null * One to one: isOneToMany is false, destinationAddressAndValueArray is null * Returns the aggregate send value in BigNumber format */ export const parseXecSendValue = ( isOneToMany, singleSendValue, destinationAddressAndValueArray, ) => { let value = new BigNumber(0); try { if (isOneToMany) { // this is a one to many XEC transaction if ( !destinationAddressAndValueArray || !destinationAddressAndValueArray.length ) { throw new Error('Invalid destinationAddressAndValueArray'); } const arrayLength = destinationAddressAndValueArray.length; for (let i = 0; i < arrayLength; i++) { // add the total value being sent in this array of recipients // each array row is: 'eCash address, send value' value = BigNumber.sum( value, new BigNumber( destinationAddressAndValueArray[i].split(',')[1], ), ); } } else { // this is a one to one XEC transaction then check singleSendValue // note: one to many transactions won't be sending a singleSendValue param if (!singleSendValue) { throw new Error('Invalid singleSendValue'); } value = new BigNumber(singleSendValue); } // If user is attempting to send an aggregate value that is less than minimum accepted by the backend if ( value.lt( new BigNumber(fromSatoshisToXec(currency.dustSats).toString()), ) ) { // Throw the same error given by the backend attempting to broadcast such a tx throw new Error('dust'); } } catch (err) { console.log('Error in parseXecSendValue: ' + err); throw err; } return value; }; /* * Generates an OP_RETURN script to reflect the various send XEC permutations * involving messaging, encryption, eToken IDs and airdrop flags. * * Returns the final encoded script object */ export const generateOpReturnScript = ( BCH, optionalOpReturnMsg, encryptionFlag, airdropFlag, airdropTokenId, encryptedEj, ) => { // encrypted mesage is mandatory when encryptionFlag is true // airdrop token id is mandatory when airdropFlag is true if ( !BCH || (encryptionFlag && !encryptedEj) || (airdropFlag && !airdropTokenId) ) { throw new Error('Invalid OP RETURN script input'); } // Note: script.push(Buffer.from(currency.opReturn.opReturnPrefixHex, 'hex')); actually evaluates to '016a' // instead of keeping the hex string intact. This behavour is specific to the initial script array element. // To get around this, the bch-js approach of directly using the opReturn prefix in decimal form for the initial entry is used here. let script = [currency.opReturn.opReturnPrefixDec]; // initialize script with the OP_RETURN op code (6a) in decimal form (106) try { if (encryptionFlag) { // if the user has opted to encrypt this message // add the encrypted cashtab messaging prefix and encrypted msg to script script.push( Buffer.from( currency.opReturn.appPrefixesHex.cashtabEncrypted, 'hex', ), // 65746162 ); // add the encrypted message to script script.push(Buffer.from(encryptedEj)); } else { // this is an un-encrypted message if (airdropFlag) { // if this was routed from the airdrop component // add the airdrop prefix to script script.push( Buffer.from( currency.opReturn.appPrefixesHex.airdrop, 'hex', ), // drop ); // add the airdrop token ID to script script.push(Buffer.from(airdropTokenId, 'hex')); } // add the cashtab prefix to script script.push( Buffer.from(currency.opReturn.appPrefixesHex.cashtab, 'hex'), // 00746162 ); // add the un-encrypted message to script if supplied if (optionalOpReturnMsg) { script.push(Buffer.from(optionalOpReturnMsg)); } } } catch (err) { console.log('Error in generateOpReturnScript(): ' + err); throw err; } const data = BCH.Script.encode(script); return data; }; export const generateTxOutput = ( BCH, isOneToMany, singleSendValue, satoshisToSend, totalInputUtxoValue, destinationAddress, destinationAddressAndValueArray, changeAddress, txFee, txBuilder, ) => { try { if ( !BCH || (isOneToMany && !destinationAddressAndValueArray) || (!isOneToMany && !destinationAddress && !singleSendValue) || !changeAddress || !satoshisToSend || !totalInputUtxoValue || !txFee || !txBuilder ) { throw new Error('Invalid tx input parameter'); } // amount to send back to the remainder address. const remainder = new BigNumber(totalInputUtxoValue) .minus(satoshisToSend) .minus(txFee); if (remainder.lt(0)) { throw new Error(`Insufficient funds`); } if (isOneToMany) { // for one to many mode, add the multiple outputs from the array let arrayLength = destinationAddressAndValueArray.length; for (let i = 0; i < arrayLength; i++) { // add each send tx from the array as an output let outputAddress = destinationAddressAndValueArray[i].split(',')[0]; let outputValue = new BigNumber( destinationAddressAndValueArray[i].split(',')[1], ); txBuilder.addOutput( outputAddress, parseInt(fromXecToSatoshis(outputValue)), ); } } else { // for one to one mode, add output w/ single address and amount to send txBuilder.addOutput( destinationAddress, parseInt(fromXecToSatoshis(singleSendValue)), ); } // if a remainder exists, return to change address as the final output if (remainder.gte(new BigNumber(currency.dustSats))) { txBuilder.addOutput(changeAddress, parseInt(remainder)); } } catch (err) { console.log('Error in generateTxOutput(): ' + err); throw err; } return txBuilder; }; export const signAndBuildTx = (BCH, inputUtxos, txBuilder, wallet) => { if ( !BCH || !inputUtxos || inputUtxos.length === 0 || !txBuilder || !wallet || // txBuilder.transaction.tx.ins is empty until the inputUtxos are signed txBuilder.transaction.tx.outs.length === 0 ) { throw new Error('Invalid buildTx parameter'); } // Sign each XEC UTXO being consumed and refresh transactionBuilder txBuilder = signUtxosByAddress(BCH, inputUtxos, wallet, txBuilder); let hex; try { // build tx const tx = txBuilder.build(); // output rawhex hex = tx.toHex(); } catch (err) { throw new Error('Transaction build failed'); } return hex; }; export function parseOpReturn(hexStr) { if ( !hexStr || typeof hexStr !== 'string' || hexStr.substring(0, 2) !== currency.opReturn.opReturnPrefixHex ) { return false; } hexStr = hexStr.slice(2); // remove the first byte i.e. 6a /* * @Return: resultArray is structured as follows: * resultArray[0] is the transaction type i.e. eToken prefix, cashtab prefix, external message itself if unrecognized prefix * resultArray[1] is the actual cashtab message or the 2nd part of an external message * resultArray[2 - n] are the additional messages for future protcols */ let resultArray = []; let message = ''; let hexStrLength = hexStr.length; for (let i = 0; hexStrLength !== 0; i++) { // part 1: check the preceding byte value for the subsequent message let byteValue = hexStr.substring(0, 2); let msgByteSize = 0; if (byteValue === currency.opReturn.opPushDataOne) { // if this byte is 4c then the next byte is the message byte size - retrieve the message byte size only msgByteSize = parseInt(hexStr.substring(2, 4), 16); // hex base 16 to decimal base 10 hexStr = hexStr.slice(4); // strip the 4c + message byte size info } else { // take the byte as the message byte size msgByteSize = parseInt(hexStr.substring(0, 2), 16); // hex base 16 to decimal base 10 hexStr = hexStr.slice(2); // strip the message byte size info } // part 2: parse the subsequent message based on bytesize const msgCharLength = 2 * msgByteSize; message = hexStr.substring(0, msgCharLength); if (i === 0 && message === currency.opReturn.appPrefixesHex.eToken) { // add the extracted eToken prefix to array then exit loop resultArray[i] = currency.opReturn.appPrefixesHex.eToken; break; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.cashtab ) { // add the extracted Cashtab prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtab; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.cashtabEncrypted ) { // add the Cashtab encryption prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtabEncrypted; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.airdrop ) { // add the airdrop prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.airdrop; } else { // this is either an external message or a subsequent cashtab message loop to extract the message resultArray[i] = message; } // strip out the parsed message hexStr = hexStr.slice(msgCharLength); hexStrLength = hexStr.length; } return resultArray; } export const fromLegacyDecimals = ( amount, cashDecimals = currency.cashDecimals, ) => { // Input 0.00000546 BCH // Output 5.46 XEC or 0.00000546 BCH, depending on currency.cashDecimals const amountBig = new BigNumber(amount); const conversionFactor = new BigNumber(10 ** (8 - cashDecimals)); const amountSmallestDenomination = amountBig .times(conversionFactor) .toNumber(); return amountSmallestDenomination; }; export const fromSatoshisToXec = ( amount, cashDecimals = currency.cashDecimals, ) => { const amountBig = new BigNumber(amount); const multiplier = new BigNumber(10 ** (-1 * cashDecimals)); const amountInBaseUnits = amountBig.times(multiplier); return amountInBaseUnits; }; export const fromXecToSatoshis = ( sendAmount, cashDecimals = currency.cashDecimals, ) => { // Replace the BCH.toSatoshi method with an equivalent function that works for arbitrary decimal places // Example, for an 8 decimal place currency like Bitcoin // Input: a BigNumber of the amount of Bitcoin to be sent // Output: a BigNumber of the amount of satoshis to be sent, or false if input is invalid // Validate // Input should be a BigNumber with no more decimal places than cashDecimals const isValidSendAmount = BigNumber.isBigNumber(sendAmount) && sendAmount.dp() <= cashDecimals; if (!isValidSendAmount) { return false; } const conversionFactor = new BigNumber(10 ** cashDecimals); const sendAmountSmallestDenomination = sendAmount.times(conversionFactor); return sendAmountSmallestDenomination; }; export const flattenContactList = contactList => { /* Converts contactList from array of objects of type {address: , name: } to array of addresses only If contact list is invalid, returns and empty array */ if (!isValidContactList(contactList)) { return []; } let flattenedContactList = []; for (let i = 0; i < contactList.length; i += 1) { const thisAddress = contactList[i].address; flattenedContactList.push(thisAddress); } return flattenedContactList; }; export const loadStoredWallet = walletStateFromStorage => { // Accept cached tokens array that does not save BigNumber type of BigNumbers // Return array with BigNumbers converted // See BigNumber.js api for how to create a BigNumber object from an object // https://mikemcl.github.io/bignumber.js/ const liveWalletState = walletStateFromStorage; const { slpBalancesAndUtxos, tokens } = liveWalletState; for (let i = 0; i < tokens.length; i += 1) { const thisTokenBalance = tokens[i].balance; thisTokenBalance._isBigNumber = true; tokens[i].balance = new BigNumber(thisTokenBalance); } // Also confirm balance is correct // Necessary step in case currency.decimals changed since last startup const balancesRebased = getWalletBalanceFromUtxos( slpBalancesAndUtxos.nonSlpUtxos, ); liveWalletState.balances = balancesRebased; return liveWalletState; }; export const getWalletBalanceFromUtxos = nonSlpUtxos => { const totalBalanceInSatoshis = nonSlpUtxos.reduce( (previousBalance, utxo) => previousBalance.plus(new BigNumber(utxo.value)), new BigNumber(0), ); return { totalBalanceInSatoshis: totalBalanceInSatoshis.toString(), totalBalance: fromSatoshisToXec(totalBalanceInSatoshis).toString(), }; }; export const isValidStoredWallet = walletStateFromStorage => { return ( typeof walletStateFromStorage === 'object' && 'state' in walletStateFromStorage && typeof walletStateFromStorage.state === 'object' && 'balances' in walletStateFromStorage.state && 'utxos' in walletStateFromStorage.state && !('hydratedUtxoDetails' in walletStateFromStorage.state) && 'slpBalancesAndUtxos' in walletStateFromStorage.state && 'tokens' in walletStateFromStorage.state ); }; export const getWalletState = wallet => { if (!wallet || !wallet.state) { return { balances: { totalBalance: 0, totalBalanceInSatoshis: 0 }, hydratedUtxoDetails: {}, tokens: [], slpBalancesAndUtxos: {}, parsedTxHistory: [], utxos: [], }; } return wallet.state; }; export function convertEtokenToEcashAddr(eTokenAddress) { if (!eTokenAddress) { return new Error( `cashMethods.convertToEcashAddr() error: No etoken address provided`, ); } // Confirm input is a valid eToken address const isValidInput = isValidEtokenAddress(eTokenAddress); if (!isValidInput) { return new Error( `cashMethods.convertToEcashAddr() error: ${eTokenAddress} is not a valid etoken address`, ); } // Check for etoken: prefix const isPrefixedEtokenAddress = eTokenAddress.slice(0, 7) === 'etoken:'; // If no prefix, assume it is checksummed for an etoken: prefix const testedEtokenAddr = isPrefixedEtokenAddress ? eTokenAddress : `etoken:${eTokenAddress}`; let ecashAddress; try { const { type, hash } = cashaddr.decode(testedEtokenAddr); ecashAddress = cashaddr.encode('ecash', type, hash); } catch (err) { return err; } return ecashAddress; } export function convertToEcashPrefix(bitcoincashPrefixedAddress) { // Prefix-less addresses may be valid, but the cashaddr.decode function used below // will throw an error without a prefix. Hence, must ensure prefix to use that function. const hasPrefix = bitcoincashPrefixedAddress.includes(':'); if (hasPrefix) { // Is it bitcoincash: or simpleledger: const { type, hash, prefix } = cashaddr.decode( bitcoincashPrefixedAddress, ); let newPrefix; if (prefix === 'bitcoincash') { newPrefix = 'ecash'; } else if (prefix === 'simpleledger') { newPrefix = 'etoken'; } else { return bitcoincashPrefixedAddress; } const convertedAddress = cashaddr.encode(newPrefix, type, hash); return convertedAddress; } else { return bitcoincashPrefixedAddress; } } export function convertEcashtoEtokenAddr(eCashAddress) { const isValidInput = isValidXecAddress(eCashAddress); if (!isValidInput) { return new Error(`${eCashAddress} is not a valid ecash address`); } // Check for ecash: prefix const isPrefixedEcashAddress = eCashAddress.slice(0, 6) === 'ecash:'; // If no prefix, assume it is checksummed for an ecash: prefix const testedEcashAddr = isPrefixedEcashAddress ? eCashAddress : `ecash:${eCashAddress}`; let eTokenAddress; try { const { type, hash } = cashaddr.decode(testedEcashAddr); eTokenAddress = cashaddr.encode('etoken', type, hash); } catch (err) { return new Error('eCash to eToken address conversion error'); } return eTokenAddress; } // converts ecash, etoken, bitcoincash and simpleledger addresses to hash160 export function toHash160(addr) { try { // decode address hash const { hash } = cashaddr.decode(addr); // encode the address hash to legacy format (bitcoin) const legacyAdress = bs58.encode(hash); // convert legacy to hash160 const addrHash160 = Buffer.from(bs58.decode(legacyAdress)).toString( 'hex', ); return addrHash160; } catch (err) { console.log('Error converting address to hash160'); throw err; } } export function toLegacyCash(addr) { // Confirm input is a valid ecash address const isValidInput = isValidXecAddress(addr); if (!isValidInput) { return new Error(`${addr} is not a valid ecash address`); } // Check for ecash: prefix const isPrefixedXecAddress = addr.slice(0, 6) === 'ecash:'; // If no prefix, assume it is checksummed for an ecash: prefix const testedXecAddr = isPrefixedXecAddress ? addr : `ecash:${addr}`; let legacyCashAddress; try { const { type, hash } = cashaddr.decode(testedXecAddr); legacyCashAddress = cashaddr.encode(currency.legacyPrefix, type, hash); } catch (err) { return err; } return legacyCashAddress; } export function toLegacyCashArray(addressArray) { let cleanArray = []; // array of bch converted addresses to be returned if ( addressArray === null || addressArray === undefined || !addressArray.length || addressArray === '' ) { return new Error('Invalid addressArray input'); } const arrayLength = addressArray.length; for (let i = 0; i < arrayLength; i++) { let addressValueArr = addressArray[i].split(','); let address = addressValueArr[0]; let value = addressValueArr[1]; // NB that toLegacyCash() includes address validation; will throw error for invalid address input const legacyAddress = toLegacyCash(address); if (legacyAddress instanceof Error) { return legacyAddress; } let convertedArrayData = legacyAddress + ',' + value + '\n'; cleanArray.push(convertedArrayData); } return cleanArray; } export function toLegacyToken(addr) { // Confirm input is a valid ecash address const isValidInput = isValidEtokenAddress(addr); if (!isValidInput) { return new Error(`${addr} is not a valid etoken address`); } // Check for ecash: prefix const isPrefixedEtokenAddress = addr.slice(0, 7) === 'etoken:'; // If no prefix, assume it is checksummed for an ecash: prefix const testedEtokenAddr = isPrefixedEtokenAddress ? addr : `etoken:${addr}`; let legacyTokenAddress; try { const { type, hash } = cashaddr.decode(testedEtokenAddr); legacyTokenAddress = cashaddr.encode('simpleledger', type, hash); } catch (err) { return err; } return legacyTokenAddress; } /* Converts a serialized buffer containing encrypted data into an object * that can be interpreted by the ecies-lite library. * * For reference on the parsing logic in this function refer to the link below on the segment of * ecies-lite's encryption function where the encKey, macKey, iv and cipher are sliced and concatenated * https://github.com/tibetty/ecies-lite/blob/8fd97e80b443422269d0223ead55802378521679/index.js#L46-L55 * * A similar PSF implmentation can also be found at: * https://github.com/Permissionless-Software-Foundation/bch-encrypt-lib/blob/master/lib/encryption.js * * For more detailed overview on the ecies encryption scheme, see https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption */ export const convertToEncryptStruct = encryptionBuffer => { // based on ecies-lite's encryption logic, the encryption buffer is concatenated as follows: // [ epk + iv + ct + mac ] whereby: // - The first 32 or 64 chars of the encryptionBuffer is the epk // - Both iv and ct params are 16 chars each, hence their combined substring is 32 chars from the end of the epk string // - within this combined iv/ct substring, the first 16 chars is the iv param, and ct param being the later half // - The mac param is appended to the end of the encryption buffer // validate input buffer if (!encryptionBuffer) { throw new Error( 'cashmethods.convertToEncryptStruct() error: input must be a buffer', ); } try { // variable tracking the starting char position for string extraction purposes let startOfBuf = 0; // *** epk param extraction *** // The first char of the encryptionBuffer indicates the type of the public key // If the first char is 4, then the public key is 64 chars // If the first char is 3 or 2, then the public key is 32 chars // Otherwise this is not a valid encryption buffer compatible with the ecies-lite library let publicKey; switch (encryptionBuffer[0]) { case 4: publicKey = encryptionBuffer.slice(0, 65); // extract first 64 chars as public key break; case 3: case 2: publicKey = encryptionBuffer.slice(0, 33); // extract first 32 chars as public key break; default: throw new Error(`Invalid type: ${encryptionBuffer[0]}`); } // *** iv and ct param extraction *** startOfBuf += publicKey.length; // sets the starting char position to the end of the public key (epk) in order to extract subsequent iv and ct substrings const encryptionTagLength = 32; // the length of the encryption tag (i.e. mac param) computed from each block of ciphertext, and is used to verify no one has tampered with the encrypted data const ivCtSubstring = encryptionBuffer.slice( startOfBuf, encryptionBuffer.length - encryptionTagLength, ); // extract the substring containing both iv and ct params, which is after the public key but before the mac param i.e. the 'encryption tag' const ivbufParam = ivCtSubstring.slice(0, 16); // extract the first 16 chars of substring as the iv param const ctbufParam = ivCtSubstring.slice(16); // extract the last 16 chars as substring the ct param // *** mac param extraction *** const macParam = encryptionBuffer.slice( encryptionBuffer.length - encryptionTagLength, encryptionBuffer.length, ); // extract the mac param appended to the end of the buffer return { iv: ivbufParam, epk: publicKey, ct: ctbufParam, mac: macParam, }; } catch (err) { console.error(`useBCH.convertToEncryptStruct() error: `, err); throw err; } }; export const isLegacyMigrationRequired = wallet => { // If the wallet does not have Path1899, // Or each Path1899, Path145, Path245 does not have a public key // Then it requires migration if ( !wallet.Path1899 || !wallet.Path1899.publicKey || !wallet.Path1899.hash160 || !wallet.Path145.publicKey || !wallet.Path145.hash160 || !wallet.Path245.publicKey || !wallet.Path245.hash160 ) { return true; } return false; }; export const getHashArrayFromWallet = wallet => { // If the wallet has wallet.Path1899.hash160, it's migrated and will have all of them // Return false for an umigrated wallet const hash160Array = wallet && wallet.Path1899 && 'hash160' in wallet.Path1899 ? [ wallet.Path245.hash160, wallet.Path145.hash160, wallet.Path1899.hash160, ] : false; return hash160Array; }; export const isActiveWebsocket = ws => { // Return true if websocket is connected and subscribed // Otherwise return false return ( ws !== null && ws && '_ws' in ws && 'readyState' in ws._ws && ws._ws.readyState === 1 && '_subs' in ws && ws._subs.length > 0 ); }; diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js index 185f70704..0d586c763 100644 --- a/web/cashtab/src/utils/validation.js +++ b/web/cashtab/src/utils/validation.js @@ -1,538 +1,583 @@ import BigNumber from 'bignumber.js'; import { currency } from 'components/Common/Ticker.js'; import { fromSatoshisToXec } from 'utils/cashMethods'; import cashaddr from 'ecashaddrjs'; // Validate cash amount export const shouldRejectAmountInput = ( cashAmount, selectedCurrency, fiatPrice, totalCashBalance, ) => { // Take cashAmount as input, a string from form input let error = false; let testedAmount = new BigNumber(cashAmount); if (selectedCurrency !== currency.ticker) { // Ensure no more than currency.cashDecimals decimal places testedAmount = new BigNumber(fiatToCrypto(cashAmount, fiatPrice)); } // Validate value for > 0 if (isNaN(testedAmount)) { error = 'Amount must be a number'; } else if (testedAmount.lte(0)) { error = 'Amount must be greater than 0'; } else if (testedAmount.lt(fromSatoshisToXec(currency.dustSats))) { error = `Send amount must be at least ${fromSatoshisToXec( currency.dustSats, ).toString()} ${currency.ticker}`; } else if (testedAmount.gt(totalCashBalance)) { error = `Amount cannot exceed your ${currency.ticker} balance`; } else if (!isNaN(testedAmount) && testedAmount.toString().includes('.')) { if ( testedAmount.toString().split('.')[1].length > currency.cashDecimals ) { error = `${currency.ticker} transactions do not support more than ${currency.cashDecimals} decimal places`; } } // return false if no error, or string error msg if error return error; }; export const fiatToCrypto = ( fiatAmount, fiatPrice, cashDecimals = currency.cashDecimals, ) => { let cryptoAmount = new BigNumber(fiatAmount) .div(new BigNumber(fiatPrice)) .toFixed(cashDecimals); return cryptoAmount; }; export const isProbablyNotAScamTokenName = tokenName => { // convert to lower case, trim leading and trailing spaces // split, filter then join on ' ' for cases where user inputs multiple spaces const sanitizedTokenName = tokenName .toLowerCase() .trim() .split(' ') .filter(string => string) .join(' '); return ( !currency.coingeckoTop500Names.includes(sanitizedTokenName) && // for cases where user adds spaces between e a c h letter !currency.coingeckoTop500Names.includes( sanitizedTokenName.split(' ').join(''), ) && // cross reference with coingeckoTop500Tickers !currency.coingeckoTop500Tickers.includes(sanitizedTokenName) && !currency.coingeckoTop500Tickers.includes( sanitizedTokenName.split(' ').join(''), ) && //cross reference with coingeckoTop500Ids !currency.coingeckoTop500Ids.includes(sanitizedTokenName) && !currency.coingeckoTop500Ids.includes( sanitizedTokenName.split(' ').join(''), ) && //cross reference with bannedFiatCurrencies !currency.settingsValidation.fiatCurrency.includes( sanitizedTokenName, ) && !currency.settingsValidation.fiatCurrency.includes( sanitizedTokenName.split(' ').join(''), ) && //cross reference with bannedTickers !currency.bannedTickers.includes(sanitizedTokenName) && !currency.bannedTickers.includes( sanitizedTokenName.split(' ').join(''), ) && //cross reference with bannedNames !currency.bannedNames.includes(sanitizedTokenName) && !currency.bannedNames.includes(sanitizedTokenName.split(' ').join('')) ); }; export const isProbablyNotAScamTokenTicker = tokenTicker => { // convert to lower case, trim leading and trailing spaces // split, filter then join on ' ' for cases where user inputs multiple spaces const sanitizedTokenTicker = tokenTicker .toLowerCase() .trim() .split(' ') .filter(string => string) .join(''); return ( !currency.coingeckoTop500Tickers.includes(sanitizedTokenTicker) && // for cases where user adds spaces between e a c h letter !currency.coingeckoTop500Tickers.includes( sanitizedTokenTicker.split(' ').join(''), ) && //cross reference with coingeckoTop500Names !currency.coingeckoTop500Names.includes(sanitizedTokenTicker) && !currency.coingeckoTop500Names.includes( sanitizedTokenTicker.split(' ').join(''), ) && //cross reference with coingeckoTop500Ids !currency.coingeckoTop500Ids.includes(sanitizedTokenTicker) && !currency.coingeckoTop500Ids.includes( sanitizedTokenTicker.split(' ').join(''), ) && //cross reference with bannedFiatCurrencies !currency.settingsValidation.fiatCurrency.includes( sanitizedTokenTicker, ) && !currency.settingsValidation.fiatCurrency.includes( sanitizedTokenTicker.split(' ').join(''), ) && //cross reference with bannedTickers !currency.bannedTickers.includes(sanitizedTokenTicker) && !currency.bannedTickers.includes( sanitizedTokenTicker.split(' ').join(''), ) && //cross reference with bannedNames !currency.bannedNames.includes(sanitizedTokenTicker) && !currency.bannedNames.includes(sanitizedTokenTicker.split(' ').join('')) ); }; export const isValidTokenName = tokenName => { return ( typeof tokenName === 'string' && tokenName.length > 0 && tokenName.length < 68 ); }; export const isValidTokenTicker = tokenTicker => { return ( typeof tokenTicker === 'string' && tokenTicker.length > 0 && tokenTicker.length < 13 ); }; export const isValidTokenDecimals = tokenDecimals => { return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes( tokenDecimals, ); }; export const isValidTokenInitialQty = (tokenInitialQty, tokenDecimals) => { const minimumQty = new BigNumber(1 / 10 ** tokenDecimals); const tokenIntialQtyBig = new BigNumber(tokenInitialQty); return ( tokenIntialQtyBig.gte(minimumQty) && tokenIntialQtyBig.lt(100000000000) && tokenIntialQtyBig.dp() <= tokenDecimals ); }; 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) ); }; export const isValidCashtabSettings = settings => { try { let isValidSettingParams = true; for (let param in currency.defaultSettings) { if ( !Object.prototype.hasOwnProperty.call(settings, param) || !currency.settingsValidation[param].includes(settings[param]) ) { isValidSettingParams = false; break; } } const isValid = typeof settings === 'object' && isValidSettingParams; return isValid; } catch (err) { return false; } }; export const parseInvalidSettingsForMigration = invalidCashtabSettings => { // create a copy of the invalidCashtabSettings let migratedCashtabSettings = invalidCashtabSettings; // determine if settings are invalid because it is missing a parameter for (let param in currency.defaultSettings) { if ( !Object.prototype.hasOwnProperty.call(invalidCashtabSettings, param) ) { // adds the default setting for only that parameter migratedCashtabSettings[param] = currency.defaultSettings[param]; } } return migratedCashtabSettings; }; export const isValidContactList = contactList => { /* A valid contact list is an array of objects An empty contact list looks like [{}] Although a valid contact list does not contain duplicated addresses, this is not checked here. This is checked for when contacts are added. Duplicate addresses will not break the app if a user somehow sideloads a contact list with everything valid except some addresses are duplicated. */ if (!Array.isArray(contactList)) { return false; } for (let i = 0; i < contactList.length; i += 1) { const contactObj = contactList[i]; // Must have keys 'address' and 'name' if ( typeof contactObj === 'object' && 'address' in contactObj && 'name' in contactObj ) { // Address must be a valid XEC address, name must be a string if ( isValidXecAddress(contactObj.address) && typeof contactObj.name === 'string' ) { continue; } return false; } else { // Check for empty object in an array of length 1, the default blank contactList if ( contactObj && Object.keys(contactObj).length === 0 && Object.getPrototypeOf(contactObj) === Object.prototype && contactList.length === 1 ) { // [{}] is valid, default blank // But a list with random blanks is not valid return true; } return false; } } // If you get here, it's good return true; }; export const isValidCashtabCache = cashtabCache => { /* Object must contain all keys listed in currency.defaultCashtabCache The tokenInfoById object must have keys that are valid token IDs, and at each one an object like: { "tokenTicker": "ST", "tokenName": "ST", "tokenDocumentUrl": "developer.bitcoin.com", "tokenDocumentHash": "", "decimals": 0, "tokenId": "bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd" } i.e. an object that contains these keys 'tokenTicker' is a string 'tokenName' is a string 'tokenDocumentUrl' is a string 'tokenDocumentHash' is a string 'decimals' is a number 'tokenId' is a valid tokenId */ // Check that every key in currency.defaultCashtabCache is also in this cashtabCache const cashtabCacheKeys = Object.keys(currency.defaultCashtabCache); for (let i = 0; i < cashtabCacheKeys.length; i += 1) { const thisKey = cashtabCacheKeys[i]; if (thisKey in cashtabCache) { continue; } return false; } // Check that tokenInfoById is expected type and that tokenIds are valid const { tokenInfoById } = cashtabCache; const tokenIds = Object.keys(tokenInfoById); for (let i = 0; i < tokenIds.length; i += 1) { const thisTokenId = tokenIds[i]; if (!isValidTokenId(thisTokenId)) { return false; } const { tokenTicker, tokenName, tokenDocumentUrl, tokenDocumentHash, decimals, tokenId, } = tokenInfoById[thisTokenId]; if ( typeof tokenTicker !== 'string' || typeof tokenName !== 'string' || typeof tokenDocumentUrl !== 'string' || typeof tokenDocumentHash !== 'string' || typeof decimals !== 'number' || !isValidTokenId(tokenId) ) { return false; } } return true; }; export const isValidXecAddress = addr => { /* Returns true for a valid XEC address Valid XEC address: - May or may not have prefix `ecash:` - Checksum must validate for prefix `ecash:` An eToken address is not considered a valid XEC address */ if (!addr) { return false; } let isValidXecAddress; let isPrefixedXecAddress; // Check for possible prefix if (addr.includes(':')) { // Test for 'ecash:' prefix isPrefixedXecAddress = addr.slice(0, 6) === 'ecash:'; // Any address including ':' that doesn't start explicitly with 'ecash:' is invalid if (!isPrefixedXecAddress) { isValidXecAddress = false; return isValidXecAddress; } } else { isPrefixedXecAddress = false; } // If no prefix, assume it is checksummed for an ecash: prefix const testedXecAddr = isPrefixedXecAddress ? addr : `ecash:${addr}`; try { const decoded = cashaddr.decode(testedXecAddr); if (decoded.prefix === 'ecash') { isValidXecAddress = true; } } catch (err) { isValidXecAddress = false; } return isValidXecAddress; }; +export const isValidBchAddress = addr => { + /* + Returns true for a valid BCH address + + Valid BCH address: + - May or may not have prefix `bitcoincash:` + - Checksum must validate for prefix `bitcoincash:` + + A simple ledger address is not considered a valid bitcoincash address + */ + + if (!addr) { + return false; + } + + let isValidBchAddress; + let isPrefixedBchAddress; + + // Check for possible prefix + if (addr.includes(':')) { + // Test for 'ecash:' prefix + isPrefixedBchAddress = addr.slice(0, 12) === 'bitcoincash:'; + // Any address including ':' that doesn't start explicitly with 'bitcoincash:' is invalid + if (!isPrefixedBchAddress) { + isValidBchAddress = false; + return isValidBchAddress; + } + } else { + isPrefixedBchAddress = false; + } + + // If no prefix, assume it is checksummed for an bitcoincash: prefix + const testedXecAddr = isPrefixedBchAddress ? addr : `bitcoincash:${addr}`; + + try { + const decoded = cashaddr.decode(testedXecAddr); + if (decoded.prefix === 'bitcoincash') { + isValidBchAddress = true; + } + } catch (err) { + isValidBchAddress = false; + } + return isValidBchAddress; +}; + export const isValidEtokenAddress = addr => { /* Returns true for a valid eToken address Valid eToken address: - May or may not have prefix `etoken:` - Checksum must validate for prefix `etoken:` An XEC address is not considered a valid eToken address */ if (!addr) { return false; } let isValidEtokenAddress; let isPrefixedEtokenAddress; // Check for possible prefix if (addr.includes(':')) { // Test for 'etoken:' prefix isPrefixedEtokenAddress = addr.slice(0, 7) === 'etoken:'; // Any token address including ':' that doesn't start explicitly with 'etoken:' is invalid if (!isPrefixedEtokenAddress) { isValidEtokenAddress = false; return isValidEtokenAddress; } } else { isPrefixedEtokenAddress = false; } // If no prefix, assume it is checksummed for an etoken: prefix const testedEtokenAddr = isPrefixedEtokenAddress ? addr : `etoken:${addr}`; try { const decoded = cashaddr.decode(testedEtokenAddr); if (decoded.prefix === 'etoken') { isValidEtokenAddress = true; } } catch (err) { isValidEtokenAddress = false; } return isValidEtokenAddress; }; export const isValidXecSendAmount = xecSendAmount => { // A valid XEC send amount must be a number higher than the app dust limit return ( xecSendAmount !== null && typeof xecSendAmount !== 'undefined' && !isNaN(parseFloat(xecSendAmount)) && parseFloat(xecSendAmount) >= fromSatoshisToXec(currency.dustSats).toNumber() ); }; export const isValidEtokenBurnAmount = (tokenBurnAmount, maxAmount) => { // A valid eToken burn amount must be between 1 and the wallet's token balance return ( tokenBurnAmount !== null && maxAmount !== null && typeof tokenBurnAmount !== 'undefined' && typeof maxAmount !== 'undefined' && new BigNumber(tokenBurnAmount).gt(0) && new BigNumber(tokenBurnAmount).lte(maxAmount) ); }; // XEC airdrop field validations export const isValidTokenId = tokenId => { // disable no-useless-escape for regex //eslint-disable-next-line const format = /[ `!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/; const specialCharCheck = format.test(tokenId); return ( typeof tokenId === 'string' && tokenId.length === 64 && tokenId.trim() != '' && !specialCharCheck ); }; export const isValidNewWalletNameLength = newWalletName => { return ( typeof newWalletName === 'string' && newWalletName.length > 0 && newWalletName.length <= currency.localStorageMaxCharacters && newWalletName.length !== '' ); }; export const isValidXecAirdrop = xecAirdrop => { return ( typeof xecAirdrop === 'string' && xecAirdrop.length > 0 && xecAirdrop.trim() != '' && new BigNumber(xecAirdrop).gt(0) ); }; export const isValidAirdropOutputsArray = airdropOutputsArray => { if (!airdropOutputsArray) { return false; } let isValid = true; // split by individual rows const addressStringArray = airdropOutputsArray.split('\n'); for (let i = 0; i < addressStringArray.length; i++) { const substring = addressStringArray[i].split(','); let valueString = substring[1]; // if the XEC being sent is less than dust sats or contains extra values per line if ( new BigNumber(valueString).lt( fromSatoshisToXec(currency.dustSats), ) || substring.length !== 2 ) { isValid = false; } } return isValid; }; export const isValidAirdropExclusionArray = airdropExclusionArray => { if (!airdropExclusionArray || airdropExclusionArray.length === 0) { return false; } let isValid = true; // split by comma as the delimiter const addressStringArray = airdropExclusionArray.split(','); // parse and validate each address in array for (let i = 0; i < addressStringArray.length; i++) { if (!isValidXecAddress(addressStringArray[i])) { return false; } } return isValid; };