diff --git a/cashtab/src/transactions/__tests__/index.test.js b/cashtab/src/transactions/__tests__/index.test.js --- a/cashtab/src/transactions/__tests__/index.test.js +++ b/cashtab/src/transactions/__tests__/index.test.js @@ -1,11 +1,16 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -import { sendXec, getMultisendTargetOutputs } from 'transactions'; +import { + sendXec, + getMultisendTargetOutputs, + ignoreUnspendableUtxos, +} from 'transactions'; import { MockChronikClient } from '../../../../apps/mock-chronik-client'; import { sendXecVectors, getMultisendTargetOutputsVectors, + ignoreUnspendableUtxosVectors, } from '../fixtures/vectors'; describe('Improved Cashtab transaction broadcasting function', () => { @@ -75,3 +80,23 @@ }); }); }); + +describe('Ignore unspendable coinbase utxos', () => { + // Unit test for each vector in fixtures for the ignoreUnspendableUtxos case + const { expectedReturns } = ignoreUnspendableUtxosVectors; + + // Successfully built and broadcast txs + expectedReturns.forEach(async formedOutput => { + const { + description, + unfilteredUtxos, + chaintipBlockheight, + spendableUtxos, + } = formedOutput; + it(`ignoreUnspendableUtxos: ${description}`, () => { + expect( + ignoreUnspendableUtxos(unfilteredUtxos, chaintipBlockheight), + ).toStrictEqual(spendableUtxos); + }); + }); +}); diff --git a/cashtab/src/transactions/fixtures/vectors.js b/cashtab/src/transactions/fixtures/vectors.js --- a/cashtab/src/transactions/fixtures/vectors.js +++ b/cashtab/src/transactions/fixtures/vectors.js @@ -398,3 +398,72 @@ }, ], }; + +export const ignoreUnspendableUtxosVectors = { + expectedReturns: [ + { + description: 'Array with no coinbase utxos returned unchanged', + chaintipBlockheight: 800000, + unfilteredUtxos: [ + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + ], + spendableUtxos: [ + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + ], + }, + { + description: + 'Array with immature coinbase utxo returned without immature coinbase utxo', + chaintipBlockheight: 800000, + unfilteredUtxos: [ + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: true, blockHeight: 800000 }, + ], + spendableUtxos: [ + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + ], + }, + { + description: + 'Array with some immature coinbase utxos and some mature coinbase utxos returned without immature coinbase utxo', + chaintipBlockheight: 800000, + unfilteredUtxos: [ + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: true, blockHeight: 799900 }, // just mature + { isCoinbase: true, blockHeight: 800000 }, // immature + { isCoinbase: true, blockHeight: 799901 }, // immature + { isCoinbase: true, blockHeight: 799999 }, // immature + ], + spendableUtxos: [ + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: true, blockHeight: 799900 }, // just mature + ], + }, + { + description: + 'If blockheight is zero, all coinbase utxos are removed', + chaintipBlockheight: 0, + unfilteredUtxos: [ + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: true, blockHeight: 800000 }, + { isCoinbase: true, blockHeight: 800000 }, + { isCoinbase: true, blockHeight: 800000 }, + ], + spendableUtxos: [ + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + { isCoinbase: false, blockHeight: 800000 }, + ], + }, + ], +}; diff --git a/cashtab/src/transactions/index.js b/cashtab/src/transactions/index.js --- a/cashtab/src/transactions/index.js +++ b/cashtab/src/transactions/index.js @@ -57,6 +57,9 @@ // Use only eCash utxos const utxos = wallet.state.nonSlpUtxos; + // Ignore immature coinbase utxos + // TODO implement ignoreUnspendableUtxos here + let { inputs, outputs } = coinSelect(utxos, targetOutputs, feeRate); // Initialize TransactionBuilder @@ -130,3 +133,25 @@ } return targetOutputs; }; + +/** + * Ignore coinbase utxos that do not have enough confirmations to be spendable + * TODO cache blockheight so you can ignore only unspendable coinbase utxos + * @param {array} unfilteredUtxos an array of chronik utxo objects + * @returns {array} an array of utxos without coinbase utxos + */ +export const ignoreUnspendableUtxos = ( + unfilteredUtxos, + chaintipBlockheight, +) => { + const COINBASE_REQUIRED_CONFS_TO_SPEND = 100; + return unfilteredUtxos.filter(unfilteredUtxo => { + return ( + unfilteredUtxo.isCoinbase === false || + (unfilteredUtxo.isCoinbase === true && + chaintipBlockheight >= + unfilteredUtxo.blockHeight + + COINBASE_REQUIRED_CONFS_TO_SPEND) + ); + }); +};