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

lightningnetwork / lnd / 13315793998

13 Feb 2025 07:43PM UTC coverage: 58.779% (+9.4%) from 49.357%
13315793998

Pull #9491

github

ziggie1984
docs: add release-notes
Pull Request #9491: Allow coop closing a channel with HTLCs on it via lncli

17 of 49 new or added lines in 4 files covered. (34.69%)

28 existing lines in 9 files now uncovered.

136085 of 231519 relevant lines covered (58.78%)

19227.52 hits per line

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

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

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

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

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

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

40
const defaultRecoveryWindow int32 = 2500
41

42
const (
43
        defaultUtxoMinConf = 1
44
)
45

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

250
// actionDecorator is used to add additional information and error handling
251
// to command actions.
252
func actionDecorator(f func(*cli.Context) error) func(*cli.Context) error {
81✔
253
        return func(c *cli.Context) error {
81✔
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"),
×
NEW
1127
                // This makes sure that a coop close will also be executed if
×
NEW
1128
                // active HTLCs are present on the channel.
×
NEW
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:
×
NEW
1188
                        fmt.Fprintln(os.Stderr, "Channel close successfully "+
×
NEW
1189
                                "initiated")
×
NEW
1190

×
NEW
1191
                        pendingHtlcs := update.CloseInstant.NumPendingHtlcs
×
NEW
1192
                        if pendingHtlcs > 0 {
×
NEW
1193
                                fmt.Fprintf(os.Stderr, "Cooperative channel "+
×
NEW
1194
                                        "close waiting for %d HTLCs to be "+
×
NEW
1195
                                        "resolved before the close process "+
×
NEW
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

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

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

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

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

1218
                case *lnrpc.CloseStatusUpdate_ChanClose:
×
NEW
1219
                        fmt.Fprintln(os.Stderr, "Channel close successfully "+
×
NEW
1220
                                "confirmed")
×
NEW
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
                },
2222
                cli.Int64Flag{
2223
                        Name: "end_height",
2224
                        Usage: "the block height until which to list " +
2225
                                "transactions, inclusive, to get " +
2226
                                "transactions until the chain tip, including " +
2227
                                "unconfirmed, set this value to -1",
2228
                },
2229
                cli.UintFlag{
2230
                        Name: "index_offset",
2231
                        Usage: "the index of a transaction that will be " +
2232
                                "used in a query to determine which " +
2233
                                "transaction should be returned in the " +
2234
                                "response",
2235
                },
2236
                cli.IntFlag{
2237
                        Name: "max_transactions",
2238
                        Usage: "(optional) the max number of transactions to " +
2239
                                "return; leave at default of 0 to return " +
2240
                                "all transactions",
2241
                        Value: 0,
2242
                },
2243
        },
2244
        Description: `
2245
        List all transactions an address of the wallet was involved in.
2246

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

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

×
2265
        req := &lnrpc.GetTransactionsRequest{
×
2266
                IndexOffset:     uint32(ctx.Uint64("index_offset")),
×
2267
                MaxTransactions: uint32(ctx.Uint64("max_transactions")),
×
2268
        }
×
2269

×
2270
        if ctx.IsSet("start_height") {
×
2271
                req.StartHeight = int32(ctx.Int64("start_height"))
×
2272
        }
×
2273
        if ctx.IsSet("end_height") {
×
2274
                req.EndHeight = int32(ctx.Int64("end_height"))
×
2275
        }
×
2276

2277
        resp, err := client.GetTransactions(ctxc, req)
×
2278
        if err != nil {
×
2279
                return err
×
2280
        }
×
2281

2282
        printRespJSON(resp)
×
2283
        return nil
×
2284
}
2285

2286
var stopCommand = cli.Command{
2287
        Name:  "stop",
2288
        Usage: "Stop and shutdown the daemon.",
2289
        Description: `
2290
        Gracefully stop all daemon subsystems before stopping the daemon itself.
2291
        This is equivalent to stopping it using CTRL-C.`,
2292
        Action: actionDecorator(stopDaemon),
2293
}
2294

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

×
2300
        resp, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2301
        if err != nil {
×
2302
                return err
×
2303
        }
×
2304

2305
        printRespJSON(resp)
×
2306

×
2307
        return nil
×
2308
}
2309

2310
var signMessageCommand = cli.Command{
2311
        Name:      "signmessage",
2312
        Category:  "Wallet",
2313
        Usage:     "Sign a message with the node's private key.",
2314
        ArgsUsage: "msg",
2315
        Description: `
2316
        Sign msg with the resident node's private key.
2317
        Returns the signature as a zbase32 string.
2318

2319
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2320
        Flags: []cli.Flag{
2321
                cli.StringFlag{
2322
                        Name:  "msg",
2323
                        Usage: "the message to sign",
2324
                },
2325
        },
2326
        Action: actionDecorator(signMessage),
2327
}
2328

2329
func signMessage(ctx *cli.Context) error {
×
2330
        ctxc := getContext()
×
2331
        client, cleanUp := getClient(ctx)
×
2332
        defer cleanUp()
×
2333

×
2334
        var msg []byte
×
2335

×
2336
        switch {
×
2337
        case ctx.IsSet("msg"):
×
2338
                msg = []byte(ctx.String("msg"))
×
2339
        case ctx.Args().Present():
×
2340
                msg = []byte(ctx.Args().First())
×
2341
        default:
×
2342
                return fmt.Errorf("msg argument missing")
×
2343
        }
2344

2345
        resp, err := client.SignMessage(ctxc, &lnrpc.SignMessageRequest{Msg: msg})
×
2346
        if err != nil {
×
2347
                return err
×
2348
        }
×
2349

2350
        printRespJSON(resp)
×
2351
        return nil
×
2352
}
2353

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

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

2378
func verifyMessage(ctx *cli.Context) error {
×
2379
        ctxc := getContext()
×
2380
        client, cleanUp := getClient(ctx)
×
2381
        defer cleanUp()
×
2382

×
2383
        var (
×
2384
                msg []byte
×
2385
                sig string
×
2386
        )
×
2387

×
2388
        args := ctx.Args()
×
2389

×
2390
        switch {
×
2391
        case ctx.IsSet("msg"):
×
2392
                msg = []byte(ctx.String("msg"))
×
2393
        case args.Present():
×
2394
                msg = []byte(ctx.Args().First())
×
2395
                args = args.Tail()
×
2396
        default:
×
2397
                return fmt.Errorf("msg argument missing")
×
2398
        }
2399

2400
        switch {
×
2401
        case ctx.IsSet("sig"):
×
2402
                sig = ctx.String("sig")
×
2403
        case args.Present():
×
2404
                sig = args.First()
×
2405
        default:
×
2406
                return fmt.Errorf("signature argument missing")
×
2407
        }
2408

2409
        req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
×
2410
        resp, err := client.VerifyMessage(ctxc, req)
×
2411
        if err != nil {
×
2412
                return err
×
2413
        }
×
2414

2415
        printRespJSON(resp)
×
2416
        return nil
×
2417
}
2418

2419
var feeReportCommand = cli.Command{
2420
        Name:     "feereport",
2421
        Category: "Channels",
2422
        Usage:    "Display the current fee policies of all active channels.",
2423
        Description: `
2424
        Returns the current fee policies of all active channels.
2425
        Fee policies can be updated using the updatechanpolicy command.`,
2426
        Action: actionDecorator(feeReport),
2427
}
2428

2429
func feeReport(ctx *cli.Context) error {
×
2430
        ctxc := getContext()
×
2431
        client, cleanUp := getClient(ctx)
×
2432
        defer cleanUp()
×
2433

×
2434
        req := &lnrpc.FeeReportRequest{}
×
2435
        resp, err := client.FeeReport(ctxc, req)
×
2436
        if err != nil {
×
2437
                return err
×
2438
        }
×
2439

2440
        printRespJSON(resp)
×
2441
        return nil
×
2442
}
2443

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

2550
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
7✔
2551
        split := strings.Split(s, ":")
7✔
2552
        if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
10✔
2553
                return nil, errBadChanPoint
3✔
2554
        }
3✔
2555

2556
        index, err := strconv.ParseInt(split[1], 10, 64)
4✔
2557
        if err != nil {
5✔
2558
                return nil, fmt.Errorf("unable to decode output index: %w", err)
1✔
2559
        }
1✔
2560

2561
        txid, err := chainhash.NewHashFromStr(split[0])
3✔
2562
        if err != nil {
4✔
2563
                return nil, fmt.Errorf("unable to parse hex string: %w", err)
1✔
2564
        }
1✔
2565

2566
        return &lnrpc.ChannelPoint{
2✔
2567
                FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
2✔
2568
                        FundingTxidBytes: txid[:],
2✔
2569
                },
2✔
2570
                OutputIndex: uint32(index),
2✔
2571
        }, nil
2✔
2572
}
2573

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

2583
        if timeLockDeltaUnCheck > routing.MaxCLTVDelta {
3✔
2584
                return 0, fmt.Errorf("time_lock_delta is too big, "+
×
2585
                        "max value is %d", routing.MaxCLTVDelta)
×
2586
        }
×
2587

2588
        return uint16(timeLockDeltaUnCheck), nil
3✔
2589
}
2590

2591
func updateChannelPolicy(ctx *cli.Context) error {
×
2592
        ctxc := getContext()
×
2593
        client, cleanUp := getClient(ctx)
×
2594
        defer cleanUp()
×
2595

×
2596
        var (
×
2597
                baseFee       int64
×
2598
                feeRate       float64
×
2599
                feeRatePpm    uint64
×
2600
                timeLockDelta uint16
×
2601
                err           error
×
2602
        )
×
2603
        args := ctx.Args()
×
2604

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

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

2632
                args = args.Tail()
×
2633
        default:
×
2634
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
×
2635
        }
2636

2637
        switch {
×
2638
        case ctx.IsSet("time_lock_delta"):
×
2639
                timeLockDeltaStr := ctx.String("time_lock_delta")
×
2640
                timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
×
2641
                if err != nil {
×
2642
                        return err
×
2643
                }
×
2644
        case args.Present():
×
2645
                timeLockDelta, err = parseTimeLockDelta(args.First())
×
2646
                if err != nil {
×
2647
                        return err
×
2648
                }
×
2649

2650
                args = args.Tail()
×
2651
        default:
×
2652
                return fmt.Errorf("time_lock_delta argument missing")
×
2653
        }
2654

2655
        var (
×
2656
                chanPoint    *lnrpc.ChannelPoint
×
2657
                chanPointStr string
×
2658
        )
×
2659

×
2660
        switch {
×
2661
        case ctx.IsSet("chan_point"):
×
2662
                chanPointStr = ctx.String("chan_point")
×
2663
        case args.Present():
×
2664
                chanPointStr = args.First()
×
2665
        }
2666

2667
        if chanPointStr != "" {
×
2668
                chanPoint, err = parseChanPoint(chanPointStr)
×
2669
                if err != nil {
×
2670
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2671
                }
×
2672
        }
2673

2674
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2675
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2676
                inboundBaseFeeMsat > math.MaxInt32 {
×
2677

×
2678
                return errors.New("inbound_base_fee_msat out of range")
×
2679
        }
×
2680

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

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

2688
        // Inbound fees are optional. However, if an update is required,
2689
        // both the base fee and the fee rate must be provided.
2690
        var inboundFee *lnrpc.InboundFee
×
2691
        if ctx.IsSet("inbound_base_fee_msat") !=
×
2692
                ctx.IsSet("inbound_fee_rate_ppm") {
×
2693

×
2694
                return errors.New("both parameters must be provided: " +
×
2695
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
×
2696
        }
×
2697

2698
        if ctx.IsSet("inbound_fee_rate_ppm") {
×
2699
                inboundFee = &lnrpc.InboundFee{
×
2700
                        BaseFeeMsat: int32(inboundBaseFeeMsat),
×
2701
                        FeeRatePpm:  int32(inboundFeeRatePpm),
×
2702
                }
×
2703
        }
×
2704

2705
        createMissingEdge := ctx.Bool("create_missing_edge")
×
2706

×
2707
        req := &lnrpc.PolicyUpdateRequest{
×
2708
                BaseFeeMsat:       baseFee,
×
2709
                TimeLockDelta:     uint32(timeLockDelta),
×
2710
                MaxHtlcMsat:       ctx.Uint64("max_htlc_msat"),
×
2711
                InboundFee:        inboundFee,
×
2712
                CreateMissingEdge: createMissingEdge,
×
2713
        }
×
2714

×
2715
        if ctx.IsSet("min_htlc_msat") {
×
2716
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2717
                req.MinHtlcMsatSpecified = true
×
2718
        }
×
2719

2720
        if chanPoint != nil {
×
2721
                req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
×
2722
                        ChanPoint: chanPoint,
×
2723
                }
×
2724
        } else {
×
2725
                req.Scope = &lnrpc.PolicyUpdateRequest_Global{
×
2726
                        Global: true,
×
2727
                }
×
2728
        }
×
2729

2730
        if feeRate != 0 {
×
2731
                req.FeeRate = feeRate
×
2732
        } else if feeRatePpm != 0 {
×
2733
                req.FeeRatePpm = uint32(feeRatePpm)
×
2734
        }
×
2735

2736
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2737
        if err != nil {
×
2738
                return err
×
2739
        }
×
2740

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

2755
        printJSON(listFailedUpdateResp)
×
2756

×
2757
        return nil
×
2758
}
2759

2760
var fishCompletionCommand = cli.Command{
2761
        Name:   "fish-completion",
2762
        Hidden: true,
2763
        Action: func(c *cli.Context) error {
×
2764
                completion, err := c.App.ToFishCompletion()
×
2765
                if err != nil {
×
2766
                        return err
×
2767
                }
×
2768

2769
                // We don't want to suggest files, so we add this
2770
                // first line to the completions.
2771
                _, err = fmt.Printf("complete -c %q -f \n%s", c.App.Name, completion)
×
2772
                return err
×
2773
        },
2774
}
2775

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

2791
        This command will return one of two types of channel backups depending
2792
        on the set of passed arguments:
2793

2794
           * If a target channel point is specified, then a single channel
2795
             backup containing only the information for that channel will be
2796
             returned.
2797

2798
           * If the --all flag is passed, then a multi-channel backup will be
2799
             returned. A multi backup is a single encrypted blob (displayed in
2800
             hex encoding) that contains several channels in a single cipher
2801
             text.
2802

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

2829
func exportChanBackup(ctx *cli.Context) error {
×
2830
        ctxc := getContext()
×
2831
        client, cleanUp := getClient(ctx)
×
2832
        defer cleanUp()
×
2833

×
2834
        // Show command help if no arguments provided
×
2835
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2836
                cli.ShowCommandHelp(ctx, "exportchanbackup")
×
2837
                return nil
×
2838
        }
×
2839

2840
        var (
×
2841
                err            error
×
2842
                chanPointStr   string
×
2843
                outputFileName string
×
2844
        )
×
2845
        args := ctx.Args()
×
2846

×
2847
        switch {
×
2848
        case ctx.IsSet("chan_point"):
×
2849
                chanPointStr = ctx.String("chan_point")
×
2850

2851
        case args.Present():
×
2852
                chanPointStr = args.First()
×
2853

2854
        case !ctx.IsSet("all"):
×
2855
                return fmt.Errorf("must specify chan_point if --all isn't set")
×
2856
        }
2857

2858
        if ctx.IsSet("output_file") {
×
2859
                outputFileName = ctx.String("output_file")
×
2860
        }
×
2861

2862
        if chanPointStr != "" {
×
2863
                chanPointRPC, err := parseChanPoint(chanPointStr)
×
2864
                if err != nil {
×
2865
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2866
                }
×
2867

2868
                chanBackup, err := client.ExportChannelBackup(
×
2869
                        ctxc, &lnrpc.ExportChannelBackupRequest{
×
2870
                                ChanPoint: chanPointRPC,
×
2871
                        },
×
2872
                )
×
2873
                if err != nil {
×
2874
                        return err
×
2875
                }
×
2876

2877
                txid, err := chainhash.NewHash(
×
2878
                        chanPointRPC.GetFundingTxidBytes(),
×
2879
                )
×
2880
                if err != nil {
×
2881
                        return err
×
2882
                }
×
2883

2884
                chanPoint := wire.OutPoint{
×
2885
                        Hash:  *txid,
×
2886
                        Index: chanPointRPC.OutputIndex,
×
2887
                }
×
2888

×
2889
                if outputFileName != "" {
×
2890
                        return os.WriteFile(
×
2891
                                outputFileName,
×
2892
                                chanBackup.ChanBackup,
×
2893
                                0666,
×
2894
                        )
×
2895
                }
×
2896

2897
                printJSON(struct {
×
2898
                        ChanPoint  string `json:"chan_point"`
×
2899
                        ChanBackup string `json:"chan_backup"`
×
2900
                }{
×
2901
                        ChanPoint:  chanPoint.String(),
×
2902
                        ChanBackup: hex.EncodeToString(chanBackup.ChanBackup),
×
2903
                })
×
2904
                return nil
×
2905
        }
2906

2907
        if !ctx.IsSet("all") {
×
2908
                return fmt.Errorf("if a channel isn't specified, -all must be")
×
2909
        }
×
2910

2911
        chanBackup, err := client.ExportAllChannelBackups(
×
2912
                ctxc, &lnrpc.ChanBackupExportRequest{},
×
2913
        )
×
2914
        if err != nil {
×
2915
                return err
×
2916
        }
×
2917

2918
        if outputFileName != "" {
×
2919
                return os.WriteFile(
×
2920
                        outputFileName,
×
2921
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2922
                        0666,
×
2923
                )
×
2924
        }
×
2925

2926
        // TODO(roasbeef): support for export | restore ?
2927

2928
        var chanPoints []string
×
2929
        for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
×
2930
                txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
×
2931
                if err != nil {
×
2932
                        return err
×
2933
                }
×
2934

2935
                chanPoints = append(chanPoints, wire.OutPoint{
×
2936
                        Hash:  *txid,
×
2937
                        Index: chanPoint.OutputIndex,
×
2938
                }.String())
×
2939
        }
2940

2941
        printRespJSON(chanBackup)
×
2942

×
2943
        return nil
×
2944
}
2945

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

2956
    The command will accept backups in one of four forms:
2957

2958
       * A single channel packed SCB, which can be obtained from
2959
         exportchanbackup. This should be passed in hex encoded format.
2960

2961
       * A packed multi-channel SCB, which couples several individual
2962
         static channel backups in single blob.
2963

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

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

2983
                cli.StringFlag{
2984
                        Name:      "single_file",
2985
                        Usage:     "the path to a single-channel backup file",
2986
                        TakesFile: true,
2987
                },
2988

2989
                cli.StringFlag{
2990
                        Name:      "multi_file",
2991
                        Usage:     "the path to a multi-channel back up file",
2992
                        TakesFile: true,
2993
                },
2994
        },
2995
        Action: actionDecorator(verifyChanBackup),
2996
}
2997

2998
func verifyChanBackup(ctx *cli.Context) error {
×
2999
        ctxc := getContext()
×
3000
        client, cleanUp := getClient(ctx)
×
3001
        defer cleanUp()
×
3002

×
3003
        // Show command help if no arguments provided
×
3004
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3005
                cli.ShowCommandHelp(ctx, "verifychanbackup")
×
3006
                return nil
×
3007
        }
×
3008

3009
        backups, err := parseChanBackups(ctx)
×
3010
        if err != nil {
×
3011
                return err
×
3012
        }
×
3013

3014
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
3015

×
3016
        if backups.GetChanBackups() != nil {
×
3017
                verifyReq.SingleChanBackups = backups.GetChanBackups()
×
3018
        }
×
3019
        if backups.GetMultiChanBackup() != nil {
×
3020
                verifyReq.MultiChanBackup = &lnrpc.MultiChanBackup{
×
3021
                        MultiChanBackup: backups.GetMultiChanBackup(),
×
3022
                }
×
3023
        }
×
3024

3025
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
3026
        if err != nil {
×
3027
                return err
×
3028
        }
×
3029

3030
        printRespJSON(resp)
×
3031
        return nil
×
3032
}
3033

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

3049
        The command will accept backups in one of four forms:
3050

3051
           * A single channel packed SCB, which can be obtained from
3052
             exportchanbackup. This should be passed in hex encoded format.
3053

3054
           * A packed multi-channel SCB, which couples several individual
3055
             static channel backups in single blob.
3056

3057
           * A file path which points to a packed single-channel backup within
3058
             a file, using the same format that lnd does in its channel.backup
3059
             file.
3060

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

3077
                cli.StringFlag{
3078
                        Name:      "single_file",
3079
                        Usage:     "the path to a single-channel backup file",
3080
                        TakesFile: true,
3081
                },
3082

3083
                cli.StringFlag{
3084
                        Name:      "multi_file",
3085
                        Usage:     "the path to a multi-channel back up file",
3086
                        TakesFile: true,
3087
                },
3088
        },
3089
        Action: actionDecorator(restoreChanBackup),
3090
}
3091

3092
// errMissingChanBackup is an error returned when we attempt to parse a channel
3093
// backup from a CLI command, and it is missing.
3094
var errMissingChanBackup = errors.New("missing channel backup")
3095

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

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

3119
        case ctx.IsSet("multi_backup"):
×
3120
                packedMulti, err := hex.DecodeString(
×
3121
                        ctx.String("multi_backup"),
×
3122
                )
×
3123
                if err != nil {
×
3124
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3125
                                "backup: %v", err)
×
3126
                }
×
3127

3128
                return &lnrpc.RestoreChanBackupRequest{
×
3129
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3130
                                MultiChanBackup: packedMulti,
×
3131
                        },
×
3132
                }, nil
×
3133

3134
        case ctx.IsSet("single_file"):
×
3135
                packedSingle, err := os.ReadFile(ctx.String("single_file"))
×
3136
                if err != nil {
×
3137
                        return nil, fmt.Errorf("unable to decode single "+
×
3138
                                "packed backup: %v", err)
×
3139
                }
×
3140

3141
                return &lnrpc.RestoreChanBackupRequest{
×
3142
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3143
                                ChanBackups: &lnrpc.ChannelBackups{
×
3144
                                        ChanBackups: []*lnrpc.ChannelBackup{{
×
3145
                                                ChanBackup: packedSingle,
×
3146
                                        }},
×
3147
                                },
×
3148
                        },
×
3149
                }, nil
×
3150

3151
        case ctx.IsSet("multi_file"):
×
3152
                packedMulti, err := os.ReadFile(ctx.String("multi_file"))
×
3153
                if err != nil {
×
3154
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3155
                                "backup: %v", err)
×
3156
                }
×
3157

3158
                return &lnrpc.RestoreChanBackupRequest{
×
3159
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3160
                                MultiChanBackup: packedMulti,
×
3161
                        },
×
3162
                }, nil
×
3163

3164
        default:
×
3165
                return nil, errMissingChanBackup
×
3166
        }
3167
}
3168

3169
func restoreChanBackup(ctx *cli.Context) error {
×
3170
        ctxc := getContext()
×
3171
        client, cleanUp := getClient(ctx)
×
3172
        defer cleanUp()
×
3173

×
3174
        // Show command help if no arguments provided
×
3175
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3176
                cli.ShowCommandHelp(ctx, "restorechanbackup")
×
3177
                return nil
×
3178
        }
×
3179

3180
        var req lnrpc.RestoreChanBackupRequest
×
3181

×
3182
        backups, err := parseChanBackups(ctx)
×
3183
        if err != nil {
×
3184
                return err
×
3185
        }
×
3186

3187
        req.Backup = backups.Backup
×
3188

×
3189
        resp, err := client.RestoreChannelBackups(ctxc, &req)
×
3190
        if err != nil {
×
3191
                return fmt.Errorf("unable to restore chan backups: %w", err)
×
3192
        }
×
3193

3194
        printRespJSON(resp)
×
3195

×
3196
        return nil
×
3197
}
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