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
,
XEC
max
= $ NaN USD
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max
= $ NaN USD
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
- 0.06047469 + 0.06 XEC
,
$ NaN USD
,
XEC
max
= $ NaN USD
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max
= $ NaN USD
, ] `; exports[`Without wallet defined 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max
= $ NaN USD
, ] `; 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
,
Create eToken
,

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
,
Create eToken
,

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
,
Create eToken
,
identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba
6.001 TBS
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [
You need some XEC in your wallet to create tokens.
,
0 XEC
,
Create eToken
,

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
,
Create eToken
,

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
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
XEC
eToken
, ] `; 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
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
XEC
eToken
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
- 0.06047469 + 0.06 XEC
,
$ NaN USD
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
XEC
eToken
, ] `; 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
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
XEC
eToken
, ] `; 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; } }