diff --git a/web/cashtab/src/hooks/__tests__/migrations.test.js b/web/cashtab/src/hooks/__tests__/migrations.test.js --- a/web/cashtab/src/hooks/__tests__/migrations.test.js +++ b/web/cashtab/src/hooks/__tests__/migrations.test.js @@ -1,5 +1,7 @@ import { currency } from '../../components/Common/Ticker'; import BigNumber from 'bignumber.js'; +import BCHJS from '@psf/bch-js'; +import useBCH from '../useBCH'; describe('Testing functions for upgrading Cashtab', () => { it('Replacement currency.dust parameter parsing matches legacy DUST parameter', () => { @@ -7,4 +9,48 @@ 0.00000546, ); }); + it('toSmallestDenomination() returns false if input is not a BigNumber', () => { + const { toSmallestDenomination } = useBCH(); + const testInput = 132.12345678; + expect(toSmallestDenomination(testInput)).toBe(false); + }); + it(`toSmallestDenomination() returns false if input is a BigNumber with more decimals than specified by cashDecimals parameter`, () => { + const { toSmallestDenomination } = useBCH(); + const testInput = new BigNumber('132.123456789'); + expect(toSmallestDenomination(testInput, 8)).toBe(false); + }); + it(`toSmallestDenomination() returns expected value if input is a BigNumber with 8 decimal places`, () => { + const { toSmallestDenomination } = useBCH(); + + const testInput = new BigNumber('100.12345678'); + expect(toSmallestDenomination(testInput, 8)).toStrictEqual( + new BigNumber('10012345678'), + ); + }); + it(`toSmallestDenomination() returns expected value if input is a BigNumber with 2 decimal places`, () => { + const { toSmallestDenomination } = useBCH(); + + const testInput = new BigNumber('100.12'); + expect(toSmallestDenomination(testInput, 2)).toStrictEqual( + new BigNumber('10012'), + ); + }); + it('toSmallestDenomination() returns exact result as toSatoshi but in BigNumber format', () => { + const BCH = new BCHJS(); + const { toSmallestDenomination } = useBCH(); + + const testAmount = new BigNumber('0.12345678'); + + // Match legacy implementation, inputting a BigNumber converted to a string by .toFixed(8) + const testAmountInSatoshis = BCH.BitcoinCash.toSatoshi( + testAmount.toFixed(8), + ); + + const testAmountInCashDecimals = toSmallestDenomination(testAmount); + + expect(testAmountInSatoshis).toStrictEqual(12345678); + expect(testAmountInCashDecimals).toStrictEqual( + new BigNumber(testAmountInSatoshis), + ); + }); }); diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -406,6 +406,30 @@ return link; }; + 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 cashDecimals decimal places + 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; + }; + const sendBch = async ( BCH, wallet, @@ -440,7 +464,15 @@ transactionBuilder = new BCH.TransactionBuilder(); else transactionBuilder = new BCH.TransactionBuilder('testnet'); - const satoshisToSend = BCH.BitcoinCash.toSatoshi(value.toFixed(8)); + const satoshisToSend = toSmallestDenomination(value); + + // Throw validation error if toSmallestDenomination returns false + if (!satoshisToSend) { + const error = new Error( + `Invalid decimal places for send amount`, + ); + throw error; + } let originalAmount = new BigNumber(0); let txFee = 0; for (let i = 0; i < utxos.length; i++) { @@ -574,6 +606,7 @@ getSlpBalancesAndUtxos, getTxHistory, getRestUrl, + toSmallestDenomination, sendBch, sendToken, };