diff --git a/web/cashtab/src/components/Common/Atoms.js b/web/cashtab/src/components/Common/Atoms.js index 6a25ba1ed..3de4810ea 100644 --- a/web/cashtab/src/components/Common/Atoms.js +++ b/web/cashtab/src/components/Common/Atoms.js @@ -1,52 +1,63 @@ import styled from 'styled-components'; export const LoadingCtn = styled.div` width: 100%; display: flex; align-items: center; justify-content: center; height: 400px; flex-direction: column; svg { width: 50px; height: 50px; fill: ${props => props.theme.primary}; } `; export const BalanceHeader = styled.div` color: ${props => props.theme.wallet.text.primary}; width: 100%; font-size: 30px; font-weight: bold; @media (max-width: 768px) { font-size: 23px; } `; export const BalanceHeaderFiat = styled.div` color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 18px; margin-bottom: 20px; font-weight: bold; @media (max-width: 768px) { font-size: 16px; } `; export const ZeroBalanceHeader = styled.div` color: ${props => props.theme.wallet.text.primary}; width: 100%; font-size: 14px; margin-bottom: 5px; `; export const TokenParamLabel = styled.span` font-weight: bold; `; export const AlertMsg = styled.p` color: ${props => props.theme.forms.error}; `; + +export const ConvertAmount = styled.div` + color: ${props => props.theme.wallet.text.secondary}; + width: 100%; + font-size: 14px; + margin-bottom: 10px; + font-weight: bold; + @media (max-width: 768px) { + font-size: 12px; + } +`; diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index 3a8d4fdba..0ec5f9105 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,573 +1,528 @@ import React, { useState, useEffect } from 'react'; -import styled from 'styled-components'; import { WalletContext } from '@utils/context'; 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'; import PrimaryButton, { SecondaryButton, } from '@components/Common/PrimaryButton'; import { SendBchInput, FormItemWithQRCodeAddon, } from '@components/Common/EnhancedInputs'; import useBCH from '@hooks/useBCH'; import useWindowDimensions from '@hooks/useWindowDimensions'; import { isMobile, isIOS, isSafari } from 'react-device-detect'; import { currency, isValidTokenPrefix, parseAddress, toLegacy, } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; import { fiatToCrypto, shouldRejectAmountInput } from '@utils/validation'; import { formatBalance } from '@utils/cashMethods'; - -export const BalanceHeader = styled.div` - p { - color: ${props => props.theme.wallet.text.secondary} - width: 100%; - font-size: 14px; - margin-bottom: 0px; - } - - h3 { - color: ${props => props.theme.wallet.text.primary}; - width: 100%; - font-size: 26px; - font-weight: bold; - margin-bottom: 0px; - } -`; - -export const BalanceHeaderFiat = styled.div` - color: ${props => props.theme.wallet.text.secondary}; - width: 100%; - font-size: 18px; - margin-bottom: 20px; - font-weight: bold; - @media (max-width: 768px) { - font-size: 16px; - } -`; - -export const ZeroBalanceHeader = styled.div` - color: ${props => props.theme.wallet.text.secondary}; - width: 100%; - font-size: 14px; - margin-bottom: 20px; -`; - -const ConvertAmount = styled.div` - color: ${props => props.theme.wallet.text.secondary}; - width: 100%; - font-size: 14px; - margin-bottom: 10px; - font-weight: bold; - @media (max-width: 768px) { - font-size: 12px; - } -`; +import { + BalanceHeader, + BalanceHeaderFiat, + ZeroBalanceHeader, + ConvertAmount, +} from '@components/Common/Atoms'; // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest const SendBCH = ({ jestBCH, filledAddress, callbackTxId }) => { // 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 { wallet, fiatPrice, slpBalancesAndUtxos, apiError } = ContextValue; let balances; const paramsInWalletState = wallet.state ? Object.keys(wallet.state) : []; // If wallet.state includes balances and parsedTxHistory params, use these // These are saved in indexedDb in the latest version of the app, hence accessible more quickly if (paramsInWalletState.includes('balances')) { balances = wallet.state.balances; } else { // If balances and parsedTxHistory are not in the wallet.state object, load them from Context // This is how the app used to work balances = ContextValue.balances; } // 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({ dirty: true, value: '', 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); // 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 showModal = () => { setIsModalVisible(true); }; const handleOk = () => { setIsModalVisible(false); submit(); }; const handleCancel = () => { setIsModalVisible(false); }; const { getBCH, getRestUrl, sendBch, calcFee } = 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(() => { setLoading(false); }, [balances.totalBalance]); useEffect(() => { // Manually parse for txInfo object on page load when Send.js is loaded with a query string // 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, }); } } async function submit() { setFormData({ ...formData, dirty: false, }); 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); 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); 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 setLoading(false); setSendBchAddressError( `Destination is not a valid ${currency.ticker} address`, ); return; } // Calculate the amount in BCH let bchValue = value; if (selectedCurrency === 'USD') { bchValue = fiatToCrypto(value, fiatPrice); } try { const link = await sendBch( BCH, wallet, slpBalancesAndUtxos.nonSlpUtxos, filledAddress || cleanAddress, bchValue, currency.defaultFee, callbackTxId, ); notification.success({ message: 'Success', description: ( Transaction successful. Click or tap here for more details ), duration: 5, }); } catch (e) { // Set loading to false here as well, as balance may not change depending on where error occured in try loop setLoading(false); let message; if (!e.error && !e.message) { message = `Transaction failed: no response from ${getRestUrl()}.`; } else if ( /Could not communicate with full node or other external service/.test( e.error, ) ) { message = 'Could not communicate with API. Please try again.'; } else if ( e.error && e.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 = e.message || e.error || JSON.stringify(e); } notification.error({ message: 'Error', description: message, duration: 5, }); console.error(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 (isValidTokenPrefix(address)) { error = `Token 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 handleSelectedCurrencyChange = e => { setSelectedCurrency(e); // Clear input field to prevent accidentally sending 1 BCH instead of 1 USD setFormData(p => ({ ...p, value: '', })); }; const handleBchAmountChange = e => { const { value, name } = e.target; let bchValue = value; const error = shouldRejectAmountInput( bchValue, selectedCurrency, fiatPrice, balances.totalBalance, ); setSendBchAmountError(error); setFormData(p => ({ ...p, [name]: value, })); }; const onMax = async () => { // Clear amt error setSendBchAmountError(false); // Set currency to BCH setSelectedCurrency(currency.ticker); try { const txFeeSats = calcFee(BCH, slpBalancesAndUtxos.nonSlpUtxos); const txFeeBch = txFeeSats / 10 ** currency.cashDecimals; let value = balances.totalBalance - txFeeBch >= 0 ? (balances.totalBalance - txFeeBch).toFixed( currency.cashDecimals, ) : 0; setFormData({ ...formData, value, }); } catch (err) { console.log(`Error in onMax:`); console.log(err); message.error( 'Unable to calculate the max value due to network errors', ); } }; // Display price in USD below input field for send amount, if it can be calculated let fiatPriceString = ''; if (fiatPrice !== null && !isNaN(formData.value)) { if (selectedCurrency === currency.ticker) { fiatPriceString = `$ ${(fiatPrice * Number(formData.value)).toFixed( 2, )} USD`; } else { fiatPriceString = `${ formData.value ? fiatToCrypto(formData.value, fiatPrice) : '0' } ${currency.ticker}`; } } return ( <>

Are you sure you want to send {formData.value}{' '} {currency.ticker} to {formData.address}?

{!balances.totalBalance ? ( You currently have 0 {currency.ticker}
Deposit some funds to use this feature
) : ( <> -

Available balance

-

- {formatBalance(balances.totalBalance)}{' '} - {currency.ticker} -

+ {formatBalance(balances.totalBalance)} {currency.ticker}
{fiatPrice !== null && ( ${(balances.totalBalance * fiatPrice).toFixed(2)}{' '} USD )} )}
handleAddressChange({ target: { name: 'address', value: result, }, }) } inputProps={{ disabled: Boolean(filledAddress), placeholder: `${currency.ticker} Address`, name: 'address', onChange: e => handleAddressChange(e), required: true, value: filledAddress || formData.address, }} > handleBchAmountChange(e), required: true, value: formData.value, }} selectProps={{ value: selectedCurrency, disabled: queryStringText !== null, onChange: e => handleSelectedCurrencyChange(e), }} > = {fiatPriceString}
{!balances.totalBalance || apiError || sendBchAmountError || sendBchAddressError ? ( Send ) : ( <> {txInfoFromUrl ? ( showModal()} > Send ) : ( submit()} > Send )} )}
{queryStringText && ( )} {apiError && ( <>

An error occured on our end. Reconnecting...

)}
); }; export default SendBCH; diff --git a/web/cashtab/src/components/Send/SendToken.js b/web/cashtab/src/components/Send/SendToken.js index 3bf6eff9b..197701de3 100644 --- a/web/cashtab/src/components/Send/SendToken.js +++ b/web/cashtab/src/components/Send/SendToken.js @@ -1,398 +1,394 @@ import React, { useState, useEffect } from 'react'; import { WalletContext } from '@utils/context'; import { Form, notification, message, Spin, Row, Col, Alert } from 'antd'; import Paragraph from 'antd/lib/typography/Paragraph'; import PrimaryButton, { SecondaryButton, } from '@components/Common/PrimaryButton'; import { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; import { FormItemWithMaxAddon, FormItemWithQRCodeAddon, } from '@components/Common/EnhancedInputs'; import useBCH from '@hooks/useBCH'; -import { BalanceHeader } from './Send'; +import { BalanceHeader } from '@components/Common/Atoms'; import { Redirect } from 'react-router-dom'; import useWindowDimensions from '@hooks/useWindowDimensions'; import { isMobile, isIOS, isSafari } from 'react-device-detect'; import { Img } from 'react-image'; import makeBlockie from 'ethereum-blockies-base64'; import BigNumber from 'bignumber.js'; import { currency, parseAddress, isValidTokenPrefix, } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; import { formatBalance, isValidStoredWallet } from '@utils/cashMethods'; const SendToken = ({ tokenId, jestBCH }) => { const { wallet, tokens, slpBalancesAndUtxos, apiError } = React.useContext( WalletContext, ); // If this wallet has migrated to latest storage structure, get token info from there // If not, use the tokens object (unless it's undefined, in which case use an empty array) const liveTokenState = isValidStoredWallet(wallet) && wallet.state.tokens ? wallet.state.tokens : tokens ? tokens : []; const token = liveTokenState.find(token => token.tokenId === tokenId); const [queryStringText, setQueryStringText] = useState(null); const [sendTokenAddressError, setSendTokenAddressError] = useState(false); const [sendTokenAmountError, setSendTokenAmountError] = 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({ dirty: true, value: '', address: '', }); const [loading, setLoading] = useState(false); const { getBCH, getRestUrl, sendToken } = useBCH(); // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); const BCH = jestBCH ? jestBCH : getBCH(); // Keep this function around for re-enabling later // eslint-disable-next-line no-unused-vars async function submit() { setFormData({ ...formData, dirty: false, }); if ( !formData.address || !formData.value || Number(formData.value <= 0) || sendTokenAmountError ) { return; } // Event("Category", "Action", "Label") // Track number of SLPA send transactions and // SLPA token IDs Event('SendToken.js', 'Send', tokenId); 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: cleanAddress, amount: value, }); notification.success({ message: 'Success', description: ( Transaction successful. Click or tap here for more details ), duration: 5, }); } catch (e) { setLoading(false); let message; if (!e.error && !e.message) { message = `Transaction failed: no response from ${getRestUrl()}.`; } else if ( /Could not communicate with full node or other external service/.test( e.error, ) ) { message = 'Could not communicate with API. Please try again.'; } else { message = e.message || e.error || JSON.stringify(e); } console.log(e); notification.error({ message: 'Error', description: message, duration: 3, }); console.error(e); } } const handleSlpAmountChange = e => { let error = false; const { value, name } = e.target; // test if exceeds balance using BigNumber let isGreaterThanBalance = false; if (!isNaN(value)) { const bigValue = new BigNumber(value); // Returns 1 if greater, -1 if less, 0 if the same, null if n/a isGreaterThanBalance = bigValue.comparedTo(token.balance); } // Validate value for > 0 if (isNaN(value)) { error = 'Amount must be a number'; } else if (value <= 0) { error = 'Amount must be greater than 0'; } else if (token && token.balance && isGreaterThanBalance === 1) { error = `Amount cannot exceed your ${token.info.tokenTicker} balance of ${token.balance}`; } else if (!isNaN(value) && value.toString().includes('.')) { if (value.toString().split('.')[1].length > token.info.decimals) { error = `This token only supports ${token.info.decimals} decimal places`; } } setSendTokenAmountError(error); setFormData(p => ({ ...p, [name]: value, })); }; const handleTokenAddressChange = e => { const { value, name } = e.target; // validate for token address // validate for parameters // show warning that query strings are not supported 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 (!isValidTokenPrefix(address)) { error = `Cashtab only supports sending to ${currency.tokenPrefixes[0]} prefixed addresses`; } setSendTokenAddressError(error); setFormData(p => ({ ...p, [name]: value, })); }; const onMax = async () => { // Clear this error before updating field setSendTokenAmountError(false); try { let value = token.balance; setFormData({ ...formData, value, }); } catch (err) { console.log(`Error in onMax:`); console.log(err); message.error( 'Unable to calculate the max value due to network errors', ); } }; useEffect(() => { // 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 setLoading(false); }, [token]); return ( <> {!token && } {token && ( <> -

Available balance

-

- {formatBalance(token.balance)}{' '} - {token.info.tokenTicker} -

+ {formatBalance(token.balance)} {token.info.tokenTicker}
handleTokenAddressChange({ target: { name: 'address', value: result, }, }) } inputProps={{ placeholder: `${currency.tokenTicker} Address`, name: 'address', onChange: e => handleTokenAddressChange(e), required: true, value: formData.address, }} /> } /> ) : ( {`identicon ), suffix: token.info.tokenTicker, onChange: e => handleSlpAmountChange(e), required: true, value: formData.value, }} />
{apiError || sendTokenAmountError || sendTokenAddressError ? ( <> Send {token.info.tokenName} {apiError && } ) : ( submit()} > Send {token.info.tokenName} )}
{queryStringText && ( )} {apiError && (

An error occured on our end. Reconnecting...

)}
)} ); }; export default SendToken; diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap index 85ce3afa0..74f48a0fc 100644 --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap @@ -1,2141 +1,2121 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [
-

- Available balance -

-

- 0.06047469 - - BCHA -

+ 0.06047469 + + BCHA
,
$ NaN USD
,
BCHA
max
= $ NaN USD
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [
-

- Available balance -

-

- 0.06047469 - - BCHA -

+ 0.06047469 + + BCHA
,
$ NaN USD
,
BCHA
max
= $ NaN USD
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
-

- Available balance -

-

- 0.06047469 - - BCHA -

+ 0.06047469 + + BCHA
,
$ NaN USD
,
BCHA
max
= $ NaN USD
, ] `; exports[`Wallet with BCH balances and tokens and state field, but no params in state 1`] = ` Array [
-

- Available balance -

-

- 0.06047469 - - BCHA -

+ 0.06047469 + + BCHA
,
$ NaN USD
,
BCHA
max
= $ NaN USD
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [
You currently have 0 BCHA
Deposit some funds to use this feature
,
BCHA
max
= $ NaN USD
, ] `; exports[`Without wallet defined 1`] = ` Array [
You currently have 0 BCHA
Deposit some funds to use this feature
,
BCHA
max
= $ NaN USD
, ] `; diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap index ff46a88d4..d974bbcf8 100644 --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap @@ -1,789 +1,774 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances and tokens 1`] = ` Array [
-

- Available balance -

-

- 6.001 - - TBS -

+ 6.001 + + TBS
,
identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba TBS max
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
-

- Available balance -

-

- 6.001 - - TBS -

+ 6.001 + + TBS
,
identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba TBS max
, ] `; exports[`Wallet with BCH balances and tokens and state field, but no params in state 1`] = ` Array [
-

- Available balance -

-

- 6.001 - - TBS -

+ 6.001 + + TBS
,
identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba TBS max
, ] `; exports[`Without wallet defined 1`] = `null`; diff --git a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap index cfe1dac86..4ad218d42 100644 --- a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap +++ b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap @@ -1,609 +1,609 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [
0.06047469 BCHA
,
$ NaN USD
,
Copied
qzagy4 7mvh6qxkvcn3acjnz73rkhkc6y7cpt zgcqy6
,
BCHA
SLPA
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [
0.06047469 BCHA
,
$ NaN USD
,
Copied
qzagy4 7mvh6qxkvcn3acjnz73rkhkc6y7cpt zgcqy6
,
BCHA
SLPA
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
0.06047469 BCHA
,
$ NaN USD
,
Copied
qzagy4 7mvh6qxkvcn3acjnz73rkhkc6y7cpt zgcqy6
,
BCHA
SLPA
, ] `; exports[`Wallet with BCH balances and tokens and state field, but no params in state 1`] = ` Array [
0.06047469 BCHA
,
$ NaN USD
,
Copied
qzagy4 7mvh6qxkvcn3acjnz73rkhkc6y7cpt zgcqy6
,
BCHA
SLPA
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive BCHA payments, or load it up with BCHA to send to others
,
0 BCHA
,
Copied
qzagy4 7mvh6qxkvcn3acjnz73rkhkc6y7cpt zgcqy6
,
BCHA
SLPA
, ] `; exports[`Without wallet defined 1`] = ` Array [

Welcome to Cashtab!

,

Cashtab is an open source, non-custodial web wallet for Bitcoin ABC .

Want to learn more? Check out the Cashtab documentation.

, , , ] `;