Merge pull request #8955 from yyforyongyu/cr-8516-240729-sendcoins

Allow selecting coins in `sendcoins`
This commit is contained in:
Oliver Gugger authored and GitHub committed 2024-08-07 08:42:25 -06:00
commit d77617461d
20 files changed
+3538 -3104

No files matched your search

+1 -19
View File
@@ -406,7 +406,7 @@ func openChannel(ctx *cli.Context) error {
if ctx.IsSet("utxo") {
utxos := ctx.StringSlice("utxo")
outpoints, err := utxosToOutpoints(utxos)
outpoints, err := UtxosToOutpoints(utxos)
if err != nil {
return fmt.Errorf("unable to decode utxos: %w", err)
}
@@ -1141,21 +1141,3 @@ func decodePsbt(psbt string) ([]byte, error) {
return nil, fmt.Errorf("not a PSBT")
}
}
// parseUtxos parses a comma separated list of utxos into outpoints that are
// passed to the server.
func utxosToOutpoints(utxos []string) ([]*lnrpc.OutPoint, error) {
var outpoints []*lnrpc.OutPoint
if len(utxos) == 0 {
return nil, fmt.Errorf("no utxos specified")
}
for _, utxo := range utxos {
outpoint, err := NewProtoOutPoint(utxo)
if err != nil {
return nil, err
}
outpoints = append(outpoints, outpoint)
}
return outpoints, nil
}
+30 -7
View File
@@ -288,10 +288,11 @@ var sendCoinsCommand = cli.Command{
},
cli.BoolFlag{
Name: "sweepall",
Usage: "if set, then the amount field will be ignored, " +
"and the wallet will attempt to sweep all " +
"outputs within the wallet to the target " +
"address",
Usage: "if set, then the amount field should be " +
"unset. This indicates that the wallet will " +
"attempt to sweep all outputs within the " +
"wallet or all funds in select utxos (when " +
"supplied) to the target address",
},
cli.Int64Flag{
Name: "amt",
@@ -330,6 +331,17 @@ var sendCoinsCommand = cli.Command{
"scripts",
},
coinSelectionStrategyFlag,
cli.StringSliceFlag{
Name: "utxo",
Usage: "a utxo specified as outpoint(tx:idx) which " +
"will be used as input for the transaction. " +
"This flag can be repeatedly used to specify " +
"multiple utxos as inputs. The selected " +
"utxos can either be entirely spent by " +
"specifying the sweepall flag or a specified " +
"amount can be spent in the utxos through " +
"the amt flag",
},
txLabelFlag,
},
Action: actionDecorator(sendCoins),
@@ -337,9 +349,10 @@ var sendCoinsCommand = cli.Command{
func sendCoins(ctx *cli.Context) error {
var (
addr string
amt int64
err error
addr string
amt int64
err error
outpoints []*lnrpc.OutPoint
)
ctxc := getContext()
args := ctx.Args()
@@ -417,6 +430,15 @@ func sendCoins(ctx *cli.Context) error {
displayAmt = balanceResponse.GetConfirmedBalance()
}
if ctx.IsSet("utxo") {
utxos := ctx.StringSlice("utxo")
outpoints, err = UtxosToOutpoints(utxos)
if err != nil {
return fmt.Errorf("unable to decode utxos: %w", err)
}
}
// Ask for confirmation if we're on an actual terminal and the output is
// not being redirected to another command. This prevents existing shell
// scripts from breaking.
@@ -440,6 +462,7 @@ func sendCoins(ctx *cli.Context) error {
MinConfs: minConfs,
SpendUnconfirmed: minConfs == 0,
CoinSelectionStrategy: coinSelectionStrategy,
Outpoints: outpoints,
}
txid, err := client.SendCoins(ctxc, req)
if err != nil {
+19
View File
@@ -85,3 +85,22 @@ func NewFailedUpdateFromProto(update *lnrpc.FailedUpdate) *FailedUpdate {
UpdateError: update.UpdateError,
}
}
// UtxosToOutpoints converts a slice of UTXO strings into a slice of OutPoint
// protobuf objects. It returns an error if no UTXOs are specified or if any
// UTXO string cannot be parsed into an OutPoint.
func UtxosToOutpoints(utxos []string) ([]*lnrpc.OutPoint, error) {
var outpoints []*lnrpc.OutPoint
if len(utxos) == 0 {
return nil, fmt.Errorf("no utxos specified")
}
for _, utxo := range utxos {
outpoint, err := NewProtoOutPoint(utxo)
if err != nil {
return nil, err
}
outpoints = append(outpoints, outpoint)
}
return outpoints, nil
}
@@ -58,6 +58,36 @@ commitment when the channel was force closed.
send payment stream context, or automatically at the end of the timeout period
if the user provided `timeout_seconds`.
* The [SendCoinsRequest](https://github.com/lightningnetwork/lnd/pull/8955) now
takes an optional param `Outpoints`, which is a list of `*lnrpc.OutPoint`
that specifies the coins from the wallet to be spent in this RPC call. To
send selected coins to a given address with a given amount,
```go
req := &lnrpc.SendCoinsRequest{
Addr: ...,
Amount: ...,
Outpoints: []*lnrpc.OutPoint{
selected_wallet_utxo_1,
selected_wallet_utxo_2,
},
}
SendCoins(req)
```
To send selected coins to a given address without change output,
```go
req := &lnrpc.SendCoinsRequest{
Addr: ...,
SendAll: true,
Outpoints: []*lnrpc.OutPoint{
selected_wallet_utxo_1,
selected_wallet_utxo_2,
},
}
SendCoins(req)
```
## lncli Additions
* [Added](https://github.com/lightningnetwork/lnd/pull/8491) the `cltv_expiry`
@@ -68,6 +98,18 @@ commitment when the channel was force closed.
command returns the fee rate estimate for on-chain transactions in sat/kw and
sat/vb to achieve a given confirmation target.
* [`sendcoins` now takes an optional utxo
flag](https://github.com/lightningnetwork/lnd/pull/8955). This allows users
to specify the coins that they want to use as inputs for the transaction. To
send selected coins to a given address with a given amount,
```sh
sendcoins --addr YOUR_ADDR --amt YOUR_AMT --utxo selected_wallet_utxo1 --utxo selected_wallet_utxo2
```
To send selected coins to a given address without change output,
```sh
sendcoins --addr YOUR_ADDR --utxo selected_wallet_utxo1 --utxo selected_wallet_utxo2 --sweepall
```
# Improvements
## Functional Updates
+10 -2
View File
@@ -46,8 +46,16 @@ var allTestCases = []*lntest.TestCase{
TestFunc: testDataLossProtection,
},
{
Name: "sweep coins",
TestFunc: testSweepAllCoins,
Name: "send all coins",
TestFunc: testSendAllCoins,
},
{
Name: "send selected coins",
TestFunc: testSendSelectedCoins,
},
{
Name: "send selected coins channel reserve",
TestFunc: testSendSelectedCoinsChannelReserve,
},
{
Name: "disconnecting target peer",
+206 -8
View File
@@ -758,9 +758,9 @@ func testAbandonChannel(ht *lntest.HarnessTest) {
ht.ForceCloseChannel(bob, chanPoint)
}
// testSweepAllCoins tests that we're able to properly sweep all coins from the
// testSendAllCoins tests that we're able to properly sweep all coins from the
// wallet into a single target address at the specified fee rate.
func testSweepAllCoins(ht *lntest.HarnessTest) {
func testSendAllCoins(ht *lntest.HarnessTest) {
// First, we'll make a new node, Ainz who'll we'll use to test wallet
// sweeping.
//
@@ -777,20 +777,22 @@ func testSweepAllCoins(ht *lntest.HarnessTest) {
sendCoinsLabel := "send all coins"
// Ensure that we can't send coins to our own Pubkey.
ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
err := ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
Addr: ainz.RPC.GetInfo().IdentityPubkey,
SendAll: true,
Label: sendCoinsLabel,
TargetConf: 6,
})
require.ErrorContains(ht, err, "cannot send coins to pubkeys")
// Ensure that we can't send coins to another user's Pubkey.
ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
err = ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
Addr: ht.Alice.RPC.GetInfo().IdentityPubkey,
SendAll: true,
Label: sendCoinsLabel,
TargetConf: 6,
})
require.ErrorContains(ht, err, "cannot send coins to pubkey")
// With the two coins above mined, we'll now instruct Ainz to sweep all
// the coins to an external address not under its control. We will first
@@ -800,20 +802,23 @@ func testSweepAllCoins(ht *lntest.HarnessTest) {
// same network as the user.
// Send coins to a testnet3 address.
ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
err = ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
Addr: "tb1qfc8fusa98jx8uvnhzavxccqlzvg749tvjw82tg",
SendAll: true,
Label: sendCoinsLabel,
TargetConf: 6,
})
require.ErrorContains(ht, err, "not valid for this network")
// Send coins to a mainnet address.
ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
err = ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
Addr: "1MPaXKp5HhsLNjVSqaL7fChE3TVyrTMRT3",
SendAll: true,
Label: sendCoinsLabel,
TargetConf: 6,
})
// TODO(yy): should instead return "not valid for this network".
require.ErrorContains(ht, err, "unknown address type")
// TODO(yy): we still allow default values to be used when neither conf
// target or fee rate is set in 0.18.0. When future release forbidden
@@ -883,7 +888,7 @@ func testSweepAllCoins(ht *lntest.HarnessTest) {
// label our transaction with an empty label, and check that we fail as
// expected.
sweepHash := sweepTx.TxHash()
err := ainz.RPC.LabelTransactionAssertErr(
err = ainz.RPC.LabelTransactionAssertErr(
&walletrpc.LabelTransactionRequest{
Txid: sweepHash[:],
Label: "",
@@ -929,13 +934,14 @@ func testSweepAllCoins(ht *lntest.HarnessTest) {
// If we try again, but this time specifying an amount, then the call
// should fail.
ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
err = ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
Addr: ht.NewMinerAddress().String(),
Amount: 10000,
SendAll: true,
Label: sendCoinsLabel,
TargetConf: 6,
})
require.ErrorContains(ht, err, "amount set while SendAll is active")
// With all the edge cases tested, we'll now test the happy paths of
// change output types.
@@ -1302,3 +1308,195 @@ func testNativeSQLNoMigration(ht *lntest.HarnessTest) {
alice.SetExtraArgs(nil)
require.NoError(ht, alice.Start(ht.Context()))
}
// testSendSelectedCoins tests that we're able to properly send the selected
// coins from the wallet to a single target address.
func testSendSelectedCoins(ht *lntest.HarnessTest) {
// First, we'll make a new node, Alice who'll we'll use to test wallet
// sweeping.
alice := ht.NewNode("Alice", nil)
// Next, we'll give Alice exactly 3 utxos of 1 BTC of different address
// types.
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
ht.FundCoinsNP2WKH(btcutil.SatoshiPerBitcoin, alice)
ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, alice)
// Get all the utxos in the wallet and assert there are three.
utxos := ht.AssertNumUTXOs(alice, 3)
// Ensure that we can't send duplicate coins.
//
// Create duplciate outpoints.
dupOutpoints := []*lnrpc.OutPoint{
utxos[0].Outpoint,
utxos[0].Outpoint,
}
// Send the duplicate outpoints and assert there's an error.
err := alice.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
Addr: ht.NewMinerAddress().String(),
Outpoints: dupOutpoints,
})
require.ErrorContains(ht, err, "selected outpoints contain duplicate")
// Send a selected coin with a specific amount.
//
// We'll send the first utxo with an amount of 0.5 BTC.
amt := btcutil.Amount(0.5 * btcutil.SatoshiPerBitcoin)
alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{
Addr: ht.NewMinerAddress().String(),
Outpoints: []*lnrpc.OutPoint{
utxos[0].Outpoint,
},
Amount: int64(amt),
})
// We expect to see the above tx in the mempool.
tx := ht.GetNumTxsFromMempool(1)[0]
// Assert the tx has the expected shape. It should have 1 input and 2
// outputs - the input is the selected UTXO, and the outputs are the
// specified amount and the change amount.
require.Len(ht, tx.TxIn, 1)
require.Len(ht, tx.TxOut, 2)
// Check it's using the selected UTXO as input.
require.Equal(ht, utxos[0].Outpoint.TxidStr,
tx.TxIn[0].PreviousOutPoint.Hash.String())
// Mine a block to confirm the above tx.
ht.MineBlocksAndAssertNumTxes(1, 1)
// We now test we can send the selected coins to a single target
// address with `SendAll` flag.
//
// Get all the utxos in the wallet and assert there are three.
utxos = ht.AssertNumUTXOs(alice, 3)
// Select the first two coins and send them to a single target address.
alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{
Addr: ht.NewMinerAddress().String(),
Outpoints: []*lnrpc.OutPoint{
utxos[0].Outpoint,
utxos[1].Outpoint,
},
SendAll: true,
})
// We expect to see the above tx in the mempool.
tx = ht.GetNumTxsFromMempool(1)[0]
// Assert the tx has the expected shape. It should have 2 inputs and 1
// output.
require.Len(ht, tx.TxIn, 2)
require.Len(ht, tx.TxOut, 1)
// Check it's using the selected UTXOs as inputs.
prevOutpoint1 := tx.TxIn[0].PreviousOutPoint.Hash.String()
prevOutpoint2 := tx.TxIn[1].PreviousOutPoint.Hash.String()
if prevOutpoint1 == utxos[0].Outpoint.TxidStr {
require.Equal(ht, utxos[1].Outpoint.TxidStr, prevOutpoint2)
} else {
require.Equal(ht, utxos[1].Outpoint.TxidStr, prevOutpoint1)
require.Equal(ht, utxos[0].Outpoint.TxidStr, prevOutpoint2)
}
// Mine a block to confirm the above tx.
ht.MineBlocksAndAssertNumTxes(1, 1)
// Since the first two UTXOs have been sent to an address outside her
// wallet, Alice should see a single UTXO now.
ht.AssertNumUTXOs(alice, 1)
}
// testSendSelectedCoinsChannelReserve tests that if sending selected coins
// would violate the channel reserve requirement the RPC call will fail. It
// also checks that change outputs will be created automatically if `SendAll`
// flag is set.
func testSendSelectedCoinsChannelReserve(ht *lntest.HarnessTest) {
chanAmt := btcutil.Amount(100_000)
// Create a two-hop network: Alice -> Bob.
//
// NOTE: Alice will have one UTXO after the funding.
_, nodes := createSimpleNetwork(
ht, []string{"--protocol.anchors"}, 2,
lntest.OpenChannelParams{
Amt: chanAmt,
},
)
alice := nodes[0]
// Fund Alice one more UTXO.
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
// Get all the utxos in the wallet and assert there are two:
// - one from the above `FundCoins`.
// - one from the change output after the channel funding.
utxos := ht.AssertNumUTXOs(alice, 2)
// Get all the outpoints.
outpoints := []*lnrpc.OutPoint{
utxos[0].Outpoint,
utxos[1].Outpoint,
}
// Calculate the total amount of the two UTXOs.
totalAmt := utxos[0].AmountSat + utxos[1].AmountSat
// Send the total amount of the two UTXOs and expect an error since
// fees cannot be covered.
err := alice.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
Addr: ht.NewMinerAddress().String(),
Outpoints: outpoints,
Amount: totalAmt,
})
require.ErrorContains(ht, err, "insufficient funds available to "+
"construct transaction")
// Get the required reserve amount.
resp := alice.RPC.RequiredReserve(
&walletrpc.RequiredReserveRequest{
AdditionalPublicChannels: 1,
},
)
// Calculate an amount which, after sending it, would violate the
// channel reserve requirement.
amt := totalAmt - resp.RequiredReserve
// Send the amount and expect an error since the channel reserve cannot
// be covered.
err = alice.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
Addr: ht.NewMinerAddress().String(),
Outpoints: outpoints,
Amount: amt,
})
require.ErrorContains(ht, err, "reserved wallet balance invalidated")
// Finally, check that we can send all the selected coins with the help
// of the `SendAll` flag as it will automatically handle reserving
// change outputs based on the channel reserve requirements.
alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{
Addr: ht.NewMinerAddress().String(),
Outpoints: outpoints,
SendAll: true,
})
// We expect to see the above tx in the mempool.
tx := ht.GetNumTxsFromMempool(1)[0]
// Assert the tx has the expected shape. It should have 2 inputs and 2
// outputs.
require.Len(ht, tx.TxIn, 2)
require.Len(ht, tx.TxOut, 2)
// Mine a block to confirm the above tx.
ht.MineBlocksAndAssertNumTxes(1, 1)
// Alice should have one reserved UTXO now.
ht.AssertNumUTXOs(alice, 1)
}
+3006 -2994
View File
File diff suppressed because it is too large. Load diff
+5 -3
View File
@@ -1190,9 +1190,8 @@ message SendCoinsRequest {
int64 sat_per_byte = 5 [deprecated = true];
/*
If set, then the amount field will be ignored, and lnd will attempt to
send all the coins under control of the internal wallet to the specified
address.
If set, the amount field should be unset. It indicates lnd will send all
wallet coins or all selected coins to the specified address.
*/
bool send_all = 6;
@@ -1208,6 +1207,9 @@ message SendCoinsRequest {
// The strategy to use for selecting coins.
CoinSelectionStrategy coin_selection_strategy = 10;
// A list of selected outpoints as inputs for the transaction.
repeated OutPoint outpoints = 11;
}
message SendCoinsResponse {
// The transaction ID of the transaction
+8 -1
View File
@@ -6965,7 +6965,7 @@
},
"send_all": {
"type": "boolean",
"description": "If set, then the amount field will be ignored, and lnd will attempt to\nsend all the coins under control of the internal wallet to the specified\naddress."
"description": "If set, the amount field should be unset. It indicates lnd will send all\nwallet coins or all selected coins to the specified address."
},
"label": {
"type": "string",
@@ -6983,6 +6983,13 @@
"coin_selection_strategy": {
"$ref": "#/definitions/lnrpcCoinSelectionStrategy",
"description": "The strategy to use for selecting coins."
},
"outpoints": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcOutPoint"
},
"description": "A list of selected outpoints as inputs for the transaction."
}
}
},
+2 -2
View File
@@ -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
+9 -4
View File
@@ -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) {
+3 -1
View File
@@ -378,12 +378,14 @@ func (h *HarnessRPC) SendCoins(
// SendCoinsAssertErr sends a given amount of money to the specified address
// from the passed node and asserts an error has returned.
func (h *HarnessRPC) SendCoinsAssertErr(req *lnrpc.SendCoinsRequest) {
func (h *HarnessRPC) SendCoinsAssertErr(req *lnrpc.SendCoinsRequest) error {
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
defer cancel()
_, err := h.LN.SendCoins(ctxt, req)
require.Error(h, err, "node %s didn't not return an error", h.Name)
return err
}
// GetTransactions makes a RPC call to GetTransactions and asserts.
+21 -7
View File
@@ -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}...,
)
}
+7 -5
View File
@@ -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.
+9 -4
View File
@@ -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
}
+5 -3
View File
@@ -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
+10 -10
View File
@@ -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 {
+30 -13
View File
@@ -1067,7 +1067,8 @@ func allowCORS(handler http.Handler, origins []string) http.Handler {
// address to a specified output value to be sent to that address.
func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64,
feeRate chainfee.SatPerKWeight, minConfs int32, label string,
strategy wallet.CoinSelectionStrategy) (*chainhash.Hash, error) {
strategy wallet.CoinSelectionStrategy,
selectedUtxos fn.Set[wire.OutPoint]) (*chainhash.Hash, error) {
outputs, err := addrPairsToOutputs(paymentMap, r.cfg.ActiveNetParams.Params)
if err != nil {
@@ -1077,7 +1078,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,
selectedUtxos, outputs, feeRate, minConfs, strategy, true,
)
if err != nil {
return nil, err
@@ -1098,7 +1099,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,
selectedUtxos, outputs, feeRate, minConfs, label, strategy,
)
if err != nil {
return nil, err
@@ -1207,7 +1208,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
@@ -1290,9 +1291,9 @@ func (r *rpcServer) SendCoins(ctx context.Context,
}
rpcsLog.Infof("[sendcoins] addr=%v, amt=%v, sat/kw=%v, min_confs=%v, "+
"send_all=%v",
"send_all=%v, select_outpoints=%v",
in.Addr, btcutil.Amount(in.Amount), int64(feePerKw), minConfs,
in.SendAll)
in.SendAll, len(in.Outpoints))
// Decode the address receiving the coins, we need to check whether the
// address is valid for this network.
@@ -1337,6 +1338,22 @@ func (r *rpcServer) SendCoins(ctx context.Context,
wallet := r.server.cc.Wallet
maxFeeRate := r.cfg.Sweeper.MaxFeeRate.FeePerKWeight()
var selectOutpoints fn.Set[wire.OutPoint]
if len(in.Outpoints) != 0 {
wireOutpoints, err := toWireOutpoints(in.Outpoints)
if err != nil {
return nil, fmt.Errorf("can't create outpoints "+
"%w", err)
}
if fn.HasDuplicates(wireOutpoints) {
return nil, fmt.Errorf("selected outpoints contain " +
"duplicate values")
}
selectOutpoints = fn.NewSet(wireOutpoints...)
}
// If the send all flag is active, then we'll attempt to sweep all the
// coins in the wallet in a single transaction (if possible),
// otherwise, we'll respect the amount, and attempt a regular 2-output
@@ -1363,7 +1380,7 @@ func (r *rpcServer) SendCoins(ctx context.Context,
sweepTxPkg, err := sweep.CraftSweepAllTx(
feePerKw, maxFeeRate, uint32(bestHeight), nil,
targetAddr, wallet, wallet, wallet.WalletController,
r.server.cc.Signer, minConfs,
r.server.cc.Signer, minConfs, selectOutpoints,
)
if err != nil {
return nil, err
@@ -1417,7 +1434,7 @@ func (r *rpcServer) SendCoins(ctx context.Context,
feePerKw, maxFeeRate, uint32(bestHeight),
outputs, targetAddr, wallet, wallet,
wallet.WalletController,
r.server.cc.Signer, minConfs,
r.server.cc.Signer, minConfs, selectOutpoints,
)
if err != nil {
return nil, err
@@ -1443,7 +1460,7 @@ func (r *rpcServer) SendCoins(ctx context.Context,
return nil, err
}
rpcsLog.Debugf("Sweeping all coins from wallet to addr=%v, "+
rpcsLog.Debugf("Sweeping coins from wallet to addr=%v, "+
"with tx=%v", in.Addr, spew.Sdump(sweepTxPkg.SweepTx))
// As our sweep transaction was created, successfully, we'll
@@ -1468,8 +1485,8 @@ func (r *rpcServer) SendCoins(ctx context.Context,
paymentMap := map[string]int64{targetAddr.String(): in.Amount}
err := wallet.WithCoinSelectLock(func() error {
newTXID, err := r.sendCoinsOnChain(
paymentMap, feePerKw, minConfs,
label, coinSelectionStrategy,
paymentMap, feePerKw, minConfs, label,
coinSelectionStrategy, selectOutpoints,
)
if err != nil {
return err
@@ -1540,8 +1557,8 @@ func (r *rpcServer) SendMany(ctx context.Context,
wallet := r.server.cc.Wallet
err = wallet.WithCoinSelectLock(func() error {
sendManyTXID, err := r.sendCoinsOnChain(
in.AddrToAmount, feePerKw, minConfs,
label, coinSelectionStrategy,
in.AddrToAmount, feePerKw, minConfs, label,
coinSelectionStrategy, nil,
)
if err != nil {
return err
+56 -18
View File
@@ -10,10 +10,12 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"golang.org/x/exp/maps"
)
var (
@@ -24,6 +26,10 @@ var (
// ErrFeePreferenceConflict is returned when both a fee rate and a conf
// target is set for a fee preference.
ErrFeePreferenceConflict = errors.New("fee preference conflict")
// ErrUnknownUTXO is returned when creating a sweeping tx using an UTXO
// that's unknown to the wallet.
ErrUnknownUTXO = errors.New("unknown utxo")
)
// FeePreference defines an interface that allows the caller to specify how the
@@ -181,11 +187,11 @@ type OutputLeaser interface {
}
// WalletSweepPackage is a package that gives the caller the ability to sweep
// ALL funds from a wallet in a single transaction. We also package a function
// closure that allows one to abort the operation.
// relevant funds from a wallet in a single transaction. We also package a
// function closure that allows one to abort the operation.
type WalletSweepPackage struct {
// SweepTx is a fully signed, and valid transaction that is broadcast,
// will sweep ALL confirmed coins in the wallet with a single
// will sweep ALL relevant confirmed coins in the wallet with a single
// transaction.
SweepTx *wire.MsgTx
@@ -208,27 +214,28 @@ type DeliveryAddr struct {
}
// CraftSweepAllTx attempts to craft a WalletSweepPackage which will allow the
// caller to sweep ALL outputs within the wallet to a list of outputs. Any
// leftover amount after these outputs and transaction fee, is sent to a single
// output, as specified by the change address. The sweep transaction will be
// crafted with the target fee rate, and will use the utxoSource and
// outputLeaser as sources for wallet funds.
// caller to sweep ALL funds in ALL or SELECT outputs within the wallet to a
// list of outputs. Any leftover amount after these outputs and transaction fee,
// is sent to a single output, as specified by the change address. The sweep
// transaction will be crafted with the target fee rate, and will use the
// utxoSource and outputLeaser as sources for wallet funds.
func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
blockHeight uint32, deliveryAddrs []DeliveryAddr,
changeAddr btcutil.Address, coinSelectLocker CoinSelectionLocker,
utxoSource UtxoSource, outputLeaser OutputLeaser,
signer input.Signer, minConfs int32) (*WalletSweepPackage, error) {
signer input.Signer, minConfs int32,
selectUtxos fn.Set[wire.OutPoint]) (*WalletSweepPackage, error) {
// TODO(roasbeef): turn off ATPL as well when available?
var allOutputs []*lnwallet.Utxo
var outputsForSweep []*lnwallet.Utxo
// We'll make a function closure up front that allows us to unlock all
// selected outputs to ensure that they become available again in the
// case of an error after the outputs have been locked, but before we
// can actually craft a sweeping transaction.
unlockOutputs := func() {
for _, utxo := range allOutputs {
for _, utxo := range outputsForSweep {
// Log the error but continue since we're already
// handling an error.
err := outputLeaser.ReleaseOutput(
@@ -242,9 +249,9 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
}
// Next, we'll use the coinSelectLocker to ensure that no coin
// selection takes place while we fetch and lock all outputs the wallet
// knows of. Otherwise, it may be possible for a new funding flow to
// lock an output while we fetch the set of unspent witnesses.
// selection takes place while we fetch and lock outputs in the
// wallet. Otherwise, it may be possible for a new funding flow to lock
// an output while we fetch the set of unspent witnesses.
err := coinSelectLocker.WithCoinSelectLock(func() error {
log.Trace("[WithCoinSelectLock] entered the lock")
@@ -260,6 +267,16 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
log.Trace("[WithCoinSelectLock] finished fetching UTXOs")
// Use select utxos, if provided.
if len(selectUtxos) > 0 {
utxos, err = fetchUtxosFromOutpoints(
utxos, selectUtxos.ToSlice(),
)
if err != nil {
return err
}
}
// We'll now lock each UTXO to ensure that other callers don't
// attempt to use these UTXOs in transactions while we're
// crafting out sweep all transaction.
@@ -278,7 +295,7 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
log.Trace("[WithCoinSelectLock] exited the lock")
allOutputs = append(allOutputs, utxos...)
outputsForSweep = append(outputsForSweep, utxos...)
return nil
})
@@ -287,15 +304,15 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
// in case we had any lingering outputs.
unlockOutputs()
return nil, fmt.Errorf("unable to fetch+lock wallet "+
"utxos: %v", err)
return nil, fmt.Errorf("unable to fetch+lock wallet utxos: %w",
err)
}
// Now that we've locked all the potential outputs to sweep, we'll
// assemble an input for each of them, so we can hand it off to the
// sweeper to generate and sign a transaction for us.
var inputsToSweep []input.Input
for _, output := range allOutputs {
for _, output := range outputsForSweep {
// As we'll be signing for outputs under control of the wallet,
// we only need to populate the output value and output script.
// The rest of the items will be populated internally within
@@ -390,3 +407,24 @@ func CraftSweepAllTx(feeRate, maxFeeRate chainfee.SatPerKWeight,
CancelSweepAttempt: unlockOutputs,
}, nil
}
// fetchUtxosFromOutpoints returns UTXOs for given outpoints. Errors if any
// outpoint is not in the passed slice of utxos.
func fetchUtxosFromOutpoints(utxos []*lnwallet.Utxo,
outpoints []wire.OutPoint) ([]*lnwallet.Utxo, error) {
lookup := fn.SliceToMap(utxos, func(utxo *lnwallet.Utxo) wire.OutPoint {
return utxo.OutPoint
}, func(utxo *lnwallet.Utxo) *lnwallet.Utxo {
return utxo
})
subMap, err := fn.NewSubMap(lookup, outpoints)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrUnknownUTXO, err.Error())
}
fetchedUtxos := maps.Values(subMap)
return fetchedUtxos, nil
}
+59 -3
View File
@@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -339,7 +340,7 @@ func TestCraftSweepAllTxCoinSelectFail(t *testing.T) {
_, err := CraftSweepAllTx(
0, 0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLeaser,
nil, 0,
nil, 0, nil,
)
// Since we instructed the coin select locker to fail above, we should
@@ -365,7 +366,7 @@ func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) {
_, err := CraftSweepAllTx(
0, 0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLeaser,
nil, 0,
nil, 0, nil,
)
// Since passed in a p2wsh output, which is unknown, we should fail to
@@ -399,7 +400,7 @@ func TestCraftSweepAllTx(t *testing.T) {
sweepPkg, err := CraftSweepAllTx(
0, 0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
utxoLeaser, signer, 0,
utxoLeaser, signer, 0, nil,
)
require.NoError(t, err, "unable to make sweep tx")
@@ -440,3 +441,58 @@ func TestCraftSweepAllTx(t *testing.T) {
sweepPkg.CancelSweepAttempt()
assertUtxosReleased(t, utxoLeaser, testUtxos[:2])
}
// TestCraftSweepAllTxWithSelectedUTXO tests that we'll properly lock the
// selected outputs within the wallet, and craft a single sweep transaction
// that pays to the target output.
func TestCraftSweepAllTxWithSelectedUTXO(t *testing.T) {
t.Parallel()
// First, we'll make a mock signer along with a fee estimator, We'll
// use zero fees to we can assert a precise output value.
signer := &mock.DummySigner{}
// Grab the first UTXO from the test UTXOs.
utxo1 := testUtxos[0]
utxoSource := newMockUtxoSource([]*lnwallet.Utxo{utxo1})
coinSelectLocker := &mockCoinSelectionLocker{}
utxoLeaser := newMockOutputLeaser()
// Create an unknown utxo.
outpointUknown := wire.OutPoint{Index: 4}
// Sweep using the uknnown utxo and expect an error.
sweepPkg, err := CraftSweepAllTx(
0, 0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
utxoLeaser, signer, 0, fn.NewSet(outpointUknown),
)
require.ErrorIs(t, err, ErrUnknownUTXO)
require.Nil(t, sweepPkg)
// Sweep again using the known utxo and expect no error.
sweepPkg, err = CraftSweepAllTx(
0, 0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
utxoLeaser, signer, 0, fn.NewSet(utxo1.OutPoint),
)
require.NoError(t, err)
// At this point utxo1 should be locked.
assertUtxosLeased(t, utxoLeaser, []*lnwallet.Utxo{utxo1})
assertNoUtxosReleased(t, utxoLeaser, []*lnwallet.Utxo{utxo1})
// Validate the sweeping tx has the expected shape.
sweepTx := sweepPkg.SweepTx
require.Len(t, sweepTx.TxIn, 1)
require.Len(t, sweepTx.TxOut, 1)
// We should have a single output that pays to our sweep script
// generated above.
expectedSweepValue := utxo1.Value
require.Equal(t, sweepScript, sweepTx.TxOut[0].PkScript)
require.EqualValues(t, expectedSweepValue, sweepTx.TxOut[0].Value)
// If we cancel the sweep attempt, then we should find utxo1 to be
// unlocked.
sweepPkg.CancelSweepAttempt()
assertUtxosReleased(t, utxoLeaser, []*lnwallet.Utxo{utxo1})
}