Skip to content

Commit

Permalink
wallet: add utxo filter function.
Browse files Browse the repository at this point in the history
Allows to attach a utxo filter function when creating a transaction
funded by the internal wallet.
  • Loading branch information
ziggie1984 committed Apr 3, 2024
1 parent 9a7dd24 commit 2a4a7d7
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 43 deletions.
17 changes: 14 additions & 3 deletions wallet/createtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down
16 changes: 9 additions & 7 deletions wallet/createtx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions wallet/txauthor/author.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package txauthor

import (
"errors"
"fmt"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
Expand Down Expand Up @@ -104,6 +105,7 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount,
if err != nil {
return nil, err
}
fmt.Println("FEE", inputAmount, targetAmount, targetFee)
if inputAmount < targetAmount+targetFee {
return nil, insufficientFundsError{}
}
Expand Down
19 changes: 16 additions & 3 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,7 @@ type (
dryRun bool
resp chan createTxResponse
selectUtxos []wire.OutPoint
allowUtxo func(wtxmgr.Credit) bool
}
createTxResponse struct {
tx *txauthor.AuthoredTx
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -1289,6 +1292,15 @@ func WithCustomSelectUtxos(utxos []wire.OutPoint) TxCreateOption {
}
}

// WithFilterUtxo 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
Expand Down Expand Up @@ -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
Expand Down
66 changes: 36 additions & 30 deletions wtxmgr/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ func valueTxRecord(rec *TxRecord) ([]byte, error) {
copy(v[8:], rec.SerializedTx)
}
byteOrder.PutUint64(v, uint64(rec.Received.Unix()))

// Add the unstable flag here via TLV records.

return v, nil
}

Expand Down Expand Up @@ -434,6 +437,9 @@ func readRawTxRecord(txHash *chainhash.Hash, v []byte, rec *TxRecord) error {
bucketTxRecords, txHash)
return storeError(ErrData, str, err)
}

// Fetch the tlv record of the unstable record.

return nil
}

Expand Down Expand Up @@ -680,21 +686,21 @@ func deleteRawCredit(ns walletdb.ReadWriteBucket, k []byte) error {
//
// Example usage:
//
// prefix := keyTxRecord(txHash, block)
// it := makeCreditIterator(ns, prefix)
// for it.next() {
// // Use it.elem
// // If necessary, read additional details from it.ck, it.cv
// }
// if it.err != nil {
// // Handle error
// }
// prefix := keyTxRecord(txHash, block)
// it := makeCreditIterator(ns, prefix)
// for it.next() {
// // Use it.elem
// // If necessary, read additional details from it.ck, it.cv
// }
// if it.err != nil {
// // Handle error
// }
//
// The elem's Spent field is not set to true if the credit is spent by an
// unmined transaction. To check for this case:
//
// k := canonicalOutPoint(&txHash, it.elem.Index)
// it.elem.Spent = existsRawUnminedInput(ns, k) != nil
// k := canonicalOutPoint(&txHash, it.elem.Index)
// it.elem.Spent = existsRawUnminedInput(ns, k) != nil
type creditIterator struct {
c walletdb.ReadWriteCursor // Set to nil after final iteration
prefix []byte
Expand Down Expand Up @@ -920,15 +926,15 @@ func deleteRawDebit(ns walletdb.ReadWriteBucket, k []byte) error {
//
// Example usage:
//
// prefix := keyTxRecord(txHash, block)
// it := makeDebitIterator(ns, prefix)
// for it.next() {
// // Use it.elem
// // If necessary, read additional details from it.ck, it.cv
// }
// if it.err != nil {
// // Handle error
// }
// prefix := keyTxRecord(txHash, block)
// it := makeDebitIterator(ns, prefix)
// for it.next() {
// // Use it.elem
// // If necessary, read additional details from it.ck, it.cv
// }
// if it.err != nil {
// // Handle error
// }
type debitIterator struct {
c walletdb.ReadWriteCursor // Set to nil after final iteration
prefix []byte
Expand Down Expand Up @@ -1097,22 +1103,22 @@ func deleteRawUnminedCredit(ns walletdb.ReadWriteBucket, k []byte) error {
// unminedCreditIterator allows for cursor iteration over all credits, in order,
// from a single unmined transaction.
//
// Example usage:
// Example usage:
//
// it := makeUnminedCreditIterator(ns, txHash)
// for it.next() {
// // Use it.elem, it.ck and it.cv
// // Optionally, use it.delete() to remove this k/v pair
// }
// if it.err != nil {
// // Handle error
// }
// it := makeUnminedCreditIterator(ns, txHash)
// for it.next() {
// // Use it.elem, it.ck and it.cv
// // Optionally, use it.delete() to remove this k/v pair
// }
// if it.err != nil {
// // Handle error
// }
//
// The spentness of the credit is not looked up for performance reasons (because
// for unspent credits, it requires another lookup in another bucket). If this
// is needed, it may be checked like this:
//
// spent := existsRawUnminedInput(ns, it.ck) != nil
// spent := existsRawUnminedInput(ns, it.ck) != nil
type unminedCreditIterator struct {
c walletdb.ReadWriteCursor
prefix []byte
Expand Down

0 comments on commit 2a4a7d7

Please sign in to comment.