Skip to content

Commit fd236c9

Browse files
committed
staticaddr: fund new address with sendcoins
Add SendCoins-backed funding through loop static deposit, keeping loop static new focused on address creation. Wire the nested lnd SendCoins request through the NewStaticAddress RPC and return the SendCoins response. Validate cheap SendCoins errors before address creation and require execute permissions for the funding-capable RPC. Regenerate CLI docs and looprpc artifacts, and add validation coverage for the static address funding request.
1 parent 7ae527e commit fd236c9

11 files changed

Lines changed: 904 additions & 115 deletions

cmd/loop/staticaddr.go

Lines changed: 274 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"math"
8+
"os"
79
"sort"
810
"strings"
911

@@ -18,6 +20,7 @@ import (
1820
"github.com/lightningnetwork/lnd/lnwallet"
1921
"github.com/lightningnetwork/lnd/routing/route"
2022
"github.com/urfave/cli/v3"
23+
"golang.org/x/term"
2124
)
2225

2326
func init() {
@@ -30,6 +33,7 @@ var staticAddressCommands = &cli.Command{
3033
Usage: "perform on-chain to off-chain swaps using static addresses.",
3134
Commands: []*cli.Command{
3235
newStaticAddressCommand,
36+
depositStaticAddressCommand,
3337
listUnspentCommand,
3438
listDepositsCommand,
3539
listWithdrawalsCommand,
@@ -44,19 +48,101 @@ var staticAddressCommands = &cli.Command{
4448
var newStaticAddressCommand = &cli.Command{
4549
Name: "new",
4650
Aliases: []string{"n"},
47-
Usage: "Return the static loop in address.",
51+
Usage: "Create a new static loop in address.",
4852
Description: `
49-
Returns the current static loop in address. On a fresh installation loopd
50-
initializes the current static-address generation during startup. If the
51-
address is still missing, this call will create it on demand. Funds sent
52-
to the address will be locked by a 2:2 multisig between us and the loop
53-
server, or a timeout path that we can sweep once it opens up. The funds
54-
can either be cooperatively spent with a signature from the server or
55-
looped in.
53+
Creates a new static loop in address. On a fresh installation loopd
54+
initializes the static-address generation during startup. Funds sent to the
55+
address will be locked by a 2:2 multisig between us and the loop server, or
56+
a timeout path that we can sweep once it opens up. The funds can either be
57+
cooperatively spent with a signature from the server or looped in.
5658
`,
5759
Action: newStaticAddress,
5860
}
5961

62+
var depositStaticAddressCommand = &cli.Command{
63+
Name: "deposit",
64+
Usage: "Create and fund a new static loop in address.",
65+
Description: `
66+
Creates a new static loop in address and initiates a deposit by calling
67+
lnd's SendCoins API with the newly created address as the destination.
68+
`,
69+
Flags: []cli.Flag{
70+
&cli.Int64Flag{
71+
Name: "amt",
72+
Usage: "the number of bitcoin denominated in satoshis " +
73+
"to send to the new static address",
74+
},
75+
&cli.BoolFlag{
76+
Name: "sweepall",
77+
Usage: "if set, then the amount field should be " +
78+
"unset. This indicates that the wallet will " +
79+
"attempt to sweep all outputs within the " +
80+
"wallet or all funds in selected utxos (when " +
81+
"supplied) to the new static address",
82+
},
83+
&cli.Int64Flag{
84+
Name: "conf_target",
85+
Usage: "(optional) the number of blocks that the " +
86+
"funding transaction should confirm in, will " +
87+
"be used for fee estimation",
88+
},
89+
&cli.Int64Flag{
90+
Name: "sat_per_byte",
91+
Usage: "Deprecated, use sat_per_vbyte instead.",
92+
Hidden: true,
93+
},
94+
&cli.Uint64Flag{
95+
Name: "sat_per_vbyte",
96+
Usage: "(optional) a manual fee expressed in " +
97+
"sat/vbyte that should be used when crafting " +
98+
"the funding transaction",
99+
},
100+
&cli.Uint64Flag{
101+
Name: "min_confs",
102+
Usage: "(optional) the minimum number of confirmations " +
103+
"each one of your outputs used for the funding " +
104+
"transaction must satisfy",
105+
Value: defaultUtxoMinConf,
106+
},
107+
&cli.BoolFlag{
108+
Name: "force, f",
109+
Usage: "if set, the funding transaction will be " +
110+
"broadcast without asking for confirmation",
111+
},
112+
staticAddressCoinSelectionStrategyFlag,
113+
&cli.StringSliceFlag{
114+
Name: "utxo",
115+
Usage: "a utxo specified as outpoint(tx:idx) which " +
116+
"will be used as input for the funding " +
117+
"transaction. This flag can be repeatedly used " +
118+
"to specify multiple utxos as inputs. The " +
119+
"selected utxos can either be entirely spent " +
120+
"by specifying the sweepall flag or a specified " +
121+
"amount can be spent in the utxos through " +
122+
"the amt flag",
123+
},
124+
staticAddressFundingLabelFlag,
125+
},
126+
Action: depositStaticAddress,
127+
}
128+
129+
var (
130+
staticAddressCoinSelectionStrategyFlag = &cli.StringFlag{
131+
Name: "coin_selection_strategy",
132+
Usage: "(optional) the strategy to use for selecting coins. " +
133+
"Possible values are 'largest', 'random', or " +
134+
"'global-config'. If either 'largest' or 'random' is " +
135+
"specified, it will override the globally configured " +
136+
"strategy in lnd.conf",
137+
Value: "global-config",
138+
}
139+
140+
staticAddressFundingLabelFlag = &cli.StringFlag{
141+
Name: "label",
142+
Usage: "(optional) a label for the funding transaction",
143+
}
144+
)
145+
60146
func newStaticAddress(ctx context.Context, cmd *cli.Command) error {
61147
if cmd.NArg() > 0 {
62148
return showCommandHelp(ctx, cmd)
@@ -85,6 +171,186 @@ func newStaticAddress(ctx context.Context, cmd *cli.Command) error {
85171
return nil
86172
}
87173

174+
func depositStaticAddress(ctx context.Context, cmd *cli.Command) error {
175+
if cmd.NArg() > 0 {
176+
return showCommandHelp(ctx, cmd)
177+
}
178+
179+
client, cleanup, err := getClient(cmd)
180+
if err != nil {
181+
return err
182+
}
183+
defer cleanup()
184+
185+
req, err := staticAddressDepositRequest(cmd, "")
186+
if err != nil {
187+
return err
188+
}
189+
190+
err = maybeDisplayNewAddressWarning(ctx, client)
191+
if err != nil {
192+
return err
193+
}
194+
195+
addrResp, err := client.NewStaticAddress(
196+
ctx, &looprpc.NewStaticAddressRequest{},
197+
)
198+
if err != nil {
199+
return err
200+
}
201+
202+
req.GetSendCoinsRequest().Addr = addrResp.Address
203+
204+
if !(cmd.Bool("force") || cmd.Bool("f")) &&
205+
term.IsTerminal(int(os.Stdout.Fd())) {
206+
207+
if !confirmStaticAddressDeposit(req, addrResp.Address) {
208+
return nil
209+
}
210+
}
211+
212+
resp, err := client.NewStaticAddress(ctx, req)
213+
if err != nil {
214+
return err
215+
}
216+
217+
printRespJSON(resp)
218+
219+
return nil
220+
}
221+
222+
func staticAddressDepositRequest(
223+
cmd *cli.Command, addr string) (*looprpc.NewStaticAddressRequest, error) {
224+
225+
if !cmd.IsSet("amt") && !cmd.Bool("sweepall") {
226+
return nil, errors.New("amount argument missing")
227+
}
228+
229+
amount := cmd.Int64("amt")
230+
if cmd.IsSet("amt") && amount <= 0 {
231+
return nil, errors.New("amount must be positive")
232+
}
233+
234+
if amount != 0 && cmd.Bool("sweepall") {
235+
return nil, errors.New("amount cannot be set if " +
236+
"attempting to sweep all coins out of the wallet")
237+
}
238+
239+
feeRateFlag, err := checkNotBothSet(
240+
cmd, "sat_per_vbyte", "sat_per_byte",
241+
)
242+
if err != nil {
243+
return nil, err
244+
}
245+
246+
if _, err := checkNotBothSet(
247+
cmd, feeRateFlag, "conf_target",
248+
); err != nil {
249+
return nil, err
250+
}
251+
252+
var satPerByte int64
253+
if cmd.IsSet("sat_per_byte") {
254+
satPerByte = cmd.Int64("sat_per_byte")
255+
if satPerByte < 0 {
256+
return nil, fmt.Errorf("sat_per_byte must be " +
257+
"non-negative")
258+
}
259+
}
260+
261+
confTarget := cmd.Int64("conf_target")
262+
if confTarget < 0 {
263+
return nil, fmt.Errorf("conf_target must be non-negative")
264+
}
265+
if confTarget > math.MaxInt32 {
266+
return nil, fmt.Errorf("conf_target exceeds maximum " +
267+
"int32 value")
268+
}
269+
270+
minConfs := cmd.Uint64("min_confs")
271+
if minConfs > math.MaxInt32 {
272+
return nil, fmt.Errorf("min_confs exceeds maximum " +
273+
"int32 value")
274+
}
275+
276+
var outpoints []*lnrpc.OutPoint
277+
utxos := cmd.StringSlice("utxo")
278+
if len(utxos) > 0 {
279+
outpoints, err = lndcommands.UtxosToOutpoints(utxos)
280+
if err != nil {
281+
return nil, fmt.Errorf("unable to decode utxos: %w", err)
282+
}
283+
}
284+
285+
coinSelectionStrategy, err := parseStaticAddressCoinSelectionStrategy(cmd)
286+
if err != nil {
287+
return nil, err
288+
}
289+
290+
return &looprpc.NewStaticAddressRequest{
291+
SendCoinsRequest: &lnrpc.SendCoinsRequest{
292+
Addr: addr,
293+
Amount: amount,
294+
TargetConf: int32(confTarget),
295+
SatPerVbyte: cmd.Uint64("sat_per_vbyte"),
296+
SatPerByte: satPerByte,
297+
SendAll: cmd.Bool("sweepall"),
298+
Label: cmd.String(
299+
staticAddressFundingLabelFlag.Name,
300+
),
301+
MinConfs: int32(minConfs),
302+
SpendUnconfirmed: minConfs == 0,
303+
CoinSelectionStrategy: coinSelectionStrategy,
304+
Outpoints: outpoints,
305+
},
306+
}, nil
307+
}
308+
309+
func parseStaticAddressCoinSelectionStrategy(cmd *cli.Command) (
310+
lnrpc.CoinSelectionStrategy, error) {
311+
312+
if !cmd.IsSet(staticAddressCoinSelectionStrategyFlag.Name) {
313+
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
314+
nil
315+
}
316+
317+
switch strategy := cmd.String(
318+
staticAddressCoinSelectionStrategyFlag.Name); strategy {
319+
case "global-config":
320+
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
321+
nil
322+
323+
case "largest":
324+
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
325+
326+
case "random":
327+
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
328+
329+
default:
330+
return 0, fmt.Errorf("unknown coin selection strategy %v",
331+
strategy)
332+
}
333+
}
334+
335+
func confirmStaticAddressDeposit(req *looprpc.NewStaticAddressRequest,
336+
addr string) bool {
337+
338+
sendCoinsReq := req.GetSendCoinsRequest()
339+
if sendCoinsReq.GetSendAll() {
340+
fmt.Println("Amount: sweep all eligible wallet funds")
341+
} else {
342+
fmt.Printf("Amount: %d\n", sendCoinsReq.GetAmount())
343+
}
344+
345+
fmt.Printf("Destination address: %s\n", addr)
346+
fmt.Printf("Confirm funding transaction (yes/no): ")
347+
348+
var answer string
349+
fmt.Scanln(&answer)
350+
351+
return answer == "yes" || answer == "y"
352+
}
353+
88354
var listUnspentCommand = &cli.Command{
89355
Name: "listunspent",
90356
Aliases: []string{"l"},

cmd/loop/staticaddr_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"strings"
56
"testing"
67

@@ -11,8 +12,35 @@ import (
1112
"github.com/lightninglabs/loop/staticaddr/deposit"
1213
"github.com/lightninglabs/loop/staticaddr/loopin"
1314
"github.com/stretchr/testify/require"
15+
"github.com/urfave/cli/v3"
1416
)
1517

18+
func TestStaticAddressDepositRequestAllowsNoUtxos(t *testing.T) {
19+
t.Parallel()
20+
21+
var req *looprpc.NewStaticAddressRequest
22+
cmd := &cli.Command{
23+
Name: "deposit",
24+
Flags: depositStaticAddressCommand.Flags,
25+
Action: func(_ context.Context, cmd *cli.Command) error {
26+
var err error
27+
req, err = staticAddressDepositRequest(
28+
cmd, "bcrt1ptestaddress",
29+
)
30+
31+
return err
32+
},
33+
}
34+
35+
err := cmd.Run(context.Background(), []string{
36+
"deposit", "--amt", "1000000",
37+
})
38+
require.NoError(t, err)
39+
require.Equal(t, "bcrt1ptestaddress", req.GetSendCoinsRequest().Addr)
40+
require.EqualValues(t, 1_000_000, req.GetSendCoinsRequest().Amount)
41+
require.Empty(t, req.GetSendCoinsRequest().Outpoints)
42+
}
43+
1644
func TestLowConfDepositWarningConfirmedOnly(t *testing.T) {
1745
t.Parallel()
1846

0 commit comments

Comments
 (0)