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

lightningnetwork / lnd / 17774622768

16 Sep 2025 05:57PM UTC coverage: 66.505% (-0.2%) from 66.657%
17774622768

Pull #10067

github

web-flow
Merge 4ec7abb62 into cbed86e21
Pull Request #10067: add sats_per_kweight option when crafting a transaction (continue)

72 of 233 new or added lines in 13 files covered. (30.9%)

355 existing lines in 32 files now uncovered.

136113 of 204666 relevant lines covered (66.5%)

21396.84 hits per line

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

8.14
/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
        },
406
        Action: actionDecorator(estimateFees),
407
}
408

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

642
        // Parse fee rate from --sat_per_vbyte and convert to sat/kw.
NEW
643
        satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
×
NEW
644
        if err != nil {
×
NEW
645
                return err
×
NEW
646
        }
×
647

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

×
655
                confirm := promptForConfirmation("Confirm payment (yes/no): ")
×
656
                if !confirm {
×
657
                        return nil
×
658
                }
×
659
        }
660

661
        req := &lnrpc.SendCoinsRequest{
×
662
                Addr:                  addr,
×
663
                Amount:                amt,
×
664
                TargetConf:            int32(ctx.Int64("conf_target")),
×
665
                SendAll:               ctx.Bool("sweepall"),
×
666
                Label:                 ctx.String(txLabelFlag.Name),
×
667
                MinConfs:              minConfs,
×
668
                SpendUnconfirmed:      minConfs == 0,
×
669
                CoinSelectionStrategy: coinSelectionStrategy,
×
670
                Outpoints:             outpoints,
×
NEW
671
                SatPerKw:              satPerKw,
×
672
        }
×
673
        txid, err := client.SendCoins(ctxc, req)
×
674
        if err != nil {
×
675
                return err
×
676
        }
×
677

678
        printRespJSON(txid)
×
679
        return nil
×
680
}
681

682
func isSameOutpoint(a, b *lnrpc.OutPoint) bool {
×
683
        return a.TxidStr == b.TxidStr && a.OutputIndex == b.OutputIndex
×
684
}
×
685

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

724
func listUnspent(ctx *cli.Context) error {
×
725
        var (
×
726
                minConfirms int64
×
727
                maxConfirms int64
×
728
                err         error
×
729
        )
×
730
        ctxc := getContext()
×
731
        args := ctx.Args()
×
732

×
733
        if ctx.IsSet("max_confs") && !ctx.IsSet("min_confs") {
×
734
                return fmt.Errorf("max_confs cannot be set without " +
×
735
                        "min_confs being set")
×
736
        }
×
737

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

750
        switch {
×
751
        case ctx.IsSet("max_confs"):
×
752
                maxConfirms = ctx.Int64("max_confs")
×
753
        case args.Present():
×
754
                maxConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
755
                if err != nil {
×
756
                        cli.ShowCommandHelp(ctx, "listunspent")
×
757
                        return nil
×
758
                }
×
759
                args = args.Tail()
×
760
        }
761

762
        unconfirmedOnly := ctx.Bool("unconfirmed_only")
×
763

×
764
        // Force minConfirms and maxConfirms to be zero if unconfirmedOnly is
×
765
        // true.
×
766
        if unconfirmedOnly && (minConfirms != 0 || maxConfirms != 0) {
×
767
                cli.ShowCommandHelp(ctx, "listunspent")
×
768
                return nil
×
769
        }
×
770

771
        // When unconfirmedOnly is inactive, we will override maxConfirms to be
772
        // a MaxInt32 to return all confirmed and unconfirmed utxos.
773
        if maxConfirms == 0 && !unconfirmedOnly {
×
774
                maxConfirms = math.MaxInt32
×
775
        }
×
776

777
        client, cleanUp := getClient(ctx)
×
778
        defer cleanUp()
×
779

×
780
        req := &lnrpc.ListUnspentRequest{
×
781
                MinConfs: int32(minConfirms),
×
782
                MaxConfs: int32(maxConfirms),
×
783
        }
×
784
        resp, err := client.ListUnspent(ctxc, req)
×
785
        if err != nil {
×
786
                return err
×
787
        }
×
788

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

802
        printJSON(listUnspentResp)
×
803

×
804
        return nil
×
805
}
806

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

815
        The send-json-string' param decodes addresses and the amount to send
816
        respectively in the following format:
817

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

850
func sendMany(ctx *cli.Context) error {
×
851
        ctxc := getContext()
×
852
        var amountToAddr map[string]int64
×
853

×
854
        jsonMap := ctx.Args().First()
×
855
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
856
                return err
×
857
        }
×
858

859
        // Check that only the field sat_per_vbyte or the deprecated field
860
        // sat_per_byte is used.
861
        feeRateFlag, err := checkNotBothSet(
×
862
                ctx, "sat_per_vbyte", "sat_per_byte",
×
863
        )
×
864
        if err != nil {
×
865
                return err
×
866
        }
×
867

868
        // Only fee rate flag or conf_target should be set, not both.
869
        if _, err := checkNotBothSet(
×
870
                ctx, feeRateFlag, "conf_target",
×
871
        ); err != nil {
×
872
                return err
×
873
        }
×
874

875
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
876
        if err != nil {
×
877
                return err
×
878
        }
×
879

880
        client, cleanUp := getClient(ctx)
×
881
        defer cleanUp()
×
882

×
NEW
883
        // Parse fee rate from --sat_per_vbyte and convert to sat/kw.
×
NEW
884
        satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
×
NEW
885
        if err != nil {
×
NEW
886
                return err
×
NEW
887
        }
×
888

889
        minConfs := int32(ctx.Uint64("min_confs"))
×
890
        txid, err := client.SendMany(ctxc, &lnrpc.SendManyRequest{
×
891
                AddrToAmount:          amountToAddr,
×
892
                TargetConf:            int32(ctx.Int64("conf_target")),
×
893
                Label:                 ctx.String(txLabelFlag.Name),
×
894
                MinConfs:              minConfs,
×
895
                SpendUnconfirmed:      minConfs == 0,
×
896
                CoinSelectionStrategy: coinSelectionStrategy,
×
NEW
897
                SatPerKw:              satPerKw,
×
898
        })
×
899
        if err != nil {
×
900
                return err
×
901
        }
×
902

903
        printRespJSON(txid)
×
904
        return nil
×
905
}
906

907
var connectCommand = cli.Command{
908
        Name:      "connect",
909
        Category:  "Peers",
910
        Usage:     "Connect to a remote lightning peer.",
911
        ArgsUsage: "<pubkey>@host",
912
        Description: `
913
        Connect to a peer using its <pubkey> and host.
914

915
        A custom timeout on the connection is supported. For instance, to timeout
916
        the connection request in 30 seconds, use the following:
917

918
        lncli connect <pubkey>@host --timeout 30s
919
        `,
920
        Flags: []cli.Flag{
921
                cli.BoolFlag{
922
                        Name: "perm",
923
                        Usage: "If set, the daemon will attempt to persistently " +
924
                                "connect to the target peer.\n" +
925
                                "           If not, the call will be synchronous.",
926
                },
927
                cli.DurationFlag{
928
                        Name: "timeout",
929
                        Usage: "The connection timeout value for current request. " +
930
                                "Valid uints are {ms, s, m, h}.\n" +
931
                                "If not set, the global connection " +
932
                                "timeout value (default to 120s) is used.",
933
                },
934
        },
935
        Action: actionDecorator(connectPeer),
936
}
937

938
func connectPeer(ctx *cli.Context) error {
×
939
        ctxc := getContext()
×
940
        client, cleanUp := getClient(ctx)
×
941
        defer cleanUp()
×
942

×
943
        targetAddress := ctx.Args().First()
×
944
        splitAddr := strings.Split(targetAddress, "@")
×
945
        if len(splitAddr) != 2 {
×
946
                return fmt.Errorf("target address expected in format: " +
×
947
                        "pubkey@host:port")
×
948
        }
×
949

950
        addr := &lnrpc.LightningAddress{
×
951
                Pubkey: splitAddr[0],
×
952
                Host:   splitAddr[1],
×
953
        }
×
954
        req := &lnrpc.ConnectPeerRequest{
×
955
                Addr:    addr,
×
956
                Perm:    ctx.Bool("perm"),
×
957
                Timeout: uint64(ctx.Duration("timeout").Seconds()),
×
958
        }
×
959

×
960
        lnid, err := client.ConnectPeer(ctxc, req)
×
961
        if err != nil {
×
962
                return err
×
963
        }
×
964

965
        printRespJSON(lnid)
×
966
        return nil
×
967
}
968

969
var disconnectCommand = cli.Command{
970
        Name:     "disconnect",
971
        Category: "Peers",
972
        Usage: "Disconnect a remote lightning peer identified by " +
973
                "public key.",
974
        ArgsUsage: "<pubkey>",
975
        Flags: []cli.Flag{
976
                cli.StringFlag{
977
                        Name: "node_key",
978
                        Usage: "The hex-encoded compressed public key of the peer " +
979
                                "to disconnect from",
980
                },
981
        },
982
        Action: actionDecorator(disconnectPeer),
983
}
984

985
func disconnectPeer(ctx *cli.Context) error {
×
986
        ctxc := getContext()
×
987
        client, cleanUp := getClient(ctx)
×
988
        defer cleanUp()
×
989

×
990
        var pubKey string
×
991
        switch {
×
992
        case ctx.IsSet("node_key"):
×
993
                pubKey = ctx.String("node_key")
×
994
        case ctx.Args().Present():
×
995
                pubKey = ctx.Args().First()
×
996
        default:
×
997
                return fmt.Errorf("must specify target public key")
×
998
        }
999

1000
        req := &lnrpc.DisconnectPeerRequest{
×
1001
                PubKey: pubKey,
×
1002
        }
×
1003

×
1004
        lnid, err := client.DisconnectPeer(ctxc, req)
×
1005
        if err != nil {
×
1006
                return err
×
1007
        }
×
1008

1009
        printRespJSON(lnid)
×
1010
        return nil
×
1011
}
1012

1013
// TODO(roasbeef): also allow short relative channel ID.
1014

1015
var closeChannelCommand = cli.Command{
1016
        Name:     "closechannel",
1017
        Category: "Channels",
1018
        Usage:    "Close an existing channel.",
1019
        Description: `
1020
        Close an existing channel. The channel can be closed either cooperatively,
1021
        or unilaterally (--force).
1022

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

1027
        In the case of a cooperative closure, one can manually set the fee to
1028
        be used for the closing transaction via either the --conf_target or
1029
        --sat_per_vbyte arguments. This will be the starting value used during
1030
        fee negotiation. This is optional. The parameter --max_fee_rate in
1031
        comparison is the end boundary of the fee negotiation, if not specified
1032
        it's always x3 of the starting value. Increasing this value increases
1033
        the chance of a successful negotiation.
1034
        Moreover if the channel has active HTLCs on it, the coop close will
1035
        wait until all HTLCs are resolved and will not allow any new HTLCs on
1036
        the channel. The channel will appear as disabled in the listchannels
1037
        output. The command will block in that case until the channel close tx
1038
        is broadcasted.
1039

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

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

1114
func closeChannel(ctx *cli.Context) error {
×
1115
        ctxc := getContext()
×
1116
        client, cleanUp := getClient(ctx)
×
1117
        defer cleanUp()
×
1118

×
1119
        // Show command help if no arguments and flags were provided.
×
1120
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1121
                cli.ShowCommandHelp(ctx, "closechannel")
×
1122
                return nil
×
1123
        }
×
1124

1125
        // Check that only the field sat_per_vbyte or the deprecated field
1126
        // sat_per_byte is used.
NEW
1127
        _, err := checkNotBothSet(
×
1128
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1129
        )
×
1130
        if err != nil {
×
1131
                return err
×
1132
        }
×
1133

1134
        channelPoint, err := parseChannelPoint(ctx)
×
1135
        if err != nil {
×
1136
                return err
×
1137
        }
×
1138

1139
        // Parse fee rate from --sat_per_vbyte and convert to sat/kw.
NEW
1140
        satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
×
NEW
1141
        if err != nil {
×
NEW
1142
                return err
×
NEW
1143
        }
×
1144

1145
        // Parse fee rate from --max_fee_rate and convert to sat/kw.
NEW
1146
        maxFeePerKw, err := parseFeeRate(ctx, "max_fee_rate")
×
NEW
1147
        if err != nil {
×
NEW
1148
                return err
×
NEW
1149
        }
×
1150

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

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

×
1172
        wg.Add(1)
×
1173
        go func() {
×
1174
                defer wg.Done()
×
1175

×
1176
                printJSON(struct {
×
1177
                        ClosingTxid string `json:"closing_txid"`
×
1178
                }{
×
1179
                        ClosingTxid: <-txidChan,
×
1180
                })
×
1181
        }()
×
1182

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

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

×
1193
        return nil
×
1194
}
1195

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

×
1205
        stream, err := client.CloseChannel(ctxc, req)
×
1206
        if err != nil {
×
1207
                return err
×
1208
        }
×
1209

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

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

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

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

1238
                        fmt.Fprintf(os.Stderr, "Channel close transaction "+
×
1239
                                "broadcasted: %v\n", txid)
×
1240

×
1241
                        txidChan <- txid.String()
×
1242

×
1243
                        if !block {
×
1244
                                return nil
×
1245
                        }
×
1246

1247
                        fmt.Fprintln(os.Stderr, "Waiting for channel close "+
×
1248
                                "confirmation ...")
×
1249

1250
                case *lnrpc.CloseStatusUpdate_ChanClose:
×
1251
                        fmt.Fprintln(os.Stderr, "Channel close successfully "+
×
1252
                                "confirmed")
×
1253

×
1254
                        return nil
×
1255
                }
1256
        }
1257
}
1258

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

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

1271
        One can request to close inactive channels only by using the
1272
        --inactive_only flag.
1273

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

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

1319
func closeAllChannels(ctx *cli.Context) error {
×
1320
        ctxc := getContext()
×
1321
        client, cleanUp := getClient(ctx)
×
1322
        defer cleanUp()
×
1323

×
1324
        // Check that only the field sat_per_vbyte or the deprecated field
×
1325
        // sat_per_byte is used.
×
NEW
1326
        _, err := checkNotBothSet(
×
1327
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1328
        )
×
1329
        if err != nil {
×
1330
                return err
×
1331
        }
×
1332

1333
        // Parse fee rate from --sat_per_vbyte and convert to sat/kw.
NEW
1334
        satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
×
NEW
1335
        if err != nil {
×
NEW
1336
                return err
×
NEW
1337
        }
×
1338

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

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

1350
        if len(openChannels.Channels) == 0 {
×
1351
                return errors.New("no open channels to close")
×
1352
        }
×
1353

1354
        var channelsToClose []*lnrpc.Channel
×
1355

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

×
1362
                confirmed := promptForConfirmation(msg)
×
1363

×
1364
                // We can safely exit if the user did not confirm.
×
1365
                if !confirmed {
×
1366
                        return nil
×
1367
                }
×
1368

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

×
1384
                confirmed := promptForConfirmation(msg)
×
1385

×
1386
                // We can safely exit if the user did not confirm.
×
1387
                if !confirmed {
×
1388
                        return nil
×
1389
                }
×
1390

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

×
1407
                                confirmed := promptForConfirmation(msg)
×
1408

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

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

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

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

1463
                        req := &lnrpc.CloseChannelRequest{
×
1464
                                ChannelPoint: &lnrpc.ChannelPoint{
×
1465
                                        FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
×
1466
                                                FundingTxidStr: s[0],
×
1467
                                        },
×
1468
                                        OutputIndex: uint32(index),
×
1469
                                },
×
NEW
1470
                                Force:      !channel.GetActive(),
×
NEW
1471
                                TargetConf: int32(ctx.Int64("conf_target")),
×
NEW
1472
                                SatPerKw:   satPerKw,
×
1473
                        }
×
1474

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

1483
                        res.ClosingTxid = <-txidChan
×
1484
                }(channel)
1485
        }
1486

1487
        for range channelsToClose {
×
1488
                res := <-resultChan
×
1489
                printJSON(res)
×
1490
        }
×
1491

1492
        return nil
×
1493
}
1494

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

×
1500
        for {
×
1501
                fmt.Print(msg)
×
1502

×
1503
                answer, err := reader.ReadString('\n')
×
1504
                if err != nil {
×
1505
                        return false
×
1506
                }
×
1507

1508
                answer = strings.ToLower(strings.TrimSpace(answer))
×
1509

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

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

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

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

1568
func abandonChannel(ctx *cli.Context) error {
×
1569
        ctxc := getContext()
×
1570
        client, cleanUp := getClient(ctx)
×
1571
        defer cleanUp()
×
1572

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

1579
        channelPoint, err := parseChannelPoint(ctx)
×
1580
        if err != nil {
×
1581
                return err
×
1582
        }
×
1583

1584
        req := &lnrpc.AbandonChannelRequest{
×
1585
                ChannelPoint:      channelPoint,
×
1586
                IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
×
1587
        }
×
1588

×
1589
        resp, err := client.AbandonChannel(ctxc, req)
×
1590
        if err != nil {
×
1591
                return err
×
1592
        }
×
1593

1594
        printRespJSON(resp)
×
1595
        return nil
×
1596
}
1597

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

×
1604
        args := ctx.Args()
×
1605

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

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

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

1642
        return channelPoint, nil
×
1643
}
1644

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

1658
func listPeers(ctx *cli.Context) error {
×
1659
        ctxc := getContext()
×
1660
        client, cleanUp := getClient(ctx)
×
1661
        defer cleanUp()
×
1662

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

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

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

1692
func walletBalance(ctx *cli.Context) error {
×
1693
        ctxc := getContext()
×
1694
        client, cleanUp := getClient(ctx)
×
1695
        defer cleanUp()
×
1696

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

1705
        printRespJSON(resp)
×
1706
        return nil
×
1707
}
1708

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

1717
func ChannelBalance(ctx *cli.Context) error {
×
1718
        ctxc := getContext()
×
1719
        client, cleanUp := getClient(ctx)
×
1720
        defer cleanUp()
×
1721

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

1728
        printRespJSON(resp)
×
1729
        return nil
×
1730
}
1731

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

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

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

×
1756
        var buf bytes.Buffer
×
1757
        fileParser.WriteManPage(&buf)
×
1758

×
1759
        err = os.WriteFile("lnd.1", buf.Bytes(), 0644)
×
1760
        if err != nil {
×
1761
                return err
×
1762
        }
×
1763

1764
        return nil
×
1765
}
1766

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

1773
func getInfo(ctx *cli.Context) error {
×
1774
        ctxc := getContext()
×
1775
        client, cleanUp := getClient(ctx)
×
1776
        defer cleanUp()
×
1777

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

1784
        printRespJSON(resp)
×
1785
        return nil
×
1786
}
1787

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

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

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

1805
        printRespJSON(resp)
×
1806
        return nil
×
1807
}
1808

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

1823
func pendingChannels(ctx *cli.Context) error {
×
1824
        ctxc := getContext()
×
1825
        client, cleanUp := getClient(ctx)
×
1826
        defer cleanUp()
×
1827

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

1837
        printRespJSON(resp)
×
1838

×
1839
        return nil
×
1840
}
1841

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

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

1886
func listAliases(ctx *cli.Context) error {
×
1887
        ctxc := getContext()
×
1888
        client, cleanUp := getClient(ctx)
×
1889
        defer cleanUp()
×
1890

×
1891
        req := &lnrpc.ListAliasesRequest{}
×
1892

×
1893
        resp, err := client.ListAliases(ctxc, req)
×
1894
        if err != nil {
×
1895
                return err
×
1896
        }
×
1897

1898
        printRespJSON(resp)
×
1899

×
1900
        return nil
×
1901
}
1902

1903
func ListChannels(ctx *cli.Context) error {
×
1904
        ctxc := getContext()
×
1905
        client, cleanUp := getClient(ctx)
×
1906
        defer cleanUp()
×
1907

×
1908
        peer := ctx.String("peer")
×
1909

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

1919
                peerKey = pk[:]
×
1920
        }
1921

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

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

×
1935
        resp, err := client.ListChannels(ctxc, req)
×
1936
        if err != nil {
×
1937
                return err
×
1938
        }
×
1939

1940
        printModifiedProtoJSON(resp)
×
1941

×
1942
        return nil
×
1943
}
1944

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

1983
func closedChannels(ctx *cli.Context) error {
×
1984
        ctxc := getContext()
×
1985
        client, cleanUp := getClient(ctx)
×
1986
        defer cleanUp()
×
1987

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

×
1997
        resp, err := client.ClosedChannels(ctxc, req)
×
1998
        if err != nil {
×
1999
                return err
×
2000
        }
×
2001

2002
        printModifiedProtoJSON(resp)
×
2003

×
2004
        return nil
×
2005
}
2006

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

2029
func describeGraph(ctx *cli.Context) error {
×
2030
        ctxc := getContext()
×
2031
        client, cleanUp := getClient(ctx)
×
2032
        defer cleanUp()
×
2033

×
2034
        req := &lnrpc.ChannelGraphRequest{
×
2035
                IncludeUnannounced: ctx.Bool("include_unannounced"),
×
2036
                IncludeAuthProof:   ctx.Bool("include_auth_proof"),
×
2037
        }
×
2038

×
2039
        graph, err := client.DescribeGraph(ctxc, req)
×
2040
        if err != nil {
×
2041
                return err
×
2042
        }
×
2043

2044
        printRespJSON(graph)
×
2045
        return nil
×
2046
}
2047

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

2056
func getNodeMetrics(ctx *cli.Context) error {
×
2057
        ctxc := getContext()
×
2058
        client, cleanUp := getClient(ctx)
×
2059
        defer cleanUp()
×
2060

×
2061
        req := &lnrpc.NodeMetricsRequest{
×
2062
                Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
×
2063
        }
×
2064

×
2065
        nodeMetrics, err := client.GetNodeMetrics(ctxc, req)
×
2066
        if err != nil {
×
2067
                return err
×
2068
        }
×
2069

2070
        printRespJSON(nodeMetrics)
×
2071
        return nil
×
2072
}
2073

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

2103
func getChanInfo(ctx *cli.Context) error {
×
2104
        ctxc := getContext()
×
2105
        client, cleanUp := getClient(ctx)
×
2106
        defer cleanUp()
×
2107

×
2108
        var (
×
2109
                chanID    uint64
×
2110
                chanPoint string
×
2111
                err       error
×
2112
        )
×
2113

×
2114
        switch {
×
2115
        case ctx.IsSet("chan_id"):
×
2116
                chanID = ctx.Uint64("chan_id")
×
2117

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

2124
        case ctx.IsSet("chan_point"):
×
2125
                chanPoint = ctx.String("chan_point")
×
2126

2127
        default:
×
2128
                return fmt.Errorf("chan_id or chan_point argument missing")
×
2129
        }
2130

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

×
2137
        chanInfo, err := client.GetChanInfo(ctxc, req)
×
2138
        if err != nil {
×
2139
                return err
×
2140
        }
×
2141

2142
        printRespJSON(chanInfo)
×
2143
        return nil
×
2144
}
2145

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

2173
func getNodeInfo(ctx *cli.Context) error {
×
2174
        ctxc := getContext()
×
2175
        client, cleanUp := getClient(ctx)
×
2176
        defer cleanUp()
×
2177

×
2178
        args := ctx.Args()
×
2179

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

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

×
2196
        nodeInfo, err := client.GetNodeInfo(ctxc, req)
×
2197
        if err != nil {
×
2198
                return err
×
2199
        }
×
2200

2201
        printRespJSON(nodeInfo)
×
2202
        return nil
×
2203
}
2204

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

2215
func getNetworkInfo(ctx *cli.Context) error {
×
2216
        ctxc := getContext()
×
2217
        client, cleanUp := getClient(ctx)
×
2218
        defer cleanUp()
×
2219

×
2220
        req := &lnrpc.NetworkInfoRequest{}
×
2221

×
2222
        netInfo, err := client.GetNetworkInfo(ctxc, req)
×
2223
        if err != nil {
×
2224
                return err
×
2225
        }
×
2226

2227
        printRespJSON(netInfo)
×
2228
        return nil
×
2229
}
2230

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

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

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

×
2260
        resp, err := client.DebugLevel(ctxc, req)
×
2261
        if err != nil {
×
2262
                return err
×
2263
        }
×
2264

2265
        printRespJSON(resp)
×
2266
        return nil
×
2267
}
2268

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

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

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

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

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

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

2334
        return startHeight, endHeight, nil
5✔
2335
}
2336

2337
func listChainTxns(ctx *cli.Context) error {
×
2338
        ctxc := getContext()
×
2339
        client, cleanUp := getClient(ctx)
×
2340
        defer cleanUp()
×
2341

×
2342
        startHeight, endHeight, err := parseBlockHeightInputs(ctx)
×
2343
        if err != nil {
×
2344
                return err
×
2345
        }
×
2346

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

×
2354
        resp, err := client.GetTransactions(ctxc, req)
×
2355
        if err != nil {
×
2356
                return err
×
2357
        }
×
2358

2359
        printRespJSON(resp)
×
2360
        return nil
×
2361
}
2362

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

2372
func stopDaemon(ctx *cli.Context) error {
×
2373
        ctxc := getContext()
×
2374
        client, cleanUp := getClient(ctx)
×
2375
        defer cleanUp()
×
2376

×
2377
        resp, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2378
        if err != nil {
×
2379
                return err
×
2380
        }
×
2381

2382
        printRespJSON(resp)
×
2383

×
2384
        return nil
×
2385
}
2386

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

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

2406
func signMessage(ctx *cli.Context) error {
×
2407
        ctxc := getContext()
×
2408
        client, cleanUp := getClient(ctx)
×
2409
        defer cleanUp()
×
2410

×
2411
        var msg []byte
×
2412

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

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

2427
        printRespJSON(resp)
×
2428
        return nil
×
2429
}
2430

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

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

2455
func verifyMessage(ctx *cli.Context) error {
×
2456
        ctxc := getContext()
×
2457
        client, cleanUp := getClient(ctx)
×
2458
        defer cleanUp()
×
2459

×
2460
        var (
×
2461
                msg []byte
×
2462
                sig string
×
2463
        )
×
2464

×
2465
        args := ctx.Args()
×
2466

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

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

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

2492
        printRespJSON(resp)
×
2493
        return nil
×
2494
}
2495

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

2506
func feeReport(ctx *cli.Context) error {
×
2507
        ctxc := getContext()
×
2508
        client, cleanUp := getClient(ctx)
×
2509
        defer cleanUp()
×
2510

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

2517
        printRespJSON(resp)
×
2518
        return nil
×
2519
}
2520

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

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

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

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

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

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

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

2665
        return uint16(timeLockDeltaUnCheck), nil
3✔
2666
}
2667

2668
func updateChannelPolicy(ctx *cli.Context) error {
×
2669
        ctxc := getContext()
×
2670
        client, cleanUp := getClient(ctx)
×
2671
        defer cleanUp()
×
2672

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

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

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

2709
                args = args.Tail()
×
2710
        default:
×
2711
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
×
2712
        }
2713

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

2727
                args = args.Tail()
×
2728
        default:
×
2729
                return fmt.Errorf("time_lock_delta argument missing")
×
2730
        }
2731

2732
        var (
×
2733
                chanPoint    *lnrpc.ChannelPoint
×
2734
                chanPointStr string
×
2735
        )
×
2736

×
2737
        switch {
×
2738
        case ctx.IsSet("chan_point"):
×
2739
                chanPointStr = ctx.String("chan_point")
×
2740
        case args.Present():
×
2741
                chanPointStr = args.First()
×
2742
        }
2743

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

2751
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2752
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2753
                inboundBaseFeeMsat > math.MaxInt32 {
×
2754

×
2755
                return errors.New("inbound_base_fee_msat out of range")
×
2756
        }
×
2757

2758
        inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
×
2759
        if inboundFeeRatePpm < math.MinInt32 ||
×
2760
                inboundFeeRatePpm > math.MaxInt32 {
×
2761

×
2762
                return errors.New("inbound_fee_rate_ppm out of range")
×
2763
        }
×
2764

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

×
2771
                return errors.New("both parameters must be provided: " +
×
2772
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
×
2773
        }
×
2774

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

2782
        createMissingEdge := ctx.Bool("create_missing_edge")
×
2783

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

×
2792
        if ctx.IsSet("min_htlc_msat") {
×
2793
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2794
                req.MinHtlcMsatSpecified = true
×
2795
        }
×
2796

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

2807
        if feeRate != 0 {
×
2808
                req.FeeRate = feeRate
×
2809
        } else if feeRatePpm != 0 {
×
2810
                req.FeeRatePpm = uint32(feeRatePpm)
×
2811
        }
×
2812

2813
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2814
        if err != nil {
×
2815
                return err
×
2816
        }
×
2817

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

2832
        printJSON(listFailedUpdateResp)
×
2833

×
2834
        return nil
×
2835
}
2836

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

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

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

2868
        This command will return one of two types of channel backups depending
2869
        on the set of passed arguments:
2870

2871
           * If a target channel point is specified, then a single channel
2872
             backup containing only the information for that channel will be
2873
             returned.
2874

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

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

2906
func exportChanBackup(ctx *cli.Context) error {
×
2907
        ctxc := getContext()
×
2908
        client, cleanUp := getClient(ctx)
×
2909
        defer cleanUp()
×
2910

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

2917
        var (
×
2918
                err            error
×
2919
                chanPointStr   string
×
2920
                outputFileName string
×
2921
        )
×
2922
        args := ctx.Args()
×
2923

×
2924
        switch {
×
2925
        case ctx.IsSet("chan_point"):
×
2926
                chanPointStr = ctx.String("chan_point")
×
2927

2928
        case args.Present():
×
2929
                chanPointStr = args.First()
×
2930

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

2935
        if ctx.IsSet("output_file") {
×
2936
                outputFileName = ctx.String("output_file")
×
2937
        }
×
2938

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

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

2954
                txid, err := chainhash.NewHash(
×
2955
                        chanPointRPC.GetFundingTxidBytes(),
×
2956
                )
×
2957
                if err != nil {
×
2958
                        return err
×
2959
                }
×
2960

2961
                chanPoint := wire.OutPoint{
×
2962
                        Hash:  *txid,
×
2963
                        Index: chanPointRPC.OutputIndex,
×
2964
                }
×
2965

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

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

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

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

2995
        if outputFileName != "" {
×
2996
                return os.WriteFile(
×
2997
                        outputFileName,
×
2998
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2999
                        0666,
×
3000
                )
×
3001
        }
×
3002

3003
        // TODO(roasbeef): support for export | restore ?
3004

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

3012
                chanPoints = append(chanPoints, wire.OutPoint{
×
3013
                        Hash:  *txid,
×
3014
                        Index: chanPoint.OutputIndex,
×
3015
                }.String())
×
3016
        }
3017

3018
        printRespJSON(chanBackup)
×
3019

×
3020
        return nil
×
3021
}
3022

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

3033
    The command will accept backups in one of four forms:
3034

3035
       * A single channel packed SCB, which can be obtained from
3036
         exportchanbackup. This should be passed in hex encoded format.
3037

3038
       * A packed multi-channel SCB, which couples several individual
3039
         static channel backups in single blob.
3040

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

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

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

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

3075
func verifyChanBackup(ctx *cli.Context) error {
×
3076
        ctxc := getContext()
×
3077
        client, cleanUp := getClient(ctx)
×
3078
        defer cleanUp()
×
3079

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

3086
        backups, err := parseChanBackups(ctx)
×
3087
        if err != nil {
×
3088
                return err
×
3089
        }
×
3090

3091
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
3092

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

3102
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
3103
        if err != nil {
×
3104
                return err
×
3105
        }
×
3106

3107
        printRespJSON(resp)
×
3108
        return nil
×
3109
}
3110

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

3126
        The command will accept backups in one of four forms:
3127

3128
           * A single channel packed SCB, which can be obtained from
3129
             exportchanbackup. This should be passed in hex encoded format.
3130

3131
           * A packed multi-channel SCB, which couples several individual
3132
             static channel backups in single blob.
3133

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

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

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

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

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

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

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

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

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

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

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

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

3235
                return &lnrpc.RestoreChanBackupRequest{
×
3236
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3237
                                MultiChanBackup: packedMulti,
×
3238
                        },
×
3239
                }, nil
×
3240

3241
        default:
×
3242
                return nil, errMissingChanBackup
×
3243
        }
3244
}
3245

3246
func restoreChanBackup(ctx *cli.Context) error {
×
3247
        ctxc := getContext()
×
3248
        client, cleanUp := getClient(ctx)
×
3249
        defer cleanUp()
×
3250

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

3257
        var req lnrpc.RestoreChanBackupRequest
×
3258

×
3259
        backups, err := parseChanBackups(ctx)
×
3260
        if err != nil {
×
3261
                return err
×
3262
        }
×
3263

3264
        req.Backup = backups.Backup
×
3265

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

3271
        printRespJSON(resp)
×
3272

×
3273
        return nil
×
3274
}
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