diff --git a/web/cashtab/src/components/Common/EnhancedInputs.js b/web/cashtab/src/components/Common/EnhancedInputs.js
index e71146f3d..48d3cff7b 100644
--- a/web/cashtab/src/components/Common/EnhancedInputs.js
+++ b/web/cashtab/src/components/Common/EnhancedInputs.js
@@ -1,342 +1,342 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { Form, Input, Select } from 'antd';
import {
ThemedDollarOutlined,
ThemedWalletOutlined,
} from '@components/Common/CustomIcons';
import styled, { css } from 'styled-components';
import ScanQRCode from './ScanQRCode';
import useBCH from '@hooks/useBCH';
import { currency } from '@components/Common/Ticker.js';
export const AntdFormCss = css`
.ant-input-group-addon {
background-color: ${props =>
props.theme.forms.addonBackground} !important;
border: 1px solid ${props => props.theme.forms.border};
color: ${props => props.theme.forms.addonForeground} !important;
}
input.ant-input,
.ant-select-selection {
background-color: ${props =>
props.theme.forms.selectionBackground} !important;
box-shadow: none !important;
border-radius: 4px;
font-weight: bold;
color: ${props => props.theme.forms.text};
opacity: 1;
height: 50px;
}
textarea.ant-input,
.ant-select-selection {
background-color: ${props =>
props.theme.forms.selectionBackground} !important;
box-shadow: none !important;
border-radius: 4px;
font-weight: bold;
color: ${props => props.theme.forms.text};
opacity: 1;
height: 50px;
min-height: 100px;
}
.ant-input-affix-wrapper {
background-color: ${props => props.theme.forms.selectionBackground};
border: 1px solid ${props => props.theme.wallet.borders.color} !important;
}
.ant-select-selector {
height: 60px !important;
border: 1px solid ${props => props.theme.wallet.borders.color} !important;
}
.ant-form-item-has-error
> div
> div.ant-form-item-control-input
> div
> span
> span
> span.ant-input-affix-wrapper {
background-color: ${props => props.theme.forms.selectionBackground};
border-color: ${props => props.theme.forms.error} !important;
}
.ant-form-item-has-error .ant-input,
.ant-form-item-has-error .ant-input-affix-wrapper,
.ant-form-item-has-error .ant-input:hover,
.ant-form-item-has-error .ant-input-affix-wrapper:hover {
background-color: ${props => props.theme.forms.selectionBackground};
border-color: ${props => props.theme.forms.error} !important;
}
.ant-form-item-has-error
.ant-select:not(.ant-select-disabled):not(.ant-select-customize-input)
.ant-select-selector {
background-color: ${props => props.theme.forms.selectionBackground};
border-color: ${props => props.theme.forms.error} !important;
}
.ant-select-single .ant-select-selector .ant-select-selection-item,
.ant-select-single .ant-select-selector .ant-select-selection-placeholder {
line-height: 60px;
text-align: left;
color: ${props => props.theme.forms.text};
font-weight: bold;
}
.ant-form-item-has-error .ant-input-group-addon {
color: ${props => props.theme.forms.error} !important;
border-color: ${props => props.theme.forms.error} !important;
}
.ant-form-item-explain.ant-form-item-explain-error {
color: ${props => props.theme.forms.error} !important;
}
`;
export const AntdFormWrapper = styled.div`
${AntdFormCss}
`;
export const InputAddonText = styled.span`
width: 100%;
height: 100%;
display: block;
${props =>
props.disabled
? `
cursor: not-allowed;
`
: `cursor: pointer;`}
`;
export const InputNumberAddonText = styled.span`
background-color: ${props => props.theme.forms.addonBackground} !important;
border: 1px solid ${props => props.theme.forms.border};
color: ${props => props.theme.forms.addonForeground} !important;
height: 50px;
line-height: 47px;
* {
color: ${props => props.theme.forms.addonForeground} !important;
}
${props =>
props.disabled
? `
cursor: not-allowed;
`
: `cursor: pointer;`}
`;
export const SendBchInput = ({
onMax,
inputProps,
selectProps,
activeFiatCode,
...otherProps
}) => {
const { Option } = Select;
const currencies = [
{
value: currency.ticker,
label: currency.ticker,
},
{
value: activeFiatCode ? activeFiatCode : 'USD',
label: activeFiatCode ? activeFiatCode : 'USD',
},
];
const currencyOptions = currencies.map(currency => {
return (
);
});
const CurrencySelect = (
);
return (
) : (
)
}
{...inputProps}
/>
{CurrencySelect}
max
);
};
SendBchInput.propTypes = {
onMax: PropTypes.func,
inputProps: PropTypes.object,
selectProps: PropTypes.object,
activeFiatCode: PropTypes.string,
};
export const FormItemWithMaxAddon = ({ onMax, inputProps, ...otherProps }) => {
return (
}
addonAfter={
max
}
{...inputProps}
/>
);
};
FormItemWithMaxAddon.propTypes = {
onMax: PropTypes.func,
inputProps: PropTypes.object,
};
// loadWithCameraOpen prop: if true, load page with camera scanning open
-export const FormItemWithQRCodeAddon = ({
+export const DestinationAddressSingle = ({
onScan,
loadWithCameraOpen,
inputProps,
...otherProps
}) => {
return (
}
autoComplete="off"
addonAfter={
}
{...inputProps}
/>
);
};
-FormItemWithQRCodeAddon.propTypes = {
+DestinationAddressSingle.propTypes = {
onScan: PropTypes.func,
loadWithCameraOpen: PropTypes.bool,
inputProps: PropTypes.object,
};
export const CurrencySelectDropdown = selectProps => {
const { Option } = Select;
// Build select dropdown from currency.fiatCurrencies
const currencyMenuOptions = [];
const currencyKeys = Object.keys(currency.fiatCurrencies);
for (let i = 0; i < currencyKeys.length; i += 1) {
const currencyMenuOption = {};
currencyMenuOption.value =
currency.fiatCurrencies[currencyKeys[i]].slug;
currencyMenuOption.label = `${
currency.fiatCurrencies[currencyKeys[i]].name
} (${currency.fiatCurrencies[currencyKeys[i]].symbol})`;
currencyMenuOptions.push(currencyMenuOption);
}
const currencyOptions = currencyMenuOptions.map(currencyMenuOption => {
return (
);
});
return (
);
};
export const AddressValidators = () => {
const { BCH } = useBCH();
return {
safelyDetectAddressFormat: value => {
try {
return BCH.Address.detectAddressFormat(value);
} catch (error) {
return null;
}
},
isSLPAddress: value =>
AddressValidators.safelyDetectAddressFormat(value) === 'slpaddr',
isBCHAddress: value =>
AddressValidators.safelyDetectAddressFormat(value) === 'cashaddr',
isLegacyAddress: value =>
AddressValidators.safelyDetectAddressFormat(value) === 'legacy',
}();
};
diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js
index e18154061..9e5aeca80 100644
--- a/web/cashtab/src/components/Send/Send.js
+++ b/web/cashtab/src/components/Send/Send.js
@@ -1,768 +1,768 @@
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,
- FormItemWithQRCodeAddon,
+ DestinationAddressSingle,
} from '@components/Common/EnhancedInputs';
import {
StyledCollapse,
AdvancedCollapse,
} from '@components/Common/StyledCollapse';
import {
Form,
message,
Modal,
Alert,
Collapse,
Input,
notification,
} from 'antd';
const { Panel } = Collapse;
const { TextArea } = Input;
import { Row, Col } from 'antd';
import Paragraph from 'antd/lib/typography/Paragraph';
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,
} from '@components/Common/Ticker.js';
import { Event } from '@utils/GoogleAnalytics';
import { fiatToCrypto, shouldRejectAmountInput } 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 ApiError from '@components/Common/ApiError';
import { formatFiatBalance } from '@utils/validation';
import WalletLabel from '@components/Common/WalletLabel.js';
import Wallet from '@components/Wallet/Wallet';
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 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);
// 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 [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 showModal = () => {
setIsModalVisible(true);
};
const handleOk = () => {
setIsModalVisible(false);
submit();
};
const handleCancel = () => {
setIsModalVisible(false);
};
const { getBCH, getRestUrl, sendBch, 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,
});
}
}
async function submit() {
setFormData({
...formData,
dirty: false,
});
if (
!formData.address ||
!formData.value ||
Number(formData.value) <= 0
) {
return;
}
let optionalOpReturnMsg = formData.opReturnMsg;
// 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;
}
// Calculate the amount in BCH
let bchValue = value;
if (selectedCurrency !== 'XEC') {
bchValue = fiatToCrypto(value, fiatPrice);
}
try {
const link = await sendBch(
BCH,
wallet,
slpBalancesAndUtxos.nonSlpUtxos,
cleanAddress,
bchValue,
currency.defaultFee,
optionalOpReturnMsg,
);
sendXecNotification(link);
} catch (e) {
// 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 (!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);
}
errorNotification(e, message, 'Sending XEC');
}
}
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 = `Invalid ${currency.ticker} 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 handleOpReturnMsgChange = e => {
const { value, name } = e.target;
setFormData(p => ({
...p,
[name]: 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 handleSignMsgChange = e => {
const { value } = e.target;
// validation
if (value && value.length && value.length < 150) {
setMsgToSign(value);
setSignMessageIsValid(true);
} else {
setSignMessageIsValid(false);
}
};
const signMessageByPk = async () => {
try {
const messageSignature = await signPkMessage(
BCH,
wallet.Path1899.fundingWif,
msgToSign,
);
setMessageSignature(messageSignature);
messageSignedNotification(messageSignature);
} catch (err) {
let message;
if (!err.error && !err.message) {
message = err.message || err.error || JSON.stringify(err);
}
errorNotification(err, message, 'Message Signing Error');
throw err;
}
// Hide the modal
setShowConfirmMsgToSign(false);
setSigCopySuccess('');
};
const handleOnSigCopy = () => {
if (messageSignature != '') {
setSigCopySuccess('Signature copied to clipboard');
}
};
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) {
// calculate conversion to fiatPrice
fiatPriceString = `${(fiatPrice * Number(formData.value)).toFixed(
2,
)}`;
// formats to fiat locale style
fiatPriceString = formatFiatBalance(Number(fiatPriceString));
// insert symbol and currency before/after the locale formatted fiat balance
fiatPriceString = `${
cashtabSettings
? `${
currency.fiatCurrencies[cashtabSettings.fiatCurrency]
.symbol
} `
: '$ '
} ${fiatPriceString} ${
cashtabSettings && cashtabSettings.fiatCurrency
? cashtabSettings.fiatCurrency.toUpperCase()
: 'USD'
}`;
} else {
fiatPriceString = `${
formData.value
? formatFiatBalance(
Number(fiatToCrypto(formData.value, fiatPrice)),
)
: formatFiatBalance(0)
} ${currency.ticker}`;
}
}
const priceApiError = fiatPrice === null && selectedCurrency !== 'XEC';
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
) : (
<>
{fiatPrice !== null && (
)}
>
)}
Signatures setShowConfirmMsgToSign(false)}
>
Message: {msgToSign}
Message:Address: setShowConfirmMsgToSign(true)}
disabled={!signMessageIsValid}
>
Sign Message
Signature:
{sigCopySuccess}
>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in Send.test.js
status => {console.log(status)} is an arbitrary stub function
*/
SendBCH.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
SendBCH.propTypes = {
jestBCH: PropTypes.object,
passLoadingStatus: PropTypes.func,
};
export default SendBCH;
diff --git a/web/cashtab/src/components/Send/SendToken.js b/web/cashtab/src/components/Send/SendToken.js
index 3504e708b..5a4eb1afb 100644
--- a/web/cashtab/src/components/Send/SendToken.js
+++ b/web/cashtab/src/components/Send/SendToken.js
@@ -1,433 +1,433 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { WalletContext } from '@utils/context';
import { Form, message, Row, Col, Alert, Descriptions } from 'antd';
import TokenIconAlert from '@components/Common/Alerts.js';
import PrimaryButton, {
SecondaryButton,
} from '@components/Common/PrimaryButton';
import {
FormItemWithMaxAddon,
- FormItemWithQRCodeAddon,
+ DestinationAddressSingle,
} from '@components/Common/EnhancedInputs';
import useBCH from '@hooks/useBCH';
import BalanceHeader from '@components/Common/BalanceHeader';
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 {
getWalletState,
convertEtokenToSimpleledger,
} from '@utils/cashMethods';
import ApiError from '@components/Common/ApiError';
import {
sendTokenNotification,
errorNotification,
} from '@components/Common/Notifications';
const SendToken = ({ tokenId, jestBCH, passLoadingStatus }) => {
const { wallet, apiError } = React.useContext(WalletContext);
const walletState = getWalletState(wallet);
const { tokens, slpBalancesAndUtxos } = walletState;
const token = tokens.find(token => token.tokenId === tokenId);
const [tokenStats, setTokenStats] = useState(null);
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 { getBCH, getRestUrl, sendToken, getTokenStats } = useBCH();
// jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
const BCH = jestBCH ? jestBCH : getBCH();
// Fetch token stats if you do not have them and API did not return an error
if (tokenStats === null) {
getTokenStats(BCH, tokenId).then(
result => {
setTokenStats(result);
},
err => {
console.log(`Error getting token stats: ${err}`);
},
);
}
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);
passLoadingStatus(true);
const { address, value } = formData;
// Clear params from address
let cleanAddress = address.split('?')[0];
// Convert to simpleledger prefix if etoken
cleanAddress = convertEtokenToSimpleledger(cleanAddress);
try {
const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, {
tokenId: tokenId,
tokenReceiverAddress: cleanAddress,
amount: value,
});
sendTokenNotification(link);
} catch (e) {
passLoadingStatus(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);
}
errorNotification(e, message, 'Sending eToken');
}
}
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;
const addressInfo = parseAddress(BCH, addressString, true);
/*
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 a valid etoken: address';
// 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
passLoadingStatus(false);
}, [token]);
return (
<>
{!token && }
{token && (
<>
{tokenStats !== null && (
{token.info.decimals}
{token.tokenId}
{tokenStats && (
<>
{tokenStats.documentUri}
{tokenStats.timestampUnix !==
null
? new Date(
tokenStats.timestampUnix *
1000,
).toLocaleDateString()
: 'Just now (Genesis tx confirming)'}
{tokenStats.containsBaton
? 'No'
: 'Yes'}
{tokenStats.initialTokenQty.toLocaleString()}
{tokenStats.totalBurned.toLocaleString()}
{tokenStats.totalMinted.toLocaleString()}
{tokenStats.circulatingSupply.toLocaleString()}
>
)}
)}
>
)}
>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in SendToken.test.js
status => {console.log(status)} is an arbitrary stub function
*/
SendToken.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
SendToken.propTypes = {
tokenId: PropTypes.string,
jestBCH: PropTypes.object,
passLoadingStatus: PropTypes.func,
};
export default SendToken;