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

lightningnetwork / lnd / 12761228048

14 Jan 2025 04:36AM UTC coverage: 57.511% (-1.2%) from 58.719%
12761228048

Pull #9390

github

NishantBansal2003
docs: add release notes.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Pull Request #9390: Enhance `lncli` listchannels command with the chan_id and short_chan_id (human readable format)

1 of 70 new or added lines in 2 files covered. (1.43%)

19537 existing lines in 250 files now uncovered.

102701 of 178577 relevant lines covered (57.51%)

24806.24 hits per line

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

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

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

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

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

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

40
const defaultRecoveryWindow int32 = 2500
41

42
const (
43
        defaultUtxoMinConf = 1
44
)
45

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

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

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

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

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

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

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

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

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

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

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

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

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

×
NEW
126
                        // Replace the entire match with the new structure.
×
NEW
127
                        return []byte(updatedField)
×
128
                },
129
        )
130

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

NEW
139
        return buf.Bytes()
×
140
}
141

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

NEW
150
        replacedBytes := channelPointPattern.ReplaceAllFunc(
×
NEW
151
                jsonBytes, func(match []byte) []byte {
×
NEW
152
                        chanPoint := channelPointPattern.FindStringSubmatch(
×
NEW
153
                                string(match),
×
NEW
154
                        )[1]
×
NEW
155

×
NEW
156
                        chanOutpoint, err := wire.NewOutPointFromString(
×
NEW
157
                                chanPoint)
×
NEW
158
                        if err != nil {
×
NEW
159
                                return match
×
NEW
160
                        }
×
161

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

×
NEW
170
                        // Replace the entire match with the new structure.
×
NEW
171
                        return []byte(updatedField)
×
172
                },
173
        )
174

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

NEW
183
        return buf.Bytes()
×
184
}
185

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

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

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

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

213
func printRespJSON(resp proto.Message) {
×
214
        jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(resp)
×
215
        if err != nil {
×
216
                fmt.Println("unable to decode response: ", err)
×
217
                return
×
218
        }
×
219

220
        // Replace custom_channel_data in the JSON.
221
        jsonBytesReplaced := replaceCustomData(jsonBytes)
×
222

×
NEW
223
        // Replace chan_id with scid, and append scid_str and scid fields.
×
NEW
224
        jsonBytesReplaced = replaceAndAppendScid(jsonBytesReplaced)
×
NEW
225

×
NEW
226
        // Append the chan_id field to the JSON.
×
NEW
227
        jsonBytesReplaced = appendChanID(jsonBytesReplaced)
×
NEW
228

×
UNCOV
229
        fmt.Printf("%s\n", jsonBytesReplaced)
×
230
}
231

232
// actionDecorator is used to add additional information and error handling
233
// to command actions.
234
func actionDecorator(f func(*cli.Context) error) func(*cli.Context) error {
81✔
235
        return func(c *cli.Context) error {
81✔
236
                if err := f(c); err != nil {
×
237
                        s, ok := status.FromError(err)
×
238

×
239
                        // If it's a command for the UnlockerService (like
×
240
                        // 'create' or 'unlock') but the wallet is already
×
241
                        // unlocked, then these methods aren't recognized any
×
242
                        // more because this service is shut down after
×
243
                        // successful unlock. That's why the code
×
244
                        // 'Unimplemented' means something different for these
×
245
                        // two commands.
×
246
                        if s.Code() == codes.Unimplemented &&
×
247
                                (c.Command.Name == "create" ||
×
248
                                        c.Command.Name == "unlock" ||
×
249
                                        c.Command.Name == "changepassword" ||
×
250
                                        c.Command.Name == "createwatchonly") {
×
251

×
252
                                return fmt.Errorf("Wallet is already unlocked")
×
253
                        }
×
254

255
                        // lnd might be active, but not possible to contact
256
                        // using RPC if the wallet is encrypted. If we get
257
                        // error code Unimplemented, it means that lnd is
258
                        // running, but the RPC server is not active yet (only
259
                        // WalletUnlocker server active) and most likely this
260
                        // is because of an encrypted wallet.
261
                        if ok && s.Code() == codes.Unimplemented {
×
262
                                return fmt.Errorf("Wallet is encrypted. " +
×
263
                                        "Please unlock using 'lncli unlock', " +
×
264
                                        "or set password using 'lncli create'" +
×
265
                                        " if this is the first time starting " +
×
266
                                        "lnd.")
×
267
                        }
×
268
                        return err
×
269
                }
270
                return nil
×
271
        }
272
}
273

274
var newAddressCommand = cli.Command{
275
        Name:      "newaddress",
276
        Category:  "Wallet",
277
        Usage:     "Generates a new address.",
278
        ArgsUsage: "address-type",
279
        Flags: []cli.Flag{
280
                cli.StringFlag{
281
                        Name: "account",
282
                        Usage: "(optional) the name of the account to " +
283
                                "generate a new address for",
284
                },
285
                cli.BoolFlag{
286
                        Name: "unused",
287
                        Usage: "(optional) return the last unused address " +
288
                                "instead of generating a new one",
289
                },
290
        },
291
        Description: `
292
        Generate a wallet new address. Address-types has to be one of:
293
            - p2wkh:  Pay to witness key hash
294
            - np2wkh: Pay to nested witness key hash
295
            - p2tr:   Pay to taproot pubkey`,
296
        Action: actionDecorator(newAddress),
297
}
298

299
func newAddress(ctx *cli.Context) error {
×
300
        ctxc := getContext()
×
301

×
302
        // Display the command's help message if we do not have the expected
×
303
        // number of arguments/flags.
×
304
        if ctx.NArg() != 1 || ctx.NumFlags() > 1 {
×
305
                return cli.ShowCommandHelp(ctx, "newaddress")
×
306
        }
×
307

308
        // Map the string encoded address type, to the concrete typed address
309
        // type enum. An unrecognized address type will result in an error.
310
        stringAddrType := ctx.Args().First()
×
311
        unused := ctx.Bool("unused")
×
312

×
313
        var addrType lnrpc.AddressType
×
314
        switch stringAddrType { // TODO(roasbeef): make them ints on the cli?
×
315
        case "p2wkh":
×
316
                addrType = lnrpc.AddressType_WITNESS_PUBKEY_HASH
×
317
                if unused {
×
318
                        addrType = lnrpc.AddressType_UNUSED_WITNESS_PUBKEY_HASH
×
319
                }
×
320
        case "np2wkh":
×
321
                addrType = lnrpc.AddressType_NESTED_PUBKEY_HASH
×
322
                if unused {
×
323
                        addrType = lnrpc.AddressType_UNUSED_NESTED_PUBKEY_HASH
×
324
                }
×
325
        case "p2tr":
×
326
                addrType = lnrpc.AddressType_TAPROOT_PUBKEY
×
327
                if unused {
×
328
                        addrType = lnrpc.AddressType_UNUSED_TAPROOT_PUBKEY
×
329
                }
×
330
        default:
×
331
                return fmt.Errorf("invalid address type %v, support address type "+
×
332
                        "are: p2wkh, np2wkh, and p2tr", stringAddrType)
×
333
        }
334

335
        client, cleanUp := getClient(ctx)
×
336
        defer cleanUp()
×
337

×
338
        addr, err := client.NewAddress(ctxc, &lnrpc.NewAddressRequest{
×
339
                Type:    addrType,
×
340
                Account: ctx.String("account"),
×
341
        })
×
342
        if err != nil {
×
343
                return err
×
344
        }
×
345

346
        printRespJSON(addr)
×
347
        return nil
×
348
}
349

350
var coinSelectionStrategyFlag = cli.StringFlag{
351
        Name: "coin_selection_strategy",
352
        Usage: "(optional) the strategy to use for selecting " +
353
                "coins. Possible values are 'largest', 'random', or " +
354
                "'global-config'. If either 'largest' or 'random' is " +
355
                "specified, it will override the globally configured " +
356
                "strategy in lnd.conf",
357
        Value: "global-config",
358
}
359

360
var estimateFeeCommand = cli.Command{
361
        Name:      "estimatefee",
362
        Category:  "On-chain",
363
        Usage:     "Get fee estimates for sending bitcoin on-chain to multiple addresses.",
364
        ArgsUsage: "send-json-string [--conf_target=N]",
365
        Description: `
366
        Get fee estimates for sending a transaction paying the specified amount(s) to the passed address(es).
367

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

370
            '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
371
        `,
372
        Flags: []cli.Flag{
373
                cli.Int64Flag{
374
                        Name: "conf_target",
375
                        Usage: "(optional) the number of blocks that the " +
376
                                "transaction *should* confirm in",
377
                },
378
                coinSelectionStrategyFlag,
379
        },
380
        Action: actionDecorator(estimateFees),
381
}
382

383
func estimateFees(ctx *cli.Context) error {
×
384
        ctxc := getContext()
×
385
        var amountToAddr map[string]int64
×
386

×
387
        jsonMap := ctx.Args().First()
×
388
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
389
                return err
×
390
        }
×
391

392
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
393
        if err != nil {
×
394
                return err
×
395
        }
×
396

397
        client, cleanUp := getClient(ctx)
×
398
        defer cleanUp()
×
399

×
400
        resp, err := client.EstimateFee(ctxc, &lnrpc.EstimateFeeRequest{
×
401
                AddrToAmount:          amountToAddr,
×
402
                TargetConf:            int32(ctx.Int64("conf_target")),
×
403
                CoinSelectionStrategy: coinSelectionStrategy,
×
404
        })
×
405
        if err != nil {
×
406
                return err
×
407
        }
×
408

409
        printRespJSON(resp)
×
410
        return nil
×
411
}
412

413
var txLabelFlag = cli.StringFlag{
414
        Name:  "label",
415
        Usage: "(optional) a label for the transaction",
416
}
417

418
var sendCoinsCommand = cli.Command{
419
        Name:      "sendcoins",
420
        Category:  "On-chain",
421
        Usage:     "Send bitcoin on-chain to an address.",
422
        ArgsUsage: "addr amt",
423
        Description: `
424
        Send amt coins in satoshis to the base58 or bech32 encoded bitcoin address addr.
425

426
        Fees used when sending the transaction can be specified via the --conf_target, or
427
        --sat_per_vbyte optional flags.
428

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

498
func sendCoins(ctx *cli.Context) error {
×
499
        var (
×
500
                addr      string
×
501
                amt       int64
×
502
                err       error
×
503
                outpoints []*lnrpc.OutPoint
×
504
        )
×
505
        ctxc := getContext()
×
506
        args := ctx.Args()
×
507

×
508
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
509
                cli.ShowCommandHelp(ctx, "sendcoins")
×
510
                return nil
×
511
        }
×
512

513
        // Check that only the field sat_per_vbyte or the deprecated field
514
        // sat_per_byte is used.
515
        feeRateFlag, err := checkNotBothSet(
×
516
                ctx, "sat_per_vbyte", "sat_per_byte",
×
517
        )
×
518
        if err != nil {
×
519
                return err
×
520
        }
×
521

522
        // Only fee rate flag or conf_target should be set, not both.
523
        if _, err := checkNotBothSet(
×
524
                ctx, feeRateFlag, "conf_target",
×
525
        ); err != nil {
×
526
                return err
×
527
        }
×
528

529
        switch {
×
530
        case ctx.IsSet("addr"):
×
531
                addr = ctx.String("addr")
×
532
        case args.Present():
×
533
                addr = args.First()
×
534
                args = args.Tail()
×
535
        default:
×
536
                return fmt.Errorf("Address argument missing")
×
537
        }
538

539
        switch {
×
540
        case ctx.IsSet("amt"):
×
541
                amt = ctx.Int64("amt")
×
542
        case args.Present():
×
543
                amt, err = strconv.ParseInt(args.First(), 10, 64)
×
544
        case !ctx.Bool("sweepall"):
×
545
                return fmt.Errorf("Amount argument missing")
×
546
        }
547
        if err != nil {
×
548
                return fmt.Errorf("unable to decode amount: %w", err)
×
549
        }
×
550

551
        if amt != 0 && ctx.Bool("sweepall") {
×
552
                return fmt.Errorf("amount cannot be set if attempting to " +
×
553
                        "sweep all coins out of the wallet")
×
554
        }
×
555

556
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
557
        if err != nil {
×
558
                return err
×
559
        }
×
560

561
        client, cleanUp := getClient(ctx)
×
562
        defer cleanUp()
×
563
        minConfs := int32(ctx.Uint64("min_confs"))
×
564

×
565
        // In case that the user has specified the sweepall flag, we'll
×
566
        // calculate the amount to send based on the current wallet balance.
×
567
        displayAmt := amt
×
568
        if ctx.Bool("sweepall") && !ctx.IsSet("utxo") {
×
569
                balanceResponse, err := client.WalletBalance(
×
570
                        ctxc, &lnrpc.WalletBalanceRequest{
×
571
                                MinConfs: minConfs,
×
572
                        },
×
573
                )
×
574
                if err != nil {
×
575
                        return fmt.Errorf("unable to retrieve wallet balance:"+
×
576
                                " %w", err)
×
577
                }
×
578
                displayAmt = balanceResponse.GetConfirmedBalance()
×
579
        }
580

581
        if ctx.IsSet("utxo") {
×
582
                utxos := ctx.StringSlice("utxo")
×
583

×
584
                outpoints, err = UtxosToOutpoints(utxos)
×
585
                if err != nil {
×
586
                        return fmt.Errorf("unable to decode utxos: %w", err)
×
587
                }
×
588

589
                if ctx.Bool("sweepall") {
×
590
                        displayAmt = 0
×
591
                        // If we're sweeping all funds of the utxos, we'll need
×
592
                        // to set the display amount to the total amount of the
×
593
                        // utxos.
×
594
                        unspents, err := client.ListUnspent(
×
595
                                ctxc, &lnrpc.ListUnspentRequest{
×
596
                                        MinConfs: 0,
×
597
                                        MaxConfs: math.MaxInt32,
×
598
                                },
×
599
                        )
×
600
                        if err != nil {
×
601
                                return err
×
602
                        }
×
603

604
                        for _, utxo := range outpoints {
×
605
                                for _, unspent := range unspents.Utxos {
×
606
                                        unspentUtxo := unspent.Outpoint
×
607
                                        if isSameOutpoint(utxo, unspentUtxo) {
×
608
                                                displayAmt += unspent.AmountSat
×
609
                                                break
×
610
                                        }
611
                                }
612
                        }
613
                }
614
        }
615

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

×
623
                confirm := promptForConfirmation("Confirm payment (yes/no): ")
×
624
                if !confirm {
×
625
                        return nil
×
626
                }
×
627
        }
628

629
        req := &lnrpc.SendCoinsRequest{
×
630
                Addr:                  addr,
×
631
                Amount:                amt,
×
632
                TargetConf:            int32(ctx.Int64("conf_target")),
×
633
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
634
                SendAll:               ctx.Bool("sweepall"),
×
635
                Label:                 ctx.String(txLabelFlag.Name),
×
636
                MinConfs:              minConfs,
×
637
                SpendUnconfirmed:      minConfs == 0,
×
638
                CoinSelectionStrategy: coinSelectionStrategy,
×
639
                Outpoints:             outpoints,
×
640
        }
×
641
        txid, err := client.SendCoins(ctxc, req)
×
642
        if err != nil {
×
643
                return err
×
644
        }
×
645

646
        printRespJSON(txid)
×
647
        return nil
×
648
}
649

650
func isSameOutpoint(a, b *lnrpc.OutPoint) bool {
×
651
        return a.TxidStr == b.TxidStr && a.OutputIndex == b.OutputIndex
×
652
}
×
653

654
var listUnspentCommand = cli.Command{
655
        Name:      "listunspent",
656
        Category:  "On-chain",
657
        Usage:     "List utxos available for spending.",
658
        ArgsUsage: "[min-confs [max-confs]] [--unconfirmed_only]",
659
        Description: `
660
        For each spendable utxo currently in the wallet, with at least min_confs
661
        confirmations, and at most max_confs confirmations, lists the txid,
662
        index, amount, address, address type, scriptPubkey and number of
663
        confirmations.  Use --min_confs=0 to include unconfirmed coins. To list
664
        all coins with at least min_confs confirmations, omit the second
665
        argument or flag '--max_confs'. To list all confirmed and unconfirmed
666
        coins, no arguments are required. To see only unconfirmed coins, use
667
        '--unconfirmed_only' with '--min_confs' and '--max_confs' set to zero or
668
        not present.
669
        `,
670
        Flags: []cli.Flag{
671
                cli.Int64Flag{
672
                        Name:  "min_confs",
673
                        Usage: "the minimum number of confirmations for a utxo",
674
                },
675
                cli.Int64Flag{
676
                        Name:  "max_confs",
677
                        Usage: "the maximum number of confirmations for a utxo",
678
                },
679
                cli.BoolFlag{
680
                        Name: "unconfirmed_only",
681
                        Usage: "when min_confs and max_confs are zero, " +
682
                                "setting false implicitly overrides max_confs " +
683
                                "to be MaxInt32, otherwise max_confs remains " +
684
                                "zero. An error is returned if the value is " +
685
                                "true and both min_confs and max_confs are " +
686
                                "non-zero. (default: false)",
687
                },
688
        },
689
        Action: actionDecorator(listUnspent),
690
}
691

692
func listUnspent(ctx *cli.Context) error {
×
693
        var (
×
694
                minConfirms int64
×
695
                maxConfirms int64
×
696
                err         error
×
697
        )
×
698
        ctxc := getContext()
×
699
        args := ctx.Args()
×
700

×
701
        if ctx.IsSet("max_confs") && !ctx.IsSet("min_confs") {
×
702
                return fmt.Errorf("max_confs cannot be set without " +
×
703
                        "min_confs being set")
×
704
        }
×
705

706
        switch {
×
707
        case ctx.IsSet("min_confs"):
×
708
                minConfirms = ctx.Int64("min_confs")
×
709
        case args.Present():
×
710
                minConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
711
                if err != nil {
×
712
                        cli.ShowCommandHelp(ctx, "listunspent")
×
713
                        return nil
×
714
                }
×
715
                args = args.Tail()
×
716
        }
717

718
        switch {
×
719
        case ctx.IsSet("max_confs"):
×
720
                maxConfirms = ctx.Int64("max_confs")
×
721
        case args.Present():
×
722
                maxConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
723
                if err != nil {
×
724
                        cli.ShowCommandHelp(ctx, "listunspent")
×
725
                        return nil
×
726
                }
×
727
                args = args.Tail()
×
728
        }
729

730
        unconfirmedOnly := ctx.Bool("unconfirmed_only")
×
731

×
732
        // Force minConfirms and maxConfirms to be zero if unconfirmedOnly is
×
733
        // true.
×
734
        if unconfirmedOnly && (minConfirms != 0 || maxConfirms != 0) {
×
735
                cli.ShowCommandHelp(ctx, "listunspent")
×
736
                return nil
×
737
        }
×
738

739
        // When unconfirmedOnly is inactive, we will override maxConfirms to be
740
        // a MaxInt32 to return all confirmed and unconfirmed utxos.
741
        if maxConfirms == 0 && !unconfirmedOnly {
×
742
                maxConfirms = math.MaxInt32
×
743
        }
×
744

745
        client, cleanUp := getClient(ctx)
×
746
        defer cleanUp()
×
747

×
748
        req := &lnrpc.ListUnspentRequest{
×
749
                MinConfs: int32(minConfirms),
×
750
                MaxConfs: int32(maxConfirms),
×
751
        }
×
752
        resp, err := client.ListUnspent(ctxc, req)
×
753
        if err != nil {
×
754
                return err
×
755
        }
×
756

757
        // Parse the response into the final json object that will be printed
758
        // to stdout. At the moment, this filters out the raw txid bytes from
759
        // each utxo's outpoint and only prints the txid string.
760
        var listUnspentResp = struct {
×
761
                Utxos []*Utxo `json:"utxos"`
×
762
        }{
×
763
                Utxos: make([]*Utxo, 0, len(resp.Utxos)),
×
764
        }
×
765
        for _, protoUtxo := range resp.Utxos {
×
766
                utxo := NewUtxoFromProto(protoUtxo)
×
767
                listUnspentResp.Utxos = append(listUnspentResp.Utxos, utxo)
×
768
        }
×
769

770
        printJSON(listUnspentResp)
×
771

×
772
        return nil
×
773
}
774

775
var sendManyCommand = cli.Command{
776
        Name:      "sendmany",
777
        Category:  "On-chain",
778
        Usage:     "Send bitcoin on-chain to multiple addresses.",
779
        ArgsUsage: "send-json-string [--conf_target=N] [--sat_per_vbyte=P]",
780
        Description: `
781
        Create and broadcast a transaction paying the specified amount(s) to the passed address(es).
782

783
        The send-json-string' param decodes addresses and the amount to send
784
        respectively in the following format:
785

786
            '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
787
        `,
788
        Flags: []cli.Flag{
789
                cli.Int64Flag{
790
                        Name: "conf_target",
791
                        Usage: "(optional) the number of blocks that the transaction *should* " +
792
                                "confirm in, will be used for fee estimation",
793
                },
794
                cli.Int64Flag{
795
                        Name:   "sat_per_byte",
796
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
797
                        Hidden: true,
798
                },
799
                cli.Int64Flag{
800
                        Name: "sat_per_vbyte",
801
                        Usage: "(optional) a manual fee expressed in " +
802
                                "sat/vbyte that should be used when crafting " +
803
                                "the transaction",
804
                },
805
                cli.Uint64Flag{
806
                        Name: "min_confs",
807
                        Usage: "(optional) the minimum number of confirmations " +
808
                                "each one of your outputs used for the transaction " +
809
                                "must satisfy",
810
                        Value: defaultUtxoMinConf,
811
                },
812
                coinSelectionStrategyFlag,
813
                txLabelFlag,
814
        },
815
        Action: actionDecorator(sendMany),
816
}
817

818
func sendMany(ctx *cli.Context) error {
×
819
        ctxc := getContext()
×
820
        var amountToAddr map[string]int64
×
821

×
822
        jsonMap := ctx.Args().First()
×
823
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
824
                return err
×
825
        }
×
826

827
        // Check that only the field sat_per_vbyte or the deprecated field
828
        // sat_per_byte is used.
829
        feeRateFlag, err := checkNotBothSet(
×
830
                ctx, "sat_per_vbyte", "sat_per_byte",
×
831
        )
×
832
        if err != nil {
×
833
                return err
×
834
        }
×
835

836
        // Only fee rate flag or conf_target should be set, not both.
837
        if _, err := checkNotBothSet(
×
838
                ctx, feeRateFlag, "conf_target",
×
839
        ); err != nil {
×
840
                return err
×
841
        }
×
842

843
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
844
        if err != nil {
×
845
                return err
×
846
        }
×
847

848
        client, cleanUp := getClient(ctx)
×
849
        defer cleanUp()
×
850

×
851
        minConfs := int32(ctx.Uint64("min_confs"))
×
852
        txid, err := client.SendMany(ctxc, &lnrpc.SendManyRequest{
×
853
                AddrToAmount:          amountToAddr,
×
854
                TargetConf:            int32(ctx.Int64("conf_target")),
×
855
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
856
                Label:                 ctx.String(txLabelFlag.Name),
×
857
                MinConfs:              minConfs,
×
858
                SpendUnconfirmed:      minConfs == 0,
×
859
                CoinSelectionStrategy: coinSelectionStrategy,
×
860
        })
×
861
        if err != nil {
×
862
                return err
×
863
        }
×
864

865
        printRespJSON(txid)
×
866
        return nil
×
867
}
868

869
var connectCommand = cli.Command{
870
        Name:      "connect",
871
        Category:  "Peers",
872
        Usage:     "Connect to a remote lightning peer.",
873
        ArgsUsage: "<pubkey>@host",
874
        Description: `
875
        Connect to a peer using its <pubkey> and host.
876

877
        A custom timeout on the connection is supported. For instance, to timeout
878
        the connection request in 30 seconds, use the following:
879

880
        lncli connect <pubkey>@host --timeout 30s
881
        `,
882
        Flags: []cli.Flag{
883
                cli.BoolFlag{
884
                        Name: "perm",
885
                        Usage: "If set, the daemon will attempt to persistently " +
886
                                "connect to the target peer.\n" +
887
                                "           If not, the call will be synchronous.",
888
                },
889
                cli.DurationFlag{
890
                        Name: "timeout",
891
                        Usage: "The connection timeout value for current request. " +
892
                                "Valid uints are {ms, s, m, h}.\n" +
893
                                "If not set, the global connection " +
894
                                "timeout value (default to 120s) is used.",
895
                },
896
        },
897
        Action: actionDecorator(connectPeer),
898
}
899

900
func connectPeer(ctx *cli.Context) error {
×
901
        ctxc := getContext()
×
902
        client, cleanUp := getClient(ctx)
×
903
        defer cleanUp()
×
904

×
905
        targetAddress := ctx.Args().First()
×
906
        splitAddr := strings.Split(targetAddress, "@")
×
907
        if len(splitAddr) != 2 {
×
908
                return fmt.Errorf("target address expected in format: " +
×
909
                        "pubkey@host:port")
×
910
        }
×
911

912
        addr := &lnrpc.LightningAddress{
×
913
                Pubkey: splitAddr[0],
×
914
                Host:   splitAddr[1],
×
915
        }
×
916
        req := &lnrpc.ConnectPeerRequest{
×
917
                Addr:    addr,
×
918
                Perm:    ctx.Bool("perm"),
×
919
                Timeout: uint64(ctx.Duration("timeout").Seconds()),
×
920
        }
×
921

×
922
        lnid, err := client.ConnectPeer(ctxc, req)
×
923
        if err != nil {
×
924
                return err
×
925
        }
×
926

927
        printRespJSON(lnid)
×
928
        return nil
×
929
}
930

931
var disconnectCommand = cli.Command{
932
        Name:     "disconnect",
933
        Category: "Peers",
934
        Usage: "Disconnect a remote lightning peer identified by " +
935
                "public key.",
936
        ArgsUsage: "<pubkey>",
937
        Flags: []cli.Flag{
938
                cli.StringFlag{
939
                        Name: "node_key",
940
                        Usage: "The hex-encoded compressed public key of the peer " +
941
                                "to disconnect from",
942
                },
943
        },
944
        Action: actionDecorator(disconnectPeer),
945
}
946

947
func disconnectPeer(ctx *cli.Context) error {
×
948
        ctxc := getContext()
×
949
        client, cleanUp := getClient(ctx)
×
950
        defer cleanUp()
×
951

×
952
        var pubKey string
×
953
        switch {
×
954
        case ctx.IsSet("node_key"):
×
955
                pubKey = ctx.String("node_key")
×
956
        case ctx.Args().Present():
×
957
                pubKey = ctx.Args().First()
×
958
        default:
×
959
                return fmt.Errorf("must specify target public key")
×
960
        }
961

962
        req := &lnrpc.DisconnectPeerRequest{
×
963
                PubKey: pubKey,
×
964
        }
×
965

×
966
        lnid, err := client.DisconnectPeer(ctxc, req)
×
967
        if err != nil {
×
968
                return err
×
969
        }
×
970

971
        printRespJSON(lnid)
×
972
        return nil
×
973
}
974

975
// TODO(roasbeef): also allow short relative channel ID.
976

977
var closeChannelCommand = cli.Command{
978
        Name:     "closechannel",
979
        Category: "Channels",
980
        Usage:    "Close an existing channel.",
981
        Description: `
982
        Close an existing channel. The channel can be closed either cooperatively,
983
        or unilaterally (--force).
984

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

989
        In the case of a cooperative closure, one can manually set the fee to
990
        be used for the closing transaction via either the --conf_target or
991
        --sat_per_vbyte arguments. This will be the starting value used during
992
        fee negotiation. This is optional. The parameter --max_fee_rate in
993
        comparison is the end boundary of the fee negotiation, if not specified
994
        it's always x3 of the starting value. Increasing this value increases
995
        the chance of a successful negotiation.
996

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

1002
        To view which funding_txids/output_indexes can be used for a channel close,
1003
        see the channel_point values within the listchannels command output.
1004
        The format for a channel_point is 'funding_txid:output_index'.`,
1005
        ArgsUsage: "funding_txid output_index",
1006
        Flags: []cli.Flag{
1007
                cli.StringFlag{
1008
                        Name:  "funding_txid",
1009
                        Usage: "the txid of the channel's funding transaction",
1010
                },
1011
                cli.IntFlag{
1012
                        Name: "output_index",
1013
                        Usage: "the output index for the funding output of the funding " +
1014
                                "transaction",
1015
                },
1016
                cli.StringFlag{
1017
                        Name: "chan_point",
1018
                        Usage: "(optional) the channel point. If set, " +
1019
                                "funding_txid and output_index flags and " +
1020
                                "positional arguments will be ignored",
1021
                },
1022
                cli.BoolFlag{
1023
                        Name:  "force",
1024
                        Usage: "attempt an uncooperative closure",
1025
                },
1026
                cli.BoolFlag{
1027
                        Name:  "block",
1028
                        Usage: "block until the channel is closed",
1029
                },
1030
                cli.Int64Flag{
1031
                        Name: "conf_target",
1032
                        Usage: "(optional) the number of blocks that the " +
1033
                                "transaction *should* confirm in, will be " +
1034
                                "used for fee estimation. If not set, " +
1035
                                "then the conf-target value set in the main " +
1036
                                "lnd config will be used.",
1037
                },
1038
                cli.Int64Flag{
1039
                        Name:   "sat_per_byte",
1040
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
1041
                        Hidden: true,
1042
                },
1043
                cli.Int64Flag{
1044
                        Name: "sat_per_vbyte",
1045
                        Usage: "(optional) a manual fee expressed in " +
1046
                                "sat/vbyte that should be used when crafting " +
1047
                                "the transaction; default is a conf-target " +
1048
                                "of 6 blocks",
1049
                },
1050
                cli.StringFlag{
1051
                        Name: "delivery_addr",
1052
                        Usage: "(optional) an address to deliver funds " +
1053
                                "upon cooperative channel closing, may only " +
1054
                                "be used if an upfront shutdown address is not " +
1055
                                "already set",
1056
                },
1057
                cli.Uint64Flag{
1058
                        Name: "max_fee_rate",
1059
                        Usage: "(optional) maximum fee rate in sat/vbyte " +
1060
                                "accepted during the negotiation (default is " +
1061
                                "x3 of the desired fee rate); increases the " +
1062
                                "success pobability of the negotiation if " +
1063
                                "set higher",
1064
                },
1065
        },
1066
        Action: actionDecorator(closeChannel),
1067
}
1068

1069
func closeChannel(ctx *cli.Context) error {
×
1070
        ctxc := getContext()
×
1071
        client, cleanUp := getClient(ctx)
×
1072
        defer cleanUp()
×
1073

×
1074
        // Show command help if no arguments and flags were provided.
×
1075
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1076
                cli.ShowCommandHelp(ctx, "closechannel")
×
1077
                return nil
×
1078
        }
×
1079

1080
        // Check that only the field sat_per_vbyte or the deprecated field
1081
        // sat_per_byte is used.
1082
        feeRateFlag, err := checkNotBothSet(
×
1083
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1084
        )
×
1085
        if err != nil {
×
1086
                return err
×
1087
        }
×
1088

1089
        channelPoint, err := parseChannelPoint(ctx)
×
1090
        if err != nil {
×
1091
                return err
×
1092
        }
×
1093

1094
        // TODO(roasbeef): implement time deadline within server
1095
        req := &lnrpc.CloseChannelRequest{
×
1096
                ChannelPoint:    channelPoint,
×
1097
                Force:           ctx.Bool("force"),
×
1098
                TargetConf:      int32(ctx.Int64("conf_target")),
×
1099
                SatPerVbyte:     ctx.Uint64(feeRateFlag),
×
1100
                DeliveryAddress: ctx.String("delivery_addr"),
×
1101
                MaxFeePerVbyte:  ctx.Uint64("max_fee_rate"),
×
1102
        }
×
1103

×
1104
        // After parsing the request, we'll spin up a goroutine that will
×
1105
        // retrieve the closing transaction ID when attempting to close the
×
1106
        // channel. We do this to because `executeChannelClose` can block, so we
×
1107
        // would like to present the closing transaction ID to the user as soon
×
1108
        // as it is broadcasted.
×
1109
        var wg sync.WaitGroup
×
1110
        txidChan := make(chan string, 1)
×
1111

×
1112
        wg.Add(1)
×
1113
        go func() {
×
1114
                defer wg.Done()
×
1115

×
1116
                printJSON(struct {
×
1117
                        ClosingTxid string `json:"closing_txid"`
×
1118
                }{
×
1119
                        ClosingTxid: <-txidChan,
×
1120
                })
×
1121
        }()
×
1122

1123
        err = executeChannelClose(ctxc, client, req, txidChan, ctx.Bool("block"))
×
1124
        if err != nil {
×
1125
                return err
×
1126
        }
×
1127

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

×
1133
        return nil
×
1134
}
1135

1136
// executeChannelClose attempts to close the channel from a request. The closing
1137
// transaction ID is sent through `txidChan` as soon as it is broadcasted to the
1138
// network. The block boolean is used to determine if we should block until the
1139
// closing transaction receives all of its required confirmations.
1140
func executeChannelClose(ctxc context.Context, client lnrpc.LightningClient,
1141
        req *lnrpc.CloseChannelRequest, txidChan chan<- string, block bool) error {
×
1142

×
1143
        stream, err := client.CloseChannel(ctxc, req)
×
1144
        if err != nil {
×
1145
                return err
×
1146
        }
×
1147

1148
        for {
×
1149
                resp, err := stream.Recv()
×
1150
                if err == io.EOF {
×
1151
                        return nil
×
1152
                } else if err != nil {
×
1153
                        return err
×
1154
                }
×
1155

1156
                switch update := resp.Update.(type) {
×
1157
                case *lnrpc.CloseStatusUpdate_CloseInstant:
×
1158
                        if req.NoWait {
×
1159
                                return nil
×
1160
                        }
×
1161
                case *lnrpc.CloseStatusUpdate_ClosePending:
×
1162
                        closingHash := update.ClosePending.Txid
×
1163
                        txid, err := chainhash.NewHash(closingHash)
×
1164
                        if err != nil {
×
1165
                                return err
×
1166
                        }
×
1167

1168
                        txidChan <- txid.String()
×
1169

×
1170
                        if !block {
×
1171
                                return nil
×
1172
                        }
×
1173
                case *lnrpc.CloseStatusUpdate_ChanClose:
×
1174
                        return nil
×
1175
                }
1176
        }
1177
}
1178

1179
var closeAllChannelsCommand = cli.Command{
1180
        Name:     "closeallchannels",
1181
        Category: "Channels",
1182
        Usage:    "Close all existing channels.",
1183
        Description: `
1184
        Close all existing channels.
1185

1186
        Channels will be closed either cooperatively or unilaterally, depending
1187
        on whether the channel is active or not. If the channel is inactive, any
1188
        settled funds within it will be time locked for a few blocks before they
1189
        can be spent.
1190

1191
        One can request to close inactive channels only by using the
1192
        --inactive_only flag.
1193

1194
        By default, one is prompted for confirmation every time an inactive
1195
        channel is requested to be closed. To avoid this, one can set the
1196
        --force flag, which will only prompt for confirmation once for all
1197
        inactive channels and proceed to close them.
1198

1199
        In the case of cooperative closures, one can manually set the fee to
1200
        be used for the closing transactions via either the --conf_target or
1201
        --sat_per_vbyte arguments. This will be the starting value used during
1202
        fee negotiation. This is optional.`,
1203
        Flags: []cli.Flag{
1204
                cli.BoolFlag{
1205
                        Name:  "inactive_only",
1206
                        Usage: "close inactive channels only",
1207
                },
1208
                cli.BoolFlag{
1209
                        Name: "force",
1210
                        Usage: "ask for confirmation once before attempting " +
1211
                                "to close existing channels",
1212
                },
1213
                cli.Int64Flag{
1214
                        Name: "conf_target",
1215
                        Usage: "(optional) the number of blocks that the " +
1216
                                "closing transactions *should* confirm in, will be " +
1217
                                "used for fee estimation",
1218
                },
1219
                cli.Int64Flag{
1220
                        Name:   "sat_per_byte",
1221
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
1222
                        Hidden: true,
1223
                },
1224
                cli.Int64Flag{
1225
                        Name: "sat_per_vbyte",
1226
                        Usage: "(optional) a manual fee expressed in " +
1227
                                "sat/vbyte that should be used when crafting " +
1228
                                "the closing transactions",
1229
                },
1230
                cli.BoolFlag{
1231
                        Name: "s, skip_confirmation",
1232
                        Usage: "Skip the confirmation prompt and close all " +
1233
                                "channels immediately",
1234
                },
1235
        },
1236
        Action: actionDecorator(closeAllChannels),
1237
}
1238

1239
func closeAllChannels(ctx *cli.Context) error {
×
1240
        ctxc := getContext()
×
1241
        client, cleanUp := getClient(ctx)
×
1242
        defer cleanUp()
×
1243

×
1244
        // Check that only the field sat_per_vbyte or the deprecated field
×
1245
        // sat_per_byte is used.
×
1246
        feeRateFlag, err := checkNotBothSet(
×
1247
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1248
        )
×
1249
        if err != nil {
×
1250
                return err
×
1251
        }
×
1252

1253
        prompt := "Do you really want to close ALL channels? (yes/no): "
×
1254
        if !ctx.Bool("skip_confirmation") && !promptForConfirmation(prompt) {
×
1255
                return errors.New("action aborted by user")
×
1256
        }
×
1257

1258
        listReq := &lnrpc.ListChannelsRequest{}
×
1259
        openChannels, err := client.ListChannels(ctxc, listReq)
×
1260
        if err != nil {
×
1261
                return fmt.Errorf("unable to fetch open channels: %w", err)
×
1262
        }
×
1263

1264
        if len(openChannels.Channels) == 0 {
×
1265
                return errors.New("no open channels to close")
×
1266
        }
×
1267

1268
        var channelsToClose []*lnrpc.Channel
×
1269

×
1270
        switch {
×
1271
        case ctx.Bool("force") && ctx.Bool("inactive_only"):
×
1272
                msg := "Unilaterally close all inactive channels? The funds " +
×
1273
                        "within these channels will be locked for some blocks " +
×
1274
                        "(CSV delay) before they can be spent. (yes/no): "
×
1275

×
1276
                confirmed := promptForConfirmation(msg)
×
1277

×
1278
                // We can safely exit if the user did not confirm.
×
1279
                if !confirmed {
×
1280
                        return nil
×
1281
                }
×
1282

1283
                // Go through the list of open channels and only add inactive
1284
                // channels to the closing list.
1285
                for _, channel := range openChannels.Channels {
×
1286
                        if !channel.GetActive() {
×
1287
                                channelsToClose = append(
×
1288
                                        channelsToClose, channel,
×
1289
                                )
×
1290
                        }
×
1291
                }
1292
        case ctx.Bool("force"):
×
1293
                msg := "Close all active and inactive channels? Inactive " +
×
1294
                        "channels will be closed unilaterally, so funds " +
×
1295
                        "within them will be locked for a few blocks (CSV " +
×
1296
                        "delay) before they can be spent. (yes/no): "
×
1297

×
1298
                confirmed := promptForConfirmation(msg)
×
1299

×
1300
                // We can safely exit if the user did not confirm.
×
1301
                if !confirmed {
×
1302
                        return nil
×
1303
                }
×
1304

1305
                channelsToClose = openChannels.Channels
×
1306
        default:
×
1307
                // Go through the list of open channels and determine which
×
1308
                // should be added to the closing list.
×
1309
                for _, channel := range openChannels.Channels {
×
1310
                        // If the channel is inactive, we'll attempt to
×
1311
                        // unilaterally close the channel, so we should prompt
×
1312
                        // the user for confirmation beforehand.
×
1313
                        if !channel.GetActive() {
×
1314
                                msg := fmt.Sprintf("Unilaterally close channel "+
×
1315
                                        "with node %s and channel point %s? "+
×
1316
                                        "The closing transaction will need %d "+
×
1317
                                        "confirmations before the funds can be "+
×
1318
                                        "spent. (yes/no): ", channel.RemotePubkey,
×
1319
                                        channel.ChannelPoint, channel.LocalConstraints.CsvDelay)
×
1320

×
1321
                                confirmed := promptForConfirmation(msg)
×
1322

×
1323
                                if confirmed {
×
1324
                                        channelsToClose = append(
×
1325
                                                channelsToClose, channel,
×
1326
                                        )
×
1327
                                }
×
1328
                        } else if !ctx.Bool("inactive_only") {
×
1329
                                // Otherwise, we'll only add active channels if
×
1330
                                // we were not requested to close inactive
×
1331
                                // channels only.
×
1332
                                channelsToClose = append(
×
1333
                                        channelsToClose, channel,
×
1334
                                )
×
1335
                        }
×
1336
                }
1337
        }
1338

1339
        // result defines the result of closing a channel. The closing
1340
        // transaction ID is populated if a channel is successfully closed.
1341
        // Otherwise, the error that prevented closing the channel is populated.
1342
        type result struct {
×
1343
                RemotePubKey string `json:"remote_pub_key"`
×
1344
                ChannelPoint string `json:"channel_point"`
×
1345
                ClosingTxid  string `json:"closing_txid"`
×
1346
                FailErr      string `json:"error"`
×
1347
        }
×
1348

×
1349
        // Launch each channel closure in a goroutine in order to execute them
×
1350
        // in parallel. Once they're all executed, we will print the results as
×
1351
        // they come.
×
1352
        resultChan := make(chan result, len(channelsToClose))
×
1353
        for _, channel := range channelsToClose {
×
1354
                go func(channel *lnrpc.Channel) {
×
1355
                        res := result{}
×
1356
                        res.RemotePubKey = channel.RemotePubkey
×
1357
                        res.ChannelPoint = channel.ChannelPoint
×
1358
                        defer func() {
×
1359
                                resultChan <- res
×
1360
                        }()
×
1361

1362
                        // Parse the channel point in order to create the close
1363
                        // channel request.
1364
                        s := strings.Split(res.ChannelPoint, ":")
×
1365
                        if len(s) != 2 {
×
1366
                                res.FailErr = "expected channel point with " +
×
1367
                                        "format txid:index"
×
1368
                                return
×
1369
                        }
×
1370
                        index, err := strconv.ParseUint(s[1], 10, 32)
×
1371
                        if err != nil {
×
1372
                                res.FailErr = fmt.Sprintf("unable to parse "+
×
1373
                                        "channel point output index: %v", err)
×
1374
                                return
×
1375
                        }
×
1376

1377
                        req := &lnrpc.CloseChannelRequest{
×
1378
                                ChannelPoint: &lnrpc.ChannelPoint{
×
1379
                                        FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
×
1380
                                                FundingTxidStr: s[0],
×
1381
                                        },
×
1382
                                        OutputIndex: uint32(index),
×
1383
                                },
×
1384
                                Force:       !channel.GetActive(),
×
1385
                                TargetConf:  int32(ctx.Int64("conf_target")),
×
1386
                                SatPerVbyte: ctx.Uint64(feeRateFlag),
×
1387
                        }
×
1388

×
1389
                        txidChan := make(chan string, 1)
×
1390
                        err = executeChannelClose(ctxc, client, req, txidChan, false)
×
1391
                        if err != nil {
×
1392
                                res.FailErr = fmt.Sprintf("unable to close "+
×
1393
                                        "channel: %v", err)
×
1394
                                return
×
1395
                        }
×
1396

1397
                        res.ClosingTxid = <-txidChan
×
1398
                }(channel)
1399
        }
1400

1401
        for range channelsToClose {
×
1402
                res := <-resultChan
×
1403
                printJSON(res)
×
1404
        }
×
1405

1406
        return nil
×
1407
}
1408

1409
// promptForConfirmation continuously prompts the user for the message until
1410
// receiving a response of "yes" or "no" and returns their answer as a bool.
1411
func promptForConfirmation(msg string) bool {
×
1412
        reader := bufio.NewReader(os.Stdin)
×
1413

×
1414
        for {
×
1415
                fmt.Print(msg)
×
1416

×
1417
                answer, err := reader.ReadString('\n')
×
1418
                if err != nil {
×
1419
                        return false
×
1420
                }
×
1421

1422
                answer = strings.ToLower(strings.TrimSpace(answer))
×
1423

×
1424
                switch {
×
1425
                case answer == "yes":
×
1426
                        return true
×
1427
                case answer == "no":
×
1428
                        return false
×
1429
                default:
×
1430
                        continue
×
1431
                }
1432
        }
1433
}
1434

1435
var abandonChannelCommand = cli.Command{
1436
        Name:     "abandonchannel",
1437
        Category: "Channels",
1438
        Usage:    "Abandons an existing channel.",
1439
        Description: `
1440
        Removes all channel state from the database except for a close
1441
        summary. This method can be used to get rid of permanently unusable
1442
        channels due to bugs fixed in newer versions of lnd.
1443

1444
        Only available when lnd is built in debug mode. The flag
1445
        --i_know_what_i_am_doing can be set to override the debug/dev mode
1446
        requirement.
1447

1448
        To view which funding_txids/output_indexes can be used for this command,
1449
        see the channel_point values within the listchannels command output.
1450
        The format for a channel_point is 'funding_txid:output_index'.`,
1451
        ArgsUsage: "funding_txid [output_index]",
1452
        Flags: []cli.Flag{
1453
                cli.StringFlag{
1454
                        Name:  "funding_txid",
1455
                        Usage: "the txid of the channel's funding transaction",
1456
                },
1457
                cli.IntFlag{
1458
                        Name: "output_index",
1459
                        Usage: "the output index for the funding output of the funding " +
1460
                                "transaction",
1461
                },
1462
                cli.StringFlag{
1463
                        Name: "chan_point",
1464
                        Usage: "(optional) the channel point. If set, " +
1465
                                "funding_txid and output_index flags and " +
1466
                                "positional arguments will be ignored",
1467
                },
1468
                cli.BoolFlag{
1469
                        Name: "i_know_what_i_am_doing",
1470
                        Usage: "override the requirement for lnd needing to " +
1471
                                "be in dev/debug mode to use this command; " +
1472
                                "when setting this the user attests that " +
1473
                                "they know the danger of using this command " +
1474
                                "on channels and that doing so can lead to " +
1475
                                "loss of funds if the channel funding TX " +
1476
                                "ever confirms (or was confirmed)",
1477
                },
1478
        },
1479
        Action: actionDecorator(abandonChannel),
1480
}
1481

1482
func abandonChannel(ctx *cli.Context) error {
×
1483
        ctxc := getContext()
×
1484
        client, cleanUp := getClient(ctx)
×
1485
        defer cleanUp()
×
1486

×
1487
        // Show command help if no arguments and flags were provided.
×
1488
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1489
                cli.ShowCommandHelp(ctx, "abandonchannel")
×
1490
                return nil
×
1491
        }
×
1492

1493
        channelPoint, err := parseChannelPoint(ctx)
×
1494
        if err != nil {
×
1495
                return err
×
1496
        }
×
1497

1498
        req := &lnrpc.AbandonChannelRequest{
×
1499
                ChannelPoint:      channelPoint,
×
1500
                IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
×
1501
        }
×
1502

×
1503
        resp, err := client.AbandonChannel(ctxc, req)
×
1504
        if err != nil {
×
1505
                return err
×
1506
        }
×
1507

1508
        printRespJSON(resp)
×
1509
        return nil
×
1510
}
1511

1512
// parseChannelPoint parses a funding txid and output index from the command
1513
// line. Both named options and unnamed parameters are supported.
1514
func parseChannelPoint(ctx *cli.Context) (*lnrpc.ChannelPoint, error) {
×
1515
        channelPoint := &lnrpc.ChannelPoint{}
×
1516
        var err error
×
1517

×
1518
        args := ctx.Args()
×
1519

×
1520
        switch {
×
1521
        case ctx.IsSet("chan_point"):
×
1522
                channelPoint, err = parseChanPoint(ctx.String("chan_point"))
×
1523
                if err != nil {
×
1524
                        return nil, fmt.Errorf("unable to parse chan_point: "+
×
1525
                                "%v", err)
×
1526
                }
×
1527
                return channelPoint, nil
×
1528

1529
        case ctx.IsSet("funding_txid"):
×
1530
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1531
                        FundingTxidStr: ctx.String("funding_txid"),
×
1532
                }
×
1533
        case args.Present():
×
1534
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1535
                        FundingTxidStr: args.First(),
×
1536
                }
×
1537
                args = args.Tail()
×
1538
        default:
×
1539
                return nil, fmt.Errorf("funding txid argument missing")
×
1540
        }
1541

1542
        switch {
×
1543
        case ctx.IsSet("output_index"):
×
1544
                channelPoint.OutputIndex = uint32(ctx.Int("output_index"))
×
1545
        case args.Present():
×
1546
                index, err := strconv.ParseUint(args.First(), 10, 32)
×
1547
                if err != nil {
×
1548
                        return nil, fmt.Errorf("unable to decode output "+
×
1549
                                "index: %w", err)
×
1550
                }
×
1551
                channelPoint.OutputIndex = uint32(index)
×
1552
        default:
×
1553
                channelPoint.OutputIndex = 0
×
1554
        }
1555

1556
        return channelPoint, nil
×
1557
}
1558

1559
var listPeersCommand = cli.Command{
1560
        Name:     "listpeers",
1561
        Category: "Peers",
1562
        Usage:    "List all active, currently connected peers.",
1563
        Flags: []cli.Flag{
1564
                cli.BoolFlag{
1565
                        Name:  "list_errors",
1566
                        Usage: "list a full set of most recent errors for the peer",
1567
                },
1568
        },
1569
        Action: actionDecorator(listPeers),
1570
}
1571

1572
func listPeers(ctx *cli.Context) error {
×
1573
        ctxc := getContext()
×
1574
        client, cleanUp := getClient(ctx)
×
1575
        defer cleanUp()
×
1576

×
1577
        // By default, we display a single error on the cli. If the user
×
1578
        // specifically requests a full error set, then we will provide it.
×
1579
        req := &lnrpc.ListPeersRequest{
×
1580
                LatestError: !ctx.IsSet("list_errors"),
×
1581
        }
×
1582
        resp, err := client.ListPeers(ctxc, req)
×
1583
        if err != nil {
×
1584
                return err
×
1585
        }
×
1586

1587
        printRespJSON(resp)
×
1588
        return nil
×
1589
}
1590

1591
var walletBalanceCommand = cli.Command{
1592
        Name:     "walletbalance",
1593
        Category: "Wallet",
1594
        Usage:    "Compute and display the wallet's current balance.",
1595
        Flags: []cli.Flag{
1596
                cli.StringFlag{
1597
                        Name: "account",
1598
                        Usage: "(optional) the account for which the balance " +
1599
                                "is shown",
1600
                        Value: "",
1601
                },
1602
        },
1603
        Action: actionDecorator(walletBalance),
1604
}
1605

1606
func walletBalance(ctx *cli.Context) error {
×
1607
        ctxc := getContext()
×
1608
        client, cleanUp := getClient(ctx)
×
1609
        defer cleanUp()
×
1610

×
1611
        req := &lnrpc.WalletBalanceRequest{
×
1612
                Account: ctx.String("account"),
×
1613
        }
×
1614
        resp, err := client.WalletBalance(ctxc, req)
×
1615
        if err != nil {
×
1616
                return err
×
1617
        }
×
1618

1619
        printRespJSON(resp)
×
1620
        return nil
×
1621
}
1622

1623
var ChannelBalanceCommand = cli.Command{
1624
        Name:     "channelbalance",
1625
        Category: "Channels",
1626
        Usage: "Returns the sum of the total available channel balance across " +
1627
                "all open channels.",
1628
        Action: actionDecorator(ChannelBalance),
1629
}
1630

1631
func ChannelBalance(ctx *cli.Context) error {
×
1632
        ctxc := getContext()
×
1633
        client, cleanUp := getClient(ctx)
×
1634
        defer cleanUp()
×
1635

×
1636
        req := &lnrpc.ChannelBalanceRequest{}
×
1637
        resp, err := client.ChannelBalance(ctxc, req)
×
1638
        if err != nil {
×
1639
                return err
×
1640
        }
×
1641

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

1646
var generateManPageCommand = cli.Command{
1647
        Name: "generatemanpage",
1648
        Usage: "Generates a man page for lncli and lnd as " +
1649
                "lncli.1 and lnd.1 respectively.",
1650
        Hidden: true,
1651
        Action: actionDecorator(generateManPage),
1652
}
1653

1654
func generateManPage(ctx *cli.Context) error {
×
1655
        // Generate the man pages for lncli as lncli.1.
×
1656
        manpages, err := ctx.App.ToMan()
×
1657
        if err != nil {
×
1658
                return err
×
1659
        }
×
1660
        err = os.WriteFile("lncli.1", []byte(manpages), 0644)
×
1661
        if err != nil {
×
1662
                return err
×
1663
        }
×
1664

1665
        // Generate the man pages for lnd as lnd.1.
1666
        config := lnd.DefaultConfig()
×
1667
        fileParser := flags.NewParser(&config, flags.Default)
×
1668
        fileParser.Name = "lnd"
×
1669

×
1670
        var buf bytes.Buffer
×
1671
        fileParser.WriteManPage(&buf)
×
1672

×
1673
        err = os.WriteFile("lnd.1", buf.Bytes(), 0644)
×
1674
        if err != nil {
×
1675
                return err
×
1676
        }
×
1677

1678
        return nil
×
1679
}
1680

1681
var getInfoCommand = cli.Command{
1682
        Name:   "getinfo",
1683
        Usage:  "Returns basic information related to the active daemon.",
1684
        Action: actionDecorator(getInfo),
1685
}
1686

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

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

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

1702
var getRecoveryInfoCommand = cli.Command{
1703
        Name:   "getrecoveryinfo",
1704
        Usage:  "Display information about an ongoing recovery attempt.",
1705
        Action: actionDecorator(getRecoveryInfo),
1706
}
1707

1708
func getRecoveryInfo(ctx *cli.Context) error {
×
1709
        ctxc := getContext()
×
1710
        client, cleanUp := getClient(ctx)
×
1711
        defer cleanUp()
×
1712

×
1713
        req := &lnrpc.GetRecoveryInfoRequest{}
×
1714
        resp, err := client.GetRecoveryInfo(ctxc, req)
×
1715
        if err != nil {
×
1716
                return err
×
1717
        }
×
1718

1719
        printRespJSON(resp)
×
1720
        return nil
×
1721
}
1722

1723
var pendingChannelsCommand = cli.Command{
1724
        Name:     "pendingchannels",
1725
        Category: "Channels",
1726
        Usage:    "Display information pertaining to pending channels.",
1727
        Flags: []cli.Flag{
1728
                cli.BoolFlag{
1729
                        Name: "include_raw_tx",
1730
                        Usage: "include the raw transaction hex for " +
1731
                                "waiting_close_channels.",
1732
                },
1733
        },
1734
        Action: actionDecorator(pendingChannels),
1735
}
1736

1737
func pendingChannels(ctx *cli.Context) error {
×
1738
        ctxc := getContext()
×
1739
        client, cleanUp := getClient(ctx)
×
1740
        defer cleanUp()
×
1741

×
1742
        includeRawTx := ctx.Bool("include_raw_tx")
×
1743
        req := &lnrpc.PendingChannelsRequest{
×
1744
                IncludeRawTx: includeRawTx,
×
1745
        }
×
1746
        resp, err := client.PendingChannels(ctxc, req)
×
1747
        if err != nil {
×
1748
                return err
×
1749
        }
×
1750

1751
        printRespJSON(resp)
×
1752

×
1753
        return nil
×
1754
}
1755

1756
var ListChannelsCommand = cli.Command{
1757
        Name:     "listchannels",
1758
        Category: "Channels",
1759
        Usage:    "List all open channels.",
1760
        Flags: []cli.Flag{
1761
                cli.BoolFlag{
1762
                        Name:  "active_only",
1763
                        Usage: "only list channels which are currently active",
1764
                },
1765
                cli.BoolFlag{
1766
                        Name:  "inactive_only",
1767
                        Usage: "only list channels which are currently inactive",
1768
                },
1769
                cli.BoolFlag{
1770
                        Name:  "public_only",
1771
                        Usage: "only list channels which are currently public",
1772
                },
1773
                cli.BoolFlag{
1774
                        Name:  "private_only",
1775
                        Usage: "only list channels which are currently private",
1776
                },
1777
                cli.StringFlag{
1778
                        Name: "peer",
1779
                        Usage: "(optional) only display channels with a " +
1780
                                "particular peer, accepts 66-byte, " +
1781
                                "hex-encoded pubkeys",
1782
                },
1783
                cli.BoolFlag{
1784
                        Name: "skip_peer_alias_lookup",
1785
                        Usage: "skip the peer alias lookup per channel in " +
1786
                                "order to improve performance",
1787
                },
1788
        },
1789
        Action: actionDecorator(ListChannels),
1790
}
1791

1792
var listAliasesCommand = cli.Command{
1793
        Name:     "listaliases",
1794
        Category: "Channels",
1795
        Usage:    "List all aliases.",
1796
        Flags:    []cli.Flag{},
1797
        Action:   actionDecorator(listAliases),
1798
}
1799

1800
func listAliases(ctx *cli.Context) error {
×
1801
        ctxc := getContext()
×
1802
        client, cleanUp := getClient(ctx)
×
1803
        defer cleanUp()
×
1804

×
1805
        req := &lnrpc.ListAliasesRequest{}
×
1806

×
1807
        resp, err := client.ListAliases(ctxc, req)
×
1808
        if err != nil {
×
1809
                return err
×
1810
        }
×
1811

1812
        printRespJSON(resp)
×
1813

×
1814
        return nil
×
1815
}
1816

1817
func ListChannels(ctx *cli.Context) error {
×
1818
        ctxc := getContext()
×
1819
        client, cleanUp := getClient(ctx)
×
1820
        defer cleanUp()
×
1821

×
1822
        peer := ctx.String("peer")
×
1823

×
1824
        // If the user requested channels with a particular key, parse the
×
1825
        // provided pubkey.
×
1826
        var peerKey []byte
×
1827
        if len(peer) > 0 {
×
1828
                pk, err := route.NewVertexFromStr(peer)
×
1829
                if err != nil {
×
1830
                        return fmt.Errorf("invalid --peer pubkey: %w", err)
×
1831
                }
×
1832

1833
                peerKey = pk[:]
×
1834
        }
1835

1836
        // By default, we will look up the peers' alias information unless the
1837
        // skip_peer_alias_lookup flag indicates otherwise.
1838
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1839

×
1840
        req := &lnrpc.ListChannelsRequest{
×
1841
                ActiveOnly:      ctx.Bool("active_only"),
×
1842
                InactiveOnly:    ctx.Bool("inactive_only"),
×
1843
                PublicOnly:      ctx.Bool("public_only"),
×
1844
                PrivateOnly:     ctx.Bool("private_only"),
×
1845
                Peer:            peerKey,
×
1846
                PeerAliasLookup: lookupPeerAlias,
×
1847
        }
×
1848

×
1849
        resp, err := client.ListChannels(ctxc, req)
×
1850
        if err != nil {
×
1851
                return err
×
1852
        }
×
1853

1854
        printRespJSON(resp)
×
1855

×
1856
        return nil
×
1857
}
1858

1859
var closedChannelsCommand = cli.Command{
1860
        Name:     "closedchannels",
1861
        Category: "Channels",
1862
        Usage:    "List all closed channels.",
1863
        Flags: []cli.Flag{
1864
                cli.BoolFlag{
1865
                        Name:  "cooperative",
1866
                        Usage: "list channels that were closed cooperatively",
1867
                },
1868
                cli.BoolFlag{
1869
                        Name: "local_force",
1870
                        Usage: "list channels that were force-closed " +
1871
                                "by the local node",
1872
                },
1873
                cli.BoolFlag{
1874
                        Name: "remote_force",
1875
                        Usage: "list channels that were force-closed " +
1876
                                "by the remote node",
1877
                },
1878
                cli.BoolFlag{
1879
                        Name: "breach",
1880
                        Usage: "list channels for which the remote node " +
1881
                                "attempted to broadcast a prior " +
1882
                                "revoked channel state",
1883
                },
1884
                cli.BoolFlag{
1885
                        Name:  "funding_canceled",
1886
                        Usage: "list channels that were never fully opened",
1887
                },
1888
                cli.BoolFlag{
1889
                        Name: "abandoned",
1890
                        Usage: "list channels that were abandoned by " +
1891
                                "the local node",
1892
                },
1893
        },
1894
        Action: actionDecorator(closedChannels),
1895
}
1896

1897
func closedChannels(ctx *cli.Context) error {
×
1898
        ctxc := getContext()
×
1899
        client, cleanUp := getClient(ctx)
×
1900
        defer cleanUp()
×
1901

×
1902
        req := &lnrpc.ClosedChannelsRequest{
×
1903
                Cooperative:     ctx.Bool("cooperative"),
×
1904
                LocalForce:      ctx.Bool("local_force"),
×
1905
                RemoteForce:     ctx.Bool("remote_force"),
×
1906
                Breach:          ctx.Bool("breach"),
×
1907
                FundingCanceled: ctx.Bool("funding_canceled"),
×
1908
                Abandoned:       ctx.Bool("abandoned"),
×
1909
        }
×
1910

×
1911
        resp, err := client.ClosedChannels(ctxc, req)
×
1912
        if err != nil {
×
1913
                return err
×
1914
        }
×
1915

1916
        printRespJSON(resp)
×
1917

×
1918
        return nil
×
1919
}
1920

1921
var describeGraphCommand = cli.Command{
1922
        Name:     "describegraph",
1923
        Category: "Graph",
1924
        Description: "Prints a human readable version of the known channel " +
1925
                "graph from the PoV of the node",
1926
        Usage: "Describe the network graph.",
1927
        Flags: []cli.Flag{
1928
                cli.BoolFlag{
1929
                        Name: "include_unannounced",
1930
                        Usage: "If set, unannounced channels will be included in the " +
1931
                                "graph. Unannounced channels are both private channels, and " +
1932
                                "public channels that are not yet announced to the network.",
1933
                },
1934
        },
1935
        Action: actionDecorator(describeGraph),
1936
}
1937

1938
func describeGraph(ctx *cli.Context) error {
×
1939
        ctxc := getContext()
×
1940
        client, cleanUp := getClient(ctx)
×
1941
        defer cleanUp()
×
1942

×
1943
        req := &lnrpc.ChannelGraphRequest{
×
1944
                IncludeUnannounced: ctx.Bool("include_unannounced"),
×
1945
        }
×
1946

×
1947
        graph, err := client.DescribeGraph(ctxc, req)
×
1948
        if err != nil {
×
1949
                return err
×
1950
        }
×
1951

1952
        printRespJSON(graph)
×
1953
        return nil
×
1954
}
1955

1956
var getNodeMetricsCommand = cli.Command{
1957
        Name:        "getnodemetrics",
1958
        Category:    "Graph",
1959
        Description: "Prints out node metrics calculated from the current graph",
1960
        Usage:       "Get node metrics.",
1961
        Action:      actionDecorator(getNodeMetrics),
1962
}
1963

1964
func getNodeMetrics(ctx *cli.Context) error {
×
1965
        ctxc := getContext()
×
1966
        client, cleanUp := getClient(ctx)
×
1967
        defer cleanUp()
×
1968

×
1969
        req := &lnrpc.NodeMetricsRequest{
×
1970
                Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
×
1971
        }
×
1972

×
1973
        nodeMetrics, err := client.GetNodeMetrics(ctxc, req)
×
1974
        if err != nil {
×
1975
                return err
×
1976
        }
×
1977

1978
        printRespJSON(nodeMetrics)
×
1979
        return nil
×
1980
}
1981

1982
var getChanInfoCommand = cli.Command{
1983
        Name:     "getchaninfo",
1984
        Category: "Graph",
1985
        Usage:    "Get the state of a channel.",
1986
        Description: "Prints out the latest authenticated state for a " +
1987
                "particular channel",
1988
        ArgsUsage: "chan_id",
1989
        Flags: []cli.Flag{
1990
                cli.Uint64Flag{
1991
                        Name: "chan_id",
1992
                        Usage: "The 8-byte compact channel ID to query for. " +
1993
                                "If this is set the chan_point param is " +
1994
                                "ignored.",
1995
                },
1996
                cli.StringFlag{
1997
                        Name: "chan_point",
1998
                        Usage: "The channel point in format txid:index. If " +
1999
                                "the chan_id param is set this param is " +
2000
                                "ignored.",
2001
                },
2002
        },
2003
        Action: actionDecorator(getChanInfo),
2004
}
2005

2006
func getChanInfo(ctx *cli.Context) error {
×
2007
        ctxc := getContext()
×
2008
        client, cleanUp := getClient(ctx)
×
2009
        defer cleanUp()
×
2010

×
2011
        var (
×
2012
                chanID    uint64
×
2013
                chanPoint string
×
2014
                err       error
×
2015
        )
×
2016

×
2017
        switch {
×
2018
        case ctx.IsSet("chan_id"):
×
2019
                chanID = ctx.Uint64("chan_id")
×
2020

2021
        case ctx.Args().Present():
×
2022
                chanID, err = strconv.ParseUint(ctx.Args().First(), 10, 64)
×
2023
                if err != nil {
×
2024
                        return fmt.Errorf("error parsing chan_id: %w", err)
×
2025
                }
×
2026

2027
        case ctx.IsSet("chan_point"):
×
2028
                chanPoint = ctx.String("chan_point")
×
2029

2030
        default:
×
2031
                return fmt.Errorf("chan_id or chan_point argument missing")
×
2032
        }
2033

2034
        req := &lnrpc.ChanInfoRequest{
×
2035
                ChanId:    chanID,
×
2036
                ChanPoint: chanPoint,
×
2037
        }
×
2038

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

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

2048
var getNodeInfoCommand = cli.Command{
2049
        Name:     "getnodeinfo",
2050
        Category: "Graph",
2051
        Usage:    "Get information on a specific node.",
2052
        Description: "Prints out the latest authenticated node state for an " +
2053
                "advertised node",
2054
        Flags: []cli.Flag{
2055
                cli.StringFlag{
2056
                        Name: "pub_key",
2057
                        Usage: "the 33-byte hex-encoded compressed public of the target " +
2058
                                "node",
2059
                },
2060
                cli.BoolFlag{
2061
                        Name: "include_channels",
2062
                        Usage: "if true, will return all known channels " +
2063
                                "associated with the node",
2064
                },
2065
        },
2066
        Action: actionDecorator(getNodeInfo),
2067
}
2068

2069
func getNodeInfo(ctx *cli.Context) error {
×
2070
        ctxc := getContext()
×
2071
        client, cleanUp := getClient(ctx)
×
2072
        defer cleanUp()
×
2073

×
2074
        args := ctx.Args()
×
2075

×
2076
        var pubKey string
×
2077
        switch {
×
2078
        case ctx.IsSet("pub_key"):
×
2079
                pubKey = ctx.String("pub_key")
×
2080
        case args.Present():
×
2081
                pubKey = args.First()
×
2082
        default:
×
2083
                return fmt.Errorf("pub_key argument missing")
×
2084
        }
2085

2086
        req := &lnrpc.NodeInfoRequest{
×
2087
                PubKey:          pubKey,
×
2088
                IncludeChannels: ctx.Bool("include_channels"),
×
2089
        }
×
2090

×
2091
        nodeInfo, err := client.GetNodeInfo(ctxc, req)
×
2092
        if err != nil {
×
2093
                return err
×
2094
        }
×
2095

2096
        printRespJSON(nodeInfo)
×
2097
        return nil
×
2098
}
2099

2100
var getNetworkInfoCommand = cli.Command{
2101
        Name:     "getnetworkinfo",
2102
        Category: "Channels",
2103
        Usage: "Get statistical information about the current " +
2104
                "state of the network.",
2105
        Description: "Returns a set of statistics pertaining to the known " +
2106
                "channel graph",
2107
        Action: actionDecorator(getNetworkInfo),
2108
}
2109

2110
func getNetworkInfo(ctx *cli.Context) error {
×
2111
        ctxc := getContext()
×
2112
        client, cleanUp := getClient(ctx)
×
2113
        defer cleanUp()
×
2114

×
2115
        req := &lnrpc.NetworkInfoRequest{}
×
2116

×
2117
        netInfo, err := client.GetNetworkInfo(ctxc, req)
×
2118
        if err != nil {
×
2119
                return err
×
2120
        }
×
2121

2122
        printRespJSON(netInfo)
×
2123
        return nil
×
2124
}
2125

2126
var debugLevelCommand = cli.Command{
2127
        Name:  "debuglevel",
2128
        Usage: "Set the debug level.",
2129
        Description: `Logging level for all subsystems {trace, debug, info, warn, error, critical, off}
2130
        You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems
2131

2132
        Use show to list available subsystems`,
2133
        Flags: []cli.Flag{
2134
                cli.BoolFlag{
2135
                        Name:  "show",
2136
                        Usage: "if true, then the list of available sub-systems will be printed out",
2137
                },
2138
                cli.StringFlag{
2139
                        Name:  "level",
2140
                        Usage: "the level specification to target either a coarse logging level, or granular set of specific sub-systems with logging levels for each",
2141
                },
2142
        },
2143
        Action: actionDecorator(debugLevel),
2144
}
2145

2146
func debugLevel(ctx *cli.Context) error {
×
2147
        ctxc := getContext()
×
2148
        client, cleanUp := getClient(ctx)
×
2149
        defer cleanUp()
×
2150
        req := &lnrpc.DebugLevelRequest{
×
2151
                Show:      ctx.Bool("show"),
×
2152
                LevelSpec: ctx.String("level"),
×
2153
        }
×
2154

×
2155
        resp, err := client.DebugLevel(ctxc, req)
×
2156
        if err != nil {
×
2157
                return err
×
2158
        }
×
2159

2160
        printRespJSON(resp)
×
2161
        return nil
×
2162
}
2163

2164
var listChainTxnsCommand = cli.Command{
2165
        Name:     "listchaintxns",
2166
        Category: "On-chain",
2167
        Usage:    "List transactions from the wallet.",
2168
        Flags: []cli.Flag{
2169
                cli.Int64Flag{
2170
                        Name: "start_height",
2171
                        Usage: "the block height from which to list " +
2172
                                "transactions, inclusive",
2173
                },
2174
                cli.Int64Flag{
2175
                        Name: "end_height",
2176
                        Usage: "the block height until which to list " +
2177
                                "transactions, inclusive, to get " +
2178
                                "transactions until the chain tip, including " +
2179
                                "unconfirmed, set this value to -1",
2180
                },
2181
                cli.UintFlag{
2182
                        Name: "index_offset",
2183
                        Usage: "the index of a transaction that will be " +
2184
                                "used in a query to determine which " +
2185
                                "transaction should be returned in the " +
2186
                                "response",
2187
                },
2188
                cli.IntFlag{
2189
                        Name: "max_transactions",
2190
                        Usage: "(optional) the max number of transactions to " +
2191
                                "return; leave at default of 0 to return " +
2192
                                "all transactions",
2193
                        Value: 0,
2194
                },
2195
        },
2196
        Description: `
2197
        List all transactions an address of the wallet was involved in.
2198

2199
        This call will return a list of wallet related transactions that paid
2200
        to an address our wallet controls, or spent utxos that we held. The
2201
        start_height and end_height flags can be used to specify an inclusive
2202
        block range over which to query for transactions. If the end_height is
2203
        less than the start_height, transactions will be queried in reverse.
2204
        To get all transactions until the chain tip, including unconfirmed
2205
        transactions (identifiable with BlockHeight=0), set end_height to -1.
2206
        By default, this call will get all transactions our wallet was involved
2207
        in, including unconfirmed transactions.
2208
`,
2209
        Action: actionDecorator(listChainTxns),
2210
}
2211

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

×
2217
        req := &lnrpc.GetTransactionsRequest{
×
2218
                IndexOffset:     uint32(ctx.Uint64("index_offset")),
×
2219
                MaxTransactions: uint32(ctx.Uint64("max_transactions")),
×
2220
        }
×
2221

×
2222
        if ctx.IsSet("start_height") {
×
2223
                req.StartHeight = int32(ctx.Int64("start_height"))
×
2224
        }
×
2225
        if ctx.IsSet("end_height") {
×
2226
                req.EndHeight = int32(ctx.Int64("end_height"))
×
2227
        }
×
2228

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

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

2238
var stopCommand = cli.Command{
2239
        Name:  "stop",
2240
        Usage: "Stop and shutdown the daemon.",
2241
        Description: `
2242
        Gracefully stop all daemon subsystems before stopping the daemon itself.
2243
        This is equivalent to stopping it using CTRL-C.`,
2244
        Action: actionDecorator(stopDaemon),
2245
}
2246

2247
func stopDaemon(ctx *cli.Context) error {
×
2248
        ctxc := getContext()
×
2249
        client, cleanUp := getClient(ctx)
×
2250
        defer cleanUp()
×
2251

×
2252
        resp, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2253
        if err != nil {
×
2254
                return err
×
2255
        }
×
2256

2257
        printRespJSON(resp)
×
2258

×
2259
        return nil
×
2260
}
2261

2262
var signMessageCommand = cli.Command{
2263
        Name:      "signmessage",
2264
        Category:  "Wallet",
2265
        Usage:     "Sign a message with the node's private key.",
2266
        ArgsUsage: "msg",
2267
        Description: `
2268
        Sign msg with the resident node's private key.
2269
        Returns the signature as a zbase32 string.
2270

2271
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2272
        Flags: []cli.Flag{
2273
                cli.StringFlag{
2274
                        Name:  "msg",
2275
                        Usage: "the message to sign",
2276
                },
2277
        },
2278
        Action: actionDecorator(signMessage),
2279
}
2280

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

×
2286
        var msg []byte
×
2287

×
2288
        switch {
×
2289
        case ctx.IsSet("msg"):
×
2290
                msg = []byte(ctx.String("msg"))
×
2291
        case ctx.Args().Present():
×
2292
                msg = []byte(ctx.Args().First())
×
2293
        default:
×
2294
                return fmt.Errorf("msg argument missing")
×
2295
        }
2296

2297
        resp, err := client.SignMessage(ctxc, &lnrpc.SignMessageRequest{Msg: msg})
×
2298
        if err != nil {
×
2299
                return err
×
2300
        }
×
2301

2302
        printRespJSON(resp)
×
2303
        return nil
×
2304
}
2305

2306
var verifyMessageCommand = cli.Command{
2307
        Name:      "verifymessage",
2308
        Category:  "Wallet",
2309
        Usage:     "Verify a message signed with the signature.",
2310
        ArgsUsage: "msg signature",
2311
        Description: `
2312
        Verify that the message was signed with a properly-formed signature
2313
        The signature must be zbase32 encoded and signed with the private key of
2314
        an active node in the resident node's channel database.
2315

2316
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2317
        Flags: []cli.Flag{
2318
                cli.StringFlag{
2319
                        Name:  "msg",
2320
                        Usage: "the message to verify",
2321
                },
2322
                cli.StringFlag{
2323
                        Name:  "sig",
2324
                        Usage: "the zbase32 encoded signature of the message",
2325
                },
2326
        },
2327
        Action: actionDecorator(verifyMessage),
2328
}
2329

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

×
2335
        var (
×
2336
                msg []byte
×
2337
                sig string
×
2338
        )
×
2339

×
2340
        args := ctx.Args()
×
2341

×
2342
        switch {
×
2343
        case ctx.IsSet("msg"):
×
2344
                msg = []byte(ctx.String("msg"))
×
2345
        case args.Present():
×
2346
                msg = []byte(ctx.Args().First())
×
2347
                args = args.Tail()
×
2348
        default:
×
2349
                return fmt.Errorf("msg argument missing")
×
2350
        }
2351

2352
        switch {
×
2353
        case ctx.IsSet("sig"):
×
2354
                sig = ctx.String("sig")
×
2355
        case args.Present():
×
2356
                sig = args.First()
×
2357
        default:
×
2358
                return fmt.Errorf("signature argument missing")
×
2359
        }
2360

2361
        req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
×
2362
        resp, err := client.VerifyMessage(ctxc, req)
×
2363
        if err != nil {
×
2364
                return err
×
2365
        }
×
2366

2367
        printRespJSON(resp)
×
2368
        return nil
×
2369
}
2370

2371
var feeReportCommand = cli.Command{
2372
        Name:     "feereport",
2373
        Category: "Channels",
2374
        Usage:    "Display the current fee policies of all active channels.",
2375
        Description: `
2376
        Returns the current fee policies of all active channels.
2377
        Fee policies can be updated using the updatechanpolicy command.`,
2378
        Action: actionDecorator(feeReport),
2379
}
2380

2381
func feeReport(ctx *cli.Context) error {
×
2382
        ctxc := getContext()
×
2383
        client, cleanUp := getClient(ctx)
×
2384
        defer cleanUp()
×
2385

×
2386
        req := &lnrpc.FeeReportRequest{}
×
2387
        resp, err := client.FeeReport(ctxc, req)
×
2388
        if err != nil {
×
2389
                return err
×
2390
        }
×
2391

2392
        printRespJSON(resp)
×
2393
        return nil
×
2394
}
2395

2396
var updateChannelPolicyCommand = cli.Command{
2397
        Name:     "updatechanpolicy",
2398
        Category: "Channels",
2399
        Usage: "Update the channel policy for all channels, or a single " +
2400
                "channel.",
2401
        ArgsUsage: "base_fee_msat fee_rate time_lock_delta " +
2402
                "[--max_htlc_msat=N] [channel_point]",
2403
        Description: `
2404
        Updates the channel policy for all channels, or just a particular
2405
        channel identified by its channel point. The update will be committed, 
2406
        and broadcast to the rest of the network within the next batch. Channel
2407
        points are encoded as: funding_txid:output_index
2408
        `,
2409
        Flags: []cli.Flag{
2410
                cli.Int64Flag{
2411
                        Name: "base_fee_msat",
2412
                        Usage: "the base fee in milli-satoshis that will be " +
2413
                                "charged for each forwarded HTLC, regardless " +
2414
                                "of payment size",
2415
                },
2416
                cli.StringFlag{
2417
                        Name: "fee_rate",
2418
                        Usage: "the fee rate that will be charged " +
2419
                                "proportionally based on the value of each " +
2420
                                "forwarded HTLC, the lowest possible rate is " +
2421
                                "0 with a granularity of 0.000001 " +
2422
                                "(millionths). Can not be set at the same " +
2423
                                "time as fee_rate_ppm",
2424
                },
2425
                cli.Uint64Flag{
2426
                        Name: "fee_rate_ppm",
2427
                        Usage: "the fee rate ppm (parts per million) that " +
2428
                                "will be charged proportionally based on the " +
2429
                                "value of each forwarded HTLC, the lowest " +
2430
                                "possible rate is 0 with a granularity of " +
2431
                                "0.000001 (millionths). Can not be set at " +
2432
                                "the same time as fee_rate",
2433
                },
2434
                cli.Int64Flag{
2435
                        Name: "inbound_base_fee_msat",
2436
                        Usage: "the base inbound fee in milli-satoshis that " +
2437
                                "will be charged for each forwarded HTLC, " +
2438
                                "regardless of payment size. Its value must " +
2439
                                "be zero or negative - it is a discount " +
2440
                                "for using a particular incoming channel. " +
2441
                                "Note that forwards will be rejected if the " +
2442
                                "discount exceeds the outbound fee " +
2443
                                "(forward at a loss), and lead to " +
2444
                                "penalization by the sender",
2445
                },
2446
                cli.Int64Flag{
2447
                        Name: "inbound_fee_rate_ppm",
2448
                        Usage: "the inbound fee rate that will be charged " +
2449
                                "proportionally based on the value of each " +
2450
                                "forwarded HTLC and the outbound fee. Fee " +
2451
                                "rate is expressed in parts per million and " +
2452
                                "must be zero or negative - it is a discount " +
2453
                                "for using a particular incoming channel. " +
2454
                                "Note that forwards will be rejected if the " +
2455
                                "discount exceeds the outbound fee " +
2456
                                "(forward at a loss), and lead to " +
2457
                                "penalization by the sender",
2458
                },
2459
                cli.Uint64Flag{
2460
                        Name: "time_lock_delta",
2461
                        Usage: "the CLTV delta that will be applied to all " +
2462
                                "forwarded HTLCs",
2463
                },
2464
                cli.Uint64Flag{
2465
                        Name: "min_htlc_msat",
2466
                        Usage: "if set, the min HTLC size that will be " +
2467
                                "applied to all forwarded HTLCs. If unset, " +
2468
                                "the min HTLC is left unchanged",
2469
                },
2470
                cli.Uint64Flag{
2471
                        Name: "max_htlc_msat",
2472
                        Usage: "if set, the max HTLC size that will be " +
2473
                                "applied to all forwarded HTLCs. If unset, " +
2474
                                "the max HTLC is left unchanged",
2475
                },
2476
                cli.StringFlag{
2477
                        Name: "chan_point",
2478
                        Usage: "the channel which this policy update should " +
2479
                                "be applied to. If nil, the policies for all " +
2480
                                "channels will be updated. Takes the form of " +
2481
                                "txid:output_index",
2482
                },
2483
                cli.BoolFlag{
2484
                        Name: "create_missing_edge",
2485
                        Usage: "Under unknown circumstances a channel can " +
2486
                                "exist with a missing edge in the graph " +
2487
                                "database. This can cause an 'edge not " +
2488
                                "found' error when calling `getchaninfo` " +
2489
                                "and/or cause the default channel policy to " +
2490
                                "be used during forwards. Setting this flag " +
2491
                                "will recreate the edge if not found, " +
2492
                                "allowing updating this channel policy and " +
2493
                                "fixing the missing edge problem for this " +
2494
                                "channel permanently. For fields not set in " +
2495
                                "this command, the default policy will be " +
2496
                                "created.",
2497
                },
2498
        },
2499
        Action: actionDecorator(updateChannelPolicy),
2500
}
2501

2502
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
7✔
2503
        split := strings.Split(s, ":")
7✔
2504
        if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
10✔
2505
                return nil, errBadChanPoint
3✔
2506
        }
3✔
2507

2508
        index, err := strconv.ParseInt(split[1], 10, 64)
4✔
2509
        if err != nil {
5✔
2510
                return nil, fmt.Errorf("unable to decode output index: %w", err)
1✔
2511
        }
1✔
2512

2513
        txid, err := chainhash.NewHashFromStr(split[0])
3✔
2514
        if err != nil {
4✔
2515
                return nil, fmt.Errorf("unable to parse hex string: %w", err)
1✔
2516
        }
1✔
2517

2518
        return &lnrpc.ChannelPoint{
2✔
2519
                FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
2✔
2520
                        FundingTxidBytes: txid[:],
2✔
2521
                },
2✔
2522
                OutputIndex: uint32(index),
2✔
2523
        }, nil
2✔
2524
}
2525

2526
// parseTimeLockDelta is expected to get a uint16 type of timeLockDelta. Its
2527
// maximum value is MaxTimeLockDelta.
2528
func parseTimeLockDelta(timeLockDeltaStr string) (uint16, error) {
5✔
2529
        timeLockDeltaUnCheck, err := strconv.ParseUint(timeLockDeltaStr, 10, 64)
5✔
2530
        if err != nil {
7✔
2531
                return 0, fmt.Errorf("failed to parse time_lock_delta: %s "+
2✔
2532
                        "to uint64, err: %v", timeLockDeltaStr, err)
2✔
2533
        }
2✔
2534

2535
        if timeLockDeltaUnCheck > routing.MaxCLTVDelta {
3✔
2536
                return 0, fmt.Errorf("time_lock_delta is too big, "+
×
2537
                        "max value is %d", routing.MaxCLTVDelta)
×
2538
        }
×
2539

2540
        return uint16(timeLockDeltaUnCheck), nil
3✔
2541
}
2542

2543
func updateChannelPolicy(ctx *cli.Context) error {
×
2544
        ctxc := getContext()
×
2545
        client, cleanUp := getClient(ctx)
×
2546
        defer cleanUp()
×
2547

×
2548
        var (
×
2549
                baseFee       int64
×
2550
                feeRate       float64
×
2551
                feeRatePpm    uint64
×
2552
                timeLockDelta uint16
×
2553
                err           error
×
2554
        )
×
2555
        args := ctx.Args()
×
2556

×
2557
        switch {
×
2558
        case ctx.IsSet("base_fee_msat"):
×
2559
                baseFee = ctx.Int64("base_fee_msat")
×
2560
        case args.Present():
×
2561
                baseFee, err = strconv.ParseInt(args.First(), 10, 64)
×
2562
                if err != nil {
×
2563
                        return fmt.Errorf("unable to decode base_fee_msat: %w",
×
2564
                                err)
×
2565
                }
×
2566
                args = args.Tail()
×
2567
        default:
×
2568
                return fmt.Errorf("base_fee_msat argument missing")
×
2569
        }
2570

2571
        switch {
×
2572
        case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
×
2573
                return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
×
2574
        case ctx.IsSet("fee_rate"):
×
2575
                feeRate = ctx.Float64("fee_rate")
×
2576
        case ctx.IsSet("fee_rate_ppm"):
×
2577
                feeRatePpm = ctx.Uint64("fee_rate_ppm")
×
2578
        case args.Present():
×
2579
                feeRate, err = strconv.ParseFloat(args.First(), 64)
×
2580
                if err != nil {
×
2581
                        return fmt.Errorf("unable to decode fee_rate: %w", err)
×
2582
                }
×
2583

2584
                args = args.Tail()
×
2585
        default:
×
2586
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
×
2587
        }
2588

2589
        switch {
×
2590
        case ctx.IsSet("time_lock_delta"):
×
2591
                timeLockDeltaStr := ctx.String("time_lock_delta")
×
2592
                timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
×
2593
                if err != nil {
×
2594
                        return err
×
2595
                }
×
2596
        case args.Present():
×
2597
                timeLockDelta, err = parseTimeLockDelta(args.First())
×
2598
                if err != nil {
×
2599
                        return err
×
2600
                }
×
2601

2602
                args = args.Tail()
×
2603
        default:
×
2604
                return fmt.Errorf("time_lock_delta argument missing")
×
2605
        }
2606

2607
        var (
×
2608
                chanPoint    *lnrpc.ChannelPoint
×
2609
                chanPointStr string
×
2610
        )
×
2611

×
2612
        switch {
×
2613
        case ctx.IsSet("chan_point"):
×
2614
                chanPointStr = ctx.String("chan_point")
×
2615
        case args.Present():
×
2616
                chanPointStr = args.First()
×
2617
        }
2618

2619
        if chanPointStr != "" {
×
2620
                chanPoint, err = parseChanPoint(chanPointStr)
×
2621
                if err != nil {
×
2622
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2623
                }
×
2624
        }
2625

2626
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2627
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2628
                inboundBaseFeeMsat > math.MaxInt32 {
×
2629

×
2630
                return errors.New("inbound_base_fee_msat out of range")
×
2631
        }
×
2632

2633
        inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
×
2634
        if inboundFeeRatePpm < math.MinInt32 ||
×
2635
                inboundFeeRatePpm > math.MaxInt32 {
×
2636

×
2637
                return errors.New("inbound_fee_rate_ppm out of range")
×
2638
        }
×
2639

2640
        // Inbound fees are optional. However, if an update is required,
2641
        // both the base fee and the fee rate must be provided.
2642
        var inboundFee *lnrpc.InboundFee
×
2643
        if ctx.IsSet("inbound_base_fee_msat") !=
×
2644
                ctx.IsSet("inbound_fee_rate_ppm") {
×
2645

×
2646
                return errors.New("both parameters must be provided: " +
×
2647
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
×
2648
        }
×
2649

2650
        if ctx.IsSet("inbound_fee_rate_ppm") {
×
2651
                inboundFee = &lnrpc.InboundFee{
×
2652
                        BaseFeeMsat: int32(inboundBaseFeeMsat),
×
2653
                        FeeRatePpm:  int32(inboundFeeRatePpm),
×
2654
                }
×
2655
        }
×
2656

2657
        createMissingEdge := ctx.Bool("create_missing_edge")
×
2658

×
2659
        req := &lnrpc.PolicyUpdateRequest{
×
2660
                BaseFeeMsat:       baseFee,
×
2661
                TimeLockDelta:     uint32(timeLockDelta),
×
2662
                MaxHtlcMsat:       ctx.Uint64("max_htlc_msat"),
×
2663
                InboundFee:        inboundFee,
×
2664
                CreateMissingEdge: createMissingEdge,
×
2665
        }
×
2666

×
2667
        if ctx.IsSet("min_htlc_msat") {
×
2668
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2669
                req.MinHtlcMsatSpecified = true
×
2670
        }
×
2671

2672
        if chanPoint != nil {
×
2673
                req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
×
2674
                        ChanPoint: chanPoint,
×
2675
                }
×
2676
        } else {
×
2677
                req.Scope = &lnrpc.PolicyUpdateRequest_Global{
×
2678
                        Global: true,
×
2679
                }
×
2680
        }
×
2681

2682
        if feeRate != 0 {
×
2683
                req.FeeRate = feeRate
×
2684
        } else if feeRatePpm != 0 {
×
2685
                req.FeeRatePpm = uint32(feeRatePpm)
×
2686
        }
×
2687

2688
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2689
        if err != nil {
×
2690
                return err
×
2691
        }
×
2692

2693
        // Parse the response into the final json object that will be printed
2694
        // to stdout. At the moment, this filters out the raw txid bytes from
2695
        // each failed update's outpoint and only prints the txid string.
2696
        var listFailedUpdateResp = struct {
×
2697
                FailedUpdates []*FailedUpdate `json:"failed_updates"`
×
2698
        }{
×
2699
                FailedUpdates: make([]*FailedUpdate, 0, len(resp.FailedUpdates)),
×
2700
        }
×
2701
        for _, protoUpdate := range resp.FailedUpdates {
×
2702
                failedUpdate := NewFailedUpdateFromProto(protoUpdate)
×
2703
                listFailedUpdateResp.FailedUpdates = append(
×
2704
                        listFailedUpdateResp.FailedUpdates, failedUpdate)
×
2705
        }
×
2706

2707
        printJSON(listFailedUpdateResp)
×
2708

×
2709
        return nil
×
2710
}
2711

2712
var fishCompletionCommand = cli.Command{
2713
        Name:   "fish-completion",
2714
        Hidden: true,
2715
        Action: func(c *cli.Context) error {
×
2716
                completion, err := c.App.ToFishCompletion()
×
2717
                if err != nil {
×
2718
                        return err
×
2719
                }
×
2720

2721
                // We don't want to suggest files, so we add this
2722
                // first line to the completions.
2723
                _, err = fmt.Printf("complete -c %q -f \n%s", c.App.Name, completion)
×
2724
                return err
×
2725
        },
2726
}
2727

2728
var exportChanBackupCommand = cli.Command{
2729
        Name:     "exportchanbackup",
2730
        Category: "Channels",
2731
        Usage: "Obtain a static channel back up for a selected channels, " +
2732
                "or all known channels.",
2733
        ArgsUsage: "[chan_point] [--all] [--output_file]",
2734
        Description: `
2735
        This command allows a user to export a Static Channel Backup (SCB) for
2736
        a selected channel. SCB's are encrypted backups of a channel's initial
2737
        state that are encrypted with a key derived from the seed of a user. In
2738
        the case of partial or complete data loss, the SCB will allow the user
2739
        to reclaim settled funds in the channel at its final state. The
2740
        exported channel backups can be restored at a later time using the
2741
        restorechanbackup command.
2742

2743
        This command will return one of two types of channel backups depending
2744
        on the set of passed arguments:
2745

2746
           * If a target channel point is specified, then a single channel
2747
             backup containing only the information for that channel will be
2748
             returned.
2749

2750
           * If the --all flag is passed, then a multi-channel backup will be
2751
             returned. A multi backup is a single encrypted blob (displayed in
2752
             hex encoding) that contains several channels in a single cipher
2753
             text.
2754

2755
        Both of the backup types can be restored using the restorechanbackup
2756
        command.
2757
        `,
2758
        Flags: []cli.Flag{
2759
                cli.StringFlag{
2760
                        Name:  "chan_point",
2761
                        Usage: "the target channel to obtain an SCB for",
2762
                },
2763
                cli.BoolFlag{
2764
                        Name: "all",
2765
                        Usage: "if specified, then a multi backup of all " +
2766
                                "active channels will be returned",
2767
                },
2768
                cli.StringFlag{
2769
                        Name: "output_file",
2770
                        Usage: `
2771
                        if specified, then rather than printing a JSON output
2772
                        of the static channel backup, a serialized version of
2773
                        the backup (either Single or Multi) will be written to
2774
                        the target file, this is the same format used by lnd in
2775
                        its channel.backup file `,
2776
                },
2777
        },
2778
        Action: actionDecorator(exportChanBackup),
2779
}
2780

2781
func exportChanBackup(ctx *cli.Context) error {
×
2782
        ctxc := getContext()
×
2783
        client, cleanUp := getClient(ctx)
×
2784
        defer cleanUp()
×
2785

×
2786
        // Show command help if no arguments provided
×
2787
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2788
                cli.ShowCommandHelp(ctx, "exportchanbackup")
×
2789
                return nil
×
2790
        }
×
2791

2792
        var (
×
2793
                err            error
×
2794
                chanPointStr   string
×
2795
                outputFileName string
×
2796
        )
×
2797
        args := ctx.Args()
×
2798

×
2799
        switch {
×
2800
        case ctx.IsSet("chan_point"):
×
2801
                chanPointStr = ctx.String("chan_point")
×
2802

2803
        case args.Present():
×
2804
                chanPointStr = args.First()
×
2805

2806
        case !ctx.IsSet("all"):
×
2807
                return fmt.Errorf("must specify chan_point if --all isn't set")
×
2808
        }
2809

2810
        if ctx.IsSet("output_file") {
×
2811
                outputFileName = ctx.String("output_file")
×
2812
        }
×
2813

2814
        if chanPointStr != "" {
×
2815
                chanPointRPC, err := parseChanPoint(chanPointStr)
×
2816
                if err != nil {
×
2817
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2818
                }
×
2819

2820
                chanBackup, err := client.ExportChannelBackup(
×
2821
                        ctxc, &lnrpc.ExportChannelBackupRequest{
×
2822
                                ChanPoint: chanPointRPC,
×
2823
                        },
×
2824
                )
×
2825
                if err != nil {
×
2826
                        return err
×
2827
                }
×
2828

2829
                txid, err := chainhash.NewHash(
×
2830
                        chanPointRPC.GetFundingTxidBytes(),
×
2831
                )
×
2832
                if err != nil {
×
2833
                        return err
×
2834
                }
×
2835

2836
                chanPoint := wire.OutPoint{
×
2837
                        Hash:  *txid,
×
2838
                        Index: chanPointRPC.OutputIndex,
×
2839
                }
×
2840

×
2841
                if outputFileName != "" {
×
2842
                        return os.WriteFile(
×
2843
                                outputFileName,
×
2844
                                chanBackup.ChanBackup,
×
2845
                                0666,
×
2846
                        )
×
2847
                }
×
2848

2849
                printJSON(struct {
×
2850
                        ChanPoint  string `json:"chan_point"`
×
2851
                        ChanBackup string `json:"chan_backup"`
×
2852
                }{
×
2853
                        ChanPoint:  chanPoint.String(),
×
2854
                        ChanBackup: hex.EncodeToString(chanBackup.ChanBackup),
×
2855
                })
×
2856
                return nil
×
2857
        }
2858

2859
        if !ctx.IsSet("all") {
×
2860
                return fmt.Errorf("if a channel isn't specified, -all must be")
×
2861
        }
×
2862

2863
        chanBackup, err := client.ExportAllChannelBackups(
×
2864
                ctxc, &lnrpc.ChanBackupExportRequest{},
×
2865
        )
×
2866
        if err != nil {
×
2867
                return err
×
2868
        }
×
2869

2870
        if outputFileName != "" {
×
2871
                return os.WriteFile(
×
2872
                        outputFileName,
×
2873
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2874
                        0666,
×
2875
                )
×
2876
        }
×
2877

2878
        // TODO(roasbeef): support for export | restore ?
2879

2880
        var chanPoints []string
×
2881
        for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
×
2882
                txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
×
2883
                if err != nil {
×
2884
                        return err
×
2885
                }
×
2886

2887
                chanPoints = append(chanPoints, wire.OutPoint{
×
2888
                        Hash:  *txid,
×
2889
                        Index: chanPoint.OutputIndex,
×
2890
                }.String())
×
2891
        }
2892

2893
        printRespJSON(chanBackup)
×
2894

×
2895
        return nil
×
2896
}
2897

2898
var verifyChanBackupCommand = cli.Command{
2899
        Name:      "verifychanbackup",
2900
        Category:  "Channels",
2901
        Usage:     "Verify an existing channel backup.",
2902
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file]",
2903
        Description: `
2904
    This command allows a user to verify an existing Single or Multi channel
2905
    backup for integrity. This is useful when a user has a backup, but is
2906
    unsure as to if it's valid or for the target node.
2907

2908
    The command will accept backups in one of four forms:
2909

2910
       * A single channel packed SCB, which can be obtained from
2911
         exportchanbackup. This should be passed in hex encoded format.
2912

2913
       * A packed multi-channel SCB, which couples several individual
2914
         static channel backups in single blob.
2915

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

2919
       * A file path which points to a packed multi-channel backup within a
2920
         file, using the same format that lnd does in its channel.backup
2921
         file.
2922
    `,
2923
        Flags: []cli.Flag{
2924
                cli.StringFlag{
2925
                        Name: "single_backup",
2926
                        Usage: "a hex encoded single channel backup obtained " +
2927
                                "from exportchanbackup",
2928
                },
2929
                cli.StringFlag{
2930
                        Name: "multi_backup",
2931
                        Usage: "a hex encoded multi-channel backup obtained " +
2932
                                "from exportchanbackup",
2933
                },
2934

2935
                cli.StringFlag{
2936
                        Name:      "single_file",
2937
                        Usage:     "the path to a single-channel backup file",
2938
                        TakesFile: true,
2939
                },
2940

2941
                cli.StringFlag{
2942
                        Name:      "multi_file",
2943
                        Usage:     "the path to a multi-channel back up file",
2944
                        TakesFile: true,
2945
                },
2946
        },
2947
        Action: actionDecorator(verifyChanBackup),
2948
}
2949

2950
func verifyChanBackup(ctx *cli.Context) error {
×
2951
        ctxc := getContext()
×
2952
        client, cleanUp := getClient(ctx)
×
2953
        defer cleanUp()
×
2954

×
2955
        // Show command help if no arguments provided
×
2956
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2957
                cli.ShowCommandHelp(ctx, "verifychanbackup")
×
2958
                return nil
×
2959
        }
×
2960

2961
        backups, err := parseChanBackups(ctx)
×
2962
        if err != nil {
×
2963
                return err
×
2964
        }
×
2965

2966
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
2967

×
2968
        if backups.GetChanBackups() != nil {
×
2969
                verifyReq.SingleChanBackups = backups.GetChanBackups()
×
2970
        }
×
2971
        if backups.GetMultiChanBackup() != nil {
×
2972
                verifyReq.MultiChanBackup = &lnrpc.MultiChanBackup{
×
2973
                        MultiChanBackup: backups.GetMultiChanBackup(),
×
2974
                }
×
2975
        }
×
2976

2977
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
2978
        if err != nil {
×
2979
                return err
×
2980
        }
×
2981

2982
        printRespJSON(resp)
×
2983
        return nil
×
2984
}
2985

2986
var restoreChanBackupCommand = cli.Command{
2987
        Name:     "restorechanbackup",
2988
        Category: "Channels",
2989
        Usage: "Restore an existing single or multi-channel static channel " +
2990
                "backup.",
2991
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file=",
2992
        Description: `
2993
        Allows a user to restore a Static Channel Backup (SCB) that was
2994
        obtained either via the exportchanbackup command, or from lnd's
2995
        automatically managed channel.backup file. This command should be used
2996
        if a user is attempting to restore a channel due to data loss on a
2997
        running node restored with the same seed as the node that created the
2998
        channel. If successful, this command will allows the user to recover
2999
        the settled funds stored in the recovered channels.
3000

3001
        The command will accept backups in one of four forms:
3002

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

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

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

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

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

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

3044
// errMissingChanBackup is an error returned when we attempt to parse a channel
3045
// backup from a CLI command, and it is missing.
3046
var errMissingChanBackup = errors.New("missing channel backup")
3047

3048
func parseChanBackups(ctx *cli.Context) (*lnrpc.RestoreChanBackupRequest, error) {
×
3049
        switch {
×
3050
        case ctx.IsSet("single_backup"):
×
3051
                packedBackup, err := hex.DecodeString(
×
3052
                        ctx.String("single_backup"),
×
3053
                )
×
3054
                if err != nil {
×
3055
                        return nil, fmt.Errorf("unable to decode single packed "+
×
3056
                                "backup: %v", err)
×
3057
                }
×
3058

3059
                return &lnrpc.RestoreChanBackupRequest{
×
3060
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3061
                                ChanBackups: &lnrpc.ChannelBackups{
×
3062
                                        ChanBackups: []*lnrpc.ChannelBackup{
×
3063
                                                {
×
3064
                                                        ChanBackup: packedBackup,
×
3065
                                                },
×
3066
                                        },
×
3067
                                },
×
3068
                        },
×
3069
                }, nil
×
3070

3071
        case ctx.IsSet("multi_backup"):
×
3072
                packedMulti, err := hex.DecodeString(
×
3073
                        ctx.String("multi_backup"),
×
3074
                )
×
3075
                if err != nil {
×
3076
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3077
                                "backup: %v", err)
×
3078
                }
×
3079

3080
                return &lnrpc.RestoreChanBackupRequest{
×
3081
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3082
                                MultiChanBackup: packedMulti,
×
3083
                        },
×
3084
                }, nil
×
3085

3086
        case ctx.IsSet("single_file"):
×
3087
                packedSingle, err := os.ReadFile(ctx.String("single_file"))
×
3088
                if err != nil {
×
3089
                        return nil, fmt.Errorf("unable to decode single "+
×
3090
                                "packed backup: %v", err)
×
3091
                }
×
3092

3093
                return &lnrpc.RestoreChanBackupRequest{
×
3094
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3095
                                ChanBackups: &lnrpc.ChannelBackups{
×
3096
                                        ChanBackups: []*lnrpc.ChannelBackup{{
×
3097
                                                ChanBackup: packedSingle,
×
3098
                                        }},
×
3099
                                },
×
3100
                        },
×
3101
                }, nil
×
3102

3103
        case ctx.IsSet("multi_file"):
×
3104
                packedMulti, err := os.ReadFile(ctx.String("multi_file"))
×
3105
                if err != nil {
×
3106
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3107
                                "backup: %v", err)
×
3108
                }
×
3109

3110
                return &lnrpc.RestoreChanBackupRequest{
×
3111
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3112
                                MultiChanBackup: packedMulti,
×
3113
                        },
×
3114
                }, nil
×
3115

3116
        default:
×
3117
                return nil, errMissingChanBackup
×
3118
        }
3119
}
3120

3121
func restoreChanBackup(ctx *cli.Context) error {
×
3122
        ctxc := getContext()
×
3123
        client, cleanUp := getClient(ctx)
×
3124
        defer cleanUp()
×
3125

×
3126
        // Show command help if no arguments provided
×
3127
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3128
                cli.ShowCommandHelp(ctx, "restorechanbackup")
×
3129
                return nil
×
3130
        }
×
3131

3132
        var req lnrpc.RestoreChanBackupRequest
×
3133

×
3134
        backups, err := parseChanBackups(ctx)
×
3135
        if err != nil {
×
3136
                return err
×
3137
        }
×
3138

3139
        req.Backup = backups.Backup
×
3140

×
3141
        resp, err := client.RestoreChannelBackups(ctxc, &req)
×
3142
        if err != nil {
×
3143
                return fmt.Errorf("unable to restore chan backups: %w", err)
×
3144
        }
×
3145

3146
        printRespJSON(resp)
×
3147

×
3148
        return nil
×
3149
}
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