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 5922095c1..882d578fd 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,1704 +1,1704 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
exports[`Wallet with BCH balances 1`] = `
Array [
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
- 0.06047469
+ 0.06
XEC
,
$
NaN
USD
,
,
]
`;
exports[`Wallet without BCH balance 1`] = `
Array [
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
exports[`Without wallet defined 1`] = `
Array [
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
diff --git a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
index 759f4a4b0..0c052c07e 100644
--- a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
+++ b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
@@ -1,419 +1,419 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
exports[`Wallet with BCH balances 1`] = `
Array [
You need some
XEC
in your wallet to create tokens.
,
0
XEC
,
,
You need at least
5.5
XEC
(
$
NaN
USD
) to create a token
,
"No ",
"eToken",
" tokens in this wallet",
]
`;
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
You need some
XEC
in your wallet to create tokens.
,
0
XEC
,
,
You need at least
5.5
XEC
(
$
NaN
USD
) to create a token
,
"No ",
"eToken",
" tokens in this wallet",
]
`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
- 0.06047469
+ 0.06
XEC
,
$
NaN
USD
,
,
,
]
`;
exports[`Wallet without BCH balance 1`] = `
Array [
You need some
XEC
in your wallet to create tokens.
,
0
XEC
,
,
You need at least
5.5
XEC
(
$
NaN
USD
) to create a token
,
"No ",
"eToken",
" tokens in this wallet",
]
`;
exports[`Without wallet defined 1`] = `
Array [
You need some
XEC
in your wallet to create tokens.
,
0
XEC
,
,
You need at least
5.5
XEC
(
$
NaN
USD
) to create a token
,
"No ",
"eToken",
" tokens in this wallet",
]
`;
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 b94a95a98..22720619c 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,622 +1,622 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
exports[`Wallet with BCH balances 1`] = `
Array [
🎉
Congratulations on your new wallet!
🎉
Start using the wallet immediately to receive
XEC
payments, or load it up with
XEC
to send to others
,
0
XEC
,
,
,
]
`;
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
🎉
Congratulations on your new wallet!
🎉
Start using the wallet immediately to receive
XEC
payments, or load it up with
XEC
to send to others
,
0
XEC
,
,
,
]
`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
- 0.06047469
+ 0.06
XEC
,
$
NaN
USD
,
,
,
]
`;
exports[`Wallet without BCH balance 1`] = `
Array [
🎉
Congratulations on your new wallet!
🎉
Start using the wallet immediately to receive
XEC
payments, or load it up with
XEC
to send to others
,
0
XEC
,
,
,
]
`;
exports[`Without wallet defined 1`] = `
Array [
Welcome to Cashtab!
,
Cashtab is an
open source,
non-custodial web wallet for
eCash
.
Want to learn more?
Check out the Cashtab documentation.
,
,
,
]
`;
diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js
index 8570c95e5..e619c34da 100644
--- a/web/cashtab/src/utils/__tests__/cashMethods.test.js
+++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js
@@ -1,125 +1,142 @@
import {
fromSmallestDenomination,
formatBalance,
batchArray,
flattenBatchedHydratedUtxos,
loadStoredWallet,
isValidStoredWallet,
fromLegacyDecimals,
convertToEcashPrefix,
convertEtokenToSimpleledger,
} from '@utils/cashMethods';
import {
unbatchedArray,
arrayBatchedByThree,
} from '../__mocks__/mockBatchedArrays';
import {
unflattenedHydrateUtxosResponse,
flattenedHydrateUtxosResponse,
} from '../__mocks__/flattenBatchedHydratedUtxosMocks';
import {
cachedUtxos,
utxosLoadedFromCache,
} from '../__mocks__/mockCachedUtxos';
import {
validStoredWallet,
invalidStoredWallet,
} from '../__mocks__/mockStoredWallets';
describe('Correctly executes cash utility functions', () => {
it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => {
expect(fromSmallestDenomination(1, 2)).toBe(0.01);
});
it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 2`, () => {
expect(fromSmallestDenomination(1000000012345678, 2)).toBe(
10000000123456.78,
);
});
it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 8`, () => {
expect(fromSmallestDenomination(1, 8)).toBe(0.00000001);
});
it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 8`, () => {
expect(fromSmallestDenomination(1000000012345678, 8)).toBe(
10000000.12345678,
);
});
- it(`Formats a large number to add spaces as thousands separator`, () => {
- expect(formatBalance(1000000012345678)).toBe('1 000 000 012 345 678');
- });
- it(`Formats a large number with 2 decimal places to add as thousands separator`, () => {
- expect(formatBalance(10000000123456.78)).toBe('10 000 000 123 456.78');
- });
- it(`Formats a large number with 9 decimal places to add as thousands separator without adding them to decimals`, () => {
- expect(formatBalance('10000000123456.789123456')).toBe(
- '10 000 000 123 456.789123456',
- );
- });
- it(`formatBalance handles an input of 0`, () => {
- expect(formatBalance('0')).toBe('0');
- });
- it(`formatBalance handles an input of undefined`, () => {
- expect(formatBalance(undefined)).toBe(undefined);
- });
- it(`formatBalance handles an input of null`, () => {
- expect(formatBalance(null)).toBe(null);
- });
it(`Correctly converts an array of length 10 to an array of 4 arrays, each with max length 3`, () => {
expect(batchArray(unbatchedArray, 3)).toStrictEqual(
arrayBatchedByThree,
);
});
it(`If array length is less than batch size, return original array as first and only element of new array`, () => {
expect(batchArray(unbatchedArray, 20)).toStrictEqual([unbatchedArray]);
});
it(`Flattens hydrateUtxos from Promise.all() response into array that can be parsed by getSlpBalancesAndUtxos`, () => {
expect(
flattenBatchedHydratedUtxos(unflattenedHydrateUtxosResponse),
).toStrictEqual(flattenedHydrateUtxosResponse);
});
it(`Accepts a cachedWalletState that has not preserved BigNumber object types, and returns the same wallet state with BigNumber type re-instituted`, () => {
expect(loadStoredWallet(cachedUtxos)).toStrictEqual(
utxosLoadedFromCache,
);
});
it(`Recognizes a stored wallet as valid if it has all required fields`, () => {
expect(isValidStoredWallet(validStoredWallet)).toBe(true);
});
it(`Recognizes a stored wallet as invalid if it is missing required fields`, () => {
expect(isValidStoredWallet(invalidStoredWallet)).toBe(false);
});
it(`Converts a legacy BCH amount to an XEC amount`, () => {
expect(fromLegacyDecimals(0.00000546, 2)).toStrictEqual(5.46);
});
it(`Leaves a legacy BCH amount unchanged if currency.cashDecimals is 8`, () => {
expect(fromLegacyDecimals(0.00000546, 8)).toStrictEqual(0.00000546);
});
it(`convertToEcashPrefix converts a bitcoincash: prefixed address to an ecash: prefixed address`, () => {
expect(
convertToEcashPrefix(
'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr',
),
).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035');
});
it(`convertToEcashPrefix returns an ecash: prefix address unchanged`, () => {
expect(
convertToEcashPrefix(
'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035',
),
).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035');
});
it(`convertEtokenToSimpleledger returns an etoken: prefix address as simpleledger:`, () => {
expect(
convertEtokenToSimpleledger(
'etoken:qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r',
),
).toBe('simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa');
});
it(`convertEtokenToSimpleledger returns a simpleledger: prefix address unchanged`, () => {
expect(
convertEtokenToSimpleledger(
'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa',
),
).toBe('simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa');
});
+ it(`test formatBalance with an input of 0`, () => {
+ expect(formatBalance('0')).toBe('0');
+ });
+ it(`test formatBalance with zero XEC balance input`, () => {
+ expect(formatBalance('0', 'en-US')).toBe('0');
+ });
+ it(`test formatBalance with a small XEC balance input with 2+ decimal figures`, () => {
+ expect(formatBalance('1574.5445', 'en-US')).toBe('1,574.54');
+ });
+ it(`test formatBalance with 1 Million XEC balance input`, () => {
+ expect(formatBalance('1000000', 'en-US')).toBe('1,000,000');
+ });
+ it(`test formatBalance with 1 Billion XEC balance input`, () => {
+ expect(formatBalance('1000000000', 'en-US')).toBe('1,000,000,000');
+ });
+ it(`test formatBalance with total supply as XEC balance input`, () => {
+ expect(formatBalance('21000000000000', 'en-US')).toBe(
+ '21,000,000,000,000',
+ );
+ });
+ it(`test formatBalance with > total supply as XEC balance input`, () => {
+ expect(formatBalance('31000000000000', 'en-US')).toBe(
+ '31,000,000,000,000',
+ );
+ });
+ it(`test formatBalance with no balance`, () => {
+ expect(formatBalance('', 'en-US')).toBe('0');
+ });
+ it(`test formatBalance with null input`, () => {
+ expect(formatBalance(null, 'en-US')).toBe('0');
+ });
+ it(`test formatBalance with undefined as input`, () => {
+ expect(formatBalance(undefined, 'en-US')).toBe('NaN');
+ });
+ it(`test formatBalance with non-numeric input`, () => {
+ expect(formatBalance('CainBCHA', 'en-US')).toBe('NaN');
+ });
});
diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js
index 03d066c53..2f26a10b8 100644
--- a/web/cashtab/src/utils/cashMethods.js
+++ b/web/cashtab/src/utils/cashMethods.js
@@ -1,231 +1,233 @@
import { currency } from '@components/Common/Ticker';
import BigNumber from 'bignumber.js';
import cashaddr from 'ecashaddrjs';
export const fromLegacyDecimals = (
amount,
cashDecimals = currency.cashDecimals,
) => {
// Input 0.00000546 BCH
// Output 5.46 XEC or 0.00000546 BCH, depending on currency.cashDecimals
const amountBig = new BigNumber(amount);
const conversionFactor = new BigNumber(10 ** (8 - cashDecimals));
const amountSmallestDenomination = amountBig
.times(conversionFactor)
.toNumber();
return amountSmallestDenomination;
};
export const fromSmallestDenomination = (
amount,
cashDecimals = currency.cashDecimals,
) => {
const amountBig = new BigNumber(amount);
const multiplier = new BigNumber(10 ** (-1 * cashDecimals));
const amountInBaseUnits = amountBig.times(multiplier);
return amountInBaseUnits.toNumber();
};
export const toSmallestDenomination = (
sendAmount,
cashDecimals = currency.cashDecimals,
) => {
// Replace the BCH.toSatoshi method with an equivalent function that works for arbitrary decimal places
// Example, for an 8 decimal place currency like Bitcoin
// Input: a BigNumber of the amount of Bitcoin to be sent
// Output: a BigNumber of the amount of satoshis to be sent, or false if input is invalid
// Validate
// Input should be a BigNumber with no more decimal places than cashDecimals
const isValidSendAmount =
BigNumber.isBigNumber(sendAmount) && sendAmount.dp() <= cashDecimals;
if (!isValidSendAmount) {
return false;
}
const conversionFactor = new BigNumber(10 ** cashDecimals);
const sendAmountSmallestDenomination = sendAmount.times(conversionFactor);
return sendAmountSmallestDenomination;
};
-export const formatBalance = x => {
+export const formatBalance = (unformattedBalance, optionalLocale) => {
try {
- let balanceInParts = x.toString().split('.');
- balanceInParts[0] = balanceInParts[0].replace(
- /\B(?=(\d{3})+(?!\d))/g,
- ' ',
- );
- return balanceInParts.join('.');
+ if (optionalLocale === undefined) {
+ return new Number(unformattedBalance).toLocaleString({
+ maximumFractionDigits: currency.cashDecimals,
+ });
+ }
+ return new Number(unformattedBalance).toLocaleString(optionalLocale, {
+ maximumFractionDigits: currency.cashDecimals,
+ });
} catch (err) {
- console.log(`Error in formatBalance for ${x}`);
+ console.log(`Error in formatBalance for ${unformattedBalance}`);
console.log(err);
- return x;
+ return unformattedBalance;
}
};
export const batchArray = (inputArray, batchSize) => {
// take an array of n elements, return an array of arrays each of length batchSize
const batchedArray = [];
for (let i = 0; i < inputArray.length; i += batchSize) {
const tempArray = inputArray.slice(i, i + batchSize);
batchedArray.push(tempArray);
}
return batchedArray;
};
export const flattenBatchedHydratedUtxos = batchedHydratedUtxoDetails => {
// Return same result as if only the bulk API call were made
// to do this, just need to move all utxos under one slpUtxos
/*
given
[
{
slpUtxos: [
{
utxos: [],
address: '',
}
],
},
{
slpUtxos: [
{
utxos: [],
address: '',
}
],
}
]
return [
{
slpUtxos: [
{
utxos: [],
address: ''
},
{
utxos: [],
address: ''
},
]
}
*/
const flattenedBatchedHydratedUtxos = { slpUtxos: [] };
for (let i = 0; i < batchedHydratedUtxoDetails.length; i += 1) {
const theseSlpUtxos = batchedHydratedUtxoDetails[i].slpUtxos[0];
flattenedBatchedHydratedUtxos.slpUtxos.push(theseSlpUtxos);
}
return flattenedBatchedHydratedUtxos;
};
export const loadStoredWallet = walletStateFromStorage => {
// Accept cached tokens array that does not save BigNumber type of BigNumbers
// Return array with BigNumbers converted
// See BigNumber.js api for how to create a BigNumber object from an object
// https://mikemcl.github.io/bignumber.js/
const liveWalletState = walletStateFromStorage;
const { slpBalancesAndUtxos, tokens } = liveWalletState;
for (let i = 0; i < tokens.length; i += 1) {
const thisTokenBalance = tokens[i].balance;
thisTokenBalance._isBigNumber = true;
tokens[i].balance = new BigNumber(thisTokenBalance);
}
// Also confirm balance is correct
// Necessary step in case currency.decimals changed since last startup
const balancesRebased = normalizeBalance(slpBalancesAndUtxos);
liveWalletState.balances = balancesRebased;
return liveWalletState;
};
export const normalizeBalance = slpBalancesAndUtxos => {
const totalBalanceInSatoshis = slpBalancesAndUtxos.nonSlpUtxos.reduce(
(previousBalance, utxo) => previousBalance + utxo.value,
0,
);
return {
totalBalanceInSatoshis,
totalBalance: fromSmallestDenomination(totalBalanceInSatoshis),
};
};
export const isValidStoredWallet = walletStateFromStorage => {
return (
typeof walletStateFromStorage === 'object' &&
'state' in walletStateFromStorage &&
typeof walletStateFromStorage.state === 'object' &&
'balances' in walletStateFromStorage.state &&
'utxos' in walletStateFromStorage.state &&
'hydratedUtxoDetails' in walletStateFromStorage.state &&
'slpBalancesAndUtxos' in walletStateFromStorage.state &&
'tokens' in walletStateFromStorage.state
);
};
export const getWalletState = wallet => {
if (!wallet || !wallet.state) {
return {
balances: { totalBalance: 0, totalBalanceInSatoshis: 0 },
hydratedUtxoDetails: {},
tokens: [],
slpBalancesAndUtxos: {},
parsedTxHistory: [],
utxos: [],
};
}
return wallet.state;
};
export function convertToEcashPrefix(bitcoincashPrefixedAddress) {
// Prefix-less addresses may be valid, but the cashaddr.decode function used below
// will throw an error without a prefix. Hence, must ensure prefix to use that function.
const hasPrefix = bitcoincashPrefixedAddress.includes(':');
if (hasPrefix) {
// Is it bitcoincash: or simpleledger:
const { type, hash, prefix } = cashaddr.decode(
bitcoincashPrefixedAddress,
);
let newPrefix;
if (prefix === 'bitcoincash') {
newPrefix = 'ecash';
} else if (prefix === 'simpleledger') {
newPrefix = 'etoken';
} else {
return bitcoincashPrefixedAddress;
}
const convertedAddress = cashaddr.encode(newPrefix, type, hash);
return convertedAddress;
} else {
return bitcoincashPrefixedAddress;
}
}
export function convertEtokenToSimpleledger(etokenPrefixedAddress) {
// Prefix-less addresses may be valid, but the cashaddr.decode function used below
// will throw an error without a prefix. Hence, must ensure prefix to use that function.
const hasPrefix = etokenPrefixedAddress.includes(':');
if (hasPrefix) {
// Is it bitcoincash: or simpleledger:
const { type, hash, prefix } = cashaddr.decode(etokenPrefixedAddress);
let newPrefix;
if (prefix === 'etoken') {
newPrefix = 'simpleledger';
} else {
// return address with no change
return etokenPrefixedAddress;
}
const convertedAddress = cashaddr.encode(newPrefix, type, hash);
return convertedAddress;
} else {
// return address with no change
return etokenPrefixedAddress;
}
}