Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions cmd/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,12 @@ var sendCoinsCommand = cli.Command{
"the amt flag",
},
txLabelFlag,
cli.StringFlag{
Name: "change_address",
Usage: "(optional) a bitcoin address to send change to. " +
"If not set, change is sent to a wallet-derived " +
"address. Cannot be used with --sweepall.",
},
},
Action: actionDecorator(sendCoins),
}
Expand Down Expand Up @@ -598,6 +604,11 @@ func sendCoins(ctx *cli.Context) error {
"sweep all coins out of the wallet")
}

if ctx.IsSet("change_address") && ctx.Bool("sweepall") {
return fmt.Errorf("change_address cannot be set when " +
"sweepall is active")
}

coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -682,6 +693,7 @@ func sendCoins(ctx *cli.Context) error {
SpendUnconfirmed: minConfs == 0,
CoinSelectionStrategy: coinSelectionStrategy,
Outpoints: outpoints,
ChangeAddress: ctx.String("change_address"),
}
txid, err := client.SendCoins(ctxc, req)
if err != nil {
Expand Down Expand Up @@ -856,6 +868,12 @@ var sendManyCommand = cli.Command{
},
coinSelectionStrategyFlag,
txLabelFlag,
cli.StringFlag{
Name: "change_address",
Usage: "(optional) a bitcoin address to send change to. " +
"If not set, change is sent to a wallet-derived " +
"address.",
},
},
Action: actionDecorator(sendMany),
}
Expand Down Expand Up @@ -902,6 +920,7 @@ func sendMany(ctx *cli.Context) error {
MinConfs: minConfs,
SpendUnconfirmed: minConfs == 0,
CoinSelectionStrategy: coinSelectionStrategy,
ChangeAddress: ctx.String("change_address"),
})
if err != nil {
return err
Expand Down
29 changes: 29 additions & 0 deletions docs/release-notes/release-notes-0.22.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,49 @@

## Functional Enhancements

* When making an on-chain send, callers can now specify a
[`change_address`](https://github.com/lightningnetwork/lnd/pull/10810) to
control where any change output is sent. This enables workflows where a user
wants to drain a specific address across multiple transactions — by pinning
the change address, every sweep transaction returns leftover funds to a
known, controlled address rather than a fresh wallet-derived one. The field
is mutually exclusive with `send_all`.

## RPC Additions

* The `routerrpc.EstimateRouteFee` RPC now supports [restricting fee estimates
to specific first-hop outgoing
channels](https://github.com/lightningnetwork/lnd/pull/10501) via the new
`outgoing_chan_ids` field in `RouteFeeRequest`.

* [`SendCoins`](https://github.com/lightningnetwork/lnd/pull/10810) now accepts
a `change_address` field on `SendCoinsRequest`. When set, change is sent to
the specified address instead of a wallet-derived one. Cannot be combined
with `send_all`.

* [`SendMany`](https://github.com/lightningnetwork/lnd/pull/10810) now accepts
a `change_address` field on `SendManyRequest` with the same semantics.

* [`WalletKit.SendOutputs`](https://github.com/lightningnetwork/lnd/pull/10810)
now accepts a `change_address` field on `SendOutputsRequest` with the same
semantics.

## lncli Additions

* The `estimateroutefee` command now supports [restricting fee estimates to
specific first-hop outgoing
channels](https://github.com/lightningnetwork/lnd/pull/10501) via the new
`--outgoing_chan_id` flag.

* `sendcoins` now accepts a
[`--change_address`](https://github.com/lightningnetwork/lnd/pull/10810) flag
to specify a custom destination for change. Mutually exclusive with
`--sweepall`.

* `sendmany` now accepts a
[`--change_address`](https://github.com/lightningnetwork/lnd/pull/10810) flag
with the same semantics.

# Improvements

## Functional Updates
Expand Down
37 changes: 30 additions & 7 deletions lnrpc/lightning.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions lnrpc/lightning.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,10 @@ message SendManyRequest {

// The strategy to use for selecting coins during sending many requests.
CoinSelectionStrategy coin_selection_strategy = 9;

// An optional address to send change to. If not set, a wallet-derived
// address is used.
string change_address = 10;
}
message SendManyResponse {
// The id of the transaction
Expand Down Expand Up @@ -1155,6 +1159,10 @@ message SendCoinsRequest {

// A list of selected outpoints as inputs for the transaction.
repeated OutPoint outpoints = 11;

// An optional address to send change to. If not set, a wallet-derived
// address is used.
string change_address = 12;
}
message SendCoinsResponse {
// The transaction ID of the transaction
Expand Down
8 changes: 8 additions & 0 deletions lnrpc/lightning.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -7543,6 +7543,10 @@
"$ref": "#/definitions/lnrpcOutPoint"
},
"description": "A list of selected outpoints as inputs for the transaction."
},
"change_address": {
"type": "string",
"description": "An optional address to send change to. If not set, a wallet-derived\naddress is used."
}
}
},
Expand Down Expand Up @@ -7626,6 +7630,10 @@
"coin_selection_strategy": {
"$ref": "#/definitions/lnrpcCoinSelectionStrategy",
"description": "The strategy to use for selecting coins during sending many requests."
},
"change_address": {
"type": "string",
"description": "An optional address to send change to. If not set, a wallet-derived\naddress is used."
}
}
},
Expand Down
19 changes: 15 additions & 4 deletions lnrpc/walletrpc/walletkit.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions lnrpc/walletrpc/walletkit.proto
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,12 @@ message SendOutputsRequest {

// The strategy to use for selecting coins during sending the outputs.
lnrpc.CoinSelectionStrategy coin_selection_strategy = 6;

/*
An optional address to send change to. If not set, a wallet-derived
address is used.
*/
string change_address = 7;
}
message SendOutputsResponse {
/*
Expand Down
4 changes: 4 additions & 0 deletions lnrpc/walletrpc/walletkit.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2116,6 +2116,10 @@
"coin_selection_strategy": {
"$ref": "#/definitions/lnrpcCoinSelectionStrategy",
"description": "The strategy to use for selecting coins during sending the outputs."
},
"change_address": {
"type": "string",
"description": "An optional address to send change to. If not set, a wallet-derived\naddress is used."
}
}
},
Expand Down
24 changes: 23 additions & 1 deletion lnrpc/walletrpc/walletkit_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"maps"
Expand Down Expand Up @@ -823,11 +824,32 @@ func (w *WalletKit) SendOutputs(ctx context.Context,
return nil, err
}

var changeAddr btcutil.Address
if req.ChangeAddress != "" {
changeAddr, err = btcutil.DecodeAddress(
req.ChangeAddress, w.cfg.ChainParams,
)
if err != nil {
return nil, fmt.Errorf("invalid change_address: %w", err)
}
if !changeAddr.IsForNet(w.cfg.ChainParams) {
return nil, fmt.Errorf("change_address %v is not valid "+
"for network %v", req.ChangeAddress,
w.cfg.ChainParams.Name)
}
decodedChangeAddr, _ := hex.DecodeString(req.ChangeAddress)
if _, err = btcec.ParsePubKey(decodedChangeAddr); err == nil {
return nil, fmt.Errorf("cannot send change to a bare " +
"public key")
}
}

// Now that we have the outputs mapped and checked for the reserve
// requirement, we can request that the wallet attempts to create this
// transaction.
tx, err := w.cfg.Wallet.SendOutputs(
nil, outputsToCreate, chainfee.SatPerKWeight(req.SatPerKw),
nil, outputsToCreate, changeAddr,
chainfee.SatPerKWeight(req.SatPerKw),
minConfs, label, coinSelectionStrategy,
)
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions lntest/mock/walletcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,16 @@ func (w *WalletController) ImportTaprootScript(waddrmgr.KeyScope,

// SendOutputs currently returns dummy values.
func (w *WalletController) SendOutputs(fn.Set[wire.OutPoint], []*wire.TxOut,
chainfee.SatPerKWeight, int32, string, base.CoinSelectionStrategy) (
*wire.MsgTx, error) {
btcutil.Address, chainfee.SatPerKWeight, int32, string,
base.CoinSelectionStrategy) (*wire.MsgTx, error) {

return nil, nil
}

// CreateSimpleTx currently returns dummy values.
func (w *WalletController) CreateSimpleTx(fn.Set[wire.OutPoint], []*wire.TxOut,
chainfee.SatPerKWeight, int32, base.CoinSelectionStrategy,
bool) (*txauthor.AuthoredTx, error) {
btcutil.Address, chainfee.SatPerKWeight, int32,
base.CoinSelectionStrategy, bool) (*txauthor.AuthoredTx, error) {

return nil, nil
}
Expand Down
Loading
Loading