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

lightningnetwork / lnd / 13902694788

17 Mar 2025 03:00PM UTC coverage: 68.637% (+10.3%) from 58.315%
13902694788

Pull #9605

github

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

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

15 existing lines in 3 files now uncovered.

130409 of 189999 relevant lines covered (68.64%)

23542.74 hits per line

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

7.27
/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 - please " +
×
NEW
289
                                "unlock using 'lncli unlock', or set " +
×
NEW
290
                                "password using 'lncli create' if this is " +
×
NEW
291
                                "the first time starting lnd")
×
NEW
292
                }
×
293

NEW
294
                return s.Err()
×
295
        }
296
}
297

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

742
        switch {
×
743
        case ctx.IsSet("max_confs"):
×
744
                maxConfirms = ctx.Int64("max_confs")
×
745
        case args.Present():
×
746
                maxConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
747
                if err != nil {
×
748
                        cli.ShowCommandHelp(ctx, "listunspent")
×
749
                        return nil
×
750
                }
×
751
                args = args.Tail()
×
752
        }
753

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

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

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

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

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

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

794
        printJSON(listUnspentResp)
×
795

×
796
        return nil
×
797
}
798

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
1167
        return nil
×
1168
}
1169

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1322
        var channelsToClose []*lnrpc.Channel
×
1323

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

×
1330
                confirmed := promptForConfirmation(msg)
×
1331

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

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

×
1352
                confirmed := promptForConfirmation(msg)
×
1353

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

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

×
1375
                                confirmed := promptForConfirmation(msg)
×
1376

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

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

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

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

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

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

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

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

1460
        return nil
×
1461
}
1462

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1610
        return channelPoint, nil
×
1611
}
1612

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1732
        return nil
×
1733
}
1734

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

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

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

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

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

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

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

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

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

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

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

1805
        printRespJSON(resp)
×
1806

×
1807
        return nil
×
1808
}
1809

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

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

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

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

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

1866
        printRespJSON(resp)
×
1867

×
1868
        return nil
×
1869
}
1870

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

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

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

1887
                peerKey = pk[:]
×
1888
        }
1889

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

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

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

1908
        printModifiedProtoJSON(resp)
×
1909

×
1910
        return nil
×
1911
}
1912

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

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

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

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

1970
        printModifiedProtoJSON(resp)
×
1971

×
1972
        return nil
×
1973
}
1974

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2311
        printRespJSON(resp)
×
2312

×
2313
        return nil
×
2314
}
2315

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

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

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

×
2340
        var msg []byte
×
2341

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2761
        printJSON(listFailedUpdateResp)
×
2762

×
2763
        return nil
×
2764
}
2765

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2947
        printRespJSON(chanBackup)
×
2948

×
2949
        return nil
×
2950
}
2951

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

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

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

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

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

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

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

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

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

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

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

3020
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
3021

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3186
        var req lnrpc.RestoreChanBackupRequest
×
3187

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

3193
        req.Backup = backups.Backup
×
3194

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

3200
        printRespJSON(resp)
×
3201

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