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 @@ -672,4 +672,120 @@ expectedPubKey, ); }); + + it(`generateTxInput() returns an input object for a valid one to one XEC tx `, async () => { + const { generateTxInput } = useBCH(); + const BCH = new BCHJS(); + const isOneToMany = false; + const utxos = mockReturnGetSlpBalancesAndUtxos.nonSlpUtxos; + let txBuilder = new BCH.TransactionBuilder(); + const destinationAddressAndValueArray = null; + const satoshisToSend = new BigNumber(2184); + const feeInSatsPerByte = currency.defaultFee; + + const inputObj = generateTxInput( + BCH, + isOneToMany, + utxos, + txBuilder, + destinationAddressAndValueArray, + satoshisToSend, + feeInSatsPerByte, + ); + expect(inputObj.txBuilder).not.toStrictEqual(null); + expect(inputObj.totalInputUtxoValue).toStrictEqual( + new BigNumber(701000), + ); + expect(inputObj.txFee).toStrictEqual(752); + expect(inputObj.inputUtxos.length).not.toStrictEqual(0); + }); + + it(`generateTxInput() returns an input object for a valid one to many XEC tx `, async () => { + const { generateTxInput } = useBCH(); + const BCH = new BCHJS(); + const isOneToMany = true; + const utxos = mockReturnGetSlpBalancesAndUtxos.nonSlpUtxos; + let txBuilder = new BCH.TransactionBuilder(); + const destinationAddressAndValueArray = [ + 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000', + 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000', + 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000', + ]; + const satoshisToSend = new BigNumber(900000); + const feeInSatsPerByte = currency.defaultFee; + + const inputObj = generateTxInput( + BCH, + isOneToMany, + utxos, + txBuilder, + destinationAddressAndValueArray, + satoshisToSend, + feeInSatsPerByte, + ); + expect(inputObj.txBuilder).not.toStrictEqual(null); + expect(inputObj.totalInputUtxoValue).toStrictEqual( + new BigNumber(1401000), + ); + expect(inputObj.txFee).toStrictEqual(1186); + expect(inputObj.inputUtxos.length).not.toStrictEqual(0); + }); + + it(`generateTxInput() throws error for a one to many XEC tx with invalid destinationAddressAndValueArray input`, async () => { + const { generateTxInput } = useBCH(); + const BCH = new BCHJS(); + const isOneToMany = true; + const utxos = mockReturnGetSlpBalancesAndUtxos.nonSlpUtxos; + let txBuilder = new BCH.TransactionBuilder(); + const destinationAddressAndValueArray = null; // invalid since isOneToMany is true + const satoshisToSend = new BigNumber(900000); + const feeInSatsPerByte = currency.defaultFee; + + let thrownError; + try { + generateTxInput( + BCH, + isOneToMany, + utxos, + txBuilder, + destinationAddressAndValueArray, + satoshisToSend, + feeInSatsPerByte, + ); + } catch (err) { + thrownError = err; + } + expect(thrownError.message).toStrictEqual('Invalid tx input parameter'); + }); + + it(`generateTxInput() throws error for a one to many XEC tx with invalid utxos input`, async () => { + const { generateTxInput } = useBCH(); + const BCH = new BCHJS(); + const isOneToMany = true; + const utxos = null; + let txBuilder = new BCH.TransactionBuilder(); + const destinationAddressAndValueArray = [ + 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000', + 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000', + 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000', + ]; + const satoshisToSend = new BigNumber(900000); + const feeInSatsPerByte = currency.defaultFee; + + let thrownError; + try { + generateTxInput( + BCH, + isOneToMany, + utxos, + txBuilder, + destinationAddressAndValueArray, + satoshisToSend, + feeInSatsPerByte, + ); + } catch (err) { + thrownError = err; + } + expect(thrownError.message).toStrictEqual('Invalid tx input parameter'); + }); }); 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 @@ -1395,6 +1395,67 @@ return encryptedEj; }; + const generateTxInput = ( + BCH, + isOneToMany, + utxos, + txBuilder, + destinationAddressAndValueArray, + satoshisToSend, + feeInSatsPerByte, + ) => { + let txInputObj = {}; + const inputUtxos = []; + let txFee = 0; + let totalInputUtxoValue = new BigNumber(0); + try { + if ( + !BCH || + (isOneToMany && !destinationAddressAndValueArray) || + !utxos || + !txBuilder || + !satoshisToSend || + !feeInSatsPerByte + ) { + throw new Error('Invalid tx input parameter'); + } + + // A normal tx will have 2 outputs, destination and change + // A one to many tx will have n outputs + 1 change output, where n is the number of recipients + const txOutputs = isOneToMany + ? destinationAddressAndValueArray.length + 1 + : 2; + for (let i = 0; i < utxos.length; i++) { + const utxo = utxos[i]; + totalInputUtxoValue = totalInputUtxoValue.plus(utxo.value); + const vout = utxo.vout; + const txid = utxo.txid; + // add input with txid and index of vout + txBuilder.addInput(txid, vout); + + inputUtxos.push(utxo); + txFee = calcFee(BCH, inputUtxos, txOutputs, feeInSatsPerByte); + + if ( + totalInputUtxoValue + .minus(satoshisToSend) + .minus(txFee) + .gte(0) + ) { + break; + } + } + } catch (err) { + console.log(`generateTxInput() error: ` + err); + throw err; + } + txInputObj.txBuilder = txBuilder; + txInputObj.totalInputUtxoValue = totalInputUtxoValue; + txInputObj.inputUtxos = inputUtxos; + txInputObj.txFee = txFee; + return txInputObj; + }; + const sendXec = async ( BCH, wallet, @@ -1730,5 +1791,6 @@ handleEncryptedOpReturn, getRecipientPublicKey, burnEtoken, + generateTxInput, }; }