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 @@ -26,6 +26,7 @@ "browser-image-compression": "^1.0.8", "camelcase": "^5.2.0", "case-sensitive-paths-webpack-plugin": "^2.3.0", + "cashaddrjs": "^0.3.12", "core-util-is": "^1.0.2", "crypto-js": "^3.1.9-1", "css-loader": "^3.6.0", diff --git a/web/cashtab/package.json b/web/cashtab/package.json --- a/web/cashtab/package.json +++ b/web/cashtab/package.json @@ -23,6 +23,7 @@ "browser-image-compression": "^1.0.8", "camelcase": "^5.2.0", "case-sensitive-paths-webpack-plugin": "^2.3.0", + "cashaddrjs": "^0.3.12", "core-util-is": "^1.0.2", "crypto-js": "^3.1.9-1", "css-loader": "^3.6.0", 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,12 @@ 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, + parseAddressParams, +} from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; const StyledScanQRCode = styled.span` @@ -54,12 +59,15 @@ 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 }; + + // Check for params and alternative prefixes + // eg mercurymessenger.io + // ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc?amount=500000 + + values = parseAddressParams(content); + // Event("Category", "Action", "Label") // Track number of successful QR code scans // BCH or slp? @@ -100,7 +108,7 @@ if (result.type === 'address') { // Hide the scanner setVisible(false); - onScan(result.values.address); + onScan(result.values); return teardownCodeReader(codeReader); } } catch (err) { 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,82 @@ 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) { + // 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 + let validCashAddress = false; + for (let i = 0; i < currency.prefixes.length; i += 1) { + if (addressString.split(currency.prefixes[i]).length > 1) { + validCashAddress = true; + } + } + return validCashAddress; +} + +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 + let validTokenAddress = false; + for (let i = 0; i < currency.tokenPrefixes.length; i += 1) { + if (addressString.split(currency.tokenPrefixes[i]).length > 1) { + validTokenAddress = true; + } + } + return validTokenAddress; +} + +export function ecashToCashAddr(ecashPrefixedAddress) { + // Convert an address with prefix and checksum for ecash: + // To an address in legacy bitcoincash: format + // Backend and validators currently support only legacy format + + const { prefix, type, hash } = cashaddr.decode(ecashPrefixedAddress); + // If prefix is `ecash:`, change to `bitcoincash:` + let newPrefix = prefix; + if (prefix === 'ecash') { + newPrefix = 'bitcoincash'; + } + const reformattedAddr = cashaddr.encode(newPrefix, type, hash); + + return reformattedAddr; +} + +export function parseAddressParams(addressString) { + // Check address string for params and return + // For now, only the `amount` param is supported by UI + // So only check for `amount` and return if present + const paramCheck = addressString.split('?'); + let cleanAddress = paramCheck[0]; + + // If it's an ecash: prefix, convert to bitcoincash: + if (cleanAddress.split('ecash:').length > 1) { + cleanAddress = ecashToCashAddr(cleanAddress); + } + + let queryString; + let amount = null; + + if (paramCheck.length > 1) { + queryString = paramCheck[1]; + + 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; + } + } + } + return { address: cleanAddress, amount: amount }; +} 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 @@ -371,7 +371,8 @@ onScan={result => setFormData({ ...formData, - address: result, + address: result.address, + value: result.amount, }) } inputProps={{ 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 @@ -209,7 +209,7 @@ onScan={result => setFormData({ ...formData, - address: result, + address: result.address, }) } inputProps={{