diff --git a/web/cashtab/extension/public/manifest.json b/web/cashtab/extension/public/manifest.json --- a/web/cashtab/extension/public/manifest.json +++ b/web/cashtab/extension/public/manifest.json @@ -3,7 +3,7 @@ "name": "Cashtab", "description": "A browser-integrated BCHA wallet from Bitcoin ABC", - "version": "0.0.6", + "version": "0.0.7", "content_scripts": [ { "matches": ["file://*/*", "http://*/*", "https://*/*"], diff --git a/web/cashtab/package-lock.json b/web/cashtab/package-lock.json --- a/web/cashtab/package-lock.json +++ b/web/cashtab/package-lock.json @@ -12,6 +12,7 @@ "@zxing/library": "0.8.0", "antd": "^4.9.3", "bignumber.js": "^9.0.0", + "cashaddrjs": "^0.3.12", "dotenv": "^8.2.0", "dotenv-expand": "^5.1.0", "ethereum-blockies-base64": "^1.0.2", @@ -2459,12 +2460,13 @@ } }, "node_modules/@npmcli/move-file": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", - "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.0.tgz", + "integrity": "sha512-Iv2iq0JuyYjKeFkSR4LPaCdDZwlGK9X2cP/01nJcp3yMJ1FjNd9vpiEYvLUgzBxKPg2SFmaOhizoQsPc0LWeOQ==", "dev": true, "dependencies": { - "mkdirp": "^1.0.4" + "mkdirp": "^1.0.4", + "rimraf": "^2.7.1" }, "engines": { "node": ">=10" @@ -2482,6 +2484,18 @@ "node": ">=10" } }, + "node_modules/@npmcli/move-file/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/@psf/bch-js": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-3.11.0.tgz", @@ -24963,9 +24977,9 @@ } }, "node_modules/rc-field-form": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.17.3.tgz", - "integrity": "sha512-EocLncL7uDkxAGywqbtDXe6r8xbru9Yz94JHY7X6XsIdc8sAIGzafMYFaX0hHuwBGbvo7mv7L74cGCuD7xK5Fw==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.17.4.tgz", + "integrity": "sha512-QI9fe0F9YAmEX946lQpxTs6Qc/FwaLeakWquiBNEmhtqurj/qDdrv+eLb4TfnHTjkdyxU3G7p901WEuuBrrdkA==", "dependencies": { "@babel/runtime": "^7.8.4", "async-validator": "^3.0.3", @@ -24975,7 +24989,8 @@ "node": ">=8.x" }, "peerDependencies": { - "react": ">= 16.9.0" + "react": ">= 16.9.0", + "react-dom": ">= 16.9.0" } }, "node_modules/rc-image": { @@ -25308,12 +25323,16 @@ } }, "node_modules/rc-tooltip": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-5.0.1.tgz", - "integrity": "sha512-3AnxhUS0j74xAV3khrKw8o6rg+Ima3nw09DJBezMPnX3ImQUAnayWsPSlN1mEnihjA43rcFkGM1emiKE+CXyMQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-5.0.2.tgz", + "integrity": "sha512-A4FejSG56PzYtSNUU4H1pVzfhtkV/+qMT2clK0CsSj+9mbc4USEtpWeX6A/jjVL+goBOMKj8qlH7BCZmZWh/Nw==", "dependencies": { "@babel/runtime": "^7.11.2", "rc-trigger": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, "node_modules/rc-tree": { @@ -25352,9 +25371,9 @@ } }, "node_modules/rc-trigger": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.2.0.tgz", - "integrity": "sha512-fpC1ZkM/IgIIDfF6XHx3Hb2zXy9wvdI5eMh+6DdLygk6Z3HGmkri6ZCXg9a0wfF9AFuzlYTeBLS1uRASZRsnMQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.2.1.tgz", + "integrity": "sha512-XZilSlSDnb0L/R3Ff2xo9C0Fho2aBDoAn8u3coM60XdLqTCo24nsOh1bfAMm0uIB1qVjh5eqeyFqnBPmXi8pJg==", "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "^2.2.6", @@ -25364,6 +25383,10 @@ }, "engines": { "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, "node_modules/rc-upload": { @@ -34949,12 +34972,13 @@ } }, "@npmcli/move-file": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", - "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.0.tgz", + "integrity": "sha512-Iv2iq0JuyYjKeFkSR4LPaCdDZwlGK9X2cP/01nJcp3yMJ1FjNd9vpiEYvLUgzBxKPg2SFmaOhizoQsPc0LWeOQ==", "dev": true, "requires": { - "mkdirp": "^1.0.4" + "mkdirp": "^1.0.4", + "rimraf": "^2.7.1" }, "dependencies": { "mkdirp": { @@ -34962,6 +34986,15 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } } } }, @@ -52749,9 +52782,9 @@ } }, "rc-field-form": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.17.3.tgz", - "integrity": "sha512-EocLncL7uDkxAGywqbtDXe6r8xbru9Yz94JHY7X6XsIdc8sAIGzafMYFaX0hHuwBGbvo7mv7L74cGCuD7xK5Fw==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.17.4.tgz", + "integrity": "sha512-QI9fe0F9YAmEX946lQpxTs6Qc/FwaLeakWquiBNEmhtqurj/qDdrv+eLb4TfnHTjkdyxU3G7p901WEuuBrrdkA==", "requires": { "@babel/runtime": "^7.8.4", "async-validator": "^3.0.3", @@ -52983,9 +53016,9 @@ } }, "rc-tooltip": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-5.0.1.tgz", - "integrity": "sha512-3AnxhUS0j74xAV3khrKw8o6rg+Ima3nw09DJBezMPnX3ImQUAnayWsPSlN1mEnihjA43rcFkGM1emiKE+CXyMQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-5.0.2.tgz", + "integrity": "sha512-A4FejSG56PzYtSNUU4H1pVzfhtkV/+qMT2clK0CsSj+9mbc4USEtpWeX6A/jjVL+goBOMKj8qlH7BCZmZWh/Nw==", "requires": { "@babel/runtime": "^7.11.2", "rc-trigger": "^5.0.0" @@ -53016,9 +53049,9 @@ } }, "rc-trigger": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.2.0.tgz", - "integrity": "sha512-fpC1ZkM/IgIIDfF6XHx3Hb2zXy9wvdI5eMh+6DdLygk6Z3HGmkri6ZCXg9a0wfF9AFuzlYTeBLS1uRASZRsnMQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.2.1.tgz", + "integrity": "sha512-XZilSlSDnb0L/R3Ff2xo9C0Fho2aBDoAn8u3coM60XdLqTCo24nsOh1bfAMm0uIB1qVjh5eqeyFqnBPmXi8pJg==", "requires": { "@babel/runtime": "^7.11.2", "classnames": "^2.2.6", diff --git a/web/cashtab/package.json b/web/cashtab/package.json --- a/web/cashtab/package.json +++ b/web/cashtab/package.json @@ -9,6 +9,7 @@ "@zxing/library": "0.8.0", "antd": "^4.9.3", "bignumber.js": "^9.0.0", + "cashaddrjs": "^0.3.12", "dotenv": "^8.2.0", "dotenv-expand": "^5.1.0", "ethereum-blockies-base64": "^1.0.2", diff --git a/web/cashtab/src/components/Common/ScanQRCode.js b/web/cashtab/src/components/Common/ScanQRCode.js --- a/web/cashtab/src/components/Common/ScanQRCode.js +++ b/web/cashtab/src/components/Common/ScanQRCode.js @@ -3,7 +3,7 @@ import { QrcodeOutlined } from '@ant-design/icons'; import styled from 'styled-components'; import { BrowserQRCodeReader } from '@zxing/library'; -import { currency } from '@components/Common/Ticker.js'; +import { currency, isCash, isToken } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; const StyledScanQRCode = styled.span` @@ -54,10 +54,7 @@ let values = {}; // If what scanner reads from QR code begins with 'bitcoincash:' or 'simpleledger:' or their successor prefixes - if ( - content.split(currency.prefix).length > 1 || - content.split(currency.tokenPrefix).length > 1 - ) { + if (isCash(content) || isToken(content)) { type = 'address'; values = { address: content }; // Event("Category", "Action", "Label") diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js --- a/web/cashtab/src/components/Common/Ticker.js +++ b/web/cashtab/src/components/Common/Ticker.js @@ -1,11 +1,13 @@ import mainLogo from '@assets/12-bitcoin-cash-square-crop.svg'; import tokenLogo from '@assets/simple-ledger-protocol-logo.png'; +import cashaddr from 'cashaddrjs'; +import BigNumber from 'bignumber.js'; export const currency = { name: 'Bitcoin ABC', ticker: 'BCHA', logo: mainLogo, - prefix: 'bitcoincash:', + prefixes: ['bitcoincash:', 'ecash:'], coingeckoId: 'bitcoin-cash-abc-2', defaultFee: 5.01, blockExplorerUrl: 'https://explorer.bitcoinabc.org', @@ -13,7 +15,99 @@ tokenName: 'Bitcoin ABC SLP', tokenTicker: 'SLPA', tokenLogo: tokenLogo, - tokenPrefix: 'simpleledger:', + tokenPrefixes: ['simpleledger:', 'etoken:'], tokenIconsUrl: '', //https://tokens.bitcoin.com/32 for BCH SLP useBlockchainWs: false, }; + +export function isCash(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 + + for (let i = 0; i < currency.prefixes.length; i += 1) { + if (addressString.startsWith(currency.prefixes[i])) { + return true; + } + } + return false; +} + +export function isToken(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 (let i = 0; i < currency.tokenPrefixes.length; i += 1) { + if (addressString.startsWith(currency.tokenPrefixes[i])) { + return true; + } + } + return false; +} + +export function toLegacy(address) { + let legacyAddress; + try { + if (isCash(address)) { + const { type, hash } = cashaddr.decode(address); + legacyAddress = cashaddr.encode('bitcoincash', type, hash); + console.log(`legacyAddress`); + } else { + throw new Error('Address prefix is not in Ticker.prefixes array'); + } + } catch (err) { + return err; + } + return legacyAddress; +} + +export function parseAddress(BCH, 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); + } 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(1e8) + .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 new file mode 100644 --- /dev/null +++ b/web/cashtab/src/components/Common/__tests__/Ticker.test.js @@ -0,0 +1,81 @@ +import { ValidationError } from 'cashaddrjs'; +import { isCash, isToken, toLegacy } from '../Ticker'; + +test('Correctly validates cash address with bitcoincash: prefix', async () => { + const result = isCash( + 'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0', + ); + expect(result).toStrictEqual(true); +}); + +test('Correctly validates cash address with ecash: prefix', async () => { + const result = isCash('ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc'); + expect(result).toStrictEqual(true); +}); + +test('Correctly validates token address with simpleledger: prefix', async () => { + const result = isToken( + 'simpleledger:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', + ); + expect(result).toStrictEqual(true); +}); + +test('Correctly validates token address with etoken: prefix (prefix only, not checksum)', async () => { + const result = isToken('etoken:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm'); + expect(result).toStrictEqual(true); +}); + +test('Recognizes unaccepted token prefix (prefix only, not checksum)', async () => { + const result = isToken( + 'wtftoken:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', + ); + expect(result).toStrictEqual(false); +}); + +test('Knows that acceptable cash prefixes are not tokens', async () => { + const result = isToken('ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc'); + expect(result).toStrictEqual(false); +}); + +test('Address with unlisted prefix is invalid', async () => { + const result = isCash( + '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() returns a valid bitcoincash: prefix address unchanged', async () => { + const result = toLegacy( + 'bitcoincash: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 in Ticker.prefixes array'), + ); +}); diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { WalletContext } from '@utils/context'; -import { Form, notification, message, Spin, Modal } from 'antd'; +import { Form, notification, message, Spin, Modal, Alert } from 'antd'; import { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; import { Row, Col } from 'antd'; import Paragraph from 'antd/lib/typography/Paragraph'; @@ -15,7 +15,12 @@ import useBCH from '@hooks/useBCH'; import useWindowDimensions from '@hooks/useWindowDimensions'; import { isMobile, isIOS, isSafari } from 'react-device-detect'; -import { currency } from '@components/Common/Ticker.js'; +import { + currency, + isToken, + parseAddress, + toLegacy, +} from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; export const BalanceHeader = styled.div` p { @@ -84,6 +89,8 @@ address: filledAddress || '', }); const [loading, setLoading] = useState(false); + const [queryStringText, setQueryStringText] = useState(null); + const [sendBchAddressError, setSendBchAddressError] = useState(false); const [sendBchAmountError, setSendBchAmountError] = useState(false); const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker); @@ -124,7 +131,6 @@ !window.location.hash || window.location.hash === '#/send' ) { - console.log(`No tx info in URL`); return; } @@ -171,6 +177,20 @@ setLoading(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); + + // If there was an error converting the address + if (!cleanAddress.startsWith('bitcoincash:')) { + // return as above with other errors + console.log(`toLegacy() returned an error:`, cleanAddress); + // Note: the address must be valid to get to this point, so unsure if this can be produced + return; + } + // Calculate the amount in BCH let bchValue = value; @@ -184,7 +204,7 @@ wallet, slpBalancesAndUtxos.nonSlpUtxos, { - addresses: [filledAddress || address], + addresses: [filledAddress || cleanAddress], values: [bchValue], }, callbackTxId, @@ -235,10 +255,60 @@ } } - const handleChange = e => { + const handleAddressChange = e => { const { value, name } = e.target; + let error = false; + let addressString = value; + + // parse address + const addressInfo = parseAddress(BCH, addressString); + /* + Model + + addressInfo = + { + address: '', + isValid: false, + queryString: '', + amount: null, + }; + */ + + const { address, isValid, 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 = 'Address is not a valid cash address'; + // If valid address but token format + if (isToken(address)) { + error = `Token addresses are not supported for ${currency.ticker} sends`; + } + } + setSendBchAddressError(error); - setFormData(p => ({ ...p, [name]: value })); + // 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 handleSelectedCurrencyChange = e => { @@ -359,26 +429,26 @@ loadWithCameraOpen={scannerSupported} disabled={Boolean(filledAddress)} validateStatus={ - !formData.dirty && !formData.address - ? 'error' - : '' + sendBchAddressError ? 'error' : '' } help={ - !formData.dirty && !formData.address - ? `Should be a valid ${currency.ticker} address` + sendBchAddressError + ? sendBchAddressError : '' } onScan={result => - setFormData({ - ...formData, - address: result, + handleAddressChange({ + target: { + name: 'address', + value: result, + }, }) } inputProps={{ disabled: Boolean(filledAddress), placeholder: `${currency.ticker} Address`, name: 'address', - onChange: e => handleChange(e), + onChange: e => handleAddressChange(e), required: true, value: filledAddress || formData.address, }} @@ -401,6 +471,7 @@ }} selectProps={{ value: selectedCurrency, + disabled: queryStringText !== null, onChange: e => handleSelectedCurrencyChange(e), }} @@ -409,7 +480,8 @@
{!balances.totalBalance || apiError || - sendBchAmountError ? ( + sendBchAmountError || + sendBchAddressError ? ( Send ) : ( <> @@ -429,6 +501,12 @@ )}
+ {queryStringText && ( + + )} {apiError && ( <> diff --git a/web/cashtab/src/components/Send/SendToken.js b/web/cashtab/src/components/Send/SendToken.js --- a/web/cashtab/src/components/Send/SendToken.js +++ b/web/cashtab/src/components/Send/SendToken.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { WalletContext } from '@utils/context'; -import { Form, notification, message, Spin, Row, Col } from 'antd'; +import { Form, notification, message, Spin, Row, Col, Alert } from 'antd'; import Paragraph from 'antd/lib/typography/Paragraph'; import PrimaryButton, { SecondaryButton, @@ -18,7 +18,7 @@ import { Img } from 'react-image'; import makeBlockie from 'ethereum-blockies-base64'; import BigNumber from 'bignumber.js'; -import { currency } from '@components/Common/Ticker.js'; +import { currency, parseAddress, isToken } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; const SendToken = ({ tokenId }) => { @@ -26,6 +26,8 @@ WalletContext, ); const token = tokens.find(token => token.tokenId === tokenId); + const [queryStringText, setQueryStringText] = useState(null); + const [sendTokenAddressError, setSendTokenAddressError] = useState(false); const [sendTokenAmountError, setSendTokenAmountError] = useState(false); // Get device window width @@ -69,10 +71,13 @@ setLoading(true); const { address, value } = formData; + // Clear params from address + let cleanAddress = address.split('?')[0]; + try { const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, { tokenId: tokenId, - tokenReceiverAddress: address, + tokenReceiverAddress: cleanAddress, amount: value, }); @@ -141,10 +146,47 @@ setFormData(p => ({ ...p, [name]: value })); }; - const handleChange = e => { + const handleTokenAddressChange = e => { const { value, name } = e.target; + // validate for token address + // validate for parameters + // show warning that query strings are not supported - setFormData(p => ({ ...p, [name]: value })); + let error = false; + let addressString = value; + // parse address + const addressInfo = parseAddress(BCH, addressString); + /* + Model + + addressInfo = + { + address: '', + isValid: false, + queryString: '', + amount: null, + }; + */ + + const { address, isValid, queryString } = addressInfo; + + // If query string, + // Show an alert that only amount and currency.ticker are supported + setQueryStringText(queryString); + + // Is this valid address? + if (!isValid) { + error = 'Address is not valid'; + // If valid address but token format + } else if (!isToken(address)) { + error = `Cashtab only supports sending to ${currency.tokenPrefixes[0]} prefixed addresses`; + } + setSendTokenAddressError(error); + + setFormData(p => ({ + ...p, + [name]: value, + })); }; const onMax = async () => { @@ -197,25 +239,26 @@ - setFormData({ - ...formData, - address: result, + handleTokenAddressChange({ + target: { + name: 'address', + value: result, + }, }) } inputProps={{ placeholder: `${currency.tokenTicker} Address`, name: 'address', - onChange: e => handleChange(e), + onChange: e => + handleTokenAddressChange(e), required: true, value: formData.address, }} @@ -278,7 +321,9 @@ }} />
- {apiError || sendTokenAmountError ? ( + {apiError || + sendTokenAmountError || + sendTokenAddressError ? ( <> Send {token.info.tokenName} @@ -293,6 +338,12 @@ )}
+ {queryStringText && ( + + )} {apiError && (