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

lightningnetwork / lnd / 15766512715

19 Jun 2025 09:21PM UTC coverage: 68.121% (-0.02%) from 68.141%
15766512715

Pull #9950

github

web-flow
Merge 409bf3c6c into 40efefeb6
Pull Request #9950: lnrpc: add auth_proof to graph APIs

6 of 27 new or added lines in 2 files covered. (22.22%)

75 existing lines in 19 files now uncovered.

134441 of 197356 relevant lines covered (68.12%)

22245.69 hits per line

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

8.25
/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/rpcperms"
28
        "github.com/lightningnetwork/lnd/signal"
29
        "github.com/urfave/cli"
30
        "golang.org/x/term"
31
        "google.golang.org/grpc/codes"
32
        "google.golang.org/grpc/status"
33
        "google.golang.org/protobuf/proto"
34
)
35

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

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

41
const defaultRecoveryWindow int32 = 2500
42

43
const (
44
        defaultUtxoMinConf = 1
45
)
46

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
281
                        return errors.New("wallet is already unlocked")
×
282
                }
×
283

284
                // lnd might be active, but not possible to contact using RPC if
285
                // the wallet is encrypted.
286
                if s.Code() == codes.Unknown && strings.Contains(
×
287
                        s.Message(), rpcperms.ErrWalletLocked.Error(),
×
288
                ) {
×
289

×
290
                        return errors.New("wallet is encrypted - please " +
×
291
                                "unlock using 'lncli unlock', or set " +
×
292
                                "password using 'lncli create' if this is " +
×
293
                                "the first time starting lnd")
×
294
                }
×
295

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

796
        printJSON(listUnspentResp)
×
797

×
798
        return nil
×
799
}
800

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
1169
        return nil
×
1170
}
1171

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1324
        var channelsToClose []*lnrpc.Channel
×
1325

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

×
1332
                confirmed := promptForConfirmation(msg)
×
1333

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

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

×
1354
                confirmed := promptForConfirmation(msg)
×
1355

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

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

×
1377
                                confirmed := promptForConfirmation(msg)
×
1378

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

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

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

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

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

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

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

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

1462
        return nil
×
1463
}
1464

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1612
        return channelPoint, nil
×
1613
}
1614

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1734
        return nil
×
1735
}
1736

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

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

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

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

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

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

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

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

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

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

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

1807
        printRespJSON(resp)
×
1808

×
1809
        return nil
×
1810
}
1811

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

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

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

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

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

1868
        printRespJSON(resp)
×
1869

×
1870
        return nil
×
1871
}
1872

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

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

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

1889
                peerKey = pk[:]
×
1890
        }
1891

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

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

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

1910
        printModifiedProtoJSON(resp)
×
1911

×
1912
        return nil
×
1913
}
1914

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

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

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

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

1972
        printModifiedProtoJSON(resp)
×
1973

×
1974
        return nil
×
1975
}
1976

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

1999
func describeGraph(ctx *cli.Context) error {
×
2000
        ctxc := getContext()
×
2001
        client, cleanUp := getClient(ctx)
×
2002
        defer cleanUp()
×
2003

×
2004
        req := &lnrpc.ChannelGraphRequest{
×
2005
                IncludeUnannounced: ctx.Bool("include_unannounced"),
×
NEW
2006
                IncludeAuthProof:   ctx.Bool("include_auth_proof"),
×
2007
        }
×
2008

×
2009
        graph, err := client.DescribeGraph(ctxc, req)
×
2010
        if err != nil {
×
2011
                return err
×
2012
        }
×
2013

2014
        printRespJSON(graph)
×
2015
        return nil
×
2016
}
2017

2018
var getNodeMetricsCommand = cli.Command{
2019
        Name:        "getnodemetrics",
2020
        Category:    "Graph",
2021
        Description: "Prints out node metrics calculated from the current graph",
2022
        Usage:       "Get node metrics.",
2023
        Action:      actionDecorator(getNodeMetrics),
2024
}
2025

2026
func getNodeMetrics(ctx *cli.Context) error {
×
2027
        ctxc := getContext()
×
2028
        client, cleanUp := getClient(ctx)
×
2029
        defer cleanUp()
×
2030

×
2031
        req := &lnrpc.NodeMetricsRequest{
×
2032
                Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
×
2033
        }
×
2034

×
2035
        nodeMetrics, err := client.GetNodeMetrics(ctxc, req)
×
2036
        if err != nil {
×
2037
                return err
×
2038
        }
×
2039

2040
        printRespJSON(nodeMetrics)
×
2041
        return nil
×
2042
}
2043

2044
var getChanInfoCommand = cli.Command{
2045
        Name:     "getchaninfo",
2046
        Category: "Graph",
2047
        Usage:    "Get the state of a channel.",
2048
        Description: "Prints out the latest authenticated state for a " +
2049
                "particular channel",
2050
        ArgsUsage: "chan_id",
2051
        Flags: []cli.Flag{
2052
                cli.Uint64Flag{
2053
                        Name: "chan_id",
2054
                        Usage: "The 8-byte compact channel ID to query for. " +
2055
                                "If this is set the chan_point param is " +
2056
                                "ignored.",
2057
                },
2058
                cli.StringFlag{
2059
                        Name: "chan_point",
2060
                        Usage: "The channel point in format txid:index. If " +
2061
                                "the chan_id param is set this param is " +
2062
                                "ignored.",
2063
                },
2064
                cli.BoolFlag{
2065
                        Name: "include_auth_proof",
2066
                        Usage: "If set, will include announcements' " +
2067
                                "signatures into ChannelEdge.",
2068
                },
2069
        },
2070
        Action: actionDecorator(getChanInfo),
2071
}
2072

2073
func getChanInfo(ctx *cli.Context) error {
×
2074
        ctxc := getContext()
×
2075
        client, cleanUp := getClient(ctx)
×
2076
        defer cleanUp()
×
2077

×
2078
        var (
×
2079
                chanID    uint64
×
2080
                chanPoint string
×
2081
                err       error
×
2082
        )
×
2083

×
2084
        switch {
×
2085
        case ctx.IsSet("chan_id"):
×
2086
                chanID = ctx.Uint64("chan_id")
×
2087

2088
        case ctx.Args().Present():
×
2089
                chanID, err = strconv.ParseUint(ctx.Args().First(), 10, 64)
×
2090
                if err != nil {
×
2091
                        return fmt.Errorf("error parsing chan_id: %w", err)
×
2092
                }
×
2093

2094
        case ctx.IsSet("chan_point"):
×
2095
                chanPoint = ctx.String("chan_point")
×
2096

2097
        default:
×
2098
                return fmt.Errorf("chan_id or chan_point argument missing")
×
2099
        }
2100

2101
        req := &lnrpc.ChanInfoRequest{
×
NEW
2102
                ChanId:           chanID,
×
NEW
2103
                ChanPoint:        chanPoint,
×
NEW
2104
                IncludeAuthProof: ctx.Bool("include_auth_proof"),
×
2105
        }
×
2106

×
2107
        chanInfo, err := client.GetChanInfo(ctxc, req)
×
2108
        if err != nil {
×
2109
                return err
×
2110
        }
×
2111

2112
        printRespJSON(chanInfo)
×
2113
        return nil
×
2114
}
2115

2116
var getNodeInfoCommand = cli.Command{
2117
        Name:     "getnodeinfo",
2118
        Category: "Graph",
2119
        Usage:    "Get information on a specific node.",
2120
        Description: "Prints out the latest authenticated node state for an " +
2121
                "advertised node",
2122
        Flags: []cli.Flag{
2123
                cli.StringFlag{
2124
                        Name: "pub_key",
2125
                        Usage: "the 33-byte hex-encoded compressed public of the target " +
2126
                                "node",
2127
                },
2128
                cli.BoolFlag{
2129
                        Name: "include_channels",
2130
                        Usage: "if true, will return all known channels " +
2131
                                "associated with the node",
2132
                },
2133
                cli.BoolFlag{
2134
                        Name: "include_auth_proof",
2135
                        Usage: "If set, will include announcements' " +
2136
                                "signatures into ChannelEdge. Depends on " +
2137
                                "include_channels",
2138
                },
2139
        },
2140
        Action: actionDecorator(getNodeInfo),
2141
}
2142

2143
func getNodeInfo(ctx *cli.Context) error {
×
2144
        ctxc := getContext()
×
2145
        client, cleanUp := getClient(ctx)
×
2146
        defer cleanUp()
×
2147

×
2148
        args := ctx.Args()
×
2149

×
2150
        var pubKey string
×
2151
        switch {
×
2152
        case ctx.IsSet("pub_key"):
×
2153
                pubKey = ctx.String("pub_key")
×
2154
        case args.Present():
×
2155
                pubKey = args.First()
×
2156
        default:
×
2157
                return fmt.Errorf("pub_key argument missing")
×
2158
        }
2159

2160
        req := &lnrpc.NodeInfoRequest{
×
NEW
2161
                PubKey:           pubKey,
×
NEW
2162
                IncludeChannels:  ctx.Bool("include_channels"),
×
NEW
2163
                IncludeAuthProof: ctx.Bool("include_auth_proof"),
×
2164
        }
×
2165

×
2166
        nodeInfo, err := client.GetNodeInfo(ctxc, req)
×
2167
        if err != nil {
×
2168
                return err
×
2169
        }
×
2170

2171
        printRespJSON(nodeInfo)
×
2172
        return nil
×
2173
}
2174

2175
var getNetworkInfoCommand = cli.Command{
2176
        Name:     "getnetworkinfo",
2177
        Category: "Channels",
2178
        Usage: "Get statistical information about the current " +
2179
                "state of the network.",
2180
        Description: "Returns a set of statistics pertaining to the known " +
2181
                "channel graph",
2182
        Action: actionDecorator(getNetworkInfo),
2183
}
2184

2185
func getNetworkInfo(ctx *cli.Context) error {
×
2186
        ctxc := getContext()
×
2187
        client, cleanUp := getClient(ctx)
×
2188
        defer cleanUp()
×
2189

×
2190
        req := &lnrpc.NetworkInfoRequest{}
×
2191

×
2192
        netInfo, err := client.GetNetworkInfo(ctxc, req)
×
2193
        if err != nil {
×
2194
                return err
×
2195
        }
×
2196

2197
        printRespJSON(netInfo)
×
2198
        return nil
×
2199
}
2200

2201
var debugLevelCommand = cli.Command{
2202
        Name:  "debuglevel",
2203
        Usage: "Set the debug level.",
2204
        Description: `Logging level for all subsystems {trace, debug, info, warn, error, critical, off}
2205
        You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems
2206

2207
        Use show to list available subsystems`,
2208
        Flags: []cli.Flag{
2209
                cli.BoolFlag{
2210
                        Name:  "show",
2211
                        Usage: "if true, then the list of available sub-systems will be printed out",
2212
                },
2213
                cli.StringFlag{
2214
                        Name:  "level",
2215
                        Usage: "the level specification to target either a coarse logging level, or granular set of specific sub-systems with logging levels for each",
2216
                },
2217
        },
2218
        Action: actionDecorator(debugLevel),
2219
}
2220

2221
func debugLevel(ctx *cli.Context) error {
×
2222
        ctxc := getContext()
×
2223
        client, cleanUp := getClient(ctx)
×
2224
        defer cleanUp()
×
2225
        req := &lnrpc.DebugLevelRequest{
×
2226
                Show:      ctx.Bool("show"),
×
2227
                LevelSpec: ctx.String("level"),
×
2228
        }
×
2229

×
2230
        resp, err := client.DebugLevel(ctxc, req)
×
2231
        if err != nil {
×
2232
                return err
×
2233
        }
×
2234

2235
        printRespJSON(resp)
×
2236
        return nil
×
2237
}
2238

2239
var listChainTxnsCommand = cli.Command{
2240
        Name:     "listchaintxns",
2241
        Category: "On-chain",
2242
        Usage:    "List transactions from the wallet.",
2243
        Flags: []cli.Flag{
2244
                cli.Int64Flag{
2245
                        Name: "start_height",
2246
                        Usage: "the block height from which to list " +
2247
                                "transactions, inclusive",
2248
                },
2249
                cli.Int64Flag{
2250
                        Name: "end_height",
2251
                        Usage: "the block height until which to list " +
2252
                                "transactions, inclusive; by default this " +
2253
                                "will return all transactions up to the " +
2254
                                "chain tip including unconfirmed " +
2255
                                "transactions",
2256
                        Value: -1,
2257
                },
2258
                cli.UintFlag{
2259
                        Name: "index_offset",
2260
                        Usage: "the index of a transaction that will be " +
2261
                                "used in a query to determine which " +
2262
                                "transaction should be returned in the " +
2263
                                "response",
2264
                },
2265
                cli.IntFlag{
2266
                        Name: "max_transactions",
2267
                        Usage: "the max number of transactions to " +
2268
                                "return; leave at default of 0 to return " +
2269
                                "all transactions",
2270
                        Value: 0,
2271
                },
2272
        },
2273
        Description: `
2274
        List all transactions an address of the wallet was involved in.
2275

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

2279
        By default, this call will get all transactions until the chain tip, 
2280
        including unconfirmed transactions (end_height=-1).`,
2281
        Action: actionDecorator(listChainTxns),
2282
}
2283

2284
func parseBlockHeightInputs(ctx *cli.Context) (int32, int32, error) {
7✔
2285
        startHeight := int32(ctx.Int64("start_height"))
7✔
2286
        endHeight := int32(ctx.Int64("end_height"))
7✔
2287

7✔
2288
        if ctx.IsSet("start_height") && ctx.IsSet("end_height") {
14✔
2289
                if endHeight != -1 && startHeight > endHeight {
8✔
2290
                        return startHeight, endHeight,
1✔
2291
                                errors.New("start_height should " +
1✔
2292
                                        "be less than end_height if " +
1✔
2293
                                        "end_height is not equal to -1")
1✔
2294
                }
1✔
2295
        }
2296

2297
        if startHeight < 0 {
7✔
2298
                return startHeight, endHeight,
1✔
2299
                        errors.New("start_height should " +
1✔
2300
                                "be greater than or " +
1✔
2301
                                "equal to 0")
1✔
2302
        }
1✔
2303

2304
        return startHeight, endHeight, nil
5✔
2305
}
2306

2307
func listChainTxns(ctx *cli.Context) error {
×
2308
        ctxc := getContext()
×
2309
        client, cleanUp := getClient(ctx)
×
2310
        defer cleanUp()
×
2311

×
2312
        startHeight, endHeight, err := parseBlockHeightInputs(ctx)
×
2313
        if err != nil {
×
2314
                return err
×
2315
        }
×
2316

2317
        req := &lnrpc.GetTransactionsRequest{
×
2318
                IndexOffset:     uint32(ctx.Uint64("index_offset")),
×
2319
                MaxTransactions: uint32(ctx.Uint64("max_transactions")),
×
2320
                StartHeight:     startHeight,
×
2321
                EndHeight:       endHeight,
×
2322
        }
×
2323

×
2324
        resp, err := client.GetTransactions(ctxc, req)
×
2325
        if err != nil {
×
2326
                return err
×
2327
        }
×
2328

2329
        printRespJSON(resp)
×
2330
        return nil
×
2331
}
2332

2333
var stopCommand = cli.Command{
2334
        Name:  "stop",
2335
        Usage: "Stop and shutdown the daemon.",
2336
        Description: `
2337
        Gracefully stop all daemon subsystems before stopping the daemon itself.
2338
        This is equivalent to stopping it using CTRL-C.`,
2339
        Action: actionDecorator(stopDaemon),
2340
}
2341

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

×
2347
        resp, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2348
        if err != nil {
×
2349
                return err
×
2350
        }
×
2351

2352
        printRespJSON(resp)
×
2353

×
2354
        return nil
×
2355
}
2356

2357
var signMessageCommand = cli.Command{
2358
        Name:      "signmessage",
2359
        Category:  "Wallet",
2360
        Usage:     "Sign a message with the node's private key.",
2361
        ArgsUsage: "msg",
2362
        Description: `
2363
        Sign msg with the resident node's private key.
2364
        Returns the signature as a zbase32 string.
2365

2366
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2367
        Flags: []cli.Flag{
2368
                cli.StringFlag{
2369
                        Name:  "msg",
2370
                        Usage: "the message to sign",
2371
                },
2372
        },
2373
        Action: actionDecorator(signMessage),
2374
}
2375

2376
func signMessage(ctx *cli.Context) error {
×
2377
        ctxc := getContext()
×
2378
        client, cleanUp := getClient(ctx)
×
2379
        defer cleanUp()
×
2380

×
2381
        var msg []byte
×
2382

×
2383
        switch {
×
2384
        case ctx.IsSet("msg"):
×
2385
                msg = []byte(ctx.String("msg"))
×
2386
        case ctx.Args().Present():
×
2387
                msg = []byte(ctx.Args().First())
×
2388
        default:
×
2389
                return fmt.Errorf("msg argument missing")
×
2390
        }
2391

2392
        resp, err := client.SignMessage(ctxc, &lnrpc.SignMessageRequest{Msg: msg})
×
2393
        if err != nil {
×
2394
                return err
×
2395
        }
×
2396

2397
        printRespJSON(resp)
×
2398
        return nil
×
2399
}
2400

2401
var verifyMessageCommand = cli.Command{
2402
        Name:      "verifymessage",
2403
        Category:  "Wallet",
2404
        Usage:     "Verify a message signed with the signature.",
2405
        ArgsUsage: "msg signature",
2406
        Description: `
2407
        Verify that the message was signed with a properly-formed signature
2408
        The signature must be zbase32 encoded and signed with the private key of
2409
        an active node in the resident node's channel database.
2410

2411
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2412
        Flags: []cli.Flag{
2413
                cli.StringFlag{
2414
                        Name:  "msg",
2415
                        Usage: "the message to verify",
2416
                },
2417
                cli.StringFlag{
2418
                        Name:  "sig",
2419
                        Usage: "the zbase32 encoded signature of the message",
2420
                },
2421
        },
2422
        Action: actionDecorator(verifyMessage),
2423
}
2424

2425
func verifyMessage(ctx *cli.Context) error {
×
2426
        ctxc := getContext()
×
2427
        client, cleanUp := getClient(ctx)
×
2428
        defer cleanUp()
×
2429

×
2430
        var (
×
2431
                msg []byte
×
2432
                sig string
×
2433
        )
×
2434

×
2435
        args := ctx.Args()
×
2436

×
2437
        switch {
×
2438
        case ctx.IsSet("msg"):
×
2439
                msg = []byte(ctx.String("msg"))
×
2440
        case args.Present():
×
2441
                msg = []byte(ctx.Args().First())
×
2442
                args = args.Tail()
×
2443
        default:
×
2444
                return fmt.Errorf("msg argument missing")
×
2445
        }
2446

2447
        switch {
×
2448
        case ctx.IsSet("sig"):
×
2449
                sig = ctx.String("sig")
×
2450
        case args.Present():
×
2451
                sig = args.First()
×
2452
        default:
×
2453
                return fmt.Errorf("signature argument missing")
×
2454
        }
2455

2456
        req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
×
2457
        resp, err := client.VerifyMessage(ctxc, req)
×
2458
        if err != nil {
×
2459
                return err
×
2460
        }
×
2461

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

2466
var feeReportCommand = cli.Command{
2467
        Name:     "feereport",
2468
        Category: "Channels",
2469
        Usage:    "Display the current fee policies of all active channels.",
2470
        Description: `
2471
        Returns the current fee policies of all active channels.
2472
        Fee policies can be updated using the updatechanpolicy command.`,
2473
        Action: actionDecorator(feeReport),
2474
}
2475

2476
func feeReport(ctx *cli.Context) error {
×
2477
        ctxc := getContext()
×
2478
        client, cleanUp := getClient(ctx)
×
2479
        defer cleanUp()
×
2480

×
2481
        req := &lnrpc.FeeReportRequest{}
×
2482
        resp, err := client.FeeReport(ctxc, req)
×
2483
        if err != nil {
×
2484
                return err
×
2485
        }
×
2486

2487
        printRespJSON(resp)
×
2488
        return nil
×
2489
}
2490

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

2597
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
7✔
2598
        split := strings.Split(s, ":")
7✔
2599
        if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
10✔
2600
                return nil, errBadChanPoint
3✔
2601
        }
3✔
2602

2603
        index, err := strconv.ParseInt(split[1], 10, 64)
4✔
2604
        if err != nil {
5✔
2605
                return nil, fmt.Errorf("unable to decode output index: %w", err)
1✔
2606
        }
1✔
2607

2608
        txid, err := chainhash.NewHashFromStr(split[0])
3✔
2609
        if err != nil {
4✔
2610
                return nil, fmt.Errorf("unable to parse hex string: %w", err)
1✔
2611
        }
1✔
2612

2613
        return &lnrpc.ChannelPoint{
2✔
2614
                FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
2✔
2615
                        FundingTxidBytes: txid[:],
2✔
2616
                },
2✔
2617
                OutputIndex: uint32(index),
2✔
2618
        }, nil
2✔
2619
}
2620

2621
// parseTimeLockDelta is expected to get a uint16 type of timeLockDelta. Its
2622
// maximum value is MaxTimeLockDelta.
2623
func parseTimeLockDelta(timeLockDeltaStr string) (uint16, error) {
5✔
2624
        timeLockDeltaUnCheck, err := strconv.ParseUint(timeLockDeltaStr, 10, 64)
5✔
2625
        if err != nil {
7✔
2626
                return 0, fmt.Errorf("failed to parse time_lock_delta: %s "+
2✔
2627
                        "to uint64, err: %v", timeLockDeltaStr, err)
2✔
2628
        }
2✔
2629

2630
        if timeLockDeltaUnCheck > routing.MaxCLTVDelta {
3✔
2631
                return 0, fmt.Errorf("time_lock_delta is too big, "+
×
2632
                        "max value is %d", routing.MaxCLTVDelta)
×
2633
        }
×
2634

2635
        return uint16(timeLockDeltaUnCheck), nil
3✔
2636
}
2637

2638
func updateChannelPolicy(ctx *cli.Context) error {
×
2639
        ctxc := getContext()
×
2640
        client, cleanUp := getClient(ctx)
×
2641
        defer cleanUp()
×
2642

×
2643
        var (
×
2644
                baseFee       int64
×
2645
                feeRate       float64
×
2646
                feeRatePpm    uint64
×
2647
                timeLockDelta uint16
×
2648
                err           error
×
2649
        )
×
2650
        args := ctx.Args()
×
2651

×
2652
        switch {
×
2653
        case ctx.IsSet("base_fee_msat"):
×
2654
                baseFee = ctx.Int64("base_fee_msat")
×
2655
        case args.Present():
×
2656
                baseFee, err = strconv.ParseInt(args.First(), 10, 64)
×
2657
                if err != nil {
×
2658
                        return fmt.Errorf("unable to decode base_fee_msat: %w",
×
2659
                                err)
×
2660
                }
×
2661
                args = args.Tail()
×
2662
        default:
×
2663
                return fmt.Errorf("base_fee_msat argument missing")
×
2664
        }
2665

2666
        switch {
×
2667
        case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
×
2668
                return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
×
2669
        case ctx.IsSet("fee_rate"):
×
2670
                feeRate = ctx.Float64("fee_rate")
×
2671
        case ctx.IsSet("fee_rate_ppm"):
×
2672
                feeRatePpm = ctx.Uint64("fee_rate_ppm")
×
2673
        case args.Present():
×
2674
                feeRate, err = strconv.ParseFloat(args.First(), 64)
×
2675
                if err != nil {
×
2676
                        return fmt.Errorf("unable to decode fee_rate: %w", err)
×
2677
                }
×
2678

2679
                args = args.Tail()
×
2680
        default:
×
2681
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
×
2682
        }
2683

2684
        switch {
×
2685
        case ctx.IsSet("time_lock_delta"):
×
2686
                timeLockDeltaStr := ctx.String("time_lock_delta")
×
2687
                timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
×
2688
                if err != nil {
×
2689
                        return err
×
2690
                }
×
2691
        case args.Present():
×
2692
                timeLockDelta, err = parseTimeLockDelta(args.First())
×
2693
                if err != nil {
×
2694
                        return err
×
2695
                }
×
2696

2697
                args = args.Tail()
×
2698
        default:
×
2699
                return fmt.Errorf("time_lock_delta argument missing")
×
2700
        }
2701

2702
        var (
×
2703
                chanPoint    *lnrpc.ChannelPoint
×
2704
                chanPointStr string
×
2705
        )
×
2706

×
2707
        switch {
×
2708
        case ctx.IsSet("chan_point"):
×
2709
                chanPointStr = ctx.String("chan_point")
×
2710
        case args.Present():
×
2711
                chanPointStr = args.First()
×
2712
        }
2713

2714
        if chanPointStr != "" {
×
2715
                chanPoint, err = parseChanPoint(chanPointStr)
×
2716
                if err != nil {
×
2717
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2718
                }
×
2719
        }
2720

2721
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2722
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2723
                inboundBaseFeeMsat > math.MaxInt32 {
×
2724

×
2725
                return errors.New("inbound_base_fee_msat out of range")
×
2726
        }
×
2727

2728
        inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
×
2729
        if inboundFeeRatePpm < math.MinInt32 ||
×
2730
                inboundFeeRatePpm > math.MaxInt32 {
×
2731

×
2732
                return errors.New("inbound_fee_rate_ppm out of range")
×
2733
        }
×
2734

2735
        // Inbound fees are optional. However, if an update is required,
2736
        // both the base fee and the fee rate must be provided.
2737
        var inboundFee *lnrpc.InboundFee
×
2738
        if ctx.IsSet("inbound_base_fee_msat") !=
×
2739
                ctx.IsSet("inbound_fee_rate_ppm") {
×
2740

×
2741
                return errors.New("both parameters must be provided: " +
×
2742
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
×
2743
        }
×
2744

2745
        if ctx.IsSet("inbound_fee_rate_ppm") {
×
2746
                inboundFee = &lnrpc.InboundFee{
×
2747
                        BaseFeeMsat: int32(inboundBaseFeeMsat),
×
2748
                        FeeRatePpm:  int32(inboundFeeRatePpm),
×
2749
                }
×
2750
        }
×
2751

2752
        createMissingEdge := ctx.Bool("create_missing_edge")
×
2753

×
2754
        req := &lnrpc.PolicyUpdateRequest{
×
2755
                BaseFeeMsat:       baseFee,
×
2756
                TimeLockDelta:     uint32(timeLockDelta),
×
2757
                MaxHtlcMsat:       ctx.Uint64("max_htlc_msat"),
×
2758
                InboundFee:        inboundFee,
×
2759
                CreateMissingEdge: createMissingEdge,
×
2760
        }
×
2761

×
2762
        if ctx.IsSet("min_htlc_msat") {
×
2763
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2764
                req.MinHtlcMsatSpecified = true
×
2765
        }
×
2766

2767
        if chanPoint != nil {
×
2768
                req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
×
2769
                        ChanPoint: chanPoint,
×
2770
                }
×
2771
        } else {
×
2772
                req.Scope = &lnrpc.PolicyUpdateRequest_Global{
×
2773
                        Global: true,
×
2774
                }
×
2775
        }
×
2776

2777
        if feeRate != 0 {
×
2778
                req.FeeRate = feeRate
×
2779
        } else if feeRatePpm != 0 {
×
2780
                req.FeeRatePpm = uint32(feeRatePpm)
×
2781
        }
×
2782

2783
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2784
        if err != nil {
×
2785
                return err
×
2786
        }
×
2787

2788
        // Parse the response into the final json object that will be printed
2789
        // to stdout. At the moment, this filters out the raw txid bytes from
2790
        // each failed update's outpoint and only prints the txid string.
2791
        var listFailedUpdateResp = struct {
×
2792
                FailedUpdates []*FailedUpdate `json:"failed_updates"`
×
2793
        }{
×
2794
                FailedUpdates: make([]*FailedUpdate, 0, len(resp.FailedUpdates)),
×
2795
        }
×
2796
        for _, protoUpdate := range resp.FailedUpdates {
×
2797
                failedUpdate := NewFailedUpdateFromProto(protoUpdate)
×
2798
                listFailedUpdateResp.FailedUpdates = append(
×
2799
                        listFailedUpdateResp.FailedUpdates, failedUpdate)
×
2800
        }
×
2801

2802
        printJSON(listFailedUpdateResp)
×
2803

×
2804
        return nil
×
2805
}
2806

2807
var fishCompletionCommand = cli.Command{
2808
        Name:   "fish-completion",
2809
        Hidden: true,
2810
        Action: func(c *cli.Context) error {
×
2811
                completion, err := c.App.ToFishCompletion()
×
2812
                if err != nil {
×
2813
                        return err
×
2814
                }
×
2815

2816
                // We don't want to suggest files, so we add this
2817
                // first line to the completions.
2818
                _, err = fmt.Printf("complete -c %q -f \n%s", c.App.Name, completion)
×
2819
                return err
×
2820
        },
2821
}
2822

2823
var exportChanBackupCommand = cli.Command{
2824
        Name:     "exportchanbackup",
2825
        Category: "Channels",
2826
        Usage: "Obtain a static channel back up for a selected channels, " +
2827
                "or all known channels.",
2828
        ArgsUsage: "[chan_point] [--all] [--output_file]",
2829
        Description: `
2830
        This command allows a user to export a Static Channel Backup (SCB) for
2831
        a selected channel. SCB's are encrypted backups of a channel's initial
2832
        state that are encrypted with a key derived from the seed of a user. In
2833
        the case of partial or complete data loss, the SCB will allow the user
2834
        to reclaim settled funds in the channel at its final state. The
2835
        exported channel backups can be restored at a later time using the
2836
        restorechanbackup command.
2837

2838
        This command will return one of two types of channel backups depending
2839
        on the set of passed arguments:
2840

2841
           * If a target channel point is specified, then a single channel
2842
             backup containing only the information for that channel will be
2843
             returned.
2844

2845
           * If the --all flag is passed, then a multi-channel backup will be
2846
             returned. A multi backup is a single encrypted blob (displayed in
2847
             hex encoding) that contains several channels in a single cipher
2848
             text.
2849

2850
        Both of the backup types can be restored using the restorechanbackup
2851
        command.
2852
        `,
2853
        Flags: []cli.Flag{
2854
                cli.StringFlag{
2855
                        Name:  "chan_point",
2856
                        Usage: "the target channel to obtain an SCB for",
2857
                },
2858
                cli.BoolFlag{
2859
                        Name: "all",
2860
                        Usage: "if specified, then a multi backup of all " +
2861
                                "active channels will be returned",
2862
                },
2863
                cli.StringFlag{
2864
                        Name: "output_file",
2865
                        Usage: `
2866
                        if specified, then rather than printing a JSON output
2867
                        of the static channel backup, a serialized version of
2868
                        the backup (either Single or Multi) will be written to
2869
                        the target file, this is the same format used by lnd in
2870
                        its channel.backup file `,
2871
                },
2872
        },
2873
        Action: actionDecorator(exportChanBackup),
2874
}
2875

2876
func exportChanBackup(ctx *cli.Context) error {
×
2877
        ctxc := getContext()
×
2878
        client, cleanUp := getClient(ctx)
×
2879
        defer cleanUp()
×
2880

×
2881
        // Show command help if no arguments provided
×
2882
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2883
                cli.ShowCommandHelp(ctx, "exportchanbackup")
×
2884
                return nil
×
2885
        }
×
2886

2887
        var (
×
2888
                err            error
×
2889
                chanPointStr   string
×
2890
                outputFileName string
×
2891
        )
×
2892
        args := ctx.Args()
×
2893

×
2894
        switch {
×
2895
        case ctx.IsSet("chan_point"):
×
2896
                chanPointStr = ctx.String("chan_point")
×
2897

2898
        case args.Present():
×
2899
                chanPointStr = args.First()
×
2900

2901
        case !ctx.IsSet("all"):
×
2902
                return fmt.Errorf("must specify chan_point if --all isn't set")
×
2903
        }
2904

2905
        if ctx.IsSet("output_file") {
×
2906
                outputFileName = ctx.String("output_file")
×
2907
        }
×
2908

2909
        if chanPointStr != "" {
×
2910
                chanPointRPC, err := parseChanPoint(chanPointStr)
×
2911
                if err != nil {
×
2912
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2913
                }
×
2914

2915
                chanBackup, err := client.ExportChannelBackup(
×
2916
                        ctxc, &lnrpc.ExportChannelBackupRequest{
×
2917
                                ChanPoint: chanPointRPC,
×
2918
                        },
×
2919
                )
×
2920
                if err != nil {
×
2921
                        return err
×
2922
                }
×
2923

2924
                txid, err := chainhash.NewHash(
×
2925
                        chanPointRPC.GetFundingTxidBytes(),
×
2926
                )
×
2927
                if err != nil {
×
2928
                        return err
×
2929
                }
×
2930

2931
                chanPoint := wire.OutPoint{
×
2932
                        Hash:  *txid,
×
2933
                        Index: chanPointRPC.OutputIndex,
×
2934
                }
×
2935

×
2936
                if outputFileName != "" {
×
2937
                        return os.WriteFile(
×
2938
                                outputFileName,
×
2939
                                chanBackup.ChanBackup,
×
2940
                                0666,
×
2941
                        )
×
2942
                }
×
2943

2944
                printJSON(struct {
×
2945
                        ChanPoint  string `json:"chan_point"`
×
2946
                        ChanBackup string `json:"chan_backup"`
×
2947
                }{
×
2948
                        ChanPoint:  chanPoint.String(),
×
2949
                        ChanBackup: hex.EncodeToString(chanBackup.ChanBackup),
×
2950
                })
×
2951
                return nil
×
2952
        }
2953

2954
        if !ctx.IsSet("all") {
×
2955
                return fmt.Errorf("if a channel isn't specified, -all must be")
×
2956
        }
×
2957

2958
        chanBackup, err := client.ExportAllChannelBackups(
×
2959
                ctxc, &lnrpc.ChanBackupExportRequest{},
×
2960
        )
×
2961
        if err != nil {
×
2962
                return err
×
2963
        }
×
2964

2965
        if outputFileName != "" {
×
2966
                return os.WriteFile(
×
2967
                        outputFileName,
×
2968
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2969
                        0666,
×
2970
                )
×
2971
        }
×
2972

2973
        // TODO(roasbeef): support for export | restore ?
2974

2975
        var chanPoints []string
×
2976
        for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
×
2977
                txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
×
2978
                if err != nil {
×
2979
                        return err
×
2980
                }
×
2981

2982
                chanPoints = append(chanPoints, wire.OutPoint{
×
2983
                        Hash:  *txid,
×
2984
                        Index: chanPoint.OutputIndex,
×
2985
                }.String())
×
2986
        }
2987

2988
        printRespJSON(chanBackup)
×
2989

×
2990
        return nil
×
2991
}
2992

2993
var verifyChanBackupCommand = cli.Command{
2994
        Name:      "verifychanbackup",
2995
        Category:  "Channels",
2996
        Usage:     "Verify an existing channel backup.",
2997
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file]",
2998
        Description: `
2999
    This command allows a user to verify an existing Single or Multi channel
3000
    backup for integrity. This is useful when a user has a backup, but is
3001
    unsure as to if it's valid or for the target node.
3002

3003
    The command will accept backups in one of four forms:
3004

3005
       * A single channel packed SCB, which can be obtained from
3006
         exportchanbackup. This should be passed in hex encoded format.
3007

3008
       * A packed multi-channel SCB, which couples several individual
3009
         static channel backups in single blob.
3010

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

3014
       * A file path which points to a packed multi-channel backup within a
3015
         file, using the same format that lnd does in its channel.backup
3016
         file.
3017
    `,
3018
        Flags: []cli.Flag{
3019
                cli.StringFlag{
3020
                        Name: "single_backup",
3021
                        Usage: "a hex encoded single channel backup obtained " +
3022
                                "from exportchanbackup",
3023
                },
3024
                cli.StringFlag{
3025
                        Name: "multi_backup",
3026
                        Usage: "a hex encoded multi-channel backup obtained " +
3027
                                "from exportchanbackup",
3028
                },
3029

3030
                cli.StringFlag{
3031
                        Name:      "single_file",
3032
                        Usage:     "the path to a single-channel backup file",
3033
                        TakesFile: true,
3034
                },
3035

3036
                cli.StringFlag{
3037
                        Name:      "multi_file",
3038
                        Usage:     "the path to a multi-channel back up file",
3039
                        TakesFile: true,
3040
                },
3041
        },
3042
        Action: actionDecorator(verifyChanBackup),
3043
}
3044

3045
func verifyChanBackup(ctx *cli.Context) error {
×
3046
        ctxc := getContext()
×
3047
        client, cleanUp := getClient(ctx)
×
3048
        defer cleanUp()
×
3049

×
3050
        // Show command help if no arguments provided
×
3051
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3052
                cli.ShowCommandHelp(ctx, "verifychanbackup")
×
3053
                return nil
×
3054
        }
×
3055

3056
        backups, err := parseChanBackups(ctx)
×
3057
        if err != nil {
×
3058
                return err
×
3059
        }
×
3060

3061
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
3062

×
3063
        if backups.GetChanBackups() != nil {
×
3064
                verifyReq.SingleChanBackups = backups.GetChanBackups()
×
3065
        }
×
3066
        if backups.GetMultiChanBackup() != nil {
×
3067
                verifyReq.MultiChanBackup = &lnrpc.MultiChanBackup{
×
3068
                        MultiChanBackup: backups.GetMultiChanBackup(),
×
3069
                }
×
3070
        }
×
3071

3072
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
3073
        if err != nil {
×
3074
                return err
×
3075
        }
×
3076

3077
        printRespJSON(resp)
×
3078
        return nil
×
3079
}
3080

3081
var restoreChanBackupCommand = cli.Command{
3082
        Name:     "restorechanbackup",
3083
        Category: "Channels",
3084
        Usage: "Restore an existing single or multi-channel static channel " +
3085
                "backup.",
3086
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file=",
3087
        Description: `
3088
        Allows a user to restore a Static Channel Backup (SCB) that was
3089
        obtained either via the exportchanbackup command, or from lnd's
3090
        automatically managed channel.backup file. This command should be used
3091
        if a user is attempting to restore a channel due to data loss on a
3092
        running node restored with the same seed as the node that created the
3093
        channel. If successful, this command will allows the user to recover
3094
        the settled funds stored in the recovered channels.
3095

3096
        The command will accept backups in one of four forms:
3097

3098
           * A single channel packed SCB, which can be obtained from
3099
             exportchanbackup. This should be passed in hex encoded format.
3100

3101
           * A packed multi-channel SCB, which couples several individual
3102
             static channel backups in single blob.
3103

3104
           * A file path which points to a packed single-channel backup within
3105
             a file, using the same format that lnd does in its channel.backup
3106
             file.
3107

3108
           * A file path which points to a packed multi-channel backup within a
3109
             file, using the same format that lnd does in its channel.backup
3110
             file.
3111
        `,
3112
        Flags: []cli.Flag{
3113
                cli.StringFlag{
3114
                        Name: "single_backup",
3115
                        Usage: "a hex encoded single channel backup obtained " +
3116
                                "from exportchanbackup",
3117
                },
3118
                cli.StringFlag{
3119
                        Name: "multi_backup",
3120
                        Usage: "a hex encoded multi-channel backup obtained " +
3121
                                "from exportchanbackup",
3122
                },
3123

3124
                cli.StringFlag{
3125
                        Name:      "single_file",
3126
                        Usage:     "the path to a single-channel backup file",
3127
                        TakesFile: true,
3128
                },
3129

3130
                cli.StringFlag{
3131
                        Name:      "multi_file",
3132
                        Usage:     "the path to a multi-channel back up file",
3133
                        TakesFile: true,
3134
                },
3135
        },
3136
        Action: actionDecorator(restoreChanBackup),
3137
}
3138

3139
// errMissingChanBackup is an error returned when we attempt to parse a channel
3140
// backup from a CLI command, and it is missing.
3141
var errMissingChanBackup = errors.New("missing channel backup")
3142

3143
func parseChanBackups(ctx *cli.Context) (*lnrpc.RestoreChanBackupRequest, error) {
×
3144
        switch {
×
3145
        case ctx.IsSet("single_backup"):
×
3146
                packedBackup, err := hex.DecodeString(
×
3147
                        ctx.String("single_backup"),
×
3148
                )
×
3149
                if err != nil {
×
3150
                        return nil, fmt.Errorf("unable to decode single packed "+
×
3151
                                "backup: %v", err)
×
3152
                }
×
3153

3154
                return &lnrpc.RestoreChanBackupRequest{
×
3155
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3156
                                ChanBackups: &lnrpc.ChannelBackups{
×
3157
                                        ChanBackups: []*lnrpc.ChannelBackup{
×
3158
                                                {
×
3159
                                                        ChanBackup: packedBackup,
×
3160
                                                },
×
3161
                                        },
×
3162
                                },
×
3163
                        },
×
3164
                }, nil
×
3165

3166
        case ctx.IsSet("multi_backup"):
×
3167
                packedMulti, err := hex.DecodeString(
×
3168
                        ctx.String("multi_backup"),
×
3169
                )
×
3170
                if err != nil {
×
3171
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3172
                                "backup: %v", err)
×
3173
                }
×
3174

3175
                return &lnrpc.RestoreChanBackupRequest{
×
3176
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3177
                                MultiChanBackup: packedMulti,
×
3178
                        },
×
3179
                }, nil
×
3180

3181
        case ctx.IsSet("single_file"):
×
3182
                packedSingle, err := os.ReadFile(ctx.String("single_file"))
×
3183
                if err != nil {
×
3184
                        return nil, fmt.Errorf("unable to decode single "+
×
3185
                                "packed backup: %v", err)
×
3186
                }
×
3187

3188
                return &lnrpc.RestoreChanBackupRequest{
×
3189
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3190
                                ChanBackups: &lnrpc.ChannelBackups{
×
3191
                                        ChanBackups: []*lnrpc.ChannelBackup{{
×
3192
                                                ChanBackup: packedSingle,
×
3193
                                        }},
×
3194
                                },
×
3195
                        },
×
3196
                }, nil
×
3197

3198
        case ctx.IsSet("multi_file"):
×
3199
                packedMulti, err := os.ReadFile(ctx.String("multi_file"))
×
3200
                if err != nil {
×
3201
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3202
                                "backup: %v", err)
×
3203
                }
×
3204

3205
                return &lnrpc.RestoreChanBackupRequest{
×
3206
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3207
                                MultiChanBackup: packedMulti,
×
3208
                        },
×
3209
                }, nil
×
3210

3211
        default:
×
3212
                return nil, errMissingChanBackup
×
3213
        }
3214
}
3215

3216
func restoreChanBackup(ctx *cli.Context) error {
×
3217
        ctxc := getContext()
×
3218
        client, cleanUp := getClient(ctx)
×
3219
        defer cleanUp()
×
3220

×
3221
        // Show command help if no arguments provided
×
3222
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3223
                cli.ShowCommandHelp(ctx, "restorechanbackup")
×
3224
                return nil
×
3225
        }
×
3226

3227
        var req lnrpc.RestoreChanBackupRequest
×
3228

×
3229
        backups, err := parseChanBackups(ctx)
×
3230
        if err != nil {
×
3231
                return err
×
3232
        }
×
3233

3234
        req.Backup = backups.Backup
×
3235

×
3236
        resp, err := client.RestoreChannelBackups(ctxc, &req)
×
3237
        if err != nil {
×
3238
                return fmt.Errorf("unable to restore chan backups: %w", err)
×
3239
        }
×
3240

3241
        printRespJSON(resp)
×
3242

×
3243
        return nil
×
3244
}
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