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

lightningnetwork / lnd / 11357847688

16 Oct 2024 02:36AM UTC coverage: 57.864% (-0.9%) from 58.779%
11357847688

Pull #9148

github

ProofOfKeags
lnwire: change DynPropose/DynCommit TLV numbers to align with spec
Pull Request #9148: DynComms [2/n]: lnwire: add authenticated wire messages for Dyn*

350 of 644 new or added lines in 12 files covered. (54.35%)

19831 existing lines in 241 files now uncovered.

99337 of 171674 relevant lines covered (57.86%)

38595.9 hits per line

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

3.55
/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/routing"
25
        "github.com/lightningnetwork/lnd/routing/route"
26
        "github.com/lightningnetwork/lnd/signal"
27
        "github.com/urfave/cli"
28
        "golang.org/x/term"
29
        "google.golang.org/grpc/codes"
30
        "google.golang.org/grpc/status"
31
        "google.golang.org/protobuf/proto"
32
)
33

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

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

39
const defaultRecoveryWindow int32 = 2500
40

41
const (
42
        defaultUtxoMinConf = 1
43
)
44

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

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

55
// replaceCustomData replaces the custom channel data hex string with the
56
// decoded custom channel data in the JSON response.
57
func replaceCustomData(jsonBytes []byte) ([]byte, error) {
6✔
58
        // If there's nothing to replace, return the original JSON.
6✔
59
        if !customDataPattern.Match(jsonBytes) {
8✔
60
                return jsonBytes, nil
2✔
61
        }
2✔
62

63
        replacedBytes := customDataPattern.ReplaceAllFunc(
4✔
64
                jsonBytes, func(match []byte) []byte {
8✔
65
                        encoded := customDataPattern.FindStringSubmatch(
4✔
66
                                string(match),
4✔
67
                        )[1]
4✔
68
                        decoded, err := hex.DecodeString(encoded)
4✔
69
                        if err != nil {
6✔
70
                                return match
2✔
71
                        }
2✔
72

73
                        return []byte("\"custom_channel_data\":" +
2✔
74
                                string(decoded))
2✔
75
                },
76
        )
77

78
        var buf bytes.Buffer
4✔
79
        err := json.Indent(&buf, replacedBytes, "", "    ")
4✔
80
        if err != nil {
5✔
81
                return nil, err
1✔
82
        }
1✔
83

1✔
84
        return buf.Bytes(), nil
1✔
85
}
86

3✔
87
func getContext() context.Context {
88
        shutdownInterceptor, err := signal.Intercept()
89
        if err != nil {
×
90
                _, _ = fmt.Fprintln(os.Stderr, err)
×
91
                os.Exit(1)
×
92
        }
×
UNCOV
93

×
94
        ctxc, cancel := context.WithCancel(context.Background())
×
95
        go func() {
96
                <-shutdownInterceptor.ShutdownChannel()
×
97
                cancel()
×
98
        }()
×
99
        return ctxc
×
UNCOV
100
}
×
UNCOV
101

×
102
func printJSON(resp interface{}) {
103
        b, err := json.Marshal(resp)
104
        if err != nil {
×
105
                fatal(err)
×
106
        }
×
UNCOV
107

×
108
        var out bytes.Buffer
×
109
        _ = json.Indent(&out, b, "", "    ")
110
        _, _ = out.WriteString("\n")
×
111
        _, _ = out.WriteTo(os.Stdout)
×
UNCOV
112
}
×
UNCOV
113

×
114
func printRespJSON(resp proto.Message) {
115
        jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(resp)
116
        if err != nil {
×
117
                fmt.Println("unable to decode response: ", err)
×
118
                return
×
119
        }
×
UNCOV
120

×
121
        jsonBytesReplaced, err := replaceCustomData(jsonBytes)
×
122
        if err != nil {
123
                fmt.Println("unable to replace custom data: ", err)
×
124
                jsonBytesReplaced = jsonBytes
×
125
        }
×
126

127
        fmt.Printf("%s\n", jsonBytesReplaced)
128
}
129

130
// actionDecorator is used to add additional information and error handling
81✔
131
// to command actions.
81✔
UNCOV
132
func actionDecorator(f func(*cli.Context) error) func(*cli.Context) error {
×
UNCOV
133
        return func(c *cli.Context) error {
×
134
                if err := f(c); err != nil {
×
135
                        s, ok := status.FromError(err)
×
136

×
137
                        // If it's a command for the UnlockerService (like
×
138
                        // 'create' or 'unlock') but the wallet is already
×
139
                        // unlocked, then these methods aren't recognized any
×
140
                        // more because this service is shut down after
×
141
                        // successful unlock. That's why the code
×
142
                        // 'Unimplemented' means something different for these
×
143
                        // two commands.
×
144
                        if s.Code() == codes.Unimplemented &&
×
145
                                (c.Command.Name == "create" ||
×
146
                                        c.Command.Name == "unlock" ||
×
147
                                        c.Command.Name == "changepassword" ||
×
148
                                        c.Command.Name == "createwatchonly") {
×
149

×
150
                                return fmt.Errorf("Wallet is already unlocked")
151
                        }
152

153
                        // lnd might be active, but not possible to contact
154
                        // using RPC if the wallet is encrypted. If we get
155
                        // error code Unimplemented, it means that lnd is
156
                        // running, but the RPC server is not active yet (only
UNCOV
157
                        // WalletUnlocker server active) and most likely this
×
UNCOV
158
                        // is because of an encrypted wallet.
×
159
                        if ok && s.Code() == codes.Unimplemented {
×
160
                                return fmt.Errorf("Wallet is encrypted. " +
×
161
                                        "Please unlock using 'lncli unlock', " +
×
162
                                        "or set password using 'lncli create'" +
×
163
                                        " if this is the first time starting " +
×
164
                                        "lnd.")
×
165
                        }
166
                        return err
×
167
                }
168
                return nil
169
        }
170
}
171

172
var newAddressCommand = cli.Command{
173
        Name:      "newaddress",
174
        Category:  "Wallet",
175
        Usage:     "Generates a new address.",
176
        ArgsUsage: "address-type",
177
        Flags: []cli.Flag{
178
                cli.StringFlag{
179
                        Name: "account",
180
                        Usage: "(optional) the name of the account to " +
181
                                "generate a new address for",
182
                },
183
                cli.BoolFlag{
184
                        Name: "unused",
185
                        Usage: "(optional) return the last unused address " +
186
                                "instead of generating a new one",
187
                },
188
        },
189
        Description: `
190
        Generate a wallet new address. Address-types has to be one of:
191
            - p2wkh:  Pay to witness key hash
192
            - np2wkh: Pay to nested witness key hash
193
            - p2tr:   Pay to taproot pubkey`,
194
        Action: actionDecorator(newAddress),
UNCOV
195
}
×
UNCOV
196

×
197
func newAddress(ctx *cli.Context) error {
×
198
        ctxc := getContext()
×
199

×
200
        // Display the command's help message if we do not have the expected
×
201
        // number of arguments/flags.
×
202
        if ctx.NArg() != 1 || ctx.NumFlags() > 1 {
×
203
                return cli.ShowCommandHelp(ctx, "newaddress")
204
        }
205

UNCOV
206
        // Map the string encoded address type, to the concrete typed address
×
UNCOV
207
        // type enum. An unrecognized address type will result in an error.
×
208
        stringAddrType := ctx.Args().First()
×
209
        unused := ctx.Bool("unused")
×
210

×
211
        var addrType lnrpc.AddressType
×
212
        switch stringAddrType { // TODO(roasbeef): make them ints on the cli?
×
213
        case "p2wkh":
×
214
                addrType = lnrpc.AddressType_WITNESS_PUBKEY_HASH
×
215
                if unused {
×
216
                        addrType = lnrpc.AddressType_UNUSED_WITNESS_PUBKEY_HASH
×
217
                }
×
218
        case "np2wkh":
×
219
                addrType = lnrpc.AddressType_NESTED_PUBKEY_HASH
×
220
                if unused {
×
221
                        addrType = lnrpc.AddressType_UNUSED_NESTED_PUBKEY_HASH
×
222
                }
×
223
        case "p2tr":
×
224
                addrType = lnrpc.AddressType_TAPROOT_PUBKEY
×
225
                if unused {
×
226
                        addrType = lnrpc.AddressType_UNUSED_TAPROOT_PUBKEY
×
227
                }
×
228
        default:
×
229
                return fmt.Errorf("invalid address type %v, support address type "+
230
                        "are: p2wkh, np2wkh, and p2tr", stringAddrType)
UNCOV
231
        }
×
UNCOV
232

×
233
        client, cleanUp := getClient(ctx)
×
234
        defer cleanUp()
×
235

×
236
        addr, err := client.NewAddress(ctxc, &lnrpc.NewAddressRequest{
×
237
                Type:    addrType,
×
238
                Account: ctx.String("account"),
×
239
        })
×
240
        if err != nil {
×
241
                return err
242
        }
×
UNCOV
243

×
244
        printRespJSON(addr)
245
        return nil
246
}
247

248
var coinSelectionStrategyFlag = cli.StringFlag{
249
        Name: "coin_selection_strategy",
250
        Usage: "(optional) the strategy to use for selecting " +
251
                "coins. Possible values are 'largest', 'random', or " +
252
                "'global-config'. If either 'largest' or 'random' is " +
253
                "specified, it will override the globally configured " +
254
                "strategy in lnd.conf",
255
        Value: "global-config",
256
}
257

258
var estimateFeeCommand = cli.Command{
259
        Name:      "estimatefee",
260
        Category:  "On-chain",
261
        Usage:     "Get fee estimates for sending bitcoin on-chain to multiple addresses.",
262
        ArgsUsage: "send-json-string [--conf_target=N]",
263
        Description: `
264
        Get fee estimates for sending a transaction paying the specified amount(s) to the passed address(es).
265

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

268
            '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
269
        `,
270
        Flags: []cli.Flag{
271
                cli.Int64Flag{
272
                        Name: "conf_target",
273
                        Usage: "(optional) the number of blocks that the " +
274
                                "transaction *should* confirm in",
275
                },
276
                coinSelectionStrategyFlag,
277
        },
278
        Action: actionDecorator(estimateFees),
UNCOV
279
}
×
UNCOV
280

×
281
func estimateFees(ctx *cli.Context) error {
×
282
        ctxc := getContext()
×
283
        var amountToAddr map[string]int64
×
284

×
285
        jsonMap := ctx.Args().First()
×
286
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
287
                return err
288
        }
×
UNCOV
289

×
290
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
291
        if err != nil {
×
292
                return err
293
        }
×
UNCOV
294

×
295
        client, cleanUp := getClient(ctx)
×
296
        defer cleanUp()
×
297

×
298
        resp, err := client.EstimateFee(ctxc, &lnrpc.EstimateFeeRequest{
×
299
                AddrToAmount:          amountToAddr,
×
300
                TargetConf:            int32(ctx.Int64("conf_target")),
×
301
                CoinSelectionStrategy: coinSelectionStrategy,
×
302
        })
×
303
        if err != nil {
×
304
                return err
305
        }
×
UNCOV
306

×
307
        printRespJSON(resp)
308
        return nil
309
}
310

311
var txLabelFlag = cli.StringFlag{
312
        Name:  "label",
313
        Usage: "(optional) a label for the transaction",
314
}
315

316
var sendCoinsCommand = cli.Command{
317
        Name:      "sendcoins",
318
        Category:  "On-chain",
319
        Usage:     "Send bitcoin on-chain to an address.",
320
        ArgsUsage: "addr amt",
321
        Description: `
322
        Send amt coins in satoshis to the base58 or bech32 encoded bitcoin address addr.
323

324
        Fees used when sending the transaction can be specified via the --conf_target, or
325
        --sat_per_vbyte optional flags.
326

327
        Positional arguments and flags can be used interchangeably but not at the same time!
328
        `,
329
        Flags: []cli.Flag{
330
                cli.StringFlag{
331
                        Name: "addr",
332
                        Usage: "the base58 or bech32 encoded bitcoin address to send coins " +
333
                                "to on-chain",
334
                },
335
                cli.BoolFlag{
336
                        Name: "sweepall",
337
                        Usage: "if set, then the amount field should be " +
338
                                "unset. This indicates that the wallet will " +
339
                                "attempt to sweep all outputs within the " +
340
                                "wallet or all funds in select utxos (when " +
341
                                "supplied) to the target address",
342
                },
343
                cli.Int64Flag{
344
                        Name:  "amt",
345
                        Usage: "the number of bitcoin denominated in satoshis to send",
346
                },
347
                cli.Int64Flag{
348
                        Name: "conf_target",
349
                        Usage: "(optional) the number of blocks that the " +
350
                                "transaction *should* confirm in, will be " +
351
                                "used for fee estimation",
352
                },
353
                cli.Int64Flag{
354
                        Name:   "sat_per_byte",
355
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
356
                        Hidden: true,
357
                },
358
                cli.Int64Flag{
359
                        Name: "sat_per_vbyte",
360
                        Usage: "(optional) a manual fee expressed in " +
361
                                "sat/vbyte that should be used when crafting " +
362
                                "the transaction",
363
                },
364
                cli.Uint64Flag{
365
                        Name: "min_confs",
366
                        Usage: "(optional) the minimum number of confirmations " +
367
                                "each one of your outputs used for the transaction " +
368
                                "must satisfy",
369
                        Value: defaultUtxoMinConf,
370
                },
371
                cli.BoolFlag{
372
                        Name: "force, f",
373
                        Usage: "if set, the transaction will be broadcast " +
374
                                "without asking for confirmation; this is " +
375
                                "set to true by default if stdout is not a " +
376
                                "terminal avoid breaking existing shell " +
377
                                "scripts",
378
                },
379
                coinSelectionStrategyFlag,
380
                cli.StringSliceFlag{
381
                        Name: "utxo",
382
                        Usage: "a utxo specified as outpoint(tx:idx) which " +
383
                                "will be used as input for the transaction. " +
384
                                "This flag can be repeatedly used to specify " +
385
                                "multiple utxos as inputs. The selected " +
386
                                "utxos can either be entirely spent by " +
387
                                "specifying the sweepall flag or a specified " +
388
                                "amount can be spent in the utxos through " +
389
                                "the amt flag",
390
                },
391
                txLabelFlag,
392
        },
393
        Action: actionDecorator(sendCoins),
UNCOV
394
}
×
UNCOV
395

×
396
func sendCoins(ctx *cli.Context) error {
×
397
        var (
×
398
                addr      string
×
399
                amt       int64
×
400
                err       error
×
401
                outpoints []*lnrpc.OutPoint
×
402
        )
×
403
        ctxc := getContext()
×
404
        args := ctx.Args()
×
405

×
406
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
407
                cli.ShowCommandHelp(ctx, "sendcoins")
×
408
                return nil
409
        }
410

UNCOV
411
        // Check that only the field sat_per_vbyte or the deprecated field
×
UNCOV
412
        // sat_per_byte is used.
×
413
        feeRateFlag, err := checkNotBothSet(
×
414
                ctx, "sat_per_vbyte", "sat_per_byte",
×
415
        )
×
416
        if err != nil {
×
417
                return err
418
        }
UNCOV
419

×
UNCOV
420
        // Only fee rate flag or conf_target should be set, not both.
×
421
        if _, err := checkNotBothSet(
×
422
                ctx, feeRateFlag, "conf_target",
×
423
        ); err != nil {
×
424
                return err
425
        }
×
UNCOV
426

×
427
        switch {
×
428
        case ctx.IsSet("addr"):
×
429
                addr = ctx.String("addr")
×
430
        case args.Present():
×
431
                addr = args.First()
×
432
                args = args.Tail()
×
433
        default:
434
                return fmt.Errorf("Address argument missing")
UNCOV
435
        }
×
UNCOV
436

×
437
        switch {
×
438
        case ctx.IsSet("amt"):
×
439
                amt = ctx.Int64("amt")
×
440
        case args.Present():
×
441
                amt, err = strconv.ParseInt(args.First(), 10, 64)
×
442
        case !ctx.Bool("sweepall"):
443
                return fmt.Errorf("Amount argument missing")
×
UNCOV
444
        }
×
445
        if err != nil {
×
446
                return fmt.Errorf("unable to decode amount: %w", err)
447
        }
×
UNCOV
448

×
449
        if amt != 0 && ctx.Bool("sweepall") {
×
450
                return fmt.Errorf("amount cannot be set if attempting to " +
×
451
                        "sweep all coins out of the wallet")
452
        }
×
UNCOV
453

×
454
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
455
        if err != nil {
×
456
                return err
457
        }
×
UNCOV
458

×
459
        client, cleanUp := getClient(ctx)
×
460
        defer cleanUp()
×
461
        minConfs := int32(ctx.Uint64("min_confs"))
×
462

×
463
        // In case that the user has specified the sweepall flag, we'll
×
464
        // calculate the amount to send based on the current wallet balance.
×
465
        displayAmt := amt
×
466
        if ctx.Bool("sweepall") {
×
467
                balanceResponse, err := client.WalletBalance(
×
468
                        ctxc, &lnrpc.WalletBalanceRequest{
×
469
                                MinConfs: minConfs,
×
470
                        },
×
471
                )
×
472
                if err != nil {
×
473
                        return fmt.Errorf("unable to retrieve wallet balance:"+
×
474
                                " %w", err)
×
475
                }
476
                displayAmt = balanceResponse.GetConfirmedBalance()
UNCOV
477
        }
×
UNCOV
478

×
479
        if ctx.IsSet("utxo") {
×
480
                utxos := ctx.StringSlice("utxo")
×
481

×
482
                outpoints, err = UtxosToOutpoints(utxos)
×
483
                if err != nil {
×
484
                        return fmt.Errorf("unable to decode utxos: %w", err)
485
                }
486
        }
487

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

×
495
                confirm := promptForConfirmation("Confirm payment (yes/no): ")
×
496
                if !confirm {
×
497
                        return nil
498
                }
UNCOV
499
        }
×
UNCOV
500

×
501
        req := &lnrpc.SendCoinsRequest{
×
502
                Addr:                  addr,
×
503
                Amount:                amt,
×
504
                TargetConf:            int32(ctx.Int64("conf_target")),
×
505
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
506
                SendAll:               ctx.Bool("sweepall"),
×
507
                Label:                 ctx.String(txLabelFlag.Name),
×
508
                MinConfs:              minConfs,
×
509
                SpendUnconfirmed:      minConfs == 0,
×
510
                CoinSelectionStrategy: coinSelectionStrategy,
×
511
                Outpoints:             outpoints,
×
512
        }
×
513
        txid, err := client.SendCoins(ctxc, req)
×
514
        if err != nil {
×
515
                return err
516
        }
×
UNCOV
517

×
518
        printRespJSON(txid)
519
        return nil
520
}
521

522
var listUnspentCommand = cli.Command{
523
        Name:      "listunspent",
524
        Category:  "On-chain",
525
        Usage:     "List utxos available for spending.",
526
        ArgsUsage: "[min-confs [max-confs]] [--unconfirmed_only]",
527
        Description: `
528
        For each spendable utxo currently in the wallet, with at least min_confs
529
        confirmations, and at most max_confs confirmations, lists the txid,
530
        index, amount, address, address type, scriptPubkey and number of
531
        confirmations.  Use --min_confs=0 to include unconfirmed coins. To list
532
        all coins with at least min_confs confirmations, omit the second
533
        argument or flag '--max_confs'. To list all confirmed and unconfirmed
534
        coins, no arguments are required. To see only unconfirmed coins, use
535
        '--unconfirmed_only' with '--min_confs' and '--max_confs' set to zero or
536
        not present.
537
        `,
538
        Flags: []cli.Flag{
539
                cli.Int64Flag{
540
                        Name:  "min_confs",
541
                        Usage: "the minimum number of confirmations for a utxo",
542
                },
543
                cli.Int64Flag{
544
                        Name:  "max_confs",
545
                        Usage: "the maximum number of confirmations for a utxo",
546
                },
547
                cli.BoolFlag{
548
                        Name: "unconfirmed_only",
549
                        Usage: "when min_confs and max_confs are zero, " +
550
                                "setting false implicitly overrides max_confs " +
551
                                "to be MaxInt32, otherwise max_confs remains " +
552
                                "zero. An error is returned if the value is " +
553
                                "true and both min_confs and max_confs are " +
554
                                "non-zero. (default: false)",
555
                },
556
        },
557
        Action: actionDecorator(listUnspent),
UNCOV
558
}
×
UNCOV
559

×
560
func listUnspent(ctx *cli.Context) error {
×
561
        var (
×
562
                minConfirms int64
×
563
                maxConfirms int64
×
564
                err         error
×
565
        )
×
566
        ctxc := getContext()
×
567
        args := ctx.Args()
×
568

×
569
        if ctx.IsSet("max_confs") && !ctx.IsSet("min_confs") {
×
570
                return fmt.Errorf("max_confs cannot be set without " +
×
571
                        "min_confs being set")
572
        }
×
UNCOV
573

×
574
        switch {
×
575
        case ctx.IsSet("min_confs"):
×
576
                minConfirms = ctx.Int64("min_confs")
×
577
        case args.Present():
×
578
                minConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
579
                if err != nil {
×
580
                        cli.ShowCommandHelp(ctx, "listunspent")
×
581
                        return nil
×
582
                }
583
                args = args.Tail()
UNCOV
584
        }
×
UNCOV
585

×
586
        switch {
×
587
        case ctx.IsSet("max_confs"):
×
588
                maxConfirms = ctx.Int64("max_confs")
×
589
        case args.Present():
×
590
                maxConfirms, err = strconv.ParseInt(args.First(), 10, 64)
×
591
                if err != nil {
×
592
                        cli.ShowCommandHelp(ctx, "listunspent")
×
593
                        return nil
×
594
                }
595
                args = args.Tail()
UNCOV
596
        }
×
UNCOV
597

×
598
        unconfirmedOnly := ctx.Bool("unconfirmed_only")
×
599

×
600
        // Force minConfirms and maxConfirms to be zero if unconfirmedOnly is
×
601
        // true.
×
602
        if unconfirmedOnly && (minConfirms != 0 || maxConfirms != 0) {
×
603
                cli.ShowCommandHelp(ctx, "listunspent")
×
604
                return nil
605
        }
606

UNCOV
607
        // When unconfirmedOnly is inactive, we will override maxConfirms to be
×
UNCOV
608
        // a MaxInt32 to return all confirmed and unconfirmed utxos.
×
609
        if maxConfirms == 0 && !unconfirmedOnly {
×
610
                maxConfirms = math.MaxInt32
611
        }
×
UNCOV
612

×
613
        client, cleanUp := getClient(ctx)
×
614
        defer cleanUp()
×
615

×
616
        req := &lnrpc.ListUnspentRequest{
×
617
                MinConfs: int32(minConfirms),
×
618
                MaxConfs: int32(maxConfirms),
×
619
        }
×
620
        resp, err := client.ListUnspent(ctxc, req)
×
621
        if err != nil {
×
622
                return err
623
        }
624

625
        // Parse the response into the final json object that will be printed
UNCOV
626
        // to stdout. At the moment, this filters out the raw txid bytes from
×
UNCOV
627
        // each utxo's outpoint and only prints the txid string.
×
628
        var listUnspentResp = struct {
×
629
                Utxos []*Utxo `json:"utxos"`
×
630
        }{
×
631
                Utxos: make([]*Utxo, 0, len(resp.Utxos)),
×
632
        }
×
633
        for _, protoUtxo := range resp.Utxos {
×
634
                utxo := NewUtxoFromProto(protoUtxo)
×
635
                listUnspentResp.Utxos = append(listUnspentResp.Utxos, utxo)
636
        }
×
UNCOV
637

×
638
        printJSON(listUnspentResp)
×
639

640
        return nil
641
}
642

643
var sendManyCommand = cli.Command{
644
        Name:      "sendmany",
645
        Category:  "On-chain",
646
        Usage:     "Send bitcoin on-chain to multiple addresses.",
647
        ArgsUsage: "send-json-string [--conf_target=N] [--sat_per_vbyte=P]",
648
        Description: `
649
        Create and broadcast a transaction paying the specified amount(s) to the passed address(es).
650

651
        The send-json-string' param decodes addresses and the amount to send
652
        respectively in the following format:
653

654
            '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
655
        `,
656
        Flags: []cli.Flag{
657
                cli.Int64Flag{
658
                        Name: "conf_target",
659
                        Usage: "(optional) the number of blocks that the transaction *should* " +
660
                                "confirm in, will be used for fee estimation",
661
                },
662
                cli.Int64Flag{
663
                        Name:   "sat_per_byte",
664
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
665
                        Hidden: true,
666
                },
667
                cli.Int64Flag{
668
                        Name: "sat_per_vbyte",
669
                        Usage: "(optional) a manual fee expressed in " +
670
                                "sat/vbyte that should be used when crafting " +
671
                                "the transaction",
672
                },
673
                cli.Uint64Flag{
674
                        Name: "min_confs",
675
                        Usage: "(optional) the minimum number of confirmations " +
676
                                "each one of your outputs used for the transaction " +
677
                                "must satisfy",
678
                        Value: defaultUtxoMinConf,
679
                },
680
                coinSelectionStrategyFlag,
681
                txLabelFlag,
682
        },
683
        Action: actionDecorator(sendMany),
UNCOV
684
}
×
UNCOV
685

×
686
func sendMany(ctx *cli.Context) error {
×
687
        ctxc := getContext()
×
688
        var amountToAddr map[string]int64
×
689

×
690
        jsonMap := ctx.Args().First()
×
691
        if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
×
692
                return err
693
        }
694

UNCOV
695
        // Check that only the field sat_per_vbyte or the deprecated field
×
UNCOV
696
        // sat_per_byte is used.
×
697
        feeRateFlag, err := checkNotBothSet(
×
698
                ctx, "sat_per_vbyte", "sat_per_byte",
×
699
        )
×
700
        if err != nil {
×
701
                return err
702
        }
UNCOV
703

×
UNCOV
704
        // Only fee rate flag or conf_target should be set, not both.
×
705
        if _, err := checkNotBothSet(
×
706
                ctx, feeRateFlag, "conf_target",
×
707
        ); err != nil {
×
708
                return err
709
        }
×
UNCOV
710

×
711
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
712
        if err != nil {
×
713
                return err
714
        }
×
UNCOV
715

×
716
        client, cleanUp := getClient(ctx)
×
717
        defer cleanUp()
×
718

×
719
        minConfs := int32(ctx.Uint64("min_confs"))
×
720
        txid, err := client.SendMany(ctxc, &lnrpc.SendManyRequest{
×
721
                AddrToAmount:          amountToAddr,
×
722
                TargetConf:            int32(ctx.Int64("conf_target")),
×
723
                SatPerVbyte:           ctx.Uint64(feeRateFlag),
×
724
                Label:                 ctx.String(txLabelFlag.Name),
×
725
                MinConfs:              minConfs,
×
726
                SpendUnconfirmed:      minConfs == 0,
×
727
                CoinSelectionStrategy: coinSelectionStrategy,
×
728
        })
×
729
        if err != nil {
×
730
                return err
731
        }
×
UNCOV
732

×
733
        printRespJSON(txid)
734
        return nil
735
}
736

737
var connectCommand = cli.Command{
738
        Name:      "connect",
739
        Category:  "Peers",
740
        Usage:     "Connect to a remote lightning peer.",
741
        ArgsUsage: "<pubkey>@host",
742
        Description: `
743
        Connect to a peer using its <pubkey> and host.
744

745
        A custom timeout on the connection is supported. For instance, to timeout
746
        the connection request in 30 seconds, use the following:
747

748
        lncli connect <pubkey>@host --timeout 30s
749
        `,
750
        Flags: []cli.Flag{
751
                cli.BoolFlag{
752
                        Name: "perm",
753
                        Usage: "If set, the daemon will attempt to persistently " +
754
                                "connect to the target peer.\n" +
755
                                "           If not, the call will be synchronous.",
756
                },
757
                cli.DurationFlag{
758
                        Name: "timeout",
759
                        Usage: "The connection timeout value for current request. " +
760
                                "Valid uints are {ms, s, m, h}.\n" +
761
                                "If not set, the global connection " +
762
                                "timeout value (default to 120s) is used.",
763
                },
764
        },
765
        Action: actionDecorator(connectPeer),
UNCOV
766
}
×
UNCOV
767

×
768
func connectPeer(ctx *cli.Context) error {
×
769
        ctxc := getContext()
×
770
        client, cleanUp := getClient(ctx)
×
771
        defer cleanUp()
×
772

×
773
        targetAddress := ctx.Args().First()
×
774
        splitAddr := strings.Split(targetAddress, "@")
×
775
        if len(splitAddr) != 2 {
×
776
                return fmt.Errorf("target address expected in format: " +
×
777
                        "pubkey@host:port")
778
        }
×
UNCOV
779

×
780
        addr := &lnrpc.LightningAddress{
×
781
                Pubkey: splitAddr[0],
×
782
                Host:   splitAddr[1],
×
783
        }
×
784
        req := &lnrpc.ConnectPeerRequest{
×
785
                Addr:    addr,
×
786
                Perm:    ctx.Bool("perm"),
×
787
                Timeout: uint64(ctx.Duration("timeout").Seconds()),
×
788
        }
×
789

×
790
        lnid, err := client.ConnectPeer(ctxc, req)
×
791
        if err != nil {
×
792
                return err
793
        }
×
UNCOV
794

×
795
        printRespJSON(lnid)
796
        return nil
797
}
798

799
var disconnectCommand = cli.Command{
800
        Name:     "disconnect",
801
        Category: "Peers",
802
        Usage: "Disconnect a remote lightning peer identified by " +
803
                "public key.",
804
        ArgsUsage: "<pubkey>",
805
        Flags: []cli.Flag{
806
                cli.StringFlag{
807
                        Name: "node_key",
808
                        Usage: "The hex-encoded compressed public key of the peer " +
809
                                "to disconnect from",
810
                },
811
        },
812
        Action: actionDecorator(disconnectPeer),
UNCOV
813
}
×
UNCOV
814

×
815
func disconnectPeer(ctx *cli.Context) error {
×
816
        ctxc := getContext()
×
817
        client, cleanUp := getClient(ctx)
×
818
        defer cleanUp()
×
819

×
820
        var pubKey string
×
821
        switch {
×
822
        case ctx.IsSet("node_key"):
×
823
                pubKey = ctx.String("node_key")
×
824
        case ctx.Args().Present():
×
825
                pubKey = ctx.Args().First()
×
826
        default:
827
                return fmt.Errorf("must specify target public key")
UNCOV
828
        }
×
UNCOV
829

×
830
        req := &lnrpc.DisconnectPeerRequest{
×
831
                PubKey: pubKey,
×
832
        }
×
833

×
834
        lnid, err := client.DisconnectPeer(ctxc, req)
×
835
        if err != nil {
×
836
                return err
837
        }
×
UNCOV
838

×
839
        printRespJSON(lnid)
840
        return nil
841
}
842

843
// TODO(roasbeef): also allow short relative channel ID.
844

845
var closeChannelCommand = cli.Command{
846
        Name:     "closechannel",
847
        Category: "Channels",
848
        Usage:    "Close an existing channel.",
849
        Description: `
850
        Close an existing channel. The channel can be closed either cooperatively,
851
        or unilaterally (--force).
852

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

857
        In the case of a cooperative closure, one can manually set the fee to
858
        be used for the closing transaction via either the --conf_target or
859
        --sat_per_vbyte arguments. This will be the starting value used during
860
        fee negotiation. This is optional. The parameter --max_fee_rate in
861
        comparison is the end boundary of the fee negotiation, if not specified
862
        it's always x3 of the starting value. Increasing this value increases
863
        the chance of a successful negotiation.
864

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

870
        To view which funding_txids/output_indexes can be used for a channel close,
871
        see the channel_point values within the listchannels command output.
872
        The format for a channel_point is 'funding_txid:output_index'.`,
873
        ArgsUsage: "funding_txid output_index",
874
        Flags: []cli.Flag{
875
                cli.StringFlag{
876
                        Name:  "funding_txid",
877
                        Usage: "the txid of the channel's funding transaction",
878
                },
879
                cli.IntFlag{
880
                        Name: "output_index",
881
                        Usage: "the output index for the funding output of the funding " +
882
                                "transaction",
883
                },
884
                cli.StringFlag{
885
                        Name: "chan_point",
886
                        Usage: "(optional) the channel point. If set, " +
887
                                "funding_txid and output_index flags and " +
888
                                "positional arguments will be ignored",
889
                },
890
                cli.BoolFlag{
891
                        Name:  "force",
892
                        Usage: "attempt an uncooperative closure",
893
                },
894
                cli.BoolFlag{
895
                        Name:  "block",
896
                        Usage: "block until the channel is closed",
897
                },
898
                cli.Int64Flag{
899
                        Name: "conf_target",
900
                        Usage: "(optional) the number of blocks that the " +
901
                                "transaction *should* confirm in, will be " +
902
                                "used for fee estimation. If not set, " +
903
                                "then the conf-target value set in the main " +
904
                                "lnd config will be used.",
905
                },
906
                cli.Int64Flag{
907
                        Name:   "sat_per_byte",
908
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
909
                        Hidden: true,
910
                },
911
                cli.Int64Flag{
912
                        Name: "sat_per_vbyte",
913
                        Usage: "(optional) a manual fee expressed in " +
914
                                "sat/vbyte that should be used when crafting " +
915
                                "the transaction; default is a conf-target " +
916
                                "of 6 blocks",
917
                },
918
                cli.StringFlag{
919
                        Name: "delivery_addr",
920
                        Usage: "(optional) an address to deliver funds " +
921
                                "upon cooperative channel closing, may only " +
922
                                "be used if an upfront shutdown address is not " +
923
                                "already set",
924
                },
925
                cli.Uint64Flag{
926
                        Name: "max_fee_rate",
927
                        Usage: "(optional) maximum fee rate in sat/vbyte " +
928
                                "accepted during the negotiation (default is " +
929
                                "x3 of the desired fee rate); increases the " +
930
                                "success pobability of the negotiation if " +
931
                                "set higher",
932
                },
933
        },
934
        Action: actionDecorator(closeChannel),
UNCOV
935
}
×
UNCOV
936

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

×
942
        // Show command help if no arguments and flags were provided.
×
943
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
944
                cli.ShowCommandHelp(ctx, "closechannel")
×
945
                return nil
946
        }
947

UNCOV
948
        // Check that only the field sat_per_vbyte or the deprecated field
×
UNCOV
949
        // sat_per_byte is used.
×
950
        feeRateFlag, err := checkNotBothSet(
×
951
                ctx, "sat_per_vbyte", "sat_per_byte",
×
952
        )
×
953
        if err != nil {
×
954
                return err
955
        }
×
UNCOV
956

×
957
        channelPoint, err := parseChannelPoint(ctx)
×
958
        if err != nil {
×
959
                return err
960
        }
UNCOV
961

×
UNCOV
962
        // TODO(roasbeef): implement time deadline within server
×
963
        req := &lnrpc.CloseChannelRequest{
×
964
                ChannelPoint:    channelPoint,
×
965
                Force:           ctx.Bool("force"),
×
966
                TargetConf:      int32(ctx.Int64("conf_target")),
×
967
                SatPerVbyte:     ctx.Uint64(feeRateFlag),
×
968
                DeliveryAddress: ctx.String("delivery_addr"),
×
969
                MaxFeePerVbyte:  ctx.Uint64("max_fee_rate"),
×
970
        }
×
971

×
972
        // After parsing the request, we'll spin up a goroutine that will
×
973
        // retrieve the closing transaction ID when attempting to close the
×
974
        // channel. We do this to because `executeChannelClose` can block, so we
×
975
        // would like to present the closing transaction ID to the user as soon
×
976
        // as it is broadcasted.
×
977
        var wg sync.WaitGroup
×
978
        txidChan := make(chan string, 1)
×
979

×
980
        wg.Add(1)
×
981
        go func() {
×
982
                defer wg.Done()
×
983

×
984
                printJSON(struct {
×
985
                        ClosingTxid string `json:"closing_txid"`
×
986
                }{
×
987
                        ClosingTxid: <-txidChan,
×
988
                })
989
        }()
×
UNCOV
990

×
991
        err = executeChannelClose(ctxc, client, req, txidChan, ctx.Bool("block"))
×
992
        if err != nil {
×
993
                return err
994
        }
995

996
        // In the case that the user did not provide the `block` flag, then we
UNCOV
997
        // need to wait for the goroutine to be done to prevent it from being
×
UNCOV
998
        // destroyed when exiting before printing the closing transaction ID.
×
999
        wg.Wait()
×
1000

1001
        return nil
1002
}
1003

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

×
1011
        stream, err := client.CloseChannel(ctxc, req)
×
1012
        if err != nil {
×
1013
                return err
1014
        }
×
UNCOV
1015

×
1016
        for {
×
1017
                resp, err := stream.Recv()
×
1018
                if err == io.EOF {
×
1019
                        return nil
×
1020
                } else if err != nil {
×
1021
                        return err
1022
                }
×
UNCOV
1023

×
1024
                switch update := resp.Update.(type) {
×
1025
                case *lnrpc.CloseStatusUpdate_CloseInstant:
×
1026
                        if req.NoWait {
×
1027
                                return nil
×
1028
                        }
×
1029
                case *lnrpc.CloseStatusUpdate_ClosePending:
×
1030
                        closingHash := update.ClosePending.Txid
×
1031
                        txid, err := chainhash.NewHash(closingHash)
×
1032
                        if err != nil {
×
1033
                                return err
1034
                        }
×
UNCOV
1035

×
1036
                        txidChan <- txid.String()
×
1037

×
1038
                        if !block {
×
1039
                                return nil
×
1040
                        }
×
1041
                case *lnrpc.CloseStatusUpdate_ChanClose:
1042
                        return nil
1043
                }
1044
        }
1045
}
1046

1047
var closeAllChannelsCommand = cli.Command{
1048
        Name:     "closeallchannels",
1049
        Category: "Channels",
1050
        Usage:    "Close all existing channels.",
1051
        Description: `
1052
        Close all existing channels.
1053

1054
        Channels will be closed either cooperatively or unilaterally, depending
1055
        on whether the channel is active or not. If the channel is inactive, any
1056
        settled funds within it will be time locked for a few blocks before they
1057
        can be spent.
1058

1059
        One can request to close inactive channels only by using the
1060
        --inactive_only flag.
1061

1062
        By default, one is prompted for confirmation every time an inactive
1063
        channel is requested to be closed. To avoid this, one can set the
1064
        --force flag, which will only prompt for confirmation once for all
1065
        inactive channels and proceed to close them.
1066

1067
        In the case of cooperative closures, one can manually set the fee to
1068
        be used for the closing transactions via either the --conf_target or
1069
        --sat_per_vbyte arguments. This will be the starting value used during
1070
        fee negotiation. This is optional.`,
1071
        Flags: []cli.Flag{
1072
                cli.BoolFlag{
1073
                        Name:  "inactive_only",
1074
                        Usage: "close inactive channels only",
1075
                },
1076
                cli.BoolFlag{
1077
                        Name: "force",
1078
                        Usage: "ask for confirmation once before attempting " +
1079
                                "to close existing channels",
1080
                },
1081
                cli.Int64Flag{
1082
                        Name: "conf_target",
1083
                        Usage: "(optional) the number of blocks that the " +
1084
                                "closing transactions *should* confirm in, will be " +
1085
                                "used for fee estimation",
1086
                },
1087
                cli.Int64Flag{
1088
                        Name:   "sat_per_byte",
1089
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
1090
                        Hidden: true,
1091
                },
1092
                cli.Int64Flag{
1093
                        Name: "sat_per_vbyte",
1094
                        Usage: "(optional) a manual fee expressed in " +
1095
                                "sat/vbyte that should be used when crafting " +
1096
                                "the closing transactions",
1097
                },
1098
                cli.BoolFlag{
1099
                        Name: "s, skip_confirmation",
1100
                        Usage: "Skip the confirmation prompt and close all " +
1101
                                "channels immediately",
1102
                },
1103
        },
1104
        Action: actionDecorator(closeAllChannels),
UNCOV
1105
}
×
UNCOV
1106

×
1107
func closeAllChannels(ctx *cli.Context) error {
×
1108
        ctxc := getContext()
×
1109
        client, cleanUp := getClient(ctx)
×
1110
        defer cleanUp()
×
1111

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

×
1121
        prompt := "Do you really want to close ALL channels? (yes/no): "
×
1122
        if !ctx.Bool("skip_confirmation") && !promptForConfirmation(prompt) {
×
1123
                return errors.New("action aborted by user")
1124
        }
×
UNCOV
1125

×
1126
        listReq := &lnrpc.ListChannelsRequest{}
×
1127
        openChannels, err := client.ListChannels(ctxc, listReq)
×
1128
        if err != nil {
×
1129
                return fmt.Errorf("unable to fetch open channels: %w", err)
1130
        }
×
UNCOV
1131

×
1132
        if len(openChannels.Channels) == 0 {
×
1133
                return errors.New("no open channels to close")
1134
        }
×
UNCOV
1135

×
1136
        var channelsToClose []*lnrpc.Channel
×
1137

×
1138
        switch {
×
1139
        case ctx.Bool("force") && ctx.Bool("inactive_only"):
×
1140
                msg := "Unilaterally close all inactive channels? The funds " +
×
1141
                        "within these channels will be locked for some blocks " +
×
1142
                        "(CSV delay) before they can be spent. (yes/no): "
×
1143

×
1144
                confirmed := promptForConfirmation(msg)
×
1145

×
1146
                // We can safely exit if the user did not confirm.
×
1147
                if !confirmed {
×
1148
                        return nil
1149
                }
1150

UNCOV
1151
                // Go through the list of open channels and only add inactive
×
UNCOV
1152
                // channels to the closing list.
×
1153
                for _, channel := range openChannels.Channels {
×
1154
                        if !channel.GetActive() {
×
1155
                                channelsToClose = append(
×
1156
                                        channelsToClose, channel,
×
1157
                                )
1158
                        }
×
UNCOV
1159
                }
×
1160
        case ctx.Bool("force"):
×
1161
                msg := "Close all active and inactive channels? Inactive " +
×
1162
                        "channels will be closed unilaterally, so funds " +
×
1163
                        "within them will be locked for a few blocks (CSV " +
×
1164
                        "delay) before they can be spent. (yes/no): "
×
1165

×
1166
                confirmed := promptForConfirmation(msg)
×
1167

×
1168
                // We can safely exit if the user did not confirm.
×
1169
                if !confirmed {
×
1170
                        return nil
1171
                }
×
UNCOV
1172

×
1173
                channelsToClose = openChannels.Channels
×
1174
        default:
×
1175
                // Go through the list of open channels and determine which
×
1176
                // should be added to the closing list.
×
1177
                for _, channel := range openChannels.Channels {
×
1178
                        // If the channel is inactive, we'll attempt to
×
1179
                        // unilaterally close the channel, so we should prompt
×
1180
                        // the user for confirmation beforehand.
×
1181
                        if !channel.GetActive() {
×
1182
                                msg := fmt.Sprintf("Unilaterally close channel "+
×
1183
                                        "with node %s and channel point %s? "+
×
1184
                                        "The closing transaction will need %d "+
×
1185
                                        "confirmations before the funds can be "+
×
1186
                                        "spent. (yes/no): ", channel.RemotePubkey,
×
1187
                                        channel.ChannelPoint, channel.LocalConstraints.CsvDelay)
×
1188

×
1189
                                confirmed := promptForConfirmation(msg)
×
1190

×
1191
                                if confirmed {
×
1192
                                        channelsToClose = append(
×
1193
                                                channelsToClose, channel,
×
1194
                                        )
×
1195
                                }
×
1196
                        } else if !ctx.Bool("inactive_only") {
×
1197
                                // Otherwise, we'll only add active channels if
×
1198
                                // we were not requested to close inactive
×
1199
                                // channels only.
×
1200
                                channelsToClose = append(
×
1201
                                        channelsToClose, channel,
×
1202
                                )
1203
                        }
1204
                }
1205
        }
1206

1207
        // result defines the result of closing a channel. The closing
UNCOV
1208
        // transaction ID is populated if a channel is successfully closed.
×
UNCOV
1209
        // Otherwise, the error that prevented closing the channel is populated.
×
1210
        type result struct {
×
1211
                RemotePubKey string `json:"remote_pub_key"`
×
1212
                ChannelPoint string `json:"channel_point"`
×
1213
                ClosingTxid  string `json:"closing_txid"`
×
1214
                FailErr      string `json:"error"`
×
1215
        }
×
1216

×
1217
        // Launch each channel closure in a goroutine in order to execute them
×
1218
        // in parallel. Once they're all executed, we will print the results as
×
1219
        // they come.
×
1220
        resultChan := make(chan result, len(channelsToClose))
×
1221
        for _, channel := range channelsToClose {
×
1222
                go func(channel *lnrpc.Channel) {
×
1223
                        res := result{}
×
1224
                        res.RemotePubKey = channel.RemotePubkey
×
1225
                        res.ChannelPoint = channel.ChannelPoint
×
1226
                        defer func() {
×
1227
                                resultChan <- res
1228
                        }()
1229

UNCOV
1230
                        // Parse the channel point in order to create the close
×
UNCOV
1231
                        // channel request.
×
1232
                        s := strings.Split(res.ChannelPoint, ":")
×
1233
                        if len(s) != 2 {
×
1234
                                res.FailErr = "expected channel point with " +
×
1235
                                        "format txid:index"
×
1236
                                return
×
1237
                        }
×
1238
                        index, err := strconv.ParseUint(s[1], 10, 32)
×
1239
                        if err != nil {
×
1240
                                res.FailErr = fmt.Sprintf("unable to parse "+
×
1241
                                        "channel point output index: %v", err)
×
1242
                                return
1243
                        }
×
UNCOV
1244

×
1245
                        req := &lnrpc.CloseChannelRequest{
×
1246
                                ChannelPoint: &lnrpc.ChannelPoint{
×
1247
                                        FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
×
1248
                                                FundingTxidStr: s[0],
×
1249
                                        },
×
1250
                                        OutputIndex: uint32(index),
×
1251
                                },
×
1252
                                Force:       !channel.GetActive(),
×
1253
                                TargetConf:  int32(ctx.Int64("conf_target")),
×
1254
                                SatPerVbyte: ctx.Uint64(feeRateFlag),
×
1255
                        }
×
1256

×
1257
                        txidChan := make(chan string, 1)
×
1258
                        err = executeChannelClose(ctxc, client, req, txidChan, false)
×
1259
                        if err != nil {
×
1260
                                res.FailErr = fmt.Sprintf("unable to close "+
×
1261
                                        "channel: %v", err)
×
1262
                                return
1263
                        }
×
1264

1265
                        res.ClosingTxid = <-txidChan
1266
                }(channel)
UNCOV
1267
        }
×
UNCOV
1268

×
1269
        for range channelsToClose {
×
1270
                res := <-resultChan
×
1271
                printJSON(res)
1272
        }
×
1273

1274
        return nil
1275
}
1276

UNCOV
1277
// promptForConfirmation continuously prompts the user for the message until
×
UNCOV
1278
// receiving a response of "yes" or "no" and returns their answer as a bool.
×
1279
func promptForConfirmation(msg string) bool {
×
1280
        reader := bufio.NewReader(os.Stdin)
×
1281

×
1282
        for {
×
1283
                fmt.Print(msg)
×
1284

×
1285
                answer, err := reader.ReadString('\n')
×
1286
                if err != nil {
×
1287
                        return false
1288
                }
×
UNCOV
1289

×
1290
                answer = strings.ToLower(strings.TrimSpace(answer))
×
1291

×
1292
                switch {
×
1293
                case answer == "yes":
×
1294
                        return true
×
1295
                case answer == "no":
×
1296
                        return false
×
1297
                default:
1298
                        continue
1299
                }
1300
        }
1301
}
1302

1303
var abandonChannelCommand = cli.Command{
1304
        Name:     "abandonchannel",
1305
        Category: "Channels",
1306
        Usage:    "Abandons an existing channel.",
1307
        Description: `
1308
        Removes all channel state from the database except for a close
1309
        summary. This method can be used to get rid of permanently unusable
1310
        channels due to bugs fixed in newer versions of lnd.
1311

1312
        Only available when lnd is built in debug mode. The flag
1313
        --i_know_what_i_am_doing can be set to override the debug/dev mode
1314
        requirement.
1315

1316
        To view which funding_txids/output_indexes can be used for this command,
1317
        see the channel_point values within the listchannels command output.
1318
        The format for a channel_point is 'funding_txid:output_index'.`,
1319
        ArgsUsage: "funding_txid [output_index]",
1320
        Flags: []cli.Flag{
1321
                cli.StringFlag{
1322
                        Name:  "funding_txid",
1323
                        Usage: "the txid of the channel's funding transaction",
1324
                },
1325
                cli.IntFlag{
1326
                        Name: "output_index",
1327
                        Usage: "the output index for the funding output of the funding " +
1328
                                "transaction",
1329
                },
1330
                cli.StringFlag{
1331
                        Name: "chan_point",
1332
                        Usage: "(optional) the channel point. If set, " +
1333
                                "funding_txid and output_index flags and " +
1334
                                "positional arguments will be ignored",
1335
                },
1336
                cli.BoolFlag{
1337
                        Name: "i_know_what_i_am_doing",
1338
                        Usage: "override the requirement for lnd needing to " +
1339
                                "be in dev/debug mode to use this command; " +
1340
                                "when setting this the user attests that " +
1341
                                "they know the danger of using this command " +
1342
                                "on channels and that doing so can lead to " +
1343
                                "loss of funds if the channel funding TX " +
1344
                                "ever confirms (or was confirmed)",
1345
                },
1346
        },
1347
        Action: actionDecorator(abandonChannel),
UNCOV
1348
}
×
UNCOV
1349

×
1350
func abandonChannel(ctx *cli.Context) error {
×
1351
        ctxc := getContext()
×
1352
        client, cleanUp := getClient(ctx)
×
1353
        defer cleanUp()
×
1354

×
1355
        // Show command help if no arguments and flags were provided.
×
1356
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1357
                cli.ShowCommandHelp(ctx, "abandonchannel")
×
1358
                return nil
1359
        }
×
UNCOV
1360

×
1361
        channelPoint, err := parseChannelPoint(ctx)
×
1362
        if err != nil {
×
1363
                return err
1364
        }
×
UNCOV
1365

×
1366
        req := &lnrpc.AbandonChannelRequest{
×
1367
                ChannelPoint:      channelPoint,
×
1368
                IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
×
1369
        }
×
1370

×
1371
        resp, err := client.AbandonChannel(ctxc, req)
×
1372
        if err != nil {
×
1373
                return err
1374
        }
×
UNCOV
1375

×
1376
        printRespJSON(resp)
1377
        return nil
1378
}
1379

UNCOV
1380
// parseChannelPoint parses a funding txid and output index from the command
×
UNCOV
1381
// line. Both named options and unnamed parameters are supported.
×
1382
func parseChannelPoint(ctx *cli.Context) (*lnrpc.ChannelPoint, error) {
×
1383
        channelPoint := &lnrpc.ChannelPoint{}
×
1384
        var err error
×
1385

×
1386
        args := ctx.Args()
×
1387

×
1388
        switch {
×
1389
        case ctx.IsSet("chan_point"):
×
1390
                channelPoint, err = parseChanPoint(ctx.String("chan_point"))
×
1391
                if err != nil {
×
1392
                        return nil, fmt.Errorf("unable to parse chan_point: "+
×
1393
                                "%v", err)
×
1394
                }
1395
                return channelPoint, nil
×
UNCOV
1396

×
1397
        case ctx.IsSet("funding_txid"):
×
1398
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1399
                        FundingTxidStr: ctx.String("funding_txid"),
×
1400
                }
×
1401
        case args.Present():
×
1402
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1403
                        FundingTxidStr: args.First(),
×
1404
                }
×
1405
                args = args.Tail()
×
1406
        default:
1407
                return nil, fmt.Errorf("funding txid argument missing")
UNCOV
1408
        }
×
UNCOV
1409

×
1410
        switch {
×
1411
        case ctx.IsSet("output_index"):
×
1412
                channelPoint.OutputIndex = uint32(ctx.Int("output_index"))
×
1413
        case args.Present():
×
1414
                index, err := strconv.ParseUint(args.First(), 10, 32)
×
1415
                if err != nil {
×
1416
                        return nil, fmt.Errorf("unable to decode output "+
×
1417
                                "index: %w", err)
×
1418
                }
×
1419
                channelPoint.OutputIndex = uint32(index)
×
1420
        default:
1421
                channelPoint.OutputIndex = 0
UNCOV
1422
        }
×
1423

1424
        return channelPoint, nil
1425
}
1426

1427
var listPeersCommand = cli.Command{
1428
        Name:     "listpeers",
1429
        Category: "Peers",
1430
        Usage:    "List all active, currently connected peers.",
1431
        Flags: []cli.Flag{
1432
                cli.BoolFlag{
1433
                        Name:  "list_errors",
1434
                        Usage: "list a full set of most recent errors for the peer",
1435
                },
1436
        },
1437
        Action: actionDecorator(listPeers),
UNCOV
1438
}
×
UNCOV
1439

×
1440
func listPeers(ctx *cli.Context) error {
×
1441
        ctxc := getContext()
×
1442
        client, cleanUp := getClient(ctx)
×
1443
        defer cleanUp()
×
1444

×
1445
        // By default, we display a single error on the cli. If the user
×
1446
        // specifically requests a full error set, then we will provide it.
×
1447
        req := &lnrpc.ListPeersRequest{
×
1448
                LatestError: !ctx.IsSet("list_errors"),
×
1449
        }
×
1450
        resp, err := client.ListPeers(ctxc, req)
×
1451
        if err != nil {
×
1452
                return err
1453
        }
×
UNCOV
1454

×
1455
        printRespJSON(resp)
1456
        return nil
1457
}
1458

1459
var walletBalanceCommand = cli.Command{
1460
        Name:     "walletbalance",
1461
        Category: "Wallet",
1462
        Usage:    "Compute and display the wallet's current balance.",
1463
        Flags: []cli.Flag{
1464
                cli.StringFlag{
1465
                        Name: "account",
1466
                        Usage: "(optional) the account for which the balance " +
1467
                                "is shown",
1468
                        Value: "",
1469
                },
1470
        },
1471
        Action: actionDecorator(walletBalance),
UNCOV
1472
}
×
UNCOV
1473

×
1474
func walletBalance(ctx *cli.Context) error {
×
1475
        ctxc := getContext()
×
1476
        client, cleanUp := getClient(ctx)
×
1477
        defer cleanUp()
×
1478

×
1479
        req := &lnrpc.WalletBalanceRequest{
×
1480
                Account: ctx.String("account"),
×
1481
        }
×
1482
        resp, err := client.WalletBalance(ctxc, req)
×
1483
        if err != nil {
×
1484
                return err
1485
        }
×
UNCOV
1486

×
1487
        printRespJSON(resp)
1488
        return nil
1489
}
1490

1491
var ChannelBalanceCommand = cli.Command{
1492
        Name:     "channelbalance",
1493
        Category: "Channels",
1494
        Usage: "Returns the sum of the total available channel balance across " +
1495
                "all open channels.",
1496
        Action: actionDecorator(ChannelBalance),
UNCOV
1497
}
×
UNCOV
1498

×
1499
func ChannelBalance(ctx *cli.Context) error {
×
1500
        ctxc := getContext()
×
1501
        client, cleanUp := getClient(ctx)
×
1502
        defer cleanUp()
×
1503

×
1504
        req := &lnrpc.ChannelBalanceRequest{}
×
1505
        resp, err := client.ChannelBalance(ctxc, req)
×
1506
        if err != nil {
×
1507
                return err
1508
        }
×
UNCOV
1509

×
1510
        printRespJSON(resp)
1511
        return nil
1512
}
1513

1514
var generateManPageCommand = cli.Command{
1515
        Name: "generatemanpage",
1516
        Usage: "Generates a man page for lncli and lnd as " +
1517
                "lncli.1 and lnd.1 respectively.",
1518
        Hidden: true,
1519
        Action: actionDecorator(generateManPage),
UNCOV
1520
}
×
UNCOV
1521

×
1522
func generateManPage(ctx *cli.Context) error {
×
1523
        // Generate the man pages for lncli as lncli.1.
×
1524
        manpages, err := ctx.App.ToMan()
×
1525
        if err != nil {
×
1526
                return err
×
1527
        }
×
1528
        err = os.WriteFile("lncli.1", []byte(manpages), 0644)
×
1529
        if err != nil {
×
1530
                return err
1531
        }
UNCOV
1532

×
UNCOV
1533
        // Generate the man pages for lnd as lnd.1.
×
1534
        config := lnd.DefaultConfig()
×
1535
        fileParser := flags.NewParser(&config, flags.Default)
×
1536
        fileParser.Name = "lnd"
×
1537

×
1538
        var buf bytes.Buffer
×
1539
        fileParser.WriteManPage(&buf)
×
1540

×
1541
        err = os.WriteFile("lnd.1", buf.Bytes(), 0644)
×
1542
        if err != nil {
×
1543
                return err
1544
        }
×
1545

1546
        return nil
1547
}
1548

1549
var getInfoCommand = cli.Command{
1550
        Name:   "getinfo",
1551
        Usage:  "Returns basic information related to the active daemon.",
1552
        Action: actionDecorator(getInfo),
UNCOV
1553
}
×
UNCOV
1554

×
1555
func getInfo(ctx *cli.Context) error {
×
1556
        ctxc := getContext()
×
1557
        client, cleanUp := getClient(ctx)
×
1558
        defer cleanUp()
×
1559

×
1560
        req := &lnrpc.GetInfoRequest{}
×
1561
        resp, err := client.GetInfo(ctxc, req)
×
1562
        if err != nil {
×
1563
                return err
1564
        }
×
UNCOV
1565

×
1566
        printRespJSON(resp)
1567
        return nil
1568
}
1569

1570
var getRecoveryInfoCommand = cli.Command{
1571
        Name:   "getrecoveryinfo",
1572
        Usage:  "Display information about an ongoing recovery attempt.",
1573
        Action: actionDecorator(getRecoveryInfo),
UNCOV
1574
}
×
UNCOV
1575

×
1576
func getRecoveryInfo(ctx *cli.Context) error {
×
1577
        ctxc := getContext()
×
1578
        client, cleanUp := getClient(ctx)
×
1579
        defer cleanUp()
×
1580

×
1581
        req := &lnrpc.GetRecoveryInfoRequest{}
×
1582
        resp, err := client.GetRecoveryInfo(ctxc, req)
×
1583
        if err != nil {
×
1584
                return err
1585
        }
×
UNCOV
1586

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

1591
var pendingChannelsCommand = cli.Command{
1592
        Name:     "pendingchannels",
1593
        Category: "Channels",
1594
        Usage:    "Display information pertaining to pending channels.",
1595
        Flags: []cli.Flag{
1596
                cli.BoolFlag{
1597
                        Name: "include_raw_tx",
1598
                        Usage: "include the raw transaction hex for " +
1599
                                "waiting_close_channels.",
1600
                },
1601
        },
1602
        Action: actionDecorator(pendingChannels),
UNCOV
1603
}
×
UNCOV
1604

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

×
1610
        includeRawTx := ctx.Bool("include_raw_tx")
×
1611
        req := &lnrpc.PendingChannelsRequest{
×
1612
                IncludeRawTx: includeRawTx,
×
1613
        }
×
1614
        resp, err := client.PendingChannels(ctxc, req)
×
1615
        if err != nil {
×
1616
                return err
1617
        }
×
UNCOV
1618

×
1619
        printRespJSON(resp)
×
1620

1621
        return nil
1622
}
1623

1624
var ListChannelsCommand = cli.Command{
1625
        Name:     "listchannels",
1626
        Category: "Channels",
1627
        Usage:    "List all open channels.",
1628
        Flags: []cli.Flag{
1629
                cli.BoolFlag{
1630
                        Name:  "active_only",
1631
                        Usage: "only list channels which are currently active",
1632
                },
1633
                cli.BoolFlag{
1634
                        Name:  "inactive_only",
1635
                        Usage: "only list channels which are currently inactive",
1636
                },
1637
                cli.BoolFlag{
1638
                        Name:  "public_only",
1639
                        Usage: "only list channels which are currently public",
1640
                },
1641
                cli.BoolFlag{
1642
                        Name:  "private_only",
1643
                        Usage: "only list channels which are currently private",
1644
                },
1645
                cli.StringFlag{
1646
                        Name: "peer",
1647
                        Usage: "(optional) only display channels with a " +
1648
                                "particular peer, accepts 66-byte, " +
1649
                                "hex-encoded pubkeys",
1650
                },
1651
                cli.BoolFlag{
1652
                        Name: "skip_peer_alias_lookup",
1653
                        Usage: "skip the peer alias lookup per channel in " +
1654
                                "order to improve performance",
1655
                },
1656
        },
1657
        Action: actionDecorator(ListChannels),
1658
}
1659

1660
var listAliasesCommand = cli.Command{
1661
        Name:     "listaliases",
1662
        Category: "Channels",
1663
        Usage:    "List all aliases.",
1664
        Flags:    []cli.Flag{},
1665
        Action:   actionDecorator(listAliases),
UNCOV
1666
}
×
UNCOV
1667

×
1668
func listAliases(ctx *cli.Context) error {
×
1669
        ctxc := getContext()
×
1670
        client, cleanUp := getClient(ctx)
×
1671
        defer cleanUp()
×
1672

×
1673
        req := &lnrpc.ListAliasesRequest{}
×
1674

×
1675
        resp, err := client.ListAliases(ctxc, req)
×
1676
        if err != nil {
×
1677
                return err
1678
        }
×
UNCOV
1679

×
1680
        printRespJSON(resp)
×
1681

1682
        return nil
UNCOV
1683
}
×
UNCOV
1684

×
1685
func ListChannels(ctx *cli.Context) error {
×
1686
        ctxc := getContext()
×
1687
        client, cleanUp := getClient(ctx)
×
1688
        defer cleanUp()
×
1689

×
1690
        peer := ctx.String("peer")
×
1691

×
1692
        // If the user requested channels with a particular key, parse the
×
1693
        // provided pubkey.
×
1694
        var peerKey []byte
×
1695
        if len(peer) > 0 {
×
1696
                pk, err := route.NewVertexFromStr(peer)
×
1697
                if err != nil {
×
1698
                        return fmt.Errorf("invalid --peer pubkey: %w", err)
1699
                }
×
1700

1701
                peerKey = pk[:]
1702
        }
1703

UNCOV
1704
        // By default, we will look up the peers' alias information unless the
×
UNCOV
1705
        // skip_peer_alias_lookup flag indicates otherwise.
×
1706
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1707

×
1708
        req := &lnrpc.ListChannelsRequest{
×
1709
                ActiveOnly:      ctx.Bool("active_only"),
×
1710
                InactiveOnly:    ctx.Bool("inactive_only"),
×
1711
                PublicOnly:      ctx.Bool("public_only"),
×
1712
                PrivateOnly:     ctx.Bool("private_only"),
×
1713
                Peer:            peerKey,
×
1714
                PeerAliasLookup: lookupPeerAlias,
×
1715
        }
×
1716

×
1717
        resp, err := client.ListChannels(ctxc, req)
×
1718
        if err != nil {
×
1719
                return err
1720
        }
×
UNCOV
1721

×
1722
        printRespJSON(resp)
×
1723

1724
        return nil
1725
}
1726

1727
var closedChannelsCommand = cli.Command{
1728
        Name:     "closedchannels",
1729
        Category: "Channels",
1730
        Usage:    "List all closed channels.",
1731
        Flags: []cli.Flag{
1732
                cli.BoolFlag{
1733
                        Name:  "cooperative",
1734
                        Usage: "list channels that were closed cooperatively",
1735
                },
1736
                cli.BoolFlag{
1737
                        Name: "local_force",
1738
                        Usage: "list channels that were force-closed " +
1739
                                "by the local node",
1740
                },
1741
                cli.BoolFlag{
1742
                        Name: "remote_force",
1743
                        Usage: "list channels that were force-closed " +
1744
                                "by the remote node",
1745
                },
1746
                cli.BoolFlag{
1747
                        Name: "breach",
1748
                        Usage: "list channels for which the remote node " +
1749
                                "attempted to broadcast a prior " +
1750
                                "revoked channel state",
1751
                },
1752
                cli.BoolFlag{
1753
                        Name:  "funding_canceled",
1754
                        Usage: "list channels that were never fully opened",
1755
                },
1756
                cli.BoolFlag{
1757
                        Name: "abandoned",
1758
                        Usage: "list channels that were abandoned by " +
1759
                                "the local node",
1760
                },
1761
        },
1762
        Action: actionDecorator(closedChannels),
UNCOV
1763
}
×
UNCOV
1764

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

×
1770
        req := &lnrpc.ClosedChannelsRequest{
×
1771
                Cooperative:     ctx.Bool("cooperative"),
×
1772
                LocalForce:      ctx.Bool("local_force"),
×
1773
                RemoteForce:     ctx.Bool("remote_force"),
×
1774
                Breach:          ctx.Bool("breach"),
×
1775
                FundingCanceled: ctx.Bool("funding_canceled"),
×
1776
                Abandoned:       ctx.Bool("abandoned"),
×
1777
        }
×
1778

×
1779
        resp, err := client.ClosedChannels(ctxc, req)
×
1780
        if err != nil {
×
1781
                return err
1782
        }
×
UNCOV
1783

×
1784
        printRespJSON(resp)
×
1785

1786
        return nil
1787
}
1788

1789
var describeGraphCommand = cli.Command{
1790
        Name:     "describegraph",
1791
        Category: "Graph",
1792
        Description: "Prints a human readable version of the known channel " +
1793
                "graph from the PoV of the node",
1794
        Usage: "Describe the network graph.",
1795
        Flags: []cli.Flag{
1796
                cli.BoolFlag{
1797
                        Name: "include_unannounced",
1798
                        Usage: "If set, unannounced channels will be included in the " +
1799
                                "graph. Unannounced channels are both private channels, and " +
1800
                                "public channels that are not yet announced to the network.",
1801
                },
1802
        },
1803
        Action: actionDecorator(describeGraph),
UNCOV
1804
}
×
UNCOV
1805

×
1806
func describeGraph(ctx *cli.Context) error {
×
1807
        ctxc := getContext()
×
1808
        client, cleanUp := getClient(ctx)
×
1809
        defer cleanUp()
×
1810

×
1811
        req := &lnrpc.ChannelGraphRequest{
×
1812
                IncludeUnannounced: ctx.Bool("include_unannounced"),
×
1813
        }
×
1814

×
1815
        graph, err := client.DescribeGraph(ctxc, req)
×
1816
        if err != nil {
×
1817
                return err
1818
        }
×
UNCOV
1819

×
1820
        printRespJSON(graph)
1821
        return nil
1822
}
1823

1824
var getNodeMetricsCommand = cli.Command{
1825
        Name:        "getnodemetrics",
1826
        Category:    "Graph",
1827
        Description: "Prints out node metrics calculated from the current graph",
1828
        Usage:       "Get node metrics.",
1829
        Action:      actionDecorator(getNodeMetrics),
UNCOV
1830
}
×
UNCOV
1831

×
1832
func getNodeMetrics(ctx *cli.Context) error {
×
1833
        ctxc := getContext()
×
1834
        client, cleanUp := getClient(ctx)
×
1835
        defer cleanUp()
×
1836

×
1837
        req := &lnrpc.NodeMetricsRequest{
×
1838
                Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
×
1839
        }
×
1840

×
1841
        nodeMetrics, err := client.GetNodeMetrics(ctxc, req)
×
1842
        if err != nil {
×
1843
                return err
1844
        }
×
UNCOV
1845

×
1846
        printRespJSON(nodeMetrics)
1847
        return nil
1848
}
1849

1850
var getChanInfoCommand = cli.Command{
1851
        Name:     "getchaninfo",
1852
        Category: "Graph",
1853
        Usage:    "Get the state of a channel.",
1854
        Description: "Prints out the latest authenticated state for a " +
1855
                "particular channel",
1856
        ArgsUsage: "chan_id",
1857
        Flags: []cli.Flag{
1858
                cli.Uint64Flag{
1859
                        Name: "chan_id",
1860
                        Usage: "The 8-byte compact channel ID to query for. " +
1861
                                "If this is set the chan_point param is " +
1862
                                "ignored.",
1863
                },
1864
                cli.StringFlag{
1865
                        Name: "chan_point",
1866
                        Usage: "The channel point in format txid:index. If " +
1867
                                "the chan_id param is set this param is " +
1868
                                "ignored.",
1869
                },
1870
        },
1871
        Action: actionDecorator(getChanInfo),
UNCOV
1872
}
×
UNCOV
1873

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

×
1879
        var (
×
1880
                chanID    uint64
×
1881
                chanPoint string
×
1882
                err       error
×
1883
        )
×
1884

×
1885
        switch {
×
1886
        case ctx.IsSet("chan_id"):
1887
                chanID = ctx.Uint64("chan_id")
×
UNCOV
1888

×
1889
        case ctx.Args().Present():
×
1890
                chanID, err = strconv.ParseUint(ctx.Args().First(), 10, 64)
×
1891
                if err != nil {
×
1892
                        return fmt.Errorf("error parsing chan_id: %w", err)
1893
                }
×
UNCOV
1894

×
1895
        case ctx.IsSet("chan_point"):
1896
                chanPoint = ctx.String("chan_point")
×
UNCOV
1897

×
1898
        default:
1899
                return fmt.Errorf("chan_id or chan_point argument missing")
UNCOV
1900
        }
×
UNCOV
1901

×
1902
        req := &lnrpc.ChanInfoRequest{
×
1903
                ChanId:    chanID,
×
1904
                ChanPoint: chanPoint,
×
1905
        }
×
1906

×
1907
        chanInfo, err := client.GetChanInfo(ctxc, req)
×
1908
        if err != nil {
×
1909
                return err
1910
        }
×
UNCOV
1911

×
1912
        printRespJSON(chanInfo)
1913
        return nil
1914
}
1915

1916
var getNodeInfoCommand = cli.Command{
1917
        Name:     "getnodeinfo",
1918
        Category: "Graph",
1919
        Usage:    "Get information on a specific node.",
1920
        Description: "Prints out the latest authenticated node state for an " +
1921
                "advertised node",
1922
        Flags: []cli.Flag{
1923
                cli.StringFlag{
1924
                        Name: "pub_key",
1925
                        Usage: "the 33-byte hex-encoded compressed public of the target " +
1926
                                "node",
1927
                },
1928
                cli.BoolFlag{
1929
                        Name: "include_channels",
1930
                        Usage: "if true, will return all known channels " +
1931
                                "associated with the node",
1932
                },
1933
        },
1934
        Action: actionDecorator(getNodeInfo),
UNCOV
1935
}
×
UNCOV
1936

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

×
1942
        args := ctx.Args()
×
1943

×
1944
        var pubKey string
×
1945
        switch {
×
1946
        case ctx.IsSet("pub_key"):
×
1947
                pubKey = ctx.String("pub_key")
×
1948
        case args.Present():
×
1949
                pubKey = args.First()
×
1950
        default:
1951
                return fmt.Errorf("pub_key argument missing")
UNCOV
1952
        }
×
UNCOV
1953

×
1954
        req := &lnrpc.NodeInfoRequest{
×
1955
                PubKey:          pubKey,
×
1956
                IncludeChannels: ctx.Bool("include_channels"),
×
1957
        }
×
1958

×
1959
        nodeInfo, err := client.GetNodeInfo(ctxc, req)
×
1960
        if err != nil {
×
1961
                return err
1962
        }
×
UNCOV
1963

×
1964
        printRespJSON(nodeInfo)
1965
        return nil
1966
}
1967

1968
var getNetworkInfoCommand = cli.Command{
1969
        Name:     "getnetworkinfo",
1970
        Category: "Channels",
1971
        Usage: "Get statistical information about the current " +
1972
                "state of the network.",
1973
        Description: "Returns a set of statistics pertaining to the known " +
1974
                "channel graph",
1975
        Action: actionDecorator(getNetworkInfo),
UNCOV
1976
}
×
UNCOV
1977

×
1978
func getNetworkInfo(ctx *cli.Context) error {
×
1979
        ctxc := getContext()
×
1980
        client, cleanUp := getClient(ctx)
×
1981
        defer cleanUp()
×
1982

×
1983
        req := &lnrpc.NetworkInfoRequest{}
×
1984

×
1985
        netInfo, err := client.GetNetworkInfo(ctxc, req)
×
1986
        if err != nil {
×
1987
                return err
1988
        }
×
UNCOV
1989

×
1990
        printRespJSON(netInfo)
1991
        return nil
1992
}
1993

1994
var debugLevelCommand = cli.Command{
1995
        Name:  "debuglevel",
1996
        Usage: "Set the debug level.",
1997
        Description: `Logging level for all subsystems {trace, debug, info, warn, error, critical, off}
1998
        You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems
1999

2000
        Use show to list available subsystems`,
2001
        Flags: []cli.Flag{
2002
                cli.BoolFlag{
2003
                        Name:  "show",
2004
                        Usage: "if true, then the list of available sub-systems will be printed out",
2005
                },
2006
                cli.StringFlag{
2007
                        Name:  "level",
2008
                        Usage: "the level specification to target either a coarse logging level, or granular set of specific sub-systems with logging levels for each",
2009
                },
2010
        },
2011
        Action: actionDecorator(debugLevel),
UNCOV
2012
}
×
UNCOV
2013

×
2014
func debugLevel(ctx *cli.Context) error {
×
2015
        ctxc := getContext()
×
2016
        client, cleanUp := getClient(ctx)
×
2017
        defer cleanUp()
×
2018
        req := &lnrpc.DebugLevelRequest{
×
2019
                Show:      ctx.Bool("show"),
×
2020
                LevelSpec: ctx.String("level"),
×
2021
        }
×
2022

×
2023
        resp, err := client.DebugLevel(ctxc, req)
×
2024
        if err != nil {
×
2025
                return err
2026
        }
×
UNCOV
2027

×
2028
        printRespJSON(resp)
2029
        return nil
2030
}
2031

2032
var listChainTxnsCommand = cli.Command{
2033
        Name:     "listchaintxns",
2034
        Category: "On-chain",
2035
        Usage:    "List transactions from the wallet.",
2036
        Flags: []cli.Flag{
2037
                cli.Int64Flag{
2038
                        Name: "start_height",
2039
                        Usage: "the block height from which to list " +
2040
                                "transactions, inclusive",
2041
                },
2042
                cli.Int64Flag{
2043
                        Name: "end_height",
2044
                        Usage: "the block height until which to list " +
2045
                                "transactions, inclusive, to get " +
2046
                                "transactions until the chain tip, including " +
2047
                                "unconfirmed, set this value to -1",
2048
                },
2049
        },
2050
        Description: `
2051
        List all transactions an address of the wallet was involved in.
2052

2053
        This call will return a list of wallet related transactions that paid
2054
        to an address our wallet controls, or spent utxos that we held. The
2055
        start_height and end_height flags can be used to specify an inclusive
2056
        block range over which to query for transactions. If the end_height is
2057
        less than the start_height, transactions will be queried in reverse.
2058
        To get all transactions until the chain tip, including unconfirmed
2059
        transactions (identifiable with BlockHeight=0), set end_height to -1.
2060
        By default, this call will get all transactions our wallet was involved
2061
        in, including unconfirmed transactions.
2062
`,
2063
        Action: actionDecorator(listChainTxns),
UNCOV
2064
}
×
UNCOV
2065

×
2066
func listChainTxns(ctx *cli.Context) error {
×
2067
        ctxc := getContext()
×
2068
        client, cleanUp := getClient(ctx)
×
2069
        defer cleanUp()
×
2070

×
2071
        req := &lnrpc.GetTransactionsRequest{}
×
2072

×
2073
        if ctx.IsSet("start_height") {
×
2074
                req.StartHeight = int32(ctx.Int64("start_height"))
×
2075
        }
×
2076
        if ctx.IsSet("end_height") {
×
2077
                req.EndHeight = int32(ctx.Int64("end_height"))
2078
        }
×
UNCOV
2079

×
2080
        resp, err := client.GetTransactions(ctxc, req)
×
2081
        if err != nil {
×
2082
                return err
2083
        }
×
UNCOV
2084

×
2085
        printRespJSON(resp)
2086
        return nil
2087
}
2088

2089
var stopCommand = cli.Command{
2090
        Name:  "stop",
2091
        Usage: "Stop and shutdown the daemon.",
2092
        Description: `
2093
        Gracefully stop all daemon subsystems before stopping the daemon itself.
2094
        This is equivalent to stopping it using CTRL-C.`,
2095
        Action: actionDecorator(stopDaemon),
UNCOV
2096
}
×
UNCOV
2097

×
2098
func stopDaemon(ctx *cli.Context) error {
×
2099
        ctxc := getContext()
×
2100
        client, cleanUp := getClient(ctx)
×
2101
        defer cleanUp()
×
2102

×
2103
        _, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2104
        if err != nil {
×
2105
                return err
2106
        }
×
2107

2108
        return nil
2109
}
2110

2111
var signMessageCommand = cli.Command{
2112
        Name:      "signmessage",
2113
        Category:  "Wallet",
2114
        Usage:     "Sign a message with the node's private key.",
2115
        ArgsUsage: "msg",
2116
        Description: `
2117
        Sign msg with the resident node's private key.
2118
        Returns the signature as a zbase32 string.
2119

2120
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2121
        Flags: []cli.Flag{
2122
                cli.StringFlag{
2123
                        Name:  "msg",
2124
                        Usage: "the message to sign",
2125
                },
2126
        },
2127
        Action: actionDecorator(signMessage),
UNCOV
2128
}
×
UNCOV
2129

×
2130
func signMessage(ctx *cli.Context) error {
×
2131
        ctxc := getContext()
×
2132
        client, cleanUp := getClient(ctx)
×
2133
        defer cleanUp()
×
2134

×
2135
        var msg []byte
×
2136

×
2137
        switch {
×
2138
        case ctx.IsSet("msg"):
×
2139
                msg = []byte(ctx.String("msg"))
×
2140
        case ctx.Args().Present():
×
2141
                msg = []byte(ctx.Args().First())
×
2142
        default:
2143
                return fmt.Errorf("msg argument missing")
UNCOV
2144
        }
×
UNCOV
2145

×
2146
        resp, err := client.SignMessage(ctxc, &lnrpc.SignMessageRequest{Msg: msg})
×
2147
        if err != nil {
×
2148
                return err
2149
        }
×
UNCOV
2150

×
2151
        printRespJSON(resp)
2152
        return nil
2153
}
2154

2155
var verifyMessageCommand = cli.Command{
2156
        Name:      "verifymessage",
2157
        Category:  "Wallet",
2158
        Usage:     "Verify a message signed with the signature.",
2159
        ArgsUsage: "msg signature",
2160
        Description: `
2161
        Verify that the message was signed with a properly-formed signature
2162
        The signature must be zbase32 encoded and signed with the private key of
2163
        an active node in the resident node's channel database.
2164

2165
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2166
        Flags: []cli.Flag{
2167
                cli.StringFlag{
2168
                        Name:  "msg",
2169
                        Usage: "the message to verify",
2170
                },
2171
                cli.StringFlag{
2172
                        Name:  "sig",
2173
                        Usage: "the zbase32 encoded signature of the message",
2174
                },
2175
        },
2176
        Action: actionDecorator(verifyMessage),
UNCOV
2177
}
×
UNCOV
2178

×
2179
func verifyMessage(ctx *cli.Context) error {
×
2180
        ctxc := getContext()
×
2181
        client, cleanUp := getClient(ctx)
×
2182
        defer cleanUp()
×
2183

×
2184
        var (
×
2185
                msg []byte
×
2186
                sig string
×
2187
        )
×
2188

×
2189
        args := ctx.Args()
×
2190

×
2191
        switch {
×
2192
        case ctx.IsSet("msg"):
×
2193
                msg = []byte(ctx.String("msg"))
×
2194
        case args.Present():
×
2195
                msg = []byte(ctx.Args().First())
×
2196
                args = args.Tail()
×
2197
        default:
2198
                return fmt.Errorf("msg argument missing")
UNCOV
2199
        }
×
UNCOV
2200

×
2201
        switch {
×
2202
        case ctx.IsSet("sig"):
×
2203
                sig = ctx.String("sig")
×
2204
        case args.Present():
×
2205
                sig = args.First()
×
2206
        default:
2207
                return fmt.Errorf("signature argument missing")
UNCOV
2208
        }
×
UNCOV
2209

×
2210
        req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
×
2211
        resp, err := client.VerifyMessage(ctxc, req)
×
2212
        if err != nil {
×
2213
                return err
2214
        }
×
UNCOV
2215

×
2216
        printRespJSON(resp)
2217
        return nil
2218
}
2219

2220
var feeReportCommand = cli.Command{
2221
        Name:     "feereport",
2222
        Category: "Channels",
2223
        Usage:    "Display the current fee policies of all active channels.",
2224
        Description: `
2225
        Returns the current fee policies of all active channels.
2226
        Fee policies can be updated using the updatechanpolicy command.`,
2227
        Action: actionDecorator(feeReport),
UNCOV
2228
}
×
UNCOV
2229

×
2230
func feeReport(ctx *cli.Context) error {
×
2231
        ctxc := getContext()
×
2232
        client, cleanUp := getClient(ctx)
×
2233
        defer cleanUp()
×
2234

×
2235
        req := &lnrpc.FeeReportRequest{}
×
2236
        resp, err := client.FeeReport(ctxc, req)
×
2237
        if err != nil {
×
2238
                return err
2239
        }
×
UNCOV
2240

×
2241
        printRespJSON(resp)
2242
        return nil
2243
}
2244

2245
var updateChannelPolicyCommand = cli.Command{
2246
        Name:     "updatechanpolicy",
2247
        Category: "Channels",
2248
        Usage: "Update the channel policy for all channels, or a single " +
2249
                "channel.",
2250
        ArgsUsage: "base_fee_msat fee_rate time_lock_delta " +
2251
                "[--max_htlc_msat=N] [channel_point]",
2252
        Description: `
2253
        Updates the channel policy for all channels, or just a particular
2254
        channel identified by its channel point. The update will be committed, 
2255
        and broadcast to the rest of the network within the next batch. Channel
2256
        points are encoded as: funding_txid:output_index
2257
        `,
2258
        Flags: []cli.Flag{
2259
                cli.Int64Flag{
2260
                        Name: "base_fee_msat",
2261
                        Usage: "the base fee in milli-satoshis that will be " +
2262
                                "charged for each forwarded HTLC, regardless " +
2263
                                "of payment size",
2264
                },
2265
                cli.StringFlag{
2266
                        Name: "fee_rate",
2267
                        Usage: "the fee rate that will be charged " +
2268
                                "proportionally based on the value of each " +
2269
                                "forwarded HTLC, the lowest possible rate is " +
2270
                                "0 with a granularity of 0.000001 " +
2271
                                "(millionths). Can not be set at the same " +
2272
                                "time as fee_rate_ppm",
2273
                },
2274
                cli.Uint64Flag{
2275
                        Name: "fee_rate_ppm",
2276
                        Usage: "the fee rate ppm (parts per million) that " +
2277
                                "will be charged proportionally based on the " +
2278
                                "value of each forwarded HTLC, the lowest " +
2279
                                "possible rate is 0 with a granularity of " +
2280
                                "0.000001 (millionths). Can not be set at " +
2281
                                "the same time as fee_rate",
2282
                },
2283
                cli.Int64Flag{
2284
                        Name: "inbound_base_fee_msat",
2285
                        Usage: "the base inbound fee in milli-satoshis that " +
2286
                                "will be charged for each forwarded HTLC, " +
2287
                                "regardless of payment size. Its value must " +
2288
                                "be zero or negative - it is a discount " +
2289
                                "for using a particular incoming channel. " +
2290
                                "Note that forwards will be rejected if the " +
2291
                                "discount exceeds the outbound fee " +
2292
                                "(forward at a loss), and lead to " +
2293
                                "penalization by the sender",
2294
                },
2295
                cli.Int64Flag{
2296
                        Name: "inbound_fee_rate_ppm",
2297
                        Usage: "the inbound fee rate that will be charged " +
2298
                                "proportionally based on the value of each " +
2299
                                "forwarded HTLC and the outbound fee. Fee " +
2300
                                "rate is expressed in parts per million and " +
2301
                                "must be zero or negative - it is a discount " +
2302
                                "for using a particular incoming channel. " +
2303
                                "Note that forwards will be rejected if the " +
2304
                                "discount exceeds the outbound fee " +
2305
                                "(forward at a loss), and lead to " +
2306
                                "penalization by the sender",
2307
                },
2308
                cli.Uint64Flag{
2309
                        Name: "time_lock_delta",
2310
                        Usage: "the CLTV delta that will be applied to all " +
2311
                                "forwarded HTLCs",
2312
                },
2313
                cli.Uint64Flag{
2314
                        Name: "min_htlc_msat",
2315
                        Usage: "if set, the min HTLC size that will be " +
2316
                                "applied to all forwarded HTLCs. If unset, " +
2317
                                "the min HTLC is left unchanged",
2318
                },
2319
                cli.Uint64Flag{
2320
                        Name: "max_htlc_msat",
2321
                        Usage: "if set, the max HTLC size that will be " +
2322
                                "applied to all forwarded HTLCs. If unset, " +
2323
                                "the max HTLC is left unchanged",
2324
                },
2325
                cli.StringFlag{
2326
                        Name: "chan_point",
2327
                        Usage: "the channel which this policy update should " +
2328
                                "be applied to. If nil, the policies for all " +
2329
                                "channels will be updated. Takes the form of " +
2330
                                "txid:output_index",
2331
                },
2332
        },
2333
        Action: actionDecorator(updateChannelPolicy),
2334
}
7✔
2335

7✔
2336
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
10✔
2337
        split := strings.Split(s, ":")
3✔
2338
        if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
3✔
2339
                return nil, errBadChanPoint
2340
        }
4✔
2341

5✔
2342
        index, err := strconv.ParseInt(split[1], 10, 64)
1✔
2343
        if err != nil {
1✔
2344
                return nil, fmt.Errorf("unable to decode output index: %w", err)
2345
        }
3✔
2346

4✔
2347
        txid, err := chainhash.NewHashFromStr(split[0])
1✔
2348
        if err != nil {
1✔
2349
                return nil, fmt.Errorf("unable to parse hex string: %w", err)
2350
        }
2✔
2351

2✔
2352
        return &lnrpc.ChannelPoint{
2✔
2353
                FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
2✔
2354
                        FundingTxidBytes: txid[:],
2✔
2355
                },
2✔
2356
                OutputIndex: uint32(index),
2357
        }, nil
2358
}
2359

2360
// parseTimeLockDelta is expected to get a uint16 type of timeLockDelta. Its
5✔
2361
// maximum value is MaxTimeLockDelta.
5✔
2362
func parseTimeLockDelta(timeLockDeltaStr string) (uint16, error) {
7✔
2363
        timeLockDeltaUnCheck, err := strconv.ParseUint(timeLockDeltaStr, 10, 64)
2✔
2364
        if err != nil {
2✔
2365
                return 0, fmt.Errorf("failed to parse time_lock_delta: %s "+
2✔
2366
                        "to uint64, err: %v", timeLockDeltaStr, err)
2367
        }
3✔
UNCOV
2368

×
UNCOV
2369
        if timeLockDeltaUnCheck > routing.MaxCLTVDelta {
×
2370
                return 0, fmt.Errorf("time_lock_delta is too big, "+
×
2371
                        "max value is %d", routing.MaxCLTVDelta)
2372
        }
3✔
2373

2374
        return uint16(timeLockDeltaUnCheck), nil
UNCOV
2375
}
×
UNCOV
2376

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

×
2382
        var (
×
2383
                baseFee       int64
×
2384
                feeRate       float64
×
2385
                feeRatePpm    uint64
×
2386
                timeLockDelta uint16
×
2387
                err           error
×
2388
        )
×
2389
        args := ctx.Args()
×
2390

×
2391
        switch {
×
2392
        case ctx.IsSet("base_fee_msat"):
×
2393
                baseFee = ctx.Int64("base_fee_msat")
×
2394
        case args.Present():
×
2395
                baseFee, err = strconv.ParseInt(args.First(), 10, 64)
×
2396
                if err != nil {
×
2397
                        return fmt.Errorf("unable to decode base_fee_msat: %w",
×
2398
                                err)
×
2399
                }
×
2400
                args = args.Tail()
×
2401
        default:
2402
                return fmt.Errorf("base_fee_msat argument missing")
UNCOV
2403
        }
×
UNCOV
2404

×
2405
        switch {
×
2406
        case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
×
2407
                return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
×
2408
        case ctx.IsSet("fee_rate"):
×
2409
                feeRate = ctx.Float64("fee_rate")
×
2410
        case ctx.IsSet("fee_rate_ppm"):
×
2411
                feeRatePpm = ctx.Uint64("fee_rate_ppm")
×
2412
        case args.Present():
×
2413
                feeRate, err = strconv.ParseFloat(args.First(), 64)
×
2414
                if err != nil {
×
2415
                        return fmt.Errorf("unable to decode fee_rate: %w", err)
2416
                }
×
UNCOV
2417

×
2418
                args = args.Tail()
×
2419
        default:
2420
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
UNCOV
2421
        }
×
UNCOV
2422

×
2423
        switch {
×
2424
        case ctx.IsSet("time_lock_delta"):
×
2425
                timeLockDeltaStr := ctx.String("time_lock_delta")
×
2426
                timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
×
2427
                if err != nil {
×
2428
                        return err
×
2429
                }
×
2430
        case args.Present():
×
2431
                timeLockDelta, err = parseTimeLockDelta(args.First())
×
2432
                if err != nil {
×
2433
                        return err
2434
                }
×
UNCOV
2435

×
2436
                args = args.Tail()
×
2437
        default:
2438
                return fmt.Errorf("time_lock_delta argument missing")
UNCOV
2439
        }
×
UNCOV
2440

×
2441
        var (
×
2442
                chanPoint    *lnrpc.ChannelPoint
×
2443
                chanPointStr string
×
2444
        )
×
2445

×
2446
        switch {
×
2447
        case ctx.IsSet("chan_point"):
×
2448
                chanPointStr = ctx.String("chan_point")
×
2449
        case args.Present():
2450
                chanPointStr = args.First()
UNCOV
2451
        }
×
UNCOV
2452

×
2453
        if chanPointStr != "" {
×
2454
                chanPoint, err = parseChanPoint(chanPointStr)
×
2455
                if err != nil {
×
2456
                        return fmt.Errorf("unable to parse chan_point: %w", err)
2457
                }
UNCOV
2458
        }
×
UNCOV
2459

×
2460
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2461
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2462
                inboundBaseFeeMsat > math.MaxInt32 {
×
2463

×
2464
                return errors.New("inbound_base_fee_msat out of range")
2465
        }
×
UNCOV
2466

×
2467
        inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
×
2468
        if inboundFeeRatePpm < math.MinInt32 ||
×
2469
                inboundFeeRatePpm > math.MaxInt32 {
×
2470

×
2471
                return errors.New("inbound_fee_rate_ppm out of range")
2472
        }
2473

UNCOV
2474
        // Inbound fees are optional. However, if an update is required,
×
UNCOV
2475
        // both the base fee and the fee rate must be provided.
×
2476
        var inboundFee *lnrpc.InboundFee
×
2477
        if ctx.IsSet("inbound_base_fee_msat") !=
×
2478
                ctx.IsSet("inbound_fee_rate_ppm") {
×
2479

×
2480
                return errors.New("both parameters must be provided: " +
×
2481
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
2482
        }
×
UNCOV
2483

×
2484
        if ctx.IsSet("inbound_fee_rate_ppm") {
×
2485
                inboundFee = &lnrpc.InboundFee{
×
2486
                        BaseFeeMsat: int32(inboundBaseFeeMsat),
×
2487
                        FeeRatePpm:  int32(inboundFeeRatePpm),
×
2488
                }
2489
        }
×
UNCOV
2490

×
2491
        req := &lnrpc.PolicyUpdateRequest{
×
2492
                BaseFeeMsat:   baseFee,
×
2493
                TimeLockDelta: uint32(timeLockDelta),
×
2494
                MaxHtlcMsat:   ctx.Uint64("max_htlc_msat"),
×
2495
                InboundFee:    inboundFee,
×
2496
        }
×
2497

×
2498
        if ctx.IsSet("min_htlc_msat") {
×
2499
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2500
                req.MinHtlcMsatSpecified = true
2501
        }
×
UNCOV
2502

×
2503
        if chanPoint != nil {
×
2504
                req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
×
2505
                        ChanPoint: chanPoint,
×
2506
                }
×
2507
        } else {
×
2508
                req.Scope = &lnrpc.PolicyUpdateRequest_Global{
×
2509
                        Global: true,
×
2510
                }
2511
        }
×
UNCOV
2512

×
2513
        if feeRate != 0 {
×
2514
                req.FeeRate = feeRate
×
2515
        } else if feeRatePpm != 0 {
×
2516
                req.FeeRatePpm = uint32(feeRatePpm)
2517
        }
×
UNCOV
2518

×
2519
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2520
        if err != nil {
×
2521
                return err
2522
        }
2523

2524
        // Parse the response into the final json object that will be printed
UNCOV
2525
        // to stdout. At the moment, this filters out the raw txid bytes from
×
UNCOV
2526
        // each failed update's outpoint and only prints the txid string.
×
2527
        var listFailedUpdateResp = struct {
×
2528
                FailedUpdates []*FailedUpdate `json:"failed_updates"`
×
2529
        }{
×
2530
                FailedUpdates: make([]*FailedUpdate, 0, len(resp.FailedUpdates)),
×
2531
        }
×
2532
        for _, protoUpdate := range resp.FailedUpdates {
×
2533
                failedUpdate := NewFailedUpdateFromProto(protoUpdate)
×
2534
                listFailedUpdateResp.FailedUpdates = append(
×
2535
                        listFailedUpdateResp.FailedUpdates, failedUpdate)
2536
        }
×
UNCOV
2537

×
2538
        printJSON(listFailedUpdateResp)
×
2539

2540
        return nil
2541
}
2542

2543
var fishCompletionCommand = cli.Command{
UNCOV
2544
        Name:   "fish-completion",
×
UNCOV
2545
        Hidden: true,
×
2546
        Action: func(c *cli.Context) error {
×
2547
                completion, err := c.App.ToFishCompletion()
×
2548
                if err != nil {
×
2549
                        return err
2550
                }
2551

UNCOV
2552
                // We don't want to suggest files, so we add this
×
UNCOV
2553
                // first line to the completions.
×
2554
                _, err = fmt.Printf("complete -c %q -f \n%s", c.App.Name, completion)
2555
                return err
2556
        },
2557
}
2558

2559
var exportChanBackupCommand = cli.Command{
2560
        Name:     "exportchanbackup",
2561
        Category: "Channels",
2562
        Usage: "Obtain a static channel back up for a selected channels, " +
2563
                "or all known channels.",
2564
        ArgsUsage: "[chan_point] [--all] [--output_file]",
2565
        Description: `
2566
        This command allows a user to export a Static Channel Backup (SCB) for
2567
        a selected channel. SCB's are encrypted backups of a channel's initial
2568
        state that are encrypted with a key derived from the seed of a user. In
2569
        the case of partial or complete data loss, the SCB will allow the user
2570
        to reclaim settled funds in the channel at its final state. The
2571
        exported channel backups can be restored at a later time using the
2572
        restorechanbackup command.
2573

2574
        This command will return one of two types of channel backups depending
2575
        on the set of passed arguments:
2576

2577
           * If a target channel point is specified, then a single channel
2578
             backup containing only the information for that channel will be
2579
             returned.
2580

2581
           * If the --all flag is passed, then a multi-channel backup will be
2582
             returned. A multi backup is a single encrypted blob (displayed in
2583
             hex encoding) that contains several channels in a single cipher
2584
             text.
2585

2586
        Both of the backup types can be restored using the restorechanbackup
2587
        command.
2588
        `,
2589
        Flags: []cli.Flag{
2590
                cli.StringFlag{
2591
                        Name:  "chan_point",
2592
                        Usage: "the target channel to obtain an SCB for",
2593
                },
2594
                cli.BoolFlag{
2595
                        Name: "all",
2596
                        Usage: "if specified, then a multi backup of all " +
2597
                                "active channels will be returned",
2598
                },
2599
                cli.StringFlag{
2600
                        Name: "output_file",
2601
                        Usage: `
2602
                        if specified, then rather than printing a JSON output
2603
                        of the static channel backup, a serialized version of
2604
                        the backup (either Single or Multi) will be written to
2605
                        the target file, this is the same format used by lnd in
2606
                        its channel.backup file `,
2607
                },
2608
        },
2609
        Action: actionDecorator(exportChanBackup),
UNCOV
2610
}
×
UNCOV
2611

×
2612
func exportChanBackup(ctx *cli.Context) error {
×
2613
        ctxc := getContext()
×
2614
        client, cleanUp := getClient(ctx)
×
2615
        defer cleanUp()
×
2616

×
2617
        // Show command help if no arguments provided
×
2618
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2619
                cli.ShowCommandHelp(ctx, "exportchanbackup")
×
2620
                return nil
2621
        }
×
UNCOV
2622

×
2623
        var (
×
2624
                err            error
×
2625
                chanPointStr   string
×
2626
                outputFileName string
×
2627
        )
×
2628
        args := ctx.Args()
×
2629

×
2630
        switch {
×
2631
        case ctx.IsSet("chan_point"):
2632
                chanPointStr = ctx.String("chan_point")
×
UNCOV
2633

×
2634
        case args.Present():
2635
                chanPointStr = args.First()
×
UNCOV
2636

×
2637
        case !ctx.IsSet("all"):
2638
                return fmt.Errorf("must specify chan_point if --all isn't set")
UNCOV
2639
        }
×
UNCOV
2640

×
2641
        if ctx.IsSet("output_file") {
×
2642
                outputFileName = ctx.String("output_file")
2643
        }
×
UNCOV
2644

×
2645
        if chanPointStr != "" {
×
2646
                chanPointRPC, err := parseChanPoint(chanPointStr)
×
2647
                if err != nil {
×
2648
                        return fmt.Errorf("unable to parse chan_point: %w", err)
2649
                }
×
UNCOV
2650

×
2651
                chanBackup, err := client.ExportChannelBackup(
×
2652
                        ctxc, &lnrpc.ExportChannelBackupRequest{
×
2653
                                ChanPoint: chanPointRPC,
×
2654
                        },
×
2655
                )
×
2656
                if err != nil {
×
2657
                        return err
2658
                }
×
UNCOV
2659

×
2660
                txid, err := chainhash.NewHash(
×
2661
                        chanPointRPC.GetFundingTxidBytes(),
×
2662
                )
×
2663
                if err != nil {
×
2664
                        return err
2665
                }
×
UNCOV
2666

×
2667
                chanPoint := wire.OutPoint{
×
2668
                        Hash:  *txid,
×
2669
                        Index: chanPointRPC.OutputIndex,
×
2670
                }
×
2671

×
2672
                if outputFileName != "" {
×
2673
                        return os.WriteFile(
×
2674
                                outputFileName,
×
2675
                                chanBackup.ChanBackup,
×
2676
                                0666,
×
2677
                        )
2678
                }
×
UNCOV
2679

×
2680
                printJSON(struct {
×
2681
                        ChanPoint  string `json:"chan_point"`
×
2682
                        ChanBackup string `json:"chan_backup"`
×
2683
                }{
×
2684
                        ChanPoint:  chanPoint.String(),
×
2685
                        ChanBackup: hex.EncodeToString(chanBackup.ChanBackup),
×
2686
                })
2687
                return nil
UNCOV
2688
        }
×
UNCOV
2689

×
2690
        if !ctx.IsSet("all") {
×
2691
                return fmt.Errorf("if a channel isn't specified, -all must be")
2692
        }
×
UNCOV
2693

×
2694
        chanBackup, err := client.ExportAllChannelBackups(
×
2695
                ctxc, &lnrpc.ChanBackupExportRequest{},
×
2696
        )
×
2697
        if err != nil {
×
2698
                return err
2699
        }
×
UNCOV
2700

×
2701
        if outputFileName != "" {
×
2702
                return os.WriteFile(
×
2703
                        outputFileName,
×
2704
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2705
                        0666,
×
2706
                )
2707
        }
2708

UNCOV
2709
        // TODO(roasbeef): support for export | restore ?
×
UNCOV
2710

×
2711
        var chanPoints []string
×
2712
        for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
×
2713
                txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
×
2714
                if err != nil {
×
2715
                        return err
2716
                }
×
UNCOV
2717

×
2718
                chanPoints = append(chanPoints, wire.OutPoint{
×
2719
                        Hash:  *txid,
×
2720
                        Index: chanPoint.OutputIndex,
2721
                }.String())
UNCOV
2722
        }
×
UNCOV
2723

×
2724
        printRespJSON(chanBackup)
×
2725

2726
        return nil
2727
}
2728

2729
var verifyChanBackupCommand = cli.Command{
2730
        Name:      "verifychanbackup",
2731
        Category:  "Channels",
2732
        Usage:     "Verify an existing channel backup.",
2733
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file]",
2734
        Description: `
2735
    This command allows a user to verify an existing Single or Multi channel
2736
    backup for integrity. This is useful when a user has a backup, but is
2737
    unsure as to if it's valid or for the target node.
2738

2739
    The command will accept backups in one of four forms:
2740

2741
       * A single channel packed SCB, which can be obtained from
2742
         exportchanbackup. This should be passed in hex encoded format.
2743

2744
       * A packed multi-channel SCB, which couples several individual
2745
         static channel backups in single blob.
2746

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

2750
       * A file path which points to a packed multi-channel backup within a
2751
         file, using the same format that lnd does in its channel.backup
2752
         file.
2753
    `,
2754
        Flags: []cli.Flag{
2755
                cli.StringFlag{
2756
                        Name: "single_backup",
2757
                        Usage: "a hex encoded single channel backup obtained " +
2758
                                "from exportchanbackup",
2759
                },
2760
                cli.StringFlag{
2761
                        Name: "multi_backup",
2762
                        Usage: "a hex encoded multi-channel backup obtained " +
2763
                                "from exportchanbackup",
2764
                },
2765

2766
                cli.StringFlag{
2767
                        Name:      "single_file",
2768
                        Usage:     "the path to a single-channel backup file",
2769
                        TakesFile: true,
2770
                },
2771

2772
                cli.StringFlag{
2773
                        Name:      "multi_file",
2774
                        Usage:     "the path to a multi-channel back up file",
2775
                        TakesFile: true,
2776
                },
2777
        },
2778
        Action: actionDecorator(verifyChanBackup),
UNCOV
2779
}
×
UNCOV
2780

×
2781
func verifyChanBackup(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, "verifychanbackup")
×
2789
                return nil
2790
        }
×
UNCOV
2791

×
2792
        backups, err := parseChanBackups(ctx)
×
2793
        if err != nil {
×
2794
                return err
2795
        }
×
UNCOV
2796

×
2797
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
2798

×
2799
        if backups.GetChanBackups() != nil {
×
2800
                verifyReq.SingleChanBackups = backups.GetChanBackups()
×
2801
        }
×
2802
        if backups.GetMultiChanBackup() != nil {
×
2803
                verifyReq.MultiChanBackup = &lnrpc.MultiChanBackup{
×
2804
                        MultiChanBackup: backups.GetMultiChanBackup(),
×
2805
                }
2806
        }
×
UNCOV
2807

×
2808
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
2809
        if err != nil {
×
2810
                return err
2811
        }
×
UNCOV
2812

×
2813
        printRespJSON(resp)
2814
        return nil
2815
}
2816

2817
var restoreChanBackupCommand = cli.Command{
2818
        Name:     "restorechanbackup",
2819
        Category: "Channels",
2820
        Usage: "Restore an existing single or multi-channel static channel " +
2821
                "backup.",
2822
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file=",
2823
        Description: `
2824
        Allows a user to restore a Static Channel Backup (SCB) that was
2825
        obtained either via the exportchanbackup command, or from lnd's
2826
        automatically managed channel.backup file. This command should be used
2827
        if a user is attempting to restore a channel due to data loss on a
2828
        running node restored with the same seed as the node that created the
2829
        channel. If successful, this command will allows the user to recover
2830
        the settled funds stored in the recovered channels.
2831

2832
        The command will accept backups in one of four forms:
2833

2834
           * A single channel packed SCB, which can be obtained from
2835
             exportchanbackup. This should be passed in hex encoded format.
2836

2837
           * A packed multi-channel SCB, which couples several individual
2838
             static channel backups in single blob.
2839

2840
           * A file path which points to a packed single-channel backup within
2841
             a file, using the same format that lnd does in its channel.backup
2842
             file.
2843

2844
           * A file path which points to a packed multi-channel backup within a
2845
             file, using the same format that lnd does in its channel.backup
2846
             file.
2847
        `,
2848
        Flags: []cli.Flag{
2849
                cli.StringFlag{
2850
                        Name: "single_backup",
2851
                        Usage: "a hex encoded single channel backup obtained " +
2852
                                "from exportchanbackup",
2853
                },
2854
                cli.StringFlag{
2855
                        Name: "multi_backup",
2856
                        Usage: "a hex encoded multi-channel backup obtained " +
2857
                                "from exportchanbackup",
2858
                },
2859

2860
                cli.StringFlag{
2861
                        Name:      "single_file",
2862
                        Usage:     "the path to a single-channel backup file",
2863
                        TakesFile: true,
2864
                },
2865

2866
                cli.StringFlag{
2867
                        Name:      "multi_file",
2868
                        Usage:     "the path to a multi-channel back up file",
2869
                        TakesFile: true,
2870
                },
2871
        },
2872
        Action: actionDecorator(restoreChanBackup),
2873
}
2874

2875
// errMissingChanBackup is an error returned when we attempt to parse a channel
2876
// backup from a CLI command, and it is missing.
UNCOV
2877
var errMissingChanBackup = errors.New("missing channel backup")
×
UNCOV
2878

×
2879
func parseChanBackups(ctx *cli.Context) (*lnrpc.RestoreChanBackupRequest, error) {
×
2880
        switch {
×
2881
        case ctx.IsSet("single_backup"):
×
2882
                packedBackup, err := hex.DecodeString(
×
2883
                        ctx.String("single_backup"),
×
2884
                )
×
2885
                if err != nil {
×
2886
                        return nil, fmt.Errorf("unable to decode single packed "+
×
2887
                                "backup: %v", err)
2888
                }
×
UNCOV
2889

×
2890
                return &lnrpc.RestoreChanBackupRequest{
×
2891
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
2892
                                ChanBackups: &lnrpc.ChannelBackups{
×
2893
                                        ChanBackups: []*lnrpc.ChannelBackup{
×
2894
                                                {
×
2895
                                                        ChanBackup: packedBackup,
×
2896
                                                },
×
2897
                                        },
×
2898
                                },
×
2899
                        },
2900
                }, nil
×
UNCOV
2901

×
2902
        case ctx.IsSet("multi_backup"):
×
2903
                packedMulti, err := hex.DecodeString(
×
2904
                        ctx.String("multi_backup"),
×
2905
                )
×
2906
                if err != nil {
×
2907
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
2908
                                "backup: %v", err)
2909
                }
×
UNCOV
2910

×
2911
                return &lnrpc.RestoreChanBackupRequest{
×
2912
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
2913
                                MultiChanBackup: packedMulti,
×
2914
                        },
2915
                }, nil
×
UNCOV
2916

×
2917
        case ctx.IsSet("single_file"):
×
2918
                packedSingle, err := os.ReadFile(ctx.String("single_file"))
×
2919
                if err != nil {
×
2920
                        return nil, fmt.Errorf("unable to decode single "+
×
2921
                                "packed backup: %v", err)
2922
                }
×
UNCOV
2923

×
2924
                return &lnrpc.RestoreChanBackupRequest{
×
2925
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
2926
                                ChanBackups: &lnrpc.ChannelBackups{
×
2927
                                        ChanBackups: []*lnrpc.ChannelBackup{{
×
2928
                                                ChanBackup: packedSingle,
×
2929
                                        }},
×
2930
                                },
×
2931
                        },
2932
                }, nil
×
UNCOV
2933

×
2934
        case ctx.IsSet("multi_file"):
×
2935
                packedMulti, err := os.ReadFile(ctx.String("multi_file"))
×
2936
                if err != nil {
×
2937
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
2938
                                "backup: %v", err)
2939
                }
×
UNCOV
2940

×
2941
                return &lnrpc.RestoreChanBackupRequest{
×
2942
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
2943
                                MultiChanBackup: packedMulti,
×
2944
                        },
2945
                }, nil
×
UNCOV
2946

×
2947
        default:
2948
                return nil, errMissingChanBackup
2949
        }
UNCOV
2950
}
×
UNCOV
2951

×
2952
func restoreChanBackup(ctx *cli.Context) error {
×
2953
        ctxc := getContext()
×
2954
        client, cleanUp := getClient(ctx)
×
2955
        defer cleanUp()
×
2956

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

×
2963
        var req lnrpc.RestoreChanBackupRequest
×
2964

×
2965
        backups, err := parseChanBackups(ctx)
×
2966
        if err != nil {
×
2967
                return err
2968
        }
×
UNCOV
2969

×
2970
        req.Backup = backups.Backup
×
2971

×
2972
        _, err = client.RestoreChannelBackups(ctxc, &req)
×
2973
        if err != nil {
×
2974
                return fmt.Errorf("unable to restore chan backups: %w", err)
2975
        }
×
2976

2977
        return nil
2978
}
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