diff --git a/cmd/loop/staticaddr.go b/cmd/loop/staticaddr.go index c973a002..b78d3e64 100644 --- a/cmd/loop/staticaddr.go +++ b/cmd/loop/staticaddr.go @@ -4,11 +4,14 @@ import ( "context" "errors" "fmt" + "math" + "os" "sort" "strings" "github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/loop/staticaddr/address" "github.com/lightninglabs/loop/staticaddr/loopin" "github.com/lightninglabs/loop/swapserverrpc" "github.com/lightningnetwork/lnd" @@ -17,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/routing/route" "github.com/urfave/cli/v3" + "golang.org/x/term" ) func init() { @@ -29,6 +33,7 @@ var staticAddressCommands = &cli.Command{ Usage: "perform on-chain to off-chain swaps using static addresses.", Commands: []*cli.Command{ newStaticAddressCommand, + depositStaticAddressCommand, listUnspentCommand, listDepositsCommand, listWithdrawalsCommand, @@ -45,15 +50,99 @@ var newStaticAddressCommand = &cli.Command{ Aliases: []string{"n"}, Usage: "Create a new static loop in address.", Description: ` - Requests a new static loop in address from the server. Funds that are - sent to this address will be locked by a 2:2 multisig between us and the - loop server, or a timeout path that we can sweep once it opens up. The - funds can either be cooperatively spent with a signature from the server - or looped in. + Creates a new static loop in address. On a fresh installation loopd + initializes the static-address generation during startup. Funds sent to the + address will be locked by a 2:2 multisig between us and the loop server, or + a timeout path that we can sweep once it opens up. The funds can either be + cooperatively spent with a signature from the server or looped in. `, Action: newStaticAddress, } +var depositStaticAddressCommand = &cli.Command{ + Name: "deposit", + Usage: "Create and fund a new static loop in address.", + Description: ` + Creates a new static loop in address and initiates a deposit by calling + lnd's SendCoins API with the newly created address as the destination. + `, + Flags: []cli.Flag{ + &cli.Int64Flag{ + Name: "amt", + Usage: "the number of bitcoin denominated in satoshis " + + "to send to the new static address", + }, + &cli.BoolFlag{ + Name: "sweepall", + 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 selected utxos (when " + + "supplied) to the new static address", + }, + &cli.Int64Flag{ + Name: "conf_target", + Usage: "(optional) the number of blocks that the " + + "funding transaction should confirm in, will " + + "be used for fee estimation", + }, + &cli.Int64Flag{ + Name: "sat_per_byte", + Usage: "Deprecated, use sat_per_vbyte instead.", + Hidden: true, + }, + &cli.Uint64Flag{ + Name: "sat_per_vbyte", + Usage: "(optional) a manual fee expressed in " + + "sat/vbyte that should be used when crafting " + + "the funding transaction", + }, + &cli.Uint64Flag{ + Name: "min_confs", + Usage: "(optional) the minimum number of confirmations " + + "each one of your outputs used for the funding " + + "transaction must satisfy", + Value: defaultUtxoMinConf, + }, + &cli.BoolFlag{ + Name: "force, f", + Usage: "if set, the funding transaction will be " + + "broadcast without asking for confirmation", + }, + staticAddressCoinSelectionStrategyFlag, + &cli.StringSliceFlag{ + Name: "utxo", + Usage: "a utxo specified as outpoint(tx:idx) which " + + "will be used as input for the funding " + + "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", + }, + staticAddressFundingLabelFlag, + }, + Action: depositStaticAddress, +} + +var ( + staticAddressCoinSelectionStrategyFlag = &cli.StringFlag{ + Name: "coin_selection_strategy", + Usage: "(optional) the strategy to use for selecting coins. " + + "Possible values are 'largest', 'random', or " + + "'global-config'. If either 'largest' or 'random' is " + + "specified, it will override the globally configured " + + "strategy in lnd.conf", + Value: "global-config", + } + + staticAddressFundingLabelFlag = &cli.StringFlag{ + Name: "label", + Usage: "(optional) a label for the funding transaction", + } +) + func newStaticAddress(ctx context.Context, cmd *cli.Command) error { if cmd.NArg() > 0 { return showCommandHelp(ctx, cmd) @@ -82,6 +171,186 @@ func newStaticAddress(ctx context.Context, cmd *cli.Command) error { return nil } +func depositStaticAddress(ctx context.Context, cmd *cli.Command) error { + if cmd.NArg() > 0 { + return showCommandHelp(ctx, cmd) + } + + client, cleanup, err := getClient(cmd) + if err != nil { + return err + } + defer cleanup() + + req, err := staticAddressDepositRequest(cmd, "") + if err != nil { + return err + } + + err = maybeDisplayNewAddressWarning(ctx, client) + if err != nil { + return err + } + + addrResp, err := client.NewStaticAddress( + ctx, &looprpc.NewStaticAddressRequest{}, + ) + if err != nil { + return err + } + + req.GetSendCoinsRequest().Addr = addrResp.Address + + if !(cmd.Bool("force") || cmd.Bool("f")) && + term.IsTerminal(int(os.Stdout.Fd())) { + + if !confirmStaticAddressDeposit(req, addrResp.Address) { + return nil + } + } + + resp, err := client.NewStaticAddress(ctx, req) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +func staticAddressDepositRequest( + cmd *cli.Command, addr string) (*looprpc.NewStaticAddressRequest, error) { + + if !cmd.IsSet("amt") && !cmd.Bool("sweepall") { + return nil, errors.New("amount argument missing") + } + + amount := cmd.Int64("amt") + if cmd.IsSet("amt") && amount <= 0 { + return nil, errors.New("amount must be positive") + } + + if amount != 0 && cmd.Bool("sweepall") { + return nil, errors.New("amount cannot be set if " + + "attempting to sweep all coins out of the wallet") + } + + feeRateFlag, err := checkNotBothSet( + cmd, "sat_per_vbyte", "sat_per_byte", + ) + if err != nil { + return nil, err + } + + if _, err := checkNotBothSet( + cmd, feeRateFlag, "conf_target", + ); err != nil { + return nil, err + } + + var satPerByte int64 + if cmd.IsSet("sat_per_byte") { + satPerByte = cmd.Int64("sat_per_byte") + if satPerByte < 0 { + return nil, fmt.Errorf("sat_per_byte must be " + + "non-negative") + } + } + + confTarget := cmd.Int64("conf_target") + if confTarget < 0 { + return nil, fmt.Errorf("conf_target must be non-negative") + } + if confTarget > math.MaxInt32 { + return nil, fmt.Errorf("conf_target exceeds maximum " + + "int32 value") + } + + minConfs := cmd.Uint64("min_confs") + if minConfs > math.MaxInt32 { + return nil, fmt.Errorf("min_confs exceeds maximum " + + "int32 value") + } + + var outpoints []*lnrpc.OutPoint + utxos := cmd.StringSlice("utxo") + if len(utxos) > 0 { + outpoints, err = lnd.UtxosToOutpoints(utxos) + if err != nil { + return nil, fmt.Errorf("unable to decode utxos: %w", err) + } + } + + coinSelectionStrategy, err := parseStaticAddressCoinSelectionStrategy(cmd) + if err != nil { + return nil, err + } + + return &looprpc.NewStaticAddressRequest{ + SendCoinsRequest: &lnrpc.SendCoinsRequest{ + Addr: addr, + Amount: amount, + TargetConf: int32(confTarget), + SatPerVbyte: cmd.Uint64("sat_per_vbyte"), + SatPerByte: satPerByte, + SendAll: cmd.Bool("sweepall"), + Label: cmd.String( + staticAddressFundingLabelFlag.Name, + ), + MinConfs: int32(minConfs), + SpendUnconfirmed: minConfs == 0, + CoinSelectionStrategy: coinSelectionStrategy, + Outpoints: outpoints, + }, + }, nil +} + +func parseStaticAddressCoinSelectionStrategy(cmd *cli.Command) ( + lnrpc.CoinSelectionStrategy, error) { + + if !cmd.IsSet(staticAddressCoinSelectionStrategyFlag.Name) { + return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG, + nil + } + + switch strategy := cmd.String( + staticAddressCoinSelectionStrategyFlag.Name); strategy { + case "global-config": + return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG, + nil + + case "largest": + return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil + + case "random": + return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil + + default: + return 0, fmt.Errorf("unknown coin selection strategy %v", + strategy) + } +} + +func confirmStaticAddressDeposit(req *looprpc.NewStaticAddressRequest, + addr string) bool { + + sendCoinsReq := req.GetSendCoinsRequest() + if sendCoinsReq.GetSendAll() { + fmt.Println("Amount: sweep all eligible wallet funds") + } else { + fmt.Printf("Amount: %d\n", sendCoinsReq.GetAmount()) + } + + fmt.Printf("Destination address: %s\n", addr) + fmt.Printf("Confirm funding transaction (yes/no): ") + + var answer string + fmt.Scanln(&answer) + + return answer == "yes" || answer == "y" +} + var listUnspentCommand = &cli.Command{ Name: "listunspent", Aliases: []string{"l"}, @@ -846,6 +1115,24 @@ func lowConfDepositWarning(allDeposits []*looprpc.Deposit, ) } +func maybeDisplayNewAddressWarning(ctx context.Context, + client looprpc.SwapClientClient) error { + + _, err := client.GetStaticAddressSummary( + ctx, &looprpc.StaticAddressSummaryRequest{}, + ) + switch { + case err == nil: + return nil + + case strings.Contains(err.Error(), address.ErrNoStaticAddress.Error()): + return displayNewAddressWarning() + + default: + return nil + } +} + func displayNewAddressWarning() error { fmt.Printf("\nWARNING: Be aware that loosing your l402.token file in " + ".loop under your home directory will take your ability to " + diff --git a/cmd/loop/staticaddr_test.go b/cmd/loop/staticaddr_test.go index 2f7bcfb8..71aafa4c 100644 --- a/cmd/loop/staticaddr_test.go +++ b/cmd/loop/staticaddr_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "strings" "testing" @@ -12,8 +13,35 @@ import ( "github.com/lightninglabs/loop/staticaddr/deposit" "github.com/lightninglabs/loop/staticaddr/loopin" "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" ) +func TestStaticAddressDepositRequestAllowsNoUtxos(t *testing.T) { + t.Parallel() + + var req *looprpc.NewStaticAddressRequest + cmd := &cli.Command{ + Name: "deposit", + Flags: depositStaticAddressCommand.Flags, + Action: func(_ context.Context, cmd *cli.Command) error { + var err error + req, err = staticAddressDepositRequest( + cmd, "bcrt1ptestaddress", + ) + + return err + }, + } + + err := cmd.Run(context.Background(), []string{ + "deposit", "--amt", "1000000", + }) + require.NoError(t, err) + require.Equal(t, "bcrt1ptestaddress", req.GetSendCoinsRequest().Addr) + require.EqualValues(t, 1_000_000, req.GetSendCoinsRequest().Amount) + require.Empty(t, req.GetSendCoinsRequest().Outpoints) +} + // TestLowConfDepositWarningConfirmedOnly verifies confirmed deposits below the // conservative warning threshold are included in the warning text. func TestLowConfDepositWarningConfirmedOnly(t *testing.T) { diff --git a/cmd/loop/testdata/sessions/static-loop-in/01_loop-static-new.json b/cmd/loop/testdata/sessions/static-loop-in/01_loop-static-new.json index 69a7ab55..e196eaa4 100644 --- a/cmd/loop/testdata/sessions/static-loop-in/01_loop-static-new.json +++ b/cmd/loop/testdata/sessions/static-loop-in/01_loop-static-new.json @@ -40,7 +40,8 @@ "event": "request", "message_type": "looprpc.NewStaticAddressRequest", "payload": { - "client_key": "" + "client_key": "", + "send_coins_request": null } } }, @@ -64,7 +65,8 @@ "lines": [ "{\n", " \"address\": \"bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw\",\n", - " \"expiry\": 14400\n", + " \"expiry\": 14400,\n", + " \"send_coins_response\": null\n", "}\n" ] } diff --git a/cmd/loop/testdata/sessions/static-loop-in/04_loop-static.json b/cmd/loop/testdata/sessions/static-loop-in/04_loop-static.json index 2b5335b7..322f930c 100644 --- a/cmd/loop/testdata/sessions/static-loop-in/04_loop-static.json +++ b/cmd/loop/testdata/sessions/static-loop-in/04_loop-static.json @@ -25,6 +25,7 @@ "\n", "COMMANDS:\n", " new, n Create a new static loop in address.\n", + " deposit Create and fund a new static loop in address.\n", " listunspent, l List unspent static address outputs.\n", " listdeposits Displays static address deposits. A filter can be applied to only show deposits in a specific state.\n", " listwithdrawals Display a summary of past withdrawals.\n", diff --git a/docs/loop.1 b/docs/loop.1 index 1e21905b..9d758516 100644 --- a/docs/loop.1 +++ b/docs/loop.1 @@ -427,6 +427,40 @@ Create a new static loop in address. .PP \fB--help, -h\fP: show help +.SS deposit +.PP +Create and fund a new static loop in address. + +.PP +\fB--amt\fP="": the number of bitcoin denominated in satoshis to send to the new static address (default: 0) + +.PP +\fB--coin_selection_strategy\fP="": (optional) the strategy to use for selecting coins. Possible values are 'largest', 'random', or 'global-config'. If either 'largest' or 'random' is specified, it will override the globally configured strategy in lnd.conf (default: global-config) + +.PP +\fB--conf_target\fP="": (optional) the number of blocks that the funding transaction should confirm in, will be used for fee estimation (default: 0) + +.PP +\fB--force\fP: if set, the funding transaction will be broadcast without asking for confirmation + +.PP +\fB--help, -h\fP: show help + +.PP +\fB--label\fP="": (optional) a label for the funding transaction + +.PP +\fB--min_confs\fP="": (optional) the minimum number of confirmations each one of your outputs used for the funding transaction must satisfy (default: 1) + +.PP +\fB--sat_per_vbyte\fP="": (optional) a manual fee expressed in sat/vbyte that should be used when crafting the funding transaction (default: 0) + +.PP +\fB--sweepall\fP: 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 selected utxos (when supplied) to the new static address + +.PP +\fB--utxo\fP="": a utxo specified as outpoint(tx:idx) which will be used as input for the funding 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 (default: []) + .SS listunspent, l List unspent static address outputs. diff --git a/docs/loop.md b/docs/loop.md index 316cebdc..1daa25b9 100644 --- a/docs/loop.md +++ b/docs/loop.md @@ -532,7 +532,7 @@ The following flags are supported: Create a new static loop in address. -Requests a new static loop in address from the server. Funds that are sent to this address will be locked by a 2:2 multisig between us and the loop server, or a timeout path that we can sweep once it opens up. The funds can either be cooperatively spent with a signature from the server or looped in. +Creates a new static loop in address. On a fresh installation loopd initializes the static-address generation during startup. Funds sent to the address will be locked by a 2:2 multisig between us and the loop server, or a timeout path that we can sweep once it opens up. The funds can either be cooperatively spent with a signature from the server or looped in. Usage: @@ -546,6 +546,33 @@ The following flags are supported: |-----------------|-------------|------|:-------------:| | `--help` (`-h`) | show help | bool | `false` | +### `static deposit` subcommand + +Create and fund a new static loop in address. + +Creates a new static loop in address and initiates a deposit by calling lnd's SendCoins API with the newly created address as the destination. + +Usage: + +```bash +$ loop [GLOBAL FLAGS] static deposit [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Type | Default value | +|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|:---------------:| +| `--amt="…"` | the number of bitcoin denominated in satoshis to send to the new static address | int | `0` | +| `--sweepall` | 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 selected utxos (when supplied) to the new static address | bool | `false` | +| `--conf_target="…"` | (optional) the number of blocks that the funding transaction should confirm in, will be used for fee estimation | int | `0` | +| `--sat_per_vbyte="…"` | (optional) a manual fee expressed in sat/vbyte that should be used when crafting the funding transaction | uint | `0` | +| `--min_confs="…"` | (optional) the minimum number of confirmations each one of your outputs used for the funding transaction must satisfy | uint | `1` | +| `--force` | if set, the funding transaction will be broadcast without asking for confirmation | bool | `false` | +| `--coin_selection_strategy="…"` | (optional) the strategy to use for selecting coins. Possible values are 'largest', 'random', or 'global-config'. If either 'largest' or 'random' is specified, it will override the globally configured strategy in lnd.conf | string | `global-config` | +| `--utxo="…"` | a utxo specified as outpoint(tx:idx) which will be used as input for the funding 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 | string | `[]` | +| `--label="…"` | (optional) a label for the funding transaction | string | +| `--help` (`-h`) | show help | bool | `false` | + ### `static listunspent` subcommand (aliases: `l`) List unspent static address outputs. diff --git a/go.mod b/go.mod index 4a32aa7d..34dfe611 100644 --- a/go.mod +++ b/go.mod @@ -182,7 +182,7 @@ require ( golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect + golang.org/x/term v0.43.0 golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.44.0 // indirect diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index b55be712..7758e4fc 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -38,6 +38,8 @@ import ( "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swapserverrpc" "github.com/lightninglabs/taproot-assets/rfqmath" + lndlabels "github.com/lightningnetwork/lnd/labels" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/queue" @@ -45,6 +47,7 @@ import ( "github.com/lightningnetwork/lnd/zpay32" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) const ( @@ -1855,20 +1858,169 @@ func rpcInstantOut(instantOut *instantout.InstantOut) *looprpc.InstantOut { // NewStaticAddress is the rpc endpoint for loop clients to request a new static // address. func (s *swapClientServer) NewStaticAddress(ctx context.Context, - _ *looprpc.NewStaticAddressRequest) ( + req *looprpc.NewStaticAddressRequest) ( *looprpc.NewStaticAddressResponse, error) { + sendCoinsReq := req.GetSendCoinsRequest() + if err := validateStaticAddressSendCoinsRequest(sendCoinsReq); err != nil { + return nil, err + } + + if sendCoinsReq.GetAddr() != "" { + return s.fundExistingStaticAddress(ctx, sendCoinsReq) + } + staticAddress, expiry, err := s.staticAddressManager.NewAddress(ctx) if err != nil { return nil, err } + sendCoinsResp, err := s.sendCoinsToStaticAddress( + ctx, staticAddress.String(), sendCoinsReq, + ) + if err != nil { + return nil, fmt.Errorf("static address %s created, but "+ + "funding transaction failed: %w", staticAddress, err) + } + return &looprpc.NewStaticAddressResponse{ - Address: staticAddress.String(), - Expiry: uint32(expiry), + Address: staticAddress.String(), + Expiry: uint32(expiry), + SendCoinsResponse: sendCoinsResp, }, nil } +func (s *swapClientServer) fundExistingStaticAddress(ctx context.Context, + req *lnrpc.SendCoinsRequest) (*looprpc.NewStaticAddressResponse, error) { + + staticAddress, expiry, err := s.staticAddressForDeposit(ctx, req.Addr) + if err != nil { + return nil, err + } + + sendCoinsResp, err := s.sendCoinsToStaticAddress( + ctx, staticAddress, req, + ) + if err != nil { + return nil, fmt.Errorf("static address %s funding transaction "+ + "failed: %w", staticAddress, err) + } + + return &looprpc.NewStaticAddressResponse{ + Address: staticAddress, + Expiry: expiry, + SendCoinsResponse: sendCoinsResp, + }, nil +} + +func (s *swapClientServer) staticAddressForDeposit(ctx context.Context, + addr string) (string, uint32, error) { + + addresses, err := s.staticAddressManager.GetAllAddresses(ctx) + if err != nil { + return "", 0, err + } + + for _, params := range addresses { + staticAddress, err := s.staticAddressManager.GetTaprootAddress( + params.ClientPubkey, params.ServerPubkey, + int64(params.Expiry), + ) + if err != nil { + return "", 0, err + } + + if staticAddress.String() == addr { + return addr, params.Expiry, nil + } + } + + return "", 0, status.Errorf(codes.InvalidArgument, + "send_coins_request.addr is not a known static address") +} + +func validateStaticAddressSendCoinsRequest(req *lnrpc.SendCoinsRequest) error { + if req == nil { + return nil + } + + switch { + case req.Amount < 0: + return status.Error(codes.InvalidArgument, "send_coins_request."+ + "amount must be non-negative") + + case req.Amount == 0 && !req.SendAll: + return status.Error(codes.InvalidArgument, "send_coins_request "+ + "must set amount or send_all") + + case req.Amount != 0 && req.SendAll: + return status.Error(codes.InvalidArgument, "send_coins_request."+ + "amount cannot be set when send_all is true") + + case req.TargetConf < 0: + return status.Error(codes.InvalidArgument, "send_coins_request."+ + "target_conf must be non-negative") + + case req.SatPerByte < 0: //nolint:staticcheck + return status.Error(codes.InvalidArgument, "send_coins_request."+ + "sat_per_byte must be non-negative") + + case req.TargetConf != 0 && + (req.SatPerVbyte != 0 || req.SatPerByte != 0): //nolint:staticcheck + + return status.Error(codes.InvalidArgument, "send_coins_request "+ + "can set either target_conf or a fee rate, but not both") + + case req.SatPerVbyte != 0 && req.SatPerByte != 0: //nolint:staticcheck + return status.Error(codes.InvalidArgument, "send_coins_request "+ + "can set either sat_per_vbyte or sat_per_byte, but not "+ + "both") + + case req.MinConfs < 0: + return status.Error(codes.InvalidArgument, "send_coins_request."+ + "min_confs must be non-negative") + } + + if _, err := lnrpc.ExtractMinConfs( + req.MinConfs, req.SpendUnconfirmed, + ); err != nil { + return status.Errorf(codes.InvalidArgument, "send_coins_request "+ + "min_confs/spend_unconfirmed invalid: %v", err) + } + + if _, err := lndlabels.ValidateAPI(req.Label); err != nil { + return status.Errorf(codes.InvalidArgument, "send_coins_request "+ + "label invalid: %v", err) + } + + if _, err := lnrpc.UnmarshallCoinSelectionStrategy( + req.CoinSelectionStrategy, nil, + ); err != nil { + return status.Errorf(codes.InvalidArgument, "send_coins_request "+ + "coin_selection_strategy invalid: %v", err) + } + + return nil +} + +func (s *swapClientServer) sendCoinsToStaticAddress(ctx context.Context, + addr string, req *lnrpc.SendCoinsRequest) (*lnrpc.SendCoinsResponse, + error) { + + if req == nil { + return nil, nil + } + + sendCoinsReq := proto.Clone(req).(*lnrpc.SendCoinsRequest) + sendCoinsReq.Addr = addr + + rawCtx, timeout, rawClient := s.lnd.Client.RawClientWithMacAuth(ctx) + rawCtx, cancel := context.WithTimeout(rawCtx, timeout) + defer cancel() + + return rawClient.SendCoins(rawCtx, sendCoinsReq) +} + // ListUnspentDeposits returns a list of utxos behind the static address. func (s *swapClientServer) ListUnspentDeposits(ctx context.Context, req *looprpc.ListUnspentDepositsRequest) ( diff --git a/loopd/swapclient_server_staticaddr_test.go b/loopd/swapclient_server_staticaddr_test.go index 9478109b..bb588d5c 100644 --- a/loopd/swapclient_server_staticaddr_test.go +++ b/loopd/swapclient_server_staticaddr_test.go @@ -2,6 +2,7 @@ package loopd import ( "context" + "strings" "testing" "github.com/btcsuite/btcd/btcec/v2" @@ -14,6 +15,7 @@ import ( "github.com/lightninglabs/loop/staticaddr/deposit" "github.com/lightninglabs/loop/staticaddr/script" mock_lnd "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet" "github.com/stretchr/testify/require" ) @@ -168,6 +170,161 @@ func newTestStaticAddressContext(t *testing.T) (*address.Manager, return addrMgr, mock } +func TestValidateStaticAddressSendCoinsRequest(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + req *lnrpc.SendCoinsRequest + err string + }{ + { + name: "nil", + }, + { + name: "amount", + req: &lnrpc.SendCoinsRequest{ + Amount: 10_000, + }, + }, + { + name: "send all", + req: &lnrpc.SendCoinsRequest{ + SendAll: true, + }, + }, + { + name: "existing addr", + req: &lnrpc.SendCoinsRequest{ + Addr: "bcrt1ptestaddress", + Amount: 10_000, + }, + }, + { + name: "missing amount", + req: &lnrpc.SendCoinsRequest{}, + err: "must set amount or send_all", + }, + { + name: "negative amount", + req: &lnrpc.SendCoinsRequest{ + Amount: -1, + }, + err: "amount must be non-negative", + }, + { + name: "amount and send all", + req: &lnrpc.SendCoinsRequest{ + Amount: 10_000, + SendAll: true, + }, + err: "amount cannot be set when send_all is true", + }, + { + name: "target and fee rate", + req: &lnrpc.SendCoinsRequest{ + Amount: 10_000, + TargetConf: 6, + SatPerVbyte: 1, + SatPerByte: 0, + SendAll: false, + MinConfs: 1, + Outpoints: nil, + SpendUnconfirmed: false, + }, + err: "can set either target_conf or a fee rate", + }, + { + name: "both fee rates", + req: &lnrpc.SendCoinsRequest{ + Amount: 10_000, + SatPerVbyte: 1, + SatPerByte: 1, + }, + err: "can set either sat_per_vbyte or sat_per_byte", + }, + { + name: "negative min confs", + req: &lnrpc.SendCoinsRequest{ + Amount: 10_000, + MinConfs: -1, + }, + err: "min_confs must be non-negative", + }, + { + name: "min confs with spend unconfirmed", + req: &lnrpc.SendCoinsRequest{ + Amount: 10_000, + MinConfs: 1, + SpendUnconfirmed: true, + }, + err: "spend_unconfirmed invalid", + }, + { + name: "invalid label", + req: &lnrpc.SendCoinsRequest{ + Amount: 10_000, + Label: strings.Repeat("x", 501), + }, + err: "label invalid", + }, + { + name: "invalid coin selection strategy", + req: &lnrpc.SendCoinsRequest{ + Amount: 10_000, + CoinSelectionStrategy: lnrpc.CoinSelectionStrategy(99), + }, + err: "coin_selection_strategy invalid", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + err := validateStaticAddressSendCoinsRequest(test.req) + if test.err == "" { + require.NoError(t, err) + return + } + + require.ErrorContains(t, err, test.err) + }) + } +} + +func TestStaticAddressForDeposit(t *testing.T) { + t.Parallel() + + ctx := context.Background() + addrMgr, _ := newTestStaticAddressContext(t) + server := &swapClientServer{ + staticAddressManager: addrMgr, + } + + addresses, err := addrMgr.GetAllAddresses(ctx) + require.NoError(t, err) + require.Len(t, addresses, 1) + + expectedAddr, err := addrMgr.GetTaprootAddress( + addresses[0].ClientPubkey, addresses[0].ServerPubkey, + int64(addresses[0].Expiry), + ) + require.NoError(t, err) + + addr, expiry, err := server.staticAddressForDeposit( + ctx, expectedAddr.String(), + ) + require.NoError(t, err) + require.Equal(t, expectedAddr.String(), addr) + require.Equal(t, addresses[0].Expiry, expiry) + + _, _, err = server.staticAddressForDeposit( + ctx, "bcrt1punknownstaticaddress", + ) + require.ErrorContains(t, err, "not a known static address") +} + // TestListStaticAddressDepositsReturnsVisibleDeposits verifies normal deposit // listings include visible deposit records. func TestListStaticAddressDepositsReturnsVisibleDeposits(t *testing.T) { diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index ec324648..f4436254 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -4873,9 +4873,14 @@ func (x *InstantOut) GetSweepTxId() string { type NewStaticAddressRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The client's public key for the 2-of-2 MuSig2 taproot static address. - ClientKey []byte `protobuf:"bytes,1,opt,name=client_key,json=clientKey,proto3" json:"client_key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ClientKey []byte `protobuf:"bytes,1,opt,name=client_key,json=clientKey,proto3" json:"client_key,omitempty"` + // If set, loopd initiates a deposit by calling lnd's SendCoins API. If the + // request's addr field is empty, loopd creates and funds a new static + // address. If addr is set, it must be an existing static address known to + // loopd. + SendCoinsRequest *lnrpc.SendCoinsRequest `protobuf:"bytes,2,opt,name=send_coins_request,json=sendCoinsRequest,proto3" json:"send_coins_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NewStaticAddressRequest) Reset() { @@ -4915,14 +4920,23 @@ func (x *NewStaticAddressRequest) GetClientKey() []byte { return nil } +func (x *NewStaticAddressRequest) GetSendCoinsRequest() *lnrpc.SendCoinsRequest { + if x != nil { + return x.SendCoinsRequest + } + return nil +} + type NewStaticAddressResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The taproot static address. Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` // The CSV expiry of the static address. - Expiry uint32 `protobuf:"varint,2,opt,name=expiry,proto3" json:"expiry,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Expiry uint32 `protobuf:"varint,2,opt,name=expiry,proto3" json:"expiry,omitempty"` + // The response from lnd's SendCoins API, if a deposit was initiated. + SendCoinsResponse *lnrpc.SendCoinsResponse `protobuf:"bytes,3,opt,name=send_coins_response,json=sendCoinsResponse,proto3" json:"send_coins_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NewStaticAddressResponse) Reset() { @@ -4969,6 +4983,13 @@ func (x *NewStaticAddressResponse) GetExpiry() uint32 { return 0 } +func (x *NewStaticAddressResponse) GetSendCoinsResponse() *lnrpc.SendCoinsResponse { + if x != nil { + return x.SendCoinsResponse + } + return nil +} + type ListUnspentDepositsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The number of minimum confirmations a utxo must have to be listed. @@ -6989,13 +7010,15 @@ const file_client_proto_rawDesc = "" + "\x05state\x18\x02 \x01(\tR\x05state\x12\x16\n" + "\x06amount\x18\x03 \x01(\x04R\x06amount\x12'\n" + "\x0freservation_ids\x18\x04 \x03(\fR\x0ereservationIds\x12\x1e\n" + - "\vsweep_tx_id\x18\x05 \x01(\tR\tsweepTxId\"8\n" + + "\vsweep_tx_id\x18\x05 \x01(\tR\tsweepTxId\"\x7f\n" + "\x17NewStaticAddressRequest\x12\x1d\n" + "\n" + - "client_key\x18\x01 \x01(\fR\tclientKey\"L\n" + + "client_key\x18\x01 \x01(\fR\tclientKey\x12E\n" + + "\x12send_coins_request\x18\x02 \x01(\v2\x17.lnrpc.SendCoinsRequestR\x10sendCoinsRequest\"\x96\x01\n" + "\x18NewStaticAddressResponse\x12\x18\n" + "\aaddress\x18\x01 \x01(\tR\aaddress\x12\x16\n" + - "\x06expiry\x18\x02 \x01(\rR\x06expiry\"V\n" + + "\x06expiry\x18\x02 \x01(\rR\x06expiry\x12H\n" + + "\x13send_coins_response\x18\x03 \x01(\v2\x18.lnrpc.SendCoinsResponseR\x11sendCoinsResponse\"V\n" + "\x1aListUnspentDepositsRequest\x12\x1b\n" + "\tmin_confs\x18\x01 \x01(\x05R\bminConfs\x12\x1b\n" + "\tmax_confs\x18\x02 \x01(\x05R\bmaxConfs\"B\n" + @@ -7349,7 +7372,9 @@ var file_client_proto_goTypes = []any{ nil, // 89: looprpc.LiquidityParameters.EasyAssetParamsEntry (*lnrpc.OpenChannelRequest)(nil), // 90: lnrpc.OpenChannelRequest (*swapserverrpc.RouteHint)(nil), // 91: looprpc.RouteHint - (*lnrpc.OutPoint)(nil), // 92: lnrpc.OutPoint + (*lnrpc.SendCoinsRequest)(nil), // 92: lnrpc.SendCoinsRequest + (*lnrpc.SendCoinsResponse)(nil), // 93: lnrpc.SendCoinsResponse + (*lnrpc.OutPoint)(nil), // 94: lnrpc.OutPoint } var file_client_proto_depIdxs = []int32{ 90, // 0: looprpc.StaticOpenChannelRequest.open_channel_request:type_name -> lnrpc.OpenChannelRequest @@ -7389,92 +7414,94 @@ var file_client_proto_depIdxs = []int32{ 51, // 34: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified 57, // 35: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation 64, // 36: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut - 69, // 37: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo - 92, // 38: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint - 7, // 39: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState - 80, // 40: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit - 81, // 41: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal - 82, // 42: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap - 7, // 43: looprpc.Deposit.state:type_name -> looprpc.DepositState - 80, // 44: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit - 8, // 45: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState - 80, // 46: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit - 91, // 47: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint - 80, // 48: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit - 87, // 49: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint - 87, // 50: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint - 46, // 51: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams - 14, // 52: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest - 15, // 53: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest - 17, // 54: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest - 19, // 55: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest - 22, // 56: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest - 27, // 57: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest - 53, // 58: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest - 28, // 59: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest - 31, // 60: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest - 28, // 61: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest - 31, // 62: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest - 34, // 63: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest - 36, // 64: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest - 36, // 65: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest - 38, // 66: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest - 42, // 67: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest - 12, // 68: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest - 44, // 69: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest - 48, // 70: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest - 50, // 71: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest - 55, // 72: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest - 58, // 73: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest - 60, // 74: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest - 62, // 75: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest - 65, // 76: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest - 67, // 77: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest - 70, // 78: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest - 72, // 79: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest - 74, // 80: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest - 76, // 81: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest - 78, // 82: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest - 83, // 83: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest - 10, // 84: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest - 16, // 85: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse - 16, // 86: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse - 18, // 87: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus - 21, // 88: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse - 23, // 89: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse - 18, // 90: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus - 54, // 91: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse - 30, // 92: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse - 33, // 93: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse - 29, // 94: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse - 32, // 95: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse - 35, // 96: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse - 37, // 97: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse - 37, // 98: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse - 39, // 99: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse - 43, // 100: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse - 13, // 101: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse - 45, // 102: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters - 49, // 103: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse - 52, // 104: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse - 56, // 105: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse - 59, // 106: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse - 61, // 107: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse - 63, // 108: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse - 66, // 109: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse - 68, // 110: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse - 71, // 111: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse - 73, // 112: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse - 75, // 113: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse - 77, // 114: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse - 79, // 115: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse - 84, // 116: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse - 11, // 117: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse - 85, // [85:118] is the sub-list for method output_type - 52, // [52:85] is the sub-list for method input_type - 52, // [52:52] is the sub-list for extension type_name - 52, // [52:52] is the sub-list for extension extendee - 0, // [0:52] is the sub-list for field type_name + 92, // 37: looprpc.NewStaticAddressRequest.send_coins_request:type_name -> lnrpc.SendCoinsRequest + 93, // 38: looprpc.NewStaticAddressResponse.send_coins_response:type_name -> lnrpc.SendCoinsResponse + 69, // 39: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo + 94, // 40: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint + 7, // 41: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState + 80, // 42: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit + 81, // 43: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal + 82, // 44: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap + 7, // 45: looprpc.Deposit.state:type_name -> looprpc.DepositState + 80, // 46: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit + 8, // 47: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState + 80, // 48: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit + 91, // 49: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint + 80, // 50: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit + 87, // 51: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint + 87, // 52: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint + 46, // 53: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams + 14, // 54: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest + 15, // 55: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest + 17, // 56: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest + 19, // 57: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest + 22, // 58: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest + 27, // 59: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest + 53, // 60: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest + 28, // 61: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest + 31, // 62: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest + 28, // 63: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest + 31, // 64: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest + 34, // 65: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest + 36, // 66: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest + 36, // 67: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest + 38, // 68: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest + 42, // 69: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest + 12, // 70: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest + 44, // 71: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest + 48, // 72: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest + 50, // 73: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest + 55, // 74: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest + 58, // 75: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest + 60, // 76: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest + 62, // 77: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest + 65, // 78: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest + 67, // 79: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest + 70, // 80: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest + 72, // 81: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest + 74, // 82: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest + 76, // 83: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest + 78, // 84: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest + 83, // 85: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest + 10, // 86: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest + 16, // 87: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse + 16, // 88: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse + 18, // 89: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus + 21, // 90: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse + 23, // 91: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse + 18, // 92: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus + 54, // 93: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse + 30, // 94: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse + 33, // 95: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse + 29, // 96: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse + 32, // 97: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse + 35, // 98: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse + 37, // 99: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse + 37, // 100: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse + 39, // 101: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse + 43, // 102: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse + 13, // 103: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse + 45, // 104: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters + 49, // 105: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse + 52, // 106: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse + 56, // 107: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse + 59, // 108: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse + 61, // 109: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse + 63, // 110: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse + 66, // 111: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse + 68, // 112: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse + 71, // 113: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse + 73, // 114: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse + 75, // 115: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse + 77, // 116: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse + 79, // 117: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse + 84, // 118: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse + 11, // 119: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse + 87, // [87:120] is the sub-list for method output_type + 54, // [54:87] is the sub-list for method input_type + 54, // [54:54] is the sub-list for extension type_name + 54, // [54:54] is the sub-list for extension extendee + 0, // [0:54] is the sub-list for field type_name } func init() { file_client_proto_init() } diff --git a/looprpc/client.proto b/looprpc/client.proto index 844dade7..53db4f0a 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -1777,6 +1777,14 @@ message NewStaticAddressRequest { The client's public key for the 2-of-2 MuSig2 taproot static address. */ bytes client_key = 1; + + /* + If set, loopd initiates a deposit by calling lnd's SendCoins API. If the + request's addr field is empty, loopd creates and funds a new static + address. If addr is set, it must be an existing static address known to + loopd. + */ + lnrpc.SendCoinsRequest send_coins_request = 2; } message NewStaticAddressResponse { @@ -1789,6 +1797,11 @@ message NewStaticAddressResponse { The CSV expiry of the static address. */ uint32 expiry = 2; + + /* + The response from lnd's SendCoins API, if a deposit was initiated. + */ + lnrpc.SendCoinsResponse send_coins_response = 3; } message ListUnspentDepositsRequest { diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index 1c94febb..44bd4e51 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -1204,6 +1204,16 @@ } } }, + "lnrpcCoinSelectionStrategy": { + "type": "string", + "enum": [ + "STRATEGY_USE_GLOBAL_CONFIG", + "STRATEGY_LARGEST", + "STRATEGY_RANDOM" + ], + "default": "STRATEGY_USE_GLOBAL_CONFIG", + "description": " - STRATEGY_USE_GLOBAL_CONFIG: Use the coin selection strategy defined in the global configuration\n(lnd.conf).\n - STRATEGY_LARGEST: Select the largest available coins first during coin selection.\n - STRATEGY_RANDOM: Randomly select the available coins during coin selection." + }, "lnrpcCommitmentType": { "type": "string", "enum": [ @@ -1436,6 +1446,73 @@ } } }, + "lnrpcSendCoinsRequest": { + "type": "object", + "properties": { + "addr": { + "type": "string", + "title": "The address to send coins to" + }, + "amount": { + "type": "string", + "format": "int64", + "title": "The amount in satoshis to send" + }, + "target_conf": { + "type": "integer", + "format": "int32", + "description": "The target number of blocks that this transaction should be confirmed\nby." + }, + "sat_per_vbyte": { + "type": "string", + "format": "uint64", + "description": "A manual fee rate set in sat/vbyte that should be used when crafting the\ntransaction." + }, + "sat_per_byte": { + "type": "string", + "format": "int64", + "description": "Deprecated, use sat_per_vbyte.\nA manual fee rate set in sat/vbyte that should be used when crafting the\ntransaction." + }, + "send_all": { + "type": "boolean", + "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", + "description": "An optional label for the transaction, limited to 500 characters." + }, + "min_confs": { + "type": "integer", + "format": "int32", + "description": "The minimum number of confirmations each one of your outputs used for\nthe transaction must satisfy." + }, + "spend_unconfirmed": { + "type": "boolean", + "description": "Whether unconfirmed outputs should be used as inputs for the transaction." + }, + "coin_selection_strategy": { + "$ref": "#/definitions/lnrpcCoinSelectionStrategy", + "description": "The strategy to use for selecting coins." + }, + "outpoints": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/lnrpcOutPoint" + }, + "description": "A list of selected outpoints as inputs for the transaction." + } + } + }, + "lnrpcSendCoinsResponse": { + "type": "object", + "properties": { + "txid": { + "type": "string", + "title": "The transaction ID of the transaction" + } + } + }, "looprpcAbandonSwapResponse": { "type": "object" }, @@ -2515,6 +2592,10 @@ "type": "string", "format": "byte", "description": "The client's public key for the 2-of-2 MuSig2 taproot static address." + }, + "send_coins_request": { + "$ref": "#/definitions/lnrpcSendCoinsRequest", + "description": "If set, loopd initiates a deposit by calling lnd's SendCoins API. If the\nrequest's addr field is empty, loopd creates and funds a new static\naddress. If addr is set, it must be an existing static address known to\nloopd." } } }, @@ -2529,6 +2610,10 @@ "type": "integer", "format": "int64", "description": "The CSV expiry of the static address." + }, + "send_coins_response": { + "$ref": "#/definitions/lnrpcSendCoinsResponse", + "description": "The response from lnd's SendCoins API, if a deposit was initiated." } } }, diff --git a/looprpc/perms.go b/looprpc/perms.go index d646f667..0a1b5c99 100644 --- a/looprpc/perms.go +++ b/looprpc/perms.go @@ -82,7 +82,7 @@ var RequiredPermissions = map[string][]bakery.Op{ }}, "/looprpc.SwapClient/NewStaticAddress": {{ Entity: "swap", - Action: "read", + Action: "execute", }, { Entity: "loop", Action: "in",