diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js index c881a47ce..c5f19c839 100644 --- a/web/cashtab/src/components/Common/Ticker.js +++ b/web/cashtab/src/components/Common/Ticker.js @@ -1,340 +1,226 @@ import mainLogo from '@assets/logo_primary.png'; import tokenLogo from '@assets/logo_secondary.png'; -import cashaddr from 'ecashaddrjs'; import BigNumber from 'bignumber.js'; export const currency = { name: 'eCash', ticker: 'XEC', logo: mainLogo, legacyPrefix: 'bitcoincash', prefixes: ['ecash'], coingeckoId: 'ecash', defaultFee: 2.01, dustSats: 550, etokenSats: 546, cashDecimals: 2, blockExplorerUrl: 'https://explorer.bitcoinabc.org', tokenExplorerUrl: 'https://explorer.be.cash', blockExplorerUrlTestnet: 'https://texplorer.bitcoinabc.org', tokenName: 'eToken', tokenTicker: 'eToken', tokenIconSubmitApi: 'https://icons.etokens.cash/new', tokenLogo: tokenLogo, tokenPrefixes: ['etoken'], tokenIconsUrl: 'https://etoken-icons.s3.us-west-2.amazonaws.com', txHistoryCount: 5, xecApiBatchSize: 20, defaultSettings: { fiatCurrency: 'usd' }, notificationDurationShort: 3, notificationDurationLong: 5, newTokenDefaultUrl: 'https://cashtab.com/', opReturn: { opReturnPrefixHex: '6a', opReturnAppPrefixLengthHex: '04', opPushDataOne: '4c', appPrefixesHex: { eToken: '534c5000', cashtab: '00746162', }, }, settingsValidation: { fiatCurrency: [ 'usd', 'idr', 'krw', 'cny', 'zar', 'vnd', 'cad', 'nok', 'eur', 'gbp', 'jpy', 'try', 'rub', 'inr', 'brl', 'php', 'ils', 'clp', 'twd', 'hkd', 'bhd', 'sar', 'aud', 'nzd', ], }, fiatCurrencies: { usd: { name: 'US Dollar', symbol: '$', slug: 'usd' }, aud: { name: 'Australian Dollar', symbol: '$', slug: 'aud' }, bhd: { name: 'Bahraini Dinar', symbol: 'BD', slug: 'bhd' }, brl: { name: 'Brazilian Real', symbol: 'R$', slug: 'brl' }, gbp: { name: 'British Pound', symbol: '£', slug: 'gbp' }, cad: { name: 'Canadian Dollar', symbol: '$', slug: 'cad' }, clp: { name: 'Chilean Peso', symbol: '$', slug: 'clp' }, cny: { name: 'Chinese Yuan', symbol: '元', slug: 'cny' }, eur: { name: 'Euro', symbol: '€', slug: 'eur' }, hkd: { name: 'Hong Kong Dollar', symbol: 'HK$', slug: 'hkd' }, inr: { name: 'Indian Rupee', symbol: '₹', slug: 'inr' }, idr: { name: 'Indonesian Rupiah', symbol: 'Rp', slug: 'idr' }, ils: { name: 'Israeli Shekel', symbol: '₪', slug: 'ils' }, jpy: { name: 'Japanese Yen', symbol: '¥', slug: 'jpy' }, krw: { name: 'Korean Won', symbol: '₩', slug: 'krw' }, nzd: { name: 'New Zealand Dollar', symbol: '$', slug: 'nzd' }, nok: { name: 'Norwegian Krone', symbol: 'kr', slug: 'nok' }, php: { name: 'Philippine Peso', symbol: '₱', slug: 'php' }, rub: { name: 'Russian Ruble', symbol: 'р.', slug: 'rub' }, twd: { name: 'New Taiwan Dollar', symbol: 'NT$', slug: 'twd' }, sar: { name: 'Saudi Riyal', symbol: 'SAR', slug: 'sar' }, zar: { name: 'South African Rand', symbol: 'R', slug: 'zar' }, try: { name: 'Turkish Lira', symbol: '₺', slug: 'try' }, vnd: { name: 'Vietnamese đồng', symbol: 'đ', slug: 'vnd' }, }, }; 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 { // 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 function isValidCashPrefix(addressString) { // Note that this function validates prefix only // Check for prefix included in currency.prefixes array // For now, validation is handled by converting to bitcoincash: prefix and checksum // and relying on legacy validation methods of bitcoincash: prefix addresses // Also accept an address with no prefix, as some exchanges provide these for (let i = 0; i < currency.prefixes.length; i += 1) { // If the addressString being tested starts with an accepted prefix or no prefix at all if ( addressString.startsWith(currency.prefixes[i] + ':') || !addressString.includes(':') ) { return true; } } return false; } export function isValidTokenPrefix(addressString) { // Check for prefix included in currency.tokenPrefixes array // For now, validation is handled by converting to simpleledger: prefix and checksum // and relying on legacy validation methods of simpleledger: prefix addresses // For token addresses, do not accept an address with no prefix for (let i = 0; i < currency.tokenPrefixes.length; i += 1) { if (addressString.startsWith(currency.tokenPrefixes[i] + ':')) { return true; } } return false; } -export function toLegacy(address) { - let testedAddress; - let legacyAddress; - - try { - if (isValidCashPrefix(address)) { - // 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 = address.includes(':'); - if (!hasPrefix) { - testedAddress = currency.legacyPrefix + ':' + address; - } else { - testedAddress = address; - } - - // Note: an `ecash:` checksum address with no prefix will not be validated by - // parseAddress in Send.js - - // Only handle the case of prefixless address that is valid `bitcoincash:` address - - const { type, hash } = cashaddr.decode(testedAddress); - legacyAddress = cashaddr.encode(currency.legacyPrefix, type, hash); - } else { - console.log(`Error: ${address} is not a cash address`); - throw new Error( - 'Address prefix is not a valid cash address with a prefix from the Ticker.prefixes array', - ); - } - } catch (err) { - return err; - } - return legacyAddress; -} - -export function toLegacyArray(addressArray) { - let cleanArray = []; // array of bch converted addresses to be returned - - try { - if ( - addressArray === null || - addressArray === undefined || - !addressArray.length || - addressArray === '' - ) { - throw new Error('Invalid addressArray input'); - } - - const arrayLength = addressArray.length; - - for (let i = 0; i < arrayLength; i++) { - let testedAddress; - let legacyAddress; - let addressValueArr = addressArray[i].split(','); - let address = addressValueArr[0]; - let value = addressValueArr[1]; - - if (isValidCashPrefix(address)) { - // 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 = address.includes(':'); - if (!hasPrefix) { - testedAddress = currency.legacyPrefix + ':' + address; - } else { - testedAddress = address; - } - - // Note: an `ecash:` checksum address with no prefix will not be validated by - // parseAddress in Send.js - - // Only handle the case of prefixless address that is valid `bitcoincash:` address - const { type, hash } = cashaddr.decode(testedAddress); - legacyAddress = cashaddr.encode( - currency.legacyPrefix, - type, - hash, - ); - - let convertedArrayData = legacyAddress + ',' + value + '\n'; - cleanArray.push(convertedArrayData); - } else { - console.log(`Error: ${address} is not a cash address`); - throw new Error( - 'Address prefix is not a valid cash address with a prefix from the Ticker.prefixes array', - ); - } - } - } catch (err) { - return err; - } - return cleanArray; -} - -export function parseAddress(BCH, addressString, isToken = false) { +export function parseAddressForParams(addressString) { // Build return obj const addressInfo = { address: '', - isValid: false, queryString: null, amount: null, }; // Parse address string for parameters const paramCheck = addressString.split('?'); let cleanAddress = paramCheck[0]; addressInfo.address = cleanAddress; - // Validate address - let isValidAddress; - try { - isValidAddress = BCH.Address.isCashAddress(cleanAddress); - // Only accept addresses with ecash: prefix - const { prefix } = cashaddr.decode(cleanAddress); - // If the address does not have a valid prefix or token prefix - if ( - (!isToken && !currency.prefixes.includes(prefix)) || - (isToken && !currency.tokenPrefixes.includes(prefix)) - ) { - // then it is not a valid destination address for XEC sends - isValidAddress = false; - } - } catch (err) { - isValidAddress = false; - } - - addressInfo.isValid = isValidAddress; - // Check for parameters // only the amount param is currently supported let queryString = null; let amount = null; if (paramCheck.length > 1) { queryString = paramCheck[1]; addressInfo.queryString = queryString; const addrParams = new URLSearchParams(queryString); if (addrParams.has('amount')) { // Amount in satoshis try { amount = new BigNumber(parseInt(addrParams.get('amount'))) .div(10 ** currency.cashDecimals) .toString(); } catch (err) { amount = null; } } } addressInfo.amount = amount; return addressInfo; } diff --git a/web/cashtab/src/components/Common/__tests__/Ticker.test.js b/web/cashtab/src/components/Common/__tests__/Ticker.test.js index 11499cf40..5039198e2 100644 --- a/web/cashtab/src/components/Common/__tests__/Ticker.test.js +++ b/web/cashtab/src/components/Common/__tests__/Ticker.test.js @@ -1,226 +1,134 @@ -import { ValidationError } from 'ecashaddrjs'; import { isValidCashPrefix, isValidTokenPrefix, - toLegacy, - toLegacyArray, parseOpReturn, - currency, } from '../Ticker'; -import { - validAddressArrayInput, - validAddressArrayOutput, - validLargeAddressArrayInput, - validLargeAddressArrayOutput, - invalidAddressArrayInput, -} from '../__mocks__/mockAddressArray'; + import { shortCashtabMessageInputHex, longCashtabMessageInputHex, shortExternalMessageInputHex, longExternalMessageInputHex, shortSegmentedExternalMessageInputHex, longSegmentedExternalMessageInputHex, mixedSegmentedExternalMessageInputHex, mockParsedShortCashtabMessageArray, mockParsedLongCashtabMessageArray, mockParsedShortExternalMessageArray, mockParsedLongExternalMessageArray, mockParsedShortSegmentedExternalMessageArray, mockParsedLongSegmentedExternalMessageArray, mockParsedMixedSegmentedExternalMessageArray, eTokenInputHex, mockParsedETokenOutputArray, } from '../__mocks__/mockOpReturnParsedArray'; test('Rejects cash address with bitcoincash: prefix', async () => { const result = isValidCashPrefix( 'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0', ); expect(result).toStrictEqual(false); }); test('Correctly validates cash address with bitcoincash: checksum but no prefix', async () => { const result = isValidCashPrefix( 'qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ); expect(result).toStrictEqual(true); }); test('Correctly validates cash address with ecash: checksum but no prefix', async () => { const result = isValidCashPrefix( 'qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', ); expect(result).toStrictEqual(true); }); test('Correctly validates cash address with ecash: prefix', async () => { const result = isValidCashPrefix( 'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', ); expect(result).toStrictEqual(true); }); test('Rejects token address with simpleledger: prefix', async () => { const result = isValidTokenPrefix( 'simpleledger:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', ); expect(result).toStrictEqual(false); }); test('Does not accept a valid token address without a prefix', async () => { const result = isValidTokenPrefix( 'qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', ); expect(result).toStrictEqual(false); }); test('Correctly validates token address with etoken: prefix (prefix only, not checksum)', async () => { const result = isValidTokenPrefix( 'etoken:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', ); expect(result).toStrictEqual(true); }); test('Recognizes unaccepted token prefix (prefix only, not checksum)', async () => { const result = isValidTokenPrefix( 'wtftoken:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', ); expect(result).toStrictEqual(false); }); test('Knows that acceptable cash prefixes are not tokens', async () => { const result = isValidTokenPrefix( 'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', ); expect(result).toStrictEqual(false); }); test('Address with unlisted prefix is invalid', async () => { const result = isValidCashPrefix( 'ecashdoge:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', ); expect(result).toStrictEqual(false); }); -test('toLegacy() converts a valid ecash: prefix address to a valid bitcoincash: prefix address', async () => { - const result = toLegacy('ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc'); - expect(result).toStrictEqual( - 'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0', - ); -}); - -test('toLegacy() accepts a valid BCH address with no prefix and returns with prefix', async () => { - const result = toLegacy('qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0'); - expect(result).toStrictEqual( - 'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0', - ); -}); - -test('toLegacy throws error if input address has invalid checksum', async () => { - const result = toLegacy('ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25m'); - - expect(result).toStrictEqual( - new ValidationError( - 'Invalid checksum: ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25m.', - ), - ); -}); - -test('toLegacy throws error if input address has invalid prefix', async () => { - const result = toLegacy( - 'notecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', - ); - - expect(result).toStrictEqual( - new Error( - 'Address prefix is not a valid cash address with a prefix from the Ticker.prefixes array', - ), - ); -}); - -test('toLegacyArray throws error if the addressArray input is null', async () => { - const result = toLegacyArray(null); - - expect(result).toStrictEqual(new Error('Invalid addressArray input')); -}); - -test('toLegacyArray throws error if the addressArray input is empty', async () => { - const result = toLegacyArray([]); - - expect(result).toStrictEqual(new Error('Invalid addressArray input')); -}); - -test('toLegacyArray throws error if the addressArray input is a number', async () => { - const result = toLegacyArray(12345); - - expect(result).toStrictEqual(new Error('Invalid addressArray input')); -}); - -test('toLegacyArray throws error if the addressArray input is undefined', async () => { - const result = toLegacyArray(undefined); - - expect(result).toStrictEqual(new Error('Invalid addressArray input')); -}); - -test('toLegacyArray successfully converts a standard sized valid addressArray input', async () => { - const result = toLegacyArray(validAddressArrayInput); - - expect(result).toStrictEqual(validAddressArrayOutput); -}); - -test('toLegacyArray successfully converts a large valid addressArray input', async () => { - const result = toLegacyArray(validLargeAddressArrayInput); - - expect(result).toStrictEqual(validLargeAddressArrayOutput); -}); - -test('toLegacyArray throws an error on an addressArray with invalid addresses', async () => { - const result = toLegacyArray(invalidAddressArrayInput); - - expect(result).toStrictEqual( - new ValidationError( - 'Invalid checksum: ecash:qrqgwxrrrrrrrrrrrrrrrrrrrrrrrrr7zsvk.', - ), - ); -}); - test('parseOpReturn() successfully parses a short cashtab message', async () => { const result = parseOpReturn(shortCashtabMessageInputHex); expect(result).toStrictEqual(mockParsedShortCashtabMessageArray); }); test('parseOpReturn() successfully parses a long cashtab message where an additional PUSHDATA1 is present', async () => { const result = parseOpReturn(longCashtabMessageInputHex); expect(result).toStrictEqual(mockParsedLongCashtabMessageArray); }); test('parseOpReturn() successfully parses a short external message', async () => { const result = parseOpReturn(shortExternalMessageInputHex); expect(result).toStrictEqual(mockParsedShortExternalMessageArray); }); test('parseOpReturn() successfully parses a long external message where an additional PUSHDATA1 is present', async () => { const result = parseOpReturn(longExternalMessageInputHex); expect(result).toStrictEqual(mockParsedLongExternalMessageArray); }); test('parseOpReturn() successfully parses an external message that is segmented into separate short parts', async () => { const result = parseOpReturn(shortSegmentedExternalMessageInputHex); expect(result).toStrictEqual(mockParsedShortSegmentedExternalMessageArray); }); test('parseOpReturn() successfully parses an external message that is segmented into separate long parts', async () => { const result = parseOpReturn(longSegmentedExternalMessageInputHex); expect(result).toStrictEqual(mockParsedLongSegmentedExternalMessageArray); }); test('parseOpReturn() successfully parses an external message that is segmented into separate long and short parts', async () => { const result = parseOpReturn(mixedSegmentedExternalMessageInputHex); expect(result).toStrictEqual(mockParsedMixedSegmentedExternalMessageArray); }); test('parseOpReturn() successfully parses an eToken output', async () => { const result = parseOpReturn(eTokenInputHex); expect(result).toStrictEqual(mockParsedETokenOutputArray); }); diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index 4dbdb8c06..c0656ebec 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,954 +1,943 @@ import React, { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import PropTypes from 'prop-types'; import { WalletContext } from '@utils/context'; import { AntdFormWrapper, SendBchInput, DestinationAddressSingle, DestinationAddressMulti, } from '@components/Common/EnhancedInputs'; import { AdvancedCollapse } from '@components/Common/StyledCollapse'; import { Form, message, Modal, Alert, Collapse, Input, Button } from 'antd'; const { Panel } = Collapse; const { TextArea } = Input; import { Row, Col } from 'antd'; import PrimaryButton, { SecondaryButton, SmartButton, } from '@components/Common/PrimaryButton'; import useBCH from '@hooks/useBCH'; import useWindowDimensions from '@hooks/useWindowDimensions'; import { sendXecNotification, errorNotification, messageSignedNotification, } from '@components/Common/Notifications'; import { isMobile, isIOS, isSafari } from 'react-device-detect'; -import { - currency, - isValidTokenPrefix, - parseAddress, - toLegacy, - toLegacyArray, -} from '@components/Common/Ticker.js'; +import { currency, parseAddressForParams } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; import { fiatToCrypto, shouldRejectAmountInput, - isValidSendToMany, + isValidXecAddress, + isValidEtokenAddress, + isValidXecSendAmount, } from '@utils/validation'; import BalanceHeader from '@components/Common/BalanceHeader'; import BalanceHeaderFiat from '@components/Common/BalanceHeaderFiat'; import { ZeroBalanceHeader, ConvertAmount, AlertMsg, } from '@components/Common/Atoms'; -import { getWalletState } from '@utils/cashMethods'; +import { + getWalletState, + convertToEcashPrefix, + toLegacyCash, + toLegacyCashArray, +} from '@utils/cashMethods'; import ApiError from '@components/Common/ApiError'; import { formatFiatBalance } from '@utils/validation'; import { TokenParamLabel } from '@components/Common/Atoms'; import { PlusSquareOutlined } from '@ant-design/icons'; import styled from 'styled-components'; -import { convertToEcashPrefix } from '@utils/cashMethods'; import { CopyToClipboard } from 'react-copy-to-clipboard'; const StyledSpacer = styled.div` height: 1px; width: 100%; background-color: ${props => props.theme.wallet.borders.color}; margin: 60px 0 50px; `; const SignMessageLabel = styled.div` text-align: left; color: #0074c2; `; const RecipientModeLabel = styled.div` color: silver; `; const TextAreaLabel = styled.div` text-align: left; color: #0074c2; padding-left: 1px; `; // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest const SendBCH = ({ jestBCH, passLoadingStatus }) => { // use balance parameters from wallet.state object and not legacy balances parameter from walletState, if user has migrated wallet // this handles edge case of user with old wallet who has not opened latest Cashtab version yet // If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object // Else set it as blank const ContextValue = React.useContext(WalletContext); const location = useLocation(); const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue; const walletState = getWalletState(wallet); const { balances, slpBalancesAndUtxos } = walletState; // Modal settings const [showConfirmMsgToSign, setShowConfirmMsgToSign] = useState(false); const [msgToSign, setMsgToSign] = useState(''); const [signMessageIsValid, setSignMessageIsValid] = useState(null); const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false); // Get device window width // If this is less than 769, the page will open with QR scanner open const { width } = useWindowDimensions(); // Load with QR code open if device is mobile and NOT iOS + anything but safari const scannerSupported = width < 769 && isMobile && !(isIOS && !isSafari); const [formData, setFormData] = useState({ value: '', address: '', opReturnMsg: '', }); const [queryStringText, setQueryStringText] = useState(null); const [sendBchAddressError, setSendBchAddressError] = useState(false); const [sendBchAmountError, setSendBchAmountError] = useState(false); const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker); // Support cashtab button from web pages const [txInfoFromUrl, setTxInfoFromUrl] = useState(false); // Show a confirmation modal on transactions created by populating form from web page button const [isModalVisible, setIsModalVisible] = useState(false); const [messageSignature, setMessageSignature] = useState(''); const [sigCopySuccess, setSigCopySuccess] = useState(''); const clearInputForms = () => { setFormData({ value: '', address: '', opReturnMsg: '', }); }; const showModal = () => { setIsModalVisible(true); }; const handleOk = () => { setIsModalVisible(false); send(); }; const handleCancel = () => { setIsModalVisible(false); }; const { getBCH, getRestUrl, sendXec, calcFee, signPkMessage } = useBCH(); // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); const BCH = jestBCH ? jestBCH : getBCH(); // If the balance has changed, unlock the UI // This is redundant, if backend has refreshed in 1.75s timeout below, UI will already be unlocked useEffect(() => { passLoadingStatus(false); }, [balances.totalBalance]); useEffect(() => { // Manually parse for txInfo object on page load when Send.js is loaded with a query string // if this was routed from Wallet screen's Reply to message link then prepopulate the address and value field if (location && location.state && location.state.replyAddress) { setFormData({ address: location.state.replyAddress, value: 5.5, }); } // Do not set txInfo in state if query strings are not present if ( !window.location || !window.location.hash || window.location.hash === '#/send' ) { return; } const txInfoArr = window.location.hash.split('?')[1].split('&'); // Iterate over this to create object const txInfo = {}; for (let i = 0; i < txInfoArr.length; i += 1) { let txInfoKeyValue = txInfoArr[i].split('='); let key = txInfoKeyValue[0]; let value = txInfoKeyValue[1]; txInfo[key] = value; } console.log(`txInfo from page params`, txInfo); setTxInfoFromUrl(txInfo); populateFormsFromUrl(txInfo); }, []); function populateFormsFromUrl(txInfo) { if (txInfo && txInfo.address && txInfo.value) { setFormData({ address: txInfo.address, value: txInfo.value, }); } } function handleSendXecError(errorObj, oneToManyFlag) { // Set loading to false here as well, as balance may not change depending on where error occured in try loop passLoadingStatus(false); let message; if (!errorObj.error && !errorObj.message) { message = `Transaction failed: no response from ${getRestUrl()}.`; } else if ( /Could not communicate with full node or other external service/.test( errorObj.error, ) ) { message = 'Could not communicate with API. Please try again.'; } else if ( errorObj.error && errorObj.error.includes( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)', ) ) { message = `The ${currency.ticker} you are trying to send has too many unconfirmed ancestors to send (limit 50). Sending will be possible after a block confirmation. Try again in about 10 minutes.`; } else { message = errorObj.message || errorObj.error || JSON.stringify(errorObj); } if (oneToManyFlag) { errorNotification(errorObj, message, 'Sending XEC one to many'); } else { errorNotification(errorObj, message, 'Sending XEC'); } } async function send() { setFormData({ ...formData, }); let optionalOpReturnMsg = formData.opReturnMsg; if (isOneToManyXECSend) { // this is a one to many XEC send transactions // ensure multi-recipient input is not blank if (!formData.address) { return; } // Event("Category", "Action", "Label") // Track number of XEC send-to-many transactions Event('Send.js', 'SendToMany', selectedCurrency); passLoadingStatus(true); const { address, value } = formData; //convert each line from TextArea input let addressAndValueArray = address.split('\n'); try { // construct array of XEC->BCH addresses due to bch-api constraint let cleanAddressAndValueArray = - toLegacyArray(addressAndValueArray); + toLegacyCashArray(addressAndValueArray); const link = await sendXec( BCH, wallet, slpBalancesAndUtxos.nonSlpUtxos, currency.defaultFee, optionalOpReturnMsg, true, // indicate send mode is one to many cleanAddressAndValueArray, ); sendXecNotification(link); clearInputForms(); } catch (e) { handleSendXecError(e, isOneToManyXECSend); } } else { // standard one to one XEC send transaction if ( !formData.address || !formData.value || Number(formData.value) <= 0 ) { return; } // Event("Category", "Action", "Label") // Track number of BCHA send transactions and whether users // are sending BCHA or USD Event('Send.js', 'Send', selectedCurrency); passLoadingStatus(true); const { address, value } = formData; // Get the param-free address let cleanAddress = address.split('?')[0]; // Ensure address has bitcoincash: prefix and checksum - cleanAddress = toLegacy(cleanAddress); - - let hasValidCashPrefix; - - try { - hasValidCashPrefix = cleanAddress.startsWith( - currency.legacyPrefix + ':', - ); - } catch (err) { - hasValidCashPrefix = false; - console.log(`toLegacy() returned an error:`, cleanAddress); - } - - if (!hasValidCashPrefix) { - // set loading to false and set address validation to false - // Now that the no-prefix case is handled, this happens when user tries to send - // BCHA to an SLPA address - passLoadingStatus(false); - setSendBchAddressError( - `Destination is not a valid ${currency.ticker} address`, - ); - return; - } + cleanAddress = toLegacyCash(cleanAddress); // Calculate the amount in BCH let bchValue = value; if (selectedCurrency !== 'XEC') { bchValue = fiatToCrypto(value, fiatPrice); } try { const link = await sendXec( BCH, wallet, slpBalancesAndUtxos.nonSlpUtxos, currency.defaultFee, optionalOpReturnMsg, false, // sendToMany boolean flag null, // address array not applicable for one to many tx cleanAddress, bchValue, ); sendXecNotification(link); clearInputForms(); } catch (e) { handleSendXecError(e, isOneToManyXECSend); } } } const handleAddressChange = e => { const { value, name } = e.target; let error = false; let addressString = value; - // parse address - const addressInfo = parseAddress(BCH, addressString); + // validate address + const isValid = isValidXecAddress(addressString); + + // parse address for parameters + const addressInfo = parseAddressForParams(addressString); /* Model addressInfo = { address: '', - isValid: false, queryString: '', amount: null, }; */ - const { address, isValid, queryString, amount } = addressInfo; + const { address, queryString, amount } = addressInfo; // If query string, // Show an alert that only amount and currency.ticker are supported setQueryStringText(queryString); // Is this valid address? if (!isValid) { error = `Invalid ${currency.ticker} address`; // If valid address but token format - if (isValidTokenPrefix(address)) { - error = `Token addresses are not supported for ${currency.ticker} sends`; + if (isValidEtokenAddress(address)) { + error = `eToken addresses are not supported for ${currency.ticker} sends`; } } setSendBchAddressError(error); // Set amount if it's in the query string if (amount !== null) { // Set currency to BCHA setSelectedCurrency(currency.ticker); // Use this object to mimic user input and get validation for the value let amountObj = { target: { name: 'value', value: amount, }, }; handleBchAmountChange(amountObj); setFormData({ ...formData, value: amount, }); } // Set address field to user input setFormData(p => ({ ...p, [name]: value, })); }; const handleMultiAddressChange = e => { const { value, name } = e.target; let error; if (!value) { - error = 'recipient input must not be blank'; + error = 'Input must not be blank'; + setSendBchAddressError(error); + return setFormData(p => ({ + ...p, + [name]: value, + })); } //convert each line from the