diff --git a/wallet/createtx.go b/wallet/createtx.go index a12a190d69..8c056912bb 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) { + coinSelectionStrategy CoinSelectionStrategy, dryRun bool, + selectedUtxos []wire.OutPoint) (*txauthor.AuthoredTx, error) { chainClient, err := w.requireChainClient() if err != nil { @@ -174,27 +174,54 @@ 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 { + mapEligibleToOutpoint := make(map[wire.OutPoint]wtxmgr. + Credit) + + for _, e := range eligible { + mapEligibleToOutpoint[e.OutPoint] = e + } + + var eligibleSelectedUtxo []wtxmgr.Credit + for _, outpoint := range selectedUtxos { + e, ok := mapEligibleToOutpoint[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 := coinSelectionStrategy. + 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..8226baaee2 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 7e81ad6ee5..937aa15478 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()