• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 13236757158

10 Feb 2025 08:39AM UTC coverage: 57.649% (-1.2%) from 58.815%
13236757158

Pull #9493

github

ziggie1984
lncli: for some cmds we don't replace the data of the response.

For some cmds it is not very practical to replace the json output
because we might pipe it into other commands. For example when
creating the route we want to pipe it into sendtoRoute.
Pull Request #9493: For some lncli cmds we should not replace the content with other data

0 of 9 new or added lines in 2 files covered. (0.0%)

19535 existing lines in 252 files now uncovered.

103517 of 179563 relevant lines covered (57.65%)

24878.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.36
/cmd/commands/commands.go
1
package commands
2

3
import (
4
        "bufio"
5
        "bytes"
6
        "context"
7
        "encoding/hex"
8
        "encoding/json"
9
        "errors"
10
        "fmt"
11
        "io"
12
        "math"
13
        "os"
14
        "regexp"
15
        "strconv"
16
        "strings"
17
        "sync"
18

19
        "github.com/btcsuite/btcd/chaincfg/chainhash"
20
        "github.com/btcsuite/btcd/wire"
21
        "github.com/jessevdk/go-flags"
22
        "github.com/lightningnetwork/lnd"
23
        "github.com/lightningnetwork/lnd/lnrpc"
24
        "github.com/lightningnetwork/lnd/lnwire"
25
        "github.com/lightningnetwork/lnd/routing"
26
        "github.com/lightningnetwork/lnd/routing/route"
27
        "github.com/lightningnetwork/lnd/signal"
28
        "github.com/urfave/cli"
29
        "golang.org/x/term"
30
        "google.golang.org/grpc/codes"
31
        "google.golang.org/grpc/status"
32
        "google.golang.org/protobuf/proto"
33
)
34

35
// TODO(roasbeef): cli logic for supporting both positional and unix style
36
// arguments.
37

38
// TODO(roasbeef): expose all fee conf targets
39

40
const defaultRecoveryWindow int32 = 2500
41

42
const (
43
        defaultUtxoMinConf = 1
44
)
45

46
var (
47
        errBadChanPoint = errors.New(
48
                "expecting chan_point to be in format of: txid:index",
49
        )
50

51
        customDataPattern = regexp.MustCompile(
52
                `"custom_channel_data":\s*"([0-9a-f]+)"`,
53
        )
54

55
        chanIDPattern = regexp.MustCompile(
56
                `"chan_id":\s*"(\d+)"`,
57
        )
58

59
        channelPointPattern = regexp.MustCompile(
60
                `"channel_point":\s*"([0-9a-fA-F]+:[0-9]+)"`,
61
        )
62
)
63

64
// replaceCustomData replaces the custom channel data hex string with the
65
// decoded custom channel data in the JSON response.
66
func replaceCustomData(jsonBytes []byte) []byte {
6✔
67
        // If there's nothing to replace, return the original JSON.
6✔
68
        if !customDataPattern.Match(jsonBytes) {
8✔
69
                return jsonBytes
2✔
70
        }
2✔
71

72
        replacedBytes := customDataPattern.ReplaceAllFunc(
4✔
73
                jsonBytes, func(match []byte) []byte {
8✔
74
                        encoded := customDataPattern.FindStringSubmatch(
4✔
75
                                string(match),
4✔
76
                        )[1]
4✔
77
                        decoded, err := hex.DecodeString(encoded)
4✔
78
                        if err != nil {
6✔
79
                                return match
2✔
80
                        }
2✔
81

82
                        return []byte("\"custom_channel_data\":" +
2✔
83
                                string(decoded))
2✔
84
                },
85
        )
86

87
        var buf bytes.Buffer
4✔
88
        err := json.Indent(&buf, replacedBytes, "", "    ")
4✔
89
        if err != nil {
5✔
90
                // If we can't indent the JSON, it likely means the replacement
1✔
91
                // data wasn't correct, so we return the original JSON.
1✔
92
                return jsonBytes
1✔
93
        }
1✔
94

95
        return buf.Bytes()
3✔
96
}
97

98
// replaceAndAppendScid replaces the chan_id with scid and appends the human
99
// readable string representation of scid.
100
func replaceAndAppendScid(jsonBytes []byte) []byte {
6✔
101
        // If there's nothing to replace, return the original JSON.
6✔
102
        if !chanIDPattern.Match(jsonBytes) {
8✔
103
                return jsonBytes
2✔
104
        }
2✔
105

106
        replacedBytes := chanIDPattern.ReplaceAllFunc(
4✔
107
                jsonBytes, func(match []byte) []byte {
8✔
108
                        // Extract the captured scid group from the match.
4✔
109
                        chanID := chanIDPattern.FindStringSubmatch(
4✔
110
                                string(match),
4✔
111
                        )[1]
4✔
112

4✔
113
                        scid, err := strconv.ParseUint(chanID, 10, 64)
4✔
114
                        if err != nil {
6✔
115
                                return match
2✔
116
                        }
2✔
117

118
                        // Format a new JSON field for the scid (chan_id),
119
                        // including both its numeric representation and its
120
                        // string representation (scid_str).
121
                        scidStr := lnwire.NewShortChanIDFromInt(scid).
2✔
122
                                AltString()
2✔
123
                        updatedField := fmt.Sprintf(
2✔
124
                                `"scid": "%d", "scid_str": "%s"`, scid, scidStr,
2✔
125
                        )
2✔
126

2✔
127
                        // Replace the entire match with the new structure.
2✔
128
                        return []byte(updatedField)
2✔
129
                },
130
        )
131

132
        var buf bytes.Buffer
4✔
133
        err := json.Indent(&buf, replacedBytes, "", "    ")
4✔
134
        if err != nil {
5✔
135
                // If we can't indent the JSON, it likely means the replacement
1✔
136
                // data wasn't correct, so we return the original JSON.
1✔
137
                return jsonBytes
1✔
138
        }
1✔
139

140
        return buf.Bytes()
3✔
141
}
142

143
// appendChanID appends the chan_id which is computed using the outpoint
144
// of the funding transaction (the txid, and output index).
145
func appendChanID(jsonBytes []byte) []byte {
6✔
146
        // If there's nothing to replace, return the original JSON.
6✔
147
        if !channelPointPattern.Match(jsonBytes) {
8✔
148
                return jsonBytes
2✔
149
        }
2✔
150

151
        replacedBytes := channelPointPattern.ReplaceAllFunc(
4✔
152
                jsonBytes, func(match []byte) []byte {
8✔
153
                        chanPoint := channelPointPattern.FindStringSubmatch(
4✔
154
                                string(match),
4✔
155
                        )[1]
4✔
156

4✔
157
                        chanOutpoint, err := wire.NewOutPointFromString(
4✔
158
                                chanPoint,
4✔
159
                        )
4✔
160
                        if err != nil {
6✔
161
                                return match
2✔
162
                        }
2✔
163

164
                        // Format a new JSON field computed from the
165
                        // channel_point (chan_id).
166
                        chanID := lnwire.NewChanIDFromOutPoint(*chanOutpoint)
2✔
167
                        updatedField := fmt.Sprintf(
2✔
168
                                `"channel_point": "%s", "chan_id": "%s"`,
2✔
169
                                chanPoint, chanID.String(),
2✔
170
                        )
2✔
171

2✔
172
                        // Replace the entire match with the new structure.
2✔
173
                        return []byte(updatedField)
2✔
174
                },
175
        )
176

177
        var buf bytes.Buffer
4✔
178
        err := json.Indent(&buf, replacedBytes, "", "    ")
4✔
179
        if err != nil {
5✔
180
                // If we can't indent the JSON, it likely means the replacement
1✔
181
                // data wasn't correct, so we return the original JSON.
1✔
182
                return jsonBytes
1✔
183
        }
1✔
184

185
        return buf.Bytes()
3✔
186
}
187

188
func getContext() context.Context {
×
189
        shutdownInterceptor, err := signal.Intercept()
×
190
        if err != nil {
×
191
                _, _ = fmt.Fprintln(os.Stderr, err)
×
192
                os.Exit(1)
×
193
        }
×
194

195
        ctxc, cancel := context.WithCancel(context.Background())
×
196
        go func() {
×
197
                <-shutdownInterceptor.ShutdownChannel()
×
198
                cancel()
×
199
        }()
×
200
        return ctxc
×
201
}
202

203
func printJSON(resp interface{}) {
×
204
        b, err := json.Marshal(resp)
×
205
        if err != nil {
×
206
                fatal(err)
×
207
        }
×
208

209
        var out bytes.Buffer
×
210
        _ = json.Indent(&out, b, "", "    ")
×
211
        _, _ = out.WriteString("\n")
×
212
        _, _ = out.WriteTo(os.Stdout)
×
213
}
214

215
func printRespJSON(resp proto.Message) {
×
216
        jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(resp)
×
217
        if err != nil {
×
218
                fmt.Println("unable to decode response: ", err)
×
219
                return
×
220
        }
×
221

222
        // Replace custom_channel_data in the JSON.
223
        jsonBytesReplaced := replaceCustomData(jsonBytes)
×
224

×
225
        // Replace chan_id with scid, and append scid_str and scid fields.
×
226
        jsonBytesReplaced = replaceAndAppendScid(jsonBytesReplaced)
×
227

×
228
        // Append the chan_id field to the JSON.
×
229
        jsonBytesReplaced = appendChanID(jsonBytesReplaced)
×
230

×
231
        fmt.Printf("%s\n", jsonBytesReplaced)
×
232
}
233

234
// printUnmodifiedProtoJSON prints the response as is, without any replacements.
NEW
235
func printUnmodifiedProtoJSON(resp proto.Message) {
×
NEW
236
        jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(resp)
×
NEW
237
        if err != nil {
×
NEW
238
                fmt.Println("unable to decode response: ", err)
×
NEW
239
                return
×
NEW
240
        }
×
241

NEW
242
        fmt.Printf("%s\n", jsonBytes)
×
243
}
244

245
// actionDecorator is used to add additional information and error handling
246
// to command actions.
247
func actionDecorator(f func(*cli.Context) error) func(*cli.Context) error {
81✔
248
        return func(c *cli.Context) error {
81✔
249
                if err := f(c); err != nil {
×
250
                        s, ok := status.FromError(err)
×
251

×
252
                        // If it's a command for the UnlockerService (like
×
253
                        // 'create' or 'unlock') but the wallet is already
×
254
                        // unlocked, then these methods aren't recognized any
×
255
                        // more because this service is shut down after
×
256
                        // successful unlock. That's why the code
×
257
                        // 'Unimplemented' means something different for these
×
258
                        // two commands.
×
259
                        if s.Code() == codes.Unimplemented &&
×
260
                                (c.Command.Name == "create" ||
×
261
                                        c.Command.Name == "unlock" ||
×
262
                                        c.Command.Name == "changepassword" ||
×
263
                                        c.Command.Name == "createwatchonly") {
×
264

×
265
                                return fmt.Errorf("Wallet is already unlocked")
×
266
                        }
×
267

268
                        // lnd might be active, but not possible to contact
269
                        // using RPC if the wallet is encrypted. If we get
270
                        // error code Unimplemented, it means that lnd is
271
                        // running, but the RPC server is not active yet (only
272
                        // WalletUnlocker server active) and most likely this
273
                        // is because of an encrypted wallet.
274
                        if ok && s.Code() == codes.Unimplemented {
×
275
                                return fmt.Errorf("Wallet is encrypted. " +
×
276
                                        "Please unlock using 'lncli unlock', " +
×
277
                                        "or set password using 'lncli create'" +
×
278
                                        " if this is the first time starting " +
×
279
                                        "lnd.")
×
280
                        }
×
281
                        return err
×
282
                }
283
                return nil
×
284
        }
285
}
286

287
var newAddressCommand = cli.Command{
288
        Name:      "newaddress",
289
        Category:  "Wallet",
290
        Usage:     "Generates a new address.",
291
        ArgsUsage: "address-type",
292
        Flags: []cli.Flag{
293
                cli.StringFlag{
294
                        Name: "account",
295
                        Usage: "(optional) the name of the account to " +
296
                                "generate a new address for",
297
                },
298
                cli.BoolFlag{
299
                        Name: "unused",
300
                        Usage: "(optional) return the last unused address " +
301
                                "instead of generating a new one",
302
                },
303
        },
304
        Description: `
305
        Generate a wallet new address. Address-types has to be one of:
306
            - p2wkh:  Pay to witness key hash
307
            - np2wkh: Pay to nested witness key hash
308
            - p2tr:   Pay to taproot pubkey`,
309
        Action: actionDecorator(newAddress),
310
}
311

312
func newAddress(ctx *cli.Context) error {
×
313
        ctxc := getContext()
×
314

×
315
        // Display the command's help message if we do not have the expected
×
316
        // number of arguments/flags.
×
317
        if ctx.NArg() != 1 || ctx.NumFlags() > 1 {
×
318
                return cli.ShowCommandHelp(ctx, "newaddress")
×
319
        }
×
320

321
        // Map the string encoded address type, to the concrete typed address
322
        // type enum. An unrecognized address type will result in an error.
323
        stringAddrType := ctx.Args().First()
×
324
        unused := ctx.Bool("unused")
×
325

×
326
        var addrType lnrpc.AddressType
×
327
        switch stringAddrType { // TODO(roasbeef): make them ints on the cli?
×
328
        case "p2wkh":
×
329
                addrType = lnrpc.AddressType_WITNESS_PUBKEY_HASH
×
330
                if unused {
×
331
                        addrType = lnrpc.AddressType_UNUSED_WITNESS_PUBKEY_HASH
×
332
                }
×
333
        case "np2wkh":
×
334
                addrType = lnrpc.AddressType_NESTED_PUBKEY_HASH
×
335
                if unused {
×
336
                        addrType = lnrpc.AddressType_UNUSED_NESTED_PUBKEY_HASH
×
337
                }
×
338
        case "p2tr":
×
339
                addrType = lnrpc.AddressType_TAPROOT_PUBKEY
×
340
                if unused {
×
341
                        addrType = lnrpc.AddressType_UNUSED_TAPROOT_PUBKEY
×
342
                }
×
343
        default:
×
344
                return fmt.Errorf("invalid address type %v, support address type "+
×
345
                        "are: p2wkh, np2wkh, and p2tr", stringAddrType)
×
346
        }
347

348
        client, cleanUp := getClient(ctx)
×
349
        defer cleanUp()
×
350

×
351
        addr, err := client.NewAddress(ctxc, &lnrpc.NewAddressRequest{
×
352
                Type:    addrType,
×
353
                Account: ctx.String("account"),
×
354
        })
×
355
        if err != nil {
×
356
                return err
×
357
        }
×
358

359
        printRespJSON(addr)
×
360
        return nil
×
361
}
362

363
var coinSelectionStrategyFlag = cli.StringFlag{
364
        Name: "coin_selection_strategy",
365
        Usage: "(optional) the strategy to use for selecting " +
366
                "coins. Possible values are 'largest', 'random', or " +
367
                "'global-config'. If either 'largest' or 'random' is " +
368
                "specified, it will override the globally configured " +
369
                "strategy in lnd.conf",
370
        Value: "global-config",
371
}
372

373
var estimateFeeCommand = cli.Command{
374
        Name:      "estimatefee",
375
        Category:  "On-chain",
376
        Usage:     "Get fee estimates for sending bitcoin on-chain to multiple addresses.",
377
        ArgsUsage: "send-json-string [--conf_target=N]",
378
        Description: `
379
        Get fee estimates for sending a transaction paying the specified amount(s) to the passed address(es).
380

381
        The send-json-string' param decodes addresses and the amount to send respectively in the following format:
382

383
            '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
384
        `,
385
        Flags: []cli.Flag{
386
                cli.Int64Flag{
387
                        Name: "conf_target",
388
                        Usage: "(optional) the number of blocks that the " +
389
                                "transaction *should* confirm in",
390
                },
391
                coinSelectionStrategyFlag,
392
        },
393
        Action: actionDecorator(estimateFees),
394
}
395

396
func estimateFees(ctx *cli.Context) error {
×
397
        ctxc := getContext()
×
398
        var amountToAddr map[string]int64
×
399

×
400
        jsonMap := ctx.Args().First()
×
401
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
402
                return err
×
403
        }
×
404

405
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
406
        if err != nil {
×
407
                return err
×
408
        }
×
409

410
        client, cleanUp := getClient(ctx)
×
411
        defer cleanUp()
×
412

×
413
        resp, err := client.EstimateFee(ctxc, &lnrpc.EstimateFeeRequest{
×
414
                AddrToAmount:          amountToAddr,
×
415
                TargetConf:            int32(ctx.Int64("conf_target")),
×
416
                CoinSelectionStrategy: coinSelectionStrategy,
×
417
        })
×
418
        if err != nil {
×
419
                return err
×
420
        }
×
421

422
        printRespJSON(resp)
×
423
        return nil
×
424
}
425

426
var txLabelFlag = cli.StringFlag{
427
        Name:  "label",
428
        Usage: "(optional) a label for the transaction",
429
}
430

431
var sendCoinsCommand = cli.Command{
432
        Name:      "sendcoins",
433
        Category:  "On-chain",
434
        Usage:     "Send bitcoin on-chain to an address.",
435
        ArgsUsage: "addr amt",
436
        Description: `
437
        Send amt coins in satoshis to the base58 or bech32 encoded bitcoin address addr.
438

439
        Fees used when sending the transaction can be specified via the --conf_target, or
440
        --sat_per_vbyte optional flags.
441

442
        Positional arguments and flags can be used interchangeably but not at the same time!
443
        `,
444
        Flags: []cli.Flag{
445
                cli.StringFlag{
446
                        Name: "addr",
447
                        Usage: "the base58 or bech32 encoded bitcoin address to send coins " +
448
                                "to on-chain",
449
                },
450
                cli.BoolFlag{
451
                        Name: "sweepall",
452
                        Usage: "if set, then the amount field should be " +
453
                                "unset. This indicates that the wallet will " +
454
                                "attempt to sweep all outputs within the " +
455
                                "wallet or all funds in select utxos (when " +
456
                                "supplied) to the target address",
457
                },
458
                cli.Int64Flag{
459
                        Name:  "amt",
460
                        Usage: "the number of bitcoin denominated in satoshis to send",
461
                },
462
                cli.Int64Flag{
463
                        Name: "conf_target",
464
                        Usage: "(optional) the number of blocks that the " +
465
                                "transaction *should* confirm in, will be " +
466
                                "used for fee estimation",
467
                },
468
                cli.Int64Flag{
469
                        Name:   "sat_per_byte",
470
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
471
                        Hidden: true,
472
                },
473
                cli.Int64Flag{
474
                        Name: "sat_per_vbyte",
475
                        Usage: "(optional) a manual fee expressed in " +
476
                                "sat/vbyte that should be used when crafting " +
477
                                "the transaction",
478
                },
479
                cli.Uint64Flag{
480
                        Name: "min_confs",
481
                        Usage: "(optional) the minimum number of confirmations " +
482
                                "each one of your outputs used for the transaction " +
483
                                "must satisfy",
484
                        Value: defaultUtxoMinConf,
485
                },
486
                cli.BoolFlag{
487
                        Name: "force, f",
488
                        Usage: "if set, the transaction will be broadcast " +
489
                                "without asking for confirmation; this is " +
490
                                "set to true by default if stdout is not a " +
491
                                "terminal avoid breaking existing shell " +
492
                                "scripts",
493
                },
494
                coinSelectionStrategyFlag,
495
                cli.StringSliceFlag{
496
                        Name: "utxo",
497
                        Usage: "a utxo specified as outpoint(tx:idx) which " +
498
                                "will be used as input for the transaction. " +
499
                                "This flag can be repeatedly used to specify " +
500
                                "multiple utxos as inputs. The selected " +
501
                                "utxos can either be entirely spent by " +
502
                                "specifying the sweepall flag or a specified " +
503
                                "amount can be spent in the utxos through " +
504
                                "the amt flag",
505
                },
506
                txLabelFlag,
507
        },
508
        Action: actionDecorator(sendCoins),
509
}
510

511
func sendCoins(ctx *cli.Context) error {
×
512
        var (
×
513
                addr      string
×
514
                amt       int64
×
515
                err       error
×
516
                outpoints []*lnrpc.OutPoint
×
517
        )
×
518
        ctxc := getContext()
×
519
        args := ctx.Args()
×
520

×
521
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
522
                cli.ShowCommandHelp(ctx, "sendcoins")
×
523
                return nil
×
524
        }
×
525

526
        // Check that only the field sat_per_vbyte or the deprecated field
527
        // sat_per_byte is used.
528
        feeRateFlag, err := checkNotBothSet(
×
529
                ctx, "sat_per_vbyte", "sat_per_byte",
×
530
        )
×
531
        if err != nil {
×
532
                return err
×
533
        }
×
534

535
        // Only fee rate flag or conf_target should be set, not both.
536
        if _, err := checkNotBothSet(
×
537
                ctx, feeRateFlag, "conf_target",
×
538
        ); err != nil {
×
539
                return err
×
540
        }
×
541

542
        switch {
×
543
        case ctx.IsSet("addr"):
×
544
                addr = ctx.String("addr")
×
545
        case args.Present():
×
546
                addr = args.First()
×
547
                args = args.Tail()
×
548
        default:
×
549
                return fmt.Errorf("Address argument missing")
×
550
        }
551

552
        switch {
×
553
        case ctx.IsSet("amt"):
×
554
                amt = ctx.Int64("amt")
×
555
        case args.Present():
×
556
                amt, err = strconv.ParseInt(args.First(), 10, 64)
×
557
        case !ctx.Bool("sweepall"):
×
558
                return fmt.Errorf("Amount argument missing")
×
559
        }
560
        if err != nil {
×
561
                return fmt.Errorf("unable to decode amount: %w", err)
×
562
        }
×
563

564
        if amt != 0 && ctx.Bool("sweepall") {
×
565
                return fmt.Errorf("amount cannot be set if attempting to " +
×
566
                        "sweep all coins out of the wallet")
×
567
        }
×
568

569
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
570
        if err != nil {
×
571
                return err
×
572
        }
×
573

574
        client, cleanUp := getClient(ctx)
×
575
        defer cleanUp()
×
576
        minConfs := int32(ctx.Uint64("min_confs"))
×
577

×
578
        // In case that the user has specified the sweepall flag, we'll
×
579
        // calculate the amount to send based on the current wallet balance.
×
580
        displayAmt := amt
×
581
        if ctx.Bool("sweepall") && !ctx.IsSet("utxo") {
×
582
                balanceResponse, err := client.WalletBalance(
×
583
                        ctxc, &lnrpc.WalletBalanceRequest{
×
584
                                MinConfs: minConfs,
×
585
                        },
×
586
                )
×
587
                if err != nil {
×
588
                        return fmt.Errorf("unable to retrieve wallet balance:"+
×
589
                                " %w", err)
×
590
                }
×
591
                displayAmt = balanceResponse.GetConfirmedBalance()
×
592
        }
593

594
        if ctx.IsSet("utxo") {
×
595
                utxos := ctx.StringSlice("utxo")
×
596

×
597
                outpoints, err = UtxosToOutpoints(utxos)
×
598
                if err != nil {
×
599
                        return fmt.Errorf("unable to decode utxos: %w", err)
×
600
                }
×
601

602
                if ctx.Bool("sweepall") {
×
603
                        displayAmt = 0
×
604
                        // If we're sweeping all funds of the utxos, we'll need
×
605
                        // to set the display amount to the total amount of the
×
606
                        // utxos.
×
607
                        unspents, err := client.ListUnspent(
×
608
                                ctxc, &lnrpc.ListUnspentRequest{
×
609
                                        MinConfs: 0,
×
610
                                        MaxConfs: math.MaxInt32,
×
611
                                },
×
612
                        )
×
613
                        if err != nil {
×
614
                                return err
×
615
                        }
×
616

617
                        for _, utxo := range outpoints {
×
618
                                for _, unspent := range unspents.Utxos {
×
619
                                        unspentUtxo := unspent.Outpoint
×
620
                                        if isSameOutpoint(utxo, unspentUtxo) {
×
621
                                                displayAmt += unspent.AmountSat
×
622
                                                break
×
623
                                        }
624
                                }
625
                        }
626
                }
627
        }
628

629
        // Ask for confirmation if we're on an actual terminal and the output is
630
        // not being redirected to another command. This prevents existing shell
631
        // scripts from breaking.
632
        if !ctx.Bool("force") && term.IsTerminal(int(os.Stdout.Fd())) {
×
633
                fmt.Printf("Amount: %d\n", displayAmt)
×
634
                fmt.Printf("Destination address: %v\n", addr)
×
635

×
636
                confirm := promptForConfirmation("Confirm payment (yes/no): ")
×
637
                if !confirm {
×
638
                        return nil
×
639
                }
×
640
        }
641

642
        req := &lnrpc.SendCoinsRequest{
×
643
                Addr:                  addr,
×
644
                Amount:                amt,
×
645
                TargetConf:            int32(ctx.Int64("conf_target")),
×
646
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
647
                SendAll:               ctx.Bool("sweepall"),
×
648
                Label:                 ctx.String(txLabelFlag.Name),
×
649
                MinConfs:              minConfs,
×
650
                SpendUnconfirmed:      minConfs == 0,
×
651
                CoinSelectionStrategy: coinSelectionStrategy,
×
652
                Outpoints:             outpoints,
×
653
        }
×
654
        txid, err := client.SendCoins(ctxc, req)
×
655
        if err != nil {
×
656
                return err
×
657
        }
×
658

659
        printRespJSON(txid)
×
660
        return nil
×
661
}
662

663
func isSameOutpoint(a, b *lnrpc.OutPoint) bool {
×
664
        return a.TxidStr == b.TxidStr && a.OutputIndex == b.OutputIndex
×
665
}
×
666

667
var listUnspentCommand = cli.Command{
668
        Name:      "listunspent",
669
        Category:  "On-chain",
670
        Usage:     "List utxos available for spending.",
671
        ArgsUsage: "[min-confs [max-confs]] [--unconfirmed_only]",
672
        Description: `
673
        For each spendable utxo currently in the wallet, with at least min_confs
674
        confirmations, and at most max_confs confirmations, lists the txid,
675
        index, amount, address, address type, scriptPubkey and number of
676
        confirmations.  Use --min_confs=0 to include unconfirmed coins. To list
677
        all coins with at least min_confs confirmations, omit the second
678
        argument or flag '--max_confs'. To list all confirmed and unconfirmed
679
        coins, no arguments are required. To see only unconfirmed coins, use
680
        '--unconfirmed_only' with '--min_confs' and '--max_confs' set to zero or
681
        not present.
682
        `,
683
        Flags: []cli.Flag{
684
                cli.Int64Flag{
685
                        Name:  "min_confs",
686
                        Usage: "the minimum number of confirmations for a utxo",
687
                },
688
                cli.Int64Flag{
689
                        Name:  "max_confs",
690
                        Usage: "the maximum number of confirmations for a utxo",
691
                },
692
                cli.BoolFlag{
693
                        Name: "unconfirmed_only",
694
                        Usage: "when min_confs and max_confs are zero, " +
695
                                "setting false implicitly overrides max_confs " +
696
                                "to be MaxInt32, otherwise max_confs remains " +
697
                                "zero. An error is returned if the value is " +
698
                                "true and both min_confs and max_confs are " +
699
                                "non-zero. (default: false)",
700
                },
701
        },
702
        Action: actionDecorator(listUnspent),
703
}
704

705
func listUnspent(ctx *cli.Context) error {
×
706
        var (
×
707
                minConfirms int64
×
708
                maxConfirms int64
×
709
                err         error
×
710
        )
×
711
        ctxc := getContext()
×
712
        args := ctx.Args()
×
713

×
714
        if ctx.IsSet("max_confs") && !ctx.IsSet("min_confs") {
×
715
                return fmt.Errorf("max_confs cannot be set without " +
×
716
                        "min_confs being set")
×
717
        }
×
718

719
        switch {
×
720
        case ctx.IsSet("min_confs"):
×
721
                minConfirms = ctx.Int64("min_confs")
×
722
        case args.Present():
×
723
                minConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
724
                if err != nil {
×
725
                        cli.ShowCommandHelp(ctx, "listunspent")
×
726
                        return nil
×
727
                }
×
728
                args = args.Tail()
×
729
        }
730

731
        switch {
×
732
        case ctx.IsSet("max_confs"):
×
733
                maxConfirms = ctx.Int64("max_confs")
×
734
        case args.Present():
×
735
                maxConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
736
                if err != nil {
×
737
                        cli.ShowCommandHelp(ctx, "listunspent")
×
738
                        return nil
×
739
                }
×
740
                args = args.Tail()
×
741
        }
742

743
        unconfirmedOnly := ctx.Bool("unconfirmed_only")
×
744

×
745
        // Force minConfirms and maxConfirms to be zero if unconfirmedOnly is
×
746
        // true.
×
747
        if unconfirmedOnly && (minConfirms != 0 || maxConfirms != 0) {
×
748
                cli.ShowCommandHelp(ctx, "listunspent")
×
749
                return nil
×
750
        }
×
751

752
        // When unconfirmedOnly is inactive, we will override maxConfirms to be
753
        // a MaxInt32 to return all confirmed and unconfirmed utxos.
754
        if maxConfirms == 0 && !unconfirmedOnly {
×
755
                maxConfirms = math.MaxInt32
×
756
        }
×
757

758
        client, cleanUp := getClient(ctx)
×
759
        defer cleanUp()
×
760

×
761
        req := &lnrpc.ListUnspentRequest{
×
762
                MinConfs: int32(minConfirms),
×
763
                MaxConfs: int32(maxConfirms),
×
764
        }
×
765
        resp, err := client.ListUnspent(ctxc, req)
×
766
        if err != nil {
×
767
                return err
×
768
        }
×
769

770
        // Parse the response into the final json object that will be printed
771
        // to stdout. At the moment, this filters out the raw txid bytes from
772
        // each utxo's outpoint and only prints the txid string.
773
        var listUnspentResp = struct {
×
774
                Utxos []*Utxo `json:"utxos"`
×
775
        }{
×
776
                Utxos: make([]*Utxo, 0, len(resp.Utxos)),
×
777
        }
×
778
        for _, protoUtxo := range resp.Utxos {
×
779
                utxo := NewUtxoFromProto(protoUtxo)
×
780
                listUnspentResp.Utxos = append(listUnspentResp.Utxos, utxo)
×
781
        }
×
782

783
        printJSON(listUnspentResp)
×
784

×
785
        return nil
×
786
}
787

788
var sendManyCommand = cli.Command{
789
        Name:      "sendmany",
790
        Category:  "On-chain",
791
        Usage:     "Send bitcoin on-chain to multiple addresses.",
792
        ArgsUsage: "send-json-string [--conf_target=N] [--sat_per_vbyte=P]",
793
        Description: `
794
        Create and broadcast a transaction paying the specified amount(s) to the passed address(es).
795

796
        The send-json-string' param decodes addresses and the amount to send
797
        respectively in the following format:
798

799
            '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
800
        `,
801
        Flags: []cli.Flag{
802
                cli.Int64Flag{
803
                        Name: "conf_target",
804
                        Usage: "(optional) the number of blocks that the transaction *should* " +
805
                                "confirm in, will be used for fee estimation",
806
                },
807
                cli.Int64Flag{
808
                        Name:   "sat_per_byte",
809
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
810
                        Hidden: true,
811
                },
812
                cli.Int64Flag{
813
                        Name: "sat_per_vbyte",
814
                        Usage: "(optional) a manual fee expressed in " +
815
                                "sat/vbyte that should be used when crafting " +
816
                                "the transaction",
817
                },
818
                cli.Uint64Flag{
819
                        Name: "min_confs",
820
                        Usage: "(optional) the minimum number of confirmations " +
821
                                "each one of your outputs used for the transaction " +
822
                                "must satisfy",
823
                        Value: defaultUtxoMinConf,
824
                },
825
                coinSelectionStrategyFlag,
826
                txLabelFlag,
827
        },
828
        Action: actionDecorator(sendMany),
829
}
830

831
func sendMany(ctx *cli.Context) error {
×
832
        ctxc := getContext()
×
833
        var amountToAddr map[string]int64
×
834

×
835
        jsonMap := ctx.Args().First()
×
836
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
837
                return err
×
838
        }
×
839

840
        // Check that only the field sat_per_vbyte or the deprecated field
841
        // sat_per_byte is used.
842
        feeRateFlag, err := checkNotBothSet(
×
843
                ctx, "sat_per_vbyte", "sat_per_byte",
×
844
        )
×
845
        if err != nil {
×
846
                return err
×
847
        }
×
848

849
        // Only fee rate flag or conf_target should be set, not both.
850
        if _, err := checkNotBothSet(
×
851
                ctx, feeRateFlag, "conf_target",
×
852
        ); err != nil {
×
853
                return err
×
854
        }
×
855

856
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
857
        if err != nil {
×
858
                return err
×
859
        }
×
860

861
        client, cleanUp := getClient(ctx)
×
862
        defer cleanUp()
×
863

×
864
        minConfs := int32(ctx.Uint64("min_confs"))
×
865
        txid, err := client.SendMany(ctxc, &lnrpc.SendManyRequest{
×
866
                AddrToAmount:          amountToAddr,
×
867
                TargetConf:            int32(ctx.Int64("conf_target")),
×
868
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
869
                Label:                 ctx.String(txLabelFlag.Name),
×
870
                MinConfs:              minConfs,
×
871
                SpendUnconfirmed:      minConfs == 0,
×
872
                CoinSelectionStrategy: coinSelectionStrategy,
×
873
        })
×
874
        if err != nil {
×
875
                return err
×
876
        }
×
877

878
        printRespJSON(txid)
×
879
        return nil
×
880
}
881

882
var connectCommand = cli.Command{
883
        Name:      "connect",
884
        Category:  "Peers",
885
        Usage:     "Connect to a remote lightning peer.",
886
        ArgsUsage: "<pubkey>@host",
887
        Description: `
888
        Connect to a peer using its <pubkey> and host.
889

890
        A custom timeout on the connection is supported. For instance, to timeout
891
        the connection request in 30 seconds, use the following:
892

893
        lncli connect <pubkey>@host --timeout 30s
894
        `,
895
        Flags: []cli.Flag{
896
                cli.BoolFlag{
897
                        Name: "perm",
898
                        Usage: "If set, the daemon will attempt to persistently " +
899
                                "connect to the target peer.\n" +
900
                                "           If not, the call will be synchronous.",
901
                },
902
                cli.DurationFlag{
903
                        Name: "timeout",
904
                        Usage: "The connection timeout value for current request. " +
905
                                "Valid uints are {ms, s, m, h}.\n" +
906
                                "If not set, the global connection " +
907
                                "timeout value (default to 120s) is used.",
908
                },
909
        },
910
        Action: actionDecorator(connectPeer),
911
}
912

913
func connectPeer(ctx *cli.Context) error {
×
914
        ctxc := getContext()
×
915
        client, cleanUp := getClient(ctx)
×
916
        defer cleanUp()
×
917

×
918
        targetAddress := ctx.Args().First()
×
919
        splitAddr := strings.Split(targetAddress, "@")
×
920
        if len(splitAddr) != 2 {
×
921
                return fmt.Errorf("target address expected in format: " +
×
922
                        "pubkey@host:port")
×
923
        }
×
924

925
        addr := &lnrpc.LightningAddress{
×
926
                Pubkey: splitAddr[0],
×
927
                Host:   splitAddr[1],
×
928
        }
×
929
        req := &lnrpc.ConnectPeerRequest{
×
930
                Addr:    addr,
×
931
                Perm:    ctx.Bool("perm"),
×
932
                Timeout: uint64(ctx.Duration("timeout").Seconds()),
×
933
        }
×
934

×
935
        lnid, err := client.ConnectPeer(ctxc, req)
×
936
        if err != nil {
×
937
                return err
×
938
        }
×
939

940
        printRespJSON(lnid)
×
941
        return nil
×
942
}
943

944
var disconnectCommand = cli.Command{
945
        Name:     "disconnect",
946
        Category: "Peers",
947
        Usage: "Disconnect a remote lightning peer identified by " +
948
                "public key.",
949
        ArgsUsage: "<pubkey>",
950
        Flags: []cli.Flag{
951
                cli.StringFlag{
952
                        Name: "node_key",
953
                        Usage: "The hex-encoded compressed public key of the peer " +
954
                                "to disconnect from",
955
                },
956
        },
957
        Action: actionDecorator(disconnectPeer),
958
}
959

960
func disconnectPeer(ctx *cli.Context) error {
×
961
        ctxc := getContext()
×
962
        client, cleanUp := getClient(ctx)
×
963
        defer cleanUp()
×
964

×
965
        var pubKey string
×
966
        switch {
×
967
        case ctx.IsSet("node_key"):
×
968
                pubKey = ctx.String("node_key")
×
969
        case ctx.Args().Present():
×
970
                pubKey = ctx.Args().First()
×
971
        default:
×
972
                return fmt.Errorf("must specify target public key")
×
973
        }
974

975
        req := &lnrpc.DisconnectPeerRequest{
×
976
                PubKey: pubKey,
×
977
        }
×
978

×
979
        lnid, err := client.DisconnectPeer(ctxc, req)
×
980
        if err != nil {
×
981
                return err
×
982
        }
×
983

984
        printRespJSON(lnid)
×
985
        return nil
×
986
}
987

988
// TODO(roasbeef): also allow short relative channel ID.
989

990
var closeChannelCommand = cli.Command{
991
        Name:     "closechannel",
992
        Category: "Channels",
993
        Usage:    "Close an existing channel.",
994
        Description: `
995
        Close an existing channel. The channel can be closed either cooperatively,
996
        or unilaterally (--force).
997

998
        A unilateral channel closure means that the latest commitment
999
        transaction will be broadcast to the network. As a result, any settled
1000
        funds will be time locked for a few blocks before they can be spent.
1001

1002
        In the case of a cooperative closure, one can manually set the fee to
1003
        be used for the closing transaction via either the --conf_target or
1004
        --sat_per_vbyte arguments. This will be the starting value used during
1005
        fee negotiation. This is optional. The parameter --max_fee_rate in
1006
        comparison is the end boundary of the fee negotiation, if not specified
1007
        it's always x3 of the starting value. Increasing this value increases
1008
        the chance of a successful negotiation.
1009

1010
        In the case of a cooperative closure, one can manually set the address
1011
        to deliver funds to upon closure. This is optional, and may only be used
1012
        if an upfront shutdown address has not already been set. If neither are
1013
        set the funds will be delivered to a new wallet address.
1014

1015
        To view which funding_txids/output_indexes can be used for a channel close,
1016
        see the channel_point values within the listchannels command output.
1017
        The format for a channel_point is 'funding_txid:output_index'.`,
1018
        ArgsUsage: "funding_txid output_index",
1019
        Flags: []cli.Flag{
1020
                cli.StringFlag{
1021
                        Name:  "funding_txid",
1022
                        Usage: "the txid of the channel's funding transaction",
1023
                },
1024
                cli.IntFlag{
1025
                        Name: "output_index",
1026
                        Usage: "the output index for the funding output of the funding " +
1027
                                "transaction",
1028
                },
1029
                cli.StringFlag{
1030
                        Name: "chan_point",
1031
                        Usage: "(optional) the channel point. If set, " +
1032
                                "funding_txid and output_index flags and " +
1033
                                "positional arguments will be ignored",
1034
                },
1035
                cli.BoolFlag{
1036
                        Name:  "force",
1037
                        Usage: "attempt an uncooperative closure",
1038
                },
1039
                cli.BoolFlag{
1040
                        Name:  "block",
1041
                        Usage: "block until the channel is closed",
1042
                },
1043
                cli.Int64Flag{
1044
                        Name: "conf_target",
1045
                        Usage: "(optional) the number of blocks that the " +
1046
                                "transaction *should* confirm in, will be " +
1047
                                "used for fee estimation. If not set, " +
1048
                                "then the conf-target value set in the main " +
1049
                                "lnd config will be used.",
1050
                },
1051
                cli.Int64Flag{
1052
                        Name:   "sat_per_byte",
1053
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
1054
                        Hidden: true,
1055
                },
1056
                cli.Int64Flag{
1057
                        Name: "sat_per_vbyte",
1058
                        Usage: "(optional) a manual fee expressed in " +
1059
                                "sat/vbyte that should be used when crafting " +
1060
                                "the transaction; default is a conf-target " +
1061
                                "of 6 blocks",
1062
                },
1063
                cli.StringFlag{
1064
                        Name: "delivery_addr",
1065
                        Usage: "(optional) an address to deliver funds " +
1066
                                "upon cooperative channel closing, may only " +
1067
                                "be used if an upfront shutdown address is not " +
1068
                                "already set",
1069
                },
1070
                cli.Uint64Flag{
1071
                        Name: "max_fee_rate",
1072
                        Usage: "(optional) maximum fee rate in sat/vbyte " +
1073
                                "accepted during the negotiation (default is " +
1074
                                "x3 of the desired fee rate); increases the " +
1075
                                "success pobability of the negotiation if " +
1076
                                "set higher",
1077
                },
1078
        },
1079
        Action: actionDecorator(closeChannel),
1080
}
1081

1082
func closeChannel(ctx *cli.Context) error {
×
1083
        ctxc := getContext()
×
1084
        client, cleanUp := getClient(ctx)
×
1085
        defer cleanUp()
×
1086

×
1087
        // Show command help if no arguments and flags were provided.
×
1088
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1089
                cli.ShowCommandHelp(ctx, "closechannel")
×
1090
                return nil
×
1091
        }
×
1092

1093
        // Check that only the field sat_per_vbyte or the deprecated field
1094
        // sat_per_byte is used.
1095
        feeRateFlag, err := checkNotBothSet(
×
1096
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1097
        )
×
1098
        if err != nil {
×
1099
                return err
×
1100
        }
×
1101

1102
        channelPoint, err := parseChannelPoint(ctx)
×
1103
        if err != nil {
×
1104
                return err
×
1105
        }
×
1106

1107
        // TODO(roasbeef): implement time deadline within server
1108
        req := &lnrpc.CloseChannelRequest{
×
1109
                ChannelPoint:    channelPoint,
×
1110
                Force:           ctx.Bool("force"),
×
1111
                TargetConf:      int32(ctx.Int64("conf_target")),
×
1112
                SatPerVbyte:     ctx.Uint64(feeRateFlag),
×
1113
                DeliveryAddress: ctx.String("delivery_addr"),
×
1114
                MaxFeePerVbyte:  ctx.Uint64("max_fee_rate"),
×
1115
        }
×
1116

×
1117
        // After parsing the request, we'll spin up a goroutine that will
×
1118
        // retrieve the closing transaction ID when attempting to close the
×
1119
        // channel. We do this to because `executeChannelClose` can block, so we
×
1120
        // would like to present the closing transaction ID to the user as soon
×
1121
        // as it is broadcasted.
×
1122
        var wg sync.WaitGroup
×
1123
        txidChan := make(chan string, 1)
×
1124

×
1125
        wg.Add(1)
×
1126
        go func() {
×
1127
                defer wg.Done()
×
1128

×
1129
                printJSON(struct {
×
1130
                        ClosingTxid string `json:"closing_txid"`
×
1131
                }{
×
1132
                        ClosingTxid: <-txidChan,
×
1133
                })
×
1134
        }()
×
1135

1136
        err = executeChannelClose(ctxc, client, req, txidChan, ctx.Bool("block"))
×
1137
        if err != nil {
×
1138
                return err
×
1139
        }
×
1140

1141
        // In the case that the user did not provide the `block` flag, then we
1142
        // need to wait for the goroutine to be done to prevent it from being
1143
        // destroyed when exiting before printing the closing transaction ID.
1144
        wg.Wait()
×
1145

×
1146
        return nil
×
1147
}
1148

1149
// executeChannelClose attempts to close the channel from a request. The closing
1150
// transaction ID is sent through `txidChan` as soon as it is broadcasted to the
1151
// network. The block boolean is used to determine if we should block until the
1152
// closing transaction receives all of its required confirmations.
1153
func executeChannelClose(ctxc context.Context, client lnrpc.LightningClient,
1154
        req *lnrpc.CloseChannelRequest, txidChan chan<- string, block bool) error {
×
1155

×
1156
        stream, err := client.CloseChannel(ctxc, req)
×
1157
        if err != nil {
×
1158
                return err
×
1159
        }
×
1160

1161
        for {
×
1162
                resp, err := stream.Recv()
×
1163
                if err == io.EOF {
×
1164
                        return nil
×
1165
                } else if err != nil {
×
1166
                        return err
×
1167
                }
×
1168

1169
                switch update := resp.Update.(type) {
×
1170
                case *lnrpc.CloseStatusUpdate_CloseInstant:
×
1171
                        if req.NoWait {
×
1172
                                return nil
×
1173
                        }
×
1174
                case *lnrpc.CloseStatusUpdate_ClosePending:
×
1175
                        closingHash := update.ClosePending.Txid
×
1176
                        txid, err := chainhash.NewHash(closingHash)
×
1177
                        if err != nil {
×
1178
                                return err
×
1179
                        }
×
1180

1181
                        txidChan <- txid.String()
×
1182

×
1183
                        if !block {
×
1184
                                return nil
×
1185
                        }
×
1186
                case *lnrpc.CloseStatusUpdate_ChanClose:
×
1187
                        return nil
×
1188
                }
1189
        }
1190
}
1191

1192
var closeAllChannelsCommand = cli.Command{
1193
        Name:     "closeallchannels",
1194
        Category: "Channels",
1195
        Usage:    "Close all existing channels.",
1196
        Description: `
1197
        Close all existing channels.
1198

1199
        Channels will be closed either cooperatively or unilaterally, depending
1200
        on whether the channel is active or not. If the channel is inactive, any
1201
        settled funds within it will be time locked for a few blocks before they
1202
        can be spent.
1203

1204
        One can request to close inactive channels only by using the
1205
        --inactive_only flag.
1206

1207
        By default, one is prompted for confirmation every time an inactive
1208
        channel is requested to be closed. To avoid this, one can set the
1209
        --force flag, which will only prompt for confirmation once for all
1210
        inactive channels and proceed to close them.
1211

1212
        In the case of cooperative closures, one can manually set the fee to
1213
        be used for the closing transactions via either the --conf_target or
1214
        --sat_per_vbyte arguments. This will be the starting value used during
1215
        fee negotiation. This is optional.`,
1216
        Flags: []cli.Flag{
1217
                cli.BoolFlag{
1218
                        Name:  "inactive_only",
1219
                        Usage: "close inactive channels only",
1220
                },
1221
                cli.BoolFlag{
1222
                        Name: "force",
1223
                        Usage: "ask for confirmation once before attempting " +
1224
                                "to close existing channels",
1225
                },
1226
                cli.Int64Flag{
1227
                        Name: "conf_target",
1228
                        Usage: "(optional) the number of blocks that the " +
1229
                                "closing transactions *should* confirm in, will be " +
1230
                                "used for fee estimation",
1231
                },
1232
                cli.Int64Flag{
1233
                        Name:   "sat_per_byte",
1234
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
1235
                        Hidden: true,
1236
                },
1237
                cli.Int64Flag{
1238
                        Name: "sat_per_vbyte",
1239
                        Usage: "(optional) a manual fee expressed in " +
1240
                                "sat/vbyte that should be used when crafting " +
1241
                                "the closing transactions",
1242
                },
1243
                cli.BoolFlag{
1244
                        Name: "s, skip_confirmation",
1245
                        Usage: "Skip the confirmation prompt and close all " +
1246
                                "channels immediately",
1247
                },
1248
        },
1249
        Action: actionDecorator(closeAllChannels),
1250
}
1251

1252
func closeAllChannels(ctx *cli.Context) error {
×
1253
        ctxc := getContext()
×
1254
        client, cleanUp := getClient(ctx)
×
1255
        defer cleanUp()
×
1256

×
1257
        // Check that only the field sat_per_vbyte or the deprecated field
×
1258
        // sat_per_byte is used.
×
1259
        feeRateFlag, err := checkNotBothSet(
×
1260
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1261
        )
×
1262
        if err != nil {
×
1263
                return err
×
1264
        }
×
1265

1266
        prompt := "Do you really want to close ALL channels? (yes/no): "
×
1267
        if !ctx.Bool("skip_confirmation") && !promptForConfirmation(prompt) {
×
1268
                return errors.New("action aborted by user")
×
1269
        }
×
1270

1271
        listReq := &lnrpc.ListChannelsRequest{}
×
1272
        openChannels, err := client.ListChannels(ctxc, listReq)
×
1273
        if err != nil {
×
1274
                return fmt.Errorf("unable to fetch open channels: %w", err)
×
1275
        }
×
1276

1277
        if len(openChannels.Channels) == 0 {
×
1278
                return errors.New("no open channels to close")
×
1279
        }
×
1280

1281
        var channelsToClose []*lnrpc.Channel
×
1282

×
1283
        switch {
×
1284
        case ctx.Bool("force") && ctx.Bool("inactive_only"):
×
1285
                msg := "Unilaterally close all inactive channels? The funds " +
×
1286
                        "within these channels will be locked for some blocks " +
×
1287
                        "(CSV delay) before they can be spent. (yes/no): "
×
1288

×
1289
                confirmed := promptForConfirmation(msg)
×
1290

×
1291
                // We can safely exit if the user did not confirm.
×
1292
                if !confirmed {
×
1293
                        return nil
×
1294
                }
×
1295

1296
                // Go through the list of open channels and only add inactive
1297
                // channels to the closing list.
1298
                for _, channel := range openChannels.Channels {
×
1299
                        if !channel.GetActive() {
×
1300
                                channelsToClose = append(
×
1301
                                        channelsToClose, channel,
×
1302
                                )
×
1303
                        }
×
1304
                }
1305
        case ctx.Bool("force"):
×
1306
                msg := "Close all active and inactive channels? Inactive " +
×
1307
                        "channels will be closed unilaterally, so funds " +
×
1308
                        "within them will be locked for a few blocks (CSV " +
×
1309
                        "delay) before they can be spent. (yes/no): "
×
1310

×
1311
                confirmed := promptForConfirmation(msg)
×
1312

×
1313
                // We can safely exit if the user did not confirm.
×
1314
                if !confirmed {
×
1315
                        return nil
×
1316
                }
×
1317

1318
                channelsToClose = openChannels.Channels
×
1319
        default:
×
1320
                // Go through the list of open channels and determine which
×
1321
                // should be added to the closing list.
×
1322
                for _, channel := range openChannels.Channels {
×
1323
                        // If the channel is inactive, we'll attempt to
×
1324
                        // unilaterally close the channel, so we should prompt
×
1325
                        // the user for confirmation beforehand.
×
1326
                        if !channel.GetActive() {
×
1327
                                msg := fmt.Sprintf("Unilaterally close channel "+
×
1328
                                        "with node %s and channel point %s? "+
×
1329
                                        "The closing transaction will need %d "+
×
1330
                                        "confirmations before the funds can be "+
×
1331
                                        "spent. (yes/no): ", channel.RemotePubkey,
×
1332
                                        channel.ChannelPoint, channel.LocalConstraints.CsvDelay)
×
1333

×
1334
                                confirmed := promptForConfirmation(msg)
×
1335

×
1336
                                if confirmed {
×
1337
                                        channelsToClose = append(
×
1338
                                                channelsToClose, channel,
×
1339
                                        )
×
1340
                                }
×
1341
                        } else if !ctx.Bool("inactive_only") {
×
1342
                                // Otherwise, we'll only add active channels if
×
1343
                                // we were not requested to close inactive
×
1344
                                // channels only.
×
1345
                                channelsToClose = append(
×
1346
                                        channelsToClose, channel,
×
1347
                                )
×
1348
                        }
×
1349
                }
1350
        }
1351

1352
        // result defines the result of closing a channel. The closing
1353
        // transaction ID is populated if a channel is successfully closed.
1354
        // Otherwise, the error that prevented closing the channel is populated.
1355
        type result struct {
×
1356
                RemotePubKey string `json:"remote_pub_key"`
×
1357
                ChannelPoint string `json:"channel_point"`
×
1358
                ClosingTxid  string `json:"closing_txid"`
×
1359
                FailErr      string `json:"error"`
×
1360
        }
×
1361

×
1362
        // Launch each channel closure in a goroutine in order to execute them
×
1363
        // in parallel. Once they're all executed, we will print the results as
×
1364
        // they come.
×
1365
        resultChan := make(chan result, len(channelsToClose))
×
1366
        for _, channel := range channelsToClose {
×
1367
                go func(channel *lnrpc.Channel) {
×
1368
                        res := result{}
×
1369
                        res.RemotePubKey = channel.RemotePubkey
×
1370
                        res.ChannelPoint = channel.ChannelPoint
×
1371
                        defer func() {
×
1372
                                resultChan <- res
×
1373
                        }()
×
1374

1375
                        // Parse the channel point in order to create the close
1376
                        // channel request.
1377
                        s := strings.Split(res.ChannelPoint, ":")
×
1378
                        if len(s) != 2 {
×
1379
                                res.FailErr = "expected channel point with " +
×
1380
                                        "format txid:index"
×
1381
                                return
×
1382
                        }
×
1383
                        index, err := strconv.ParseUint(s[1], 10, 32)
×
1384
                        if err != nil {
×
1385
                                res.FailErr = fmt.Sprintf("unable to parse "+
×
1386
                                        "channel point output index: %v", err)
×
1387
                                return
×
1388
                        }
×
1389

1390
                        req := &lnrpc.CloseChannelRequest{
×
1391
                                ChannelPoint: &lnrpc.ChannelPoint{
×
1392
                                        FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
×
1393
                                                FundingTxidStr: s[0],
×
1394
                                        },
×
1395
                                        OutputIndex: uint32(index),
×
1396
                                },
×
1397
                                Force:       !channel.GetActive(),
×
1398
                                TargetConf:  int32(ctx.Int64("conf_target")),
×
1399
                                SatPerVbyte: ctx.Uint64(feeRateFlag),
×
1400
                        }
×
1401

×
1402
                        txidChan := make(chan string, 1)
×
1403
                        err = executeChannelClose(ctxc, client, req, txidChan, false)
×
1404
                        if err != nil {
×
1405
                                res.FailErr = fmt.Sprintf("unable to close "+
×
1406
                                        "channel: %v", err)
×
1407
                                return
×
1408
                        }
×
1409

1410
                        res.ClosingTxid = <-txidChan
×
1411
                }(channel)
1412
        }
1413

1414
        for range channelsToClose {
×
1415
                res := <-resultChan
×
1416
                printJSON(res)
×
1417
        }
×
1418

1419
        return nil
×
1420
}
1421

1422
// promptForConfirmation continuously prompts the user for the message until
1423
// receiving a response of "yes" or "no" and returns their answer as a bool.
1424
func promptForConfirmation(msg string) bool {
×
1425
        reader := bufio.NewReader(os.Stdin)
×
1426

×
1427
        for {
×
1428
                fmt.Print(msg)
×
1429

×
1430
                answer, err := reader.ReadString('\n')
×
1431
                if err != nil {
×
1432
                        return false
×
1433
                }
×
1434

1435
                answer = strings.ToLower(strings.TrimSpace(answer))
×
1436

×
1437
                switch {
×
1438
                case answer == "yes":
×
1439
                        return true
×
1440
                case answer == "no":
×
1441
                        return false
×
1442
                default:
×
1443
                        continue
×
1444
                }
1445
        }
1446
}
1447

1448
var abandonChannelCommand = cli.Command{
1449
        Name:     "abandonchannel",
1450
        Category: "Channels",
1451
        Usage:    "Abandons an existing channel.",
1452
        Description: `
1453
        Removes all channel state from the database except for a close
1454
        summary. This method can be used to get rid of permanently unusable
1455
        channels due to bugs fixed in newer versions of lnd.
1456

1457
        Only available when lnd is built in debug mode. The flag
1458
        --i_know_what_i_am_doing can be set to override the debug/dev mode
1459
        requirement.
1460

1461
        To view which funding_txids/output_indexes can be used for this command,
1462
        see the channel_point values within the listchannels command output.
1463
        The format for a channel_point is 'funding_txid:output_index'.`,
1464
        ArgsUsage: "funding_txid [output_index]",
1465
        Flags: []cli.Flag{
1466
                cli.StringFlag{
1467
                        Name:  "funding_txid",
1468
                        Usage: "the txid of the channel's funding transaction",
1469
                },
1470
                cli.IntFlag{
1471
                        Name: "output_index",
1472
                        Usage: "the output index for the funding output of the funding " +
1473
                                "transaction",
1474
                },
1475
                cli.StringFlag{
1476
                        Name: "chan_point",
1477
                        Usage: "(optional) the channel point. If set, " +
1478
                                "funding_txid and output_index flags and " +
1479
                                "positional arguments will be ignored",
1480
                },
1481
                cli.BoolFlag{
1482
                        Name: "i_know_what_i_am_doing",
1483
                        Usage: "override the requirement for lnd needing to " +
1484
                                "be in dev/debug mode to use this command; " +
1485
                                "when setting this the user attests that " +
1486
                                "they know the danger of using this command " +
1487
                                "on channels and that doing so can lead to " +
1488
                                "loss of funds if the channel funding TX " +
1489
                                "ever confirms (or was confirmed)",
1490
                },
1491
        },
1492
        Action: actionDecorator(abandonChannel),
1493
}
1494

1495
func abandonChannel(ctx *cli.Context) error {
×
1496
        ctxc := getContext()
×
1497
        client, cleanUp := getClient(ctx)
×
1498
        defer cleanUp()
×
1499

×
1500
        // Show command help if no arguments and flags were provided.
×
1501
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1502
                cli.ShowCommandHelp(ctx, "abandonchannel")
×
1503
                return nil
×
1504
        }
×
1505

1506
        channelPoint, err := parseChannelPoint(ctx)
×
1507
        if err != nil {
×
1508
                return err
×
1509
        }
×
1510

1511
        req := &lnrpc.AbandonChannelRequest{
×
1512
                ChannelPoint:      channelPoint,
×
1513
                IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
×
1514
        }
×
1515

×
1516
        resp, err := client.AbandonChannel(ctxc, req)
×
1517
        if err != nil {
×
1518
                return err
×
1519
        }
×
1520

1521
        printRespJSON(resp)
×
1522
        return nil
×
1523
}
1524

1525
// parseChannelPoint parses a funding txid and output index from the command
1526
// line. Both named options and unnamed parameters are supported.
1527
func parseChannelPoint(ctx *cli.Context) (*lnrpc.ChannelPoint, error) {
×
1528
        channelPoint := &lnrpc.ChannelPoint{}
×
1529
        var err error
×
1530

×
1531
        args := ctx.Args()
×
1532

×
1533
        switch {
×
1534
        case ctx.IsSet("chan_point"):
×
1535
                channelPoint, err = parseChanPoint(ctx.String("chan_point"))
×
1536
                if err != nil {
×
1537
                        return nil, fmt.Errorf("unable to parse chan_point: "+
×
1538
                                "%v", err)
×
1539
                }
×
1540
                return channelPoint, nil
×
1541

1542
        case ctx.IsSet("funding_txid"):
×
1543
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1544
                        FundingTxidStr: ctx.String("funding_txid"),
×
1545
                }
×
1546
        case args.Present():
×
1547
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1548
                        FundingTxidStr: args.First(),
×
1549
                }
×
1550
                args = args.Tail()
×
1551
        default:
×
1552
                return nil, fmt.Errorf("funding txid argument missing")
×
1553
        }
1554

1555
        switch {
×
1556
        case ctx.IsSet("output_index"):
×
1557
                channelPoint.OutputIndex = uint32(ctx.Int("output_index"))
×
1558
        case args.Present():
×
1559
                index, err := strconv.ParseUint(args.First(), 10, 32)
×
1560
                if err != nil {
×
1561
                        return nil, fmt.Errorf("unable to decode output "+
×
1562
                                "index: %w", err)
×
1563
                }
×
1564
                channelPoint.OutputIndex = uint32(index)
×
1565
        default:
×
1566
                channelPoint.OutputIndex = 0
×
1567
        }
1568

1569
        return channelPoint, nil
×
1570
}
1571

1572
var listPeersCommand = cli.Command{
1573
        Name:     "listpeers",
1574
        Category: "Peers",
1575
        Usage:    "List all active, currently connected peers.",
1576
        Flags: []cli.Flag{
1577
                cli.BoolFlag{
1578
                        Name:  "list_errors",
1579
                        Usage: "list a full set of most recent errors for the peer",
1580
                },
1581
        },
1582
        Action: actionDecorator(listPeers),
1583
}
1584

1585
func listPeers(ctx *cli.Context) error {
×
1586
        ctxc := getContext()
×
1587
        client, cleanUp := getClient(ctx)
×
1588
        defer cleanUp()
×
1589

×
1590
        // By default, we display a single error on the cli. If the user
×
1591
        // specifically requests a full error set, then we will provide it.
×
1592
        req := &lnrpc.ListPeersRequest{
×
1593
                LatestError: !ctx.IsSet("list_errors"),
×
1594
        }
×
1595
        resp, err := client.ListPeers(ctxc, req)
×
1596
        if err != nil {
×
1597
                return err
×
1598
        }
×
1599

1600
        printRespJSON(resp)
×
1601
        return nil
×
1602
}
1603

1604
var walletBalanceCommand = cli.Command{
1605
        Name:     "walletbalance",
1606
        Category: "Wallet",
1607
        Usage:    "Compute and display the wallet's current balance.",
1608
        Flags: []cli.Flag{
1609
                cli.StringFlag{
1610
                        Name: "account",
1611
                        Usage: "(optional) the account for which the balance " +
1612
                                "is shown",
1613
                        Value: "",
1614
                },
1615
        },
1616
        Action: actionDecorator(walletBalance),
1617
}
1618

1619
func walletBalance(ctx *cli.Context) error {
×
1620
        ctxc := getContext()
×
1621
        client, cleanUp := getClient(ctx)
×
1622
        defer cleanUp()
×
1623

×
1624
        req := &lnrpc.WalletBalanceRequest{
×
1625
                Account: ctx.String("account"),
×
1626
        }
×
1627
        resp, err := client.WalletBalance(ctxc, req)
×
1628
        if err != nil {
×
1629
                return err
×
1630
        }
×
1631

1632
        printRespJSON(resp)
×
1633
        return nil
×
1634
}
1635

1636
var ChannelBalanceCommand = cli.Command{
1637
        Name:     "channelbalance",
1638
        Category: "Channels",
1639
        Usage: "Returns the sum of the total available channel balance across " +
1640
                "all open channels.",
1641
        Action: actionDecorator(ChannelBalance),
1642
}
1643

1644
func ChannelBalance(ctx *cli.Context) error {
×
1645
        ctxc := getContext()
×
1646
        client, cleanUp := getClient(ctx)
×
1647
        defer cleanUp()
×
1648

×
1649
        req := &lnrpc.ChannelBalanceRequest{}
×
1650
        resp, err := client.ChannelBalance(ctxc, req)
×
1651
        if err != nil {
×
1652
                return err
×
1653
        }
×
1654

1655
        printRespJSON(resp)
×
1656
        return nil
×
1657
}
1658

1659
var generateManPageCommand = cli.Command{
1660
        Name: "generatemanpage",
1661
        Usage: "Generates a man page for lncli and lnd as " +
1662
                "lncli.1 and lnd.1 respectively.",
1663
        Hidden: true,
1664
        Action: actionDecorator(generateManPage),
1665
}
1666

1667
func generateManPage(ctx *cli.Context) error {
×
1668
        // Generate the man pages for lncli as lncli.1.
×
1669
        manpages, err := ctx.App.ToMan()
×
1670
        if err != nil {
×
1671
                return err
×
1672
        }
×
1673
        err = os.WriteFile("lncli.1", []byte(manpages), 0644)
×
1674
        if err != nil {
×
1675
                return err
×
1676
        }
×
1677

1678
        // Generate the man pages for lnd as lnd.1.
1679
        config := lnd.DefaultConfig()
×
1680
        fileParser := flags.NewParser(&config, flags.Default)
×
1681
        fileParser.Name = "lnd"
×
1682

×
1683
        var buf bytes.Buffer
×
1684
        fileParser.WriteManPage(&buf)
×
1685

×
1686
        err = os.WriteFile("lnd.1", buf.Bytes(), 0644)
×
1687
        if err != nil {
×
1688
                return err
×
1689
        }
×
1690

1691
        return nil
×
1692
}
1693

1694
var getInfoCommand = cli.Command{
1695
        Name:   "getinfo",
1696
        Usage:  "Returns basic information related to the active daemon.",
1697
        Action: actionDecorator(getInfo),
1698
}
1699

1700
func getInfo(ctx *cli.Context) error {
×
1701
        ctxc := getContext()
×
1702
        client, cleanUp := getClient(ctx)
×
1703
        defer cleanUp()
×
1704

×
1705
        req := &lnrpc.GetInfoRequest{}
×
1706
        resp, err := client.GetInfo(ctxc, req)
×
1707
        if err != nil {
×
1708
                return err
×
1709
        }
×
1710

1711
        printRespJSON(resp)
×
1712
        return nil
×
1713
}
1714

1715
var getRecoveryInfoCommand = cli.Command{
1716
        Name:   "getrecoveryinfo",
1717
        Usage:  "Display information about an ongoing recovery attempt.",
1718
        Action: actionDecorator(getRecoveryInfo),
1719
}
1720

1721
func getRecoveryInfo(ctx *cli.Context) error {
×
1722
        ctxc := getContext()
×
1723
        client, cleanUp := getClient(ctx)
×
1724
        defer cleanUp()
×
1725

×
1726
        req := &lnrpc.GetRecoveryInfoRequest{}
×
1727
        resp, err := client.GetRecoveryInfo(ctxc, req)
×
1728
        if err != nil {
×
1729
                return err
×
1730
        }
×
1731

1732
        printRespJSON(resp)
×
1733
        return nil
×
1734
}
1735

1736
var pendingChannelsCommand = cli.Command{
1737
        Name:     "pendingchannels",
1738
        Category: "Channels",
1739
        Usage:    "Display information pertaining to pending channels.",
1740
        Flags: []cli.Flag{
1741
                cli.BoolFlag{
1742
                        Name: "include_raw_tx",
1743
                        Usage: "include the raw transaction hex for " +
1744
                                "waiting_close_channels.",
1745
                },
1746
        },
1747
        Action: actionDecorator(pendingChannels),
1748
}
1749

1750
func pendingChannels(ctx *cli.Context) error {
×
1751
        ctxc := getContext()
×
1752
        client, cleanUp := getClient(ctx)
×
1753
        defer cleanUp()
×
1754

×
1755
        includeRawTx := ctx.Bool("include_raw_tx")
×
1756
        req := &lnrpc.PendingChannelsRequest{
×
1757
                IncludeRawTx: includeRawTx,
×
1758
        }
×
1759
        resp, err := client.PendingChannels(ctxc, req)
×
1760
        if err != nil {
×
1761
                return err
×
1762
        }
×
1763

1764
        printRespJSON(resp)
×
1765

×
1766
        return nil
×
1767
}
1768

1769
var ListChannelsCommand = cli.Command{
1770
        Name:     "listchannels",
1771
        Category: "Channels",
1772
        Usage:    "List all open channels.",
1773
        Flags: []cli.Flag{
1774
                cli.BoolFlag{
1775
                        Name:  "active_only",
1776
                        Usage: "only list channels which are currently active",
1777
                },
1778
                cli.BoolFlag{
1779
                        Name:  "inactive_only",
1780
                        Usage: "only list channels which are currently inactive",
1781
                },
1782
                cli.BoolFlag{
1783
                        Name:  "public_only",
1784
                        Usage: "only list channels which are currently public",
1785
                },
1786
                cli.BoolFlag{
1787
                        Name:  "private_only",
1788
                        Usage: "only list channels which are currently private",
1789
                },
1790
                cli.StringFlag{
1791
                        Name: "peer",
1792
                        Usage: "(optional) only display channels with a " +
1793
                                "particular peer, accepts 66-byte, " +
1794
                                "hex-encoded pubkeys",
1795
                },
1796
                cli.BoolFlag{
1797
                        Name: "skip_peer_alias_lookup",
1798
                        Usage: "skip the peer alias lookup per channel in " +
1799
                                "order to improve performance",
1800
                },
1801
        },
1802
        Action: actionDecorator(ListChannels),
1803
}
1804

1805
var listAliasesCommand = cli.Command{
1806
        Name:     "listaliases",
1807
        Category: "Channels",
1808
        Usage:    "List all aliases.",
1809
        Flags:    []cli.Flag{},
1810
        Action:   actionDecorator(listAliases),
1811
}
1812

1813
func listAliases(ctx *cli.Context) error {
×
1814
        ctxc := getContext()
×
1815
        client, cleanUp := getClient(ctx)
×
1816
        defer cleanUp()
×
1817

×
1818
        req := &lnrpc.ListAliasesRequest{}
×
1819

×
1820
        resp, err := client.ListAliases(ctxc, req)
×
1821
        if err != nil {
×
1822
                return err
×
1823
        }
×
1824

1825
        printRespJSON(resp)
×
1826

×
1827
        return nil
×
1828
}
1829

1830
func ListChannels(ctx *cli.Context) error {
×
1831
        ctxc := getContext()
×
1832
        client, cleanUp := getClient(ctx)
×
1833
        defer cleanUp()
×
1834

×
1835
        peer := ctx.String("peer")
×
1836

×
1837
        // If the user requested channels with a particular key, parse the
×
1838
        // provided pubkey.
×
1839
        var peerKey []byte
×
1840
        if len(peer) > 0 {
×
1841
                pk, err := route.NewVertexFromStr(peer)
×
1842
                if err != nil {
×
1843
                        return fmt.Errorf("invalid --peer pubkey: %w", err)
×
1844
                }
×
1845

1846
                peerKey = pk[:]
×
1847
        }
1848

1849
        // By default, we will look up the peers' alias information unless the
1850
        // skip_peer_alias_lookup flag indicates otherwise.
1851
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1852

×
1853
        req := &lnrpc.ListChannelsRequest{
×
1854
                ActiveOnly:      ctx.Bool("active_only"),
×
1855
                InactiveOnly:    ctx.Bool("inactive_only"),
×
1856
                PublicOnly:      ctx.Bool("public_only"),
×
1857
                PrivateOnly:     ctx.Bool("private_only"),
×
1858
                Peer:            peerKey,
×
1859
                PeerAliasLookup: lookupPeerAlias,
×
1860
        }
×
1861

×
1862
        resp, err := client.ListChannels(ctxc, req)
×
1863
        if err != nil {
×
1864
                return err
×
1865
        }
×
1866

1867
        printRespJSON(resp)
×
1868

×
1869
        return nil
×
1870
}
1871

1872
var closedChannelsCommand = cli.Command{
1873
        Name:     "closedchannels",
1874
        Category: "Channels",
1875
        Usage:    "List all closed channels.",
1876
        Flags: []cli.Flag{
1877
                cli.BoolFlag{
1878
                        Name:  "cooperative",
1879
                        Usage: "list channels that were closed cooperatively",
1880
                },
1881
                cli.BoolFlag{
1882
                        Name: "local_force",
1883
                        Usage: "list channels that were force-closed " +
1884
                                "by the local node",
1885
                },
1886
                cli.BoolFlag{
1887
                        Name: "remote_force",
1888
                        Usage: "list channels that were force-closed " +
1889
                                "by the remote node",
1890
                },
1891
                cli.BoolFlag{
1892
                        Name: "breach",
1893
                        Usage: "list channels for which the remote node " +
1894
                                "attempted to broadcast a prior " +
1895
                                "revoked channel state",
1896
                },
1897
                cli.BoolFlag{
1898
                        Name:  "funding_canceled",
1899
                        Usage: "list channels that were never fully opened",
1900
                },
1901
                cli.BoolFlag{
1902
                        Name: "abandoned",
1903
                        Usage: "list channels that were abandoned by " +
1904
                                "the local node",
1905
                },
1906
        },
1907
        Action: actionDecorator(closedChannels),
1908
}
1909

1910
func closedChannels(ctx *cli.Context) error {
×
1911
        ctxc := getContext()
×
1912
        client, cleanUp := getClient(ctx)
×
1913
        defer cleanUp()
×
1914

×
1915
        req := &lnrpc.ClosedChannelsRequest{
×
1916
                Cooperative:     ctx.Bool("cooperative"),
×
1917
                LocalForce:      ctx.Bool("local_force"),
×
1918
                RemoteForce:     ctx.Bool("remote_force"),
×
1919
                Breach:          ctx.Bool("breach"),
×
1920
                FundingCanceled: ctx.Bool("funding_canceled"),
×
1921
                Abandoned:       ctx.Bool("abandoned"),
×
1922
        }
×
1923

×
1924
        resp, err := client.ClosedChannels(ctxc, req)
×
1925
        if err != nil {
×
1926
                return err
×
1927
        }
×
1928

1929
        printRespJSON(resp)
×
1930

×
1931
        return nil
×
1932
}
1933

1934
var describeGraphCommand = cli.Command{
1935
        Name:     "describegraph",
1936
        Category: "Graph",
1937
        Description: "Prints a human readable version of the known channel " +
1938
                "graph from the PoV of the node",
1939
        Usage: "Describe the network graph.",
1940
        Flags: []cli.Flag{
1941
                cli.BoolFlag{
1942
                        Name: "include_unannounced",
1943
                        Usage: "If set, unannounced channels will be included in the " +
1944
                                "graph. Unannounced channels are both private channels, and " +
1945
                                "public channels that are not yet announced to the network.",
1946
                },
1947
        },
1948
        Action: actionDecorator(describeGraph),
1949
}
1950

1951
func describeGraph(ctx *cli.Context) error {
×
1952
        ctxc := getContext()
×
1953
        client, cleanUp := getClient(ctx)
×
1954
        defer cleanUp()
×
1955

×
1956
        req := &lnrpc.ChannelGraphRequest{
×
1957
                IncludeUnannounced: ctx.Bool("include_unannounced"),
×
1958
        }
×
1959

×
1960
        graph, err := client.DescribeGraph(ctxc, req)
×
1961
        if err != nil {
×
1962
                return err
×
1963
        }
×
1964

1965
        printRespJSON(graph)
×
1966
        return nil
×
1967
}
1968

1969
var getNodeMetricsCommand = cli.Command{
1970
        Name:        "getnodemetrics",
1971
        Category:    "Graph",
1972
        Description: "Prints out node metrics calculated from the current graph",
1973
        Usage:       "Get node metrics.",
1974
        Action:      actionDecorator(getNodeMetrics),
1975
}
1976

1977
func getNodeMetrics(ctx *cli.Context) error {
×
1978
        ctxc := getContext()
×
1979
        client, cleanUp := getClient(ctx)
×
1980
        defer cleanUp()
×
1981

×
1982
        req := &lnrpc.NodeMetricsRequest{
×
1983
                Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
×
1984
        }
×
1985

×
1986
        nodeMetrics, err := client.GetNodeMetrics(ctxc, req)
×
1987
        if err != nil {
×
1988
                return err
×
1989
        }
×
1990

1991
        printRespJSON(nodeMetrics)
×
1992
        return nil
×
1993
}
1994

1995
var getChanInfoCommand = cli.Command{
1996
        Name:     "getchaninfo",
1997
        Category: "Graph",
1998
        Usage:    "Get the state of a channel.",
1999
        Description: "Prints out the latest authenticated state for a " +
2000
                "particular channel",
2001
        ArgsUsage: "chan_id",
2002
        Flags: []cli.Flag{
2003
                cli.Uint64Flag{
2004
                        Name: "chan_id",
2005
                        Usage: "The 8-byte compact channel ID to query for. " +
2006
                                "If this is set the chan_point param is " +
2007
                                "ignored.",
2008
                },
2009
                cli.StringFlag{
2010
                        Name: "chan_point",
2011
                        Usage: "The channel point in format txid:index. If " +
2012
                                "the chan_id param is set this param is " +
2013
                                "ignored.",
2014
                },
2015
        },
2016
        Action: actionDecorator(getChanInfo),
2017
}
2018

2019
func getChanInfo(ctx *cli.Context) error {
×
2020
        ctxc := getContext()
×
2021
        client, cleanUp := getClient(ctx)
×
2022
        defer cleanUp()
×
2023

×
2024
        var (
×
2025
                chanID    uint64
×
2026
                chanPoint string
×
2027
                err       error
×
2028
        )
×
2029

×
2030
        switch {
×
2031
        case ctx.IsSet("chan_id"):
×
2032
                chanID = ctx.Uint64("chan_id")
×
2033

2034
        case ctx.Args().Present():
×
2035
                chanID, err = strconv.ParseUint(ctx.Args().First(), 10, 64)
×
2036
                if err != nil {
×
2037
                        return fmt.Errorf("error parsing chan_id: %w", err)
×
2038
                }
×
2039

2040
        case ctx.IsSet("chan_point"):
×
2041
                chanPoint = ctx.String("chan_point")
×
2042

2043
        default:
×
2044
                return fmt.Errorf("chan_id or chan_point argument missing")
×
2045
        }
2046

2047
        req := &lnrpc.ChanInfoRequest{
×
2048
                ChanId:    chanID,
×
2049
                ChanPoint: chanPoint,
×
2050
        }
×
2051

×
2052
        chanInfo, err := client.GetChanInfo(ctxc, req)
×
2053
        if err != nil {
×
2054
                return err
×
2055
        }
×
2056

2057
        printRespJSON(chanInfo)
×
2058
        return nil
×
2059
}
2060

2061
var getNodeInfoCommand = cli.Command{
2062
        Name:     "getnodeinfo",
2063
        Category: "Graph",
2064
        Usage:    "Get information on a specific node.",
2065
        Description: "Prints out the latest authenticated node state for an " +
2066
                "advertised node",
2067
        Flags: []cli.Flag{
2068
                cli.StringFlag{
2069
                        Name: "pub_key",
2070
                        Usage: "the 33-byte hex-encoded compressed public of the target " +
2071
                                "node",
2072
                },
2073
                cli.BoolFlag{
2074
                        Name: "include_channels",
2075
                        Usage: "if true, will return all known channels " +
2076
                                "associated with the node",
2077
                },
2078
        },
2079
        Action: actionDecorator(getNodeInfo),
2080
}
2081

2082
func getNodeInfo(ctx *cli.Context) error {
×
2083
        ctxc := getContext()
×
2084
        client, cleanUp := getClient(ctx)
×
2085
        defer cleanUp()
×
2086

×
2087
        args := ctx.Args()
×
2088

×
2089
        var pubKey string
×
2090
        switch {
×
2091
        case ctx.IsSet("pub_key"):
×
2092
                pubKey = ctx.String("pub_key")
×
2093
        case args.Present():
×
2094
                pubKey = args.First()
×
2095
        default:
×
2096
                return fmt.Errorf("pub_key argument missing")
×
2097
        }
2098

2099
        req := &lnrpc.NodeInfoRequest{
×
2100
                PubKey:          pubKey,
×
2101
                IncludeChannels: ctx.Bool("include_channels"),
×
2102
        }
×
2103

×
2104
        nodeInfo, err := client.GetNodeInfo(ctxc, req)
×
2105
        if err != nil {
×
2106
                return err
×
2107
        }
×
2108

2109
        printRespJSON(nodeInfo)
×
2110
        return nil
×
2111
}
2112

2113
var getNetworkInfoCommand = cli.Command{
2114
        Name:     "getnetworkinfo",
2115
        Category: "Channels",
2116
        Usage: "Get statistical information about the current " +
2117
                "state of the network.",
2118
        Description: "Returns a set of statistics pertaining to the known " +
2119
                "channel graph",
2120
        Action: actionDecorator(getNetworkInfo),
2121
}
2122

2123
func getNetworkInfo(ctx *cli.Context) error {
×
2124
        ctxc := getContext()
×
2125
        client, cleanUp := getClient(ctx)
×
2126
        defer cleanUp()
×
2127

×
2128
        req := &lnrpc.NetworkInfoRequest{}
×
2129

×
2130
        netInfo, err := client.GetNetworkInfo(ctxc, req)
×
2131
        if err != nil {
×
2132
                return err
×
2133
        }
×
2134

2135
        printRespJSON(netInfo)
×
2136
        return nil
×
2137
}
2138

2139
var debugLevelCommand = cli.Command{
2140
        Name:  "debuglevel",
2141
        Usage: "Set the debug level.",
2142
        Description: `Logging level for all subsystems {trace, debug, info, warn, error, critical, off}
2143
        You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems
2144

2145
        Use show to list available subsystems`,
2146
        Flags: []cli.Flag{
2147
                cli.BoolFlag{
2148
                        Name:  "show",
2149
                        Usage: "if true, then the list of available sub-systems will be printed out",
2150
                },
2151
                cli.StringFlag{
2152
                        Name:  "level",
2153
                        Usage: "the level specification to target either a coarse logging level, or granular set of specific sub-systems with logging levels for each",
2154
                },
2155
        },
2156
        Action: actionDecorator(debugLevel),
2157
}
2158

2159
func debugLevel(ctx *cli.Context) error {
×
2160
        ctxc := getContext()
×
2161
        client, cleanUp := getClient(ctx)
×
2162
        defer cleanUp()
×
2163
        req := &lnrpc.DebugLevelRequest{
×
2164
                Show:      ctx.Bool("show"),
×
2165
                LevelSpec: ctx.String("level"),
×
2166
        }
×
2167

×
2168
        resp, err := client.DebugLevel(ctxc, req)
×
2169
        if err != nil {
×
2170
                return err
×
2171
        }
×
2172

2173
        printRespJSON(resp)
×
2174
        return nil
×
2175
}
2176

2177
var listChainTxnsCommand = cli.Command{
2178
        Name:     "listchaintxns",
2179
        Category: "On-chain",
2180
        Usage:    "List transactions from the wallet.",
2181
        Flags: []cli.Flag{
2182
                cli.Int64Flag{
2183
                        Name: "start_height",
2184
                        Usage: "the block height from which to list " +
2185
                                "transactions, inclusive",
2186
                },
2187
                cli.Int64Flag{
2188
                        Name: "end_height",
2189
                        Usage: "the block height until which to list " +
2190
                                "transactions, inclusive, to get " +
2191
                                "transactions until the chain tip, including " +
2192
                                "unconfirmed, set this value to -1",
2193
                },
2194
                cli.UintFlag{
2195
                        Name: "index_offset",
2196
                        Usage: "the index of a transaction that will be " +
2197
                                "used in a query to determine which " +
2198
                                "transaction should be returned in the " +
2199
                                "response",
2200
                },
2201
                cli.IntFlag{
2202
                        Name: "max_transactions",
2203
                        Usage: "(optional) the max number of transactions to " +
2204
                                "return; leave at default of 0 to return " +
2205
                                "all transactions",
2206
                        Value: 0,
2207
                },
2208
        },
2209
        Description: `
2210
        List all transactions an address of the wallet was involved in.
2211

2212
        This call will return a list of wallet related transactions that paid
2213
        to an address our wallet controls, or spent utxos that we held. The
2214
        start_height and end_height flags can be used to specify an inclusive
2215
        block range over which to query for transactions. If the end_height is
2216
        less than the start_height, transactions will be queried in reverse.
2217
        To get all transactions until the chain tip, including unconfirmed
2218
        transactions (identifiable with BlockHeight=0), set end_height to -1.
2219
        By default, this call will get all transactions our wallet was involved
2220
        in, including unconfirmed transactions.
2221
`,
2222
        Action: actionDecorator(listChainTxns),
2223
}
2224

2225
func listChainTxns(ctx *cli.Context) error {
×
2226
        ctxc := getContext()
×
2227
        client, cleanUp := getClient(ctx)
×
2228
        defer cleanUp()
×
2229

×
2230
        req := &lnrpc.GetTransactionsRequest{
×
2231
                IndexOffset:     uint32(ctx.Uint64("index_offset")),
×
2232
                MaxTransactions: uint32(ctx.Uint64("max_transactions")),
×
2233
        }
×
2234

×
2235
        if ctx.IsSet("start_height") {
×
2236
                req.StartHeight = int32(ctx.Int64("start_height"))
×
2237
        }
×
2238
        if ctx.IsSet("end_height") {
×
2239
                req.EndHeight = int32(ctx.Int64("end_height"))
×
2240
        }
×
2241

2242
        resp, err := client.GetTransactions(ctxc, req)
×
2243
        if err != nil {
×
2244
                return err
×
2245
        }
×
2246

2247
        printRespJSON(resp)
×
2248
        return nil
×
2249
}
2250

2251
var stopCommand = cli.Command{
2252
        Name:  "stop",
2253
        Usage: "Stop and shutdown the daemon.",
2254
        Description: `
2255
        Gracefully stop all daemon subsystems before stopping the daemon itself.
2256
        This is equivalent to stopping it using CTRL-C.`,
2257
        Action: actionDecorator(stopDaemon),
2258
}
2259

2260
func stopDaemon(ctx *cli.Context) error {
×
2261
        ctxc := getContext()
×
2262
        client, cleanUp := getClient(ctx)
×
2263
        defer cleanUp()
×
2264

×
2265
        resp, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2266
        if err != nil {
×
2267
                return err
×
2268
        }
×
2269

2270
        printRespJSON(resp)
×
2271

×
2272
        return nil
×
2273
}
2274

2275
var signMessageCommand = cli.Command{
2276
        Name:      "signmessage",
2277
        Category:  "Wallet",
2278
        Usage:     "Sign a message with the node's private key.",
2279
        ArgsUsage: "msg",
2280
        Description: `
2281
        Sign msg with the resident node's private key.
2282
        Returns the signature as a zbase32 string.
2283

2284
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2285
        Flags: []cli.Flag{
2286
                cli.StringFlag{
2287
                        Name:  "msg",
2288
                        Usage: "the message to sign",
2289
                },
2290
        },
2291
        Action: actionDecorator(signMessage),
2292
}
2293

2294
func signMessage(ctx *cli.Context) error {
×
2295
        ctxc := getContext()
×
2296
        client, cleanUp := getClient(ctx)
×
2297
        defer cleanUp()
×
2298

×
2299
        var msg []byte
×
2300

×
2301
        switch {
×
2302
        case ctx.IsSet("msg"):
×
2303
                msg = []byte(ctx.String("msg"))
×
2304
        case ctx.Args().Present():
×
2305
                msg = []byte(ctx.Args().First())
×
2306
        default:
×
2307
                return fmt.Errorf("msg argument missing")
×
2308
        }
2309

2310
        resp, err := client.SignMessage(ctxc, &lnrpc.SignMessageRequest{Msg: msg})
×
2311
        if err != nil {
×
2312
                return err
×
2313
        }
×
2314

2315
        printRespJSON(resp)
×
2316
        return nil
×
2317
}
2318

2319
var verifyMessageCommand = cli.Command{
2320
        Name:      "verifymessage",
2321
        Category:  "Wallet",
2322
        Usage:     "Verify a message signed with the signature.",
2323
        ArgsUsage: "msg signature",
2324
        Description: `
2325
        Verify that the message was signed with a properly-formed signature
2326
        The signature must be zbase32 encoded and signed with the private key of
2327
        an active node in the resident node's channel database.
2328

2329
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2330
        Flags: []cli.Flag{
2331
                cli.StringFlag{
2332
                        Name:  "msg",
2333
                        Usage: "the message to verify",
2334
                },
2335
                cli.StringFlag{
2336
                        Name:  "sig",
2337
                        Usage: "the zbase32 encoded signature of the message",
2338
                },
2339
        },
2340
        Action: actionDecorator(verifyMessage),
2341
}
2342

2343
func verifyMessage(ctx *cli.Context) error {
×
2344
        ctxc := getContext()
×
2345
        client, cleanUp := getClient(ctx)
×
2346
        defer cleanUp()
×
2347

×
2348
        var (
×
2349
                msg []byte
×
2350
                sig string
×
2351
        )
×
2352

×
2353
        args := ctx.Args()
×
2354

×
2355
        switch {
×
2356
        case ctx.IsSet("msg"):
×
2357
                msg = []byte(ctx.String("msg"))
×
2358
        case args.Present():
×
2359
                msg = []byte(ctx.Args().First())
×
2360
                args = args.Tail()
×
2361
        default:
×
2362
                return fmt.Errorf("msg argument missing")
×
2363
        }
2364

2365
        switch {
×
2366
        case ctx.IsSet("sig"):
×
2367
                sig = ctx.String("sig")
×
2368
        case args.Present():
×
2369
                sig = args.First()
×
2370
        default:
×
2371
                return fmt.Errorf("signature argument missing")
×
2372
        }
2373

2374
        req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
×
2375
        resp, err := client.VerifyMessage(ctxc, req)
×
2376
        if err != nil {
×
2377
                return err
×
2378
        }
×
2379

2380
        printRespJSON(resp)
×
2381
        return nil
×
2382
}
2383

2384
var feeReportCommand = cli.Command{
2385
        Name:     "feereport",
2386
        Category: "Channels",
2387
        Usage:    "Display the current fee policies of all active channels.",
2388
        Description: `
2389
        Returns the current fee policies of all active channels.
2390
        Fee policies can be updated using the updatechanpolicy command.`,
2391
        Action: actionDecorator(feeReport),
2392
}
2393

2394
func feeReport(ctx *cli.Context) error {
×
2395
        ctxc := getContext()
×
2396
        client, cleanUp := getClient(ctx)
×
2397
        defer cleanUp()
×
2398

×
2399
        req := &lnrpc.FeeReportRequest{}
×
2400
        resp, err := client.FeeReport(ctxc, req)
×
2401
        if err != nil {
×
2402
                return err
×
2403
        }
×
2404

2405
        printRespJSON(resp)
×
2406
        return nil
×
2407
}
2408

2409
var updateChannelPolicyCommand = cli.Command{
2410
        Name:     "updatechanpolicy",
2411
        Category: "Channels",
2412
        Usage: "Update the channel policy for all channels, or a single " +
2413
                "channel.",
2414
        ArgsUsage: "base_fee_msat fee_rate time_lock_delta " +
2415
                "[--max_htlc_msat=N] [channel_point]",
2416
        Description: `
2417
        Updates the channel policy for all channels, or just a particular
2418
        channel identified by its channel point. The update will be committed, 
2419
        and broadcast to the rest of the network within the next batch. Channel
2420
        points are encoded as: funding_txid:output_index
2421
        `,
2422
        Flags: []cli.Flag{
2423
                cli.Int64Flag{
2424
                        Name: "base_fee_msat",
2425
                        Usage: "the base fee in milli-satoshis that will be " +
2426
                                "charged for each forwarded HTLC, regardless " +
2427
                                "of payment size",
2428
                },
2429
                cli.StringFlag{
2430
                        Name: "fee_rate",
2431
                        Usage: "the fee rate that will be charged " +
2432
                                "proportionally based on the value of each " +
2433
                                "forwarded HTLC, the lowest possible rate is " +
2434
                                "0 with a granularity of 0.000001 " +
2435
                                "(millionths). Can not be set at the same " +
2436
                                "time as fee_rate_ppm",
2437
                },
2438
                cli.Uint64Flag{
2439
                        Name: "fee_rate_ppm",
2440
                        Usage: "the fee rate ppm (parts per million) that " +
2441
                                "will be charged proportionally based on the " +
2442
                                "value of each forwarded HTLC, the lowest " +
2443
                                "possible rate is 0 with a granularity of " +
2444
                                "0.000001 (millionths). Can not be set at " +
2445
                                "the same time as fee_rate",
2446
                },
2447
                cli.Int64Flag{
2448
                        Name: "inbound_base_fee_msat",
2449
                        Usage: "the base inbound fee in milli-satoshis that " +
2450
                                "will be charged for each forwarded HTLC, " +
2451
                                "regardless of payment size. Its value must " +
2452
                                "be zero or negative - it is a discount " +
2453
                                "for using a particular incoming channel. " +
2454
                                "Note that forwards will be rejected if the " +
2455
                                "discount exceeds the outbound fee " +
2456
                                "(forward at a loss), and lead to " +
2457
                                "penalization by the sender",
2458
                },
2459
                cli.Int64Flag{
2460
                        Name: "inbound_fee_rate_ppm",
2461
                        Usage: "the inbound fee rate that will be charged " +
2462
                                "proportionally based on the value of each " +
2463
                                "forwarded HTLC and the outbound fee. Fee " +
2464
                                "rate is expressed in parts per million and " +
2465
                                "must be zero or negative - it is a discount " +
2466
                                "for using a particular incoming channel. " +
2467
                                "Note that forwards will be rejected if the " +
2468
                                "discount exceeds the outbound fee " +
2469
                                "(forward at a loss), and lead to " +
2470
                                "penalization by the sender",
2471
                },
2472
                cli.Uint64Flag{
2473
                        Name: "time_lock_delta",
2474
                        Usage: "the CLTV delta that will be applied to all " +
2475
                                "forwarded HTLCs",
2476
                },
2477
                cli.Uint64Flag{
2478
                        Name: "min_htlc_msat",
2479
                        Usage: "if set, the min HTLC size that will be " +
2480
                                "applied to all forwarded HTLCs. If unset, " +
2481
                                "the min HTLC is left unchanged",
2482
                },
2483
                cli.Uint64Flag{
2484
                        Name: "max_htlc_msat",
2485
                        Usage: "if set, the max HTLC size that will be " +
2486
                                "applied to all forwarded HTLCs. If unset, " +
2487
                                "the max HTLC is left unchanged",
2488
                },
2489
                cli.StringFlag{
2490
                        Name: "chan_point",
2491
                        Usage: "the channel which this policy update should " +
2492
                                "be applied to. If nil, the policies for all " +
2493
                                "channels will be updated. Takes the form of " +
2494
                                "txid:output_index",
2495
                },
2496
                cli.BoolFlag{
2497
                        Name: "create_missing_edge",
2498
                        Usage: "Under unknown circumstances a channel can " +
2499
                                "exist with a missing edge in the graph " +
2500
                                "database. This can cause an 'edge not " +
2501
                                "found' error when calling `getchaninfo` " +
2502
                                "and/or cause the default channel policy to " +
2503
                                "be used during forwards. Setting this flag " +
2504
                                "will recreate the edge if not found, " +
2505
                                "allowing updating this channel policy and " +
2506
                                "fixing the missing edge problem for this " +
2507
                                "channel permanently. For fields not set in " +
2508
                                "this command, the default policy will be " +
2509
                                "created.",
2510
                },
2511
        },
2512
        Action: actionDecorator(updateChannelPolicy),
2513
}
2514

2515
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
7✔
2516
        split := strings.Split(s, ":")
7✔
2517
        if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
10✔
2518
                return nil, errBadChanPoint
3✔
2519
        }
3✔
2520

2521
        index, err := strconv.ParseInt(split[1], 10, 64)
4✔
2522
        if err != nil {
5✔
2523
                return nil, fmt.Errorf("unable to decode output index: %w", err)
1✔
2524
        }
1✔
2525

2526
        txid, err := chainhash.NewHashFromStr(split[0])
3✔
2527
        if err != nil {
4✔
2528
                return nil, fmt.Errorf("unable to parse hex string: %w", err)
1✔
2529
        }
1✔
2530

2531
        return &lnrpc.ChannelPoint{
2✔
2532
                FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
2✔
2533
                        FundingTxidBytes: txid[:],
2✔
2534
                },
2✔
2535
                OutputIndex: uint32(index),
2✔
2536
        }, nil
2✔
2537
}
2538

2539
// parseTimeLockDelta is expected to get a uint16 type of timeLockDelta. Its
2540
// maximum value is MaxTimeLockDelta.
2541
func parseTimeLockDelta(timeLockDeltaStr string) (uint16, error) {
5✔
2542
        timeLockDeltaUnCheck, err := strconv.ParseUint(timeLockDeltaStr, 10, 64)
5✔
2543
        if err != nil {
7✔
2544
                return 0, fmt.Errorf("failed to parse time_lock_delta: %s "+
2✔
2545
                        "to uint64, err: %v", timeLockDeltaStr, err)
2✔
2546
        }
2✔
2547

2548
        if timeLockDeltaUnCheck > routing.MaxCLTVDelta {
3✔
2549
                return 0, fmt.Errorf("time_lock_delta is too big, "+
×
2550
                        "max value is %d", routing.MaxCLTVDelta)
×
2551
        }
×
2552

2553
        return uint16(timeLockDeltaUnCheck), nil
3✔
2554
}
2555

2556
func updateChannelPolicy(ctx *cli.Context) error {
×
2557
        ctxc := getContext()
×
2558
        client, cleanUp := getClient(ctx)
×
2559
        defer cleanUp()
×
2560

×
2561
        var (
×
2562
                baseFee       int64
×
2563
                feeRate       float64
×
2564
                feeRatePpm    uint64
×
2565
                timeLockDelta uint16
×
2566
                err           error
×
2567
        )
×
2568
        args := ctx.Args()
×
2569

×
2570
        switch {
×
2571
        case ctx.IsSet("base_fee_msat"):
×
2572
                baseFee = ctx.Int64("base_fee_msat")
×
2573
        case args.Present():
×
2574
                baseFee, err = strconv.ParseInt(args.First(), 10, 64)
×
2575
                if err != nil {
×
2576
                        return fmt.Errorf("unable to decode base_fee_msat: %w",
×
2577
                                err)
×
2578
                }
×
2579
                args = args.Tail()
×
2580
        default:
×
2581
                return fmt.Errorf("base_fee_msat argument missing")
×
2582
        }
2583

2584
        switch {
×
2585
        case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
×
2586
                return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
×
2587
        case ctx.IsSet("fee_rate"):
×
2588
                feeRate = ctx.Float64("fee_rate")
×
2589
        case ctx.IsSet("fee_rate_ppm"):
×
2590
                feeRatePpm = ctx.Uint64("fee_rate_ppm")
×
2591
        case args.Present():
×
2592
                feeRate, err = strconv.ParseFloat(args.First(), 64)
×
2593
                if err != nil {
×
2594
                        return fmt.Errorf("unable to decode fee_rate: %w", err)
×
2595
                }
×
2596

2597
                args = args.Tail()
×
2598
        default:
×
2599
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
×
2600
        }
2601

2602
        switch {
×
2603
        case ctx.IsSet("time_lock_delta"):
×
2604
                timeLockDeltaStr := ctx.String("time_lock_delta")
×
2605
                timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
×
2606
                if err != nil {
×
2607
                        return err
×
2608
                }
×
2609
        case args.Present():
×
2610
                timeLockDelta, err = parseTimeLockDelta(args.First())
×
2611
                if err != nil {
×
2612
                        return err
×
2613
                }
×
2614

2615
                args = args.Tail()
×
2616
        default:
×
2617
                return fmt.Errorf("time_lock_delta argument missing")
×
2618
        }
2619

2620
        var (
×
2621
                chanPoint    *lnrpc.ChannelPoint
×
2622
                chanPointStr string
×
2623
        )
×
2624

×
2625
        switch {
×
2626
        case ctx.IsSet("chan_point"):
×
2627
                chanPointStr = ctx.String("chan_point")
×
2628
        case args.Present():
×
2629
                chanPointStr = args.First()
×
2630
        }
2631

2632
        if chanPointStr != "" {
×
2633
                chanPoint, err = parseChanPoint(chanPointStr)
×
2634
                if err != nil {
×
2635
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2636
                }
×
2637
        }
2638

2639
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2640
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2641
                inboundBaseFeeMsat > math.MaxInt32 {
×
2642

×
2643
                return errors.New("inbound_base_fee_msat out of range")
×
2644
        }
×
2645

2646
        inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
×
2647
        if inboundFeeRatePpm < math.MinInt32 ||
×
2648
                inboundFeeRatePpm > math.MaxInt32 {
×
2649

×
2650
                return errors.New("inbound_fee_rate_ppm out of range")
×
2651
        }
×
2652

2653
        // Inbound fees are optional. However, if an update is required,
2654
        // both the base fee and the fee rate must be provided.
2655
        var inboundFee *lnrpc.InboundFee
×
2656
        if ctx.IsSet("inbound_base_fee_msat") !=
×
2657
                ctx.IsSet("inbound_fee_rate_ppm") {
×
2658

×
2659
                return errors.New("both parameters must be provided: " +
×
2660
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
×
2661
        }
×
2662

2663
        if ctx.IsSet("inbound_fee_rate_ppm") {
×
2664
                inboundFee = &lnrpc.InboundFee{
×
2665
                        BaseFeeMsat: int32(inboundBaseFeeMsat),
×
2666
                        FeeRatePpm:  int32(inboundFeeRatePpm),
×
2667
                }
×
2668
        }
×
2669

2670
        createMissingEdge := ctx.Bool("create_missing_edge")
×
2671

×
2672
        req := &lnrpc.PolicyUpdateRequest{
×
2673
                BaseFeeMsat:       baseFee,
×
2674
                TimeLockDelta:     uint32(timeLockDelta),
×
2675
                MaxHtlcMsat:       ctx.Uint64("max_htlc_msat"),
×
2676
                InboundFee:        inboundFee,
×
2677
                CreateMissingEdge: createMissingEdge,
×
2678
        }
×
2679

×
2680
        if ctx.IsSet("min_htlc_msat") {
×
2681
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2682
                req.MinHtlcMsatSpecified = true
×
2683
        }
×
2684

2685
        if chanPoint != nil {
×
2686
                req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
×
2687
                        ChanPoint: chanPoint,
×
2688
                }
×
2689
        } else {
×
2690
                req.Scope = &lnrpc.PolicyUpdateRequest_Global{
×
2691
                        Global: true,
×
2692
                }
×
2693
        }
×
2694

2695
        if feeRate != 0 {
×
2696
                req.FeeRate = feeRate
×
2697
        } else if feeRatePpm != 0 {
×
2698
                req.FeeRatePpm = uint32(feeRatePpm)
×
2699
        }
×
2700

2701
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2702
        if err != nil {
×
2703
                return err
×
2704
        }
×
2705

2706
        // Parse the response into the final json object that will be printed
2707
        // to stdout. At the moment, this filters out the raw txid bytes from
2708
        // each failed update's outpoint and only prints the txid string.
2709
        var listFailedUpdateResp = struct {
×
2710
                FailedUpdates []*FailedUpdate `json:"failed_updates"`
×
2711
        }{
×
2712
                FailedUpdates: make([]*FailedUpdate, 0, len(resp.FailedUpdates)),
×
2713
        }
×
2714
        for _, protoUpdate := range resp.FailedUpdates {
×
2715
                failedUpdate := NewFailedUpdateFromProto(protoUpdate)
×
2716
                listFailedUpdateResp.FailedUpdates = append(
×
2717
                        listFailedUpdateResp.FailedUpdates, failedUpdate)
×
2718
        }
×
2719

2720
        printJSON(listFailedUpdateResp)
×
2721

×
2722
        return nil
×
2723
}
2724

2725
var fishCompletionCommand = cli.Command{
2726
        Name:   "fish-completion",
2727
        Hidden: true,
2728
        Action: func(c *cli.Context) error {
×
2729
                completion, err := c.App.ToFishCompletion()
×
2730
                if err != nil {
×
2731
                        return err
×
2732
                }
×
2733

2734
                // We don't want to suggest files, so we add this
2735
                // first line to the completions.
2736
                _, err = fmt.Printf("complete -c %q -f \n%s", c.App.Name, completion)
×
2737
                return err
×
2738
        },
2739
}
2740

2741
var exportChanBackupCommand = cli.Command{
2742
        Name:     "exportchanbackup",
2743
        Category: "Channels",
2744
        Usage: "Obtain a static channel back up for a selected channels, " +
2745
                "or all known channels.",
2746
        ArgsUsage: "[chan_point] [--all] [--output_file]",
2747
        Description: `
2748
        This command allows a user to export a Static Channel Backup (SCB) for
2749
        a selected channel. SCB's are encrypted backups of a channel's initial
2750
        state that are encrypted with a key derived from the seed of a user. In
2751
        the case of partial or complete data loss, the SCB will allow the user
2752
        to reclaim settled funds in the channel at its final state. The
2753
        exported channel backups can be restored at a later time using the
2754
        restorechanbackup command.
2755

2756
        This command will return one of two types of channel backups depending
2757
        on the set of passed arguments:
2758

2759
           * If a target channel point is specified, then a single channel
2760
             backup containing only the information for that channel will be
2761
             returned.
2762

2763
           * If the --all flag is passed, then a multi-channel backup will be
2764
             returned. A multi backup is a single encrypted blob (displayed in
2765
             hex encoding) that contains several channels in a single cipher
2766
             text.
2767

2768
        Both of the backup types can be restored using the restorechanbackup
2769
        command.
2770
        `,
2771
        Flags: []cli.Flag{
2772
                cli.StringFlag{
2773
                        Name:  "chan_point",
2774
                        Usage: "the target channel to obtain an SCB for",
2775
                },
2776
                cli.BoolFlag{
2777
                        Name: "all",
2778
                        Usage: "if specified, then a multi backup of all " +
2779
                                "active channels will be returned",
2780
                },
2781
                cli.StringFlag{
2782
                        Name: "output_file",
2783
                        Usage: `
2784
                        if specified, then rather than printing a JSON output
2785
                        of the static channel backup, a serialized version of
2786
                        the backup (either Single or Multi) will be written to
2787
                        the target file, this is the same format used by lnd in
2788
                        its channel.backup file `,
2789
                },
2790
        },
2791
        Action: actionDecorator(exportChanBackup),
2792
}
2793

2794
func exportChanBackup(ctx *cli.Context) error {
×
2795
        ctxc := getContext()
×
2796
        client, cleanUp := getClient(ctx)
×
2797
        defer cleanUp()
×
2798

×
2799
        // Show command help if no arguments provided
×
2800
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2801
                cli.ShowCommandHelp(ctx, "exportchanbackup")
×
2802
                return nil
×
2803
        }
×
2804

2805
        var (
×
2806
                err            error
×
2807
                chanPointStr   string
×
2808
                outputFileName string
×
2809
        )
×
2810
        args := ctx.Args()
×
2811

×
2812
        switch {
×
2813
        case ctx.IsSet("chan_point"):
×
2814
                chanPointStr = ctx.String("chan_point")
×
2815

2816
        case args.Present():
×
2817
                chanPointStr = args.First()
×
2818

2819
        case !ctx.IsSet("all"):
×
2820
                return fmt.Errorf("must specify chan_point if --all isn't set")
×
2821
        }
2822

2823
        if ctx.IsSet("output_file") {
×
2824
                outputFileName = ctx.String("output_file")
×
2825
        }
×
2826

2827
        if chanPointStr != "" {
×
2828
                chanPointRPC, err := parseChanPoint(chanPointStr)
×
2829
                if err != nil {
×
2830
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2831
                }
×
2832

2833
                chanBackup, err := client.ExportChannelBackup(
×
2834
                        ctxc, &lnrpc.ExportChannelBackupRequest{
×
2835
                                ChanPoint: chanPointRPC,
×
2836
                        },
×
2837
                )
×
2838
                if err != nil {
×
2839
                        return err
×
2840
                }
×
2841

2842
                txid, err := chainhash.NewHash(
×
2843
                        chanPointRPC.GetFundingTxidBytes(),
×
2844
                )
×
2845
                if err != nil {
×
2846
                        return err
×
2847
                }
×
2848

2849
                chanPoint := wire.OutPoint{
×
2850
                        Hash:  *txid,
×
2851
                        Index: chanPointRPC.OutputIndex,
×
2852
                }
×
2853

×
2854
                if outputFileName != "" {
×
2855
                        return os.WriteFile(
×
2856
                                outputFileName,
×
2857
                                chanBackup.ChanBackup,
×
2858
                                0666,
×
2859
                        )
×
2860
                }
×
2861

2862
                printJSON(struct {
×
2863
                        ChanPoint  string `json:"chan_point"`
×
2864
                        ChanBackup string `json:"chan_backup"`
×
2865
                }{
×
2866
                        ChanPoint:  chanPoint.String(),
×
2867
                        ChanBackup: hex.EncodeToString(chanBackup.ChanBackup),
×
2868
                })
×
2869
                return nil
×
2870
        }
2871

2872
        if !ctx.IsSet("all") {
×
2873
                return fmt.Errorf("if a channel isn't specified, -all must be")
×
2874
        }
×
2875

2876
        chanBackup, err := client.ExportAllChannelBackups(
×
2877
                ctxc, &lnrpc.ChanBackupExportRequest{},
×
2878
        )
×
2879
        if err != nil {
×
2880
                return err
×
2881
        }
×
2882

2883
        if outputFileName != "" {
×
2884
                return os.WriteFile(
×
2885
                        outputFileName,
×
2886
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2887
                        0666,
×
2888
                )
×
2889
        }
×
2890

2891
        // TODO(roasbeef): support for export | restore ?
2892

2893
        var chanPoints []string
×
2894
        for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
×
2895
                txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
×
2896
                if err != nil {
×
2897
                        return err
×
2898
                }
×
2899

2900
                chanPoints = append(chanPoints, wire.OutPoint{
×
2901
                        Hash:  *txid,
×
2902
                        Index: chanPoint.OutputIndex,
×
2903
                }.String())
×
2904
        }
2905

2906
        printRespJSON(chanBackup)
×
2907

×
2908
        return nil
×
2909
}
2910

2911
var verifyChanBackupCommand = cli.Command{
2912
        Name:      "verifychanbackup",
2913
        Category:  "Channels",
2914
        Usage:     "Verify an existing channel backup.",
2915
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file]",
2916
        Description: `
2917
    This command allows a user to verify an existing Single or Multi channel
2918
    backup for integrity. This is useful when a user has a backup, but is
2919
    unsure as to if it's valid or for the target node.
2920

2921
    The command will accept backups in one of four forms:
2922

2923
       * A single channel packed SCB, which can be obtained from
2924
         exportchanbackup. This should be passed in hex encoded format.
2925

2926
       * A packed multi-channel SCB, which couples several individual
2927
         static channel backups in single blob.
2928

2929
       * A file path which points to a packed single-channel backup within a
2930
         file, using the same format that lnd does in its channel.backup file.
2931

2932
       * A file path which points to a packed multi-channel backup within a
2933
         file, using the same format that lnd does in its channel.backup
2934
         file.
2935
    `,
2936
        Flags: []cli.Flag{
2937
                cli.StringFlag{
2938
                        Name: "single_backup",
2939
                        Usage: "a hex encoded single channel backup obtained " +
2940
                                "from exportchanbackup",
2941
                },
2942
                cli.StringFlag{
2943
                        Name: "multi_backup",
2944
                        Usage: "a hex encoded multi-channel backup obtained " +
2945
                                "from exportchanbackup",
2946
                },
2947

2948
                cli.StringFlag{
2949
                        Name:      "single_file",
2950
                        Usage:     "the path to a single-channel backup file",
2951
                        TakesFile: true,
2952
                },
2953

2954
                cli.StringFlag{
2955
                        Name:      "multi_file",
2956
                        Usage:     "the path to a multi-channel back up file",
2957
                        TakesFile: true,
2958
                },
2959
        },
2960
        Action: actionDecorator(verifyChanBackup),
2961
}
2962

2963
func verifyChanBackup(ctx *cli.Context) error {
×
2964
        ctxc := getContext()
×
2965
        client, cleanUp := getClient(ctx)
×
2966
        defer cleanUp()
×
2967

×
2968
        // Show command help if no arguments provided
×
2969
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2970
                cli.ShowCommandHelp(ctx, "verifychanbackup")
×
2971
                return nil
×
2972
        }
×
2973

2974
        backups, err := parseChanBackups(ctx)
×
2975
        if err != nil {
×
2976
                return err
×
2977
        }
×
2978

2979
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
2980

×
2981
        if backups.GetChanBackups() != nil {
×
2982
                verifyReq.SingleChanBackups = backups.GetChanBackups()
×
2983
        }
×
2984
        if backups.GetMultiChanBackup() != nil {
×
2985
                verifyReq.MultiChanBackup = &lnrpc.MultiChanBackup{
×
2986
                        MultiChanBackup: backups.GetMultiChanBackup(),
×
2987
                }
×
2988
        }
×
2989

2990
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
2991
        if err != nil {
×
2992
                return err
×
2993
        }
×
2994

2995
        printRespJSON(resp)
×
2996
        return nil
×
2997
}
2998

2999
var restoreChanBackupCommand = cli.Command{
3000
        Name:     "restorechanbackup",
3001
        Category: "Channels",
3002
        Usage: "Restore an existing single or multi-channel static channel " +
3003
                "backup.",
3004
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file=",
3005
        Description: `
3006
        Allows a user to restore a Static Channel Backup (SCB) that was
3007
        obtained either via the exportchanbackup command, or from lnd's
3008
        automatically managed channel.backup file. This command should be used
3009
        if a user is attempting to restore a channel due to data loss on a
3010
        running node restored with the same seed as the node that created the
3011
        channel. If successful, this command will allows the user to recover
3012
        the settled funds stored in the recovered channels.
3013

3014
        The command will accept backups in one of four forms:
3015

3016
           * A single channel packed SCB, which can be obtained from
3017
             exportchanbackup. This should be passed in hex encoded format.
3018

3019
           * A packed multi-channel SCB, which couples several individual
3020
             static channel backups in single blob.
3021

3022
           * A file path which points to a packed single-channel backup within
3023
             a file, using the same format that lnd does in its channel.backup
3024
             file.
3025

3026
           * A file path which points to a packed multi-channel backup within a
3027
             file, using the same format that lnd does in its channel.backup
3028
             file.
3029
        `,
3030
        Flags: []cli.Flag{
3031
                cli.StringFlag{
3032
                        Name: "single_backup",
3033
                        Usage: "a hex encoded single channel backup obtained " +
3034
                                "from exportchanbackup",
3035
                },
3036
                cli.StringFlag{
3037
                        Name: "multi_backup",
3038
                        Usage: "a hex encoded multi-channel backup obtained " +
3039
                                "from exportchanbackup",
3040
                },
3041

3042
                cli.StringFlag{
3043
                        Name:      "single_file",
3044
                        Usage:     "the path to a single-channel backup file",
3045
                        TakesFile: true,
3046
                },
3047

3048
                cli.StringFlag{
3049
                        Name:      "multi_file",
3050
                        Usage:     "the path to a multi-channel back up file",
3051
                        TakesFile: true,
3052
                },
3053
        },
3054
        Action: actionDecorator(restoreChanBackup),
3055
}
3056

3057
// errMissingChanBackup is an error returned when we attempt to parse a channel
3058
// backup from a CLI command, and it is missing.
3059
var errMissingChanBackup = errors.New("missing channel backup")
3060

3061
func parseChanBackups(ctx *cli.Context) (*lnrpc.RestoreChanBackupRequest, error) {
×
3062
        switch {
×
3063
        case ctx.IsSet("single_backup"):
×
3064
                packedBackup, err := hex.DecodeString(
×
3065
                        ctx.String("single_backup"),
×
3066
                )
×
3067
                if err != nil {
×
3068
                        return nil, fmt.Errorf("unable to decode single packed "+
×
3069
                                "backup: %v", err)
×
3070
                }
×
3071

3072
                return &lnrpc.RestoreChanBackupRequest{
×
3073
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3074
                                ChanBackups: &lnrpc.ChannelBackups{
×
3075
                                        ChanBackups: []*lnrpc.ChannelBackup{
×
3076
                                                {
×
3077
                                                        ChanBackup: packedBackup,
×
3078
                                                },
×
3079
                                        },
×
3080
                                },
×
3081
                        },
×
3082
                }, nil
×
3083

3084
        case ctx.IsSet("multi_backup"):
×
3085
                packedMulti, err := hex.DecodeString(
×
3086
                        ctx.String("multi_backup"),
×
3087
                )
×
3088
                if err != nil {
×
3089
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3090
                                "backup: %v", err)
×
3091
                }
×
3092

3093
                return &lnrpc.RestoreChanBackupRequest{
×
3094
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3095
                                MultiChanBackup: packedMulti,
×
3096
                        },
×
3097
                }, nil
×
3098

3099
        case ctx.IsSet("single_file"):
×
3100
                packedSingle, err := os.ReadFile(ctx.String("single_file"))
×
3101
                if err != nil {
×
3102
                        return nil, fmt.Errorf("unable to decode single "+
×
3103
                                "packed backup: %v", err)
×
3104
                }
×
3105

3106
                return &lnrpc.RestoreChanBackupRequest{
×
3107
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3108
                                ChanBackups: &lnrpc.ChannelBackups{
×
3109
                                        ChanBackups: []*lnrpc.ChannelBackup{{
×
3110
                                                ChanBackup: packedSingle,
×
3111
                                        }},
×
3112
                                },
×
3113
                        },
×
3114
                }, nil
×
3115

3116
        case ctx.IsSet("multi_file"):
×
3117
                packedMulti, err := os.ReadFile(ctx.String("multi_file"))
×
3118
                if err != nil {
×
3119
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3120
                                "backup: %v", err)
×
3121
                }
×
3122

3123
                return &lnrpc.RestoreChanBackupRequest{
×
3124
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3125
                                MultiChanBackup: packedMulti,
×
3126
                        },
×
3127
                }, nil
×
3128

3129
        default:
×
3130
                return nil, errMissingChanBackup
×
3131
        }
3132
}
3133

3134
func restoreChanBackup(ctx *cli.Context) error {
×
3135
        ctxc := getContext()
×
3136
        client, cleanUp := getClient(ctx)
×
3137
        defer cleanUp()
×
3138

×
3139
        // Show command help if no arguments provided
×
3140
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3141
                cli.ShowCommandHelp(ctx, "restorechanbackup")
×
3142
                return nil
×
3143
        }
×
3144

3145
        var req lnrpc.RestoreChanBackupRequest
×
3146

×
3147
        backups, err := parseChanBackups(ctx)
×
3148
        if err != nil {
×
3149
                return err
×
3150
        }
×
3151

3152
        req.Backup = backups.Backup
×
3153

×
3154
        resp, err := client.RestoreChannelBackups(ctxc, &req)
×
3155
        if err != nil {
×
3156
                return fmt.Errorf("unable to restore chan backups: %w", err)
×
3157
        }
×
3158

3159
        printRespJSON(resp)
×
3160

×
3161
        return nil
×
3162
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc