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

lightningnetwork / lnd / 13866475355

14 Mar 2025 10:36PM UTC coverage: 68.606% (+10.3%) from 58.315%
13866475355

Pull #9558

github

web-flow
Merge 1ea70677f into 053d63e11
Pull Request #9558: Fix input sanitation for listchaintxns lncli cmd

18 of 25 new or added lines in 1 file covered. (72.0%)

22 existing lines in 4 files now uncovered.

130363 of 190018 relevant lines covered (68.61%)

23507.63 hits per line

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

8.28
/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✔
254
                if err := f(c); err != nil {
×
255
                        s, ok := status.FromError(err)
×
256

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

×
270
                                return fmt.Errorf("Wallet is already unlocked")
×
271
                        }
×
272

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

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

317
func newAddress(ctx *cli.Context) error {
×
318
        ctxc := getContext()
×
319

×
320
        // Display the command's help message if we do not have the expected
×
321
        // number of arguments/flags.
×
322
        if ctx.NArg() != 1 || ctx.NumFlags() > 1 {
×
323
                return cli.ShowCommandHelp(ctx, "newaddress")
×
324
        }
×
325

326
        // Map the string encoded address type, to the concrete typed address
327
        // type enum. An unrecognized address type will result in an error.
328
        stringAddrType := ctx.Args().First()
×
329
        unused := ctx.Bool("unused")
×
330

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

353
        client, cleanUp := getClient(ctx)
×
354
        defer cleanUp()
×
355

×
356
        addr, err := client.NewAddress(ctxc, &lnrpc.NewAddressRequest{
×
357
                Type:    addrType,
×
358
                Account: ctx.String("account"),
×
359
        })
×
360
        if err != nil {
×
361
                return err
×
362
        }
×
363

364
        printRespJSON(addr)
×
365
        return nil
×
366
}
367

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

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

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

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

401
func estimateFees(ctx *cli.Context) error {
×
402
        ctxc := getContext()
×
403
        var amountToAddr map[string]int64
×
404

×
405
        jsonMap := ctx.Args().First()
×
406
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
407
                return err
×
408
        }
×
409

410
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
411
        if err != nil {
×
412
                return err
×
413
        }
×
414

415
        client, cleanUp := getClient(ctx)
×
416
        defer cleanUp()
×
417

×
418
        resp, err := client.EstimateFee(ctxc, &lnrpc.EstimateFeeRequest{
×
419
                AddrToAmount:          amountToAddr,
×
420
                TargetConf:            int32(ctx.Int64("conf_target")),
×
421
                CoinSelectionStrategy: coinSelectionStrategy,
×
422
        })
×
423
        if err != nil {
×
424
                return err
×
425
        }
×
426

427
        printRespJSON(resp)
×
428
        return nil
×
429
}
430

431
var txLabelFlag = cli.StringFlag{
432
        Name:  "label",
433
        Usage: "(optional) a label for the transaction",
434
}
435

436
var sendCoinsCommand = cli.Command{
437
        Name:      "sendcoins",
438
        Category:  "On-chain",
439
        Usage:     "Send bitcoin on-chain to an address.",
440
        ArgsUsage: "addr amt",
441
        Description: `
442
        Send amt coins in satoshis to the base58 or bech32 encoded bitcoin address addr.
443

444
        Fees used when sending the transaction can be specified via the --conf_target, or
445
        --sat_per_vbyte optional flags.
446

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

516
func sendCoins(ctx *cli.Context) error {
×
517
        var (
×
518
                addr      string
×
519
                amt       int64
×
520
                err       error
×
521
                outpoints []*lnrpc.OutPoint
×
522
        )
×
523
        ctxc := getContext()
×
524
        args := ctx.Args()
×
525

×
526
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
527
                cli.ShowCommandHelp(ctx, "sendcoins")
×
528
                return nil
×
529
        }
×
530

531
        // Check that only the field sat_per_vbyte or the deprecated field
532
        // sat_per_byte is used.
533
        feeRateFlag, err := checkNotBothSet(
×
534
                ctx, "sat_per_vbyte", "sat_per_byte",
×
535
        )
×
536
        if err != nil {
×
537
                return err
×
538
        }
×
539

540
        // Only fee rate flag or conf_target should be set, not both.
541
        if _, err := checkNotBothSet(
×
542
                ctx, feeRateFlag, "conf_target",
×
543
        ); err != nil {
×
544
                return err
×
545
        }
×
546

547
        switch {
×
548
        case ctx.IsSet("addr"):
×
549
                addr = ctx.String("addr")
×
550
        case args.Present():
×
551
                addr = args.First()
×
552
                args = args.Tail()
×
553
        default:
×
554
                return fmt.Errorf("Address argument missing")
×
555
        }
556

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

569
        if amt != 0 && ctx.Bool("sweepall") {
×
570
                return fmt.Errorf("amount cannot be set if attempting to " +
×
571
                        "sweep all coins out of the wallet")
×
572
        }
×
573

574
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
575
        if err != nil {
×
576
                return err
×
577
        }
×
578

579
        client, cleanUp := getClient(ctx)
×
580
        defer cleanUp()
×
581
        minConfs := int32(ctx.Uint64("min_confs"))
×
582

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

599
        if ctx.IsSet("utxo") {
×
600
                utxos := ctx.StringSlice("utxo")
×
601

×
602
                outpoints, err = UtxosToOutpoints(utxos)
×
603
                if err != nil {
×
604
                        return fmt.Errorf("unable to decode utxos: %w", err)
×
605
                }
×
606

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

622
                        for _, utxo := range outpoints {
×
623
                                for _, unspent := range unspents.Utxos {
×
624
                                        unspentUtxo := unspent.Outpoint
×
625
                                        if isSameOutpoint(utxo, unspentUtxo) {
×
626
                                                displayAmt += unspent.AmountSat
×
627
                                                break
×
628
                                        }
629
                                }
630
                        }
631
                }
632
        }
633

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

×
641
                confirm := promptForConfirmation("Confirm payment (yes/no): ")
×
642
                if !confirm {
×
643
                        return nil
×
644
                }
×
645
        }
646

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

664
        printRespJSON(txid)
×
665
        return nil
×
666
}
667

668
func isSameOutpoint(a, b *lnrpc.OutPoint) bool {
×
669
        return a.TxidStr == b.TxidStr && a.OutputIndex == b.OutputIndex
×
670
}
×
671

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

710
func listUnspent(ctx *cli.Context) error {
×
711
        var (
×
712
                minConfirms int64
×
713
                maxConfirms int64
×
714
                err         error
×
715
        )
×
716
        ctxc := getContext()
×
717
        args := ctx.Args()
×
718

×
719
        if ctx.IsSet("max_confs") && !ctx.IsSet("min_confs") {
×
720
                return fmt.Errorf("max_confs cannot be set without " +
×
721
                        "min_confs being set")
×
722
        }
×
723

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

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

748
        unconfirmedOnly := ctx.Bool("unconfirmed_only")
×
749

×
750
        // Force minConfirms and maxConfirms to be zero if unconfirmedOnly is
×
751
        // true.
×
752
        if unconfirmedOnly && (minConfirms != 0 || maxConfirms != 0) {
×
753
                cli.ShowCommandHelp(ctx, "listunspent")
×
754
                return nil
×
755
        }
×
756

757
        // When unconfirmedOnly is inactive, we will override maxConfirms to be
758
        // a MaxInt32 to return all confirmed and unconfirmed utxos.
759
        if maxConfirms == 0 && !unconfirmedOnly {
×
760
                maxConfirms = math.MaxInt32
×
761
        }
×
762

763
        client, cleanUp := getClient(ctx)
×
764
        defer cleanUp()
×
765

×
766
        req := &lnrpc.ListUnspentRequest{
×
767
                MinConfs: int32(minConfirms),
×
768
                MaxConfs: int32(maxConfirms),
×
769
        }
×
770
        resp, err := client.ListUnspent(ctxc, req)
×
771
        if err != nil {
×
772
                return err
×
773
        }
×
774

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

788
        printJSON(listUnspentResp)
×
789

×
790
        return nil
×
791
}
792

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

801
        The send-json-string' param decodes addresses and the amount to send
802
        respectively in the following format:
803

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

836
func sendMany(ctx *cli.Context) error {
×
837
        ctxc := getContext()
×
838
        var amountToAddr map[string]int64
×
839

×
840
        jsonMap := ctx.Args().First()
×
841
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
842
                return err
×
843
        }
×
844

845
        // Check that only the field sat_per_vbyte or the deprecated field
846
        // sat_per_byte is used.
847
        feeRateFlag, err := checkNotBothSet(
×
848
                ctx, "sat_per_vbyte", "sat_per_byte",
×
849
        )
×
850
        if err != nil {
×
851
                return err
×
852
        }
×
853

854
        // Only fee rate flag or conf_target should be set, not both.
855
        if _, err := checkNotBothSet(
×
856
                ctx, feeRateFlag, "conf_target",
×
857
        ); err != nil {
×
858
                return err
×
859
        }
×
860

861
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
862
        if err != nil {
×
863
                return err
×
864
        }
×
865

866
        client, cleanUp := getClient(ctx)
×
867
        defer cleanUp()
×
868

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

883
        printRespJSON(txid)
×
884
        return nil
×
885
}
886

887
var connectCommand = cli.Command{
888
        Name:      "connect",
889
        Category:  "Peers",
890
        Usage:     "Connect to a remote lightning peer.",
891
        ArgsUsage: "<pubkey>@host",
892
        Description: `
893
        Connect to a peer using its <pubkey> and host.
894

895
        A custom timeout on the connection is supported. For instance, to timeout
896
        the connection request in 30 seconds, use the following:
897

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

918
func connectPeer(ctx *cli.Context) error {
×
919
        ctxc := getContext()
×
920
        client, cleanUp := getClient(ctx)
×
921
        defer cleanUp()
×
922

×
923
        targetAddress := ctx.Args().First()
×
924
        splitAddr := strings.Split(targetAddress, "@")
×
925
        if len(splitAddr) != 2 {
×
926
                return fmt.Errorf("target address expected in format: " +
×
927
                        "pubkey@host:port")
×
928
        }
×
929

930
        addr := &lnrpc.LightningAddress{
×
931
                Pubkey: splitAddr[0],
×
932
                Host:   splitAddr[1],
×
933
        }
×
934
        req := &lnrpc.ConnectPeerRequest{
×
935
                Addr:    addr,
×
936
                Perm:    ctx.Bool("perm"),
×
937
                Timeout: uint64(ctx.Duration("timeout").Seconds()),
×
938
        }
×
939

×
940
        lnid, err := client.ConnectPeer(ctxc, req)
×
941
        if err != nil {
×
942
                return err
×
943
        }
×
944

945
        printRespJSON(lnid)
×
946
        return nil
×
947
}
948

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

965
func disconnectPeer(ctx *cli.Context) error {
×
966
        ctxc := getContext()
×
967
        client, cleanUp := getClient(ctx)
×
968
        defer cleanUp()
×
969

×
970
        var pubKey string
×
971
        switch {
×
972
        case ctx.IsSet("node_key"):
×
973
                pubKey = ctx.String("node_key")
×
974
        case ctx.Args().Present():
×
975
                pubKey = ctx.Args().First()
×
976
        default:
×
977
                return fmt.Errorf("must specify target public key")
×
978
        }
979

980
        req := &lnrpc.DisconnectPeerRequest{
×
981
                PubKey: pubKey,
×
982
        }
×
983

×
984
        lnid, err := client.DisconnectPeer(ctxc, req)
×
985
        if err != nil {
×
986
                return err
×
987
        }
×
988

989
        printRespJSON(lnid)
×
990
        return nil
×
991
}
992

993
// TODO(roasbeef): also allow short relative channel ID.
994

995
var closeChannelCommand = cli.Command{
996
        Name:     "closechannel",
997
        Category: "Channels",
998
        Usage:    "Close an existing channel.",
999
        Description: `
1000
        Close an existing channel. The channel can be closed either cooperatively,
1001
        or unilaterally (--force).
1002

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

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

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

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

1094
func closeChannel(ctx *cli.Context) error {
×
1095
        ctxc := getContext()
×
1096
        client, cleanUp := getClient(ctx)
×
1097
        defer cleanUp()
×
1098

×
1099
        // Show command help if no arguments and flags were provided.
×
1100
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1101
                cli.ShowCommandHelp(ctx, "closechannel")
×
1102
                return nil
×
1103
        }
×
1104

1105
        // Check that only the field sat_per_vbyte or the deprecated field
1106
        // sat_per_byte is used.
1107
        feeRateFlag, err := checkNotBothSet(
×
1108
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1109
        )
×
1110
        if err != nil {
×
1111
                return err
×
1112
        }
×
1113

1114
        channelPoint, err := parseChannelPoint(ctx)
×
1115
        if err != nil {
×
1116
                return err
×
1117
        }
×
1118

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

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

×
1140
        wg.Add(1)
×
1141
        go func() {
×
1142
                defer wg.Done()
×
1143

×
1144
                printJSON(struct {
×
1145
                        ClosingTxid string `json:"closing_txid"`
×
1146
                }{
×
1147
                        ClosingTxid: <-txidChan,
×
1148
                })
×
1149
        }()
×
1150

1151
        err = executeChannelClose(ctxc, client, req, txidChan, ctx.Bool("block"))
×
1152
        if err != nil {
×
1153
                return err
×
1154
        }
×
1155

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

×
1161
        return nil
×
1162
}
1163

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

×
1173
        stream, err := client.CloseChannel(ctxc, req)
×
1174
        if err != nil {
×
1175
                return err
×
1176
        }
×
1177

1178
        for {
×
1179
                resp, err := stream.Recv()
×
1180
                if err == io.EOF {
×
1181
                        return nil
×
1182
                } else if err != nil {
×
1183
                        return err
×
1184
                }
×
1185

1186
                switch update := resp.Update.(type) {
×
1187
                case *lnrpc.CloseStatusUpdate_CloseInstant:
×
1188
                        fmt.Fprintln(os.Stderr, "Channel close successfully "+
×
1189
                                "initiated")
×
1190

×
1191
                        pendingHtlcs := update.CloseInstant.NumPendingHtlcs
×
1192
                        if pendingHtlcs > 0 {
×
1193
                                fmt.Fprintf(os.Stderr, "Cooperative channel "+
×
1194
                                        "close waiting for %d HTLCs to be "+
×
1195
                                        "resolved before the close process "+
×
1196
                                        "can kick off\n", pendingHtlcs)
×
1197
                        }
×
1198

1199
                case *lnrpc.CloseStatusUpdate_ClosePending:
×
1200
                        closingHash := update.ClosePending.Txid
×
1201
                        txid, err := chainhash.NewHash(closingHash)
×
1202
                        if err != nil {
×
1203
                                return err
×
1204
                        }
×
1205

1206
                        fmt.Fprintf(os.Stderr, "Channel close transaction "+
×
1207
                                "broadcasted: %v\n", txid)
×
1208

×
1209
                        txidChan <- txid.String()
×
1210

×
1211
                        if !block {
×
1212
                                return nil
×
1213
                        }
×
1214

1215
                        fmt.Fprintln(os.Stderr, "Waiting for channel close "+
×
1216
                                "confirmation ...")
×
1217

1218
                case *lnrpc.CloseStatusUpdate_ChanClose:
×
1219
                        fmt.Fprintln(os.Stderr, "Channel close successfully "+
×
1220
                                "confirmed")
×
1221

×
1222
                        return nil
×
1223
                }
1224
        }
1225
}
1226

1227
var closeAllChannelsCommand = cli.Command{
1228
        Name:     "closeallchannels",
1229
        Category: "Channels",
1230
        Usage:    "Close all existing channels.",
1231
        Description: `
1232
        Close all existing channels.
1233

1234
        Channels will be closed either cooperatively or unilaterally, depending
1235
        on whether the channel is active or not. If the channel is inactive, any
1236
        settled funds within it will be time locked for a few blocks before they
1237
        can be spent.
1238

1239
        One can request to close inactive channels only by using the
1240
        --inactive_only flag.
1241

1242
        By default, one is prompted for confirmation every time an inactive
1243
        channel is requested to be closed. To avoid this, one can set the
1244
        --force flag, which will only prompt for confirmation once for all
1245
        inactive channels and proceed to close them.
1246

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

1287
func closeAllChannels(ctx *cli.Context) error {
×
1288
        ctxc := getContext()
×
1289
        client, cleanUp := getClient(ctx)
×
1290
        defer cleanUp()
×
1291

×
1292
        // Check that only the field sat_per_vbyte or the deprecated field
×
1293
        // sat_per_byte is used.
×
1294
        feeRateFlag, err := checkNotBothSet(
×
1295
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1296
        )
×
1297
        if err != nil {
×
1298
                return err
×
1299
        }
×
1300

1301
        prompt := "Do you really want to close ALL channels? (yes/no): "
×
1302
        if !ctx.Bool("skip_confirmation") && !promptForConfirmation(prompt) {
×
1303
                return errors.New("action aborted by user")
×
1304
        }
×
1305

1306
        listReq := &lnrpc.ListChannelsRequest{}
×
1307
        openChannels, err := client.ListChannels(ctxc, listReq)
×
1308
        if err != nil {
×
1309
                return fmt.Errorf("unable to fetch open channels: %w", err)
×
1310
        }
×
1311

1312
        if len(openChannels.Channels) == 0 {
×
1313
                return errors.New("no open channels to close")
×
1314
        }
×
1315

1316
        var channelsToClose []*lnrpc.Channel
×
1317

×
1318
        switch {
×
1319
        case ctx.Bool("force") && ctx.Bool("inactive_only"):
×
1320
                msg := "Unilaterally close all inactive channels? The funds " +
×
1321
                        "within these channels will be locked for some blocks " +
×
1322
                        "(CSV delay) before they can be spent. (yes/no): "
×
1323

×
1324
                confirmed := promptForConfirmation(msg)
×
1325

×
1326
                // We can safely exit if the user did not confirm.
×
1327
                if !confirmed {
×
1328
                        return nil
×
1329
                }
×
1330

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

×
1346
                confirmed := promptForConfirmation(msg)
×
1347

×
1348
                // We can safely exit if the user did not confirm.
×
1349
                if !confirmed {
×
1350
                        return nil
×
1351
                }
×
1352

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

×
1369
                                confirmed := promptForConfirmation(msg)
×
1370

×
1371
                                if confirmed {
×
1372
                                        channelsToClose = append(
×
1373
                                                channelsToClose, channel,
×
1374
                                        )
×
1375
                                }
×
1376
                        } else if !ctx.Bool("inactive_only") {
×
1377
                                // Otherwise, we'll only add active channels if
×
1378
                                // we were not requested to close inactive
×
1379
                                // channels only.
×
1380
                                channelsToClose = append(
×
1381
                                        channelsToClose, channel,
×
1382
                                )
×
1383
                        }
×
1384
                }
1385
        }
1386

1387
        // result defines the result of closing a channel. The closing
1388
        // transaction ID is populated if a channel is successfully closed.
1389
        // Otherwise, the error that prevented closing the channel is populated.
1390
        type result struct {
×
1391
                RemotePubKey string `json:"remote_pub_key"`
×
1392
                ChannelPoint string `json:"channel_point"`
×
1393
                ClosingTxid  string `json:"closing_txid"`
×
1394
                FailErr      string `json:"error"`
×
1395
        }
×
1396

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

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

1425
                        req := &lnrpc.CloseChannelRequest{
×
1426
                                ChannelPoint: &lnrpc.ChannelPoint{
×
1427
                                        FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
×
1428
                                                FundingTxidStr: s[0],
×
1429
                                        },
×
1430
                                        OutputIndex: uint32(index),
×
1431
                                },
×
1432
                                Force:       !channel.GetActive(),
×
1433
                                TargetConf:  int32(ctx.Int64("conf_target")),
×
1434
                                SatPerVbyte: ctx.Uint64(feeRateFlag),
×
1435
                        }
×
1436

×
1437
                        txidChan := make(chan string, 1)
×
1438
                        err = executeChannelClose(ctxc, client, req, txidChan, false)
×
1439
                        if err != nil {
×
1440
                                res.FailErr = fmt.Sprintf("unable to close "+
×
1441
                                        "channel: %v", err)
×
1442
                                return
×
1443
                        }
×
1444

1445
                        res.ClosingTxid = <-txidChan
×
1446
                }(channel)
1447
        }
1448

1449
        for range channelsToClose {
×
1450
                res := <-resultChan
×
1451
                printJSON(res)
×
1452
        }
×
1453

1454
        return nil
×
1455
}
1456

1457
// promptForConfirmation continuously prompts the user for the message until
1458
// receiving a response of "yes" or "no" and returns their answer as a bool.
1459
func promptForConfirmation(msg string) bool {
×
1460
        reader := bufio.NewReader(os.Stdin)
×
1461

×
1462
        for {
×
1463
                fmt.Print(msg)
×
1464

×
1465
                answer, err := reader.ReadString('\n')
×
1466
                if err != nil {
×
1467
                        return false
×
1468
                }
×
1469

1470
                answer = strings.ToLower(strings.TrimSpace(answer))
×
1471

×
1472
                switch {
×
1473
                case answer == "yes":
×
1474
                        return true
×
1475
                case answer == "no":
×
1476
                        return false
×
1477
                default:
×
1478
                        continue
×
1479
                }
1480
        }
1481
}
1482

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

1492
        Only available when lnd is built in debug mode. The flag
1493
        --i_know_what_i_am_doing can be set to override the debug/dev mode
1494
        requirement.
1495

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

1530
func abandonChannel(ctx *cli.Context) error {
×
1531
        ctxc := getContext()
×
1532
        client, cleanUp := getClient(ctx)
×
1533
        defer cleanUp()
×
1534

×
1535
        // Show command help if no arguments and flags were provided.
×
1536
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1537
                cli.ShowCommandHelp(ctx, "abandonchannel")
×
1538
                return nil
×
1539
        }
×
1540

1541
        channelPoint, err := parseChannelPoint(ctx)
×
1542
        if err != nil {
×
1543
                return err
×
1544
        }
×
1545

1546
        req := &lnrpc.AbandonChannelRequest{
×
1547
                ChannelPoint:      channelPoint,
×
1548
                IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
×
1549
        }
×
1550

×
1551
        resp, err := client.AbandonChannel(ctxc, req)
×
1552
        if err != nil {
×
1553
                return err
×
1554
        }
×
1555

1556
        printRespJSON(resp)
×
1557
        return nil
×
1558
}
1559

1560
// parseChannelPoint parses a funding txid and output index from the command
1561
// line. Both named options and unnamed parameters are supported.
1562
func parseChannelPoint(ctx *cli.Context) (*lnrpc.ChannelPoint, error) {
×
1563
        channelPoint := &lnrpc.ChannelPoint{}
×
1564
        var err error
×
1565

×
1566
        args := ctx.Args()
×
1567

×
1568
        switch {
×
1569
        case ctx.IsSet("chan_point"):
×
1570
                channelPoint, err = parseChanPoint(ctx.String("chan_point"))
×
1571
                if err != nil {
×
1572
                        return nil, fmt.Errorf("unable to parse chan_point: "+
×
1573
                                "%v", err)
×
1574
                }
×
1575
                return channelPoint, nil
×
1576

1577
        case ctx.IsSet("funding_txid"):
×
1578
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1579
                        FundingTxidStr: ctx.String("funding_txid"),
×
1580
                }
×
1581
        case args.Present():
×
1582
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1583
                        FundingTxidStr: args.First(),
×
1584
                }
×
1585
                args = args.Tail()
×
1586
        default:
×
1587
                return nil, fmt.Errorf("funding txid argument missing")
×
1588
        }
1589

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

1604
        return channelPoint, nil
×
1605
}
1606

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

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

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

1635
        printRespJSON(resp)
×
1636
        return nil
×
1637
}
1638

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

1654
func walletBalance(ctx *cli.Context) error {
×
1655
        ctxc := getContext()
×
1656
        client, cleanUp := getClient(ctx)
×
1657
        defer cleanUp()
×
1658

×
1659
        req := &lnrpc.WalletBalanceRequest{
×
1660
                Account: ctx.String("account"),
×
1661
        }
×
1662
        resp, err := client.WalletBalance(ctxc, req)
×
1663
        if err != nil {
×
1664
                return err
×
1665
        }
×
1666

1667
        printRespJSON(resp)
×
1668
        return nil
×
1669
}
1670

1671
var ChannelBalanceCommand = cli.Command{
1672
        Name:     "channelbalance",
1673
        Category: "Channels",
1674
        Usage: "Returns the sum of the total available channel balance across " +
1675
                "all open channels.",
1676
        Action: actionDecorator(ChannelBalance),
1677
}
1678

1679
func ChannelBalance(ctx *cli.Context) error {
×
1680
        ctxc := getContext()
×
1681
        client, cleanUp := getClient(ctx)
×
1682
        defer cleanUp()
×
1683

×
1684
        req := &lnrpc.ChannelBalanceRequest{}
×
1685
        resp, err := client.ChannelBalance(ctxc, req)
×
1686
        if err != nil {
×
1687
                return err
×
1688
        }
×
1689

1690
        printRespJSON(resp)
×
1691
        return nil
×
1692
}
1693

1694
var generateManPageCommand = cli.Command{
1695
        Name: "generatemanpage",
1696
        Usage: "Generates a man page for lncli and lnd as " +
1697
                "lncli.1 and lnd.1 respectively.",
1698
        Hidden: true,
1699
        Action: actionDecorator(generateManPage),
1700
}
1701

1702
func generateManPage(ctx *cli.Context) error {
×
1703
        // Generate the man pages for lncli as lncli.1.
×
1704
        manpages, err := ctx.App.ToMan()
×
1705
        if err != nil {
×
1706
                return err
×
1707
        }
×
1708
        err = os.WriteFile("lncli.1", []byte(manpages), 0644)
×
1709
        if err != nil {
×
1710
                return err
×
1711
        }
×
1712

1713
        // Generate the man pages for lnd as lnd.1.
1714
        config := lnd.DefaultConfig()
×
1715
        fileParser := flags.NewParser(&config, flags.Default)
×
1716
        fileParser.Name = "lnd"
×
1717

×
1718
        var buf bytes.Buffer
×
1719
        fileParser.WriteManPage(&buf)
×
1720

×
1721
        err = os.WriteFile("lnd.1", buf.Bytes(), 0644)
×
1722
        if err != nil {
×
1723
                return err
×
1724
        }
×
1725

1726
        return nil
×
1727
}
1728

1729
var getInfoCommand = cli.Command{
1730
        Name:   "getinfo",
1731
        Usage:  "Returns basic information related to the active daemon.",
1732
        Action: actionDecorator(getInfo),
1733
}
1734

1735
func getInfo(ctx *cli.Context) error {
×
1736
        ctxc := getContext()
×
1737
        client, cleanUp := getClient(ctx)
×
1738
        defer cleanUp()
×
1739

×
1740
        req := &lnrpc.GetInfoRequest{}
×
1741
        resp, err := client.GetInfo(ctxc, req)
×
1742
        if err != nil {
×
1743
                return err
×
1744
        }
×
1745

1746
        printRespJSON(resp)
×
1747
        return nil
×
1748
}
1749

1750
var getRecoveryInfoCommand = cli.Command{
1751
        Name:   "getrecoveryinfo",
1752
        Usage:  "Display information about an ongoing recovery attempt.",
1753
        Action: actionDecorator(getRecoveryInfo),
1754
}
1755

1756
func getRecoveryInfo(ctx *cli.Context) error {
×
1757
        ctxc := getContext()
×
1758
        client, cleanUp := getClient(ctx)
×
1759
        defer cleanUp()
×
1760

×
1761
        req := &lnrpc.GetRecoveryInfoRequest{}
×
1762
        resp, err := client.GetRecoveryInfo(ctxc, req)
×
1763
        if err != nil {
×
1764
                return err
×
1765
        }
×
1766

1767
        printRespJSON(resp)
×
1768
        return nil
×
1769
}
1770

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

1785
func pendingChannels(ctx *cli.Context) error {
×
1786
        ctxc := getContext()
×
1787
        client, cleanUp := getClient(ctx)
×
1788
        defer cleanUp()
×
1789

×
1790
        includeRawTx := ctx.Bool("include_raw_tx")
×
1791
        req := &lnrpc.PendingChannelsRequest{
×
1792
                IncludeRawTx: includeRawTx,
×
1793
        }
×
1794
        resp, err := client.PendingChannels(ctxc, req)
×
1795
        if err != nil {
×
1796
                return err
×
1797
        }
×
1798

1799
        printRespJSON(resp)
×
1800

×
1801
        return nil
×
1802
}
1803

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

1840
var listAliasesCommand = cli.Command{
1841
        Name:     "listaliases",
1842
        Category: "Channels",
1843
        Usage:    "List all aliases.",
1844
        Flags:    []cli.Flag{},
1845
        Action:   actionDecorator(listAliases),
1846
}
1847

1848
func listAliases(ctx *cli.Context) error {
×
1849
        ctxc := getContext()
×
1850
        client, cleanUp := getClient(ctx)
×
1851
        defer cleanUp()
×
1852

×
1853
        req := &lnrpc.ListAliasesRequest{}
×
1854

×
1855
        resp, err := client.ListAliases(ctxc, req)
×
1856
        if err != nil {
×
1857
                return err
×
1858
        }
×
1859

1860
        printRespJSON(resp)
×
1861

×
1862
        return nil
×
1863
}
1864

1865
func ListChannels(ctx *cli.Context) error {
×
1866
        ctxc := getContext()
×
1867
        client, cleanUp := getClient(ctx)
×
1868
        defer cleanUp()
×
1869

×
1870
        peer := ctx.String("peer")
×
1871

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

1881
                peerKey = pk[:]
×
1882
        }
1883

1884
        // By default, we will look up the peers' alias information unless the
1885
        // skip_peer_alias_lookup flag indicates otherwise.
1886
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1887

×
1888
        req := &lnrpc.ListChannelsRequest{
×
1889
                ActiveOnly:      ctx.Bool("active_only"),
×
1890
                InactiveOnly:    ctx.Bool("inactive_only"),
×
1891
                PublicOnly:      ctx.Bool("public_only"),
×
1892
                PrivateOnly:     ctx.Bool("private_only"),
×
1893
                Peer:            peerKey,
×
1894
                PeerAliasLookup: lookupPeerAlias,
×
1895
        }
×
1896

×
1897
        resp, err := client.ListChannels(ctxc, req)
×
1898
        if err != nil {
×
1899
                return err
×
1900
        }
×
1901

1902
        printModifiedProtoJSON(resp)
×
1903

×
1904
        return nil
×
1905
}
1906

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

1945
func closedChannels(ctx *cli.Context) error {
×
1946
        ctxc := getContext()
×
1947
        client, cleanUp := getClient(ctx)
×
1948
        defer cleanUp()
×
1949

×
1950
        req := &lnrpc.ClosedChannelsRequest{
×
1951
                Cooperative:     ctx.Bool("cooperative"),
×
1952
                LocalForce:      ctx.Bool("local_force"),
×
1953
                RemoteForce:     ctx.Bool("remote_force"),
×
1954
                Breach:          ctx.Bool("breach"),
×
1955
                FundingCanceled: ctx.Bool("funding_canceled"),
×
1956
                Abandoned:       ctx.Bool("abandoned"),
×
1957
        }
×
1958

×
1959
        resp, err := client.ClosedChannels(ctxc, req)
×
1960
        if err != nil {
×
1961
                return err
×
1962
        }
×
1963

1964
        printModifiedProtoJSON(resp)
×
1965

×
1966
        return nil
×
1967
}
1968

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

1986
func describeGraph(ctx *cli.Context) error {
×
1987
        ctxc := getContext()
×
1988
        client, cleanUp := getClient(ctx)
×
1989
        defer cleanUp()
×
1990

×
1991
        req := &lnrpc.ChannelGraphRequest{
×
1992
                IncludeUnannounced: ctx.Bool("include_unannounced"),
×
1993
        }
×
1994

×
1995
        graph, err := client.DescribeGraph(ctxc, req)
×
1996
        if err != nil {
×
1997
                return err
×
1998
        }
×
1999

2000
        printRespJSON(graph)
×
2001
        return nil
×
2002
}
2003

2004
var getNodeMetricsCommand = cli.Command{
2005
        Name:        "getnodemetrics",
2006
        Category:    "Graph",
2007
        Description: "Prints out node metrics calculated from the current graph",
2008
        Usage:       "Get node metrics.",
2009
        Action:      actionDecorator(getNodeMetrics),
2010
}
2011

2012
func getNodeMetrics(ctx *cli.Context) error {
×
2013
        ctxc := getContext()
×
2014
        client, cleanUp := getClient(ctx)
×
2015
        defer cleanUp()
×
2016

×
2017
        req := &lnrpc.NodeMetricsRequest{
×
2018
                Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
×
2019
        }
×
2020

×
2021
        nodeMetrics, err := client.GetNodeMetrics(ctxc, req)
×
2022
        if err != nil {
×
2023
                return err
×
2024
        }
×
2025

2026
        printRespJSON(nodeMetrics)
×
2027
        return nil
×
2028
}
2029

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

2054
func getChanInfo(ctx *cli.Context) error {
×
2055
        ctxc := getContext()
×
2056
        client, cleanUp := getClient(ctx)
×
2057
        defer cleanUp()
×
2058

×
2059
        var (
×
2060
                chanID    uint64
×
2061
                chanPoint string
×
2062
                err       error
×
2063
        )
×
2064

×
2065
        switch {
×
2066
        case ctx.IsSet("chan_id"):
×
2067
                chanID = ctx.Uint64("chan_id")
×
2068

2069
        case ctx.Args().Present():
×
2070
                chanID, err = strconv.ParseUint(ctx.Args().First(), 10, 64)
×
2071
                if err != nil {
×
2072
                        return fmt.Errorf("error parsing chan_id: %w", err)
×
2073
                }
×
2074

2075
        case ctx.IsSet("chan_point"):
×
2076
                chanPoint = ctx.String("chan_point")
×
2077

2078
        default:
×
2079
                return fmt.Errorf("chan_id or chan_point argument missing")
×
2080
        }
2081

2082
        req := &lnrpc.ChanInfoRequest{
×
2083
                ChanId:    chanID,
×
2084
                ChanPoint: chanPoint,
×
2085
        }
×
2086

×
2087
        chanInfo, err := client.GetChanInfo(ctxc, req)
×
2088
        if err != nil {
×
2089
                return err
×
2090
        }
×
2091

2092
        printRespJSON(chanInfo)
×
2093
        return nil
×
2094
}
2095

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

2117
func getNodeInfo(ctx *cli.Context) error {
×
2118
        ctxc := getContext()
×
2119
        client, cleanUp := getClient(ctx)
×
2120
        defer cleanUp()
×
2121

×
2122
        args := ctx.Args()
×
2123

×
2124
        var pubKey string
×
2125
        switch {
×
2126
        case ctx.IsSet("pub_key"):
×
2127
                pubKey = ctx.String("pub_key")
×
2128
        case args.Present():
×
2129
                pubKey = args.First()
×
2130
        default:
×
2131
                return fmt.Errorf("pub_key argument missing")
×
2132
        }
2133

2134
        req := &lnrpc.NodeInfoRequest{
×
2135
                PubKey:          pubKey,
×
2136
                IncludeChannels: ctx.Bool("include_channels"),
×
2137
        }
×
2138

×
2139
        nodeInfo, err := client.GetNodeInfo(ctxc, req)
×
2140
        if err != nil {
×
2141
                return err
×
2142
        }
×
2143

2144
        printRespJSON(nodeInfo)
×
2145
        return nil
×
2146
}
2147

2148
var getNetworkInfoCommand = cli.Command{
2149
        Name:     "getnetworkinfo",
2150
        Category: "Channels",
2151
        Usage: "Get statistical information about the current " +
2152
                "state of the network.",
2153
        Description: "Returns a set of statistics pertaining to the known " +
2154
                "channel graph",
2155
        Action: actionDecorator(getNetworkInfo),
2156
}
2157

2158
func getNetworkInfo(ctx *cli.Context) error {
×
2159
        ctxc := getContext()
×
2160
        client, cleanUp := getClient(ctx)
×
2161
        defer cleanUp()
×
2162

×
2163
        req := &lnrpc.NetworkInfoRequest{}
×
2164

×
2165
        netInfo, err := client.GetNetworkInfo(ctxc, req)
×
2166
        if err != nil {
×
2167
                return err
×
2168
        }
×
2169

2170
        printRespJSON(netInfo)
×
2171
        return nil
×
2172
}
2173

2174
var debugLevelCommand = cli.Command{
2175
        Name:  "debuglevel",
2176
        Usage: "Set the debug level.",
2177
        Description: `Logging level for all subsystems {trace, debug, info, warn, error, critical, off}
2178
        You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems
2179

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

2194
func debugLevel(ctx *cli.Context) error {
×
2195
        ctxc := getContext()
×
2196
        client, cleanUp := getClient(ctx)
×
2197
        defer cleanUp()
×
2198
        req := &lnrpc.DebugLevelRequest{
×
2199
                Show:      ctx.Bool("show"),
×
2200
                LevelSpec: ctx.String("level"),
×
2201
        }
×
2202

×
2203
        resp, err := client.DebugLevel(ctxc, req)
×
2204
        if err != nil {
×
2205
                return err
×
2206
        }
×
2207

2208
        printRespJSON(resp)
×
2209
        return nil
×
2210
}
2211

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

2250
        This call will return a list of wallet related transactions that paid
2251
        to an address our wallet controls, or spent utxos that we held.
2252

2253
        By default, this call will get all transactions until the chain tip, 
2254
        including unconfirmed transactions (end_height=-1).`,
2255
        Action: actionDecorator(listChainTxns),
2256
}
2257

2258
func parseBlockHeightInputs(ctx *cli.Context) (int32, int32, error) {
7✔
2259
        startHeight := int32(ctx.Int64("start_height"))
7✔
2260
        endHeight := int32(ctx.Int64("end_height"))
7✔
2261

7✔
2262
        if ctx.IsSet("start_height") && ctx.IsSet("end_height") {
14✔
2263
                if endHeight != -1 && startHeight > endHeight {
8✔
2264
                        return startHeight, endHeight,
1✔
2265
                                errors.New("start_height should " +
1✔
2266
                                        "be less than end_height if " +
1✔
2267
                                        "end_height is not equal to -1")
1✔
2268
                }
1✔
2269
        }
2270

2271
        if startHeight < 0 {
7✔
2272
                return startHeight, endHeight,
1✔
2273
                        errors.New("start_height should " +
1✔
2274
                                "be greater than or " +
1✔
2275
                                "equal to 0")
1✔
2276
        }
1✔
2277

2278
        return startHeight, endHeight, nil
5✔
2279
}
2280

2281
func listChainTxns(ctx *cli.Context) error {
×
2282
        ctxc := getContext()
×
2283
        client, cleanUp := getClient(ctx)
×
2284
        defer cleanUp()
×
2285

×
NEW
2286
        startHeight, endHeight, err := parseBlockHeightInputs(ctx)
×
NEW
2287

×
NEW
2288
        if err != nil {
×
NEW
2289
                return err
×
NEW
2290
        }
×
2291

2292
        req := &lnrpc.GetTransactionsRequest{
×
2293
                IndexOffset:     uint32(ctx.Uint64("index_offset")),
×
2294
                MaxTransactions: uint32(ctx.Uint64("max_transactions")),
×
2295
        }
×
NEW
2296
        req.StartHeight = startHeight
×
NEW
2297
        req.EndHeight = endHeight
×
2298

×
2299
        resp, err := client.GetTransactions(ctxc, req)
×
2300
        if err != nil {
×
2301
                return err
×
2302
        }
×
2303

2304
        printRespJSON(resp)
×
2305
        return nil
×
2306
}
2307

2308
var stopCommand = cli.Command{
2309
        Name:  "stop",
2310
        Usage: "Stop and shutdown the daemon.",
2311
        Description: `
2312
        Gracefully stop all daemon subsystems before stopping the daemon itself.
2313
        This is equivalent to stopping it using CTRL-C.`,
2314
        Action: actionDecorator(stopDaemon),
2315
}
2316

2317
func stopDaemon(ctx *cli.Context) error {
×
2318
        ctxc := getContext()
×
2319
        client, cleanUp := getClient(ctx)
×
2320
        defer cleanUp()
×
2321

×
2322
        resp, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2323
        if err != nil {
×
2324
                return err
×
2325
        }
×
2326

2327
        printRespJSON(resp)
×
2328

×
2329
        return nil
×
2330
}
2331

2332
var signMessageCommand = cli.Command{
2333
        Name:      "signmessage",
2334
        Category:  "Wallet",
2335
        Usage:     "Sign a message with the node's private key.",
2336
        ArgsUsage: "msg",
2337
        Description: `
2338
        Sign msg with the resident node's private key.
2339
        Returns the signature as a zbase32 string.
2340

2341
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2342
        Flags: []cli.Flag{
2343
                cli.StringFlag{
2344
                        Name:  "msg",
2345
                        Usage: "the message to sign",
2346
                },
2347
        },
2348
        Action: actionDecorator(signMessage),
2349
}
2350

2351
func signMessage(ctx *cli.Context) error {
×
2352
        ctxc := getContext()
×
2353
        client, cleanUp := getClient(ctx)
×
2354
        defer cleanUp()
×
2355

×
2356
        var msg []byte
×
2357

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

2367
        resp, err := client.SignMessage(ctxc, &lnrpc.SignMessageRequest{Msg: msg})
×
2368
        if err != nil {
×
2369
                return err
×
2370
        }
×
2371

2372
        printRespJSON(resp)
×
2373
        return nil
×
2374
}
2375

2376
var verifyMessageCommand = cli.Command{
2377
        Name:      "verifymessage",
2378
        Category:  "Wallet",
2379
        Usage:     "Verify a message signed with the signature.",
2380
        ArgsUsage: "msg signature",
2381
        Description: `
2382
        Verify that the message was signed with a properly-formed signature
2383
        The signature must be zbase32 encoded and signed with the private key of
2384
        an active node in the resident node's channel database.
2385

2386
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2387
        Flags: []cli.Flag{
2388
                cli.StringFlag{
2389
                        Name:  "msg",
2390
                        Usage: "the message to verify",
2391
                },
2392
                cli.StringFlag{
2393
                        Name:  "sig",
2394
                        Usage: "the zbase32 encoded signature of the message",
2395
                },
2396
        },
2397
        Action: actionDecorator(verifyMessage),
2398
}
2399

2400
func verifyMessage(ctx *cli.Context) error {
×
2401
        ctxc := getContext()
×
2402
        client, cleanUp := getClient(ctx)
×
2403
        defer cleanUp()
×
2404

×
2405
        var (
×
2406
                msg []byte
×
2407
                sig string
×
2408
        )
×
2409

×
2410
        args := ctx.Args()
×
2411

×
2412
        switch {
×
2413
        case ctx.IsSet("msg"):
×
2414
                msg = []byte(ctx.String("msg"))
×
2415
        case args.Present():
×
2416
                msg = []byte(ctx.Args().First())
×
2417
                args = args.Tail()
×
2418
        default:
×
2419
                return fmt.Errorf("msg argument missing")
×
2420
        }
2421

2422
        switch {
×
2423
        case ctx.IsSet("sig"):
×
2424
                sig = ctx.String("sig")
×
2425
        case args.Present():
×
2426
                sig = args.First()
×
2427
        default:
×
2428
                return fmt.Errorf("signature argument missing")
×
2429
        }
2430

2431
        req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
×
2432
        resp, err := client.VerifyMessage(ctxc, req)
×
2433
        if err != nil {
×
2434
                return err
×
2435
        }
×
2436

2437
        printRespJSON(resp)
×
2438
        return nil
×
2439
}
2440

2441
var feeReportCommand = cli.Command{
2442
        Name:     "feereport",
2443
        Category: "Channels",
2444
        Usage:    "Display the current fee policies of all active channels.",
2445
        Description: `
2446
        Returns the current fee policies of all active channels.
2447
        Fee policies can be updated using the updatechanpolicy command.`,
2448
        Action: actionDecorator(feeReport),
2449
}
2450

2451
func feeReport(ctx *cli.Context) error {
×
2452
        ctxc := getContext()
×
2453
        client, cleanUp := getClient(ctx)
×
2454
        defer cleanUp()
×
2455

×
2456
        req := &lnrpc.FeeReportRequest{}
×
2457
        resp, err := client.FeeReport(ctxc, req)
×
2458
        if err != nil {
×
2459
                return err
×
2460
        }
×
2461

2462
        printRespJSON(resp)
×
2463
        return nil
×
2464
}
2465

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

2572
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
7✔
2573
        split := strings.Split(s, ":")
7✔
2574
        if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
10✔
2575
                return nil, errBadChanPoint
3✔
2576
        }
3✔
2577

2578
        index, err := strconv.ParseInt(split[1], 10, 64)
4✔
2579
        if err != nil {
5✔
2580
                return nil, fmt.Errorf("unable to decode output index: %w", err)
1✔
2581
        }
1✔
2582

2583
        txid, err := chainhash.NewHashFromStr(split[0])
3✔
2584
        if err != nil {
4✔
2585
                return nil, fmt.Errorf("unable to parse hex string: %w", err)
1✔
2586
        }
1✔
2587

2588
        return &lnrpc.ChannelPoint{
2✔
2589
                FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
2✔
2590
                        FundingTxidBytes: txid[:],
2✔
2591
                },
2✔
2592
                OutputIndex: uint32(index),
2✔
2593
        }, nil
2✔
2594
}
2595

2596
// parseTimeLockDelta is expected to get a uint16 type of timeLockDelta. Its
2597
// maximum value is MaxTimeLockDelta.
2598
func parseTimeLockDelta(timeLockDeltaStr string) (uint16, error) {
5✔
2599
        timeLockDeltaUnCheck, err := strconv.ParseUint(timeLockDeltaStr, 10, 64)
5✔
2600
        if err != nil {
7✔
2601
                return 0, fmt.Errorf("failed to parse time_lock_delta: %s "+
2✔
2602
                        "to uint64, err: %v", timeLockDeltaStr, err)
2✔
2603
        }
2✔
2604

2605
        if timeLockDeltaUnCheck > routing.MaxCLTVDelta {
3✔
2606
                return 0, fmt.Errorf("time_lock_delta is too big, "+
×
2607
                        "max value is %d", routing.MaxCLTVDelta)
×
2608
        }
×
2609

2610
        return uint16(timeLockDeltaUnCheck), nil
3✔
2611
}
2612

2613
func updateChannelPolicy(ctx *cli.Context) error {
×
2614
        ctxc := getContext()
×
2615
        client, cleanUp := getClient(ctx)
×
2616
        defer cleanUp()
×
2617

×
2618
        var (
×
2619
                baseFee       int64
×
2620
                feeRate       float64
×
2621
                feeRatePpm    uint64
×
2622
                timeLockDelta uint16
×
2623
                err           error
×
2624
        )
×
2625
        args := ctx.Args()
×
2626

×
2627
        switch {
×
2628
        case ctx.IsSet("base_fee_msat"):
×
2629
                baseFee = ctx.Int64("base_fee_msat")
×
2630
        case args.Present():
×
2631
                baseFee, err = strconv.ParseInt(args.First(), 10, 64)
×
2632
                if err != nil {
×
2633
                        return fmt.Errorf("unable to decode base_fee_msat: %w",
×
2634
                                err)
×
2635
                }
×
2636
                args = args.Tail()
×
2637
        default:
×
2638
                return fmt.Errorf("base_fee_msat argument missing")
×
2639
        }
2640

2641
        switch {
×
2642
        case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
×
2643
                return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
×
2644
        case ctx.IsSet("fee_rate"):
×
2645
                feeRate = ctx.Float64("fee_rate")
×
2646
        case ctx.IsSet("fee_rate_ppm"):
×
2647
                feeRatePpm = ctx.Uint64("fee_rate_ppm")
×
2648
        case args.Present():
×
2649
                feeRate, err = strconv.ParseFloat(args.First(), 64)
×
2650
                if err != nil {
×
2651
                        return fmt.Errorf("unable to decode fee_rate: %w", err)
×
2652
                }
×
2653

2654
                args = args.Tail()
×
2655
        default:
×
2656
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
×
2657
        }
2658

2659
        switch {
×
2660
        case ctx.IsSet("time_lock_delta"):
×
2661
                timeLockDeltaStr := ctx.String("time_lock_delta")
×
2662
                timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
×
2663
                if err != nil {
×
2664
                        return err
×
2665
                }
×
2666
        case args.Present():
×
2667
                timeLockDelta, err = parseTimeLockDelta(args.First())
×
2668
                if err != nil {
×
2669
                        return err
×
2670
                }
×
2671

2672
                args = args.Tail()
×
2673
        default:
×
2674
                return fmt.Errorf("time_lock_delta argument missing")
×
2675
        }
2676

2677
        var (
×
2678
                chanPoint    *lnrpc.ChannelPoint
×
2679
                chanPointStr string
×
2680
        )
×
2681

×
2682
        switch {
×
2683
        case ctx.IsSet("chan_point"):
×
2684
                chanPointStr = ctx.String("chan_point")
×
2685
        case args.Present():
×
2686
                chanPointStr = args.First()
×
2687
        }
2688

2689
        if chanPointStr != "" {
×
2690
                chanPoint, err = parseChanPoint(chanPointStr)
×
2691
                if err != nil {
×
2692
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2693
                }
×
2694
        }
2695

2696
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2697
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2698
                inboundBaseFeeMsat > math.MaxInt32 {
×
2699

×
2700
                return errors.New("inbound_base_fee_msat out of range")
×
2701
        }
×
2702

2703
        inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
×
2704
        if inboundFeeRatePpm < math.MinInt32 ||
×
2705
                inboundFeeRatePpm > math.MaxInt32 {
×
2706

×
2707
                return errors.New("inbound_fee_rate_ppm out of range")
×
2708
        }
×
2709

2710
        // Inbound fees are optional. However, if an update is required,
2711
        // both the base fee and the fee rate must be provided.
2712
        var inboundFee *lnrpc.InboundFee
×
2713
        if ctx.IsSet("inbound_base_fee_msat") !=
×
2714
                ctx.IsSet("inbound_fee_rate_ppm") {
×
2715

×
2716
                return errors.New("both parameters must be provided: " +
×
2717
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
×
2718
        }
×
2719

2720
        if ctx.IsSet("inbound_fee_rate_ppm") {
×
2721
                inboundFee = &lnrpc.InboundFee{
×
2722
                        BaseFeeMsat: int32(inboundBaseFeeMsat),
×
2723
                        FeeRatePpm:  int32(inboundFeeRatePpm),
×
2724
                }
×
2725
        }
×
2726

2727
        createMissingEdge := ctx.Bool("create_missing_edge")
×
2728

×
2729
        req := &lnrpc.PolicyUpdateRequest{
×
2730
                BaseFeeMsat:       baseFee,
×
2731
                TimeLockDelta:     uint32(timeLockDelta),
×
2732
                MaxHtlcMsat:       ctx.Uint64("max_htlc_msat"),
×
2733
                InboundFee:        inboundFee,
×
2734
                CreateMissingEdge: createMissingEdge,
×
2735
        }
×
2736

×
2737
        if ctx.IsSet("min_htlc_msat") {
×
2738
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2739
                req.MinHtlcMsatSpecified = true
×
2740
        }
×
2741

2742
        if chanPoint != nil {
×
2743
                req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
×
2744
                        ChanPoint: chanPoint,
×
2745
                }
×
2746
        } else {
×
2747
                req.Scope = &lnrpc.PolicyUpdateRequest_Global{
×
2748
                        Global: true,
×
2749
                }
×
2750
        }
×
2751

2752
        if feeRate != 0 {
×
2753
                req.FeeRate = feeRate
×
2754
        } else if feeRatePpm != 0 {
×
2755
                req.FeeRatePpm = uint32(feeRatePpm)
×
2756
        }
×
2757

2758
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2759
        if err != nil {
×
2760
                return err
×
2761
        }
×
2762

2763
        // Parse the response into the final json object that will be printed
2764
        // to stdout. At the moment, this filters out the raw txid bytes from
2765
        // each failed update's outpoint and only prints the txid string.
2766
        var listFailedUpdateResp = struct {
×
2767
                FailedUpdates []*FailedUpdate `json:"failed_updates"`
×
2768
        }{
×
2769
                FailedUpdates: make([]*FailedUpdate, 0, len(resp.FailedUpdates)),
×
2770
        }
×
2771
        for _, protoUpdate := range resp.FailedUpdates {
×
2772
                failedUpdate := NewFailedUpdateFromProto(protoUpdate)
×
2773
                listFailedUpdateResp.FailedUpdates = append(
×
2774
                        listFailedUpdateResp.FailedUpdates, failedUpdate)
×
2775
        }
×
2776

2777
        printJSON(listFailedUpdateResp)
×
2778

×
2779
        return nil
×
2780
}
2781

2782
var fishCompletionCommand = cli.Command{
2783
        Name:   "fish-completion",
2784
        Hidden: true,
2785
        Action: func(c *cli.Context) error {
×
2786
                completion, err := c.App.ToFishCompletion()
×
2787
                if err != nil {
×
2788
                        return err
×
2789
                }
×
2790

2791
                // We don't want to suggest files, so we add this
2792
                // first line to the completions.
2793
                _, err = fmt.Printf("complete -c %q -f \n%s", c.App.Name, completion)
×
2794
                return err
×
2795
        },
2796
}
2797

2798
var exportChanBackupCommand = cli.Command{
2799
        Name:     "exportchanbackup",
2800
        Category: "Channels",
2801
        Usage: "Obtain a static channel back up for a selected channels, " +
2802
                "or all known channels.",
2803
        ArgsUsage: "[chan_point] [--all] [--output_file]",
2804
        Description: `
2805
        This command allows a user to export a Static Channel Backup (SCB) for
2806
        a selected channel. SCB's are encrypted backups of a channel's initial
2807
        state that are encrypted with a key derived from the seed of a user. In
2808
        the case of partial or complete data loss, the SCB will allow the user
2809
        to reclaim settled funds in the channel at its final state. The
2810
        exported channel backups can be restored at a later time using the
2811
        restorechanbackup command.
2812

2813
        This command will return one of two types of channel backups depending
2814
        on the set of passed arguments:
2815

2816
           * If a target channel point is specified, then a single channel
2817
             backup containing only the information for that channel will be
2818
             returned.
2819

2820
           * If the --all flag is passed, then a multi-channel backup will be
2821
             returned. A multi backup is a single encrypted blob (displayed in
2822
             hex encoding) that contains several channels in a single cipher
2823
             text.
2824

2825
        Both of the backup types can be restored using the restorechanbackup
2826
        command.
2827
        `,
2828
        Flags: []cli.Flag{
2829
                cli.StringFlag{
2830
                        Name:  "chan_point",
2831
                        Usage: "the target channel to obtain an SCB for",
2832
                },
2833
                cli.BoolFlag{
2834
                        Name: "all",
2835
                        Usage: "if specified, then a multi backup of all " +
2836
                                "active channels will be returned",
2837
                },
2838
                cli.StringFlag{
2839
                        Name: "output_file",
2840
                        Usage: `
2841
                        if specified, then rather than printing a JSON output
2842
                        of the static channel backup, a serialized version of
2843
                        the backup (either Single or Multi) will be written to
2844
                        the target file, this is the same format used by lnd in
2845
                        its channel.backup file `,
2846
                },
2847
        },
2848
        Action: actionDecorator(exportChanBackup),
2849
}
2850

2851
func exportChanBackup(ctx *cli.Context) error {
×
2852
        ctxc := getContext()
×
2853
        client, cleanUp := getClient(ctx)
×
2854
        defer cleanUp()
×
2855

×
2856
        // Show command help if no arguments provided
×
2857
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2858
                cli.ShowCommandHelp(ctx, "exportchanbackup")
×
2859
                return nil
×
2860
        }
×
2861

2862
        var (
×
2863
                err            error
×
2864
                chanPointStr   string
×
2865
                outputFileName string
×
2866
        )
×
2867
        args := ctx.Args()
×
2868

×
2869
        switch {
×
2870
        case ctx.IsSet("chan_point"):
×
2871
                chanPointStr = ctx.String("chan_point")
×
2872

2873
        case args.Present():
×
2874
                chanPointStr = args.First()
×
2875

2876
        case !ctx.IsSet("all"):
×
2877
                return fmt.Errorf("must specify chan_point if --all isn't set")
×
2878
        }
2879

2880
        if ctx.IsSet("output_file") {
×
2881
                outputFileName = ctx.String("output_file")
×
2882
        }
×
2883

2884
        if chanPointStr != "" {
×
2885
                chanPointRPC, err := parseChanPoint(chanPointStr)
×
2886
                if err != nil {
×
2887
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2888
                }
×
2889

2890
                chanBackup, err := client.ExportChannelBackup(
×
2891
                        ctxc, &lnrpc.ExportChannelBackupRequest{
×
2892
                                ChanPoint: chanPointRPC,
×
2893
                        },
×
2894
                )
×
2895
                if err != nil {
×
2896
                        return err
×
2897
                }
×
2898

2899
                txid, err := chainhash.NewHash(
×
2900
                        chanPointRPC.GetFundingTxidBytes(),
×
2901
                )
×
2902
                if err != nil {
×
2903
                        return err
×
2904
                }
×
2905

2906
                chanPoint := wire.OutPoint{
×
2907
                        Hash:  *txid,
×
2908
                        Index: chanPointRPC.OutputIndex,
×
2909
                }
×
2910

×
2911
                if outputFileName != "" {
×
2912
                        return os.WriteFile(
×
2913
                                outputFileName,
×
2914
                                chanBackup.ChanBackup,
×
2915
                                0666,
×
2916
                        )
×
2917
                }
×
2918

2919
                printJSON(struct {
×
2920
                        ChanPoint  string `json:"chan_point"`
×
2921
                        ChanBackup string `json:"chan_backup"`
×
2922
                }{
×
2923
                        ChanPoint:  chanPoint.String(),
×
2924
                        ChanBackup: hex.EncodeToString(chanBackup.ChanBackup),
×
2925
                })
×
2926
                return nil
×
2927
        }
2928

2929
        if !ctx.IsSet("all") {
×
2930
                return fmt.Errorf("if a channel isn't specified, -all must be")
×
2931
        }
×
2932

2933
        chanBackup, err := client.ExportAllChannelBackups(
×
2934
                ctxc, &lnrpc.ChanBackupExportRequest{},
×
2935
        )
×
2936
        if err != nil {
×
2937
                return err
×
2938
        }
×
2939

2940
        if outputFileName != "" {
×
2941
                return os.WriteFile(
×
2942
                        outputFileName,
×
2943
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2944
                        0666,
×
2945
                )
×
2946
        }
×
2947

2948
        // TODO(roasbeef): support for export | restore ?
2949

2950
        var chanPoints []string
×
2951
        for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
×
2952
                txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
×
2953
                if err != nil {
×
2954
                        return err
×
2955
                }
×
2956

2957
                chanPoints = append(chanPoints, wire.OutPoint{
×
2958
                        Hash:  *txid,
×
2959
                        Index: chanPoint.OutputIndex,
×
2960
                }.String())
×
2961
        }
2962

2963
        printRespJSON(chanBackup)
×
2964

×
2965
        return nil
×
2966
}
2967

2968
var verifyChanBackupCommand = cli.Command{
2969
        Name:      "verifychanbackup",
2970
        Category:  "Channels",
2971
        Usage:     "Verify an existing channel backup.",
2972
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file]",
2973
        Description: `
2974
    This command allows a user to verify an existing Single or Multi channel
2975
    backup for integrity. This is useful when a user has a backup, but is
2976
    unsure as to if it's valid or for the target node.
2977

2978
    The command will accept backups in one of four forms:
2979

2980
       * A single channel packed SCB, which can be obtained from
2981
         exportchanbackup. This should be passed in hex encoded format.
2982

2983
       * A packed multi-channel SCB, which couples several individual
2984
         static channel backups in single blob.
2985

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

2989
       * A file path which points to a packed multi-channel backup within a
2990
         file, using the same format that lnd does in its channel.backup
2991
         file.
2992
    `,
2993
        Flags: []cli.Flag{
2994
                cli.StringFlag{
2995
                        Name: "single_backup",
2996
                        Usage: "a hex encoded single channel backup obtained " +
2997
                                "from exportchanbackup",
2998
                },
2999
                cli.StringFlag{
3000
                        Name: "multi_backup",
3001
                        Usage: "a hex encoded multi-channel backup obtained " +
3002
                                "from exportchanbackup",
3003
                },
3004

3005
                cli.StringFlag{
3006
                        Name:      "single_file",
3007
                        Usage:     "the path to a single-channel backup file",
3008
                        TakesFile: true,
3009
                },
3010

3011
                cli.StringFlag{
3012
                        Name:      "multi_file",
3013
                        Usage:     "the path to a multi-channel back up file",
3014
                        TakesFile: true,
3015
                },
3016
        },
3017
        Action: actionDecorator(verifyChanBackup),
3018
}
3019

3020
func verifyChanBackup(ctx *cli.Context) error {
×
3021
        ctxc := getContext()
×
3022
        client, cleanUp := getClient(ctx)
×
3023
        defer cleanUp()
×
3024

×
3025
        // Show command help if no arguments provided
×
3026
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3027
                cli.ShowCommandHelp(ctx, "verifychanbackup")
×
3028
                return nil
×
3029
        }
×
3030

3031
        backups, err := parseChanBackups(ctx)
×
3032
        if err != nil {
×
3033
                return err
×
3034
        }
×
3035

3036
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
3037

×
3038
        if backups.GetChanBackups() != nil {
×
3039
                verifyReq.SingleChanBackups = backups.GetChanBackups()
×
3040
        }
×
3041
        if backups.GetMultiChanBackup() != nil {
×
3042
                verifyReq.MultiChanBackup = &lnrpc.MultiChanBackup{
×
3043
                        MultiChanBackup: backups.GetMultiChanBackup(),
×
3044
                }
×
3045
        }
×
3046

3047
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
3048
        if err != nil {
×
3049
                return err
×
3050
        }
×
3051

3052
        printRespJSON(resp)
×
3053
        return nil
×
3054
}
3055

3056
var restoreChanBackupCommand = cli.Command{
3057
        Name:     "restorechanbackup",
3058
        Category: "Channels",
3059
        Usage: "Restore an existing single or multi-channel static channel " +
3060
                "backup.",
3061
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file=",
3062
        Description: `
3063
        Allows a user to restore a Static Channel Backup (SCB) that was
3064
        obtained either via the exportchanbackup command, or from lnd's
3065
        automatically managed channel.backup file. This command should be used
3066
        if a user is attempting to restore a channel due to data loss on a
3067
        running node restored with the same seed as the node that created the
3068
        channel. If successful, this command will allows the user to recover
3069
        the settled funds stored in the recovered channels.
3070

3071
        The command will accept backups in one of four forms:
3072

3073
           * A single channel packed SCB, which can be obtained from
3074
             exportchanbackup. This should be passed in hex encoded format.
3075

3076
           * A packed multi-channel SCB, which couples several individual
3077
             static channel backups in single blob.
3078

3079
           * A file path which points to a packed single-channel backup within
3080
             a file, using the same format that lnd does in its channel.backup
3081
             file.
3082

3083
           * A file path which points to a packed multi-channel backup within a
3084
             file, using the same format that lnd does in its channel.backup
3085
             file.
3086
        `,
3087
        Flags: []cli.Flag{
3088
                cli.StringFlag{
3089
                        Name: "single_backup",
3090
                        Usage: "a hex encoded single channel backup obtained " +
3091
                                "from exportchanbackup",
3092
                },
3093
                cli.StringFlag{
3094
                        Name: "multi_backup",
3095
                        Usage: "a hex encoded multi-channel backup obtained " +
3096
                                "from exportchanbackup",
3097
                },
3098

3099
                cli.StringFlag{
3100
                        Name:      "single_file",
3101
                        Usage:     "the path to a single-channel backup file",
3102
                        TakesFile: true,
3103
                },
3104

3105
                cli.StringFlag{
3106
                        Name:      "multi_file",
3107
                        Usage:     "the path to a multi-channel back up file",
3108
                        TakesFile: true,
3109
                },
3110
        },
3111
        Action: actionDecorator(restoreChanBackup),
3112
}
3113

3114
// errMissingChanBackup is an error returned when we attempt to parse a channel
3115
// backup from a CLI command, and it is missing.
3116
var errMissingChanBackup = errors.New("missing channel backup")
3117

3118
func parseChanBackups(ctx *cli.Context) (*lnrpc.RestoreChanBackupRequest, error) {
×
3119
        switch {
×
3120
        case ctx.IsSet("single_backup"):
×
3121
                packedBackup, err := hex.DecodeString(
×
3122
                        ctx.String("single_backup"),
×
3123
                )
×
3124
                if err != nil {
×
3125
                        return nil, fmt.Errorf("unable to decode single packed "+
×
3126
                                "backup: %v", err)
×
3127
                }
×
3128

3129
                return &lnrpc.RestoreChanBackupRequest{
×
3130
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3131
                                ChanBackups: &lnrpc.ChannelBackups{
×
3132
                                        ChanBackups: []*lnrpc.ChannelBackup{
×
3133
                                                {
×
3134
                                                        ChanBackup: packedBackup,
×
3135
                                                },
×
3136
                                        },
×
3137
                                },
×
3138
                        },
×
3139
                }, nil
×
3140

3141
        case ctx.IsSet("multi_backup"):
×
3142
                packedMulti, err := hex.DecodeString(
×
3143
                        ctx.String("multi_backup"),
×
3144
                )
×
3145
                if err != nil {
×
3146
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3147
                                "backup: %v", err)
×
3148
                }
×
3149

3150
                return &lnrpc.RestoreChanBackupRequest{
×
3151
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3152
                                MultiChanBackup: packedMulti,
×
3153
                        },
×
3154
                }, nil
×
3155

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

3163
                return &lnrpc.RestoreChanBackupRequest{
×
3164
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3165
                                ChanBackups: &lnrpc.ChannelBackups{
×
3166
                                        ChanBackups: []*lnrpc.ChannelBackup{{
×
3167
                                                ChanBackup: packedSingle,
×
3168
                                        }},
×
3169
                                },
×
3170
                        },
×
3171
                }, nil
×
3172

3173
        case ctx.IsSet("multi_file"):
×
3174
                packedMulti, err := os.ReadFile(ctx.String("multi_file"))
×
3175
                if err != nil {
×
3176
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3177
                                "backup: %v", err)
×
3178
                }
×
3179

3180
                return &lnrpc.RestoreChanBackupRequest{
×
3181
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3182
                                MultiChanBackup: packedMulti,
×
3183
                        },
×
3184
                }, nil
×
3185

3186
        default:
×
3187
                return nil, errMissingChanBackup
×
3188
        }
3189
}
3190

3191
func restoreChanBackup(ctx *cli.Context) error {
×
3192
        ctxc := getContext()
×
3193
        client, cleanUp := getClient(ctx)
×
3194
        defer cleanUp()
×
3195

×
3196
        // Show command help if no arguments provided
×
3197
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3198
                cli.ShowCommandHelp(ctx, "restorechanbackup")
×
3199
                return nil
×
3200
        }
×
3201

3202
        var req lnrpc.RestoreChanBackupRequest
×
3203

×
3204
        backups, err := parseChanBackups(ctx)
×
3205
        if err != nil {
×
3206
                return err
×
3207
        }
×
3208

3209
        req.Backup = backups.Backup
×
3210

×
3211
        resp, err := client.RestoreChannelBackups(ctxc, &req)
×
3212
        if err != nil {
×
3213
                return fmt.Errorf("unable to restore chan backups: %w", err)
×
3214
        }
×
3215

3216
        printRespJSON(resp)
×
3217

×
3218
        return nil
×
3219
}
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