diff --git a/wallet/createtx.go b/wallet/createtx.go index da025ab4ff..dd87cf1b3d 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 { @@ -112,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 { @@ -127,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 @@ -147,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 } - } - arrangedCoins, err := coinSelectionStrategy.ArrangeCoins( - wrappedEligible, feeSatPerKb, - ) - if err != nil { - return err - } - inputSource := makeInputSource(arrangedCoins) + 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, + ) + } + + 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/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 - } -} diff --git a/wallet/wallet.go b/wallet/wallet.go index 335025dab4..81be5529f4 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 @@ -1240,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() @@ -1257,6 +1258,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 +1281,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 @@ -1322,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 @@ -3382,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. @@ -3402,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