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
2326func 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{
4448var 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+
60146func 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+
88354var listUnspentCommand = & cli.Command {
89355 Name : "listunspent" ,
90356 Aliases : []string {"l" },
0 commit comments