diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js
index 99ba1e7ac..07f8f9f7f 100644
--- a/web/cashtab/src/components/Configure/Configure.js
+++ b/web/cashtab/src/components/Configure/Configure.js
@@ -1,605 +1,664 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { Collapse, Form, Input, Modal, Alert } from 'antd';
import {
PlusSquareOutlined,
WalletFilled,
ImportOutlined,
LockOutlined,
} from '@ant-design/icons';
import { WalletContext } from '@utils/context';
import { StyledCollapse } from '@components/Common/StyledCollapse';
import {
AntdFormWrapper,
CurrencySelectDropdown,
} from '@components/Common/EnhancedInputs';
import PrimaryButton, {
SecondaryButton,
SmartButton,
} from '@components/Common/PrimaryButton';
import {
ThemedCopyOutlined,
ThemedWalletOutlined,
ThemedDollarOutlined,
} from '@components/Common/CustomIcons';
import { ReactComponent as Trashcan } from '@assets/trashcan.svg';
import { ReactComponent as Edit } from '@assets/edit.svg';
import { Event } from '@utils/GoogleAnalytics';
import ApiError from '@components/Common/ApiError';
+import { formatSavedBalance } from '@utils/validation';
const { Panel } = Collapse;
const SettingsLink = styled.a`
text-decoration: underline;
color: ${props => props.theme.primary};
:visited {
text-decoration: underline;
color: ${props => props.theme.primary};
}
:hover {
color: ${props => props.theme.brandSecondary};
}
`;
const SWRow = styled.div`
border-radius: 3px;
padding: 10px 0;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6px;
@media (max-width: 500px) {
flex-direction: column;
margin-bottom: 12px;
}
`;
const SWName = styled.div`
width: 50%;
display: flex;
align-items: center;
justify-content: space-between;
word-wrap: break-word;
hyphens: auto;
@media (max-width: 500px) {
width: 100%;
justify-content: center;
margin-bottom: 15px;
}
h3 {
font-size: 16px;
color: ${props => props.theme.wallet.text.secondary};
margin: 0;
- text-align: left;
+ text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
+ h3.overflow {
+ width: 100px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ h3.overflow:hover {
+ background-color: #eee;
+ overflow: visible;
+ inline-size: 100px;
+ white-space: normal;
+ }
+`;
+
+const SWBalance = styled.div`
+ width: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ word-wrap: break-word;
+ hyphens: auto;
+ @media (max-width: 500px) {
+ width: 100%;
+ justify-content: center;
+ margin-bottom: 15px;
+ }
+ div {
+ font-size: 13px;
+ color: ${props => props.theme.wallet.text.secondary};
+ margin: 0;
+ text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ div.overflow {
+ width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ div.overflow:hover {
+ background-color: #eee;
+ overflow: visible;
+ inline-size: 150px;
+ white-space: normal;
+ }
`;
const SWButtonCtn = styled.div`
width: 50%;
display: flex;
align-items: center;
justify-content: flex-end;
@media (max-width: 500px) {
width: 100%;
justify-content: center;
}
button {
cursor: pointer;
@media (max-width: 768px) {
font-size: 14px;
}
}
svg {
stroke: ${props => props.theme.wallet.text.secondary};
fill: ${props => props.theme.wallet.text.secondary};
width: 25px;
height: 25px;
margin-right: 20px;
cursor: pointer;
:first-child:hover {
stroke: ${props => props.theme.primary};
fill: ${props => props.theme.primary};
}
:hover {
stroke: ${props => props.theme.settings.delete};
fill: ${props => props.theme.settings.delete};
}
}
`;
const AWRow = styled.div`
padding: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
h3 {
font-size: 16px;
display: inline-block;
color: ${props => props.theme.wallet.text.secondary};
margin: 0;
text-align: left;
font-weight: bold;
@media (max-width: 500px) {
font-size: 14px;
}
}
h4 {
font-size: 16px;
display: inline-block;
color: ${props => props.theme.primary} !important;
margin: 0;
text-align: right;
}
@media (max-width: 500px) {
flex-direction: column;
margin-bottom: 12px;
}
`;
const StyledConfigure = styled.div`
h2 {
color: ${props => props.theme.wallet.text.primary};
font-size: 25px;
}
p {
color: ${props => props.theme.wallet.text.secondary};
}
`;
const StyledSpacer = styled.div`
height: 1px;
width: 100%;
background-color: ${props => props.theme.wallet.borders.color};
margin: 60px 0 50px;
`;
const Configure = () => {
const ContextValue = React.useContext(WalletContext);
const { wallet, apiError } = ContextValue;
const {
addNewSavedWallet,
activateWallet,
renameWallet,
deleteWallet,
validateMnemonic,
getSavedWallets,
cashtabSettings,
changeCashtabSettings,
} = ContextValue;
const [savedWallets, setSavedWallets] = useState([]);
const [formData, setFormData] = useState({
dirty: true,
mnemonic: '',
});
const [showRenameWalletModal, setShowRenameWalletModal] = useState(false);
const [showDeleteWalletModal, setShowDeleteWalletModal] = useState(false);
const [walletToBeRenamed, setWalletToBeRenamed] = useState(null);
const [walletToBeDeleted, setWalletToBeDeleted] = useState(null);
const [newWalletName, setNewWalletName] = useState('');
const [
confirmationOfWalletToBeDeleted,
setConfirmationOfWalletToBeDeleted,
] = useState('');
const [newWalletNameIsValid, setNewWalletNameIsValid] = useState(null);
const [walletDeleteValid, setWalletDeleteValid] = useState(null);
const [seedInput, openSeedInput] = useState(false);
const showPopulatedDeleteWalletModal = walletInfo => {
setWalletToBeDeleted(walletInfo);
setShowDeleteWalletModal(true);
};
const showPopulatedRenameWalletModal = walletInfo => {
setWalletToBeRenamed(walletInfo);
setShowRenameWalletModal(true);
};
const cancelRenameWallet = () => {
// Delete form value
setNewWalletName('');
setShowRenameWalletModal(false);
};
const cancelDeleteWallet = () => {
setWalletToBeDeleted(null);
setConfirmationOfWalletToBeDeleted('');
setShowDeleteWalletModal(false);
};
const updateSavedWallets = async activeWallet => {
if (activeWallet) {
let savedWallets;
try {
savedWallets = await getSavedWallets(activeWallet);
setSavedWallets(savedWallets);
} catch (err) {
console.log(`Error in getSavedWallets()`);
console.log(err);
}
}
};
const [isValidMnemonic, setIsValidMnemonic] = useState(null);
useEffect(() => {
// Update savedWallets every time the active wallet changes
updateSavedWallets(wallet);
}, [wallet]);
// Need this function to ensure that savedWallets are updated on new wallet creation
const updateSavedWalletsOnCreate = async importMnemonic => {
// Event("Category", "Action", "Label")
// Track number of times a different wallet is activated
Event('Configure.js', 'Create Wallet', 'New');
const walletAdded = await addNewSavedWallet(importMnemonic);
if (!walletAdded) {
Modal.error({
title: 'This wallet already exists!',
content: 'Wallet not added',
});
} else {
Modal.success({
content: 'Wallet added to your saved wallets',
});
}
await updateSavedWallets(wallet);
};
// Same here
// TODO you need to lock UI here until this is complete
// Otherwise user may try to load an already-loading wallet, wreak havoc with indexedDB
const updateSavedWalletsOnLoad = async walletToActivate => {
// Event("Category", "Action", "Label")
// Track number of times a different wallet is activated
Event('Configure.js', 'Activate', '');
await activateWallet(walletToActivate);
};
async function submit() {
setFormData({
...formData,
dirty: false,
});
// Exit if no user input
if (!formData.mnemonic) {
return;
}
// Exit if mnemonic is invalid
if (!isValidMnemonic) {
return;
}
// Event("Category", "Action", "Label")
// Track number of times a different wallet is activated
Event('Configure.js', 'Create Wallet', 'Imported');
updateSavedWalletsOnCreate(formData.mnemonic);
}
const handleChange = e => {
const { value, name } = e.target;
// Validate mnemonic on change
// Import button should be disabled unless mnemonic is valid
setIsValidMnemonic(validateMnemonic(value));
setFormData(p => ({ ...p, [name]: value }));
};
const changeWalletName = async () => {
if (newWalletName === '' || newWalletName.length > 24) {
setNewWalletNameIsValid(false);
return;
}
// Hide modal
setShowRenameWalletModal(false);
// Change wallet name
console.log(
`Changing wallet ${walletToBeRenamed.name} name to ${newWalletName}`,
);
const renameSuccess = await renameWallet(
walletToBeRenamed.name,
newWalletName,
);
if (renameSuccess) {
Modal.success({
content: `Wallet "${walletToBeRenamed.name}" renamed to "${newWalletName}"`,
});
} else {
Modal.error({
content: `Rename failed. All wallets must have a unique name.`,
});
}
await updateSavedWallets(wallet);
// Clear wallet name for form
setNewWalletName('');
};
const deleteSelectedWallet = async () => {
if (!walletDeleteValid && walletDeleteValid !== null) {
return;
}
if (
confirmationOfWalletToBeDeleted !==
`delete ${walletToBeDeleted.name}`
) {
setWalletDeleteValid(false);
return;
}
// Hide modal
setShowDeleteWalletModal(false);
// Change wallet name
console.log(`Deleting wallet "${walletToBeDeleted.name}"`);
const walletDeletedSuccess = await deleteWallet(walletToBeDeleted);
if (walletDeletedSuccess) {
Modal.success({
content: `Wallet "${walletToBeDeleted.name}" successfully deleted`,
});
} else {
Modal.error({
content: `Error deleting ${walletToBeDeleted.name}.`,
});
}
await updateSavedWallets(wallet);
// Clear wallet delete confirmation from form
setConfirmationOfWalletToBeDeleted('');
};
const handleWalletNameInput = e => {
const { value } = e.target;
// validation
if (value && value.length && value.length < 24) {
setNewWalletNameIsValid(true);
} else {
setNewWalletNameIsValid(false);
}
setNewWalletName(value);
};
const handleWalletToDeleteInput = e => {
const { value } = e.target;
if (value && value === `delete ${walletToBeDeleted.name}`) {
setWalletDeleteValid(true);
} else {
setWalletDeleteValid(false);
}
setConfirmationOfWalletToBeDeleted(value);
};
return (
{walletToBeRenamed !== null && (
cancelRenameWallet()}
>
}
placeholder="Enter new wallet name"
name="newName"
value={newWalletName}
onChange={e => handleWalletNameInput(e)}
/>
)}
{walletToBeDeleted !== null && (
cancelDeleteWallet()}
>
}
placeholder={`Type "delete ${walletToBeDeleted.name}" to confirm`}
name="walletToBeDeletedInput"
value={confirmationOfWalletToBeDeleted}
onChange={e => handleWalletToDeleteInput(e)}
/>
)}
Backup your wallet
{wallet && wallet.mnemonic && (
{wallet && wallet.mnemonic ? wallet.mnemonic : ''}
)}
Manage Wallets
{apiError ? (
) : (
<>
updateSavedWalletsOnCreate()}>
New Wallet
openSeedInput(!seedInput)}>
Import Wallet
{seedInput && (
<>
Copy and paste your mnemonic seed phrase below
to import an existing wallet
}
type="email"
placeholder="mnemonic (seed phrase)"
name="mnemonic"
autoComplete="off"
onChange={e => handleChange(e)}
required
/>
submit()}
>
Import
>
)}
>
)}
{savedWallets && savedWallets.length > 0 && (
<>
{wallet.name}
Currently active
{savedWallets.map(sw => (
- {sw.name}
+
+ {sw.name}
+
-
+
+
+ [
+ {sw && sw.state
+ ? formatSavedBalance(
+ sw.state.balances
+ .totalBalance,
+ )
+ : 'N/A'}{' '}
+ XEC]
+
+
showPopulatedRenameWalletModal(
sw,
)
}
/>
showPopulatedDeleteWalletModal(
sw,
)
}
/>
))}
>
)}
Fiat Currency
changeCashtabSettings('fiatCurrency', fiatCode)
}
/>
[
Documentation
]
);
};
export default Configure;
diff --git a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
index 840145997..fe8efb317 100644
--- a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
+++ b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
@@ -1,605 +1,605 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
exports[`Configure with a wallet 1`] = `
Backup your wallet
Your seed phrase is the only way to restore your wallet. Write it down. Keep it safe.
Click to reveal seed phrase
Manage Wallets
Fiat Currency
[
Documentation
]
`;
exports[`Configure without a wallet 1`] = `
Backup your wallet
Your seed phrase is the only way to restore your wallet. Write it down. Keep it safe.
Manage Wallets
Fiat Currency
[
Documentation
]
`;
diff --git a/web/cashtab/src/utils/__tests__/validation.test.js b/web/cashtab/src/utils/__tests__/validation.test.js
index 6f4589c63..f251bafdf 100644
--- a/web/cashtab/src/utils/__tests__/validation.test.js
+++ b/web/cashtab/src/utils/__tests__/validation.test.js
@@ -1,228 +1,263 @@
import {
shouldRejectAmountInput,
fiatToCrypto,
isValidTokenName,
isValidTokenTicker,
isValidTokenDecimals,
isValidTokenInitialQty,
isValidTokenDocumentUrl,
isValidTokenStats,
isValidCashtabSettings,
+ formatSavedBalance,
} 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');
+ });
});
diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js
index 646eeb1e9..cb037a1b6 100644
--- a/web/cashtab/src/utils/validation.js
+++ b/web/cashtab/src/utils/validation.js
@@ -1,121 +1,141 @@
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';
+ }
+};