diff --git a/web/cashtab/src/utils/__mocks__/mockInputUtxos.js b/web/cashtab/src/utils/__mocks__/mockInputUtxos.js new file mode 100644 --- /dev/null +++ b/web/cashtab/src/utils/__mocks__/mockInputUtxos.js @@ -0,0 +1,53 @@ +export const mockSingleInputUtxo = [ + { + height: 669639, + tx_hash: + '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + tx_pos: 0, + value: 1000, + txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, +]; + +export const mockMultipleInputUtxos = [ + { + height: 669639, + tx_hash: + '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + tx_pos: 0, + value: 1000, + txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + 'ddace66ea968e16e55ebf218814401acc38e0a39150529fa3d1108af04e81373', + tx_pos: 0, + value: 300000, + txid: 'ddace66ea968e16e55ebf218814401acc38e0a39150529fa3d1108af04e81373', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, + { + height: 669639, + tx_hash: + 'f1147285ac384159b5dfae513bda47a0459f876d046b48f13c8a7ec4f0d20d96', + tx_pos: 0, + value: 700000, + txid: 'f1147285ac384159b5dfae513bda47a0459f876d046b48f13c8a7ec4f0d20d96', + vout: 0, + isValid: false, + address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', + wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', + }, +]; diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js --- a/web/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js @@ -28,6 +28,8 @@ parseChronikTx, checkWalletForTokenInfo, isActiveWebsocket, + signAndBuildTx, + toSmallestDenomination, } from 'utils/cashMethods'; import { currency } from 'components/Common/Ticker'; import { @@ -161,7 +163,8 @@ incrementallyHydratedUtxosAfterProcessingOneMissing, } from '../__mocks__/incrementalUtxoMocks'; import mockLegacyWallets from 'hooks/__mocks__/mockLegacyWallets'; - +import BCHJS from '@psf/bch-js'; +import BigNumber from 'bignumber.js'; import { lambdaHash160s, lambdaIncomingXecTx, @@ -172,6 +175,99 @@ disconnectedWebsocketAlpha, unsubscribedWebsocket, } from '../__mocks__/chronikWs'; +import { + mockSingleInputUtxo, + mockMultipleInputUtxos, +} from '../__mocks__/mockInputUtxos'; + +it(`signAndBuildTx() successfully returns a raw tx hex for a single input utxo and single output utxo`, () => { + // txbuilder output params + const BCH = new BCHJS(); + let txBuilder = new BCH.TransactionBuilder(); + + // mock txBuilder inputs + txBuilder.addInput( + mockSingleInputUtxo[0].txid, + mockSingleInputUtxo[0].vout, + ); + // mock txBuilder output + txBuilder.addOutput( + mockSingleInputUtxo[0].address, + parseInt(toSmallestDenomination(new BigNumber(currency.dustSats))), + ); + + const rawTxHex = signAndBuildTx(BCH, mockSingleInputUtxo, txBuilder); + expect(rawTxHex).toStrictEqual( + '0200000001582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006b483045022100b04fce11f536f6ec9ae93016cb5bde7b5118f04ad37b79fb5b6ffffd6df4d3db022071246042531338ceb4168e9d8852f38f8dcb65aba0105ca84142b5d829ece5b741210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff01d8d60000000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac00000000', + ); +}); + +it(`signAndBuildTx() successfully returns a raw tx hex for multiple input utxos with a single output utxo`, () => { + // txbuilder output params + const BCH = new BCHJS(); + let txBuilder = new BCH.TransactionBuilder(); + + // mock txBuilder inputs + for (let i = 0; i < mockMultipleInputUtxos.length; i++) { + txBuilder.addInput( + mockMultipleInputUtxos[i].txid, + mockMultipleInputUtxos[i].vout, + ); + } + + // mock txBuilder output + txBuilder.addOutput( + mockMultipleInputUtxos[0].address, + parseInt(toSmallestDenomination(new BigNumber(currency.dustSats))), + ); + + const rawTxHex = signAndBuildTx(BCH, mockMultipleInputUtxos, txBuilder); + expect(rawTxHex).toStrictEqual( + '0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a473044022045fd5fe9abd792dbe4192291b1e26c711691b5734522ff6decfc8ca0805c2824022042ddd0cfae30e2d95bcdc80aac697f7007f4c14d488a54ce8d97e461d7b363e241210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006b483045022100f5562104ff85aeb934184fb9bae551337d6de7cf73f31b17b75629d035ec8f0602206ba2e9fd6e141b7571465058d5c7b5dcece020f0f8c943f9c510a706e11bb40241210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006b483045022100d08609b29696086276cb0d8871f40a84f78e037f1e04d188e4f5b0de091ef2e7022001c5573990e9ff4a05b1a74fa49da1996962e2ccc404163efb493ef2a5a6274e41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff01d8d60000000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac00000000', + ); +}); + +it(`signAndBuildTx() successfully returns a raw tx hex for multiple input utxos with multiple output utxos`, () => { + // txbuilder output params + const BCH = new BCHJS(); + let txBuilder = new BCH.TransactionBuilder(); + + // mock txBuilder inputs + for (let i = 0; i < mockMultipleInputUtxos.length; i++) { + txBuilder.addInput( + mockMultipleInputUtxos[i].txid, + mockMultipleInputUtxos[i].vout, + ); + } + + // mock txBuilder output + for (let j = 0; j < mockMultipleInputUtxos.length; j++) { + txBuilder.addOutput( + mockMultipleInputUtxos[j].address, + parseInt(toSmallestDenomination(new BigNumber(currency.dustSats))), + ); + } + + const rawTxHex = signAndBuildTx(BCH, mockMultipleInputUtxos, txBuilder); + expect(rawTxHex).toStrictEqual( + '0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006b483045022100fb2e4954c4e11d5b4930cdfe69ca7f12e8b55c42ac59752b922e179f5ed994540220545e07ee799dddf7127eb304ccf595768f32d146bbcc338236931f01d5085be341210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006b483045022100f588ccc6c25a3d8e9c2bea65a85a03650e2cbc338a24cb898f187fbe39e4def902207443c3ef4e1d307d56fb642e2d32627648f491c82990586141edf83f63ba119141210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006a47304402200b8f2bf2c90b2e1ef8a18fe1c7cd99e0271c17e67e714a0fad7305e4f2d122ac02205c68b7d136c9317895fe724888248f62cfce279b56a82c9db50d3821f21ceda441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff03d8d60000000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88acd8d60000000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88acd8d60000000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac00000000', + ); +}); + +it(`signAndBuildTx() throws error on invalid input params`, () => { + // txbuilder output params + const BCH = new BCHJS(); + let txBuilder = new BCH.TransactionBuilder(); + const inputUtxo = null; // invalid input param + + let thrownError; + try { + signAndBuildTx(BCH, inputUtxo, txBuilder); + } catch (err) { + thrownError = err; + } + expect(thrownError.message).toStrictEqual('Invalid buildTx parameter'); +}); describe('Correctly executes cash utility functions', () => { it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => { diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js --- a/web/cashtab/src/utils/cashMethods.js +++ b/web/cashtab/src/utils/cashMethods.js @@ -8,6 +8,31 @@ import BigNumber from 'bignumber.js'; import cashaddr from 'ecashaddrjs'; +export const signAndBuildTx = (BCH, inputUtxos, txBuilder) => { + if (!BCH || !inputUtxos || !txBuilder) { + throw new Error('Invalid buildTx parameter'); + } + + // Sign the transactions with the HD node. + for (let i = 0; i < inputUtxos.length; i++) { + const utxo = inputUtxos[i]; + txBuilder.sign( + i, + BCH.ECPair.fromWIF(utxo.wif), + undefined, + txBuilder.hashTypes.SIGHASH_ALL, + utxo.value, + ); + } + + // build tx + const tx = txBuilder.build(); + // output rawhex + const hex = tx.toHex(); + + return hex; +}; + export function parseOpReturn(hexStr) { if ( !hexStr ||