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

lightningnetwork / lnd / 18531626817

15 Oct 2025 02:05PM UTC coverage: 66.653% (+0.002%) from 66.651%
18531626817

Pull #10296

github

web-flow
Merge f2412f3e9 into 6ade31d05
Pull Request #10296: estimatefee: tx fee estimate for selected inputs

8 of 33 new or added lines in 2 files covered. (24.24%)

77 existing lines in 18 files now uncovered.

137263 of 205937 relevant lines covered (66.65%)

21343.2 hits per line

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

8.2
/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 {
83✔
254
        return func(c *cli.Context) error {
83✔
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
                cli.BoolFlag{
406
                        Name: "show_outpoints",
407
                        Usage: "(optional) if set, the outpoints that will " +
408
                                "be used for the transaction will be printed " +
409
                                "to the console",
410
                },
411
                cli.StringSliceFlag{
412
                        Name: "utxo",
413
                        Usage: "a utxo specified as outpoint(tx:idx) which " +
414
                                "will be used as input for the transaction " +
415
                                "to be estimated. This flag can be " +
416
                                "repeatedly used to specify multiple utxos " +
417
                                "as inputs.",
418
                },
419
        },
420
        Action: actionDecorator(estimateFees),
421
}
422

423
func estimateFees(ctx *cli.Context) error {
×
424
        ctxc := getContext()
×
425
        var amountToAddr map[string]int64
×
426

×
427
        jsonMap := ctx.Args().First()
×
428
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
429
                return err
×
430
        }
×
431

432
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
433
        if err != nil {
×
434
                return err
×
435
        }
×
436

437
        client, cleanUp := getClient(ctx)
×
438
        defer cleanUp()
×
439

×
NEW
440
        var outpoints []*lnrpc.OutPoint
×
NEW
441
        if ctx.IsSet("utxo") {
×
NEW
442
                utxos := ctx.StringSlice("utxo")
×
NEW
443

×
NEW
444
                outpoints, err = UtxosToOutpoints(utxos)
×
NEW
445
                if err != nil {
×
NEW
446
                        return fmt.Errorf("unable to decode utxos: %w", err)
×
NEW
447
                }
×
448

449
        }
450

451
        resp, err := client.EstimateFee(ctxc, &lnrpc.EstimateFeeRequest{
×
452
                AddrToAmount:          amountToAddr,
×
453
                TargetConf:            int32(ctx.Int64("conf_target")),
×
454
                CoinSelectionStrategy: coinSelectionStrategy,
×
NEW
455
                Outpoints:             outpoints,
×
NEW
456
                ShowOutpoints:         ctx.Bool("show_outpoints"),
×
457
        })
×
458
        if err != nil {
×
459
                return err
×
460
        }
×
461

462
        printRespJSON(resp)
×
463
        return nil
×
464
}
465

466
var txLabelFlag = cli.StringFlag{
467
        Name:  "label",
468
        Usage: "(optional) a label for the transaction",
469
}
470

471
var sendCoinsCommand = cli.Command{
472
        Name:      "sendcoins",
473
        Category:  "On-chain",
474
        Usage:     "Send bitcoin on-chain to an address.",
475
        ArgsUsage: "addr amt",
476
        Description: `
477
        Send amt coins in satoshis to the base58 or bech32 encoded bitcoin address addr.
478

479
        Fees used when sending the transaction can be specified via the --conf_target, or
480
        --sat_per_vbyte optional flags.
481

482
        Positional arguments and flags can be used interchangeably but not at the same time!
483
        `,
484
        Flags: []cli.Flag{
485
                cli.StringFlag{
486
                        Name: "addr",
487
                        Usage: "the base58 or bech32 encoded bitcoin address to send coins " +
488
                                "to on-chain",
489
                },
490
                cli.BoolFlag{
491
                        Name: "sweepall",
492
                        Usage: "if set, then the amount field should be " +
493
                                "unset. This indicates that the wallet will " +
494
                                "attempt to sweep all outputs within the " +
495
                                "wallet or all funds in select utxos (when " +
496
                                "supplied) to the target address",
497
                },
498
                cli.Int64Flag{
499
                        Name:  "amt",
500
                        Usage: "the number of bitcoin denominated in satoshis to send",
501
                },
502
                cli.Int64Flag{
503
                        Name: "conf_target",
504
                        Usage: "(optional) the number of blocks that the " +
505
                                "transaction *should* confirm in, will be " +
506
                                "used for fee estimation",
507
                },
508
                cli.Int64Flag{
509
                        Name:   "sat_per_byte",
510
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
511
                        Hidden: true,
512
                },
513
                cli.Int64Flag{
514
                        Name: "sat_per_vbyte",
515
                        Usage: "(optional) a manual fee expressed in " +
516
                                "sat/vbyte that should be used when crafting " +
517
                                "the transaction",
518
                },
519
                cli.Uint64Flag{
520
                        Name: "min_confs",
521
                        Usage: "(optional) the minimum number of confirmations " +
522
                                "each one of your outputs used for the transaction " +
523
                                "must satisfy",
524
                        Value: defaultUtxoMinConf,
525
                },
526
                cli.BoolFlag{
527
                        Name: "force, f",
528
                        Usage: "if set, the transaction will be broadcast " +
529
                                "without asking for confirmation; this is " +
530
                                "set to true by default if stdout is not a " +
531
                                "terminal avoid breaking existing shell " +
532
                                "scripts",
533
                },
534
                coinSelectionStrategyFlag,
535
                cli.StringSliceFlag{
536
                        Name: "utxo",
537
                        Usage: "a utxo specified as outpoint(tx:idx) which " +
538
                                "will be used as input for the transaction. " +
539
                                "This flag can be repeatedly used to specify " +
540
                                "multiple utxos as inputs. The selected " +
541
                                "utxos can either be entirely spent by " +
542
                                "specifying the sweepall flag or a specified " +
543
                                "amount can be spent in the utxos through " +
544
                                "the amt flag",
545
                },
546
                txLabelFlag,
547
        },
548
        Action: actionDecorator(sendCoins),
549
}
550

551
func sendCoins(ctx *cli.Context) error {
×
552
        var (
×
553
                addr      string
×
554
                amt       int64
×
555
                err       error
×
556
                outpoints []*lnrpc.OutPoint
×
557
        )
×
558
        ctxc := getContext()
×
559
        args := ctx.Args()
×
560

×
561
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
562
                cli.ShowCommandHelp(ctx, "sendcoins")
×
563
                return nil
×
564
        }
×
565

566
        // Check that only the field sat_per_vbyte or the deprecated field
567
        // sat_per_byte is used.
568
        feeRateFlag, err := checkNotBothSet(
×
569
                ctx, "sat_per_vbyte", "sat_per_byte",
×
570
        )
×
571
        if err != nil {
×
572
                return err
×
573
        }
×
574

575
        // Only fee rate flag or conf_target should be set, not both.
576
        if _, err := checkNotBothSet(
×
577
                ctx, feeRateFlag, "conf_target",
×
578
        ); err != nil {
×
579
                return err
×
580
        }
×
581

582
        switch {
×
583
        case ctx.IsSet("addr"):
×
584
                addr = ctx.String("addr")
×
585
        case args.Present():
×
586
                addr = args.First()
×
587
                args = args.Tail()
×
588
        default:
×
589
                return fmt.Errorf("Address argument missing")
×
590
        }
591

592
        switch {
×
593
        case ctx.IsSet("amt"):
×
594
                amt = ctx.Int64("amt")
×
595
        case args.Present():
×
596
                amt, err = strconv.ParseInt(args.First(), 10, 64)
×
597
        case !ctx.Bool("sweepall"):
×
598
                return fmt.Errorf("Amount argument missing")
×
599
        }
600
        if err != nil {
×
601
                return fmt.Errorf("unable to decode amount: %w", err)
×
602
        }
×
603

604
        if amt != 0 && ctx.Bool("sweepall") {
×
605
                return fmt.Errorf("amount cannot be set if attempting to " +
×
606
                        "sweep all coins out of the wallet")
×
607
        }
×
608

609
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
610
        if err != nil {
×
611
                return err
×
612
        }
×
613

614
        client, cleanUp := getClient(ctx)
×
615
        defer cleanUp()
×
616
        minConfs := int32(ctx.Uint64("min_confs"))
×
617

×
618
        // In case that the user has specified the sweepall flag, we'll
×
619
        // calculate the amount to send based on the current wallet balance.
×
620
        displayAmt := amt
×
621
        if ctx.Bool("sweepall") && !ctx.IsSet("utxo") {
×
622
                balanceResponse, err := client.WalletBalance(
×
623
                        ctxc, &lnrpc.WalletBalanceRequest{
×
624
                                MinConfs: minConfs,
×
625
                        },
×
626
                )
×
627
                if err != nil {
×
628
                        return fmt.Errorf("unable to retrieve wallet balance:"+
×
629
                                " %w", err)
×
630
                }
×
631
                displayAmt = balanceResponse.GetConfirmedBalance()
×
632
        }
633

634
        if ctx.IsSet("utxo") {
×
635
                utxos := ctx.StringSlice("utxo")
×
636

×
637
                outpoints, err = UtxosToOutpoints(utxos)
×
638
                if err != nil {
×
639
                        return fmt.Errorf("unable to decode utxos: %w", err)
×
640
                }
×
641

642
                if ctx.Bool("sweepall") {
×
643
                        displayAmt = 0
×
644
                        // If we're sweeping all funds of the utxos, we'll need
×
645
                        // to set the display amount to the total amount of the
×
646
                        // utxos.
×
647
                        unspents, err := client.ListUnspent(
×
648
                                ctxc, &lnrpc.ListUnspentRequest{
×
649
                                        MinConfs: 0,
×
650
                                        MaxConfs: math.MaxInt32,
×
651
                                },
×
652
                        )
×
653
                        if err != nil {
×
654
                                return err
×
655
                        }
×
656

657
                        for _, utxo := range outpoints {
×
658
                                for _, unspent := range unspents.Utxos {
×
659
                                        unspentUtxo := unspent.Outpoint
×
660
                                        if isSameOutpoint(utxo, unspentUtxo) {
×
661
                                                displayAmt += unspent.AmountSat
×
662
                                                break
×
663
                                        }
664
                                }
665
                        }
666
                }
667
        }
668

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

×
676
                confirm := promptForConfirmation("Confirm payment (yes/no): ")
×
677
                if !confirm {
×
678
                        return nil
×
679
                }
×
680
        }
681

682
        req := &lnrpc.SendCoinsRequest{
×
683
                Addr:                  addr,
×
684
                Amount:                amt,
×
685
                TargetConf:            int32(ctx.Int64("conf_target")),
×
686
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
687
                SendAll:               ctx.Bool("sweepall"),
×
688
                Label:                 ctx.String(txLabelFlag.Name),
×
689
                MinConfs:              minConfs,
×
690
                SpendUnconfirmed:      minConfs == 0,
×
691
                CoinSelectionStrategy: coinSelectionStrategy,
×
692
                Outpoints:             outpoints,
×
693
        }
×
694
        txid, err := client.SendCoins(ctxc, req)
×
695
        if err != nil {
×
696
                return err
×
697
        }
×
698

699
        printRespJSON(txid)
×
700
        return nil
×
701
}
702

703
func isSameOutpoint(a, b *lnrpc.OutPoint) bool {
×
704
        return a.TxidStr == b.TxidStr && a.OutputIndex == b.OutputIndex
×
705
}
×
706

707
var listUnspentCommand = cli.Command{
708
        Name:      "listunspent",
709
        Category:  "On-chain",
710
        Usage:     "List utxos available for spending.",
711
        ArgsUsage: "[min-confs [max-confs]] [--unconfirmed_only]",
712
        Description: `
713
        For each spendable utxo currently in the wallet, with at least min_confs
714
        confirmations, and at most max_confs confirmations, lists the txid,
715
        index, amount, address, address type, scriptPubkey and number of
716
        confirmations.  Use --min_confs=0 to include unconfirmed coins. To list
717
        all coins with at least min_confs confirmations, omit the second
718
        argument or flag '--max_confs'. To list all confirmed and unconfirmed
719
        coins, no arguments are required. To see only unconfirmed coins, use
720
        '--unconfirmed_only' with '--min_confs' and '--max_confs' set to zero or
721
        not present.
722
        `,
723
        Flags: []cli.Flag{
724
                cli.Int64Flag{
725
                        Name:  "min_confs",
726
                        Usage: "the minimum number of confirmations for a utxo",
727
                },
728
                cli.Int64Flag{
729
                        Name:  "max_confs",
730
                        Usage: "the maximum number of confirmations for a utxo",
731
                },
732
                cli.BoolFlag{
733
                        Name: "unconfirmed_only",
734
                        Usage: "when min_confs and max_confs are zero, " +
735
                                "setting false implicitly overrides max_confs " +
736
                                "to be MaxInt32, otherwise max_confs remains " +
737
                                "zero. An error is returned if the value is " +
738
                                "true and both min_confs and max_confs are " +
739
                                "non-zero. (default: false)",
740
                },
741
        },
742
        Action: actionDecorator(listUnspent),
743
}
744

745
func listUnspent(ctx *cli.Context) error {
×
746
        var (
×
747
                minConfirms int64
×
748
                maxConfirms int64
×
749
                err         error
×
750
        )
×
751
        ctxc := getContext()
×
752
        args := ctx.Args()
×
753

×
754
        if ctx.IsSet("max_confs") && !ctx.IsSet("min_confs") {
×
755
                return fmt.Errorf("max_confs cannot be set without " +
×
756
                        "min_confs being set")
×
757
        }
×
758

759
        switch {
×
760
        case ctx.IsSet("min_confs"):
×
761
                minConfirms = ctx.Int64("min_confs")
×
762
        case args.Present():
×
763
                minConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
764
                if err != nil {
×
765
                        cli.ShowCommandHelp(ctx, "listunspent")
×
766
                        return nil
×
767
                }
×
768
                args = args.Tail()
×
769
        }
770

771
        switch {
×
772
        case ctx.IsSet("max_confs"):
×
773
                maxConfirms = ctx.Int64("max_confs")
×
774
        case args.Present():
×
775
                maxConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
776
                if err != nil {
×
777
                        cli.ShowCommandHelp(ctx, "listunspent")
×
778
                        return nil
×
779
                }
×
780
                args = args.Tail()
×
781
        }
782

783
        unconfirmedOnly := ctx.Bool("unconfirmed_only")
×
784

×
785
        // Force minConfirms and maxConfirms to be zero if unconfirmedOnly is
×
786
        // true.
×
787
        if unconfirmedOnly && (minConfirms != 0 || maxConfirms != 0) {
×
788
                cli.ShowCommandHelp(ctx, "listunspent")
×
789
                return nil
×
790
        }
×
791

792
        // When unconfirmedOnly is inactive, we will override maxConfirms to be
793
        // a MaxInt32 to return all confirmed and unconfirmed utxos.
794
        if maxConfirms == 0 && !unconfirmedOnly {
×
795
                maxConfirms = math.MaxInt32
×
796
        }
×
797

798
        client, cleanUp := getClient(ctx)
×
799
        defer cleanUp()
×
800

×
801
        req := &lnrpc.ListUnspentRequest{
×
802
                MinConfs: int32(minConfirms),
×
803
                MaxConfs: int32(maxConfirms),
×
804
        }
×
805
        resp, err := client.ListUnspent(ctxc, req)
×
806
        if err != nil {
×
807
                return err
×
808
        }
×
809

810
        // Parse the response into the final json object that will be printed
811
        // to stdout. At the moment, this filters out the raw txid bytes from
812
        // each utxo's outpoint and only prints the txid string.
813
        var listUnspentResp = struct {
×
814
                Utxos []*Utxo `json:"utxos"`
×
815
        }{
×
816
                Utxos: make([]*Utxo, 0, len(resp.Utxos)),
×
817
        }
×
818
        for _, protoUtxo := range resp.Utxos {
×
819
                utxo := NewUtxoFromProto(protoUtxo)
×
820
                listUnspentResp.Utxos = append(listUnspentResp.Utxos, utxo)
×
821
        }
×
822

823
        printJSON(listUnspentResp)
×
824

×
825
        return nil
×
826
}
827

828
var sendManyCommand = cli.Command{
829
        Name:      "sendmany",
830
        Category:  "On-chain",
831
        Usage:     "Send bitcoin on-chain to multiple addresses.",
832
        ArgsUsage: "send-json-string [--conf_target=N] [--sat_per_vbyte=P]",
833
        Description: `
834
        Create and broadcast a transaction paying the specified amount(s) to the passed address(es).
835

836
        The send-json-string' param decodes addresses and the amount to send
837
        respectively in the following format:
838

839
            '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
840
        `,
841
        Flags: []cli.Flag{
842
                cli.Int64Flag{
843
                        Name: "conf_target",
844
                        Usage: "(optional) the number of blocks that the transaction *should* " +
845
                                "confirm in, will be used for fee estimation",
846
                },
847
                cli.Int64Flag{
848
                        Name:   "sat_per_byte",
849
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
850
                        Hidden: true,
851
                },
852
                cli.Int64Flag{
853
                        Name: "sat_per_vbyte",
854
                        Usage: "(optional) a manual fee expressed in " +
855
                                "sat/vbyte that should be used when crafting " +
856
                                "the transaction",
857
                },
858
                cli.Uint64Flag{
859
                        Name: "min_confs",
860
                        Usage: "(optional) the minimum number of confirmations " +
861
                                "each one of your outputs used for the transaction " +
862
                                "must satisfy",
863
                        Value: defaultUtxoMinConf,
864
                },
865
                coinSelectionStrategyFlag,
866
                txLabelFlag,
867
        },
868
        Action: actionDecorator(sendMany),
869
}
870

871
func sendMany(ctx *cli.Context) error {
×
872
        ctxc := getContext()
×
873
        var amountToAddr map[string]int64
×
874

×
875
        jsonMap := ctx.Args().First()
×
876
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
877
                return err
×
878
        }
×
879

880
        // Check that only the field sat_per_vbyte or the deprecated field
881
        // sat_per_byte is used.
882
        feeRateFlag, err := checkNotBothSet(
×
883
                ctx, "sat_per_vbyte", "sat_per_byte",
×
884
        )
×
885
        if err != nil {
×
886
                return err
×
887
        }
×
888

889
        // Only fee rate flag or conf_target should be set, not both.
890
        if _, err := checkNotBothSet(
×
891
                ctx, feeRateFlag, "conf_target",
×
892
        ); err != nil {
×
893
                return err
×
894
        }
×
895

896
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
897
        if err != nil {
×
898
                return err
×
899
        }
×
900

901
        client, cleanUp := getClient(ctx)
×
902
        defer cleanUp()
×
903

×
904
        minConfs := int32(ctx.Uint64("min_confs"))
×
905
        txid, err := client.SendMany(ctxc, &lnrpc.SendManyRequest{
×
906
                AddrToAmount:          amountToAddr,
×
907
                TargetConf:            int32(ctx.Int64("conf_target")),
×
908
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
909
                Label:                 ctx.String(txLabelFlag.Name),
×
910
                MinConfs:              minConfs,
×
911
                SpendUnconfirmed:      minConfs == 0,
×
912
                CoinSelectionStrategy: coinSelectionStrategy,
×
913
        })
×
914
        if err != nil {
×
915
                return err
×
916
        }
×
917

918
        printRespJSON(txid)
×
919
        return nil
×
920
}
921

922
var connectCommand = cli.Command{
923
        Name:      "connect",
924
        Category:  "Peers",
925
        Usage:     "Connect to a remote lightning peer.",
926
        ArgsUsage: "<pubkey>@host",
927
        Description: `
928
        Connect to a peer using its <pubkey> and host.
929

930
        A custom timeout on the connection is supported. For instance, to timeout
931
        the connection request in 30 seconds, use the following:
932

933
        lncli connect <pubkey>@host --timeout 30s
934
        `,
935
        Flags: []cli.Flag{
936
                cli.BoolFlag{
937
                        Name: "perm",
938
                        Usage: "If set, the daemon will attempt to persistently " +
939
                                "connect to the target peer.\n" +
940
                                "           If not, the call will be synchronous.",
941
                },
942
                cli.DurationFlag{
943
                        Name: "timeout",
944
                        Usage: "The connection timeout value for current request. " +
945
                                "Valid uints are {ms, s, m, h}.\n" +
946
                                "If not set, the global connection " +
947
                                "timeout value (default to 120s) is used.",
948
                },
949
        },
950
        Action: actionDecorator(connectPeer),
951
}
952

953
func connectPeer(ctx *cli.Context) error {
×
954
        ctxc := getContext()
×
955
        client, cleanUp := getClient(ctx)
×
956
        defer cleanUp()
×
957

×
958
        targetAddress := ctx.Args().First()
×
959
        splitAddr := strings.Split(targetAddress, "@")
×
960
        if len(splitAddr) != 2 {
×
961
                return fmt.Errorf("target address expected in format: " +
×
962
                        "pubkey@host:port")
×
963
        }
×
964

965
        addr := &lnrpc.LightningAddress{
×
966
                Pubkey: splitAddr[0],
×
967
                Host:   splitAddr[1],
×
968
        }
×
969
        req := &lnrpc.ConnectPeerRequest{
×
970
                Addr:    addr,
×
971
                Perm:    ctx.Bool("perm"),
×
972
                Timeout: uint64(ctx.Duration("timeout").Seconds()),
×
973
        }
×
974

×
975
        lnid, err := client.ConnectPeer(ctxc, req)
×
976
        if err != nil {
×
977
                return err
×
978
        }
×
979

980
        printRespJSON(lnid)
×
981
        return nil
×
982
}
983

984
var disconnectCommand = cli.Command{
985
        Name:     "disconnect",
986
        Category: "Peers",
987
        Usage: "Disconnect a remote lightning peer identified by " +
988
                "public key.",
989
        ArgsUsage: "<pubkey>",
990
        Flags: []cli.Flag{
991
                cli.StringFlag{
992
                        Name: "node_key",
993
                        Usage: "The hex-encoded compressed public key of the peer " +
994
                                "to disconnect from",
995
                },
996
        },
997
        Action: actionDecorator(disconnectPeer),
998
}
999

1000
func disconnectPeer(ctx *cli.Context) error {
×
1001
        ctxc := getContext()
×
1002
        client, cleanUp := getClient(ctx)
×
1003
        defer cleanUp()
×
1004

×
1005
        var pubKey string
×
1006
        switch {
×
1007
        case ctx.IsSet("node_key"):
×
1008
                pubKey = ctx.String("node_key")
×
1009
        case ctx.Args().Present():
×
1010
                pubKey = ctx.Args().First()
×
1011
        default:
×
1012
                return fmt.Errorf("must specify target public key")
×
1013
        }
1014

1015
        req := &lnrpc.DisconnectPeerRequest{
×
1016
                PubKey: pubKey,
×
1017
        }
×
1018

×
1019
        lnid, err := client.DisconnectPeer(ctxc, req)
×
1020
        if err != nil {
×
1021
                return err
×
1022
        }
×
1023

1024
        printRespJSON(lnid)
×
1025
        return nil
×
1026
}
1027

1028
// TODO(roasbeef): also allow short relative channel ID.
1029

1030
var closeChannelCommand = cli.Command{
1031
        Name:     "closechannel",
1032
        Category: "Channels",
1033
        Usage:    "Close an existing channel.",
1034
        Description: `
1035
        Close an existing channel. The channel can be closed either cooperatively,
1036
        or unilaterally (--force).
1037

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

1042
        In the case of a cooperative closure, one can manually set the fee to
1043
        be used for the closing transaction via either the --conf_target or
1044
        --sat_per_vbyte arguments. This will be the starting value used during
1045
        fee negotiation. This is optional. The parameter --max_fee_rate in
1046
        comparison is the end boundary of the fee negotiation, if not specified
1047
        it's always x3 of the starting value. Increasing this value increases
1048
        the chance of a successful negotiation.
1049
        Moreover if the channel has active HTLCs on it, the coop close will
1050
        wait until all HTLCs are resolved and will not allow any new HTLCs on
1051
        the channel. The channel will appear as disabled in the listchannels
1052
        output. The command will block in that case until the channel close tx
1053
        is broadcasted.
1054

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

1060
        To view which funding_txids/output_indexes can be used for a channel close,
1061
        see the channel_point values within the listchannels command output.
1062
        The format for a channel_point is 'funding_txid:output_index'.`,
1063
        ArgsUsage: "funding_txid output_index",
1064
        Flags: []cli.Flag{
1065
                cli.StringFlag{
1066
                        Name:  "funding_txid",
1067
                        Usage: "the txid of the channel's funding transaction",
1068
                },
1069
                cli.IntFlag{
1070
                        Name: "output_index",
1071
                        Usage: "the output index for the funding output of the funding " +
1072
                                "transaction",
1073
                },
1074
                cli.StringFlag{
1075
                        Name: "chan_point",
1076
                        Usage: "(optional) the channel point. If set, " +
1077
                                "funding_txid and output_index flags and " +
1078
                                "positional arguments will be ignored",
1079
                },
1080
                cli.BoolFlag{
1081
                        Name:  "force",
1082
                        Usage: "attempt an uncooperative closure",
1083
                },
1084
                cli.BoolFlag{
1085
                        Name: "block",
1086
                        Usage: `block will wait for the channel to be closed,
1087
                        "meaning that it will wait for the channel close tx to
1088
                        get 1 confirmation.`,
1089
                },
1090
                cli.Int64Flag{
1091
                        Name: "conf_target",
1092
                        Usage: "(optional) the number of blocks that the " +
1093
                                "transaction *should* confirm in, will be " +
1094
                                "used for fee estimation. If not set, " +
1095
                                "then the conf-target value set in the main " +
1096
                                "lnd config will be used.",
1097
                },
1098
                cli.Int64Flag{
1099
                        Name:   "sat_per_byte",
1100
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
1101
                        Hidden: true,
1102
                },
1103
                cli.Int64Flag{
1104
                        Name: "sat_per_vbyte",
1105
                        Usage: "(optional) a manual fee expressed in " +
1106
                                "sat/vbyte that should be used when crafting " +
1107
                                "the transaction; default is a conf-target " +
1108
                                "of 6 blocks",
1109
                },
1110
                cli.StringFlag{
1111
                        Name: "delivery_addr",
1112
                        Usage: "(optional) an address to deliver funds " +
1113
                                "upon cooperative channel closing, may only " +
1114
                                "be used if an upfront shutdown address is not " +
1115
                                "already set",
1116
                },
1117
                cli.Uint64Flag{
1118
                        Name: "max_fee_rate",
1119
                        Usage: "(optional) maximum fee rate in sat/vbyte " +
1120
                                "accepted during the negotiation (default is " +
1121
                                "x3 of the desired fee rate); increases the " +
1122
                                "success pobability of the negotiation if " +
1123
                                "set higher",
1124
                },
1125
        },
1126
        Action: actionDecorator(closeChannel),
1127
}
1128

1129
func closeChannel(ctx *cli.Context) error {
×
1130
        ctxc := getContext()
×
1131
        client, cleanUp := getClient(ctx)
×
1132
        defer cleanUp()
×
1133

×
1134
        // Show command help if no arguments and flags were provided.
×
1135
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1136
                cli.ShowCommandHelp(ctx, "closechannel")
×
1137
                return nil
×
1138
        }
×
1139

1140
        // Check that only the field sat_per_vbyte or the deprecated field
1141
        // sat_per_byte is used.
1142
        feeRateFlag, err := checkNotBothSet(
×
1143
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1144
        )
×
1145
        if err != nil {
×
1146
                return err
×
1147
        }
×
1148

1149
        channelPoint, err := parseChannelPoint(ctx)
×
1150
        if err != nil {
×
1151
                return err
×
1152
        }
×
1153

1154
        // TODO(roasbeef): implement time deadline within server
1155
        req := &lnrpc.CloseChannelRequest{
×
1156
                ChannelPoint:    channelPoint,
×
1157
                Force:           ctx.Bool("force"),
×
1158
                TargetConf:      int32(ctx.Int64("conf_target")),
×
1159
                SatPerVbyte:     ctx.Uint64(feeRateFlag),
×
1160
                DeliveryAddress: ctx.String("delivery_addr"),
×
1161
                MaxFeePerVbyte:  ctx.Uint64("max_fee_rate"),
×
1162
                // This makes sure that a coop close will also be executed if
×
1163
                // active HTLCs are present on the channel.
×
1164
                NoWait: true,
×
1165
        }
×
1166

×
1167
        // After parsing the request, we'll spin up a goroutine that will
×
1168
        // retrieve the closing transaction ID when attempting to close the
×
1169
        // channel. We do this to because `executeChannelClose` can block, so we
×
1170
        // would like to present the closing transaction ID to the user as soon
×
1171
        // as it is broadcasted.
×
1172
        var wg sync.WaitGroup
×
1173
        txidChan := make(chan string, 1)
×
1174

×
1175
        wg.Add(1)
×
1176
        go func() {
×
1177
                defer wg.Done()
×
1178

×
1179
                printJSON(struct {
×
1180
                        ClosingTxid string `json:"closing_txid"`
×
1181
                }{
×
1182
                        ClosingTxid: <-txidChan,
×
1183
                })
×
1184
        }()
×
1185

1186
        err = executeChannelClose(ctxc, client, req, txidChan, ctx.Bool("block"))
×
1187
        if err != nil {
×
1188
                return err
×
1189
        }
×
1190

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

×
1196
        return nil
×
1197
}
1198

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

×
1208
        stream, err := client.CloseChannel(ctxc, req)
×
1209
        if err != nil {
×
1210
                return err
×
1211
        }
×
1212

1213
        for {
×
1214
                resp, err := stream.Recv()
×
1215
                if err == io.EOF {
×
1216
                        return nil
×
1217
                } else if err != nil {
×
1218
                        return err
×
1219
                }
×
1220

1221
                switch update := resp.Update.(type) {
×
1222
                case *lnrpc.CloseStatusUpdate_CloseInstant:
×
1223
                        fmt.Fprintln(os.Stderr, "Channel close successfully "+
×
1224
                                "initiated")
×
1225

×
1226
                        pendingHtlcs := update.CloseInstant.NumPendingHtlcs
×
1227
                        if pendingHtlcs > 0 {
×
1228
                                fmt.Fprintf(os.Stderr, "Cooperative channel "+
×
1229
                                        "close waiting for %d HTLCs to be "+
×
1230
                                        "resolved before the close process "+
×
1231
                                        "can kick off\n", pendingHtlcs)
×
1232
                        }
×
1233

1234
                case *lnrpc.CloseStatusUpdate_ClosePending:
×
1235
                        closingHash := update.ClosePending.Txid
×
1236
                        txid, err := chainhash.NewHash(closingHash)
×
1237
                        if err != nil {
×
1238
                                return err
×
1239
                        }
×
1240

1241
                        fmt.Fprintf(os.Stderr, "Channel close transaction "+
×
1242
                                "broadcasted: %v\n", txid)
×
1243

×
1244
                        txidChan <- txid.String()
×
1245

×
1246
                        if !block {
×
1247
                                return nil
×
1248
                        }
×
1249

1250
                        fmt.Fprintln(os.Stderr, "Waiting for channel close "+
×
1251
                                "confirmation ...")
×
1252

1253
                case *lnrpc.CloseStatusUpdate_ChanClose:
×
1254
                        fmt.Fprintln(os.Stderr, "Channel close successfully "+
×
1255
                                "confirmed")
×
1256

×
1257
                        return nil
×
1258
                }
1259
        }
1260
}
1261

1262
var closeAllChannelsCommand = cli.Command{
1263
        Name:     "closeallchannels",
1264
        Category: "Channels",
1265
        Usage:    "Close all existing channels.",
1266
        Description: `
1267
        Close all existing channels.
1268

1269
        Channels will be closed either cooperatively or unilaterally, depending
1270
        on whether the channel is active or not. If the channel is inactive, any
1271
        settled funds within it will be time locked for a few blocks before they
1272
        can be spent.
1273

1274
        One can request to close inactive channels only by using the
1275
        --inactive_only flag.
1276

1277
        By default, one is prompted for confirmation every time an inactive
1278
        channel is requested to be closed. To avoid this, one can set the
1279
        --force flag, which will only prompt for confirmation once for all
1280
        inactive channels and proceed to close them.
1281

1282
        In the case of cooperative closures, one can manually set the fee to
1283
        be used for the closing transactions via either the --conf_target or
1284
        --sat_per_vbyte arguments. This will be the starting value used during
1285
        fee negotiation. This is optional.`,
1286
        Flags: []cli.Flag{
1287
                cli.BoolFlag{
1288
                        Name:  "inactive_only",
1289
                        Usage: "close inactive channels only",
1290
                },
1291
                cli.BoolFlag{
1292
                        Name: "force",
1293
                        Usage: "ask for confirmation once before attempting " +
1294
                                "to close existing channels",
1295
                },
1296
                cli.Int64Flag{
1297
                        Name: "conf_target",
1298
                        Usage: "(optional) the number of blocks that the " +
1299
                                "closing transactions *should* confirm in, will be " +
1300
                                "used for fee estimation",
1301
                },
1302
                cli.Int64Flag{
1303
                        Name:   "sat_per_byte",
1304
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
1305
                        Hidden: true,
1306
                },
1307
                cli.Int64Flag{
1308
                        Name: "sat_per_vbyte",
1309
                        Usage: "(optional) a manual fee expressed in " +
1310
                                "sat/vbyte that should be used when crafting " +
1311
                                "the closing transactions",
1312
                },
1313
                cli.BoolFlag{
1314
                        Name: "s, skip_confirmation",
1315
                        Usage: "Skip the confirmation prompt and close all " +
1316
                                "channels immediately",
1317
                },
1318
        },
1319
        Action: actionDecorator(closeAllChannels),
1320
}
1321

1322
func closeAllChannels(ctx *cli.Context) error {
×
1323
        ctxc := getContext()
×
1324
        client, cleanUp := getClient(ctx)
×
1325
        defer cleanUp()
×
1326

×
1327
        // Check that only the field sat_per_vbyte or the deprecated field
×
1328
        // sat_per_byte is used.
×
1329
        feeRateFlag, err := checkNotBothSet(
×
1330
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1331
        )
×
1332
        if err != nil {
×
1333
                return err
×
1334
        }
×
1335

1336
        prompt := "Do you really want to close ALL channels? (yes/no): "
×
1337
        if !ctx.Bool("skip_confirmation") && !promptForConfirmation(prompt) {
×
1338
                return errors.New("action aborted by user")
×
1339
        }
×
1340

1341
        listReq := &lnrpc.ListChannelsRequest{}
×
1342
        openChannels, err := client.ListChannels(ctxc, listReq)
×
1343
        if err != nil {
×
1344
                return fmt.Errorf("unable to fetch open channels: %w", err)
×
1345
        }
×
1346

1347
        if len(openChannels.Channels) == 0 {
×
1348
                return errors.New("no open channels to close")
×
1349
        }
×
1350

1351
        var channelsToClose []*lnrpc.Channel
×
1352

×
1353
        switch {
×
1354
        case ctx.Bool("force") && ctx.Bool("inactive_only"):
×
1355
                msg := "Unilaterally close all inactive channels? The funds " +
×
1356
                        "within these channels will be locked for some blocks " +
×
1357
                        "(CSV delay) before they can be spent. (yes/no): "
×
1358

×
1359
                confirmed := promptForConfirmation(msg)
×
1360

×
1361
                // We can safely exit if the user did not confirm.
×
1362
                if !confirmed {
×
1363
                        return nil
×
1364
                }
×
1365

1366
                // Go through the list of open channels and only add inactive
1367
                // channels to the closing list.
1368
                for _, channel := range openChannels.Channels {
×
1369
                        if !channel.GetActive() {
×
1370
                                channelsToClose = append(
×
1371
                                        channelsToClose, channel,
×
1372
                                )
×
1373
                        }
×
1374
                }
1375
        case ctx.Bool("force"):
×
1376
                msg := "Close all active and inactive channels? Inactive " +
×
1377
                        "channels will be closed unilaterally, so funds " +
×
1378
                        "within them will be locked for a few blocks (CSV " +
×
1379
                        "delay) before they can be spent. (yes/no): "
×
1380

×
1381
                confirmed := promptForConfirmation(msg)
×
1382

×
1383
                // We can safely exit if the user did not confirm.
×
1384
                if !confirmed {
×
1385
                        return nil
×
1386
                }
×
1387

1388
                channelsToClose = openChannels.Channels
×
1389
        default:
×
1390
                // Go through the list of open channels and determine which
×
1391
                // should be added to the closing list.
×
1392
                for _, channel := range openChannels.Channels {
×
1393
                        // If the channel is inactive, we'll attempt to
×
1394
                        // unilaterally close the channel, so we should prompt
×
1395
                        // the user for confirmation beforehand.
×
1396
                        if !channel.GetActive() {
×
1397
                                msg := fmt.Sprintf("Unilaterally close channel "+
×
1398
                                        "with node %s and channel point %s? "+
×
1399
                                        "The closing transaction will need %d "+
×
1400
                                        "confirmations before the funds can be "+
×
1401
                                        "spent. (yes/no): ", channel.RemotePubkey,
×
1402
                                        channel.ChannelPoint, channel.LocalConstraints.CsvDelay)
×
1403

×
1404
                                confirmed := promptForConfirmation(msg)
×
1405

×
1406
                                if confirmed {
×
1407
                                        channelsToClose = append(
×
1408
                                                channelsToClose, channel,
×
1409
                                        )
×
1410
                                }
×
1411
                        } else if !ctx.Bool("inactive_only") {
×
1412
                                // Otherwise, we'll only add active channels if
×
1413
                                // we were not requested to close inactive
×
1414
                                // channels only.
×
1415
                                channelsToClose = append(
×
1416
                                        channelsToClose, channel,
×
1417
                                )
×
1418
                        }
×
1419
                }
1420
        }
1421

1422
        // result defines the result of closing a channel. The closing
1423
        // transaction ID is populated if a channel is successfully closed.
1424
        // Otherwise, the error that prevented closing the channel is populated.
1425
        type result struct {
×
1426
                RemotePubKey string `json:"remote_pub_key"`
×
1427
                ChannelPoint string `json:"channel_point"`
×
1428
                ClosingTxid  string `json:"closing_txid"`
×
1429
                FailErr      string `json:"error"`
×
1430
        }
×
1431

×
1432
        // Launch each channel closure in a goroutine in order to execute them
×
1433
        // in parallel. Once they're all executed, we will print the results as
×
1434
        // they come.
×
1435
        resultChan := make(chan result, len(channelsToClose))
×
1436
        for _, channel := range channelsToClose {
×
1437
                go func(channel *lnrpc.Channel) {
×
1438
                        res := result{}
×
1439
                        res.RemotePubKey = channel.RemotePubkey
×
1440
                        res.ChannelPoint = channel.ChannelPoint
×
1441
                        defer func() {
×
1442
                                resultChan <- res
×
1443
                        }()
×
1444

1445
                        // Parse the channel point in order to create the close
1446
                        // channel request.
1447
                        s := strings.Split(res.ChannelPoint, ":")
×
1448
                        if len(s) != 2 {
×
1449
                                res.FailErr = "expected channel point with " +
×
1450
                                        "format txid:index"
×
1451
                                return
×
1452
                        }
×
1453
                        index, err := strconv.ParseUint(s[1], 10, 32)
×
1454
                        if err != nil {
×
1455
                                res.FailErr = fmt.Sprintf("unable to parse "+
×
1456
                                        "channel point output index: %v", err)
×
1457
                                return
×
1458
                        }
×
1459

1460
                        req := &lnrpc.CloseChannelRequest{
×
1461
                                ChannelPoint: &lnrpc.ChannelPoint{
×
1462
                                        FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
×
1463
                                                FundingTxidStr: s[0],
×
1464
                                        },
×
1465
                                        OutputIndex: uint32(index),
×
1466
                                },
×
1467
                                Force:       !channel.GetActive(),
×
1468
                                TargetConf:  int32(ctx.Int64("conf_target")),
×
1469
                                SatPerVbyte: ctx.Uint64(feeRateFlag),
×
1470
                        }
×
1471

×
1472
                        txidChan := make(chan string, 1)
×
1473
                        err = executeChannelClose(ctxc, client, req, txidChan, false)
×
1474
                        if err != nil {
×
1475
                                res.FailErr = fmt.Sprintf("unable to close "+
×
1476
                                        "channel: %v", err)
×
1477
                                return
×
1478
                        }
×
1479

1480
                        res.ClosingTxid = <-txidChan
×
1481
                }(channel)
1482
        }
1483

1484
        for range channelsToClose {
×
1485
                res := <-resultChan
×
1486
                printJSON(res)
×
1487
        }
×
1488

1489
        return nil
×
1490
}
1491

1492
// promptForConfirmation continuously prompts the user for the message until
1493
// receiving a response of "yes" or "no" and returns their answer as a bool.
1494
func promptForConfirmation(msg string) bool {
×
1495
        reader := bufio.NewReader(os.Stdin)
×
1496

×
1497
        for {
×
1498
                fmt.Print(msg)
×
1499

×
1500
                answer, err := reader.ReadString('\n')
×
1501
                if err != nil {
×
1502
                        return false
×
1503
                }
×
1504

1505
                answer = strings.ToLower(strings.TrimSpace(answer))
×
1506

×
1507
                switch {
×
1508
                case answer == "yes":
×
1509
                        return true
×
1510
                case answer == "no":
×
1511
                        return false
×
1512
                default:
×
1513
                        continue
×
1514
                }
1515
        }
1516
}
1517

1518
var abandonChannelCommand = cli.Command{
1519
        Name:     "abandonchannel",
1520
        Category: "Channels",
1521
        Usage:    "Abandons an existing channel.",
1522
        Description: `
1523
        Removes all channel state from the database except for a close
1524
        summary. This method can be used to get rid of permanently unusable
1525
        channels due to bugs fixed in newer versions of lnd.
1526

1527
        Only available when lnd is built in debug mode. The flag
1528
        --i_know_what_i_am_doing can be set to override the debug/dev mode
1529
        requirement.
1530

1531
        To view which funding_txids/output_indexes can be used for this command,
1532
        see the channel_point values within the listchannels command output.
1533
        The format for a channel_point is 'funding_txid:output_index'.`,
1534
        ArgsUsage: "funding_txid [output_index]",
1535
        Flags: []cli.Flag{
1536
                cli.StringFlag{
1537
                        Name:  "funding_txid",
1538
                        Usage: "the txid of the channel's funding transaction",
1539
                },
1540
                cli.IntFlag{
1541
                        Name: "output_index",
1542
                        Usage: "the output index for the funding output of the funding " +
1543
                                "transaction",
1544
                },
1545
                cli.StringFlag{
1546
                        Name: "chan_point",
1547
                        Usage: "(optional) the channel point. If set, " +
1548
                                "funding_txid and output_index flags and " +
1549
                                "positional arguments will be ignored",
1550
                },
1551
                cli.BoolFlag{
1552
                        Name: "i_know_what_i_am_doing",
1553
                        Usage: "override the requirement for lnd needing to " +
1554
                                "be in dev/debug mode to use this command; " +
1555
                                "when setting this the user attests that " +
1556
                                "they know the danger of using this command " +
1557
                                "on channels and that doing so can lead to " +
1558
                                "loss of funds if the channel funding TX " +
1559
                                "ever confirms (or was confirmed)",
1560
                },
1561
        },
1562
        Action: actionDecorator(abandonChannel),
1563
}
1564

1565
func abandonChannel(ctx *cli.Context) error {
×
1566
        ctxc := getContext()
×
1567
        client, cleanUp := getClient(ctx)
×
1568
        defer cleanUp()
×
1569

×
1570
        // Show command help if no arguments and flags were provided.
×
1571
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1572
                cli.ShowCommandHelp(ctx, "abandonchannel")
×
1573
                return nil
×
1574
        }
×
1575

1576
        channelPoint, err := parseChannelPoint(ctx)
×
1577
        if err != nil {
×
1578
                return err
×
1579
        }
×
1580

1581
        req := &lnrpc.AbandonChannelRequest{
×
1582
                ChannelPoint:      channelPoint,
×
1583
                IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
×
1584
        }
×
1585

×
1586
        resp, err := client.AbandonChannel(ctxc, req)
×
1587
        if err != nil {
×
1588
                return err
×
1589
        }
×
1590

1591
        printRespJSON(resp)
×
1592
        return nil
×
1593
}
1594

1595
// parseChannelPoint parses a funding txid and output index from the command
1596
// line. Both named options and unnamed parameters are supported.
1597
func parseChannelPoint(ctx *cli.Context) (*lnrpc.ChannelPoint, error) {
×
1598
        channelPoint := &lnrpc.ChannelPoint{}
×
1599
        var err error
×
1600

×
1601
        args := ctx.Args()
×
1602

×
1603
        switch {
×
1604
        case ctx.IsSet("chan_point"):
×
1605
                channelPoint, err = parseChanPoint(ctx.String("chan_point"))
×
1606
                if err != nil {
×
1607
                        return nil, fmt.Errorf("unable to parse chan_point: "+
×
1608
                                "%v", err)
×
1609
                }
×
1610
                return channelPoint, nil
×
1611

1612
        case ctx.IsSet("funding_txid"):
×
1613
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1614
                        FundingTxidStr: ctx.String("funding_txid"),
×
1615
                }
×
1616
        case args.Present():
×
1617
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1618
                        FundingTxidStr: args.First(),
×
1619
                }
×
1620
                args = args.Tail()
×
1621
        default:
×
1622
                return nil, fmt.Errorf("funding txid argument missing")
×
1623
        }
1624

1625
        switch {
×
1626
        case ctx.IsSet("output_index"):
×
1627
                channelPoint.OutputIndex = uint32(ctx.Int("output_index"))
×
1628
        case args.Present():
×
1629
                index, err := strconv.ParseUint(args.First(), 10, 32)
×
1630
                if err != nil {
×
1631
                        return nil, fmt.Errorf("unable to decode output "+
×
1632
                                "index: %w", err)
×
1633
                }
×
1634
                channelPoint.OutputIndex = uint32(index)
×
1635
        default:
×
1636
                channelPoint.OutputIndex = 0
×
1637
        }
1638

1639
        return channelPoint, nil
×
1640
}
1641

1642
var listPeersCommand = cli.Command{
1643
        Name:     "listpeers",
1644
        Category: "Peers",
1645
        Usage:    "List all active, currently connected peers.",
1646
        Flags: []cli.Flag{
1647
                cli.BoolFlag{
1648
                        Name:  "list_errors",
1649
                        Usage: "list a full set of most recent errors for the peer",
1650
                },
1651
        },
1652
        Action: actionDecorator(listPeers),
1653
}
1654

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

×
1660
        // By default, we display a single error on the cli. If the user
×
1661
        // specifically requests a full error set, then we will provide it.
×
1662
        req := &lnrpc.ListPeersRequest{
×
1663
                LatestError: !ctx.IsSet("list_errors"),
×
1664
        }
×
1665
        resp, err := client.ListPeers(ctxc, req)
×
1666
        if err != nil {
×
1667
                return err
×
1668
        }
×
1669

1670
        printRespJSON(resp)
×
1671
        return nil
×
1672
}
1673

1674
var walletBalanceCommand = cli.Command{
1675
        Name:     "walletbalance",
1676
        Category: "Wallet",
1677
        Usage:    "Compute and display the wallet's current balance.",
1678
        Flags: []cli.Flag{
1679
                cli.StringFlag{
1680
                        Name: "account",
1681
                        Usage: "(optional) the account for which the balance " +
1682
                                "is shown",
1683
                        Value: "",
1684
                },
1685
        },
1686
        Action: actionDecorator(walletBalance),
1687
}
1688

1689
func walletBalance(ctx *cli.Context) error {
×
1690
        ctxc := getContext()
×
1691
        client, cleanUp := getClient(ctx)
×
1692
        defer cleanUp()
×
1693

×
1694
        req := &lnrpc.WalletBalanceRequest{
×
1695
                Account: ctx.String("account"),
×
1696
        }
×
1697
        resp, err := client.WalletBalance(ctxc, req)
×
1698
        if err != nil {
×
1699
                return err
×
1700
        }
×
1701

1702
        printRespJSON(resp)
×
1703
        return nil
×
1704
}
1705

1706
var ChannelBalanceCommand = cli.Command{
1707
        Name:     "channelbalance",
1708
        Category: "Channels",
1709
        Usage: "Returns the sum of the total available channel balance across " +
1710
                "all open channels.",
1711
        Action: actionDecorator(ChannelBalance),
1712
}
1713

1714
func ChannelBalance(ctx *cli.Context) error {
×
1715
        ctxc := getContext()
×
1716
        client, cleanUp := getClient(ctx)
×
1717
        defer cleanUp()
×
1718

×
1719
        req := &lnrpc.ChannelBalanceRequest{}
×
1720
        resp, err := client.ChannelBalance(ctxc, req)
×
1721
        if err != nil {
×
1722
                return err
×
1723
        }
×
1724

1725
        printRespJSON(resp)
×
1726
        return nil
×
1727
}
1728

1729
var generateManPageCommand = cli.Command{
1730
        Name: "generatemanpage",
1731
        Usage: "Generates a man page for lncli and lnd as " +
1732
                "lncli.1 and lnd.1 respectively.",
1733
        Hidden: true,
1734
        Action: actionDecorator(generateManPage),
1735
}
1736

1737
func generateManPage(ctx *cli.Context) error {
×
1738
        // Generate the man pages for lncli as lncli.1.
×
1739
        manpages, err := ctx.App.ToMan()
×
1740
        if err != nil {
×
1741
                return err
×
1742
        }
×
1743
        err = os.WriteFile("lncli.1", []byte(manpages), 0644)
×
1744
        if err != nil {
×
1745
                return err
×
1746
        }
×
1747

1748
        // Generate the man pages for lnd as lnd.1.
1749
        config := lnd.DefaultConfig()
×
1750
        fileParser := flags.NewParser(&config, flags.Default)
×
1751
        fileParser.Name = "lnd"
×
1752

×
1753
        var buf bytes.Buffer
×
1754
        fileParser.WriteManPage(&buf)
×
1755

×
1756
        err = os.WriteFile("lnd.1", buf.Bytes(), 0644)
×
1757
        if err != nil {
×
1758
                return err
×
1759
        }
×
1760

1761
        return nil
×
1762
}
1763

1764
var getInfoCommand = cli.Command{
1765
        Name:   "getinfo",
1766
        Usage:  "Returns basic information related to the active daemon.",
1767
        Action: actionDecorator(getInfo),
1768
}
1769

1770
func getInfo(ctx *cli.Context) error {
×
1771
        ctxc := getContext()
×
1772
        client, cleanUp := getClient(ctx)
×
1773
        defer cleanUp()
×
1774

×
1775
        req := &lnrpc.GetInfoRequest{}
×
1776
        resp, err := client.GetInfo(ctxc, req)
×
1777
        if err != nil {
×
1778
                return err
×
1779
        }
×
1780

1781
        printRespJSON(resp)
×
1782
        return nil
×
1783
}
1784

1785
var getRecoveryInfoCommand = cli.Command{
1786
        Name:   "getrecoveryinfo",
1787
        Usage:  "Display information about an ongoing recovery attempt.",
1788
        Action: actionDecorator(getRecoveryInfo),
1789
}
1790

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

×
1796
        req := &lnrpc.GetRecoveryInfoRequest{}
×
1797
        resp, err := client.GetRecoveryInfo(ctxc, req)
×
1798
        if err != nil {
×
1799
                return err
×
1800
        }
×
1801

1802
        printRespJSON(resp)
×
1803
        return nil
×
1804
}
1805

1806
var pendingChannelsCommand = cli.Command{
1807
        Name:     "pendingchannels",
1808
        Category: "Channels",
1809
        Usage:    "Display information pertaining to pending channels.",
1810
        Flags: []cli.Flag{
1811
                cli.BoolFlag{
1812
                        Name: "include_raw_tx",
1813
                        Usage: "include the raw transaction hex for " +
1814
                                "waiting_close_channels.",
1815
                },
1816
        },
1817
        Action: actionDecorator(pendingChannels),
1818
}
1819

1820
func pendingChannels(ctx *cli.Context) error {
×
1821
        ctxc := getContext()
×
1822
        client, cleanUp := getClient(ctx)
×
1823
        defer cleanUp()
×
1824

×
1825
        includeRawTx := ctx.Bool("include_raw_tx")
×
1826
        req := &lnrpc.PendingChannelsRequest{
×
1827
                IncludeRawTx: includeRawTx,
×
1828
        }
×
1829
        resp, err := client.PendingChannels(ctxc, req)
×
1830
        if err != nil {
×
1831
                return err
×
1832
        }
×
1833

1834
        printRespJSON(resp)
×
1835

×
1836
        return nil
×
1837
}
1838

1839
var ListChannelsCommand = cli.Command{
1840
        Name:     "listchannels",
1841
        Category: "Channels",
1842
        Usage:    "List all open channels.",
1843
        Flags: []cli.Flag{
1844
                cli.BoolFlag{
1845
                        Name:  "active_only",
1846
                        Usage: "only list channels which are currently active",
1847
                },
1848
                cli.BoolFlag{
1849
                        Name:  "inactive_only",
1850
                        Usage: "only list channels which are currently inactive",
1851
                },
1852
                cli.BoolFlag{
1853
                        Name:  "public_only",
1854
                        Usage: "only list channels which are currently public",
1855
                },
1856
                cli.BoolFlag{
1857
                        Name:  "private_only",
1858
                        Usage: "only list channels which are currently private",
1859
                },
1860
                cli.StringFlag{
1861
                        Name: "peer",
1862
                        Usage: "(optional) only display channels with a " +
1863
                                "particular peer, accepts 66-byte, " +
1864
                                "hex-encoded pubkeys",
1865
                },
1866
                cli.BoolFlag{
1867
                        Name: "skip_peer_alias_lookup",
1868
                        Usage: "skip the peer alias lookup per channel in " +
1869
                                "order to improve performance",
1870
                },
1871
        },
1872
        Action: actionDecorator(ListChannels),
1873
}
1874

1875
var listAliasesCommand = cli.Command{
1876
        Name:     "listaliases",
1877
        Category: "Channels",
1878
        Usage:    "List all aliases.",
1879
        Flags:    []cli.Flag{},
1880
        Action:   actionDecorator(listAliases),
1881
}
1882

1883
func listAliases(ctx *cli.Context) error {
×
1884
        ctxc := getContext()
×
1885
        client, cleanUp := getClient(ctx)
×
1886
        defer cleanUp()
×
1887

×
1888
        req := &lnrpc.ListAliasesRequest{}
×
1889

×
1890
        resp, err := client.ListAliases(ctxc, req)
×
1891
        if err != nil {
×
1892
                return err
×
1893
        }
×
1894

1895
        printRespJSON(resp)
×
1896

×
1897
        return nil
×
1898
}
1899

1900
func ListChannels(ctx *cli.Context) error {
×
1901
        ctxc := getContext()
×
1902
        client, cleanUp := getClient(ctx)
×
1903
        defer cleanUp()
×
1904

×
1905
        peer := ctx.String("peer")
×
1906

×
1907
        // If the user requested channels with a particular key, parse the
×
1908
        // provided pubkey.
×
1909
        var peerKey []byte
×
1910
        if len(peer) > 0 {
×
1911
                pk, err := route.NewVertexFromStr(peer)
×
1912
                if err != nil {
×
1913
                        return fmt.Errorf("invalid --peer pubkey: %w", err)
×
1914
                }
×
1915

1916
                peerKey = pk[:]
×
1917
        }
1918

1919
        // By default, we will look up the peers' alias information unless the
1920
        // skip_peer_alias_lookup flag indicates otherwise.
1921
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1922

×
1923
        req := &lnrpc.ListChannelsRequest{
×
1924
                ActiveOnly:      ctx.Bool("active_only"),
×
1925
                InactiveOnly:    ctx.Bool("inactive_only"),
×
1926
                PublicOnly:      ctx.Bool("public_only"),
×
1927
                PrivateOnly:     ctx.Bool("private_only"),
×
1928
                Peer:            peerKey,
×
1929
                PeerAliasLookup: lookupPeerAlias,
×
1930
        }
×
1931

×
1932
        resp, err := client.ListChannels(ctxc, req)
×
1933
        if err != nil {
×
1934
                return err
×
1935
        }
×
1936

1937
        printModifiedProtoJSON(resp)
×
1938

×
1939
        return nil
×
1940
}
1941

1942
var closedChannelsCommand = cli.Command{
1943
        Name:     "closedchannels",
1944
        Category: "Channels",
1945
        Usage:    "List all closed channels.",
1946
        Flags: []cli.Flag{
1947
                cli.BoolFlag{
1948
                        Name:  "cooperative",
1949
                        Usage: "list channels that were closed cooperatively",
1950
                },
1951
                cli.BoolFlag{
1952
                        Name: "local_force",
1953
                        Usage: "list channels that were force-closed " +
1954
                                "by the local node",
1955
                },
1956
                cli.BoolFlag{
1957
                        Name: "remote_force",
1958
                        Usage: "list channels that were force-closed " +
1959
                                "by the remote node",
1960
                },
1961
                cli.BoolFlag{
1962
                        Name: "breach",
1963
                        Usage: "list channels for which the remote node " +
1964
                                "attempted to broadcast a prior " +
1965
                                "revoked channel state",
1966
                },
1967
                cli.BoolFlag{
1968
                        Name:  "funding_canceled",
1969
                        Usage: "list channels that were never fully opened",
1970
                },
1971
                cli.BoolFlag{
1972
                        Name: "abandoned",
1973
                        Usage: "list channels that were abandoned by " +
1974
                                "the local node",
1975
                },
1976
        },
1977
        Action: actionDecorator(closedChannels),
1978
}
1979

1980
func closedChannels(ctx *cli.Context) error {
×
1981
        ctxc := getContext()
×
1982
        client, cleanUp := getClient(ctx)
×
1983
        defer cleanUp()
×
1984

×
1985
        req := &lnrpc.ClosedChannelsRequest{
×
1986
                Cooperative:     ctx.Bool("cooperative"),
×
1987
                LocalForce:      ctx.Bool("local_force"),
×
1988
                RemoteForce:     ctx.Bool("remote_force"),
×
1989
                Breach:          ctx.Bool("breach"),
×
1990
                FundingCanceled: ctx.Bool("funding_canceled"),
×
1991
                Abandoned:       ctx.Bool("abandoned"),
×
1992
        }
×
1993

×
1994
        resp, err := client.ClosedChannels(ctxc, req)
×
1995
        if err != nil {
×
1996
                return err
×
1997
        }
×
1998

1999
        printModifiedProtoJSON(resp)
×
2000

×
2001
        return nil
×
2002
}
2003

2004
var describeGraphCommand = cli.Command{
2005
        Name:     "describegraph",
2006
        Category: "Graph",
2007
        Description: "Prints a human readable version of the known channel " +
2008
                "graph from the PoV of the node",
2009
        Usage: "Describe the network graph.",
2010
        Flags: []cli.Flag{
2011
                cli.BoolFlag{
2012
                        Name: "include_unannounced",
2013
                        Usage: "If set, unannounced channels will be included in the " +
2014
                                "graph. Unannounced channels are both private channels, and " +
2015
                                "public channels that are not yet announced to the network.",
2016
                },
2017
                cli.BoolFlag{
2018
                        Name: "include_auth_proof",
2019
                        Usage: "If set, will include announcements' " +
2020
                                "signatures into ChannelEdge.",
2021
                },
2022
        },
2023
        Action: actionDecorator(describeGraph),
2024
}
2025

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

×
2031
        req := &lnrpc.ChannelGraphRequest{
×
2032
                IncludeUnannounced: ctx.Bool("include_unannounced"),
×
2033
                IncludeAuthProof:   ctx.Bool("include_auth_proof"),
×
2034
        }
×
2035

×
2036
        graph, err := client.DescribeGraph(ctxc, req)
×
2037
        if err != nil {
×
2038
                return err
×
2039
        }
×
2040

2041
        printRespJSON(graph)
×
2042
        return nil
×
2043
}
2044

2045
var getNodeMetricsCommand = cli.Command{
2046
        Name:        "getnodemetrics",
2047
        Category:    "Graph",
2048
        Description: "Prints out node metrics calculated from the current graph",
2049
        Usage:       "Get node metrics.",
2050
        Action:      actionDecorator(getNodeMetrics),
2051
}
2052

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

×
2058
        req := &lnrpc.NodeMetricsRequest{
×
2059
                Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
×
2060
        }
×
2061

×
2062
        nodeMetrics, err := client.GetNodeMetrics(ctxc, req)
×
2063
        if err != nil {
×
2064
                return err
×
2065
        }
×
2066

2067
        printRespJSON(nodeMetrics)
×
2068
        return nil
×
2069
}
2070

2071
var getChanInfoCommand = cli.Command{
2072
        Name:     "getchaninfo",
2073
        Category: "Graph",
2074
        Usage:    "Get the state of a channel.",
2075
        Description: "Prints out the latest authenticated state for a " +
2076
                "particular channel",
2077
        ArgsUsage: "chan_id",
2078
        Flags: []cli.Flag{
2079
                cli.Uint64Flag{
2080
                        Name: "chan_id",
2081
                        Usage: "The 8-byte compact channel ID to query for. " +
2082
                                "If this is set the chan_point param is " +
2083
                                "ignored.",
2084
                },
2085
                cli.StringFlag{
2086
                        Name: "chan_point",
2087
                        Usage: "The channel point in format txid:index. If " +
2088
                                "the chan_id param is set this param is " +
2089
                                "ignored.",
2090
                },
2091
                cli.BoolFlag{
2092
                        Name: "include_auth_proof",
2093
                        Usage: "If set, will include announcements' " +
2094
                                "signatures into ChannelEdge.",
2095
                },
2096
        },
2097
        Action: actionDecorator(getChanInfo),
2098
}
2099

2100
func getChanInfo(ctx *cli.Context) error {
×
2101
        ctxc := getContext()
×
2102
        client, cleanUp := getClient(ctx)
×
2103
        defer cleanUp()
×
2104

×
2105
        var (
×
2106
                chanID    uint64
×
2107
                chanPoint string
×
2108
                err       error
×
2109
        )
×
2110

×
2111
        switch {
×
2112
        case ctx.IsSet("chan_id"):
×
2113
                chanID = ctx.Uint64("chan_id")
×
2114

2115
        case ctx.Args().Present():
×
2116
                chanID, err = strconv.ParseUint(ctx.Args().First(), 10, 64)
×
2117
                if err != nil {
×
2118
                        return fmt.Errorf("error parsing chan_id: %w", err)
×
2119
                }
×
2120

2121
        case ctx.IsSet("chan_point"):
×
2122
                chanPoint = ctx.String("chan_point")
×
2123

2124
        default:
×
2125
                return fmt.Errorf("chan_id or chan_point argument missing")
×
2126
        }
2127

2128
        req := &lnrpc.ChanInfoRequest{
×
2129
                ChanId:           chanID,
×
2130
                ChanPoint:        chanPoint,
×
2131
                IncludeAuthProof: ctx.Bool("include_auth_proof"),
×
2132
        }
×
2133

×
2134
        chanInfo, err := client.GetChanInfo(ctxc, req)
×
2135
        if err != nil {
×
2136
                return err
×
2137
        }
×
2138

2139
        printRespJSON(chanInfo)
×
2140
        return nil
×
2141
}
2142

2143
var getNodeInfoCommand = cli.Command{
2144
        Name:     "getnodeinfo",
2145
        Category: "Graph",
2146
        Usage:    "Get information on a specific node.",
2147
        Description: "Prints out the latest authenticated node state for an " +
2148
                "advertised node",
2149
        Flags: []cli.Flag{
2150
                cli.StringFlag{
2151
                        Name: "pub_key",
2152
                        Usage: "the 33-byte hex-encoded compressed public of the target " +
2153
                                "node",
2154
                },
2155
                cli.BoolFlag{
2156
                        Name: "include_channels",
2157
                        Usage: "if true, will return all known channels " +
2158
                                "associated with the node",
2159
                },
2160
                cli.BoolFlag{
2161
                        Name: "include_auth_proof",
2162
                        Usage: "If set, will include announcements' " +
2163
                                "signatures into ChannelEdge. Depends on " +
2164
                                "include_channels",
2165
                },
2166
        },
2167
        Action: actionDecorator(getNodeInfo),
2168
}
2169

2170
func getNodeInfo(ctx *cli.Context) error {
×
2171
        ctxc := getContext()
×
2172
        client, cleanUp := getClient(ctx)
×
2173
        defer cleanUp()
×
2174

×
2175
        args := ctx.Args()
×
2176

×
2177
        var pubKey string
×
2178
        switch {
×
2179
        case ctx.IsSet("pub_key"):
×
2180
                pubKey = ctx.String("pub_key")
×
2181
        case args.Present():
×
2182
                pubKey = args.First()
×
2183
        default:
×
2184
                return fmt.Errorf("pub_key argument missing")
×
2185
        }
2186

2187
        req := &lnrpc.NodeInfoRequest{
×
2188
                PubKey:           pubKey,
×
2189
                IncludeChannels:  ctx.Bool("include_channels"),
×
2190
                IncludeAuthProof: ctx.Bool("include_auth_proof"),
×
2191
        }
×
2192

×
2193
        nodeInfo, err := client.GetNodeInfo(ctxc, req)
×
2194
        if err != nil {
×
2195
                return err
×
2196
        }
×
2197

2198
        printRespJSON(nodeInfo)
×
2199
        return nil
×
2200
}
2201

2202
var getNetworkInfoCommand = cli.Command{
2203
        Name:     "getnetworkinfo",
2204
        Category: "Channels",
2205
        Usage: "Get statistical information about the current " +
2206
                "state of the network.",
2207
        Description: "Returns a set of statistics pertaining to the known " +
2208
                "channel graph",
2209
        Action: actionDecorator(getNetworkInfo),
2210
}
2211

2212
func getNetworkInfo(ctx *cli.Context) error {
×
2213
        ctxc := getContext()
×
2214
        client, cleanUp := getClient(ctx)
×
2215
        defer cleanUp()
×
2216

×
2217
        req := &lnrpc.NetworkInfoRequest{}
×
2218

×
2219
        netInfo, err := client.GetNetworkInfo(ctxc, req)
×
2220
        if err != nil {
×
2221
                return err
×
2222
        }
×
2223

2224
        printRespJSON(netInfo)
×
2225
        return nil
×
2226
}
2227

2228
var debugLevelCommand = cli.Command{
2229
        Name:  "debuglevel",
2230
        Usage: "Set the debug level.",
2231
        Description: `Logging level for all subsystems {trace, debug, info, warn, error, critical, off}
2232
        You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems
2233

2234
        Use show to list available subsystems`,
2235
        Flags: []cli.Flag{
2236
                cli.BoolFlag{
2237
                        Name:  "show",
2238
                        Usage: "if true, then the list of available sub-systems will be printed out",
2239
                },
2240
                cli.StringFlag{
2241
                        Name:  "level",
2242
                        Usage: "the level specification to target either a coarse logging level, or granular set of specific sub-systems with logging levels for each",
2243
                },
2244
        },
2245
        Action: actionDecorator(debugLevel),
2246
}
2247

2248
func debugLevel(ctx *cli.Context) error {
×
2249
        ctxc := getContext()
×
2250
        client, cleanUp := getClient(ctx)
×
2251
        defer cleanUp()
×
2252
        req := &lnrpc.DebugLevelRequest{
×
2253
                Show:      ctx.Bool("show"),
×
2254
                LevelSpec: ctx.String("level"),
×
2255
        }
×
2256

×
2257
        resp, err := client.DebugLevel(ctxc, req)
×
2258
        if err != nil {
×
2259
                return err
×
2260
        }
×
2261

2262
        printRespJSON(resp)
×
2263
        return nil
×
2264
}
2265

2266
var listChainTxnsCommand = cli.Command{
2267
        Name:     "listchaintxns",
2268
        Category: "On-chain",
2269
        Usage:    "List transactions from the wallet.",
2270
        Flags: []cli.Flag{
2271
                cli.Int64Flag{
2272
                        Name: "start_height",
2273
                        Usage: "the block height from which to list " +
2274
                                "transactions, inclusive",
2275
                },
2276
                cli.Int64Flag{
2277
                        Name: "end_height",
2278
                        Usage: "the block height until which to list " +
2279
                                "transactions, inclusive; by default this " +
2280
                                "will return all transactions up to the " +
2281
                                "chain tip including unconfirmed " +
2282
                                "transactions",
2283
                        Value: -1,
2284
                },
2285
                cli.UintFlag{
2286
                        Name: "index_offset",
2287
                        Usage: "the index of a transaction that will be " +
2288
                                "used in a query to determine which " +
2289
                                "transaction should be returned in the " +
2290
                                "response",
2291
                },
2292
                cli.IntFlag{
2293
                        Name: "max_transactions",
2294
                        Usage: "the max number of transactions to " +
2295
                                "return; leave at default of 0 to return " +
2296
                                "all transactions",
2297
                        Value: 0,
2298
                },
2299
        },
2300
        Description: `
2301
        List all transactions an address of the wallet was involved in.
2302

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

2306
        By default, this call will get all transactions until the chain tip, 
2307
        including unconfirmed transactions (end_height=-1).`,
2308
        Action: actionDecorator(listChainTxns),
2309
}
2310

2311
func parseBlockHeightInputs(ctx *cli.Context) (int32, int32, error) {
7✔
2312
        startHeight := int32(ctx.Int64("start_height"))
7✔
2313
        endHeight := int32(ctx.Int64("end_height"))
7✔
2314

7✔
2315
        if ctx.IsSet("start_height") && ctx.IsSet("end_height") {
14✔
2316
                if endHeight != -1 && startHeight > endHeight {
8✔
2317
                        return startHeight, endHeight,
1✔
2318
                                errors.New("start_height should " +
1✔
2319
                                        "be less than end_height if " +
1✔
2320
                                        "end_height is not equal to -1")
1✔
2321
                }
1✔
2322
        }
2323

2324
        if startHeight < 0 {
7✔
2325
                return startHeight, endHeight,
1✔
2326
                        errors.New("start_height should " +
1✔
2327
                                "be greater than or " +
1✔
2328
                                "equal to 0")
1✔
2329
        }
1✔
2330

2331
        return startHeight, endHeight, nil
5✔
2332
}
2333

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

×
2339
        startHeight, endHeight, err := parseBlockHeightInputs(ctx)
×
2340
        if err != nil {
×
2341
                return err
×
2342
        }
×
2343

2344
        req := &lnrpc.GetTransactionsRequest{
×
2345
                IndexOffset:     uint32(ctx.Uint64("index_offset")),
×
2346
                MaxTransactions: uint32(ctx.Uint64("max_transactions")),
×
2347
                StartHeight:     startHeight,
×
2348
                EndHeight:       endHeight,
×
2349
        }
×
2350

×
2351
        resp, err := client.GetTransactions(ctxc, req)
×
2352
        if err != nil {
×
2353
                return err
×
2354
        }
×
2355

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

2360
var stopCommand = cli.Command{
2361
        Name:  "stop",
2362
        Usage: "Stop and shutdown the daemon.",
2363
        Description: `
2364
        Gracefully stop all daemon subsystems before stopping the daemon itself.
2365
        This is equivalent to stopping it using CTRL-C.`,
2366
        Action: actionDecorator(stopDaemon),
2367
}
2368

2369
func stopDaemon(ctx *cli.Context) error {
×
2370
        ctxc := getContext()
×
2371
        client, cleanUp := getClient(ctx)
×
2372
        defer cleanUp()
×
2373

×
2374
        resp, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2375
        if err != nil {
×
2376
                return err
×
2377
        }
×
2378

2379
        printRespJSON(resp)
×
2380

×
2381
        return nil
×
2382
}
2383

2384
var signMessageCommand = cli.Command{
2385
        Name:      "signmessage",
2386
        Category:  "Wallet",
2387
        Usage:     "Sign a message with the node's private key.",
2388
        ArgsUsage: "msg",
2389
        Description: `
2390
        Sign msg with the resident node's private key.
2391
        Returns the signature as a zbase32 string.
2392

2393
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2394
        Flags: []cli.Flag{
2395
                cli.StringFlag{
2396
                        Name:  "msg",
2397
                        Usage: "the message to sign",
2398
                },
2399
        },
2400
        Action: actionDecorator(signMessage),
2401
}
2402

2403
func signMessage(ctx *cli.Context) error {
×
2404
        ctxc := getContext()
×
2405
        client, cleanUp := getClient(ctx)
×
2406
        defer cleanUp()
×
2407

×
2408
        var msg []byte
×
2409

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

2419
        resp, err := client.SignMessage(ctxc, &lnrpc.SignMessageRequest{Msg: msg})
×
2420
        if err != nil {
×
2421
                return err
×
2422
        }
×
2423

2424
        printRespJSON(resp)
×
2425
        return nil
×
2426
}
2427

2428
var verifyMessageCommand = cli.Command{
2429
        Name:      "verifymessage",
2430
        Category:  "Wallet",
2431
        Usage:     "Verify a message signed with the signature.",
2432
        ArgsUsage: "msg signature",
2433
        Description: `
2434
        Verify that the message was signed with a properly-formed signature
2435
        The signature must be zbase32 encoded and signed with the private key of
2436
        an active node in the resident node's channel database.
2437

2438
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2439
        Flags: []cli.Flag{
2440
                cli.StringFlag{
2441
                        Name:  "msg",
2442
                        Usage: "the message to verify",
2443
                },
2444
                cli.StringFlag{
2445
                        Name:  "sig",
2446
                        Usage: "the zbase32 encoded signature of the message",
2447
                },
2448
        },
2449
        Action: actionDecorator(verifyMessage),
2450
}
2451

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

×
2457
        var (
×
2458
                msg []byte
×
2459
                sig string
×
2460
        )
×
2461

×
2462
        args := ctx.Args()
×
2463

×
2464
        switch {
×
2465
        case ctx.IsSet("msg"):
×
2466
                msg = []byte(ctx.String("msg"))
×
2467
        case args.Present():
×
2468
                msg = []byte(ctx.Args().First())
×
2469
                args = args.Tail()
×
2470
        default:
×
2471
                return fmt.Errorf("msg argument missing")
×
2472
        }
2473

2474
        switch {
×
2475
        case ctx.IsSet("sig"):
×
2476
                sig = ctx.String("sig")
×
2477
        case args.Present():
×
2478
                sig = args.First()
×
2479
        default:
×
2480
                return fmt.Errorf("signature argument missing")
×
2481
        }
2482

2483
        req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
×
2484
        resp, err := client.VerifyMessage(ctxc, req)
×
2485
        if err != nil {
×
2486
                return err
×
2487
        }
×
2488

2489
        printRespJSON(resp)
×
2490
        return nil
×
2491
}
2492

2493
var feeReportCommand = cli.Command{
2494
        Name:     "feereport",
2495
        Category: "Channels",
2496
        Usage:    "Display the current fee policies of all active channels.",
2497
        Description: `
2498
        Returns the current fee policies of all active channels.
2499
        Fee policies can be updated using the updatechanpolicy command.`,
2500
        Action: actionDecorator(feeReport),
2501
}
2502

2503
func feeReport(ctx *cli.Context) error {
×
2504
        ctxc := getContext()
×
2505
        client, cleanUp := getClient(ctx)
×
2506
        defer cleanUp()
×
2507

×
2508
        req := &lnrpc.FeeReportRequest{}
×
2509
        resp, err := client.FeeReport(ctxc, req)
×
2510
        if err != nil {
×
2511
                return err
×
2512
        }
×
2513

2514
        printRespJSON(resp)
×
2515
        return nil
×
2516
}
2517

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

2624
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
7✔
2625
        split := strings.Split(s, ":")
7✔
2626
        if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
10✔
2627
                return nil, errBadChanPoint
3✔
2628
        }
3✔
2629

2630
        index, err := strconv.ParseInt(split[1], 10, 64)
4✔
2631
        if err != nil {
5✔
2632
                return nil, fmt.Errorf("unable to decode output index: %w", err)
1✔
2633
        }
1✔
2634

2635
        txid, err := chainhash.NewHashFromStr(split[0])
3✔
2636
        if err != nil {
4✔
2637
                return nil, fmt.Errorf("unable to parse hex string: %w", err)
1✔
2638
        }
1✔
2639

2640
        return &lnrpc.ChannelPoint{
2✔
2641
                FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
2✔
2642
                        FundingTxidBytes: txid[:],
2✔
2643
                },
2✔
2644
                OutputIndex: uint32(index),
2✔
2645
        }, nil
2✔
2646
}
2647

2648
// parseTimeLockDelta is expected to get a uint16 type of timeLockDelta. Its
2649
// maximum value is MaxTimeLockDelta.
2650
func parseTimeLockDelta(timeLockDeltaStr string) (uint16, error) {
5✔
2651
        timeLockDeltaUnCheck, err := strconv.ParseUint(timeLockDeltaStr, 10, 64)
5✔
2652
        if err != nil {
7✔
2653
                return 0, fmt.Errorf("failed to parse time_lock_delta: %s "+
2✔
2654
                        "to uint64, err: %v", timeLockDeltaStr, err)
2✔
2655
        }
2✔
2656

2657
        if timeLockDeltaUnCheck > routing.MaxCLTVDelta {
3✔
2658
                return 0, fmt.Errorf("time_lock_delta is too big, "+
×
2659
                        "max value is %d", routing.MaxCLTVDelta)
×
2660
        }
×
2661

2662
        return uint16(timeLockDeltaUnCheck), nil
3✔
2663
}
2664

2665
func updateChannelPolicy(ctx *cli.Context) error {
×
2666
        ctxc := getContext()
×
2667
        client, cleanUp := getClient(ctx)
×
2668
        defer cleanUp()
×
2669

×
2670
        var (
×
2671
                baseFee       int64
×
2672
                feeRate       float64
×
2673
                feeRatePpm    uint64
×
2674
                timeLockDelta uint16
×
2675
                err           error
×
2676
        )
×
2677
        args := ctx.Args()
×
2678

×
2679
        switch {
×
2680
        case ctx.IsSet("base_fee_msat"):
×
2681
                baseFee = ctx.Int64("base_fee_msat")
×
2682
        case args.Present():
×
2683
                baseFee, err = strconv.ParseInt(args.First(), 10, 64)
×
2684
                if err != nil {
×
2685
                        return fmt.Errorf("unable to decode base_fee_msat: %w",
×
2686
                                err)
×
2687
                }
×
2688
                args = args.Tail()
×
2689
        default:
×
2690
                return fmt.Errorf("base_fee_msat argument missing")
×
2691
        }
2692

2693
        switch {
×
2694
        case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
×
2695
                return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
×
2696
        case ctx.IsSet("fee_rate"):
×
2697
                feeRate = ctx.Float64("fee_rate")
×
2698
        case ctx.IsSet("fee_rate_ppm"):
×
2699
                feeRatePpm = ctx.Uint64("fee_rate_ppm")
×
2700
        case args.Present():
×
2701
                feeRate, err = strconv.ParseFloat(args.First(), 64)
×
2702
                if err != nil {
×
2703
                        return fmt.Errorf("unable to decode fee_rate: %w", err)
×
2704
                }
×
2705

2706
                args = args.Tail()
×
2707
        default:
×
2708
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
×
2709
        }
2710

2711
        switch {
×
2712
        case ctx.IsSet("time_lock_delta"):
×
2713
                timeLockDeltaStr := ctx.String("time_lock_delta")
×
2714
                timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
×
2715
                if err != nil {
×
2716
                        return err
×
2717
                }
×
2718
        case args.Present():
×
2719
                timeLockDelta, err = parseTimeLockDelta(args.First())
×
2720
                if err != nil {
×
2721
                        return err
×
2722
                }
×
2723

2724
                args = args.Tail()
×
2725
        default:
×
2726
                return fmt.Errorf("time_lock_delta argument missing")
×
2727
        }
2728

2729
        var (
×
2730
                chanPoint    *lnrpc.ChannelPoint
×
2731
                chanPointStr string
×
2732
        )
×
2733

×
2734
        switch {
×
2735
        case ctx.IsSet("chan_point"):
×
2736
                chanPointStr = ctx.String("chan_point")
×
2737
        case args.Present():
×
2738
                chanPointStr = args.First()
×
2739
        }
2740

2741
        if chanPointStr != "" {
×
2742
                chanPoint, err = parseChanPoint(chanPointStr)
×
2743
                if err != nil {
×
2744
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2745
                }
×
2746
        }
2747

2748
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2749
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2750
                inboundBaseFeeMsat > math.MaxInt32 {
×
2751

×
2752
                return errors.New("inbound_base_fee_msat out of range")
×
2753
        }
×
2754

2755
        inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
×
2756
        if inboundFeeRatePpm < math.MinInt32 ||
×
2757
                inboundFeeRatePpm > math.MaxInt32 {
×
2758

×
2759
                return errors.New("inbound_fee_rate_ppm out of range")
×
2760
        }
×
2761

2762
        // Inbound fees are optional. However, if an update is required,
2763
        // both the base fee and the fee rate must be provided.
2764
        var inboundFee *lnrpc.InboundFee
×
2765
        if ctx.IsSet("inbound_base_fee_msat") !=
×
2766
                ctx.IsSet("inbound_fee_rate_ppm") {
×
2767

×
2768
                return errors.New("both parameters must be provided: " +
×
2769
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
×
2770
        }
×
2771

2772
        if ctx.IsSet("inbound_fee_rate_ppm") {
×
2773
                inboundFee = &lnrpc.InboundFee{
×
2774
                        BaseFeeMsat: int32(inboundBaseFeeMsat),
×
2775
                        FeeRatePpm:  int32(inboundFeeRatePpm),
×
2776
                }
×
2777
        }
×
2778

2779
        createMissingEdge := ctx.Bool("create_missing_edge")
×
2780

×
2781
        req := &lnrpc.PolicyUpdateRequest{
×
2782
                BaseFeeMsat:       baseFee,
×
2783
                TimeLockDelta:     uint32(timeLockDelta),
×
2784
                MaxHtlcMsat:       ctx.Uint64("max_htlc_msat"),
×
2785
                InboundFee:        inboundFee,
×
2786
                CreateMissingEdge: createMissingEdge,
×
2787
        }
×
2788

×
2789
        if ctx.IsSet("min_htlc_msat") {
×
2790
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2791
                req.MinHtlcMsatSpecified = true
×
2792
        }
×
2793

2794
        if chanPoint != nil {
×
2795
                req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
×
2796
                        ChanPoint: chanPoint,
×
2797
                }
×
2798
        } else {
×
2799
                req.Scope = &lnrpc.PolicyUpdateRequest_Global{
×
2800
                        Global: true,
×
2801
                }
×
2802
        }
×
2803

2804
        if feeRate != 0 {
×
2805
                req.FeeRate = feeRate
×
2806
        } else if feeRatePpm != 0 {
×
2807
                req.FeeRatePpm = uint32(feeRatePpm)
×
2808
        }
×
2809

2810
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2811
        if err != nil {
×
2812
                return err
×
2813
        }
×
2814

2815
        // Parse the response into the final json object that will be printed
2816
        // to stdout. At the moment, this filters out the raw txid bytes from
2817
        // each failed update's outpoint and only prints the txid string.
2818
        var listFailedUpdateResp = struct {
×
2819
                FailedUpdates []*FailedUpdate `json:"failed_updates"`
×
2820
        }{
×
2821
                FailedUpdates: make([]*FailedUpdate, 0, len(resp.FailedUpdates)),
×
2822
        }
×
2823
        for _, protoUpdate := range resp.FailedUpdates {
×
2824
                failedUpdate := NewFailedUpdateFromProto(protoUpdate)
×
2825
                listFailedUpdateResp.FailedUpdates = append(
×
2826
                        listFailedUpdateResp.FailedUpdates, failedUpdate)
×
2827
        }
×
2828

2829
        printJSON(listFailedUpdateResp)
×
2830

×
2831
        return nil
×
2832
}
2833

2834
var fishCompletionCommand = cli.Command{
2835
        Name:   "fish-completion",
2836
        Hidden: true,
2837
        Action: func(c *cli.Context) error {
×
2838
                completion, err := c.App.ToFishCompletion()
×
2839
                if err != nil {
×
2840
                        return err
×
2841
                }
×
2842

2843
                // We don't want to suggest files, so we add this
2844
                // first line to the completions.
2845
                _, err = fmt.Printf("complete -c %q -f \n%s", c.App.Name, completion)
×
2846
                return err
×
2847
        },
2848
}
2849

2850
var exportChanBackupCommand = cli.Command{
2851
        Name:     "exportchanbackup",
2852
        Category: "Channels",
2853
        Usage: "Obtain a static channel back up for a selected channels, " +
2854
                "or all known channels.",
2855
        ArgsUsage: "[chan_point] [--all] [--output_file]",
2856
        Description: `
2857
        This command allows a user to export a Static Channel Backup (SCB) for
2858
        a selected channel. SCB's are encrypted backups of a channel's initial
2859
        state that are encrypted with a key derived from the seed of a user. In
2860
        the case of partial or complete data loss, the SCB will allow the user
2861
        to reclaim settled funds in the channel at its final state. The
2862
        exported channel backups can be restored at a later time using the
2863
        restorechanbackup command.
2864

2865
        This command will return one of two types of channel backups depending
2866
        on the set of passed arguments:
2867

2868
           * If a target channel point is specified, then a single channel
2869
             backup containing only the information for that channel will be
2870
             returned.
2871

2872
           * If the --all flag is passed, then a multi-channel backup will be
2873
             returned. A multi backup is a single encrypted blob (displayed in
2874
             hex encoding) that contains several channels in a single cipher
2875
             text.
2876

2877
        Both of the backup types can be restored using the restorechanbackup
2878
        command.
2879
        `,
2880
        Flags: []cli.Flag{
2881
                cli.StringFlag{
2882
                        Name:  "chan_point",
2883
                        Usage: "the target channel to obtain an SCB for",
2884
                },
2885
                cli.BoolFlag{
2886
                        Name: "all",
2887
                        Usage: "if specified, then a multi backup of all " +
2888
                                "active channels will be returned",
2889
                },
2890
                cli.StringFlag{
2891
                        Name: "output_file",
2892
                        Usage: `
2893
                        if specified, then rather than printing a JSON output
2894
                        of the static channel backup, a serialized version of
2895
                        the backup (either Single or Multi) will be written to
2896
                        the target file, this is the same format used by lnd in
2897
                        its channel.backup file `,
2898
                },
2899
        },
2900
        Action: actionDecorator(exportChanBackup),
2901
}
2902

2903
func exportChanBackup(ctx *cli.Context) error {
×
2904
        ctxc := getContext()
×
2905
        client, cleanUp := getClient(ctx)
×
2906
        defer cleanUp()
×
2907

×
2908
        // Show command help if no arguments provided
×
2909
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2910
                cli.ShowCommandHelp(ctx, "exportchanbackup")
×
2911
                return nil
×
2912
        }
×
2913

2914
        var (
×
2915
                err            error
×
2916
                chanPointStr   string
×
2917
                outputFileName string
×
2918
        )
×
2919
        args := ctx.Args()
×
2920

×
2921
        switch {
×
2922
        case ctx.IsSet("chan_point"):
×
2923
                chanPointStr = ctx.String("chan_point")
×
2924

2925
        case args.Present():
×
2926
                chanPointStr = args.First()
×
2927

2928
        case !ctx.IsSet("all"):
×
2929
                return fmt.Errorf("must specify chan_point if --all isn't set")
×
2930
        }
2931

2932
        if ctx.IsSet("output_file") {
×
2933
                outputFileName = ctx.String("output_file")
×
2934
        }
×
2935

2936
        if chanPointStr != "" {
×
2937
                chanPointRPC, err := parseChanPoint(chanPointStr)
×
2938
                if err != nil {
×
2939
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2940
                }
×
2941

2942
                chanBackup, err := client.ExportChannelBackup(
×
2943
                        ctxc, &lnrpc.ExportChannelBackupRequest{
×
2944
                                ChanPoint: chanPointRPC,
×
2945
                        },
×
2946
                )
×
2947
                if err != nil {
×
2948
                        return err
×
2949
                }
×
2950

2951
                txid, err := chainhash.NewHash(
×
2952
                        chanPointRPC.GetFundingTxidBytes(),
×
2953
                )
×
2954
                if err != nil {
×
2955
                        return err
×
2956
                }
×
2957

2958
                chanPoint := wire.OutPoint{
×
2959
                        Hash:  *txid,
×
2960
                        Index: chanPointRPC.OutputIndex,
×
2961
                }
×
2962

×
2963
                if outputFileName != "" {
×
2964
                        return os.WriteFile(
×
2965
                                outputFileName,
×
2966
                                chanBackup.ChanBackup,
×
2967
                                0666,
×
2968
                        )
×
2969
                }
×
2970

2971
                printJSON(struct {
×
2972
                        ChanPoint  string `json:"chan_point"`
×
2973
                        ChanBackup string `json:"chan_backup"`
×
2974
                }{
×
2975
                        ChanPoint:  chanPoint.String(),
×
2976
                        ChanBackup: hex.EncodeToString(chanBackup.ChanBackup),
×
2977
                })
×
2978
                return nil
×
2979
        }
2980

2981
        if !ctx.IsSet("all") {
×
2982
                return fmt.Errorf("if a channel isn't specified, -all must be")
×
2983
        }
×
2984

2985
        chanBackup, err := client.ExportAllChannelBackups(
×
2986
                ctxc, &lnrpc.ChanBackupExportRequest{},
×
2987
        )
×
2988
        if err != nil {
×
2989
                return err
×
2990
        }
×
2991

2992
        if outputFileName != "" {
×
2993
                return os.WriteFile(
×
2994
                        outputFileName,
×
2995
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2996
                        0666,
×
2997
                )
×
2998
        }
×
2999

3000
        // TODO(roasbeef): support for export | restore ?
3001

3002
        var chanPoints []string
×
3003
        for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
×
3004
                txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
×
3005
                if err != nil {
×
3006
                        return err
×
3007
                }
×
3008

3009
                chanPoints = append(chanPoints, wire.OutPoint{
×
3010
                        Hash:  *txid,
×
3011
                        Index: chanPoint.OutputIndex,
×
3012
                }.String())
×
3013
        }
3014

3015
        printRespJSON(chanBackup)
×
3016

×
3017
        return nil
×
3018
}
3019

3020
var verifyChanBackupCommand = cli.Command{
3021
        Name:      "verifychanbackup",
3022
        Category:  "Channels",
3023
        Usage:     "Verify an existing channel backup.",
3024
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file]",
3025
        Description: `
3026
    This command allows a user to verify an existing Single or Multi channel
3027
    backup for integrity. This is useful when a user has a backup, but is
3028
    unsure as to if it's valid or for the target node.
3029

3030
    The command will accept backups in one of four forms:
3031

3032
       * A single channel packed SCB, which can be obtained from
3033
         exportchanbackup. This should be passed in hex encoded format.
3034

3035
       * A packed multi-channel SCB, which couples several individual
3036
         static channel backups in single blob.
3037

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

3041
       * A file path which points to a packed multi-channel backup within a
3042
         file, using the same format that lnd does in its channel.backup
3043
         file.
3044
    `,
3045
        Flags: []cli.Flag{
3046
                cli.StringFlag{
3047
                        Name: "single_backup",
3048
                        Usage: "a hex encoded single channel backup obtained " +
3049
                                "from exportchanbackup",
3050
                },
3051
                cli.StringFlag{
3052
                        Name: "multi_backup",
3053
                        Usage: "a hex encoded multi-channel backup obtained " +
3054
                                "from exportchanbackup",
3055
                },
3056

3057
                cli.StringFlag{
3058
                        Name:      "single_file",
3059
                        Usage:     "the path to a single-channel backup file",
3060
                        TakesFile: true,
3061
                },
3062

3063
                cli.StringFlag{
3064
                        Name:      "multi_file",
3065
                        Usage:     "the path to a multi-channel back up file",
3066
                        TakesFile: true,
3067
                },
3068
        },
3069
        Action: actionDecorator(verifyChanBackup),
3070
}
3071

3072
func verifyChanBackup(ctx *cli.Context) error {
×
3073
        ctxc := getContext()
×
3074
        client, cleanUp := getClient(ctx)
×
3075
        defer cleanUp()
×
3076

×
3077
        // Show command help if no arguments provided
×
3078
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3079
                cli.ShowCommandHelp(ctx, "verifychanbackup")
×
3080
                return nil
×
3081
        }
×
3082

3083
        backups, err := parseChanBackups(ctx)
×
3084
        if err != nil {
×
3085
                return err
×
3086
        }
×
3087

3088
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
3089

×
3090
        if backups.GetChanBackups() != nil {
×
3091
                verifyReq.SingleChanBackups = backups.GetChanBackups()
×
3092
        }
×
3093
        if backups.GetMultiChanBackup() != nil {
×
3094
                verifyReq.MultiChanBackup = &lnrpc.MultiChanBackup{
×
3095
                        MultiChanBackup: backups.GetMultiChanBackup(),
×
3096
                }
×
3097
        }
×
3098

3099
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
3100
        if err != nil {
×
3101
                return err
×
3102
        }
×
3103

3104
        printRespJSON(resp)
×
3105
        return nil
×
3106
}
3107

3108
var restoreChanBackupCommand = cli.Command{
3109
        Name:     "restorechanbackup",
3110
        Category: "Channels",
3111
        Usage: "Restore an existing single or multi-channel static channel " +
3112
                "backup.",
3113
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file=",
3114
        Description: `
3115
        Allows a user to restore a Static Channel Backup (SCB) that was
3116
        obtained either via the exportchanbackup command, or from lnd's
3117
        automatically managed channel.backup file. This command should be used
3118
        if a user is attempting to restore a channel due to data loss on a
3119
        running node restored with the same seed as the node that created the
3120
        channel. If successful, this command will allows the user to recover
3121
        the settled funds stored in the recovered channels.
3122

3123
        The command will accept backups in one of four forms:
3124

3125
           * A single channel packed SCB, which can be obtained from
3126
             exportchanbackup. This should be passed in hex encoded format.
3127

3128
           * A packed multi-channel SCB, which couples several individual
3129
             static channel backups in single blob.
3130

3131
           * A file path which points to a packed single-channel backup within
3132
             a file, using the same format that lnd does in its channel.backup
3133
             file.
3134

3135
           * A file path which points to a packed multi-channel backup within a
3136
             file, using the same format that lnd does in its channel.backup
3137
             file.
3138
        `,
3139
        Flags: []cli.Flag{
3140
                cli.StringFlag{
3141
                        Name: "single_backup",
3142
                        Usage: "a hex encoded single channel backup obtained " +
3143
                                "from exportchanbackup",
3144
                },
3145
                cli.StringFlag{
3146
                        Name: "multi_backup",
3147
                        Usage: "a hex encoded multi-channel backup obtained " +
3148
                                "from exportchanbackup",
3149
                },
3150

3151
                cli.StringFlag{
3152
                        Name:      "single_file",
3153
                        Usage:     "the path to a single-channel backup file",
3154
                        TakesFile: true,
3155
                },
3156

3157
                cli.StringFlag{
3158
                        Name:      "multi_file",
3159
                        Usage:     "the path to a multi-channel back up file",
3160
                        TakesFile: true,
3161
                },
3162
        },
3163
        Action: actionDecorator(restoreChanBackup),
3164
}
3165

3166
// errMissingChanBackup is an error returned when we attempt to parse a channel
3167
// backup from a CLI command, and it is missing.
3168
var errMissingChanBackup = errors.New("missing channel backup")
3169

3170
func parseChanBackups(ctx *cli.Context) (*lnrpc.RestoreChanBackupRequest, error) {
×
3171
        switch {
×
3172
        case ctx.IsSet("single_backup"):
×
3173
                packedBackup, err := hex.DecodeString(
×
3174
                        ctx.String("single_backup"),
×
3175
                )
×
3176
                if err != nil {
×
3177
                        return nil, fmt.Errorf("unable to decode single packed "+
×
3178
                                "backup: %v", err)
×
3179
                }
×
3180

3181
                return &lnrpc.RestoreChanBackupRequest{
×
3182
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3183
                                ChanBackups: &lnrpc.ChannelBackups{
×
3184
                                        ChanBackups: []*lnrpc.ChannelBackup{
×
3185
                                                {
×
3186
                                                        ChanBackup: packedBackup,
×
3187
                                                },
×
3188
                                        },
×
3189
                                },
×
3190
                        },
×
3191
                }, nil
×
3192

3193
        case ctx.IsSet("multi_backup"):
×
3194
                packedMulti, err := hex.DecodeString(
×
3195
                        ctx.String("multi_backup"),
×
3196
                )
×
3197
                if err != nil {
×
3198
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3199
                                "backup: %v", err)
×
3200
                }
×
3201

3202
                return &lnrpc.RestoreChanBackupRequest{
×
3203
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3204
                                MultiChanBackup: packedMulti,
×
3205
                        },
×
3206
                }, nil
×
3207

3208
        case ctx.IsSet("single_file"):
×
3209
                packedSingle, err := os.ReadFile(ctx.String("single_file"))
×
3210
                if err != nil {
×
3211
                        return nil, fmt.Errorf("unable to decode single "+
×
3212
                                "packed backup: %v", err)
×
3213
                }
×
3214

3215
                return &lnrpc.RestoreChanBackupRequest{
×
3216
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3217
                                ChanBackups: &lnrpc.ChannelBackups{
×
3218
                                        ChanBackups: []*lnrpc.ChannelBackup{{
×
3219
                                                ChanBackup: packedSingle,
×
3220
                                        }},
×
3221
                                },
×
3222
                        },
×
3223
                }, nil
×
3224

3225
        case ctx.IsSet("multi_file"):
×
3226
                packedMulti, err := os.ReadFile(ctx.String("multi_file"))
×
3227
                if err != nil {
×
3228
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3229
                                "backup: %v", err)
×
3230
                }
×
3231

3232
                return &lnrpc.RestoreChanBackupRequest{
×
3233
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3234
                                MultiChanBackup: packedMulti,
×
3235
                        },
×
3236
                }, nil
×
3237

3238
        default:
×
3239
                return nil, errMissingChanBackup
×
3240
        }
3241
}
3242

3243
func restoreChanBackup(ctx *cli.Context) error {
×
3244
        ctxc := getContext()
×
3245
        client, cleanUp := getClient(ctx)
×
3246
        defer cleanUp()
×
3247

×
3248
        // Show command help if no arguments provided
×
3249
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3250
                cli.ShowCommandHelp(ctx, "restorechanbackup")
×
3251
                return nil
×
3252
        }
×
3253

3254
        var req lnrpc.RestoreChanBackupRequest
×
3255

×
3256
        backups, err := parseChanBackups(ctx)
×
3257
        if err != nil {
×
3258
                return err
×
3259
        }
×
3260

3261
        req.Backup = backups.Backup
×
3262

×
3263
        resp, err := client.RestoreChannelBackups(ctxc, &req)
×
3264
        if err != nil {
×
3265
                return fmt.Errorf("unable to restore chan backups: %w", err)
×
3266
        }
×
3267

3268
        printRespJSON(resp)
×
3269

×
3270
        return nil
×
3271
}
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