diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js
index cd34ed7b5..b3d43fd38 100644
--- a/web/cashtab/src/components/Send/Send.js
+++ b/web/cashtab/src/components/Send/Send.js
@@ -1,523 +1,537 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { WalletContext } from '@utils/context';
import { Form, message, Modal, Alert } from 'antd';
import { Row, Col } from 'antd';
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 {
sendXecNotification,
errorNotification,
} 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';
// 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 { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue;
const walletState = getWalletState(wallet);
const { balances, slpBalancesAndUtxos } = walletState;
// 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 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(() => {
passLoadingStatus(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);
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,
);
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 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) {
+ // 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
} `
: '$ '
- } ${(fiatPrice * Number(formData.value)).toFixed(2)} ${
+ } ${fiatPriceString} ${
cashtabSettings && cashtabSettings.fiatCurrency
? cashtabSettings.fiatCurrency.toUpperCase()
: 'USD'
}`;
} else {
fiatPriceString = `${
- formData.value ? fiatToCrypto(formData.value, fiatPrice) : '0'
+ 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 && (
)}
>
)}
>
);
};
/*
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/utils/__tests__/validation.test.js b/web/cashtab/src/utils/__tests__/validation.test.js
index f251bafdf..352bf76c5 100644
--- a/web/cashtab/src/utils/__tests__/validation.test.js
+++ b/web/cashtab/src/utils/__tests__/validation.test.js
@@ -1,263 +1,284 @@
import {
shouldRejectAmountInput,
fiatToCrypto,
isValidTokenName,
isValidTokenTicker,
isValidTokenDecimals,
isValidTokenInitialQty,
isValidTokenDocumentUrl,
isValidTokenStats,
isValidCashtabSettings,
formatSavedBalance,
+ formatFiatBalance,
} from '../validation';
import { currency } from '@components/Common/Ticker.js';
import { fromSmallestDenomination } from '@utils/cashMethods';
import {
stStatsValid,
noCovidStatsValid,
noCovidStatsInvalid,
cGenStatsValid,
} from '../__mocks__/mockTokenStats';
describe('Validation utils', () => {
it(`Returns 'false' if ${currency.ticker} send amount is a valid send amount`, () => {
expect(shouldRejectAmountInput('10', currency.ticker, 20.0, 300)).toBe(
false,
);
});
it(`Returns 'false' if ${currency.ticker} send amount is a valid send amount in USD`, () => {
// Here, user is trying to send $170 USD, where 1 BCHA = $20 USD, and the user has a balance of 15 BCHA or $300
expect(shouldRejectAmountInput('170', 'USD', 20.0, 15)).toBe(false);
});
it(`Returns not a number if ${currency.ticker} send amount is not a number`, () => {
const expectedValidationError = `Amount must be a number`;
expect(
shouldRejectAmountInput('Not a number', currency.ticker, 20.0, 3),
).toBe(expectedValidationError);
});
it(`Returns amount must be greater than 0 if ${currency.ticker} send amount is 0`, () => {
const expectedValidationError = `Amount must be greater than 0`;
expect(shouldRejectAmountInput('0', currency.ticker, 20.0, 3)).toBe(
expectedValidationError,
);
});
it(`Returns amount must be greater than 0 if ${currency.ticker} send amount is less than 0`, () => {
const expectedValidationError = `Amount must be greater than 0`;
expect(
shouldRejectAmountInput('-0.031', currency.ticker, 20.0, 3),
).toBe(expectedValidationError);
});
it(`Returns balance error if ${currency.ticker} send amount is greater than user balance`, () => {
const expectedValidationError = `Amount cannot exceed your ${currency.ticker} balance`;
expect(shouldRejectAmountInput('17', currency.ticker, 20.0, 3)).toBe(
expectedValidationError,
);
});
it(`Returns balance error if ${currency.ticker} send amount is greater than user balance`, () => {
const expectedValidationError = `Amount cannot exceed your ${currency.ticker} balance`;
expect(shouldRejectAmountInput('17', currency.ticker, 20.0, 3)).toBe(
expectedValidationError,
);
});
it(`Returns error if ${
currency.ticker
} send amount is less than ${fromSmallestDenomination(
currency.dustSats,
).toString()} minimum`, () => {
const expectedValidationError = `Send amount must be at least ${fromSmallestDenomination(
currency.dustSats,
).toString()} ${currency.ticker}`;
expect(
shouldRejectAmountInput(
(
fromSmallestDenomination(currency.dustSats).toString() -
0.00000001
).toString(),
currency.ticker,
20.0,
3,
),
).toBe(expectedValidationError);
});
it(`Returns error if ${
currency.ticker
} send amount is less than ${fromSmallestDenomination(
currency.dustSats,
).toString()} minimum in fiat currency`, () => {
const expectedValidationError = `Send amount must be at least ${fromSmallestDenomination(
currency.dustSats,
).toString()} ${currency.ticker}`;
expect(
shouldRejectAmountInput('0.0000005', 'USD', 0.00005, 1000000),
).toBe(expectedValidationError);
});
it(`Returns balance error if ${currency.ticker} send amount is greater than user balance with fiat currency selected`, () => {
const expectedValidationError = `Amount cannot exceed your ${currency.ticker} balance`;
// Here, user is trying to send $170 USD, where 1 BCHA = $20 USD, and the user has a balance of 5 BCHA or $100
expect(shouldRejectAmountInput('170', 'USD', 20.0, 5)).toBe(
expectedValidationError,
);
});
it(`Returns precision error if ${currency.ticker} send amount has more than ${currency.cashDecimals} decimal places`, () => {
const expectedValidationError = `${currency.ticker} transactions do not support more than ${currency.cashDecimals} decimal places`;
expect(
shouldRejectAmountInput('17.123456789', currency.ticker, 20.0, 35),
).toBe(expectedValidationError);
});
it(`Returns expected crypto amount with ${currency.cashDecimals} decimals of precision even if inputs have higher precision`, () => {
expect(fiatToCrypto('10.97231694823432', 20.3231342349234234, 8)).toBe(
'0.53989295',
);
});
it(`Returns expected crypto amount with ${currency.cashDecimals} decimals of precision even if inputs have higher precision`, () => {
expect(fiatToCrypto('10.97231694823432', 20.3231342349234234, 2)).toBe(
'0.54',
);
});
it(`Returns expected crypto amount with ${currency.cashDecimals} decimals of precision even if inputs have lower precision`, () => {
expect(fiatToCrypto('10.94', 10, 8)).toBe('1.09400000');
});
it(`Accepts a valid ${currency.tokenTicker} token name`, () => {
expect(isValidTokenName('Valid token name')).toBe(true);
});
it(`Accepts a valid ${currency.tokenTicker} token name that is a stringified number`, () => {
expect(isValidTokenName('123456789')).toBe(true);
});
it(`Rejects ${currency.tokenTicker} token name if longer than 68 characters`, () => {
expect(
isValidTokenName(
'This token name is not valid because it is longer than 68 characters which is really pretty long for a token name when you think about it and all',
),
).toBe(false);
});
it(`Rejects ${currency.tokenTicker} token name if empty string`, () => {
expect(isValidTokenName('')).toBe(false);
});
it(`Accepts a 4-char ${currency.tokenTicker} token ticker`, () => {
expect(isValidTokenTicker('DOGE')).toBe(true);
});
it(`Accepts a 12-char ${currency.tokenTicker} token ticker`, () => {
expect(isValidTokenTicker('123456789123')).toBe(true);
});
it(`Rejects ${currency.tokenTicker} token ticker if empty string`, () => {
expect(isValidTokenTicker('')).toBe(false);
});
it(`Rejects ${currency.tokenTicker} token ticker if > 12 chars`, () => {
expect(isValidTokenTicker('1234567891234')).toBe(false);
});
it(`Accepts ${currency.tokenDecimals} if zero`, () => {
expect(isValidTokenDecimals('0')).toBe(true);
});
it(`Accepts ${currency.tokenDecimals} if between 0 and 9 inclusive`, () => {
expect(isValidTokenDecimals('9')).toBe(true);
});
it(`Rejects ${currency.tokenDecimals} if empty string`, () => {
expect(isValidTokenDecimals('')).toBe(false);
});
it(`Rejects ${currency.tokenDecimals} if non-integer`, () => {
expect(isValidTokenDecimals('1.7')).toBe(false);
});
it(`Accepts ${currency.tokenDecimals} initial genesis quantity at minimum amount for 3 decimal places`, () => {
expect(isValidTokenInitialQty('0.001', '3')).toBe(true);
});
it(`Accepts ${currency.tokenDecimals} initial genesis quantity at minimum amount for 9 decimal places`, () => {
expect(isValidTokenInitialQty('0.000000001', '9')).toBe(true);
});
it(`Accepts ${currency.tokenDecimals} initial genesis quantity at amount below 100 billion`, () => {
expect(isValidTokenInitialQty('1000', '0')).toBe(true);
});
it(`Accepts highest possible ${currency.tokenDecimals} initial genesis quantity at amount below 100 billion`, () => {
expect(isValidTokenInitialQty('99999999999.999999999', '9')).toBe(true);
});
it(`Accepts ${currency.tokenDecimals} initial genesis quantity if decimal places equal tokenDecimals`, () => {
expect(isValidTokenInitialQty('0.123', '3')).toBe(true);
});
it(`Accepts ${currency.tokenDecimals} initial genesis quantity if decimal places are less than tokenDecimals`, () => {
expect(isValidTokenInitialQty('0.12345', '9')).toBe(true);
});
it(`Rejects ${currency.tokenDecimals} initial genesis quantity of zero`, () => {
expect(isValidTokenInitialQty('0', '9')).toBe(false);
});
it(`Rejects ${currency.tokenDecimals} initial genesis quantity if tokenDecimals is not valid`, () => {
expect(isValidTokenInitialQty('0', '')).toBe(false);
});
it(`Rejects ${currency.tokenDecimals} initial genesis quantity if 100 billion or higher`, () => {
expect(isValidTokenInitialQty('100000000000', '0')).toBe(false);
});
it(`Rejects ${currency.tokenDecimals} initial genesis quantity if it has more decimal places than tokenDecimals`, () => {
expect(isValidTokenInitialQty('1.5', '0')).toBe(false);
});
it(`Accepts a valid ${currency.tokenTicker} token document URL`, () => {
expect(isValidTokenDocumentUrl('cashtabapp.com')).toBe(true);
});
it(`Accepts a valid ${currency.tokenTicker} token document URL including special URL characters`, () => {
expect(isValidTokenDocumentUrl('https://cashtabapp.com/')).toBe(true);
});
it(`Accepts a blank string as a valid ${currency.tokenTicker} token document URL`, () => {
expect(isValidTokenDocumentUrl('')).toBe(true);
});
it(`Rejects ${currency.tokenTicker} token name if longer than 68 characters`, () => {
expect(
isValidTokenDocumentUrl(
'http://www.ThisTokenDocumentUrlIsActuallyMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchTooLong.com/',
),
).toBe(false);
});
it(`Correctly validates token stats for token created before the ${currency.ticker} fork`, () => {
expect(isValidTokenStats(stStatsValid)).toBe(true);
});
it(`Correctly validates token stats for token created after the ${currency.ticker} fork`, () => {
expect(isValidTokenStats(noCovidStatsValid)).toBe(true);
});
it(`Correctly validates token stats for token with no minting baton`, () => {
expect(isValidTokenStats(cGenStatsValid)).toBe(true);
});
it(`Recognizes a token stats object with missing required keys as invalid`, () => {
expect(isValidTokenStats(noCovidStatsInvalid)).toBe(false);
});
it(`Recognizes a valid cashtab settings object`, () => {
expect(isValidCashtabSettings({ fiatCurrency: 'usd' })).toBe(true);
});
it(`Rejects a cashtab settings object for an unsupported currency`, () => {
expect(isValidCashtabSettings({ fiatCurrency: 'xau' })).toBe(false);
});
it(`Rejects a corrupted cashtab settings object for an unsupported currency`, () => {
expect(isValidCashtabSettings({ fiatCurrencyWrongLabel: 'usd' })).toBe(
false,
);
});
it(`test formatSavedBalance with zero XEC balance input`, () => {
expect(formatSavedBalance('0', 'en-US')).toBe('0');
});
it(`test formatSavedBalance with a small XEC balance input with 2+ decimal figures`, () => {
expect(formatSavedBalance('1574.5445', 'en-US')).toBe('1,574.54');
});
it(`test formatSavedBalance with 1 Million XEC balance input`, () => {
expect(formatSavedBalance('1000000', 'en-US')).toBe('1,000,000');
});
it(`test formatSavedBalance with 1 Billion XEC balance input`, () => {
expect(formatSavedBalance('1000000000', 'en-US')).toBe('1,000,000,000');
});
it(`test formatSavedBalance with total supply as XEC balance input`, () => {
expect(formatSavedBalance('21000000000000', 'en-US')).toBe(
'21,000,000,000,000',
);
});
it(`test formatSavedBalance with > total supply as XEC balance input`, () => {
expect(formatSavedBalance('31000000000000', 'en-US')).toBe(
'31,000,000,000,000',
);
});
it(`test formatSavedBalance with no balance`, () => {
expect(formatSavedBalance('', 'en-US')).toBe('0');
});
it(`test formatSavedBalance with null input`, () => {
expect(formatSavedBalance(null, 'en-US')).toBe('0');
});
it(`test formatSavedBalance with undefined sw.state.balance or sw.state.balance.totalBalance as input`, () => {
expect(formatSavedBalance(undefined, 'en-US')).toBe('N/A');
});
it(`test formatSavedBalance with non-numeric input`, () => {
expect(formatSavedBalance('CainBCHA', 'en-US')).toBe('NaN');
});
+ it(`test formatFiatBalance with zero XEC balance input`, () => {
+ expect(formatFiatBalance(Number('0'), 'en-US')).toBe('0.00');
+ });
+ it(`test formatFiatBalance with a small XEC balance input with 2+ decimal figures`, () => {
+ expect(formatFiatBalance(Number('565.54111'), 'en-US')).toBe('565.54');
+ });
+ it(`test formatFiatBalance with a large XEC balance input with 2+ decimal figures`, () => {
+ expect(formatFiatBalance(Number('131646565.54111'), 'en-US')).toBe(
+ '131,646,565.54',
+ );
+ });
+ it(`test formatFiatBalance with no balance`, () => {
+ expect(formatFiatBalance('', 'en-US')).toBe('');
+ });
+ it(`test formatFiatBalance with null input`, () => {
+ expect(formatFiatBalance(null, 'en-US')).toBe(null);
+ });
+ it(`test formatFiatBalance with undefined input`, () => {
+ expect(formatFiatBalance(undefined, 'en-US')).toBe(undefined);
+ });
});
diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js
index cb037a1b6..3fa8bdd32 100644
--- a/web/cashtab/src/utils/validation.js
+++ b/web/cashtab/src/utils/validation.js
@@ -1,141 +1,159 @@
import BigNumber from 'bignumber.js';
import { currency } from '@components/Common/Ticker.js';
import { fromSmallestDenomination } from '@utils/cashMethods';
// Validate cash amount
export const shouldRejectAmountInput = (
cashAmount,
selectedCurrency,
fiatPrice,
totalCashBalance,
) => {
// Take cashAmount as input, a string from form input
let error = false;
let testedAmount = new BigNumber(cashAmount);
if (selectedCurrency !== currency.ticker) {
// Ensure no more than currency.cashDecimals decimal places
testedAmount = new BigNumber(fiatToCrypto(cashAmount, fiatPrice));
}
// Validate value for > 0
if (isNaN(testedAmount)) {
error = 'Amount must be a number';
} else if (testedAmount.lte(0)) {
error = 'Amount must be greater than 0';
} else if (
testedAmount.lt(fromSmallestDenomination(currency.dustSats).toString())
) {
error = `Send amount must be at least ${fromSmallestDenomination(
currency.dustSats,
).toString()} ${currency.ticker}`;
} else if (testedAmount.gt(totalCashBalance)) {
error = `Amount cannot exceed your ${currency.ticker} balance`;
} else if (!isNaN(testedAmount) && testedAmount.toString().includes('.')) {
if (
testedAmount.toString().split('.')[1].length > currency.cashDecimals
) {
error = `${currency.ticker} transactions do not support more than ${currency.cashDecimals} decimal places`;
}
}
// return false if no error, or string error msg if error
return error;
};
export const fiatToCrypto = (
fiatAmount,
fiatPrice,
cashDecimals = currency.cashDecimals,
) => {
let cryptoAmount = new BigNumber(fiatAmount)
.div(new BigNumber(fiatPrice))
.toFixed(cashDecimals);
return cryptoAmount;
};
export const isValidTokenName = tokenName => {
return (
typeof tokenName === 'string' &&
tokenName.length > 0 &&
tokenName.length < 68
);
};
export const isValidTokenTicker = tokenTicker => {
return (
typeof tokenTicker === 'string' &&
tokenTicker.length > 0 &&
tokenTicker.length < 13
);
};
export const isValidTokenDecimals = tokenDecimals => {
return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(
tokenDecimals,
);
};
export const isValidTokenInitialQty = (tokenInitialQty, tokenDecimals) => {
const minimumQty = new BigNumber(1 / 10 ** tokenDecimals);
const tokenIntialQtyBig = new BigNumber(tokenInitialQty);
return (
tokenIntialQtyBig.gte(minimumQty) &&
tokenIntialQtyBig.lt(100000000000) &&
tokenIntialQtyBig.dp() <= tokenDecimals
);
};
export const isValidTokenDocumentUrl = tokenDocumentUrl => {
return (
typeof tokenDocumentUrl === 'string' &&
tokenDocumentUrl.length >= 0 &&
tokenDocumentUrl.length < 68
);
};
export const isValidTokenStats = tokenStats => {
return (
typeof tokenStats === 'object' &&
'timestampUnix' in tokenStats &&
'documentUri' in tokenStats &&
'containsBaton' in tokenStats &&
'initialTokenQty' in tokenStats &&
'totalMinted' in tokenStats &&
'totalBurned' in tokenStats &&
'circulatingSupply' in tokenStats
);
};
export const isValidCashtabSettings = settings => {
try {
const isValid =
typeof settings === 'object' &&
Object.prototype.hasOwnProperty.call(settings, 'fiatCurrency') &&
currency.settingsValidation.fiatCurrency.includes(
settings.fiatCurrency,
);
return isValid;
} catch (err) {
return false;
}
};
export const formatSavedBalance = (swBalance, optionalLocale) => {
try {
if (swBalance === undefined) {
return 'N/A';
} else {
if (optionalLocale === undefined) {
return new Number(swBalance).toLocaleString({
maximumFractionDigits: currency.cashDecimals,
});
} else {
return new Number(swBalance).toLocaleString(optionalLocale, {
maximumFractionDigits: currency.cashDecimals,
});
}
}
} catch (err) {
return 'N/A';
}
};
+
+export const formatFiatBalance = (fiatBalance, optionalLocale) => {
+ try {
+ if (fiatBalance === 0) {
+ return Number(fiatBalance).toFixed(currency.cashDecimals);
+ }
+ if (optionalLocale === undefined) {
+ return fiatBalance.toLocaleString({
+ maximumFractionDigits: currency.cashDecimals,
+ });
+ }
+ return fiatBalance.toLocaleString(optionalLocale, {
+ maximumFractionDigits: currency.cashDecimals,
+ });
+ } catch (err) {
+ return fiatBalance;
+ }
+};