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

lightningnetwork / lnd / 13902158453

17 Mar 2025 02:36PM UTC coverage: 68.633% (+10.3%) from 58.315%
13902158453

Pull #9605

github

web-flow
Merge 55a221ba8 into 053d63e11
Pull Request #9605: cmd: fix error parsed from status

0 of 24 new or added lines in 1 file covered. (0.0%)

25 existing lines in 5 files now uncovered.

130402 of 190000 relevant lines covered (68.63%)

23572.6 hits per line

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

7.26
/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
// printRespJSON prints the response in a json format.
216
func printRespJSON(resp proto.Message) {
×
217
        jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(resp)
×
218
        if err != nil {
×
219
                fmt.Println("unable to decode response: ", err)
×
220
                return
×
221
        }
×
222

223
        // Make the custom data human readable.
224
        jsonBytesReplaced := replaceCustomData(jsonBytes)
×
225

×
226
        fmt.Printf("%s\n", jsonBytesReplaced)
×
227
}
228

229
// printModifiedProtoJSON prints the response with some additional formatting
230
// and replacements.
231
func printModifiedProtoJSON(resp proto.Message) {
×
232
        jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(resp)
×
233
        if err != nil {
×
234
                fmt.Println("unable to decode response: ", err)
×
235
                return
×
236
        }
×
237

238
        // Replace custom_channel_data in the JSON.
239
        jsonBytesReplaced := replaceCustomData(jsonBytes)
×
240

×
241
        // Replace chan_id with scid, and append scid_str and scid fields.
×
242
        jsonBytesReplaced = replaceAndAppendScid(jsonBytesReplaced)
×
243

×
244
        // Append the chan_id field to the JSON.
×
245
        jsonBytesReplaced = appendChanID(jsonBytesReplaced)
×
246

×
247
        fmt.Printf("%s\n", jsonBytesReplaced)
×
248
}
249

250
// actionDecorator is used to add additional information and error handling
251
// to command actions.
252
func actionDecorator(f func(*cli.Context) error) func(*cli.Context) error {
81✔
253
        return func(c *cli.Context) error {
81✔
NEW
254
                err := f(c)
×
255

×
NEW
256
                // Exit early if there's no error.
×
NEW
257
                if err == nil {
×
NEW
258
                        return nil
×
NEW
259
                }
×
260

261
                // Try to parse the Status representatio from this error.
NEW
262
                s, ok := status.FromError(err)
×
NEW
263

×
NEW
264
                // If this cannot be represented by a Status, exit early.
×
NEW
265
                if !ok {
×
266
                        return err
×
267
                }
×
268

269
                // If it's a command for the UnlockerService (like 'create' or
270
                // 'unlock') but the wallet is already unlocked, then these
271
                // methods aren't recognized any more because this service is
272
                // shut down after successful unlock.
NEW
273
                if s.Code() == codes.Unknown &&
×
NEW
274
                        (c.Command.Name == "create" ||
×
NEW
275
                                c.Command.Name == "unlock" ||
×
NEW
276
                                c.Command.Name == "changepassword" ||
×
NEW
277
                                c.Command.Name == "createwatchonly") {
×
NEW
278

×
NEW
279
                        return errors.New("wallet is already unlocked")
×
NEW
280
                }
×
281

282
                // lnd might be active, but not possible to contact using RPC if
283
                // the wallet is encrypted. If we get error code Unknown, it
284
                // means that lnd is running, but the RPC server is not active
285
                // yet (only WalletUnlocker server active) and most likely this
286
                // is because of an encrypted wallet.
NEW
287
                if s.Code() == codes.Unknown {
×
NEW
288
                        return errors.New("wallet is encrypted. " +
×
NEW
289
                                "Please unlock using 'lncli unlock', " +
×
NEW
290
                                "or set password using 'lncli create'" +
×
NEW
291
                                " if this is the first time starting " +
×
NEW
292
                                "lnd.")
×
NEW
293
                }
×
294

295
                return s.Err()
×
296
        }
297
}
298

299
var newAddressCommand = cli.Command{
300
        Name:      "newaddress",
301
        Category:  "Wallet",
302
        Usage:     "Generates a new address.",
303
        ArgsUsage: "address-type",
304
        Flags: []cli.Flag{
305
                cli.StringFlag{
306
                        Name: "account",
307
                        Usage: "(optional) the name of the account to " +
308
                                "generate a new address for",
309
                },
310
                cli.BoolFlag{
311
                        Name: "unused",
312
                        Usage: "(optional) return the last unused address " +
313
                                "instead of generating a new one",
314
                },
315
        },
316
        Description: `
317
        Generate a wallet new address. Address-types has to be one of:
318
            - p2wkh:  Pay to witness key hash
319
            - np2wkh: Pay to nested witness key hash
320
            - p2tr:   Pay to taproot pubkey`,
321
        Action: actionDecorator(newAddress),
322
}
323

324
func newAddress(ctx *cli.Context) error {
×
325
        ctxc := getContext()
×
326

×
327
        // Display the command's help message if we do not have the expected
×
328
        // number of arguments/flags.
×
329
        if ctx.NArg() != 1 || ctx.NumFlags() > 1 {
×
330
                return cli.ShowCommandHelp(ctx, "newaddress")
×
331
        }
×
332

333
        // Map the string encoded address type, to the concrete typed address
334
        // type enum. An unrecognized address type will result in an error.
335
        stringAddrType := ctx.Args().First()
×
336
        unused := ctx.Bool("unused")
×
337

×
338
        var addrType lnrpc.AddressType
×
339
        switch stringAddrType { // TODO(roasbeef): make them ints on the cli?
×
340
        case "p2wkh":
×
341
                addrType = lnrpc.AddressType_WITNESS_PUBKEY_HASH
×
342
                if unused {
×
343
                        addrType = lnrpc.AddressType_UNUSED_WITNESS_PUBKEY_HASH
×
344
                }
×
345
        case "np2wkh":
×
346
                addrType = lnrpc.AddressType_NESTED_PUBKEY_HASH
×
347
                if unused {
×
348
                        addrType = lnrpc.AddressType_UNUSED_NESTED_PUBKEY_HASH
×
349
                }
×
350
        case "p2tr":
×
351
                addrType = lnrpc.AddressType_TAPROOT_PUBKEY
×
352
                if unused {
×
353
                        addrType = lnrpc.AddressType_UNUSED_TAPROOT_PUBKEY
×
354
                }
×
355
        default:
×
356
                return fmt.Errorf("invalid address type %v, support address type "+
×
357
                        "are: p2wkh, np2wkh, and p2tr", stringAddrType)
×
358
        }
359

360
        client, cleanUp := getClient(ctx)
×
361
        defer cleanUp()
×
362

×
363
        addr, err := client.NewAddress(ctxc, &lnrpc.NewAddressRequest{
×
364
                Type:    addrType,
×
365
                Account: ctx.String("account"),
×
366
        })
×
367
        if err != nil {
×
368
                return err
×
369
        }
×
370

371
        printRespJSON(addr)
×
372
        return nil
×
373
}
374

375
var coinSelectionStrategyFlag = cli.StringFlag{
376
        Name: "coin_selection_strategy",
377
        Usage: "(optional) the strategy to use for selecting " +
378
                "coins. Possible values are 'largest', 'random', or " +
379
                "'global-config'. If either 'largest' or 'random' is " +
380
                "specified, it will override the globally configured " +
381
                "strategy in lnd.conf",
382
        Value: "global-config",
383
}
384

385
var estimateFeeCommand = cli.Command{
386
        Name:      "estimatefee",
387
        Category:  "On-chain",
388
        Usage:     "Get fee estimates for sending bitcoin on-chain to multiple addresses.",
389
        ArgsUsage: "send-json-string [--conf_target=N]",
390
        Description: `
391
        Get fee estimates for sending a transaction paying the specified amount(s) to the passed address(es).
392

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

395
            '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
396
        `,
397
        Flags: []cli.Flag{
398
                cli.Int64Flag{
399
                        Name: "conf_target",
400
                        Usage: "(optional) the number of blocks that the " +
401
                                "transaction *should* confirm in",
402
                },
403
                coinSelectionStrategyFlag,
404
        },
405
        Action: actionDecorator(estimateFees),
406
}
407

408
func estimateFees(ctx *cli.Context) error {
×
409
        ctxc := getContext()
×
410
        var amountToAddr map[string]int64
×
411

×
412
        jsonMap := ctx.Args().First()
×
413
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
414
                return err
×
415
        }
×
416

417
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
418
        if err != nil {
×
419
                return err
×
420
        }
×
421

422
        client, cleanUp := getClient(ctx)
×
423
        defer cleanUp()
×
424

×
425
        resp, err := client.EstimateFee(ctxc, &lnrpc.EstimateFeeRequest{
×
426
                AddrToAmount:          amountToAddr,
×
427
                TargetConf:            int32(ctx.Int64("conf_target")),
×
428
                CoinSelectionStrategy: coinSelectionStrategy,
×
429
        })
×
430
        if err != nil {
×
431
                return err
×
432
        }
×
433

434
        printRespJSON(resp)
×
435
        return nil
×
436
}
437

438
var txLabelFlag = cli.StringFlag{
439
        Name:  "label",
440
        Usage: "(optional) a label for the transaction",
441
}
442

443
var sendCoinsCommand = cli.Command{
444
        Name:      "sendcoins",
445
        Category:  "On-chain",
446
        Usage:     "Send bitcoin on-chain to an address.",
447
        ArgsUsage: "addr amt",
448
        Description: `
449
        Send amt coins in satoshis to the base58 or bech32 encoded bitcoin address addr.
450

451
        Fees used when sending the transaction can be specified via the --conf_target, or
452
        --sat_per_vbyte optional flags.
453

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

523
func sendCoins(ctx *cli.Context) error {
×
524
        var (
×
525
                addr      string
×
526
                amt       int64
×
527
                err       error
×
528
                outpoints []*lnrpc.OutPoint
×
529
        )
×
530
        ctxc := getContext()
×
531
        args := ctx.Args()
×
532

×
533
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
534
                cli.ShowCommandHelp(ctx, "sendcoins")
×
535
                return nil
×
536
        }
×
537

538
        // Check that only the field sat_per_vbyte or the deprecated field
539
        // sat_per_byte is used.
540
        feeRateFlag, err := checkNotBothSet(
×
541
                ctx, "sat_per_vbyte", "sat_per_byte",
×
542
        )
×
543
        if err != nil {
×
544
                return err
×
545
        }
×
546

547
        // Only fee rate flag or conf_target should be set, not both.
548
        if _, err := checkNotBothSet(
×
549
                ctx, feeRateFlag, "conf_target",
×
550
        ); err != nil {
×
551
                return err
×
552
        }
×
553

554
        switch {
×
555
        case ctx.IsSet("addr"):
×
556
                addr = ctx.String("addr")
×
557
        case args.Present():
×
558
                addr = args.First()
×
559
                args = args.Tail()
×
560
        default:
×
561
                return fmt.Errorf("Address argument missing")
×
562
        }
563

564
        switch {
×
565
        case ctx.IsSet("amt"):
×
566
                amt = ctx.Int64("amt")
×
567
        case args.Present():
×
568
                amt, err = strconv.ParseInt(args.First(), 10, 64)
×
569
        case !ctx.Bool("sweepall"):
×
570
                return fmt.Errorf("Amount argument missing")
×
571
        }
572
        if err != nil {
×
573
                return fmt.Errorf("unable to decode amount: %w", err)
×
574
        }
×
575

576
        if amt != 0 && ctx.Bool("sweepall") {
×
577
                return fmt.Errorf("amount cannot be set if attempting to " +
×
578
                        "sweep all coins out of the wallet")
×
579
        }
×
580

581
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
582
        if err != nil {
×
583
                return err
×
584
        }
×
585

586
        client, cleanUp := getClient(ctx)
×
587
        defer cleanUp()
×
588
        minConfs := int32(ctx.Uint64("min_confs"))
×
589

×
590
        // In case that the user has specified the sweepall flag, we'll
×
591
        // calculate the amount to send based on the current wallet balance.
×
592
        displayAmt := amt
×
593
        if ctx.Bool("sweepall") && !ctx.IsSet("utxo") {
×
594
                balanceResponse, err := client.WalletBalance(
×
595
                        ctxc, &lnrpc.WalletBalanceRequest{
×
596
                                MinConfs: minConfs,
×
597
                        },
×
598
                )
×
599
                if err != nil {
×
600
                        return fmt.Errorf("unable to retrieve wallet balance:"+
×
601
                                " %w", err)
×
602
                }
×
603
                displayAmt = balanceResponse.GetConfirmedBalance()
×
604
        }
605

606
        if ctx.IsSet("utxo") {
×
607
                utxos := ctx.StringSlice("utxo")
×
608

×
609
                outpoints, err = UtxosToOutpoints(utxos)
×
610
                if err != nil {
×
611
                        return fmt.Errorf("unable to decode utxos: %w", err)
×
612
                }
×
613

614
                if ctx.Bool("sweepall") {
×
615
                        displayAmt = 0
×
616
                        // If we're sweeping all funds of the utxos, we'll need
×
617
                        // to set the display amount to the total amount of the
×
618
                        // utxos.
×
619
                        unspents, err := client.ListUnspent(
×
620
                                ctxc, &lnrpc.ListUnspentRequest{
×
621
                                        MinConfs: 0,
×
622
                                        MaxConfs: math.MaxInt32,
×
623
                                },
×
624
                        )
×
625
                        if err != nil {
×
626
                                return err
×
627
                        }
×
628

629
                        for _, utxo := range outpoints {
×
630
                                for _, unspent := range unspents.Utxos {
×
631
                                        unspentUtxo := unspent.Outpoint
×
632
                                        if isSameOutpoint(utxo, unspentUtxo) {
×
633
                                                displayAmt += unspent.AmountSat
×
634
                                                break
×
635
                                        }
636
                                }
637
                        }
638
                }
639
        }
640

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

×
648
                confirm := promptForConfirmation("Confirm payment (yes/no): ")
×
649
                if !confirm {
×
650
                        return nil
×
651
                }
×
652
        }
653

654
        req := &lnrpc.SendCoinsRequest{
×
655
                Addr:                  addr,
×
656
                Amount:                amt,
×
657
                TargetConf:            int32(ctx.Int64("conf_target")),
×
658
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
659
                SendAll:               ctx.Bool("sweepall"),
×
660
                Label:                 ctx.String(txLabelFlag.Name),
×
661
                MinConfs:              minConfs,
×
662
                SpendUnconfirmed:      minConfs == 0,
×
663
                CoinSelectionStrategy: coinSelectionStrategy,
×
664
                Outpoints:             outpoints,
×
665
        }
×
666
        txid, err := client.SendCoins(ctxc, req)
×
667
        if err != nil {
×
668
                return err
×
669
        }
×
670

671
        printRespJSON(txid)
×
672
        return nil
×
673
}
674

675
func isSameOutpoint(a, b *lnrpc.OutPoint) bool {
×
676
        return a.TxidStr == b.TxidStr && a.OutputIndex == b.OutputIndex
×
677
}
×
678

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

717
func listUnspent(ctx *cli.Context) error {
×
718
        var (
×
719
                minConfirms int64
×
720
                maxConfirms int64
×
721
                err         error
×
722
        )
×
723
        ctxc := getContext()
×
724
        args := ctx.Args()
×
725

×
726
        if ctx.IsSet("max_confs") && !ctx.IsSet("min_confs") {
×
727
                return fmt.Errorf("max_confs cannot be set without " +
×
728
                        "min_confs being set")
×
729
        }
×
730

731
        switch {
×
732
        case ctx.IsSet("min_confs"):
×
733
                minConfirms = ctx.Int64("min_confs")
×
734
        case args.Present():
×
735
                minConfirms, 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
        switch {
×
744
        case ctx.IsSet("max_confs"):
×
745
                maxConfirms = ctx.Int64("max_confs")
×
746
        case args.Present():
×
747
                maxConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
748
                if err != nil {
×
749
                        cli.ShowCommandHelp(ctx, "listunspent")
×
750
                        return nil
×
751
                }
×
752
                args = args.Tail()
×
753
        }
754

755
        unconfirmedOnly := ctx.Bool("unconfirmed_only")
×
756

×
757
        // Force minConfirms and maxConfirms to be zero if unconfirmedOnly is
×
758
        // true.
×
759
        if unconfirmedOnly && (minConfirms != 0 || maxConfirms != 0) {
×
760
                cli.ShowCommandHelp(ctx, "listunspent")
×
761
                return nil
×
762
        }
×
763

764
        // When unconfirmedOnly is inactive, we will override maxConfirms to be
765
        // a MaxInt32 to return all confirmed and unconfirmed utxos.
766
        if maxConfirms == 0 && !unconfirmedOnly {
×
767
                maxConfirms = math.MaxInt32
×
768
        }
×
769

770
        client, cleanUp := getClient(ctx)
×
771
        defer cleanUp()
×
772

×
773
        req := &lnrpc.ListUnspentRequest{
×
774
                MinConfs: int32(minConfirms),
×
775
                MaxConfs: int32(maxConfirms),
×
776
        }
×
777
        resp, err := client.ListUnspent(ctxc, req)
×
778
        if err != nil {
×
779
                return err
×
780
        }
×
781

782
        // Parse the response into the final json object that will be printed
783
        // to stdout. At the moment, this filters out the raw txid bytes from
784
        // each utxo's outpoint and only prints the txid string.
785
        var listUnspentResp = struct {
×
786
                Utxos []*Utxo `json:"utxos"`
×
787
        }{
×
788
                Utxos: make([]*Utxo, 0, len(resp.Utxos)),
×
789
        }
×
790
        for _, protoUtxo := range resp.Utxos {
×
791
                utxo := NewUtxoFromProto(protoUtxo)
×
792
                listUnspentResp.Utxos = append(listUnspentResp.Utxos, utxo)
×
793
        }
×
794

795
        printJSON(listUnspentResp)
×
796

×
797
        return nil
×
798
}
799

800
var sendManyCommand = cli.Command{
801
        Name:      "sendmany",
802
        Category:  "On-chain",
803
        Usage:     "Send bitcoin on-chain to multiple addresses.",
804
        ArgsUsage: "send-json-string [--conf_target=N] [--sat_per_vbyte=P]",
805
        Description: `
806
        Create and broadcast a transaction paying the specified amount(s) to the passed address(es).
807

808
        The send-json-string' param decodes addresses and the amount to send
809
        respectively in the following format:
810

811
            '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
812
        `,
813
        Flags: []cli.Flag{
814
                cli.Int64Flag{
815
                        Name: "conf_target",
816
                        Usage: "(optional) the number of blocks that the transaction *should* " +
817
                                "confirm in, will be used for fee estimation",
818
                },
819
                cli.Int64Flag{
820
                        Name:   "sat_per_byte",
821
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
822
                        Hidden: true,
823
                },
824
                cli.Int64Flag{
825
                        Name: "sat_per_vbyte",
826
                        Usage: "(optional) a manual fee expressed in " +
827
                                "sat/vbyte that should be used when crafting " +
828
                                "the transaction",
829
                },
830
                cli.Uint64Flag{
831
                        Name: "min_confs",
832
                        Usage: "(optional) the minimum number of confirmations " +
833
                                "each one of your outputs used for the transaction " +
834
                                "must satisfy",
835
                        Value: defaultUtxoMinConf,
836
                },
837
                coinSelectionStrategyFlag,
838
                txLabelFlag,
839
        },
840
        Action: actionDecorator(sendMany),
841
}
842

843
func sendMany(ctx *cli.Context) error {
×
844
        ctxc := getContext()
×
845
        var amountToAddr map[string]int64
×
846

×
847
        jsonMap := ctx.Args().First()
×
848
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
849
                return err
×
850
        }
×
851

852
        // Check that only the field sat_per_vbyte or the deprecated field
853
        // sat_per_byte is used.
854
        feeRateFlag, err := checkNotBothSet(
×
855
                ctx, "sat_per_vbyte", "sat_per_byte",
×
856
        )
×
857
        if err != nil {
×
858
                return err
×
859
        }
×
860

861
        // Only fee rate flag or conf_target should be set, not both.
862
        if _, err := checkNotBothSet(
×
863
                ctx, feeRateFlag, "conf_target",
×
864
        ); err != nil {
×
865
                return err
×
866
        }
×
867

868
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
869
        if err != nil {
×
870
                return err
×
871
        }
×
872

873
        client, cleanUp := getClient(ctx)
×
874
        defer cleanUp()
×
875

×
876
        minConfs := int32(ctx.Uint64("min_confs"))
×
877
        txid, err := client.SendMany(ctxc, &lnrpc.SendManyRequest{
×
878
                AddrToAmount:          amountToAddr,
×
879
                TargetConf:            int32(ctx.Int64("conf_target")),
×
880
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
881
                Label:                 ctx.String(txLabelFlag.Name),
×
882
                MinConfs:              minConfs,
×
883
                SpendUnconfirmed:      minConfs == 0,
×
884
                CoinSelectionStrategy: coinSelectionStrategy,
×
885
        })
×
886
        if err != nil {
×
887
                return err
×
888
        }
×
889

890
        printRespJSON(txid)
×
891
        return nil
×
892
}
893

894
var connectCommand = cli.Command{
895
        Name:      "connect",
896
        Category:  "Peers",
897
        Usage:     "Connect to a remote lightning peer.",
898
        ArgsUsage: "<pubkey>@host",
899
        Description: `
900
        Connect to a peer using its <pubkey> and host.
901

902
        A custom timeout on the connection is supported. For instance, to timeout
903
        the connection request in 30 seconds, use the following:
904

905
        lncli connect <pubkey>@host --timeout 30s
906
        `,
907
        Flags: []cli.Flag{
908
                cli.BoolFlag{
909
                        Name: "perm",
910
                        Usage: "If set, the daemon will attempt to persistently " +
911
                                "connect to the target peer.\n" +
912
                                "           If not, the call will be synchronous.",
913
                },
914
                cli.DurationFlag{
915
                        Name: "timeout",
916
                        Usage: "The connection timeout value for current request. " +
917
                                "Valid uints are {ms, s, m, h}.\n" +
918
                                "If not set, the global connection " +
919
                                "timeout value (default to 120s) is used.",
920
                },
921
        },
922
        Action: actionDecorator(connectPeer),
923
}
924

925
func connectPeer(ctx *cli.Context) error {
×
926
        ctxc := getContext()
×
927
        client, cleanUp := getClient(ctx)
×
928
        defer cleanUp()
×
929

×
930
        targetAddress := ctx.Args().First()
×
931
        splitAddr := strings.Split(targetAddress, "@")
×
932
        if len(splitAddr) != 2 {
×
933
                return fmt.Errorf("target address expected in format: " +
×
934
                        "pubkey@host:port")
×
935
        }
×
936

937
        addr := &lnrpc.LightningAddress{
×
938
                Pubkey: splitAddr[0],
×
939
                Host:   splitAddr[1],
×
940
        }
×
941
        req := &lnrpc.ConnectPeerRequest{
×
942
                Addr:    addr,
×
943
                Perm:    ctx.Bool("perm"),
×
944
                Timeout: uint64(ctx.Duration("timeout").Seconds()),
×
945
        }
×
946

×
947
        lnid, err := client.ConnectPeer(ctxc, req)
×
948
        if err != nil {
×
949
                return err
×
950
        }
×
951

952
        printRespJSON(lnid)
×
953
        return nil
×
954
}
955

956
var disconnectCommand = cli.Command{
957
        Name:     "disconnect",
958
        Category: "Peers",
959
        Usage: "Disconnect a remote lightning peer identified by " +
960
                "public key.",
961
        ArgsUsage: "<pubkey>",
962
        Flags: []cli.Flag{
963
                cli.StringFlag{
964
                        Name: "node_key",
965
                        Usage: "The hex-encoded compressed public key of the peer " +
966
                                "to disconnect from",
967
                },
968
        },
969
        Action: actionDecorator(disconnectPeer),
970
}
971

972
func disconnectPeer(ctx *cli.Context) error {
×
973
        ctxc := getContext()
×
974
        client, cleanUp := getClient(ctx)
×
975
        defer cleanUp()
×
976

×
977
        var pubKey string
×
978
        switch {
×
979
        case ctx.IsSet("node_key"):
×
980
                pubKey = ctx.String("node_key")
×
981
        case ctx.Args().Present():
×
982
                pubKey = ctx.Args().First()
×
983
        default:
×
984
                return fmt.Errorf("must specify target public key")
×
985
        }
986

987
        req := &lnrpc.DisconnectPeerRequest{
×
988
                PubKey: pubKey,
×
989
        }
×
990

×
991
        lnid, err := client.DisconnectPeer(ctxc, req)
×
992
        if err != nil {
×
993
                return err
×
994
        }
×
995

996
        printRespJSON(lnid)
×
997
        return nil
×
998
}
999

1000
// TODO(roasbeef): also allow short relative channel ID.
1001

1002
var closeChannelCommand = cli.Command{
1003
        Name:     "closechannel",
1004
        Category: "Channels",
1005
        Usage:    "Close an existing channel.",
1006
        Description: `
1007
        Close an existing channel. The channel can be closed either cooperatively,
1008
        or unilaterally (--force).
1009

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

1014
        In the case of a cooperative closure, one can manually set the fee to
1015
        be used for the closing transaction via either the --conf_target or
1016
        --sat_per_vbyte arguments. This will be the starting value used during
1017
        fee negotiation. This is optional. The parameter --max_fee_rate in
1018
        comparison is the end boundary of the fee negotiation, if not specified
1019
        it's always x3 of the starting value. Increasing this value increases
1020
        the chance of a successful negotiation.
1021
        Moreover if the channel has active HTLCs on it, the coop close will
1022
        wait until all HTLCs are resolved and will not allow any new HTLCs on
1023
        the channel. The channel will appear as disabled in the listchannels
1024
        output. The command will block in that case until the channel close tx
1025
        is broadcasted.
1026

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

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

1101
func closeChannel(ctx *cli.Context) error {
×
1102
        ctxc := getContext()
×
1103
        client, cleanUp := getClient(ctx)
×
1104
        defer cleanUp()
×
1105

×
1106
        // Show command help if no arguments and flags were provided.
×
1107
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1108
                cli.ShowCommandHelp(ctx, "closechannel")
×
1109
                return nil
×
1110
        }
×
1111

1112
        // Check that only the field sat_per_vbyte or the deprecated field
1113
        // sat_per_byte is used.
1114
        feeRateFlag, err := checkNotBothSet(
×
1115
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1116
        )
×
1117
        if err != nil {
×
1118
                return err
×
1119
        }
×
1120

1121
        channelPoint, err := parseChannelPoint(ctx)
×
1122
        if err != nil {
×
1123
                return err
×
1124
        }
×
1125

1126
        // TODO(roasbeef): implement time deadline within server
1127
        req := &lnrpc.CloseChannelRequest{
×
1128
                ChannelPoint:    channelPoint,
×
1129
                Force:           ctx.Bool("force"),
×
1130
                TargetConf:      int32(ctx.Int64("conf_target")),
×
1131
                SatPerVbyte:     ctx.Uint64(feeRateFlag),
×
1132
                DeliveryAddress: ctx.String("delivery_addr"),
×
1133
                MaxFeePerVbyte:  ctx.Uint64("max_fee_rate"),
×
1134
                // This makes sure that a coop close will also be executed if
×
1135
                // active HTLCs are present on the channel.
×
1136
                NoWait: true,
×
1137
        }
×
1138

×
1139
        // After parsing the request, we'll spin up a goroutine that will
×
1140
        // retrieve the closing transaction ID when attempting to close the
×
1141
        // channel. We do this to because `executeChannelClose` can block, so we
×
1142
        // would like to present the closing transaction ID to the user as soon
×
1143
        // as it is broadcasted.
×
1144
        var wg sync.WaitGroup
×
1145
        txidChan := make(chan string, 1)
×
1146

×
1147
        wg.Add(1)
×
1148
        go func() {
×
1149
                defer wg.Done()
×
1150

×
1151
                printJSON(struct {
×
1152
                        ClosingTxid string `json:"closing_txid"`
×
1153
                }{
×
1154
                        ClosingTxid: <-txidChan,
×
1155
                })
×
1156
        }()
×
1157

1158
        err = executeChannelClose(ctxc, client, req, txidChan, ctx.Bool("block"))
×
1159
        if err != nil {
×
1160
                return err
×
1161
        }
×
1162

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

×
1168
        return nil
×
1169
}
1170

1171
// executeChannelClose attempts to close the channel from a request. The closing
1172
// transaction ID is sent through `txidChan` as soon as it is broadcasted to the
1173
// network. The block boolean is used to determine if we should block until the
1174
// closing transaction receives a confirmation of 1 block. The logging outputs
1175
// are sent to stderr to avoid conflicts with the JSON output of the command
1176
// and potential work flows which depend on a proper JSON output.
1177
func executeChannelClose(ctxc context.Context, client lnrpc.LightningClient,
1178
        req *lnrpc.CloseChannelRequest, txidChan chan<- string, block bool) error {
×
1179

×
1180
        stream, err := client.CloseChannel(ctxc, req)
×
1181
        if err != nil {
×
1182
                return err
×
1183
        }
×
1184

1185
        for {
×
1186
                resp, err := stream.Recv()
×
1187
                if err == io.EOF {
×
1188
                        return nil
×
1189
                } else if err != nil {
×
1190
                        return err
×
1191
                }
×
1192

1193
                switch update := resp.Update.(type) {
×
1194
                case *lnrpc.CloseStatusUpdate_CloseInstant:
×
1195
                        fmt.Fprintln(os.Stderr, "Channel close successfully "+
×
1196
                                "initiated")
×
1197

×
1198
                        pendingHtlcs := update.CloseInstant.NumPendingHtlcs
×
1199
                        if pendingHtlcs > 0 {
×
1200
                                fmt.Fprintf(os.Stderr, "Cooperative channel "+
×
1201
                                        "close waiting for %d HTLCs to be "+
×
1202
                                        "resolved before the close process "+
×
1203
                                        "can kick off\n", pendingHtlcs)
×
1204
                        }
×
1205

1206
                case *lnrpc.CloseStatusUpdate_ClosePending:
×
1207
                        closingHash := update.ClosePending.Txid
×
1208
                        txid, err := chainhash.NewHash(closingHash)
×
1209
                        if err != nil {
×
1210
                                return err
×
1211
                        }
×
1212

1213
                        fmt.Fprintf(os.Stderr, "Channel close transaction "+
×
1214
                                "broadcasted: %v\n", txid)
×
1215

×
1216
                        txidChan <- txid.String()
×
1217

×
1218
                        if !block {
×
1219
                                return nil
×
1220
                        }
×
1221

1222
                        fmt.Fprintln(os.Stderr, "Waiting for channel close "+
×
1223
                                "confirmation ...")
×
1224

1225
                case *lnrpc.CloseStatusUpdate_ChanClose:
×
1226
                        fmt.Fprintln(os.Stderr, "Channel close successfully "+
×
1227
                                "confirmed")
×
1228

×
1229
                        return nil
×
1230
                }
1231
        }
1232
}
1233

1234
var closeAllChannelsCommand = cli.Command{
1235
        Name:     "closeallchannels",
1236
        Category: "Channels",
1237
        Usage:    "Close all existing channels.",
1238
        Description: `
1239
        Close all existing channels.
1240

1241
        Channels will be closed either cooperatively or unilaterally, depending
1242
        on whether the channel is active or not. If the channel is inactive, any
1243
        settled funds within it will be time locked for a few blocks before they
1244
        can be spent.
1245

1246
        One can request to close inactive channels only by using the
1247
        --inactive_only flag.
1248

1249
        By default, one is prompted for confirmation every time an inactive
1250
        channel is requested to be closed. To avoid this, one can set the
1251
        --force flag, which will only prompt for confirmation once for all
1252
        inactive channels and proceed to close them.
1253

1254
        In the case of cooperative closures, one can manually set the fee to
1255
        be used for the closing transactions via either the --conf_target or
1256
        --sat_per_vbyte arguments. This will be the starting value used during
1257
        fee negotiation. This is optional.`,
1258
        Flags: []cli.Flag{
1259
                cli.BoolFlag{
1260
                        Name:  "inactive_only",
1261
                        Usage: "close inactive channels only",
1262
                },
1263
                cli.BoolFlag{
1264
                        Name: "force",
1265
                        Usage: "ask for confirmation once before attempting " +
1266
                                "to close existing channels",
1267
                },
1268
                cli.Int64Flag{
1269
                        Name: "conf_target",
1270
                        Usage: "(optional) the number of blocks that the " +
1271
                                "closing transactions *should* confirm in, will be " +
1272
                                "used for fee estimation",
1273
                },
1274
                cli.Int64Flag{
1275
                        Name:   "sat_per_byte",
1276
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
1277
                        Hidden: true,
1278
                },
1279
                cli.Int64Flag{
1280
                        Name: "sat_per_vbyte",
1281
                        Usage: "(optional) a manual fee expressed in " +
1282
                                "sat/vbyte that should be used when crafting " +
1283
                                "the closing transactions",
1284
                },
1285
                cli.BoolFlag{
1286
                        Name: "s, skip_confirmation",
1287
                        Usage: "Skip the confirmation prompt and close all " +
1288
                                "channels immediately",
1289
                },
1290
        },
1291
        Action: actionDecorator(closeAllChannels),
1292
}
1293

1294
func closeAllChannels(ctx *cli.Context) error {
×
1295
        ctxc := getContext()
×
1296
        client, cleanUp := getClient(ctx)
×
1297
        defer cleanUp()
×
1298

×
1299
        // Check that only the field sat_per_vbyte or the deprecated field
×
1300
        // sat_per_byte is used.
×
1301
        feeRateFlag, err := checkNotBothSet(
×
1302
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1303
        )
×
1304
        if err != nil {
×
1305
                return err
×
1306
        }
×
1307

1308
        prompt := "Do you really want to close ALL channels? (yes/no): "
×
1309
        if !ctx.Bool("skip_confirmation") && !promptForConfirmation(prompt) {
×
1310
                return errors.New("action aborted by user")
×
1311
        }
×
1312

1313
        listReq := &lnrpc.ListChannelsRequest{}
×
1314
        openChannels, err := client.ListChannels(ctxc, listReq)
×
1315
        if err != nil {
×
1316
                return fmt.Errorf("unable to fetch open channels: %w", err)
×
1317
        }
×
1318

1319
        if len(openChannels.Channels) == 0 {
×
1320
                return errors.New("no open channels to close")
×
1321
        }
×
1322

1323
        var channelsToClose []*lnrpc.Channel
×
1324

×
1325
        switch {
×
1326
        case ctx.Bool("force") && ctx.Bool("inactive_only"):
×
1327
                msg := "Unilaterally close all inactive channels? The funds " +
×
1328
                        "within these channels will be locked for some blocks " +
×
1329
                        "(CSV delay) before they can be spent. (yes/no): "
×
1330

×
1331
                confirmed := promptForConfirmation(msg)
×
1332

×
1333
                // We can safely exit if the user did not confirm.
×
1334
                if !confirmed {
×
1335
                        return nil
×
1336
                }
×
1337

1338
                // Go through the list of open channels and only add inactive
1339
                // channels to the closing list.
1340
                for _, channel := range openChannels.Channels {
×
1341
                        if !channel.GetActive() {
×
1342
                                channelsToClose = append(
×
1343
                                        channelsToClose, channel,
×
1344
                                )
×
1345
                        }
×
1346
                }
1347
        case ctx.Bool("force"):
×
1348
                msg := "Close all active and inactive channels? Inactive " +
×
1349
                        "channels will be closed unilaterally, so funds " +
×
1350
                        "within them will be locked for a few blocks (CSV " +
×
1351
                        "delay) before they can be spent. (yes/no): "
×
1352

×
1353
                confirmed := promptForConfirmation(msg)
×
1354

×
1355
                // We can safely exit if the user did not confirm.
×
1356
                if !confirmed {
×
1357
                        return nil
×
1358
                }
×
1359

1360
                channelsToClose = openChannels.Channels
×
1361
        default:
×
1362
                // Go through the list of open channels and determine which
×
1363
                // should be added to the closing list.
×
1364
                for _, channel := range openChannels.Channels {
×
1365
                        // If the channel is inactive, we'll attempt to
×
1366
                        // unilaterally close the channel, so we should prompt
×
1367
                        // the user for confirmation beforehand.
×
1368
                        if !channel.GetActive() {
×
1369
                                msg := fmt.Sprintf("Unilaterally close channel "+
×
1370
                                        "with node %s and channel point %s? "+
×
1371
                                        "The closing transaction will need %d "+
×
1372
                                        "confirmations before the funds can be "+
×
1373
                                        "spent. (yes/no): ", channel.RemotePubkey,
×
1374
                                        channel.ChannelPoint, channel.LocalConstraints.CsvDelay)
×
1375

×
1376
                                confirmed := promptForConfirmation(msg)
×
1377

×
1378
                                if confirmed {
×
1379
                                        channelsToClose = append(
×
1380
                                                channelsToClose, channel,
×
1381
                                        )
×
1382
                                }
×
1383
                        } else if !ctx.Bool("inactive_only") {
×
1384
                                // Otherwise, we'll only add active channels if
×
1385
                                // we were not requested to close inactive
×
1386
                                // channels only.
×
1387
                                channelsToClose = append(
×
1388
                                        channelsToClose, channel,
×
1389
                                )
×
1390
                        }
×
1391
                }
1392
        }
1393

1394
        // result defines the result of closing a channel. The closing
1395
        // transaction ID is populated if a channel is successfully closed.
1396
        // Otherwise, the error that prevented closing the channel is populated.
1397
        type result struct {
×
1398
                RemotePubKey string `json:"remote_pub_key"`
×
1399
                ChannelPoint string `json:"channel_point"`
×
1400
                ClosingTxid  string `json:"closing_txid"`
×
1401
                FailErr      string `json:"error"`
×
1402
        }
×
1403

×
1404
        // Launch each channel closure in a goroutine in order to execute them
×
1405
        // in parallel. Once they're all executed, we will print the results as
×
1406
        // they come.
×
1407
        resultChan := make(chan result, len(channelsToClose))
×
1408
        for _, channel := range channelsToClose {
×
1409
                go func(channel *lnrpc.Channel) {
×
1410
                        res := result{}
×
1411
                        res.RemotePubKey = channel.RemotePubkey
×
1412
                        res.ChannelPoint = channel.ChannelPoint
×
1413
                        defer func() {
×
1414
                                resultChan <- res
×
1415
                        }()
×
1416

1417
                        // Parse the channel point in order to create the close
1418
                        // channel request.
1419
                        s := strings.Split(res.ChannelPoint, ":")
×
1420
                        if len(s) != 2 {
×
1421
                                res.FailErr = "expected channel point with " +
×
1422
                                        "format txid:index"
×
1423
                                return
×
1424
                        }
×
1425
                        index, err := strconv.ParseUint(s[1], 10, 32)
×
1426
                        if err != nil {
×
1427
                                res.FailErr = fmt.Sprintf("unable to parse "+
×
1428
                                        "channel point output index: %v", err)
×
1429
                                return
×
1430
                        }
×
1431

1432
                        req := &lnrpc.CloseChannelRequest{
×
1433
                                ChannelPoint: &lnrpc.ChannelPoint{
×
1434
                                        FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
×
1435
                                                FundingTxidStr: s[0],
×
1436
                                        },
×
1437
                                        OutputIndex: uint32(index),
×
1438
                                },
×
1439
                                Force:       !channel.GetActive(),
×
1440
                                TargetConf:  int32(ctx.Int64("conf_target")),
×
1441
                                SatPerVbyte: ctx.Uint64(feeRateFlag),
×
1442
                        }
×
1443

×
1444
                        txidChan := make(chan string, 1)
×
1445
                        err = executeChannelClose(ctxc, client, req, txidChan, false)
×
1446
                        if err != nil {
×
1447
                                res.FailErr = fmt.Sprintf("unable to close "+
×
1448
                                        "channel: %v", err)
×
1449
                                return
×
1450
                        }
×
1451

1452
                        res.ClosingTxid = <-txidChan
×
1453
                }(channel)
1454
        }
1455

1456
        for range channelsToClose {
×
1457
                res := <-resultChan
×
1458
                printJSON(res)
×
1459
        }
×
1460

1461
        return nil
×
1462
}
1463

1464
// promptForConfirmation continuously prompts the user for the message until
1465
// receiving a response of "yes" or "no" and returns their answer as a bool.
1466
func promptForConfirmation(msg string) bool {
×
1467
        reader := bufio.NewReader(os.Stdin)
×
1468

×
1469
        for {
×
1470
                fmt.Print(msg)
×
1471

×
1472
                answer, err := reader.ReadString('\n')
×
1473
                if err != nil {
×
1474
                        return false
×
1475
                }
×
1476

1477
                answer = strings.ToLower(strings.TrimSpace(answer))
×
1478

×
1479
                switch {
×
1480
                case answer == "yes":
×
1481
                        return true
×
1482
                case answer == "no":
×
1483
                        return false
×
1484
                default:
×
1485
                        continue
×
1486
                }
1487
        }
1488
}
1489

1490
var abandonChannelCommand = cli.Command{
1491
        Name:     "abandonchannel",
1492
        Category: "Channels",
1493
        Usage:    "Abandons an existing channel.",
1494
        Description: `
1495
        Removes all channel state from the database except for a close
1496
        summary. This method can be used to get rid of permanently unusable
1497
        channels due to bugs fixed in newer versions of lnd.
1498

1499
        Only available when lnd is built in debug mode. The flag
1500
        --i_know_what_i_am_doing can be set to override the debug/dev mode
1501
        requirement.
1502

1503
        To view which funding_txids/output_indexes can be used for this command,
1504
        see the channel_point values within the listchannels command output.
1505
        The format for a channel_point is 'funding_txid:output_index'.`,
1506
        ArgsUsage: "funding_txid [output_index]",
1507
        Flags: []cli.Flag{
1508
                cli.StringFlag{
1509
                        Name:  "funding_txid",
1510
                        Usage: "the txid of the channel's funding transaction",
1511
                },
1512
                cli.IntFlag{
1513
                        Name: "output_index",
1514
                        Usage: "the output index for the funding output of the funding " +
1515
                                "transaction",
1516
                },
1517
                cli.StringFlag{
1518
                        Name: "chan_point",
1519
                        Usage: "(optional) the channel point. If set, " +
1520
                                "funding_txid and output_index flags and " +
1521
                                "positional arguments will be ignored",
1522
                },
1523
                cli.BoolFlag{
1524
                        Name: "i_know_what_i_am_doing",
1525
                        Usage: "override the requirement for lnd needing to " +
1526
                                "be in dev/debug mode to use this command; " +
1527
                                "when setting this the user attests that " +
1528
                                "they know the danger of using this command " +
1529
                                "on channels and that doing so can lead to " +
1530
                                "loss of funds if the channel funding TX " +
1531
                                "ever confirms (or was confirmed)",
1532
                },
1533
        },
1534
        Action: actionDecorator(abandonChannel),
1535
}
1536

1537
func abandonChannel(ctx *cli.Context) error {
×
1538
        ctxc := getContext()
×
1539
        client, cleanUp := getClient(ctx)
×
1540
        defer cleanUp()
×
1541

×
1542
        // Show command help if no arguments and flags were provided.
×
1543
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1544
                cli.ShowCommandHelp(ctx, "abandonchannel")
×
1545
                return nil
×
1546
        }
×
1547

1548
        channelPoint, err := parseChannelPoint(ctx)
×
1549
        if err != nil {
×
1550
                return err
×
1551
        }
×
1552

1553
        req := &lnrpc.AbandonChannelRequest{
×
1554
                ChannelPoint:      channelPoint,
×
1555
                IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
×
1556
        }
×
1557

×
1558
        resp, err := client.AbandonChannel(ctxc, req)
×
1559
        if err != nil {
×
1560
                return err
×
1561
        }
×
1562

1563
        printRespJSON(resp)
×
1564
        return nil
×
1565
}
1566

1567
// parseChannelPoint parses a funding txid and output index from the command
1568
// line. Both named options and unnamed parameters are supported.
1569
func parseChannelPoint(ctx *cli.Context) (*lnrpc.ChannelPoint, error) {
×
1570
        channelPoint := &lnrpc.ChannelPoint{}
×
1571
        var err error
×
1572

×
1573
        args := ctx.Args()
×
1574

×
1575
        switch {
×
1576
        case ctx.IsSet("chan_point"):
×
1577
                channelPoint, err = parseChanPoint(ctx.String("chan_point"))
×
1578
                if err != nil {
×
1579
                        return nil, fmt.Errorf("unable to parse chan_point: "+
×
1580
                                "%v", err)
×
1581
                }
×
1582
                return channelPoint, nil
×
1583

1584
        case ctx.IsSet("funding_txid"):
×
1585
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1586
                        FundingTxidStr: ctx.String("funding_txid"),
×
1587
                }
×
1588
        case args.Present():
×
1589
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1590
                        FundingTxidStr: args.First(),
×
1591
                }
×
1592
                args = args.Tail()
×
1593
        default:
×
1594
                return nil, fmt.Errorf("funding txid argument missing")
×
1595
        }
1596

1597
        switch {
×
1598
        case ctx.IsSet("output_index"):
×
1599
                channelPoint.OutputIndex = uint32(ctx.Int("output_index"))
×
1600
        case args.Present():
×
1601
                index, err := strconv.ParseUint(args.First(), 10, 32)
×
1602
                if err != nil {
×
1603
                        return nil, fmt.Errorf("unable to decode output "+
×
1604
                                "index: %w", err)
×
1605
                }
×
1606
                channelPoint.OutputIndex = uint32(index)
×
1607
        default:
×
1608
                channelPoint.OutputIndex = 0
×
1609
        }
1610

1611
        return channelPoint, nil
×
1612
}
1613

1614
var listPeersCommand = cli.Command{
1615
        Name:     "listpeers",
1616
        Category: "Peers",
1617
        Usage:    "List all active, currently connected peers.",
1618
        Flags: []cli.Flag{
1619
                cli.BoolFlag{
1620
                        Name:  "list_errors",
1621
                        Usage: "list a full set of most recent errors for the peer",
1622
                },
1623
        },
1624
        Action: actionDecorator(listPeers),
1625
}
1626

1627
func listPeers(ctx *cli.Context) error {
×
1628
        ctxc := getContext()
×
1629
        client, cleanUp := getClient(ctx)
×
1630
        defer cleanUp()
×
1631

×
1632
        // By default, we display a single error on the cli. If the user
×
1633
        // specifically requests a full error set, then we will provide it.
×
1634
        req := &lnrpc.ListPeersRequest{
×
1635
                LatestError: !ctx.IsSet("list_errors"),
×
1636
        }
×
1637
        resp, err := client.ListPeers(ctxc, req)
×
1638
        if err != nil {
×
1639
                return err
×
1640
        }
×
1641

1642
        printRespJSON(resp)
×
1643
        return nil
×
1644
}
1645

1646
var walletBalanceCommand = cli.Command{
1647
        Name:     "walletbalance",
1648
        Category: "Wallet",
1649
        Usage:    "Compute and display the wallet's current balance.",
1650
        Flags: []cli.Flag{
1651
                cli.StringFlag{
1652
                        Name: "account",
1653
                        Usage: "(optional) the account for which the balance " +
1654
                                "is shown",
1655
                        Value: "",
1656
                },
1657
        },
1658
        Action: actionDecorator(walletBalance),
1659
}
1660

1661
func walletBalance(ctx *cli.Context) error {
×
1662
        ctxc := getContext()
×
1663
        client, cleanUp := getClient(ctx)
×
1664
        defer cleanUp()
×
1665

×
1666
        req := &lnrpc.WalletBalanceRequest{
×
1667
                Account: ctx.String("account"),
×
1668
        }
×
1669
        resp, err := client.WalletBalance(ctxc, req)
×
1670
        if err != nil {
×
1671
                return err
×
1672
        }
×
1673

1674
        printRespJSON(resp)
×
1675
        return nil
×
1676
}
1677

1678
var ChannelBalanceCommand = cli.Command{
1679
        Name:     "channelbalance",
1680
        Category: "Channels",
1681
        Usage: "Returns the sum of the total available channel balance across " +
1682
                "all open channels.",
1683
        Action: actionDecorator(ChannelBalance),
1684
}
1685

1686
func ChannelBalance(ctx *cli.Context) error {
×
1687
        ctxc := getContext()
×
1688
        client, cleanUp := getClient(ctx)
×
1689
        defer cleanUp()
×
1690

×
1691
        req := &lnrpc.ChannelBalanceRequest{}
×
1692
        resp, err := client.ChannelBalance(ctxc, req)
×
1693
        if err != nil {
×
1694
                return err
×
1695
        }
×
1696

1697
        printRespJSON(resp)
×
1698
        return nil
×
1699
}
1700

1701
var generateManPageCommand = cli.Command{
1702
        Name: "generatemanpage",
1703
        Usage: "Generates a man page for lncli and lnd as " +
1704
                "lncli.1 and lnd.1 respectively.",
1705
        Hidden: true,
1706
        Action: actionDecorator(generateManPage),
1707
}
1708

1709
func generateManPage(ctx *cli.Context) error {
×
1710
        // Generate the man pages for lncli as lncli.1.
×
1711
        manpages, err := ctx.App.ToMan()
×
1712
        if err != nil {
×
1713
                return err
×
1714
        }
×
1715
        err = os.WriteFile("lncli.1", []byte(manpages), 0644)
×
1716
        if err != nil {
×
1717
                return err
×
1718
        }
×
1719

1720
        // Generate the man pages for lnd as lnd.1.
1721
        config := lnd.DefaultConfig()
×
1722
        fileParser := flags.NewParser(&config, flags.Default)
×
1723
        fileParser.Name = "lnd"
×
1724

×
1725
        var buf bytes.Buffer
×
1726
        fileParser.WriteManPage(&buf)
×
1727

×
1728
        err = os.WriteFile("lnd.1", buf.Bytes(), 0644)
×
1729
        if err != nil {
×
1730
                return err
×
1731
        }
×
1732

1733
        return nil
×
1734
}
1735

1736
var getInfoCommand = cli.Command{
1737
        Name:   "getinfo",
1738
        Usage:  "Returns basic information related to the active daemon.",
1739
        Action: actionDecorator(getInfo),
1740
}
1741

1742
func getInfo(ctx *cli.Context) error {
×
1743
        ctxc := getContext()
×
1744
        client, cleanUp := getClient(ctx)
×
1745
        defer cleanUp()
×
1746

×
1747
        req := &lnrpc.GetInfoRequest{}
×
1748
        resp, err := client.GetInfo(ctxc, req)
×
1749
        if err != nil {
×
1750
                return err
×
1751
        }
×
1752

1753
        printRespJSON(resp)
×
1754
        return nil
×
1755
}
1756

1757
var getRecoveryInfoCommand = cli.Command{
1758
        Name:   "getrecoveryinfo",
1759
        Usage:  "Display information about an ongoing recovery attempt.",
1760
        Action: actionDecorator(getRecoveryInfo),
1761
}
1762

1763
func getRecoveryInfo(ctx *cli.Context) error {
×
1764
        ctxc := getContext()
×
1765
        client, cleanUp := getClient(ctx)
×
1766
        defer cleanUp()
×
1767

×
1768
        req := &lnrpc.GetRecoveryInfoRequest{}
×
1769
        resp, err := client.GetRecoveryInfo(ctxc, req)
×
1770
        if err != nil {
×
1771
                return err
×
1772
        }
×
1773

1774
        printRespJSON(resp)
×
1775
        return nil
×
1776
}
1777

1778
var pendingChannelsCommand = cli.Command{
1779
        Name:     "pendingchannels",
1780
        Category: "Channels",
1781
        Usage:    "Display information pertaining to pending channels.",
1782
        Flags: []cli.Flag{
1783
                cli.BoolFlag{
1784
                        Name: "include_raw_tx",
1785
                        Usage: "include the raw transaction hex for " +
1786
                                "waiting_close_channels.",
1787
                },
1788
        },
1789
        Action: actionDecorator(pendingChannels),
1790
}
1791

1792
func pendingChannels(ctx *cli.Context) error {
×
1793
        ctxc := getContext()
×
1794
        client, cleanUp := getClient(ctx)
×
1795
        defer cleanUp()
×
1796

×
1797
        includeRawTx := ctx.Bool("include_raw_tx")
×
1798
        req := &lnrpc.PendingChannelsRequest{
×
1799
                IncludeRawTx: includeRawTx,
×
1800
        }
×
1801
        resp, err := client.PendingChannels(ctxc, req)
×
1802
        if err != nil {
×
1803
                return err
×
1804
        }
×
1805

1806
        printRespJSON(resp)
×
1807

×
1808
        return nil
×
1809
}
1810

1811
var ListChannelsCommand = cli.Command{
1812
        Name:     "listchannels",
1813
        Category: "Channels",
1814
        Usage:    "List all open channels.",
1815
        Flags: []cli.Flag{
1816
                cli.BoolFlag{
1817
                        Name:  "active_only",
1818
                        Usage: "only list channels which are currently active",
1819
                },
1820
                cli.BoolFlag{
1821
                        Name:  "inactive_only",
1822
                        Usage: "only list channels which are currently inactive",
1823
                },
1824
                cli.BoolFlag{
1825
                        Name:  "public_only",
1826
                        Usage: "only list channels which are currently public",
1827
                },
1828
                cli.BoolFlag{
1829
                        Name:  "private_only",
1830
                        Usage: "only list channels which are currently private",
1831
                },
1832
                cli.StringFlag{
1833
                        Name: "peer",
1834
                        Usage: "(optional) only display channels with a " +
1835
                                "particular peer, accepts 66-byte, " +
1836
                                "hex-encoded pubkeys",
1837
                },
1838
                cli.BoolFlag{
1839
                        Name: "skip_peer_alias_lookup",
1840
                        Usage: "skip the peer alias lookup per channel in " +
1841
                                "order to improve performance",
1842
                },
1843
        },
1844
        Action: actionDecorator(ListChannels),
1845
}
1846

1847
var listAliasesCommand = cli.Command{
1848
        Name:     "listaliases",
1849
        Category: "Channels",
1850
        Usage:    "List all aliases.",
1851
        Flags:    []cli.Flag{},
1852
        Action:   actionDecorator(listAliases),
1853
}
1854

1855
func listAliases(ctx *cli.Context) error {
×
1856
        ctxc := getContext()
×
1857
        client, cleanUp := getClient(ctx)
×
1858
        defer cleanUp()
×
1859

×
1860
        req := &lnrpc.ListAliasesRequest{}
×
1861

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

1867
        printRespJSON(resp)
×
1868

×
1869
        return nil
×
1870
}
1871

1872
func ListChannels(ctx *cli.Context) error {
×
1873
        ctxc := getContext()
×
1874
        client, cleanUp := getClient(ctx)
×
1875
        defer cleanUp()
×
1876

×
1877
        peer := ctx.String("peer")
×
1878

×
1879
        // If the user requested channels with a particular key, parse the
×
1880
        // provided pubkey.
×
1881
        var peerKey []byte
×
1882
        if len(peer) > 0 {
×
1883
                pk, err := route.NewVertexFromStr(peer)
×
1884
                if err != nil {
×
1885
                        return fmt.Errorf("invalid --peer pubkey: %w", err)
×
1886
                }
×
1887

1888
                peerKey = pk[:]
×
1889
        }
1890

1891
        // By default, we will look up the peers' alias information unless the
1892
        // skip_peer_alias_lookup flag indicates otherwise.
1893
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1894

×
1895
        req := &lnrpc.ListChannelsRequest{
×
1896
                ActiveOnly:      ctx.Bool("active_only"),
×
1897
                InactiveOnly:    ctx.Bool("inactive_only"),
×
1898
                PublicOnly:      ctx.Bool("public_only"),
×
1899
                PrivateOnly:     ctx.Bool("private_only"),
×
1900
                Peer:            peerKey,
×
1901
                PeerAliasLookup: lookupPeerAlias,
×
1902
        }
×
1903

×
1904
        resp, err := client.ListChannels(ctxc, req)
×
1905
        if err != nil {
×
1906
                return err
×
1907
        }
×
1908

1909
        printModifiedProtoJSON(resp)
×
1910

×
1911
        return nil
×
1912
}
1913

1914
var closedChannelsCommand = cli.Command{
1915
        Name:     "closedchannels",
1916
        Category: "Channels",
1917
        Usage:    "List all closed channels.",
1918
        Flags: []cli.Flag{
1919
                cli.BoolFlag{
1920
                        Name:  "cooperative",
1921
                        Usage: "list channels that were closed cooperatively",
1922
                },
1923
                cli.BoolFlag{
1924
                        Name: "local_force",
1925
                        Usage: "list channels that were force-closed " +
1926
                                "by the local node",
1927
                },
1928
                cli.BoolFlag{
1929
                        Name: "remote_force",
1930
                        Usage: "list channels that were force-closed " +
1931
                                "by the remote node",
1932
                },
1933
                cli.BoolFlag{
1934
                        Name: "breach",
1935
                        Usage: "list channels for which the remote node " +
1936
                                "attempted to broadcast a prior " +
1937
                                "revoked channel state",
1938
                },
1939
                cli.BoolFlag{
1940
                        Name:  "funding_canceled",
1941
                        Usage: "list channels that were never fully opened",
1942
                },
1943
                cli.BoolFlag{
1944
                        Name: "abandoned",
1945
                        Usage: "list channels that were abandoned by " +
1946
                                "the local node",
1947
                },
1948
        },
1949
        Action: actionDecorator(closedChannels),
1950
}
1951

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

×
1957
        req := &lnrpc.ClosedChannelsRequest{
×
1958
                Cooperative:     ctx.Bool("cooperative"),
×
1959
                LocalForce:      ctx.Bool("local_force"),
×
1960
                RemoteForce:     ctx.Bool("remote_force"),
×
1961
                Breach:          ctx.Bool("breach"),
×
1962
                FundingCanceled: ctx.Bool("funding_canceled"),
×
1963
                Abandoned:       ctx.Bool("abandoned"),
×
1964
        }
×
1965

×
1966
        resp, err := client.ClosedChannels(ctxc, req)
×
1967
        if err != nil {
×
1968
                return err
×
1969
        }
×
1970

1971
        printModifiedProtoJSON(resp)
×
1972

×
1973
        return nil
×
1974
}
1975

1976
var describeGraphCommand = cli.Command{
1977
        Name:     "describegraph",
1978
        Category: "Graph",
1979
        Description: "Prints a human readable version of the known channel " +
1980
                "graph from the PoV of the node",
1981
        Usage: "Describe the network graph.",
1982
        Flags: []cli.Flag{
1983
                cli.BoolFlag{
1984
                        Name: "include_unannounced",
1985
                        Usage: "If set, unannounced channels will be included in the " +
1986
                                "graph. Unannounced channels are both private channels, and " +
1987
                                "public channels that are not yet announced to the network.",
1988
                },
1989
        },
1990
        Action: actionDecorator(describeGraph),
1991
}
1992

1993
func describeGraph(ctx *cli.Context) error {
×
1994
        ctxc := getContext()
×
1995
        client, cleanUp := getClient(ctx)
×
1996
        defer cleanUp()
×
1997

×
1998
        req := &lnrpc.ChannelGraphRequest{
×
1999
                IncludeUnannounced: ctx.Bool("include_unannounced"),
×
2000
        }
×
2001

×
2002
        graph, err := client.DescribeGraph(ctxc, req)
×
2003
        if err != nil {
×
2004
                return err
×
2005
        }
×
2006

2007
        printRespJSON(graph)
×
2008
        return nil
×
2009
}
2010

2011
var getNodeMetricsCommand = cli.Command{
2012
        Name:        "getnodemetrics",
2013
        Category:    "Graph",
2014
        Description: "Prints out node metrics calculated from the current graph",
2015
        Usage:       "Get node metrics.",
2016
        Action:      actionDecorator(getNodeMetrics),
2017
}
2018

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

×
2024
        req := &lnrpc.NodeMetricsRequest{
×
2025
                Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
×
2026
        }
×
2027

×
2028
        nodeMetrics, err := client.GetNodeMetrics(ctxc, req)
×
2029
        if err != nil {
×
2030
                return err
×
2031
        }
×
2032

2033
        printRespJSON(nodeMetrics)
×
2034
        return nil
×
2035
}
2036

2037
var getChanInfoCommand = cli.Command{
2038
        Name:     "getchaninfo",
2039
        Category: "Graph",
2040
        Usage:    "Get the state of a channel.",
2041
        Description: "Prints out the latest authenticated state for a " +
2042
                "particular channel",
2043
        ArgsUsage: "chan_id",
2044
        Flags: []cli.Flag{
2045
                cli.Uint64Flag{
2046
                        Name: "chan_id",
2047
                        Usage: "The 8-byte compact channel ID to query for. " +
2048
                                "If this is set the chan_point param is " +
2049
                                "ignored.",
2050
                },
2051
                cli.StringFlag{
2052
                        Name: "chan_point",
2053
                        Usage: "The channel point in format txid:index. If " +
2054
                                "the chan_id param is set this param is " +
2055
                                "ignored.",
2056
                },
2057
        },
2058
        Action: actionDecorator(getChanInfo),
2059
}
2060

2061
func getChanInfo(ctx *cli.Context) error {
×
2062
        ctxc := getContext()
×
2063
        client, cleanUp := getClient(ctx)
×
2064
        defer cleanUp()
×
2065

×
2066
        var (
×
2067
                chanID    uint64
×
2068
                chanPoint string
×
2069
                err       error
×
2070
        )
×
2071

×
2072
        switch {
×
2073
        case ctx.IsSet("chan_id"):
×
2074
                chanID = ctx.Uint64("chan_id")
×
2075

2076
        case ctx.Args().Present():
×
2077
                chanID, err = strconv.ParseUint(ctx.Args().First(), 10, 64)
×
2078
                if err != nil {
×
2079
                        return fmt.Errorf("error parsing chan_id: %w", err)
×
2080
                }
×
2081

2082
        case ctx.IsSet("chan_point"):
×
2083
                chanPoint = ctx.String("chan_point")
×
2084

2085
        default:
×
2086
                return fmt.Errorf("chan_id or chan_point argument missing")
×
2087
        }
2088

2089
        req := &lnrpc.ChanInfoRequest{
×
2090
                ChanId:    chanID,
×
2091
                ChanPoint: chanPoint,
×
2092
        }
×
2093

×
2094
        chanInfo, err := client.GetChanInfo(ctxc, req)
×
2095
        if err != nil {
×
2096
                return err
×
2097
        }
×
2098

2099
        printRespJSON(chanInfo)
×
2100
        return nil
×
2101
}
2102

2103
var getNodeInfoCommand = cli.Command{
2104
        Name:     "getnodeinfo",
2105
        Category: "Graph",
2106
        Usage:    "Get information on a specific node.",
2107
        Description: "Prints out the latest authenticated node state for an " +
2108
                "advertised node",
2109
        Flags: []cli.Flag{
2110
                cli.StringFlag{
2111
                        Name: "pub_key",
2112
                        Usage: "the 33-byte hex-encoded compressed public of the target " +
2113
                                "node",
2114
                },
2115
                cli.BoolFlag{
2116
                        Name: "include_channels",
2117
                        Usage: "if true, will return all known channels " +
2118
                                "associated with the node",
2119
                },
2120
        },
2121
        Action: actionDecorator(getNodeInfo),
2122
}
2123

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

×
2129
        args := ctx.Args()
×
2130

×
2131
        var pubKey string
×
2132
        switch {
×
2133
        case ctx.IsSet("pub_key"):
×
2134
                pubKey = ctx.String("pub_key")
×
2135
        case args.Present():
×
2136
                pubKey = args.First()
×
2137
        default:
×
2138
                return fmt.Errorf("pub_key argument missing")
×
2139
        }
2140

2141
        req := &lnrpc.NodeInfoRequest{
×
2142
                PubKey:          pubKey,
×
2143
                IncludeChannels: ctx.Bool("include_channels"),
×
2144
        }
×
2145

×
2146
        nodeInfo, err := client.GetNodeInfo(ctxc, req)
×
2147
        if err != nil {
×
2148
                return err
×
2149
        }
×
2150

2151
        printRespJSON(nodeInfo)
×
2152
        return nil
×
2153
}
2154

2155
var getNetworkInfoCommand = cli.Command{
2156
        Name:     "getnetworkinfo",
2157
        Category: "Channels",
2158
        Usage: "Get statistical information about the current " +
2159
                "state of the network.",
2160
        Description: "Returns a set of statistics pertaining to the known " +
2161
                "channel graph",
2162
        Action: actionDecorator(getNetworkInfo),
2163
}
2164

2165
func getNetworkInfo(ctx *cli.Context) error {
×
2166
        ctxc := getContext()
×
2167
        client, cleanUp := getClient(ctx)
×
2168
        defer cleanUp()
×
2169

×
2170
        req := &lnrpc.NetworkInfoRequest{}
×
2171

×
2172
        netInfo, err := client.GetNetworkInfo(ctxc, req)
×
2173
        if err != nil {
×
2174
                return err
×
2175
        }
×
2176

2177
        printRespJSON(netInfo)
×
2178
        return nil
×
2179
}
2180

2181
var debugLevelCommand = cli.Command{
2182
        Name:  "debuglevel",
2183
        Usage: "Set the debug level.",
2184
        Description: `Logging level for all subsystems {trace, debug, info, warn, error, critical, off}
2185
        You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems
2186

2187
        Use show to list available subsystems`,
2188
        Flags: []cli.Flag{
2189
                cli.BoolFlag{
2190
                        Name:  "show",
2191
                        Usage: "if true, then the list of available sub-systems will be printed out",
2192
                },
2193
                cli.StringFlag{
2194
                        Name:  "level",
2195
                        Usage: "the level specification to target either a coarse logging level, or granular set of specific sub-systems with logging levels for each",
2196
                },
2197
        },
2198
        Action: actionDecorator(debugLevel),
2199
}
2200

2201
func debugLevel(ctx *cli.Context) error {
×
2202
        ctxc := getContext()
×
2203
        client, cleanUp := getClient(ctx)
×
2204
        defer cleanUp()
×
2205
        req := &lnrpc.DebugLevelRequest{
×
2206
                Show:      ctx.Bool("show"),
×
2207
                LevelSpec: ctx.String("level"),
×
2208
        }
×
2209

×
2210
        resp, err := client.DebugLevel(ctxc, req)
×
2211
        if err != nil {
×
2212
                return err
×
2213
        }
×
2214

2215
        printRespJSON(resp)
×
2216
        return nil
×
2217
}
2218

2219
var listChainTxnsCommand = cli.Command{
2220
        Name:     "listchaintxns",
2221
        Category: "On-chain",
2222
        Usage:    "List transactions from the wallet.",
2223
        Flags: []cli.Flag{
2224
                cli.Int64Flag{
2225
                        Name: "start_height",
2226
                        Usage: "the block height from which to list " +
2227
                                "transactions, inclusive",
2228
                },
2229
                cli.Int64Flag{
2230
                        Name: "end_height",
2231
                        Usage: "the block height until which to list " +
2232
                                "transactions, inclusive, to get " +
2233
                                "transactions until the chain tip, including " +
2234
                                "unconfirmed, set this value to -1",
2235
                },
2236
                cli.UintFlag{
2237
                        Name: "index_offset",
2238
                        Usage: "the index of a transaction that will be " +
2239
                                "used in a query to determine which " +
2240
                                "transaction should be returned in the " +
2241
                                "response",
2242
                },
2243
                cli.IntFlag{
2244
                        Name: "max_transactions",
2245
                        Usage: "(optional) the max number of transactions to " +
2246
                                "return; leave at default of 0 to return " +
2247
                                "all transactions",
2248
                        Value: 0,
2249
                },
2250
        },
2251
        Description: `
2252
        List all transactions an address of the wallet was involved in.
2253

2254
        This call will return a list of wallet related transactions that paid
2255
        to an address our wallet controls, or spent utxos that we held. The
2256
        start_height and end_height flags can be used to specify an inclusive
2257
        block range over which to query for transactions. If the end_height is
2258
        less than the start_height, transactions will be queried in reverse.
2259
        To get all transactions until the chain tip, including unconfirmed
2260
        transactions (identifiable with BlockHeight=0), set end_height to -1.
2261
        By default, this call will get all transactions our wallet was involved
2262
        in, including unconfirmed transactions.
2263
`,
2264
        Action: actionDecorator(listChainTxns),
2265
}
2266

2267
func listChainTxns(ctx *cli.Context) error {
×
2268
        ctxc := getContext()
×
2269
        client, cleanUp := getClient(ctx)
×
2270
        defer cleanUp()
×
2271

×
2272
        req := &lnrpc.GetTransactionsRequest{
×
2273
                IndexOffset:     uint32(ctx.Uint64("index_offset")),
×
2274
                MaxTransactions: uint32(ctx.Uint64("max_transactions")),
×
2275
        }
×
2276

×
2277
        if ctx.IsSet("start_height") {
×
2278
                req.StartHeight = int32(ctx.Int64("start_height"))
×
2279
        }
×
2280
        if ctx.IsSet("end_height") {
×
2281
                req.EndHeight = int32(ctx.Int64("end_height"))
×
2282
        }
×
2283

2284
        resp, err := client.GetTransactions(ctxc, req)
×
2285
        if err != nil {
×
2286
                return err
×
2287
        }
×
2288

2289
        printRespJSON(resp)
×
2290
        return nil
×
2291
}
2292

2293
var stopCommand = cli.Command{
2294
        Name:  "stop",
2295
        Usage: "Stop and shutdown the daemon.",
2296
        Description: `
2297
        Gracefully stop all daemon subsystems before stopping the daemon itself.
2298
        This is equivalent to stopping it using CTRL-C.`,
2299
        Action: actionDecorator(stopDaemon),
2300
}
2301

2302
func stopDaemon(ctx *cli.Context) error {
×
2303
        ctxc := getContext()
×
2304
        client, cleanUp := getClient(ctx)
×
2305
        defer cleanUp()
×
2306

×
2307
        resp, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2308
        if err != nil {
×
2309
                return err
×
2310
        }
×
2311

2312
        printRespJSON(resp)
×
2313

×
2314
        return nil
×
2315
}
2316

2317
var signMessageCommand = cli.Command{
2318
        Name:      "signmessage",
2319
        Category:  "Wallet",
2320
        Usage:     "Sign a message with the node's private key.",
2321
        ArgsUsage: "msg",
2322
        Description: `
2323
        Sign msg with the resident node's private key.
2324
        Returns the signature as a zbase32 string.
2325

2326
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2327
        Flags: []cli.Flag{
2328
                cli.StringFlag{
2329
                        Name:  "msg",
2330
                        Usage: "the message to sign",
2331
                },
2332
        },
2333
        Action: actionDecorator(signMessage),
2334
}
2335

2336
func signMessage(ctx *cli.Context) error {
×
2337
        ctxc := getContext()
×
2338
        client, cleanUp := getClient(ctx)
×
2339
        defer cleanUp()
×
2340

×
2341
        var msg []byte
×
2342

×
2343
        switch {
×
2344
        case ctx.IsSet("msg"):
×
2345
                msg = []byte(ctx.String("msg"))
×
2346
        case ctx.Args().Present():
×
2347
                msg = []byte(ctx.Args().First())
×
2348
        default:
×
2349
                return fmt.Errorf("msg argument missing")
×
2350
        }
2351

2352
        resp, err := client.SignMessage(ctxc, &lnrpc.SignMessageRequest{Msg: msg})
×
2353
        if err != nil {
×
2354
                return err
×
2355
        }
×
2356

2357
        printRespJSON(resp)
×
2358
        return nil
×
2359
}
2360

2361
var verifyMessageCommand = cli.Command{
2362
        Name:      "verifymessage",
2363
        Category:  "Wallet",
2364
        Usage:     "Verify a message signed with the signature.",
2365
        ArgsUsage: "msg signature",
2366
        Description: `
2367
        Verify that the message was signed with a properly-formed signature
2368
        The signature must be zbase32 encoded and signed with the private key of
2369
        an active node in the resident node's channel database.
2370

2371
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2372
        Flags: []cli.Flag{
2373
                cli.StringFlag{
2374
                        Name:  "msg",
2375
                        Usage: "the message to verify",
2376
                },
2377
                cli.StringFlag{
2378
                        Name:  "sig",
2379
                        Usage: "the zbase32 encoded signature of the message",
2380
                },
2381
        },
2382
        Action: actionDecorator(verifyMessage),
2383
}
2384

2385
func verifyMessage(ctx *cli.Context) error {
×
2386
        ctxc := getContext()
×
2387
        client, cleanUp := getClient(ctx)
×
2388
        defer cleanUp()
×
2389

×
2390
        var (
×
2391
                msg []byte
×
2392
                sig string
×
2393
        )
×
2394

×
2395
        args := ctx.Args()
×
2396

×
2397
        switch {
×
2398
        case ctx.IsSet("msg"):
×
2399
                msg = []byte(ctx.String("msg"))
×
2400
        case args.Present():
×
2401
                msg = []byte(ctx.Args().First())
×
2402
                args = args.Tail()
×
2403
        default:
×
2404
                return fmt.Errorf("msg argument missing")
×
2405
        }
2406

2407
        switch {
×
2408
        case ctx.IsSet("sig"):
×
2409
                sig = ctx.String("sig")
×
2410
        case args.Present():
×
2411
                sig = args.First()
×
2412
        default:
×
2413
                return fmt.Errorf("signature argument missing")
×
2414
        }
2415

2416
        req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
×
2417
        resp, err := client.VerifyMessage(ctxc, req)
×
2418
        if err != nil {
×
2419
                return err
×
2420
        }
×
2421

2422
        printRespJSON(resp)
×
2423
        return nil
×
2424
}
2425

2426
var feeReportCommand = cli.Command{
2427
        Name:     "feereport",
2428
        Category: "Channels",
2429
        Usage:    "Display the current fee policies of all active channels.",
2430
        Description: `
2431
        Returns the current fee policies of all active channels.
2432
        Fee policies can be updated using the updatechanpolicy command.`,
2433
        Action: actionDecorator(feeReport),
2434
}
2435

2436
func feeReport(ctx *cli.Context) error {
×
2437
        ctxc := getContext()
×
2438
        client, cleanUp := getClient(ctx)
×
2439
        defer cleanUp()
×
2440

×
2441
        req := &lnrpc.FeeReportRequest{}
×
2442
        resp, err := client.FeeReport(ctxc, req)
×
2443
        if err != nil {
×
2444
                return err
×
2445
        }
×
2446

2447
        printRespJSON(resp)
×
2448
        return nil
×
2449
}
2450

2451
var updateChannelPolicyCommand = cli.Command{
2452
        Name:     "updatechanpolicy",
2453
        Category: "Channels",
2454
        Usage: "Update the channel policy for all channels, or a single " +
2455
                "channel.",
2456
        ArgsUsage: "base_fee_msat fee_rate time_lock_delta " +
2457
                "[--max_htlc_msat=N] [channel_point]",
2458
        Description: `
2459
        Updates the channel policy for all channels, or just a particular
2460
        channel identified by its channel point. The update will be committed, 
2461
        and broadcast to the rest of the network within the next batch. Channel
2462
        points are encoded as: funding_txid:output_index
2463
        `,
2464
        Flags: []cli.Flag{
2465
                cli.Int64Flag{
2466
                        Name: "base_fee_msat",
2467
                        Usage: "the base fee in milli-satoshis that will be " +
2468
                                "charged for each forwarded HTLC, regardless " +
2469
                                "of payment size",
2470
                },
2471
                cli.StringFlag{
2472
                        Name: "fee_rate",
2473
                        Usage: "the fee rate that will be charged " +
2474
                                "proportionally based on the value of each " +
2475
                                "forwarded HTLC, the lowest possible rate is " +
2476
                                "0 with a granularity of 0.000001 " +
2477
                                "(millionths). Can not be set at the same " +
2478
                                "time as fee_rate_ppm",
2479
                },
2480
                cli.Uint64Flag{
2481
                        Name: "fee_rate_ppm",
2482
                        Usage: "the fee rate ppm (parts per million) that " +
2483
                                "will be charged proportionally based on the " +
2484
                                "value of each forwarded HTLC, the lowest " +
2485
                                "possible rate is 0 with a granularity of " +
2486
                                "0.000001 (millionths). Can not be set at " +
2487
                                "the same time as fee_rate",
2488
                },
2489
                cli.Int64Flag{
2490
                        Name: "inbound_base_fee_msat",
2491
                        Usage: "the base inbound fee in milli-satoshis that " +
2492
                                "will be charged for each forwarded HTLC, " +
2493
                                "regardless of payment size. Its value must " +
2494
                                "be zero or negative - it is a discount " +
2495
                                "for using a particular incoming channel. " +
2496
                                "Note that forwards will be rejected if the " +
2497
                                "discount exceeds the outbound fee " +
2498
                                "(forward at a loss), and lead to " +
2499
                                "penalization by the sender",
2500
                },
2501
                cli.Int64Flag{
2502
                        Name: "inbound_fee_rate_ppm",
2503
                        Usage: "the inbound fee rate that will be charged " +
2504
                                "proportionally based on the value of each " +
2505
                                "forwarded HTLC and the outbound fee. Fee " +
2506
                                "rate is expressed in parts per million and " +
2507
                                "must be zero or negative - it is a discount " +
2508
                                "for using a particular incoming channel. " +
2509
                                "Note that forwards will be rejected if the " +
2510
                                "discount exceeds the outbound fee " +
2511
                                "(forward at a loss), and lead to " +
2512
                                "penalization by the sender",
2513
                },
2514
                cli.Uint64Flag{
2515
                        Name: "time_lock_delta",
2516
                        Usage: "the CLTV delta that will be applied to all " +
2517
                                "forwarded HTLCs",
2518
                },
2519
                cli.Uint64Flag{
2520
                        Name: "min_htlc_msat",
2521
                        Usage: "if set, the min HTLC size that will be " +
2522
                                "applied to all forwarded HTLCs. If unset, " +
2523
                                "the min HTLC is left unchanged",
2524
                },
2525
                cli.Uint64Flag{
2526
                        Name: "max_htlc_msat",
2527
                        Usage: "if set, the max HTLC size that will be " +
2528
                                "applied to all forwarded HTLCs. If unset, " +
2529
                                "the max HTLC is left unchanged",
2530
                },
2531
                cli.StringFlag{
2532
                        Name: "chan_point",
2533
                        Usage: "the channel which this policy update should " +
2534
                                "be applied to. If nil, the policies for all " +
2535
                                "channels will be updated. Takes the form of " +
2536
                                "txid:output_index",
2537
                },
2538
                cli.BoolFlag{
2539
                        Name: "create_missing_edge",
2540
                        Usage: "Under unknown circumstances a channel can " +
2541
                                "exist with a missing edge in the graph " +
2542
                                "database. This can cause an 'edge not " +
2543
                                "found' error when calling `getchaninfo` " +
2544
                                "and/or cause the default channel policy to " +
2545
                                "be used during forwards. Setting this flag " +
2546
                                "will recreate the edge if not found, " +
2547
                                "allowing updating this channel policy and " +
2548
                                "fixing the missing edge problem for this " +
2549
                                "channel permanently. For fields not set in " +
2550
                                "this command, the default policy will be " +
2551
                                "created.",
2552
                },
2553
        },
2554
        Action: actionDecorator(updateChannelPolicy),
2555
}
2556

2557
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
7✔
2558
        split := strings.Split(s, ":")
7✔
2559
        if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
10✔
2560
                return nil, errBadChanPoint
3✔
2561
        }
3✔
2562

2563
        index, err := strconv.ParseInt(split[1], 10, 64)
4✔
2564
        if err != nil {
5✔
2565
                return nil, fmt.Errorf("unable to decode output index: %w", err)
1✔
2566
        }
1✔
2567

2568
        txid, err := chainhash.NewHashFromStr(split[0])
3✔
2569
        if err != nil {
4✔
2570
                return nil, fmt.Errorf("unable to parse hex string: %w", err)
1✔
2571
        }
1✔
2572

2573
        return &lnrpc.ChannelPoint{
2✔
2574
                FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
2✔
2575
                        FundingTxidBytes: txid[:],
2✔
2576
                },
2✔
2577
                OutputIndex: uint32(index),
2✔
2578
        }, nil
2✔
2579
}
2580

2581
// parseTimeLockDelta is expected to get a uint16 type of timeLockDelta. Its
2582
// maximum value is MaxTimeLockDelta.
2583
func parseTimeLockDelta(timeLockDeltaStr string) (uint16, error) {
5✔
2584
        timeLockDeltaUnCheck, err := strconv.ParseUint(timeLockDeltaStr, 10, 64)
5✔
2585
        if err != nil {
7✔
2586
                return 0, fmt.Errorf("failed to parse time_lock_delta: %s "+
2✔
2587
                        "to uint64, err: %v", timeLockDeltaStr, err)
2✔
2588
        }
2✔
2589

2590
        if timeLockDeltaUnCheck > routing.MaxCLTVDelta {
3✔
2591
                return 0, fmt.Errorf("time_lock_delta is too big, "+
×
2592
                        "max value is %d", routing.MaxCLTVDelta)
×
2593
        }
×
2594

2595
        return uint16(timeLockDeltaUnCheck), nil
3✔
2596
}
2597

2598
func updateChannelPolicy(ctx *cli.Context) error {
×
2599
        ctxc := getContext()
×
2600
        client, cleanUp := getClient(ctx)
×
2601
        defer cleanUp()
×
2602

×
2603
        var (
×
2604
                baseFee       int64
×
2605
                feeRate       float64
×
2606
                feeRatePpm    uint64
×
2607
                timeLockDelta uint16
×
2608
                err           error
×
2609
        )
×
2610
        args := ctx.Args()
×
2611

×
2612
        switch {
×
2613
        case ctx.IsSet("base_fee_msat"):
×
2614
                baseFee = ctx.Int64("base_fee_msat")
×
2615
        case args.Present():
×
2616
                baseFee, err = strconv.ParseInt(args.First(), 10, 64)
×
2617
                if err != nil {
×
2618
                        return fmt.Errorf("unable to decode base_fee_msat: %w",
×
2619
                                err)
×
2620
                }
×
2621
                args = args.Tail()
×
2622
        default:
×
2623
                return fmt.Errorf("base_fee_msat argument missing")
×
2624
        }
2625

2626
        switch {
×
2627
        case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
×
2628
                return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
×
2629
        case ctx.IsSet("fee_rate"):
×
2630
                feeRate = ctx.Float64("fee_rate")
×
2631
        case ctx.IsSet("fee_rate_ppm"):
×
2632
                feeRatePpm = ctx.Uint64("fee_rate_ppm")
×
2633
        case args.Present():
×
2634
                feeRate, err = strconv.ParseFloat(args.First(), 64)
×
2635
                if err != nil {
×
2636
                        return fmt.Errorf("unable to decode fee_rate: %w", err)
×
2637
                }
×
2638

2639
                args = args.Tail()
×
2640
        default:
×
2641
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
×
2642
        }
2643

2644
        switch {
×
2645
        case ctx.IsSet("time_lock_delta"):
×
2646
                timeLockDeltaStr := ctx.String("time_lock_delta")
×
2647
                timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
×
2648
                if err != nil {
×
2649
                        return err
×
2650
                }
×
2651
        case args.Present():
×
2652
                timeLockDelta, err = parseTimeLockDelta(args.First())
×
2653
                if err != nil {
×
2654
                        return err
×
2655
                }
×
2656

2657
                args = args.Tail()
×
2658
        default:
×
2659
                return fmt.Errorf("time_lock_delta argument missing")
×
2660
        }
2661

2662
        var (
×
2663
                chanPoint    *lnrpc.ChannelPoint
×
2664
                chanPointStr string
×
2665
        )
×
2666

×
2667
        switch {
×
2668
        case ctx.IsSet("chan_point"):
×
2669
                chanPointStr = ctx.String("chan_point")
×
2670
        case args.Present():
×
2671
                chanPointStr = args.First()
×
2672
        }
2673

2674
        if chanPointStr != "" {
×
2675
                chanPoint, err = parseChanPoint(chanPointStr)
×
2676
                if err != nil {
×
2677
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2678
                }
×
2679
        }
2680

2681
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2682
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2683
                inboundBaseFeeMsat > math.MaxInt32 {
×
2684

×
2685
                return errors.New("inbound_base_fee_msat out of range")
×
2686
        }
×
2687

2688
        inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
×
2689
        if inboundFeeRatePpm < math.MinInt32 ||
×
2690
                inboundFeeRatePpm > math.MaxInt32 {
×
2691

×
2692
                return errors.New("inbound_fee_rate_ppm out of range")
×
2693
        }
×
2694

2695
        // Inbound fees are optional. However, if an update is required,
2696
        // both the base fee and the fee rate must be provided.
2697
        var inboundFee *lnrpc.InboundFee
×
2698
        if ctx.IsSet("inbound_base_fee_msat") !=
×
2699
                ctx.IsSet("inbound_fee_rate_ppm") {
×
2700

×
2701
                return errors.New("both parameters must be provided: " +
×
2702
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
×
2703
        }
×
2704

2705
        if ctx.IsSet("inbound_fee_rate_ppm") {
×
2706
                inboundFee = &lnrpc.InboundFee{
×
2707
                        BaseFeeMsat: int32(inboundBaseFeeMsat),
×
2708
                        FeeRatePpm:  int32(inboundFeeRatePpm),
×
2709
                }
×
2710
        }
×
2711

2712
        createMissingEdge := ctx.Bool("create_missing_edge")
×
2713

×
2714
        req := &lnrpc.PolicyUpdateRequest{
×
2715
                BaseFeeMsat:       baseFee,
×
2716
                TimeLockDelta:     uint32(timeLockDelta),
×
2717
                MaxHtlcMsat:       ctx.Uint64("max_htlc_msat"),
×
2718
                InboundFee:        inboundFee,
×
2719
                CreateMissingEdge: createMissingEdge,
×
2720
        }
×
2721

×
2722
        if ctx.IsSet("min_htlc_msat") {
×
2723
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2724
                req.MinHtlcMsatSpecified = true
×
2725
        }
×
2726

2727
        if chanPoint != nil {
×
2728
                req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
×
2729
                        ChanPoint: chanPoint,
×
2730
                }
×
2731
        } else {
×
2732
                req.Scope = &lnrpc.PolicyUpdateRequest_Global{
×
2733
                        Global: true,
×
2734
                }
×
2735
        }
×
2736

2737
        if feeRate != 0 {
×
2738
                req.FeeRate = feeRate
×
2739
        } else if feeRatePpm != 0 {
×
2740
                req.FeeRatePpm = uint32(feeRatePpm)
×
2741
        }
×
2742

2743
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2744
        if err != nil {
×
2745
                return err
×
2746
        }
×
2747

2748
        // Parse the response into the final json object that will be printed
2749
        // to stdout. At the moment, this filters out the raw txid bytes from
2750
        // each failed update's outpoint and only prints the txid string.
2751
        var listFailedUpdateResp = struct {
×
2752
                FailedUpdates []*FailedUpdate `json:"failed_updates"`
×
2753
        }{
×
2754
                FailedUpdates: make([]*FailedUpdate, 0, len(resp.FailedUpdates)),
×
2755
        }
×
2756
        for _, protoUpdate := range resp.FailedUpdates {
×
2757
                failedUpdate := NewFailedUpdateFromProto(protoUpdate)
×
2758
                listFailedUpdateResp.FailedUpdates = append(
×
2759
                        listFailedUpdateResp.FailedUpdates, failedUpdate)
×
2760
        }
×
2761

2762
        printJSON(listFailedUpdateResp)
×
2763

×
2764
        return nil
×
2765
}
2766

2767
var fishCompletionCommand = cli.Command{
2768
        Name:   "fish-completion",
2769
        Hidden: true,
2770
        Action: func(c *cli.Context) error {
×
2771
                completion, err := c.App.ToFishCompletion()
×
2772
                if err != nil {
×
2773
                        return err
×
2774
                }
×
2775

2776
                // We don't want to suggest files, so we add this
2777
                // first line to the completions.
2778
                _, err = fmt.Printf("complete -c %q -f \n%s", c.App.Name, completion)
×
2779
                return err
×
2780
        },
2781
}
2782

2783
var exportChanBackupCommand = cli.Command{
2784
        Name:     "exportchanbackup",
2785
        Category: "Channels",
2786
        Usage: "Obtain a static channel back up for a selected channels, " +
2787
                "or all known channels.",
2788
        ArgsUsage: "[chan_point] [--all] [--output_file]",
2789
        Description: `
2790
        This command allows a user to export a Static Channel Backup (SCB) for
2791
        a selected channel. SCB's are encrypted backups of a channel's initial
2792
        state that are encrypted with a key derived from the seed of a user. In
2793
        the case of partial or complete data loss, the SCB will allow the user
2794
        to reclaim settled funds in the channel at its final state. The
2795
        exported channel backups can be restored at a later time using the
2796
        restorechanbackup command.
2797

2798
        This command will return one of two types of channel backups depending
2799
        on the set of passed arguments:
2800

2801
           * If a target channel point is specified, then a single channel
2802
             backup containing only the information for that channel will be
2803
             returned.
2804

2805
           * If the --all flag is passed, then a multi-channel backup will be
2806
             returned. A multi backup is a single encrypted blob (displayed in
2807
             hex encoding) that contains several channels in a single cipher
2808
             text.
2809

2810
        Both of the backup types can be restored using the restorechanbackup
2811
        command.
2812
        `,
2813
        Flags: []cli.Flag{
2814
                cli.StringFlag{
2815
                        Name:  "chan_point",
2816
                        Usage: "the target channel to obtain an SCB for",
2817
                },
2818
                cli.BoolFlag{
2819
                        Name: "all",
2820
                        Usage: "if specified, then a multi backup of all " +
2821
                                "active channels will be returned",
2822
                },
2823
                cli.StringFlag{
2824
                        Name: "output_file",
2825
                        Usage: `
2826
                        if specified, then rather than printing a JSON output
2827
                        of the static channel backup, a serialized version of
2828
                        the backup (either Single or Multi) will be written to
2829
                        the target file, this is the same format used by lnd in
2830
                        its channel.backup file `,
2831
                },
2832
        },
2833
        Action: actionDecorator(exportChanBackup),
2834
}
2835

2836
func exportChanBackup(ctx *cli.Context) error {
×
2837
        ctxc := getContext()
×
2838
        client, cleanUp := getClient(ctx)
×
2839
        defer cleanUp()
×
2840

×
2841
        // Show command help if no arguments provided
×
2842
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2843
                cli.ShowCommandHelp(ctx, "exportchanbackup")
×
2844
                return nil
×
2845
        }
×
2846

2847
        var (
×
2848
                err            error
×
2849
                chanPointStr   string
×
2850
                outputFileName string
×
2851
        )
×
2852
        args := ctx.Args()
×
2853

×
2854
        switch {
×
2855
        case ctx.IsSet("chan_point"):
×
2856
                chanPointStr = ctx.String("chan_point")
×
2857

2858
        case args.Present():
×
2859
                chanPointStr = args.First()
×
2860

2861
        case !ctx.IsSet("all"):
×
2862
                return fmt.Errorf("must specify chan_point if --all isn't set")
×
2863
        }
2864

2865
        if ctx.IsSet("output_file") {
×
2866
                outputFileName = ctx.String("output_file")
×
2867
        }
×
2868

2869
        if chanPointStr != "" {
×
2870
                chanPointRPC, err := parseChanPoint(chanPointStr)
×
2871
                if err != nil {
×
2872
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2873
                }
×
2874

2875
                chanBackup, err := client.ExportChannelBackup(
×
2876
                        ctxc, &lnrpc.ExportChannelBackupRequest{
×
2877
                                ChanPoint: chanPointRPC,
×
2878
                        },
×
2879
                )
×
2880
                if err != nil {
×
2881
                        return err
×
2882
                }
×
2883

2884
                txid, err := chainhash.NewHash(
×
2885
                        chanPointRPC.GetFundingTxidBytes(),
×
2886
                )
×
2887
                if err != nil {
×
2888
                        return err
×
2889
                }
×
2890

2891
                chanPoint := wire.OutPoint{
×
2892
                        Hash:  *txid,
×
2893
                        Index: chanPointRPC.OutputIndex,
×
2894
                }
×
2895

×
2896
                if outputFileName != "" {
×
2897
                        return os.WriteFile(
×
2898
                                outputFileName,
×
2899
                                chanBackup.ChanBackup,
×
2900
                                0666,
×
2901
                        )
×
2902
                }
×
2903

2904
                printJSON(struct {
×
2905
                        ChanPoint  string `json:"chan_point"`
×
2906
                        ChanBackup string `json:"chan_backup"`
×
2907
                }{
×
2908
                        ChanPoint:  chanPoint.String(),
×
2909
                        ChanBackup: hex.EncodeToString(chanBackup.ChanBackup),
×
2910
                })
×
2911
                return nil
×
2912
        }
2913

2914
        if !ctx.IsSet("all") {
×
2915
                return fmt.Errorf("if a channel isn't specified, -all must be")
×
2916
        }
×
2917

2918
        chanBackup, err := client.ExportAllChannelBackups(
×
2919
                ctxc, &lnrpc.ChanBackupExportRequest{},
×
2920
        )
×
2921
        if err != nil {
×
2922
                return err
×
2923
        }
×
2924

2925
        if outputFileName != "" {
×
2926
                return os.WriteFile(
×
2927
                        outputFileName,
×
2928
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2929
                        0666,
×
2930
                )
×
2931
        }
×
2932

2933
        // TODO(roasbeef): support for export | restore ?
2934

2935
        var chanPoints []string
×
2936
        for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
×
2937
                txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
×
2938
                if err != nil {
×
2939
                        return err
×
2940
                }
×
2941

2942
                chanPoints = append(chanPoints, wire.OutPoint{
×
2943
                        Hash:  *txid,
×
2944
                        Index: chanPoint.OutputIndex,
×
2945
                }.String())
×
2946
        }
2947

2948
        printRespJSON(chanBackup)
×
2949

×
2950
        return nil
×
2951
}
2952

2953
var verifyChanBackupCommand = cli.Command{
2954
        Name:      "verifychanbackup",
2955
        Category:  "Channels",
2956
        Usage:     "Verify an existing channel backup.",
2957
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file]",
2958
        Description: `
2959
    This command allows a user to verify an existing Single or Multi channel
2960
    backup for integrity. This is useful when a user has a backup, but is
2961
    unsure as to if it's valid or for the target node.
2962

2963
    The command will accept backups in one of four forms:
2964

2965
       * A single channel packed SCB, which can be obtained from
2966
         exportchanbackup. This should be passed in hex encoded format.
2967

2968
       * A packed multi-channel SCB, which couples several individual
2969
         static channel backups in single blob.
2970

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

2974
       * A file path which points to a packed multi-channel backup within a
2975
         file, using the same format that lnd does in its channel.backup
2976
         file.
2977
    `,
2978
        Flags: []cli.Flag{
2979
                cli.StringFlag{
2980
                        Name: "single_backup",
2981
                        Usage: "a hex encoded single channel backup obtained " +
2982
                                "from exportchanbackup",
2983
                },
2984
                cli.StringFlag{
2985
                        Name: "multi_backup",
2986
                        Usage: "a hex encoded multi-channel backup obtained " +
2987
                                "from exportchanbackup",
2988
                },
2989

2990
                cli.StringFlag{
2991
                        Name:      "single_file",
2992
                        Usage:     "the path to a single-channel backup file",
2993
                        TakesFile: true,
2994
                },
2995

2996
                cli.StringFlag{
2997
                        Name:      "multi_file",
2998
                        Usage:     "the path to a multi-channel back up file",
2999
                        TakesFile: true,
3000
                },
3001
        },
3002
        Action: actionDecorator(verifyChanBackup),
3003
}
3004

3005
func verifyChanBackup(ctx *cli.Context) error {
×
3006
        ctxc := getContext()
×
3007
        client, cleanUp := getClient(ctx)
×
3008
        defer cleanUp()
×
3009

×
3010
        // Show command help if no arguments provided
×
3011
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3012
                cli.ShowCommandHelp(ctx, "verifychanbackup")
×
3013
                return nil
×
3014
        }
×
3015

3016
        backups, err := parseChanBackups(ctx)
×
3017
        if err != nil {
×
3018
                return err
×
3019
        }
×
3020

3021
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
3022

×
3023
        if backups.GetChanBackups() != nil {
×
3024
                verifyReq.SingleChanBackups = backups.GetChanBackups()
×
3025
        }
×
3026
        if backups.GetMultiChanBackup() != nil {
×
3027
                verifyReq.MultiChanBackup = &lnrpc.MultiChanBackup{
×
3028
                        MultiChanBackup: backups.GetMultiChanBackup(),
×
3029
                }
×
3030
        }
×
3031

3032
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
3033
        if err != nil {
×
3034
                return err
×
3035
        }
×
3036

3037
        printRespJSON(resp)
×
3038
        return nil
×
3039
}
3040

3041
var restoreChanBackupCommand = cli.Command{
3042
        Name:     "restorechanbackup",
3043
        Category: "Channels",
3044
        Usage: "Restore an existing single or multi-channel static channel " +
3045
                "backup.",
3046
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file=",
3047
        Description: `
3048
        Allows a user to restore a Static Channel Backup (SCB) that was
3049
        obtained either via the exportchanbackup command, or from lnd's
3050
        automatically managed channel.backup file. This command should be used
3051
        if a user is attempting to restore a channel due to data loss on a
3052
        running node restored with the same seed as the node that created the
3053
        channel. If successful, this command will allows the user to recover
3054
        the settled funds stored in the recovered channels.
3055

3056
        The command will accept backups in one of four forms:
3057

3058
           * A single channel packed SCB, which can be obtained from
3059
             exportchanbackup. This should be passed in hex encoded format.
3060

3061
           * A packed multi-channel SCB, which couples several individual
3062
             static channel backups in single blob.
3063

3064
           * A file path which points to a packed single-channel backup within
3065
             a file, using the same format that lnd does in its channel.backup
3066
             file.
3067

3068
           * A file path which points to a packed multi-channel backup within a
3069
             file, using the same format that lnd does in its channel.backup
3070
             file.
3071
        `,
3072
        Flags: []cli.Flag{
3073
                cli.StringFlag{
3074
                        Name: "single_backup",
3075
                        Usage: "a hex encoded single channel backup obtained " +
3076
                                "from exportchanbackup",
3077
                },
3078
                cli.StringFlag{
3079
                        Name: "multi_backup",
3080
                        Usage: "a hex encoded multi-channel backup obtained " +
3081
                                "from exportchanbackup",
3082
                },
3083

3084
                cli.StringFlag{
3085
                        Name:      "single_file",
3086
                        Usage:     "the path to a single-channel backup file",
3087
                        TakesFile: true,
3088
                },
3089

3090
                cli.StringFlag{
3091
                        Name:      "multi_file",
3092
                        Usage:     "the path to a multi-channel back up file",
3093
                        TakesFile: true,
3094
                },
3095
        },
3096
        Action: actionDecorator(restoreChanBackup),
3097
}
3098

3099
// errMissingChanBackup is an error returned when we attempt to parse a channel
3100
// backup from a CLI command, and it is missing.
3101
var errMissingChanBackup = errors.New("missing channel backup")
3102

3103
func parseChanBackups(ctx *cli.Context) (*lnrpc.RestoreChanBackupRequest, error) {
×
3104
        switch {
×
3105
        case ctx.IsSet("single_backup"):
×
3106
                packedBackup, err := hex.DecodeString(
×
3107
                        ctx.String("single_backup"),
×
3108
                )
×
3109
                if err != nil {
×
3110
                        return nil, fmt.Errorf("unable to decode single packed "+
×
3111
                                "backup: %v", err)
×
3112
                }
×
3113

3114
                return &lnrpc.RestoreChanBackupRequest{
×
3115
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3116
                                ChanBackups: &lnrpc.ChannelBackups{
×
3117
                                        ChanBackups: []*lnrpc.ChannelBackup{
×
3118
                                                {
×
3119
                                                        ChanBackup: packedBackup,
×
3120
                                                },
×
3121
                                        },
×
3122
                                },
×
3123
                        },
×
3124
                }, nil
×
3125

3126
        case ctx.IsSet("multi_backup"):
×
3127
                packedMulti, err := hex.DecodeString(
×
3128
                        ctx.String("multi_backup"),
×
3129
                )
×
3130
                if err != nil {
×
3131
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3132
                                "backup: %v", err)
×
3133
                }
×
3134

3135
                return &lnrpc.RestoreChanBackupRequest{
×
3136
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3137
                                MultiChanBackup: packedMulti,
×
3138
                        },
×
3139
                }, nil
×
3140

3141
        case ctx.IsSet("single_file"):
×
3142
                packedSingle, err := os.ReadFile(ctx.String("single_file"))
×
3143
                if err != nil {
×
3144
                        return nil, fmt.Errorf("unable to decode single "+
×
3145
                                "packed backup: %v", err)
×
3146
                }
×
3147

3148
                return &lnrpc.RestoreChanBackupRequest{
×
3149
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3150
                                ChanBackups: &lnrpc.ChannelBackups{
×
3151
                                        ChanBackups: []*lnrpc.ChannelBackup{{
×
3152
                                                ChanBackup: packedSingle,
×
3153
                                        }},
×
3154
                                },
×
3155
                        },
×
3156
                }, nil
×
3157

3158
        case ctx.IsSet("multi_file"):
×
3159
                packedMulti, err := os.ReadFile(ctx.String("multi_file"))
×
3160
                if err != nil {
×
3161
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3162
                                "backup: %v", err)
×
3163
                }
×
3164

3165
                return &lnrpc.RestoreChanBackupRequest{
×
3166
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3167
                                MultiChanBackup: packedMulti,
×
3168
                        },
×
3169
                }, nil
×
3170

3171
        default:
×
3172
                return nil, errMissingChanBackup
×
3173
        }
3174
}
3175

3176
func restoreChanBackup(ctx *cli.Context) error {
×
3177
        ctxc := getContext()
×
3178
        client, cleanUp := getClient(ctx)
×
3179
        defer cleanUp()
×
3180

×
3181
        // Show command help if no arguments provided
×
3182
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3183
                cli.ShowCommandHelp(ctx, "restorechanbackup")
×
3184
                return nil
×
3185
        }
×
3186

3187
        var req lnrpc.RestoreChanBackupRequest
×
3188

×
3189
        backups, err := parseChanBackups(ctx)
×
3190
        if err != nil {
×
3191
                return err
×
3192
        }
×
3193

3194
        req.Backup = backups.Backup
×
3195

×
3196
        resp, err := client.RestoreChannelBackups(ctxc, &req)
×
3197
        if err != nil {
×
3198
                return fmt.Errorf("unable to restore chan backups: %w", err)
×
3199
        }
×
3200

3201
        printRespJSON(resp)
×
3202

×
3203
        return nil
×
3204
}
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