diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js --- a/web/cashtab/src/components/Common/Ticker.js +++ b/web/cashtab/src/components/Common/Ticker.js @@ -3,6 +3,13 @@ import cashaddr from 'cashaddrjs'; import BigNumber from 'bignumber.js'; +export const feeLevels = { + minimum: 1.01, + normal: 3.01, + express: 5.01, + ludicrous: 83.3, +}; + export const currency = { name: 'Bitcoin ABC', ticker: 'BCHA', @@ -10,7 +17,7 @@ legacyPrefix: 'bitcoincash', prefixes: ['bitcoincash', 'ecash'], coingeckoId: 'bitcoin-cash-abc-2', - defaultFee: 5.01, + defaultFee: feeLevels.express, // Must select from feeLevels object dust: '0.00000546', // The minimum amount of BCHA that can be sent by the app cashDecimals: 8, blockExplorerUrl: 'https://explorer.bitcoinabc.org', diff --git a/web/cashtab/src/hooks/__mocks__/sendBCH.js b/web/cashtab/src/hooks/__mocks__/sendBCH.js --- a/web/cashtab/src/hooks/__mocks__/sendBCH.js +++ b/web/cashtab/src/hooks/__mocks__/sendBCH.js @@ -27,18 +27,36 @@ }, addresses: ['bitcoincash:qr2npxqwznhp7gphatcqzexeclx0hhwdxg386ez36n'], values: ['0.00000546'], + expectedFeeOutputs: { + minimum: { + expectedHex: [ + '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006b483045022100d52237ac2c000c0be195bb27d5488b378559cf4a7958c9a1b1ce7a6850773a2b02202e3c148450c6efdb11f199a9398332b313b54686ff98cf74b254f9ae144b0c7d4121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0222020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288ac62ff0100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000', + ], + expectedOneOutputFee: 229, + expectedTwoOutputFee: 378, + }, + normal: { + expectedHex: [ + '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006b483045022100ea9661be764a02a74c8f5511becd65436bc358cbfe84e9c9be25e359747cecc102204d757b9a1175eb958cc93dc83a29c2c1fc46f95afb58674b9d677776ab7c18d94121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0222020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288ac9efd0100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000', + ], + expectedOneOutputFee: 681, + expectedTwoOutputFee: 1126, + }, + express: { + expectedHex: [ + '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006a473044022048f8a04db2fb6d83de621c5d77243e14f94be62b9f97d5fb391a1587f04cee2b02204a406baa3f08256591eca9bb24434946effaec33214298cf6a41638985f6515d4121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0222020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288acdafb0100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000', + ], + expectedOneOutputFee: 1133, + expectedTwoOutputFee: 1874, + }, + ludicrous: { + expectedHex: [ + '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006b483045022100b71662c5187dfc0df8ee0e16bb06aedae38714dbe8855abd83c96228e49fe8ca0220032313e0481141eac90e382c6f780b8bb0c44e9ceaa977eb3adee933cbfb28034121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0222020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288acbdb60100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000', + ], + expectedOneOutputFee: 18826, + expectedTwoOutputFee: 31155, + }, + }, expectedTxId: '7a39961bbd7e27d804fb3169ef38a83234710fbc53897a4eb0c98454854a26d1', - expectedHex: [ - '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006b483045022100d52237ac2c000c0be195bb27d5488b378559cf4a7958c9a1b1ce7a6850773a2b02202e3c148450c6efdb11f199a9398332b313b54686ff98cf74b254f9ae144b0c7d4121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0222020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288ac62ff0100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000', - ], - expectedHexThreeSatPerByteFee: [ - '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006b483045022100ea9661be764a02a74c8f5511becd65436bc358cbfe84e9c9be25e359747cecc102204d757b9a1175eb958cc93dc83a29c2c1fc46f95afb58674b9d677776ab7c18d94121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0222020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288ac9efd0100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000', - ], - expectedHexFiveSatPerByteFee: [ - '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006a473044022048f8a04db2fb6d83de621c5d77243e14f94be62b9f97d5fb391a1587f04cee2b02204a406baa3f08256591eca9bb24434946effaec33214298cf6a41638985f6515d4121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0222020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288acdafb0100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000', - ], - expectedHexEightyThreeSatPerByteFee: [ - '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006b483045022100b71662c5187dfc0df8ee0e16bb06aedae38714dbe8855abd83c96228e49fe8ca0220032313e0481141eac90e382c6f780b8bb0c44e9ceaa977eb3adee933cbfb28034121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0222020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288acbdb60100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000', - ], }; diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js --- a/web/cashtab/src/hooks/__tests__/useBCH.test.js +++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js @@ -5,10 +5,34 @@ import mockReturnGetSlpBalancesAndUtxos from '../__mocks__/mockReturnGetSlpBalancesAndUtxos'; import sendBCHMock from '../__mocks__/sendBCH'; import BCHJS from '@psf/bch-js'; // TODO: should be removed when external lib not needed anymore -import { currency } from '../../components/Common/Ticker'; +import { currency, feeLevels } from '../../components/Common/Ticker'; import sendBCH from '../__mocks__/sendBCH'; import BigNumber from 'bignumber.js'; +// Determine constants that depend on Cashtab fee setting +let mockPointer; +switch (currency.defaultFee) { + case feeLevels.minimum: + mockPointer = 'minimum'; + break; + case feeLevels.normal: + mockPointer = 'normal'; + break; + case feeLevels.express: + mockPointer = 'express'; + break; + case feeLevels.ludicrous: + mockPointer = 'ludicrous'; + break; + default: + mockPointer = 'normal'; +} +const { + expectedHex, + expectedOneOutputFee, + expectedTwoOutputFee, +} = sendBCHMock.expectedFeeOutputs[mockPointer]; + describe('useBCH hook', () => { it('gets Rest Api Url on testnet', () => { process = { @@ -42,16 +66,16 @@ const { calcFee } = useBCH(); const BCH = new BCHJS(); const utxosMock = [{}, {}]; - // For 1.01 sat/byte fee - let expectedTxFee = 378; - if (currency.defaultFee === 3.01) { - expectedTxFee = 1126; - } else if (currency.defaultFee === 5.01) { - expectedTxFee = 1874; - } else if ((currency.defaultFee = 83.3)) { - expectedTxFee = 31155; - } - expect(calcFee(BCH, utxosMock)).toBe(expectedTxFee); + + expect(calcFee(BCH, utxosMock)).toBe(expectedTwoOutputFee); + }); + + it('calculates fee correctly for 1 P2PKH output', () => { + const { calcFee } = useBCH(); + const BCH = new BCHJS(); + const utxosMock = [{}]; + + expect(calcFee(BCH, utxosMock)).toBe(expectedOneOutputFee); }); it('gets SLP and BCH balances and utxos from hydrated utxo details', async () => { @@ -67,22 +91,8 @@ it('sends BCH correctly', async () => { const { sendBch } = useBCH(); const BCH = new BCHJS(); - const { - expectedTxId, - expectedHex, - utxos, - wallet, - addresses, - values, - } = sendBCHMock; - let expectedHexByFee = expectedHex; - if (currency.defaultFee === 3.01) { - expectedHexByFee = sendBCHMock.expectedHexThreeSatPerByteFee; - } else if (currency.defaultFee === 5.01) { - expectedHexByFee = sendBCHMock.expectedHexFiveSatPerByteFee; - } else if (currency.defaultFee === 83.3) { - expectedHexByFee = sendBCHMock.expectedHexEightyThreeSatPerByteFee; - } + const { expectedTxId, utxos, wallet, addresses, values } = sendBCHMock; + BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); @@ -90,7 +100,7 @@ `${currency.blockExplorerUrl}/tx/${expectedTxId}`, ); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( - expectedHexByFee, + expectedHex, ); }); @@ -98,22 +108,8 @@ const { sendBch } = useBCH(); const BCH = new BCHJS(); const callback = jest.fn(); - const { - expectedTxId, - expectedHex, - utxos, - wallet, - addresses, - values, - } = sendBCHMock; - let expectedHexByFee = expectedHex; - if (currency.defaultFee === 3.01) { - expectedHexByFee = sendBCHMock.expectedHexThreeSatPerByteFee; - } else if (currency.defaultFee === 5.01) { - expectedHexByFee = sendBCHMock.expectedHexFiveSatPerByteFee; - } else if (currency.defaultFee === 83.3) { - expectedHexByFee = sendBCHMock.expectedHexEightyThreeSatPerByteFee; - } + const { expectedTxId, utxos, wallet, addresses, values } = sendBCHMock; + BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); @@ -121,27 +117,28 @@ await sendBch(BCH, wallet, utxos, { addresses, values }, callback), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( - expectedHexByFee, + expectedHex, ); expect(callback).toHaveBeenCalledWith(expectedTxId); }); - it('sends BCH with less BCH available on balance', async () => { + it(`Throws error if called trying to send one satoshi ${currency.ticker} more than available in utxo set`, async () => { const { sendBch } = useBCH(); const BCH = new BCHJS(); - const { - expectedTxId, - expectedHex, - utxos, - wallet, - addresses, - } = sendBCHMock; + const { expectedTxId, utxos, wallet, addresses } = sendBCHMock; + BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); + const oneBaseUnitMoreThanBalance = new BigNumber(utxos[0].value) + .minus(expectedOneOutputFee) + .plus(1) + .div(10 ** currency.cashDecimals) + .toString(); + const failedSendBch = sendBch(BCH, wallet, utxos, { addresses, - values: [1], + values: [oneBaseUnitMoreThanBalance], }); expect(failedSendBch).rejects.toThrow(new Error('Insufficient funds')); const nullValuesSendBch = await sendBch(BCH, wallet, utxos, {