Merge pull request #8955 from yyforyongyu/cr-8516-240729-sendcoins
Allow selecting coins in `sendcoins`
This commit is contained in:
20 files changed
+3538
-3104
No files matched your search
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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}...,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
Reference in new issue
Block a user