From 2bc8246fc49312b8e1ad5ef6f51e0f088d868186 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 3 Apr 2024 14:57:34 +0100 Subject: [PATCH] wallet: add utxo filter function. Allows to attach a utxo filter function when creating a transaction funded by the internal wallet. --- wallet/createtx.go | 17 ++++++++++++++--- wallet/createtx_test.go | 16 +++++++++------- wallet/wallet.go | 19 ++++++++++++++++--- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/wallet/createtx.go b/wallet/createtx.go index d534ae96d3..9c2b061984 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -140,7 +140,9 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, coinSelectKeyScope, changeKeyScope *waddrmgr.KeyScope, account uint32, minconf int32, feeSatPerKb btcutil.Amount, strategy CoinSelectionStrategy, dryRun bool, - selectedUtxos []wire.OutPoint) (*txauthor.AuthoredTx, error) { + selectedUtxos []wire.OutPoint, + allowUtxo func(utxo wtxmgr.Credit) bool) ( + *txauthor.AuthoredTx, error) { chainClient, err := w.requireChainClient() if err != nil { @@ -168,7 +170,8 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, } eligible, err := w.findEligibleOutputs( - dbtx, coinSelectKeyScope, account, minconf, bs, + dbtx, coinSelectKeyScope, account, minconf, + bs, allowUtxo, ) if err != nil { return err @@ -322,7 +325,8 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, keyScope *waddrmgr.KeyScope, account uint32, minconf int32, - bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { + bs *waddrmgr.BlockStamp, + allowUtxo func(utxo wtxmgr.Credit) bool) ([]wtxmgr.Credit, error) { addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) @@ -341,6 +345,13 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, for i := range unspent { output := &unspent[i] + // Restrict the selected utxos if a filter function is provided. + if allowUtxo != nil && + !allowUtxo(*output) { + + continue + } + // Only include this output if it meets the required number of // confirmations. Coinbase transactions must have reached // maturity before their outputs may be spent. diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go index bb351a1fc4..c0badf35d7 100644 --- a/wallet/createtx_test.go +++ b/wallet/createtx_test.go @@ -28,6 +28,8 @@ var ( "02a4", ) testBlockHeight int32 = 276425 + + alwaysAllowUtxo = func(utxo wtxmgr.Credit) bool { return true } ) // TestTxToOutput checks that no new address is added to he database if we @@ -79,7 +81,7 @@ func TestTxToOutputsDryRun(t *testing.T) { // database us not inflated. dryRunTx, err := w.txToOutputs( txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, true, - nil, + nil, alwaysAllowUtxo, ) if err != nil { t.Fatalf("unable to author tx: %v", err) @@ -97,7 +99,7 @@ func TestTxToOutputsDryRun(t *testing.T) { dryRunTx2, err := w.txToOutputs( txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, true, - nil, + nil, alwaysAllowUtxo, ) if err != nil { t.Fatalf("unable to author tx: %v", err) @@ -133,7 +135,7 @@ func TestTxToOutputsDryRun(t *testing.T) { // to the database. tx, err := w.txToOutputs( txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, false, - nil, + nil, alwaysAllowUtxo, ) if err != nil { t.Fatalf("unable to author tx: %v", err) @@ -283,7 +285,7 @@ func TestTxToOutputsRandom(t *testing.T) { createTx := func() *txauthor.AuthoredTx { tx, err := w.txToOutputs( txOuts, nil, nil, 0, 1, feeSatPerKb, - CoinSelectionRandom, true, nil, + CoinSelectionRandom, true, nil, alwaysAllowUtxo, ) require.NoError(t, err) return tx @@ -355,7 +357,7 @@ func TestCreateSimpleCustomChange(t *testing.T) { } tx1, err := w.txToOutputs( []*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000, - CoinSelectionLargest, true, nil, + CoinSelectionLargest, true, nil, alwaysAllowUtxo, ) require.NoError(t, err) @@ -381,7 +383,7 @@ func TestCreateSimpleCustomChange(t *testing.T) { tx2, err := w.txToOutputs( []*wire.TxOut{targetTxOut}, &waddrmgr.KeyScopeBIP0086, &waddrmgr.KeyScopeBIP0084, 0, 1, 1000, CoinSelectionLargest, - true, nil, + true, nil, alwaysAllowUtxo, ) require.NoError(t, err) @@ -465,7 +467,7 @@ func TestSelectUtxosTxoToOutpoint(t *testing.T) { } tx1, err := w.txToOutputs( []*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000, - CoinSelectionLargest, true, selectUtxos, + CoinSelectionLargest, true, selectUtxos, alwaysAllowUtxo, ) require.NoError(t, err) diff --git a/wallet/wallet.go b/wallet/wallet.go index 7356348cc1..362ca9b787 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1201,6 +1201,7 @@ type ( dryRun bool resp chan createTxResponse selectUtxos []wire.OutPoint + allowUtxo func(wtxmgr.Credit) bool } createTxResponse struct { tx *txauthor.AuthoredTx @@ -1239,9 +1240,10 @@ out: } tx, err := w.txToOutputs( - txr.outputs, txr.coinSelectKeyScope, txr.changeKeyScope, - txr.account, txr.minconf, txr.feeSatPerKB, - txr.coinSelectionStrategy, txr.dryRun, txr.selectUtxos, + txr.outputs, txr.coinSelectKeyScope, + txr.changeKeyScope, txr.account, txr.minconf, + txr.feeSatPerKB, txr.coinSelectionStrategy, + txr.dryRun, txr.selectUtxos, txr.allowUtxo, ) release() @@ -1259,6 +1261,7 @@ out: type txCreateOptions struct { changeKeyScope *waddrmgr.KeyScope selectUtxos []wire.OutPoint + allowUtxo func(wtxmgr.Credit) bool } // TxCreateOption is a set of optional arguments to modify the tx creation @@ -1289,6 +1292,15 @@ func WithCustomSelectUtxos(utxos []wire.OutPoint) TxCreateOption { } } +// WithUtxoFilter is used to restrict the selection of the internal wallet +// inputs by further external conditions. Utxos which pass the filter are +// considered when creating the transaction. +func WithUtxoFilter(allowUtxo func(utxo wtxmgr.Credit) bool) TxCreateOption { + return func(opts *txCreateOptions) { + opts.allowUtxo = allowUtxo + } +} + // CreateSimpleTx creates a new signed transaction spending unspent outputs with // at least minconf confirmations spending to any number of address/amount // pairs. Only unspent outputs belonging to the given key scope and account will @@ -1333,6 +1345,7 @@ func (w *Wallet) CreateSimpleTx(coinSelectKeyScope *waddrmgr.KeyScope, dryRun: dryRun, resp: make(chan createTxResponse), selectUtxos: opts.selectUtxos, + allowUtxo: opts.allowUtxo, } w.createTxRequests <- req resp := <-req.resp