diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index 4c3996515..6ee5783ec 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -811,8 +811,8 @@ func (w *WalletKit) SendOutputs(ctx context.Context, // requirement, we can request that the wallet attempts to create this // transaction. tx, err := w.cfg.Wallet.SendOutputs( - outputsToCreate, chainfee.SatPerKWeight(req.SatPerKw), minConfs, - label, coinSelectionStrategy, + nil, outputsToCreate, chainfee.SatPerKWeight(req.SatPerKw), + minConfs, label, coinSelectionStrategy, ) if err != nil { return nil, err diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go index 52aecb382..7af22e038 100644 --- a/lntest/mock/walletcontroller.go +++ b/lntest/mock/walletcontroller.go @@ -16,6 +16,7 @@ import ( base "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -33,6 +34,10 @@ type WalletController struct { Utxos []*lnwallet.Utxo } +// A compile time check to ensure this mocked WalletController implements the +// WalletController. +var _ lnwallet.WalletController = (*WalletController)(nil) + // BackEnd returns "mock" to signify a mock wallet controller. func (w *WalletController) BackEnd() string { return "mock" @@ -140,15 +145,15 @@ func (w *WalletController) ImportTaprootScript(waddrmgr.KeyScope, } // SendOutputs currently returns dummy values. -func (w *WalletController) SendOutputs([]*wire.TxOut, - chainfee.SatPerKWeight, int32, string, - base.CoinSelectionStrategy) (*wire.MsgTx, error) { +func (w *WalletController) SendOutputs(fn.Set[wire.OutPoint], []*wire.TxOut, + chainfee.SatPerKWeight, int32, string, base.CoinSelectionStrategy) ( + *wire.MsgTx, error) { return nil, nil } // CreateSimpleTx currently returns dummy values. -func (w *WalletController) CreateSimpleTx([]*wire.TxOut, +func (w *WalletController) CreateSimpleTx(fn.Set[wire.OutPoint], []*wire.TxOut, chainfee.SatPerKWeight, int32, base.CoinSelectionStrategy, bool) (*txauthor.AuthoredTx, error) { diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index 1094a052c..b3f9b4f9b 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -19,6 +19,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" base "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/wallet/txrules" @@ -26,6 +27,7 @@ import ( "github.com/btcsuite/btcwallet/wtxmgr" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/blockcache" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" @@ -977,8 +979,9 @@ func (b *BtcWallet) ImportTaprootScript(scope waddrmgr.KeyScope, // NOTE: This method requires the global coin selection lock to be held. // // This is a part of the WalletController interface. -func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut, - feeRate chainfee.SatPerKWeight, minConfs int32, label string, +func (b *BtcWallet) SendOutputs(inputs fn.Set[wire.OutPoint], + outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight, + minConfs int32, label string, strategy base.CoinSelectionStrategy) (*wire.MsgTx, error) { // Convert our fee rate from sat/kw to sat/kb since it's required by @@ -995,6 +998,14 @@ func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut, return nil, lnwallet.ErrInvalidMinconf } + // Use selected UTXOs if specified, otherwise default selection. + if len(inputs) != 0 { + return b.wallet.SendOutputsWithInput( + outputs, nil, defaultAccount, minConfs, feeSatPerKB, + strategy, label, inputs.ToSlice(), + ) + } + return b.wallet.SendOutputs( outputs, nil, defaultAccount, minConfs, feeSatPerKB, strategy, label, @@ -1014,10 +1025,10 @@ func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut, // NOTE: This method requires the global coin selection lock to be held. // // This is a part of the WalletController interface. -func (b *BtcWallet) CreateSimpleTx(outputs []*wire.TxOut, - feeRate chainfee.SatPerKWeight, minConfs int32, - strategy base.CoinSelectionStrategy, - dryRun bool) (*txauthor.AuthoredTx, error) { +func (b *BtcWallet) CreateSimpleTx(inputs fn.Set[wire.OutPoint], + outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight, minConfs int32, + strategy base.CoinSelectionStrategy, dryRun bool) ( + *txauthor.AuthoredTx, error) { // The fee rate is passed in using units of sat/kw, so we'll convert // this to sat/KB as the CreateSimpleTx method requires this unit. @@ -1047,9 +1058,12 @@ func (b *BtcWallet) CreateSimpleTx(outputs []*wire.TxOut, } } + // Add the optional inputs to the transaction. + optFunc := wallet.WithCustomSelectUtxos(inputs.ToSlice()) + return b.wallet.CreateSimpleTx( nil, defaultAccount, outputs, minConfs, feeSatPerKB, - strategy, dryRun, + strategy, dryRun, []wallet.TxCreateOption{optFunc}..., ) } diff --git a/lnwallet/interface.go b/lnwallet/interface.go index a48e92560..af5955d8e 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -18,6 +18,7 @@ import ( base "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -344,8 +345,8 @@ type WalletController interface { // be used when crafting the transaction. // // NOTE: This method requires the global coin selection lock to be held. - SendOutputs(outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight, - minConfs int32, label string, + SendOutputs(inputs fn.Set[wire.OutPoint], outputs []*wire.TxOut, + feeRate chainfee.SatPerKWeight, minConfs int32, label string, strategy base.CoinSelectionStrategy) (*wire.MsgTx, error) // CreateSimpleTx creates a Bitcoin transaction paying to the specified @@ -360,9 +361,10 @@ type WalletController interface { // SHOULD NOT be broadcasted. // // NOTE: This method requires the global coin selection lock to be held. - CreateSimpleTx(outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight, - minConfs int32, strategy base.CoinSelectionStrategy, - dryRun bool) (*txauthor.AuthoredTx, error) + CreateSimpleTx(inputs fn.Set[wire.OutPoint], outputs []*wire.TxOut, + feeRate chainfee.SatPerKWeight, minConfs int32, + strategy base.CoinSelectionStrategy, dryRun bool) ( + *txauthor.AuthoredTx, error) // GetTransactionDetails returns a detailed description of a transaction // given its transaction hash. diff --git a/lnwallet/mock.go b/lnwallet/mock.go index 1873de79a..e6bd0b2e4 100644 --- a/lnwallet/mock.go +++ b/lnwallet/mock.go @@ -17,6 +17,7 @@ import ( "github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/wtxmgr" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -35,6 +36,10 @@ type mockWalletController struct { Utxos []*Utxo } +// A compile time check to ensure that mockWalletController implements the +// WalletController. +var _ WalletController = (*mockWalletController)(nil) + // BackEnd returns "mock" to signify a mock wallet controller. func (w *mockWalletController) BackEnd() string { return "mock" @@ -145,7 +150,7 @@ func (w *mockWalletController) ImportTaprootScript(waddrmgr.KeyScope, } // SendOutputs currently returns dummy values. -func (w *mockWalletController) SendOutputs([]*wire.TxOut, +func (w *mockWalletController) SendOutputs(fn.Set[wire.OutPoint], []*wire.TxOut, chainfee.SatPerKWeight, int32, string, base.CoinSelectionStrategy) (*wire.MsgTx, error) { @@ -153,9 +158,9 @@ func (w *mockWalletController) SendOutputs([]*wire.TxOut, } // CreateSimpleTx currently returns dummy values. -func (w *mockWalletController) CreateSimpleTx([]*wire.TxOut, - chainfee.SatPerKWeight, int32, base.CoinSelectionStrategy, - bool) (*txauthor.AuthoredTx, error) { +func (w *mockWalletController) CreateSimpleTx(fn.Set[wire.OutPoint], + []*wire.TxOut, chainfee.SatPerKWeight, int32, + base.CoinSelectionStrategy, bool) (*txauthor.AuthoredTx, error) { return nil, nil } diff --git a/lnwallet/rpcwallet/rpcwallet.go b/lnwallet/rpcwallet/rpcwallet.go index 13f435579..c70217416 100644 --- a/lnwallet/rpcwallet/rpcwallet.go +++ b/lnwallet/rpcwallet/rpcwallet.go @@ -22,6 +22,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" basewallet "github.com/btcsuite/btcwallet/wallet" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lncfg" @@ -120,12 +121,13 @@ func (r *RPCKeyRing) NewAddress(addrType lnwallet.AddressType, change bool, // NOTE: This is a part of the WalletController interface. // // NOTE: This method only signs with BIP49/84 keys. -func (r *RPCKeyRing) SendOutputs(outputs []*wire.TxOut, - feeRate chainfee.SatPerKWeight, minConfs int32, label string, +func (r *RPCKeyRing) SendOutputs(inputs fn.Set[wire.OutPoint], + outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight, + minConfs int32, label string, strategy basewallet.CoinSelectionStrategy) (*wire.MsgTx, error) { tx, err := r.WalletController.SendOutputs( - outputs, feeRate, minConfs, label, strategy, + inputs, outputs, feeRate, minConfs, label, strategy, ) if err != nil && err != basewallet.ErrTxUnsigned { return nil, err diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index dfabdc741..bcb396caf 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -169,7 +169,7 @@ func sendCoins(t *testing.T, miner *rpctest.Harness, t.Helper() tx, err := sender.SendOutputs( - []*wire.TxOut{output}, feeRate, minConf, labels.External, + nil, []*wire.TxOut{output}, feeRate, minConf, labels.External, sender.Cfg.CoinSelectionStrategy, ) require.NoError(t, err, "unable to send transaction") @@ -1193,7 +1193,7 @@ func testListTransactionDetails(miner *rpctest.Harness, require.NoError(t, err, "unable to make output script") burnOutput := wire.NewTxOut(outputAmt, outputScript) burnTX, err := alice.SendOutputs( - []*wire.TxOut{burnOutput}, 2500, 1, labels.External, + nil, []*wire.TxOut{burnOutput}, 2500, 1, labels.External, alice.Cfg.CoinSelectionStrategy, ) require.NoError(t, err, "unable to create burn tx") @@ -1453,7 +1453,7 @@ func testTransactionSubscriptions(miner *rpctest.Harness, burnOutput := wire.NewTxOut(outputAmt, outputScript) tx, err := alice.SendOutputs( - []*wire.TxOut{burnOutput}, 2500, 1, labels.External, + nil, []*wire.TxOut{burnOutput}, 2500, 1, labels.External, alice.Cfg.CoinSelectionStrategy, ) require.NoError(t, err, "unable to create tx") @@ -1642,7 +1642,7 @@ func newTx(t *testing.T, r *rpctest.Harness, pubKey *btcec.PublicKey, PkScript: keyScript, } tx, err := alice.SendOutputs( - []*wire.TxOut{newOutput}, 2500, 1, labels.External, + nil, []*wire.TxOut{newOutput}, 2500, 1, labels.External, alice.Cfg.CoinSelectionStrategy, ) require.NoError(t, err, "unable to create output") @@ -1958,7 +1958,7 @@ func testSignOutputUsingTweaks(r *rpctest.Harness, PkScript: keyScript, } tx, err := alice.SendOutputs( - []*wire.TxOut{newOutput}, 2500, 1, labels.External, + nil, []*wire.TxOut{newOutput}, 2500, 1, labels.External, alice.Cfg.CoinSelectionStrategy, ) if err != nil { @@ -2077,7 +2077,7 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet, PkScript: script, } tx, err := w.SendOutputs( - []*wire.TxOut{output}, 2500, 1, labels.External, + nil, []*wire.TxOut{output}, 2500, 1, labels.External, w.Cfg.CoinSelectionStrategy, ) require.NoError(t, err, "unable to send outputs") @@ -2302,7 +2302,7 @@ func testSpendUnconfirmed(miner *rpctest.Harness, PkScript: alicePkScript, } _, err = bob.SendOutputs( - []*wire.TxOut{output}, txFeeRate, 0, labels.External, + nil, []*wire.TxOut{output}, txFeeRate, 0, labels.External, bob.Cfg.CoinSelectionStrategy, ) if err == nil { @@ -2329,7 +2329,7 @@ func testSpendUnconfirmed(miner *rpctest.Harness, // First, verify that we don't have enough balance to send the coins // using confirmed outputs only. _, err = bob.SendOutputs( - []*wire.TxOut{output}, txFeeRate, 1, labels.External, + nil, []*wire.TxOut{output}, txFeeRate, 1, labels.External, bob.Cfg.CoinSelectionStrategy, ) if err == nil { @@ -2571,7 +2571,7 @@ func testCreateSimpleTx(r *rpctest.Harness, w *lnwallet.LightningWallet, // Now try creating a tx spending to these outputs. createTx, createErr := w.CreateSimpleTx( - outputs, feeRate, minConfs, + nil, outputs, feeRate, minConfs, w.Cfg.CoinSelectionStrategy, true, ) switch { @@ -2590,7 +2590,7 @@ func testCreateSimpleTx(r *rpctest.Harness, w *lnwallet.LightningWallet, // only difference is that the dry run tx is not signed, and // that the change output position might be different. tx, sendErr := w.SendOutputs( - outputs, feeRate, minConfs, labels.External, + nil, outputs, feeRate, minConfs, labels.External, w.Cfg.CoinSelectionStrategy, ) switch { diff --git a/rpcserver.go b/rpcserver.go index 86bc6415c..5672b7b43 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1077,7 +1077,7 @@ func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64, // We first do a dry run, to sanity check we won't spend our wallet // balance below the reserved amount. authoredTx, err := r.server.cc.Wallet.CreateSimpleTx( - outputs, feeRate, minConfs, strategy, true, + nil, outputs, feeRate, minConfs, strategy, true, ) if err != nil { return nil, err @@ -1098,7 +1098,7 @@ func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64, // If that checks out, we're fairly confident that creating sending to // these outputs will keep the wallet balance above the reserve. tx, err := r.server.cc.Wallet.SendOutputs( - outputs, feeRate, minConfs, label, strategy, + nil, outputs, feeRate, minConfs, label, strategy, ) if err != nil { return nil, err @@ -1207,7 +1207,7 @@ func (r *rpcServer) EstimateFee(ctx context.Context, wallet := r.server.cc.Wallet err = wallet.WithCoinSelectLock(func() error { tx, err = wallet.CreateSimpleTx( - outputs, feePerKw, minConfs, coinSelectionStrategy, + nil, outputs, feePerKw, minConfs, coinSelectionStrategy, true, ) return err