From b4c1e19731cedac6f754b2927b1b5dc2a501546b Mon Sep 17 00:00:00 2001 From: Ononiwu Maureen Date: Wed, 28 Feb 2024 09:32:11 +0100 Subject: [PATCH 1/5] wallet: Add `selectUtxos` to `txCreateOptions` Signed-off-by: Ononiwu Maureen --- wallet/wallet.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wallet/wallet.go b/wallet/wallet.go index 335025dab4..4a368aeb75 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1257,6 +1257,7 @@ out: // scope, which otherwise will default to the specified coin selection scope. type txCreateOptions struct { changeKeyScope *waddrmgr.KeyScope + selectUtxos []wire.OutPoint } // TxCreateOption is a set of optional arguments to modify the tx creation @@ -1279,6 +1280,14 @@ func WithCustomChangeScope(changeScope *waddrmgr.KeyScope) TxCreateOption { } } +// WithCustomSelectUtxos is used to specify the inputs to be used while +// creating txns. +func WithCustomSelectUtxos(utxos []wire.OutPoint) TxCreateOption { + return func(opts *txCreateOptions) { + opts.selectUtxos = utxos + } +} + // 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 From 7ccfd8533001f257d99ff1e98aff27641383c122 Mon Sep 17 00:00:00 2001 From: Ononiwu Maureen Date: Wed, 28 Feb 2024 09:29:19 +0100 Subject: [PATCH 2/5] wallet: Add `selectUtxos` to `createTxRequest` Signed-off-by: Ononiwu Maureen --- wallet/wallet.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wallet/wallet.go b/wallet/wallet.go index 4a368aeb75..4cf66f2cc9 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1200,6 +1200,7 @@ type ( coinSelectionStrategy CoinSelectionStrategy dryRun bool resp chan createTxResponse + selectUtxos []wire.OutPoint } createTxResponse struct { tx *txauthor.AuthoredTx @@ -1331,6 +1332,7 @@ func (w *Wallet) CreateSimpleTx(coinSelectKeyScope *waddrmgr.KeyScope, coinSelectionStrategy: coinSelectionStrategy, dryRun: dryRun, resp: make(chan createTxResponse), + selectUtxos: opts.selectUtxos, } w.createTxRequests <- req resp := <-req.resp From 4c786517fa8a9d6c8b28fd53892633315ef702ef Mon Sep 17 00:00:00 2001 From: Ononiwu Maureen Date: Mon, 4 Mar 2024 15:20:59 +0100 Subject: [PATCH 3/5] wallet: move `constantInputSource` to createtx.go Signed-off-by: Ononiwu Maureen --- wallet/createtx.go | 27 +++++++++++++++++++++++++++ wallet/psbt.go | 27 --------------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/wallet/createtx.go b/wallet/createtx.go index da025ab4ff..a12a190d69 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -56,6 +56,33 @@ func makeInputSource(eligible []Coin) txauthor.InputSource { } } +// constantInputSource creates an input source function that always returns the +// static set of user-selected UTXOs. +func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { + // Current inputs and their total value. These won't change over + // different invocations as we want our inputs to remain static since + // they're selected by the user. + currentTotal := btcutil.Amount(0) + currentInputs := make([]*wire.TxIn, 0, len(eligible)) + currentScripts := make([][]byte, 0, len(eligible)) + currentInputValues := make([]btcutil.Amount, 0, len(eligible)) + + for _, credit := range eligible { + nextInput := wire.NewTxIn(&credit.OutPoint, nil, nil) + currentTotal += credit.Amount + currentInputs = append(currentInputs, nextInput) + currentScripts = append(currentScripts, credit.PkScript) + currentInputValues = append(currentInputValues, credit.Amount) + } + + return func(target btcutil.Amount) (btcutil.Amount, []*wire.TxIn, + []btcutil.Amount, [][]byte, error) { + + return currentTotal, currentInputs, currentInputValues, + currentScripts, nil + } +} + // secretSource is an implementation of txauthor.SecretSource for the wallet's // address manager. type secretSource struct { diff --git a/wallet/psbt.go b/wallet/psbt.go index ee2c792c09..a285370e26 100644 --- a/wallet/psbt.go +++ b/wallet/psbt.go @@ -567,30 +567,3 @@ func PsbtPrevOutputFetcher(packet *psbt.Packet) *txscript.MultiPrevOutFetcher { return fetcher } - -// constantInputSource creates an input source function that always returns the -// static set of user-selected UTXOs. -func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { - // Current inputs and their total value. These won't change over - // different invocations as we want our inputs to remain static since - // they're selected by the user. - currentTotal := btcutil.Amount(0) - currentInputs := make([]*wire.TxIn, 0, len(eligible)) - currentScripts := make([][]byte, 0, len(eligible)) - currentInputValues := make([]btcutil.Amount, 0, len(eligible)) - - for _, credit := range eligible { - nextInput := wire.NewTxIn(&credit.OutPoint, nil, nil) - currentTotal += credit.Amount - currentInputs = append(currentInputs, nextInput) - currentScripts = append(currentScripts, credit.PkScript) - currentInputValues = append(currentInputValues, credit.Amount) - } - - return func(target btcutil.Amount) (btcutil.Amount, []*wire.TxIn, - []btcutil.Amount, [][]byte, error) { - - return currentTotal, currentInputs, currentInputValues, - currentScripts, nil - } -} From fe15d37f886870ce22b042cd0bd0407f3030d355 Mon Sep 17 00:00:00 2001 From: Ononiwu Maureen Date: Mon, 4 Mar 2024 17:58:35 +0100 Subject: [PATCH 4/5] wallet: Create new function `SendOutputWithInput` Signed-off-by: Ononiwu Maureen --- wallet/wallet.go | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/wallet/wallet.go b/wallet/wallet.go index 4cf66f2cc9..80602ad7c8 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -3393,8 +3393,33 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu // returns the transaction upon success. func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, account uint32, minconf int32, satPerKb btcutil.Amount, - coinSelectionStrategy CoinSelectionStrategy, label string) ( - *wire.MsgTx, error) { + coinSelectionStrategy CoinSelectionStrategy, label string) (*wire.MsgTx, + error) { + + return w.sendOutputs( + outputs, keyScope, account, minconf, satPerKb, + coinSelectionStrategy, label, + ) +} + +// SendOutputsWithInput creates and sends payment transactions using the +// provided selected utxos. It returns the transaction upon success. +func (w *Wallet) SendOutputsWithInput(outputs []*wire.TxOut, + keyScope *waddrmgr.KeyScope, + account uint32, minconf int32, satPerKb btcutil.Amount, + coinSelectionStrategy CoinSelectionStrategy, label string, + selectedUtxos []wire.OutPoint) (*wire.MsgTx, error) { + + return w.sendOutputs(outputs, keyScope, account, minconf, satPerKb, + coinSelectionStrategy, label, selectedUtxos...) +} + +// sendOutputs creates and sends payment transactions.It returns the transaction +// upon success. +func (w *Wallet) sendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, + account uint32, minconf int32, satPerKb btcutil.Amount, + coinSelectionStrategy CoinSelectionStrategy, label string, + selectedUtxos ...wire.OutPoint) (*wire.MsgTx, error) { // Ensure the outputs to be created adhere to the network's consensus // rules. @@ -3413,7 +3438,9 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, // been confirmed. createdTx, err := w.CreateSimpleTx( keyScope, account, outputs, minconf, satPerKb, - coinSelectionStrategy, false, + coinSelectionStrategy, false, WithCustomSelectUtxos( + selectedUtxos, + ), ) if err != nil { return nil, err From d75f2c3f477affcf636542e2ec1538ffcddf4bae Mon Sep 17 00:00:00 2001 From: Ononiwu Maureen Date: Fri, 8 Mar 2024 14:23:16 +0100 Subject: [PATCH 5/5] wallet: Add selectedutxos to txToOutputs Signed-off-by: Ononiwu Maureen --- wallet/createtx.go | 76 +++++++++++++++++++++++---------- wallet/createtx_test.go | 93 +++++++++++++++++++++++++++++++++++++++-- wallet/wallet.go | 6 +-- 3 files changed, 146 insertions(+), 29 deletions(-) diff --git a/wallet/createtx.go b/wallet/createtx.go index a12a190d69..dd87cf1b3d 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -139,8 +139,8 @@ func (s secretSource) GetScript(addr btcutil.Address) ([]byte, error) { func (w *Wallet) txToOutputs(outputs []*wire.TxOut, coinSelectKeyScope, changeKeyScope *waddrmgr.KeyScope, account uint32, minconf int32, feeSatPerKb btcutil.Amount, - coinSelectionStrategy CoinSelectionStrategy, dryRun bool) ( - *txauthor.AuthoredTx, error) { + strategy CoinSelectionStrategy, dryRun bool, + selectedUtxos []wire.OutPoint) (*txauthor.AuthoredTx, error) { chainClient, err := w.requireChainClient() if err != nil { @@ -154,8 +154,8 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, } // Fall back to default coin selection strategy if none is supplied. - if coinSelectionStrategy == nil { - coinSelectionStrategy = CoinSelectionLargest + if strategy == nil { + strategy = CoinSelectionLargest } var tx *txauthor.AuthoredTx @@ -174,27 +174,57 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, return err } - // Wrap our coins in a type that implements the SelectableCoin - // interface, so we can arrange them according to the selected - // coin selection strategy. - wrappedEligible := make([]Coin, len(eligible)) - for i := range eligible { - wrappedEligible[i] = Coin{ - TxOut: wire.TxOut{ - Value: int64(eligible[i].Amount), - PkScript: eligible[i].PkScript, - }, - OutPoint: eligible[i].OutPoint, + var inputSource txauthor.InputSource + if len(selectedUtxos) > 0 { + eligibleByOutpoint := make( + map[wire.OutPoint]wtxmgr.Credit, + ) + + for _, e := range eligible { + eligibleByOutpoint[e.OutPoint] = e + } + + var eligibleSelectedUtxo []wtxmgr.Credit + for _, outpoint := range selectedUtxos { + e, ok := eligibleByOutpoint[outpoint] + + if !ok { + return fmt.Errorf("selected outpoint "+ + "not eligible for "+ + "spending: %v", outpoint) + } + eligibleSelectedUtxo = append( + eligibleSelectedUtxo, e, + ) } - } - arrangedCoins, err := coinSelectionStrategy.ArrangeCoins( - wrappedEligible, feeSatPerKb, - ) - if err != nil { - return err - } - inputSource := makeInputSource(arrangedCoins) + inputSource = constantInputSource(eligibleSelectedUtxo) + + } else { + // Wrap our coins in a type that implements the + // SelectableCoin interface, so we can arrange them + // according to the selected coin selection strategy. + wrappedEligible := make([]Coin, len(eligible)) + for i := range eligible { + wrappedEligible[i] = Coin{ + TxOut: wire.TxOut{ + Value: int64( + eligible[i].Amount, + ), + PkScript: eligible[i].PkScript, + }, + OutPoint: eligible[i].OutPoint, + } + } + + arrangedCoins, err := strategy.ArrangeCoins( + wrappedEligible, feeSatPerKb, + ) + if err != nil { + return err + } + inputSource = makeInputSource(arrangedCoins) + } tx, err = txauthor.NewUnsignedTransaction( outputs, feeSatPerKb, inputSource, changeSource, diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go index 55577604aa..bb351a1fc4 100644 --- a/wallet/createtx_test.go +++ b/wallet/createtx_test.go @@ -79,6 +79,7 @@ func TestTxToOutputsDryRun(t *testing.T) { // database us not inflated. dryRunTx, err := w.txToOutputs( txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, true, + nil, ) if err != nil { t.Fatalf("unable to author tx: %v", err) @@ -96,6 +97,7 @@ func TestTxToOutputsDryRun(t *testing.T) { dryRunTx2, err := w.txToOutputs( txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, true, + nil, ) if err != nil { t.Fatalf("unable to author tx: %v", err) @@ -131,6 +133,7 @@ func TestTxToOutputsDryRun(t *testing.T) { // to the database. tx, err := w.txToOutputs( txOuts, nil, nil, 0, 1, 1000, CoinSelectionLargest, false, + nil, ) if err != nil { t.Fatalf("unable to author tx: %v", err) @@ -280,7 +283,7 @@ func TestTxToOutputsRandom(t *testing.T) { createTx := func() *txauthor.AuthoredTx { tx, err := w.txToOutputs( txOuts, nil, nil, 0, 1, feeSatPerKb, - CoinSelectionRandom, true, + CoinSelectionRandom, true, nil, ) require.NoError(t, err) return tx @@ -352,7 +355,7 @@ func TestCreateSimpleCustomChange(t *testing.T) { } tx1, err := w.txToOutputs( []*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000, - CoinSelectionLargest, true, + CoinSelectionLargest, true, nil, ) require.NoError(t, err) @@ -378,7 +381,7 @@ func TestCreateSimpleCustomChange(t *testing.T) { tx2, err := w.txToOutputs( []*wire.TxOut{targetTxOut}, &waddrmgr.KeyScopeBIP0086, &waddrmgr.KeyScopeBIP0084, 0, 1, 1000, CoinSelectionLargest, - true, + true, nil, ) require.NoError(t, err) @@ -399,3 +402,87 @@ func TestCreateSimpleCustomChange(t *testing.T) { require.Equal(t, scriptType, txscript.WitnessV0PubKeyHashTy) } } + +// TestSelectUtxosTxoToOutpoint tests that it is possible to use passed +// selected utxos to craft a transaction in `txToOutpoint`. +func TestSelectUtxosTxoToOutpoint(t *testing.T) { + t.Parallel() + + w, cleanup := testWallet(t) + defer cleanup() + + // First, we'll make a P2TR and a P2WKH address to send some coins to. + p2wkhAddr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084) + require.NoError(t, err) + + p2trAddr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0086) + require.NoError(t, err) + + // We'll now make a transaction that'll send coins to both outputs, + // then "credit" the wallet for that send. + p2wkhScript, err := txscript.PayToAddrScript(p2wkhAddr) + require.NoError(t, err) + + p2trScript, err := txscript.PayToAddrScript(p2trAddr) + require.NoError(t, err) + + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + {}, + }, + TxOut: []*wire.TxOut{ + wire.NewTxOut(1_000_000, p2wkhScript), + wire.NewTxOut(2_000_000, p2trScript), + wire.NewTxOut(3_000_000, p2trScript), + wire.NewTxOut(7_000_000, p2trScript), + }, + } + addUtxo(t, w, incomingTx) + + // We expect 4 unspent utxos. + unspent, err := w.ListUnspent(0, 80, "") + require.NoError(t, err, "unexpected error while calling "+ + "list unspent") + + require.Len(t, unspent, 4, "expected 4 unspent "+ + "utxos") + + selectUtxos := []wire.OutPoint{ + { + Hash: incomingTx.TxHash(), + Index: 1, + }, + { + Hash: incomingTx.TxHash(), + Index: 2, + }, + } + + // Test by sending 200_000. + targetTxOut := &wire.TxOut{ + Value: 200_000, + PkScript: p2trScript, + } + tx1, err := w.txToOutputs( + []*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000, + CoinSelectionLargest, true, selectUtxos, + ) + require.NoError(t, err) + + // We expect all and only our select utxos to be input in this + // transaction. + require.Len(t, tx1.Tx.TxIn, len(selectUtxos)) + + lookupSelectUtxos := make(map[wire.OutPoint]struct{}) + for _, utxo := range selectUtxos { + lookupSelectUtxos[utxo] = struct{}{} + } + + for _, tx := range tx1.Tx.TxIn { + _, ok := lookupSelectUtxos[tx.PreviousOutPoint] + require.True(t, ok, "unexpected outpoint in txin") + } + + // Expect two outputs, change and the actual payment to the address. + require.Len(t, tx1.Tx.TxOut, 2) +} diff --git a/wallet/wallet.go b/wallet/wallet.go index 80602ad7c8..81be5529f4 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1241,7 +1241,7 @@ out: tx, err := w.txToOutputs( txr.outputs, txr.coinSelectKeyScope, txr.changeKeyScope, txr.account, txr.minconf, txr.feeSatPerKB, - txr.coinSelectionStrategy, txr.dryRun, + txr.coinSelectionStrategy, txr.dryRun, txr.selectUtxos, ) release() @@ -3414,8 +3414,8 @@ func (w *Wallet) SendOutputsWithInput(outputs []*wire.TxOut, coinSelectionStrategy, label, selectedUtxos...) } -// sendOutputs creates and sends payment transactions.It returns the transaction -// upon success. +// sendOutputs creates and sends payment transactions. It returns the +// transaction upon success. func (w *Wallet) sendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, account uint32, minconf int32, satPerKb btcutil.Amount, coinSelectionStrategy CoinSelectionStrategy, label string,