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

lightningnetwork / lnd / 13225686505

09 Feb 2025 12:33PM UTC coverage: 58.805% (-0.01%) from 58.815%
13225686505

Pull #9491

github

ziggie1984
docs: add release-notes
Pull Request #9491: Allow coop closing a channel with HTLCs on it via lncli

2 of 18 new or added lines in 2 files covered. (11.11%)

78 existing lines in 17 files now uncovered.

136212 of 231635 relevant lines covered (58.8%)

19174.42 hits per line

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

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

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

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

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

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

40
const defaultRecoveryWindow int32 = 2500
41

42
const (
43
        defaultUtxoMinConf = 1
44
)
45

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
225
        // Replace chan_id with scid, and append scid_str and scid fields.
×
226
        jsonBytesReplaced = replaceAndAppendScid(jsonBytesReplaced)
×
227

×
228
        // Append the chan_id field to the JSON.
×
229
        jsonBytesReplaced = appendChanID(jsonBytesReplaced)
×
230

×
231
        fmt.Printf("%s\n", jsonBytesReplaced)
×
232
}
233

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

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

×
254
                                return fmt.Errorf("Wallet is already unlocked")
×
255
                        }
×
256

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

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

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

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

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

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

337
        client, cleanUp := getClient(ctx)
×
338
        defer cleanUp()
×
339

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

348
        printRespJSON(addr)
×
349
        return nil
×
350
}
351

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

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

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

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

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

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

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

399
        client, cleanUp := getClient(ctx)
×
400
        defer cleanUp()
×
401

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

411
        printRespJSON(resp)
×
412
        return nil
×
413
}
414

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

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

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

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

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

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

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

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

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

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

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

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

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

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

583
        if ctx.IsSet("utxo") {
×
584
                utxos := ctx.StringSlice("utxo")
×
585

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

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

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

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

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

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

648
        printRespJSON(txid)
×
649
        return nil
×
650
}
651

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

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

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

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

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

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

732
        unconfirmedOnly := ctx.Bool("unconfirmed_only")
×
733

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

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

747
        client, cleanUp := getClient(ctx)
×
748
        defer cleanUp()
×
749

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

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

772
        printJSON(listUnspentResp)
×
773

×
774
        return nil
×
775
}
776

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

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

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

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

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

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

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

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

850
        client, cleanUp := getClient(ctx)
×
851
        defer cleanUp()
×
852

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

867
        printRespJSON(txid)
×
868
        return nil
×
869
}
870

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

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

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

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

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

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

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

929
        printRespJSON(lnid)
×
930
        return nil
×
931
}
932

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

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

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

964
        req := &lnrpc.DisconnectPeerRequest{
×
965
                PubKey: pubKey,
×
966
        }
×
967

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

973
        printRespJSON(lnid)
×
974
        return nil
×
975
}
976

977
// TODO(roasbeef): also allow short relative channel ID.
978

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

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

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

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

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

1085
func closeChannel(ctx *cli.Context) error {
×
1086
        ctxc := getContext()
×
1087
        client, cleanUp := getClient(ctx)
×
1088
        defer cleanUp()
×
1089

×
1090
        // Show command help if no arguments and flags were provided.
×
1091
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1092
                cli.ShowCommandHelp(ctx, "closechannel")
×
1093
                return nil
×
1094
        }
×
1095

1096
        // Check that only the field sat_per_vbyte or the deprecated field
1097
        // sat_per_byte is used.
1098
        feeRateFlag, err := checkNotBothSet(
×
1099
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1100
        )
×
1101
        if err != nil {
×
1102
                return err
×
1103
        }
×
1104

1105
        channelPoint, err := parseChannelPoint(ctx)
×
1106
        if err != nil {
×
1107
                return err
×
1108
        }
×
1109

1110
        // TODO(roasbeef): implement time deadline within server
1111
        req := &lnrpc.CloseChannelRequest{
×
1112
                ChannelPoint:    channelPoint,
×
1113
                Force:           ctx.Bool("force"),
×
1114
                TargetConf:      int32(ctx.Int64("conf_target")),
×
1115
                SatPerVbyte:     ctx.Uint64(feeRateFlag),
×
1116
                DeliveryAddress: ctx.String("delivery_addr"),
×
1117
                MaxFeePerVbyte:  ctx.Uint64("max_fee_rate"),
×
NEW
1118
                NoWait:          ctx.Bool("coop_close_with_htlcs"),
×
1119
        }
×
1120

×
1121
        // After parsing the request, we'll spin up a goroutine that will
×
1122
        // retrieve the closing transaction ID when attempting to close the
×
1123
        // channel. We do this to because `executeChannelClose` can block, so we
×
1124
        // would like to present the closing transaction ID to the user as soon
×
1125
        // as it is broadcasted.
×
1126
        var wg sync.WaitGroup
×
1127
        txidChan := make(chan string, 1)
×
1128

×
1129
        wg.Add(1)
×
1130
        go func() {
×
1131
                defer wg.Done()
×
1132

×
1133
                printJSON(struct {
×
1134
                        ClosingTxid string `json:"closing_txid"`
×
1135
                }{
×
1136
                        ClosingTxid: <-txidChan,
×
1137
                })
×
1138
        }()
×
1139

1140
        err = executeChannelClose(ctxc, client, req, txidChan, ctx.Bool("block"))
×
1141
        if err != nil {
×
1142
                return err
×
1143
        }
×
1144

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

×
1150
        return nil
×
1151
}
1152

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

×
1160
        stream, err := client.CloseChannel(ctxc, req)
×
1161
        if err != nil {
×
1162
                return err
×
1163
        }
×
1164

1165
        for {
×
1166
                resp, err := stream.Recv()
×
1167
                if err == io.EOF {
×
1168
                        return nil
×
1169
                } else if err != nil {
×
1170
                        return err
×
1171
                }
×
1172

1173
                switch update := resp.Update.(type) {
×
1174
                case *lnrpc.CloseStatusUpdate_CloseInstant:
×
NEW
1175
                        fmt.Println("Channel close successfully initiated " +
×
NEW
1176
                                "(potentially waiting for HTLCs to be " +
×
NEW
1177
                                "resolved), waiting for close tx to be " +
×
NEW
1178
                                "broadcasted ...")
×
1179

1180
                case *lnrpc.CloseStatusUpdate_ClosePending:
×
1181
                        closingHash := update.ClosePending.Txid
×
1182
                        txid, err := chainhash.NewHash(closingHash)
×
1183
                        if err != nil {
×
1184
                                return err
×
1185
                        }
×
1186

NEW
1187
                        fmt.Println("Channel close transaction broadcasted")
×
NEW
1188

×
1189
                        txidChan <- txid.String()
×
1190

×
1191
                        if !block {
×
1192
                                return nil
×
1193
                        }
×
1194

NEW
1195
                        fmt.Println("Waiting for channel close " +
×
NEW
1196
                                "confirmation ...")
×
1197

1198
                case *lnrpc.CloseStatusUpdate_ChanClose:
×
NEW
1199
                        fmt.Println("Channel close successfully confirmed")
×
NEW
1200

×
UNCOV
1201
                        return nil
×
1202
                }
1203
        }
1204
}
1205

1206
var closeAllChannelsCommand = cli.Command{
1207
        Name:     "closeallchannels",
1208
        Category: "Channels",
1209
        Usage:    "Close all existing channels.",
1210
        Description: `
1211
        Close all existing channels.
1212

1213
        Channels will be closed either cooperatively or unilaterally, depending
1214
        on whether the channel is active or not. If the channel is inactive, any
1215
        settled funds within it will be time locked for a few blocks before they
1216
        can be spent.
1217

1218
        One can request to close inactive channels only by using the
1219
        --inactive_only flag.
1220

1221
        By default, one is prompted for confirmation every time an inactive
1222
        channel is requested to be closed. To avoid this, one can set the
1223
        --force flag, which will only prompt for confirmation once for all
1224
        inactive channels and proceed to close them.
1225

1226
        In the case of cooperative closures, one can manually set the fee to
1227
        be used for the closing transactions via either the --conf_target or
1228
        --sat_per_vbyte arguments. This will be the starting value used during
1229
        fee negotiation. This is optional.`,
1230
        Flags: []cli.Flag{
1231
                cli.BoolFlag{
1232
                        Name:  "inactive_only",
1233
                        Usage: "close inactive channels only",
1234
                },
1235
                cli.BoolFlag{
1236
                        Name: "force",
1237
                        Usage: "ask for confirmation once before attempting " +
1238
                                "to close existing channels",
1239
                },
1240
                cli.Int64Flag{
1241
                        Name: "conf_target",
1242
                        Usage: "(optional) the number of blocks that the " +
1243
                                "closing transactions *should* confirm in, will be " +
1244
                                "used for fee estimation",
1245
                },
1246
                cli.Int64Flag{
1247
                        Name:   "sat_per_byte",
1248
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
1249
                        Hidden: true,
1250
                },
1251
                cli.Int64Flag{
1252
                        Name: "sat_per_vbyte",
1253
                        Usage: "(optional) a manual fee expressed in " +
1254
                                "sat/vbyte that should be used when crafting " +
1255
                                "the closing transactions",
1256
                },
1257
                cli.BoolFlag{
1258
                        Name: "s, skip_confirmation",
1259
                        Usage: "Skip the confirmation prompt and close all " +
1260
                                "channels immediately",
1261
                },
1262
        },
1263
        Action: actionDecorator(closeAllChannels),
1264
}
1265

1266
func closeAllChannels(ctx *cli.Context) error {
×
1267
        ctxc := getContext()
×
1268
        client, cleanUp := getClient(ctx)
×
1269
        defer cleanUp()
×
1270

×
1271
        // Check that only the field sat_per_vbyte or the deprecated field
×
1272
        // sat_per_byte is used.
×
1273
        feeRateFlag, err := checkNotBothSet(
×
1274
                ctx, "sat_per_vbyte", "sat_per_byte",
×
1275
        )
×
1276
        if err != nil {
×
1277
                return err
×
1278
        }
×
1279

1280
        prompt := "Do you really want to close ALL channels? (yes/no): "
×
1281
        if !ctx.Bool("skip_confirmation") && !promptForConfirmation(prompt) {
×
1282
                return errors.New("action aborted by user")
×
1283
        }
×
1284

1285
        listReq := &lnrpc.ListChannelsRequest{}
×
1286
        openChannels, err := client.ListChannels(ctxc, listReq)
×
1287
        if err != nil {
×
1288
                return fmt.Errorf("unable to fetch open channels: %w", err)
×
1289
        }
×
1290

1291
        if len(openChannels.Channels) == 0 {
×
1292
                return errors.New("no open channels to close")
×
1293
        }
×
1294

1295
        var channelsToClose []*lnrpc.Channel
×
1296

×
1297
        switch {
×
1298
        case ctx.Bool("force") && ctx.Bool("inactive_only"):
×
1299
                msg := "Unilaterally close all inactive channels? The funds " +
×
1300
                        "within these channels will be locked for some blocks " +
×
1301
                        "(CSV delay) before they can be spent. (yes/no): "
×
1302

×
1303
                confirmed := promptForConfirmation(msg)
×
1304

×
1305
                // We can safely exit if the user did not confirm.
×
1306
                if !confirmed {
×
1307
                        return nil
×
1308
                }
×
1309

1310
                // Go through the list of open channels and only add inactive
1311
                // channels to the closing list.
1312
                for _, channel := range openChannels.Channels {
×
1313
                        if !channel.GetActive() {
×
1314
                                channelsToClose = append(
×
1315
                                        channelsToClose, channel,
×
1316
                                )
×
1317
                        }
×
1318
                }
1319
        case ctx.Bool("force"):
×
1320
                msg := "Close all active and inactive channels? Inactive " +
×
1321
                        "channels will be closed unilaterally, so funds " +
×
1322
                        "within them will be locked for a few blocks (CSV " +
×
1323
                        "delay) before they can be spent. (yes/no): "
×
1324

×
1325
                confirmed := promptForConfirmation(msg)
×
1326

×
1327
                // We can safely exit if the user did not confirm.
×
1328
                if !confirmed {
×
1329
                        return nil
×
1330
                }
×
1331

1332
                channelsToClose = openChannels.Channels
×
1333
        default:
×
1334
                // Go through the list of open channels and determine which
×
1335
                // should be added to the closing list.
×
1336
                for _, channel := range openChannels.Channels {
×
1337
                        // If the channel is inactive, we'll attempt to
×
1338
                        // unilaterally close the channel, so we should prompt
×
1339
                        // the user for confirmation beforehand.
×
1340
                        if !channel.GetActive() {
×
1341
                                msg := fmt.Sprintf("Unilaterally close channel "+
×
1342
                                        "with node %s and channel point %s? "+
×
1343
                                        "The closing transaction will need %d "+
×
1344
                                        "confirmations before the funds can be "+
×
1345
                                        "spent. (yes/no): ", channel.RemotePubkey,
×
1346
                                        channel.ChannelPoint, channel.LocalConstraints.CsvDelay)
×
1347

×
1348
                                confirmed := promptForConfirmation(msg)
×
1349

×
1350
                                if confirmed {
×
1351
                                        channelsToClose = append(
×
1352
                                                channelsToClose, channel,
×
1353
                                        )
×
1354
                                }
×
1355
                        } else if !ctx.Bool("inactive_only") {
×
1356
                                // Otherwise, we'll only add active channels if
×
1357
                                // we were not requested to close inactive
×
1358
                                // channels only.
×
1359
                                channelsToClose = append(
×
1360
                                        channelsToClose, channel,
×
1361
                                )
×
1362
                        }
×
1363
                }
1364
        }
1365

1366
        // result defines the result of closing a channel. The closing
1367
        // transaction ID is populated if a channel is successfully closed.
1368
        // Otherwise, the error that prevented closing the channel is populated.
1369
        type result struct {
×
1370
                RemotePubKey string `json:"remote_pub_key"`
×
1371
                ChannelPoint string `json:"channel_point"`
×
1372
                ClosingTxid  string `json:"closing_txid"`
×
1373
                FailErr      string `json:"error"`
×
1374
        }
×
1375

×
1376
        // Launch each channel closure in a goroutine in order to execute them
×
1377
        // in parallel. Once they're all executed, we will print the results as
×
1378
        // they come.
×
1379
        resultChan := make(chan result, len(channelsToClose))
×
1380
        for _, channel := range channelsToClose {
×
1381
                go func(channel *lnrpc.Channel) {
×
1382
                        res := result{}
×
1383
                        res.RemotePubKey = channel.RemotePubkey
×
1384
                        res.ChannelPoint = channel.ChannelPoint
×
1385
                        defer func() {
×
1386
                                resultChan <- res
×
1387
                        }()
×
1388

1389
                        // Parse the channel point in order to create the close
1390
                        // channel request.
1391
                        s := strings.Split(res.ChannelPoint, ":")
×
1392
                        if len(s) != 2 {
×
1393
                                res.FailErr = "expected channel point with " +
×
1394
                                        "format txid:index"
×
1395
                                return
×
1396
                        }
×
1397
                        index, err := strconv.ParseUint(s[1], 10, 32)
×
1398
                        if err != nil {
×
1399
                                res.FailErr = fmt.Sprintf("unable to parse "+
×
1400
                                        "channel point output index: %v", err)
×
1401
                                return
×
1402
                        }
×
1403

1404
                        req := &lnrpc.CloseChannelRequest{
×
1405
                                ChannelPoint: &lnrpc.ChannelPoint{
×
1406
                                        FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
×
1407
                                                FundingTxidStr: s[0],
×
1408
                                        },
×
1409
                                        OutputIndex: uint32(index),
×
1410
                                },
×
1411
                                Force:       !channel.GetActive(),
×
1412
                                TargetConf:  int32(ctx.Int64("conf_target")),
×
1413
                                SatPerVbyte: ctx.Uint64(feeRateFlag),
×
1414
                        }
×
1415

×
1416
                        txidChan := make(chan string, 1)
×
1417
                        err = executeChannelClose(ctxc, client, req, txidChan, false)
×
1418
                        if err != nil {
×
1419
                                res.FailErr = fmt.Sprintf("unable to close "+
×
1420
                                        "channel: %v", err)
×
1421
                                return
×
1422
                        }
×
1423

1424
                        res.ClosingTxid = <-txidChan
×
1425
                }(channel)
1426
        }
1427

1428
        for range channelsToClose {
×
1429
                res := <-resultChan
×
1430
                printJSON(res)
×
1431
        }
×
1432

1433
        return nil
×
1434
}
1435

1436
// promptForConfirmation continuously prompts the user for the message until
1437
// receiving a response of "yes" or "no" and returns their answer as a bool.
1438
func promptForConfirmation(msg string) bool {
×
1439
        reader := bufio.NewReader(os.Stdin)
×
1440

×
1441
        for {
×
1442
                fmt.Print(msg)
×
1443

×
1444
                answer, err := reader.ReadString('\n')
×
1445
                if err != nil {
×
1446
                        return false
×
1447
                }
×
1448

1449
                answer = strings.ToLower(strings.TrimSpace(answer))
×
1450

×
1451
                switch {
×
1452
                case answer == "yes":
×
1453
                        return true
×
1454
                case answer == "no":
×
1455
                        return false
×
1456
                default:
×
1457
                        continue
×
1458
                }
1459
        }
1460
}
1461

1462
var abandonChannelCommand = cli.Command{
1463
        Name:     "abandonchannel",
1464
        Category: "Channels",
1465
        Usage:    "Abandons an existing channel.",
1466
        Description: `
1467
        Removes all channel state from the database except for a close
1468
        summary. This method can be used to get rid of permanently unusable
1469
        channels due to bugs fixed in newer versions of lnd.
1470

1471
        Only available when lnd is built in debug mode. The flag
1472
        --i_know_what_i_am_doing can be set to override the debug/dev mode
1473
        requirement.
1474

1475
        To view which funding_txids/output_indexes can be used for this command,
1476
        see the channel_point values within the listchannels command output.
1477
        The format for a channel_point is 'funding_txid:output_index'.`,
1478
        ArgsUsage: "funding_txid [output_index]",
1479
        Flags: []cli.Flag{
1480
                cli.StringFlag{
1481
                        Name:  "funding_txid",
1482
                        Usage: "the txid of the channel's funding transaction",
1483
                },
1484
                cli.IntFlag{
1485
                        Name: "output_index",
1486
                        Usage: "the output index for the funding output of the funding " +
1487
                                "transaction",
1488
                },
1489
                cli.StringFlag{
1490
                        Name: "chan_point",
1491
                        Usage: "(optional) the channel point. If set, " +
1492
                                "funding_txid and output_index flags and " +
1493
                                "positional arguments will be ignored",
1494
                },
1495
                cli.BoolFlag{
1496
                        Name: "i_know_what_i_am_doing",
1497
                        Usage: "override the requirement for lnd needing to " +
1498
                                "be in dev/debug mode to use this command; " +
1499
                                "when setting this the user attests that " +
1500
                                "they know the danger of using this command " +
1501
                                "on channels and that doing so can lead to " +
1502
                                "loss of funds if the channel funding TX " +
1503
                                "ever confirms (or was confirmed)",
1504
                },
1505
        },
1506
        Action: actionDecorator(abandonChannel),
1507
}
1508

1509
func abandonChannel(ctx *cli.Context) error {
×
1510
        ctxc := getContext()
×
1511
        client, cleanUp := getClient(ctx)
×
1512
        defer cleanUp()
×
1513

×
1514
        // Show command help if no arguments and flags were provided.
×
1515
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1516
                cli.ShowCommandHelp(ctx, "abandonchannel")
×
1517
                return nil
×
1518
        }
×
1519

1520
        channelPoint, err := parseChannelPoint(ctx)
×
1521
        if err != nil {
×
1522
                return err
×
1523
        }
×
1524

1525
        req := &lnrpc.AbandonChannelRequest{
×
1526
                ChannelPoint:      channelPoint,
×
1527
                IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
×
1528
        }
×
1529

×
1530
        resp, err := client.AbandonChannel(ctxc, req)
×
1531
        if err != nil {
×
1532
                return err
×
1533
        }
×
1534

1535
        printRespJSON(resp)
×
1536
        return nil
×
1537
}
1538

1539
// parseChannelPoint parses a funding txid and output index from the command
1540
// line. Both named options and unnamed parameters are supported.
1541
func parseChannelPoint(ctx *cli.Context) (*lnrpc.ChannelPoint, error) {
×
1542
        channelPoint := &lnrpc.ChannelPoint{}
×
1543
        var err error
×
1544

×
1545
        args := ctx.Args()
×
1546

×
1547
        switch {
×
1548
        case ctx.IsSet("chan_point"):
×
1549
                channelPoint, err = parseChanPoint(ctx.String("chan_point"))
×
1550
                if err != nil {
×
1551
                        return nil, fmt.Errorf("unable to parse chan_point: "+
×
1552
                                "%v", err)
×
1553
                }
×
1554
                return channelPoint, nil
×
1555

1556
        case ctx.IsSet("funding_txid"):
×
1557
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1558
                        FundingTxidStr: ctx.String("funding_txid"),
×
1559
                }
×
1560
        case args.Present():
×
1561
                channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
×
1562
                        FundingTxidStr: args.First(),
×
1563
                }
×
1564
                args = args.Tail()
×
1565
        default:
×
1566
                return nil, fmt.Errorf("funding txid argument missing")
×
1567
        }
1568

1569
        switch {
×
1570
        case ctx.IsSet("output_index"):
×
1571
                channelPoint.OutputIndex = uint32(ctx.Int("output_index"))
×
1572
        case args.Present():
×
1573
                index, err := strconv.ParseUint(args.First(), 10, 32)
×
1574
                if err != nil {
×
1575
                        return nil, fmt.Errorf("unable to decode output "+
×
1576
                                "index: %w", err)
×
1577
                }
×
1578
                channelPoint.OutputIndex = uint32(index)
×
1579
        default:
×
1580
                channelPoint.OutputIndex = 0
×
1581
        }
1582

1583
        return channelPoint, nil
×
1584
}
1585

1586
var listPeersCommand = cli.Command{
1587
        Name:     "listpeers",
1588
        Category: "Peers",
1589
        Usage:    "List all active, currently connected peers.",
1590
        Flags: []cli.Flag{
1591
                cli.BoolFlag{
1592
                        Name:  "list_errors",
1593
                        Usage: "list a full set of most recent errors for the peer",
1594
                },
1595
        },
1596
        Action: actionDecorator(listPeers),
1597
}
1598

1599
func listPeers(ctx *cli.Context) error {
×
1600
        ctxc := getContext()
×
1601
        client, cleanUp := getClient(ctx)
×
1602
        defer cleanUp()
×
1603

×
1604
        // By default, we display a single error on the cli. If the user
×
1605
        // specifically requests a full error set, then we will provide it.
×
1606
        req := &lnrpc.ListPeersRequest{
×
1607
                LatestError: !ctx.IsSet("list_errors"),
×
1608
        }
×
1609
        resp, err := client.ListPeers(ctxc, req)
×
1610
        if err != nil {
×
1611
                return err
×
1612
        }
×
1613

1614
        printRespJSON(resp)
×
1615
        return nil
×
1616
}
1617

1618
var walletBalanceCommand = cli.Command{
1619
        Name:     "walletbalance",
1620
        Category: "Wallet",
1621
        Usage:    "Compute and display the wallet's current balance.",
1622
        Flags: []cli.Flag{
1623
                cli.StringFlag{
1624
                        Name: "account",
1625
                        Usage: "(optional) the account for which the balance " +
1626
                                "is shown",
1627
                        Value: "",
1628
                },
1629
        },
1630
        Action: actionDecorator(walletBalance),
1631
}
1632

1633
func walletBalance(ctx *cli.Context) error {
×
1634
        ctxc := getContext()
×
1635
        client, cleanUp := getClient(ctx)
×
1636
        defer cleanUp()
×
1637

×
1638
        req := &lnrpc.WalletBalanceRequest{
×
1639
                Account: ctx.String("account"),
×
1640
        }
×
1641
        resp, err := client.WalletBalance(ctxc, req)
×
1642
        if err != nil {
×
1643
                return err
×
1644
        }
×
1645

1646
        printRespJSON(resp)
×
1647
        return nil
×
1648
}
1649

1650
var ChannelBalanceCommand = cli.Command{
1651
        Name:     "channelbalance",
1652
        Category: "Channels",
1653
        Usage: "Returns the sum of the total available channel balance across " +
1654
                "all open channels.",
1655
        Action: actionDecorator(ChannelBalance),
1656
}
1657

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

×
1663
        req := &lnrpc.ChannelBalanceRequest{}
×
1664
        resp, err := client.ChannelBalance(ctxc, req)
×
1665
        if err != nil {
×
1666
                return err
×
1667
        }
×
1668

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

1673
var generateManPageCommand = cli.Command{
1674
        Name: "generatemanpage",
1675
        Usage: "Generates a man page for lncli and lnd as " +
1676
                "lncli.1 and lnd.1 respectively.",
1677
        Hidden: true,
1678
        Action: actionDecorator(generateManPage),
1679
}
1680

1681
func generateManPage(ctx *cli.Context) error {
×
1682
        // Generate the man pages for lncli as lncli.1.
×
1683
        manpages, err := ctx.App.ToMan()
×
1684
        if err != nil {
×
1685
                return err
×
1686
        }
×
1687
        err = os.WriteFile("lncli.1", []byte(manpages), 0644)
×
1688
        if err != nil {
×
1689
                return err
×
1690
        }
×
1691

1692
        // Generate the man pages for lnd as lnd.1.
1693
        config := lnd.DefaultConfig()
×
1694
        fileParser := flags.NewParser(&config, flags.Default)
×
1695
        fileParser.Name = "lnd"
×
1696

×
1697
        var buf bytes.Buffer
×
1698
        fileParser.WriteManPage(&buf)
×
1699

×
1700
        err = os.WriteFile("lnd.1", buf.Bytes(), 0644)
×
1701
        if err != nil {
×
1702
                return err
×
1703
        }
×
1704

1705
        return nil
×
1706
}
1707

1708
var getInfoCommand = cli.Command{
1709
        Name:   "getinfo",
1710
        Usage:  "Returns basic information related to the active daemon.",
1711
        Action: actionDecorator(getInfo),
1712
}
1713

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

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

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

1729
var getRecoveryInfoCommand = cli.Command{
1730
        Name:   "getrecoveryinfo",
1731
        Usage:  "Display information about an ongoing recovery attempt.",
1732
        Action: actionDecorator(getRecoveryInfo),
1733
}
1734

1735
func getRecoveryInfo(ctx *cli.Context) error {
×
1736
        ctxc := getContext()
×
1737
        client, cleanUp := getClient(ctx)
×
1738
        defer cleanUp()
×
1739

×
1740
        req := &lnrpc.GetRecoveryInfoRequest{}
×
1741
        resp, err := client.GetRecoveryInfo(ctxc, req)
×
1742
        if err != nil {
×
1743
                return err
×
1744
        }
×
1745

1746
        printRespJSON(resp)
×
1747
        return nil
×
1748
}
1749

1750
var pendingChannelsCommand = cli.Command{
1751
        Name:     "pendingchannels",
1752
        Category: "Channels",
1753
        Usage:    "Display information pertaining to pending channels.",
1754
        Flags: []cli.Flag{
1755
                cli.BoolFlag{
1756
                        Name: "include_raw_tx",
1757
                        Usage: "include the raw transaction hex for " +
1758
                                "waiting_close_channels.",
1759
                },
1760
        },
1761
        Action: actionDecorator(pendingChannels),
1762
}
1763

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

×
1769
        includeRawTx := ctx.Bool("include_raw_tx")
×
1770
        req := &lnrpc.PendingChannelsRequest{
×
1771
                IncludeRawTx: includeRawTx,
×
1772
        }
×
1773
        resp, err := client.PendingChannels(ctxc, req)
×
1774
        if err != nil {
×
1775
                return err
×
1776
        }
×
1777

1778
        printRespJSON(resp)
×
1779

×
1780
        return nil
×
1781
}
1782

1783
var ListChannelsCommand = cli.Command{
1784
        Name:     "listchannels",
1785
        Category: "Channels",
1786
        Usage:    "List all open channels.",
1787
        Flags: []cli.Flag{
1788
                cli.BoolFlag{
1789
                        Name:  "active_only",
1790
                        Usage: "only list channels which are currently active",
1791
                },
1792
                cli.BoolFlag{
1793
                        Name:  "inactive_only",
1794
                        Usage: "only list channels which are currently inactive",
1795
                },
1796
                cli.BoolFlag{
1797
                        Name:  "public_only",
1798
                        Usage: "only list channels which are currently public",
1799
                },
1800
                cli.BoolFlag{
1801
                        Name:  "private_only",
1802
                        Usage: "only list channels which are currently private",
1803
                },
1804
                cli.StringFlag{
1805
                        Name: "peer",
1806
                        Usage: "(optional) only display channels with a " +
1807
                                "particular peer, accepts 66-byte, " +
1808
                                "hex-encoded pubkeys",
1809
                },
1810
                cli.BoolFlag{
1811
                        Name: "skip_peer_alias_lookup",
1812
                        Usage: "skip the peer alias lookup per channel in " +
1813
                                "order to improve performance",
1814
                },
1815
        },
1816
        Action: actionDecorator(ListChannels),
1817
}
1818

1819
var listAliasesCommand = cli.Command{
1820
        Name:     "listaliases",
1821
        Category: "Channels",
1822
        Usage:    "List all aliases.",
1823
        Flags:    []cli.Flag{},
1824
        Action:   actionDecorator(listAliases),
1825
}
1826

1827
func listAliases(ctx *cli.Context) error {
×
1828
        ctxc := getContext()
×
1829
        client, cleanUp := getClient(ctx)
×
1830
        defer cleanUp()
×
1831

×
1832
        req := &lnrpc.ListAliasesRequest{}
×
1833

×
1834
        resp, err := client.ListAliases(ctxc, req)
×
1835
        if err != nil {
×
1836
                return err
×
1837
        }
×
1838

1839
        printRespJSON(resp)
×
1840

×
1841
        return nil
×
1842
}
1843

1844
func ListChannels(ctx *cli.Context) error {
×
1845
        ctxc := getContext()
×
1846
        client, cleanUp := getClient(ctx)
×
1847
        defer cleanUp()
×
1848

×
1849
        peer := ctx.String("peer")
×
1850

×
1851
        // If the user requested channels with a particular key, parse the
×
1852
        // provided pubkey.
×
1853
        var peerKey []byte
×
1854
        if len(peer) > 0 {
×
1855
                pk, err := route.NewVertexFromStr(peer)
×
1856
                if err != nil {
×
1857
                        return fmt.Errorf("invalid --peer pubkey: %w", err)
×
1858
                }
×
1859

1860
                peerKey = pk[:]
×
1861
        }
1862

1863
        // By default, we will look up the peers' alias information unless the
1864
        // skip_peer_alias_lookup flag indicates otherwise.
1865
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1866

×
1867
        req := &lnrpc.ListChannelsRequest{
×
1868
                ActiveOnly:      ctx.Bool("active_only"),
×
1869
                InactiveOnly:    ctx.Bool("inactive_only"),
×
1870
                PublicOnly:      ctx.Bool("public_only"),
×
1871
                PrivateOnly:     ctx.Bool("private_only"),
×
1872
                Peer:            peerKey,
×
1873
                PeerAliasLookup: lookupPeerAlias,
×
1874
        }
×
1875

×
1876
        resp, err := client.ListChannels(ctxc, req)
×
1877
        if err != nil {
×
1878
                return err
×
1879
        }
×
1880

1881
        printRespJSON(resp)
×
1882

×
1883
        return nil
×
1884
}
1885

1886
var closedChannelsCommand = cli.Command{
1887
        Name:     "closedchannels",
1888
        Category: "Channels",
1889
        Usage:    "List all closed channels.",
1890
        Flags: []cli.Flag{
1891
                cli.BoolFlag{
1892
                        Name:  "cooperative",
1893
                        Usage: "list channels that were closed cooperatively",
1894
                },
1895
                cli.BoolFlag{
1896
                        Name: "local_force",
1897
                        Usage: "list channels that were force-closed " +
1898
                                "by the local node",
1899
                },
1900
                cli.BoolFlag{
1901
                        Name: "remote_force",
1902
                        Usage: "list channels that were force-closed " +
1903
                                "by the remote node",
1904
                },
1905
                cli.BoolFlag{
1906
                        Name: "breach",
1907
                        Usage: "list channels for which the remote node " +
1908
                                "attempted to broadcast a prior " +
1909
                                "revoked channel state",
1910
                },
1911
                cli.BoolFlag{
1912
                        Name:  "funding_canceled",
1913
                        Usage: "list channels that were never fully opened",
1914
                },
1915
                cli.BoolFlag{
1916
                        Name: "abandoned",
1917
                        Usage: "list channels that were abandoned by " +
1918
                                "the local node",
1919
                },
1920
        },
1921
        Action: actionDecorator(closedChannels),
1922
}
1923

1924
func closedChannels(ctx *cli.Context) error {
×
1925
        ctxc := getContext()
×
1926
        client, cleanUp := getClient(ctx)
×
1927
        defer cleanUp()
×
1928

×
1929
        req := &lnrpc.ClosedChannelsRequest{
×
1930
                Cooperative:     ctx.Bool("cooperative"),
×
1931
                LocalForce:      ctx.Bool("local_force"),
×
1932
                RemoteForce:     ctx.Bool("remote_force"),
×
1933
                Breach:          ctx.Bool("breach"),
×
1934
                FundingCanceled: ctx.Bool("funding_canceled"),
×
1935
                Abandoned:       ctx.Bool("abandoned"),
×
1936
        }
×
1937

×
1938
        resp, err := client.ClosedChannels(ctxc, req)
×
1939
        if err != nil {
×
1940
                return err
×
1941
        }
×
1942

1943
        printRespJSON(resp)
×
1944

×
1945
        return nil
×
1946
}
1947

1948
var describeGraphCommand = cli.Command{
1949
        Name:     "describegraph",
1950
        Category: "Graph",
1951
        Description: "Prints a human readable version of the known channel " +
1952
                "graph from the PoV of the node",
1953
        Usage: "Describe the network graph.",
1954
        Flags: []cli.Flag{
1955
                cli.BoolFlag{
1956
                        Name: "include_unannounced",
1957
                        Usage: "If set, unannounced channels will be included in the " +
1958
                                "graph. Unannounced channels are both private channels, and " +
1959
                                "public channels that are not yet announced to the network.",
1960
                },
1961
        },
1962
        Action: actionDecorator(describeGraph),
1963
}
1964

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

×
1970
        req := &lnrpc.ChannelGraphRequest{
×
1971
                IncludeUnannounced: ctx.Bool("include_unannounced"),
×
1972
        }
×
1973

×
1974
        graph, err := client.DescribeGraph(ctxc, req)
×
1975
        if err != nil {
×
1976
                return err
×
1977
        }
×
1978

1979
        printRespJSON(graph)
×
1980
        return nil
×
1981
}
1982

1983
var getNodeMetricsCommand = cli.Command{
1984
        Name:        "getnodemetrics",
1985
        Category:    "Graph",
1986
        Description: "Prints out node metrics calculated from the current graph",
1987
        Usage:       "Get node metrics.",
1988
        Action:      actionDecorator(getNodeMetrics),
1989
}
1990

1991
func getNodeMetrics(ctx *cli.Context) error {
×
1992
        ctxc := getContext()
×
1993
        client, cleanUp := getClient(ctx)
×
1994
        defer cleanUp()
×
1995

×
1996
        req := &lnrpc.NodeMetricsRequest{
×
1997
                Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
×
1998
        }
×
1999

×
2000
        nodeMetrics, err := client.GetNodeMetrics(ctxc, req)
×
2001
        if err != nil {
×
2002
                return err
×
2003
        }
×
2004

2005
        printRespJSON(nodeMetrics)
×
2006
        return nil
×
2007
}
2008

2009
var getChanInfoCommand = cli.Command{
2010
        Name:     "getchaninfo",
2011
        Category: "Graph",
2012
        Usage:    "Get the state of a channel.",
2013
        Description: "Prints out the latest authenticated state for a " +
2014
                "particular channel",
2015
        ArgsUsage: "chan_id",
2016
        Flags: []cli.Flag{
2017
                cli.Uint64Flag{
2018
                        Name: "chan_id",
2019
                        Usage: "The 8-byte compact channel ID to query for. " +
2020
                                "If this is set the chan_point param is " +
2021
                                "ignored.",
2022
                },
2023
                cli.StringFlag{
2024
                        Name: "chan_point",
2025
                        Usage: "The channel point in format txid:index. If " +
2026
                                "the chan_id param is set this param is " +
2027
                                "ignored.",
2028
                },
2029
        },
2030
        Action: actionDecorator(getChanInfo),
2031
}
2032

2033
func getChanInfo(ctx *cli.Context) error {
×
2034
        ctxc := getContext()
×
2035
        client, cleanUp := getClient(ctx)
×
2036
        defer cleanUp()
×
2037

×
2038
        var (
×
2039
                chanID    uint64
×
2040
                chanPoint string
×
2041
                err       error
×
2042
        )
×
2043

×
2044
        switch {
×
2045
        case ctx.IsSet("chan_id"):
×
2046
                chanID = ctx.Uint64("chan_id")
×
2047

2048
        case ctx.Args().Present():
×
2049
                chanID, err = strconv.ParseUint(ctx.Args().First(), 10, 64)
×
2050
                if err != nil {
×
2051
                        return fmt.Errorf("error parsing chan_id: %w", err)
×
2052
                }
×
2053

2054
        case ctx.IsSet("chan_point"):
×
2055
                chanPoint = ctx.String("chan_point")
×
2056

2057
        default:
×
2058
                return fmt.Errorf("chan_id or chan_point argument missing")
×
2059
        }
2060

2061
        req := &lnrpc.ChanInfoRequest{
×
2062
                ChanId:    chanID,
×
2063
                ChanPoint: chanPoint,
×
2064
        }
×
2065

×
2066
        chanInfo, err := client.GetChanInfo(ctxc, req)
×
2067
        if err != nil {
×
2068
                return err
×
2069
        }
×
2070

2071
        printRespJSON(chanInfo)
×
2072
        return nil
×
2073
}
2074

2075
var getNodeInfoCommand = cli.Command{
2076
        Name:     "getnodeinfo",
2077
        Category: "Graph",
2078
        Usage:    "Get information on a specific node.",
2079
        Description: "Prints out the latest authenticated node state for an " +
2080
                "advertised node",
2081
        Flags: []cli.Flag{
2082
                cli.StringFlag{
2083
                        Name: "pub_key",
2084
                        Usage: "the 33-byte hex-encoded compressed public of the target " +
2085
                                "node",
2086
                },
2087
                cli.BoolFlag{
2088
                        Name: "include_channels",
2089
                        Usage: "if true, will return all known channels " +
2090
                                "associated with the node",
2091
                },
2092
        },
2093
        Action: actionDecorator(getNodeInfo),
2094
}
2095

2096
func getNodeInfo(ctx *cli.Context) error {
×
2097
        ctxc := getContext()
×
2098
        client, cleanUp := getClient(ctx)
×
2099
        defer cleanUp()
×
2100

×
2101
        args := ctx.Args()
×
2102

×
2103
        var pubKey string
×
2104
        switch {
×
2105
        case ctx.IsSet("pub_key"):
×
2106
                pubKey = ctx.String("pub_key")
×
2107
        case args.Present():
×
2108
                pubKey = args.First()
×
2109
        default:
×
2110
                return fmt.Errorf("pub_key argument missing")
×
2111
        }
2112

2113
        req := &lnrpc.NodeInfoRequest{
×
2114
                PubKey:          pubKey,
×
2115
                IncludeChannels: ctx.Bool("include_channels"),
×
2116
        }
×
2117

×
2118
        nodeInfo, err := client.GetNodeInfo(ctxc, req)
×
2119
        if err != nil {
×
2120
                return err
×
2121
        }
×
2122

2123
        printRespJSON(nodeInfo)
×
2124
        return nil
×
2125
}
2126

2127
var getNetworkInfoCommand = cli.Command{
2128
        Name:     "getnetworkinfo",
2129
        Category: "Channels",
2130
        Usage: "Get statistical information about the current " +
2131
                "state of the network.",
2132
        Description: "Returns a set of statistics pertaining to the known " +
2133
                "channel graph",
2134
        Action: actionDecorator(getNetworkInfo),
2135
}
2136

2137
func getNetworkInfo(ctx *cli.Context) error {
×
2138
        ctxc := getContext()
×
2139
        client, cleanUp := getClient(ctx)
×
2140
        defer cleanUp()
×
2141

×
2142
        req := &lnrpc.NetworkInfoRequest{}
×
2143

×
2144
        netInfo, err := client.GetNetworkInfo(ctxc, req)
×
2145
        if err != nil {
×
2146
                return err
×
2147
        }
×
2148

2149
        printRespJSON(netInfo)
×
2150
        return nil
×
2151
}
2152

2153
var debugLevelCommand = cli.Command{
2154
        Name:  "debuglevel",
2155
        Usage: "Set the debug level.",
2156
        Description: `Logging level for all subsystems {trace, debug, info, warn, error, critical, off}
2157
        You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems
2158

2159
        Use show to list available subsystems`,
2160
        Flags: []cli.Flag{
2161
                cli.BoolFlag{
2162
                        Name:  "show",
2163
                        Usage: "if true, then the list of available sub-systems will be printed out",
2164
                },
2165
                cli.StringFlag{
2166
                        Name:  "level",
2167
                        Usage: "the level specification to target either a coarse logging level, or granular set of specific sub-systems with logging levels for each",
2168
                },
2169
        },
2170
        Action: actionDecorator(debugLevel),
2171
}
2172

2173
func debugLevel(ctx *cli.Context) error {
×
2174
        ctxc := getContext()
×
2175
        client, cleanUp := getClient(ctx)
×
2176
        defer cleanUp()
×
2177
        req := &lnrpc.DebugLevelRequest{
×
2178
                Show:      ctx.Bool("show"),
×
2179
                LevelSpec: ctx.String("level"),
×
2180
        }
×
2181

×
2182
        resp, err := client.DebugLevel(ctxc, req)
×
2183
        if err != nil {
×
2184
                return err
×
2185
        }
×
2186

2187
        printRespJSON(resp)
×
2188
        return nil
×
2189
}
2190

2191
var listChainTxnsCommand = cli.Command{
2192
        Name:     "listchaintxns",
2193
        Category: "On-chain",
2194
        Usage:    "List transactions from the wallet.",
2195
        Flags: []cli.Flag{
2196
                cli.Int64Flag{
2197
                        Name: "start_height",
2198
                        Usage: "the block height from which to list " +
2199
                                "transactions, inclusive",
2200
                },
2201
                cli.Int64Flag{
2202
                        Name: "end_height",
2203
                        Usage: "the block height until which to list " +
2204
                                "transactions, inclusive, to get " +
2205
                                "transactions until the chain tip, including " +
2206
                                "unconfirmed, set this value to -1",
2207
                },
2208
                cli.UintFlag{
2209
                        Name: "index_offset",
2210
                        Usage: "the index of a transaction that will be " +
2211
                                "used in a query to determine which " +
2212
                                "transaction should be returned in the " +
2213
                                "response",
2214
                },
2215
                cli.IntFlag{
2216
                        Name: "max_transactions",
2217
                        Usage: "(optional) the max number of transactions to " +
2218
                                "return; leave at default of 0 to return " +
2219
                                "all transactions",
2220
                        Value: 0,
2221
                },
2222
        },
2223
        Description: `
2224
        List all transactions an address of the wallet was involved in.
2225

2226
        This call will return a list of wallet related transactions that paid
2227
        to an address our wallet controls, or spent utxos that we held. The
2228
        start_height and end_height flags can be used to specify an inclusive
2229
        block range over which to query for transactions. If the end_height is
2230
        less than the start_height, transactions will be queried in reverse.
2231
        To get all transactions until the chain tip, including unconfirmed
2232
        transactions (identifiable with BlockHeight=0), set end_height to -1.
2233
        By default, this call will get all transactions our wallet was involved
2234
        in, including unconfirmed transactions.
2235
`,
2236
        Action: actionDecorator(listChainTxns),
2237
}
2238

2239
func listChainTxns(ctx *cli.Context) error {
×
2240
        ctxc := getContext()
×
2241
        client, cleanUp := getClient(ctx)
×
2242
        defer cleanUp()
×
2243

×
2244
        req := &lnrpc.GetTransactionsRequest{
×
2245
                IndexOffset:     uint32(ctx.Uint64("index_offset")),
×
2246
                MaxTransactions: uint32(ctx.Uint64("max_transactions")),
×
2247
        }
×
2248

×
2249
        if ctx.IsSet("start_height") {
×
2250
                req.StartHeight = int32(ctx.Int64("start_height"))
×
2251
        }
×
2252
        if ctx.IsSet("end_height") {
×
2253
                req.EndHeight = int32(ctx.Int64("end_height"))
×
2254
        }
×
2255

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

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

2265
var stopCommand = cli.Command{
2266
        Name:  "stop",
2267
        Usage: "Stop and shutdown the daemon.",
2268
        Description: `
2269
        Gracefully stop all daemon subsystems before stopping the daemon itself.
2270
        This is equivalent to stopping it using CTRL-C.`,
2271
        Action: actionDecorator(stopDaemon),
2272
}
2273

2274
func stopDaemon(ctx *cli.Context) error {
×
2275
        ctxc := getContext()
×
2276
        client, cleanUp := getClient(ctx)
×
2277
        defer cleanUp()
×
2278

×
2279
        resp, err := client.StopDaemon(ctxc, &lnrpc.StopRequest{})
×
2280
        if err != nil {
×
2281
                return err
×
2282
        }
×
2283

2284
        printRespJSON(resp)
×
2285

×
2286
        return nil
×
2287
}
2288

2289
var signMessageCommand = cli.Command{
2290
        Name:      "signmessage",
2291
        Category:  "Wallet",
2292
        Usage:     "Sign a message with the node's private key.",
2293
        ArgsUsage: "msg",
2294
        Description: `
2295
        Sign msg with the resident node's private key.
2296
        Returns the signature as a zbase32 string.
2297

2298
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2299
        Flags: []cli.Flag{
2300
                cli.StringFlag{
2301
                        Name:  "msg",
2302
                        Usage: "the message to sign",
2303
                },
2304
        },
2305
        Action: actionDecorator(signMessage),
2306
}
2307

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

×
2313
        var msg []byte
×
2314

×
2315
        switch {
×
2316
        case ctx.IsSet("msg"):
×
2317
                msg = []byte(ctx.String("msg"))
×
2318
        case ctx.Args().Present():
×
2319
                msg = []byte(ctx.Args().First())
×
2320
        default:
×
2321
                return fmt.Errorf("msg argument missing")
×
2322
        }
2323

2324
        resp, err := client.SignMessage(ctxc, &lnrpc.SignMessageRequest{Msg: msg})
×
2325
        if err != nil {
×
2326
                return err
×
2327
        }
×
2328

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

2333
var verifyMessageCommand = cli.Command{
2334
        Name:      "verifymessage",
2335
        Category:  "Wallet",
2336
        Usage:     "Verify a message signed with the signature.",
2337
        ArgsUsage: "msg signature",
2338
        Description: `
2339
        Verify that the message was signed with a properly-formed signature
2340
        The signature must be zbase32 encoded and signed with the private key of
2341
        an active node in the resident node's channel database.
2342

2343
        Positional arguments and flags can be used interchangeably but not at the same time!`,
2344
        Flags: []cli.Flag{
2345
                cli.StringFlag{
2346
                        Name:  "msg",
2347
                        Usage: "the message to verify",
2348
                },
2349
                cli.StringFlag{
2350
                        Name:  "sig",
2351
                        Usage: "the zbase32 encoded signature of the message",
2352
                },
2353
        },
2354
        Action: actionDecorator(verifyMessage),
2355
}
2356

2357
func verifyMessage(ctx *cli.Context) error {
×
2358
        ctxc := getContext()
×
2359
        client, cleanUp := getClient(ctx)
×
2360
        defer cleanUp()
×
2361

×
2362
        var (
×
2363
                msg []byte
×
2364
                sig string
×
2365
        )
×
2366

×
2367
        args := ctx.Args()
×
2368

×
2369
        switch {
×
2370
        case ctx.IsSet("msg"):
×
2371
                msg = []byte(ctx.String("msg"))
×
2372
        case args.Present():
×
2373
                msg = []byte(ctx.Args().First())
×
2374
                args = args.Tail()
×
2375
        default:
×
2376
                return fmt.Errorf("msg argument missing")
×
2377
        }
2378

2379
        switch {
×
2380
        case ctx.IsSet("sig"):
×
2381
                sig = ctx.String("sig")
×
2382
        case args.Present():
×
2383
                sig = args.First()
×
2384
        default:
×
2385
                return fmt.Errorf("signature argument missing")
×
2386
        }
2387

2388
        req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
×
2389
        resp, err := client.VerifyMessage(ctxc, req)
×
2390
        if err != nil {
×
2391
                return err
×
2392
        }
×
2393

2394
        printRespJSON(resp)
×
2395
        return nil
×
2396
}
2397

2398
var feeReportCommand = cli.Command{
2399
        Name:     "feereport",
2400
        Category: "Channels",
2401
        Usage:    "Display the current fee policies of all active channels.",
2402
        Description: `
2403
        Returns the current fee policies of all active channels.
2404
        Fee policies can be updated using the updatechanpolicy command.`,
2405
        Action: actionDecorator(feeReport),
2406
}
2407

2408
func feeReport(ctx *cli.Context) error {
×
2409
        ctxc := getContext()
×
2410
        client, cleanUp := getClient(ctx)
×
2411
        defer cleanUp()
×
2412

×
2413
        req := &lnrpc.FeeReportRequest{}
×
2414
        resp, err := client.FeeReport(ctxc, req)
×
2415
        if err != nil {
×
2416
                return err
×
2417
        }
×
2418

2419
        printRespJSON(resp)
×
2420
        return nil
×
2421
}
2422

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

2529
func parseChanPoint(s string) (*lnrpc.ChannelPoint, error) {
7✔
2530
        split := strings.Split(s, ":")
7✔
2531
        if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
10✔
2532
                return nil, errBadChanPoint
3✔
2533
        }
3✔
2534

2535
        index, err := strconv.ParseInt(split[1], 10, 64)
4✔
2536
        if err != nil {
5✔
2537
                return nil, fmt.Errorf("unable to decode output index: %w", err)
1✔
2538
        }
1✔
2539

2540
        txid, err := chainhash.NewHashFromStr(split[0])
3✔
2541
        if err != nil {
4✔
2542
                return nil, fmt.Errorf("unable to parse hex string: %w", err)
1✔
2543
        }
1✔
2544

2545
        return &lnrpc.ChannelPoint{
2✔
2546
                FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
2✔
2547
                        FundingTxidBytes: txid[:],
2✔
2548
                },
2✔
2549
                OutputIndex: uint32(index),
2✔
2550
        }, nil
2✔
2551
}
2552

2553
// parseTimeLockDelta is expected to get a uint16 type of timeLockDelta. Its
2554
// maximum value is MaxTimeLockDelta.
2555
func parseTimeLockDelta(timeLockDeltaStr string) (uint16, error) {
5✔
2556
        timeLockDeltaUnCheck, err := strconv.ParseUint(timeLockDeltaStr, 10, 64)
5✔
2557
        if err != nil {
7✔
2558
                return 0, fmt.Errorf("failed to parse time_lock_delta: %s "+
2✔
2559
                        "to uint64, err: %v", timeLockDeltaStr, err)
2✔
2560
        }
2✔
2561

2562
        if timeLockDeltaUnCheck > routing.MaxCLTVDelta {
3✔
2563
                return 0, fmt.Errorf("time_lock_delta is too big, "+
×
2564
                        "max value is %d", routing.MaxCLTVDelta)
×
2565
        }
×
2566

2567
        return uint16(timeLockDeltaUnCheck), nil
3✔
2568
}
2569

2570
func updateChannelPolicy(ctx *cli.Context) error {
×
2571
        ctxc := getContext()
×
2572
        client, cleanUp := getClient(ctx)
×
2573
        defer cleanUp()
×
2574

×
2575
        var (
×
2576
                baseFee       int64
×
2577
                feeRate       float64
×
2578
                feeRatePpm    uint64
×
2579
                timeLockDelta uint16
×
2580
                err           error
×
2581
        )
×
2582
        args := ctx.Args()
×
2583

×
2584
        switch {
×
2585
        case ctx.IsSet("base_fee_msat"):
×
2586
                baseFee = ctx.Int64("base_fee_msat")
×
2587
        case args.Present():
×
2588
                baseFee, err = strconv.ParseInt(args.First(), 10, 64)
×
2589
                if err != nil {
×
2590
                        return fmt.Errorf("unable to decode base_fee_msat: %w",
×
2591
                                err)
×
2592
                }
×
2593
                args = args.Tail()
×
2594
        default:
×
2595
                return fmt.Errorf("base_fee_msat argument missing")
×
2596
        }
2597

2598
        switch {
×
2599
        case ctx.IsSet("fee_rate") && ctx.IsSet("fee_rate_ppm"):
×
2600
                return fmt.Errorf("fee_rate or fee_rate_ppm can not both be set")
×
2601
        case ctx.IsSet("fee_rate"):
×
2602
                feeRate = ctx.Float64("fee_rate")
×
2603
        case ctx.IsSet("fee_rate_ppm"):
×
2604
                feeRatePpm = ctx.Uint64("fee_rate_ppm")
×
2605
        case args.Present():
×
2606
                feeRate, err = strconv.ParseFloat(args.First(), 64)
×
2607
                if err != nil {
×
2608
                        return fmt.Errorf("unable to decode fee_rate: %w", err)
×
2609
                }
×
2610

2611
                args = args.Tail()
×
2612
        default:
×
2613
                return fmt.Errorf("fee_rate or fee_rate_ppm argument missing")
×
2614
        }
2615

2616
        switch {
×
2617
        case ctx.IsSet("time_lock_delta"):
×
2618
                timeLockDeltaStr := ctx.String("time_lock_delta")
×
2619
                timeLockDelta, err = parseTimeLockDelta(timeLockDeltaStr)
×
2620
                if err != nil {
×
2621
                        return err
×
2622
                }
×
2623
        case args.Present():
×
2624
                timeLockDelta, err = parseTimeLockDelta(args.First())
×
2625
                if err != nil {
×
2626
                        return err
×
2627
                }
×
2628

2629
                args = args.Tail()
×
2630
        default:
×
2631
                return fmt.Errorf("time_lock_delta argument missing")
×
2632
        }
2633

2634
        var (
×
2635
                chanPoint    *lnrpc.ChannelPoint
×
2636
                chanPointStr string
×
2637
        )
×
2638

×
2639
        switch {
×
2640
        case ctx.IsSet("chan_point"):
×
2641
                chanPointStr = ctx.String("chan_point")
×
2642
        case args.Present():
×
2643
                chanPointStr = args.First()
×
2644
        }
2645

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

2653
        inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
×
2654
        if inboundBaseFeeMsat < math.MinInt32 ||
×
2655
                inboundBaseFeeMsat > math.MaxInt32 {
×
2656

×
2657
                return errors.New("inbound_base_fee_msat out of range")
×
2658
        }
×
2659

2660
        inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
×
2661
        if inboundFeeRatePpm < math.MinInt32 ||
×
2662
                inboundFeeRatePpm > math.MaxInt32 {
×
2663

×
2664
                return errors.New("inbound_fee_rate_ppm out of range")
×
2665
        }
×
2666

2667
        // Inbound fees are optional. However, if an update is required,
2668
        // both the base fee and the fee rate must be provided.
2669
        var inboundFee *lnrpc.InboundFee
×
2670
        if ctx.IsSet("inbound_base_fee_msat") !=
×
2671
                ctx.IsSet("inbound_fee_rate_ppm") {
×
2672

×
2673
                return errors.New("both parameters must be provided: " +
×
2674
                        "inbound_base_fee_msat and inbound_fee_rate_ppm")
×
2675
        }
×
2676

2677
        if ctx.IsSet("inbound_fee_rate_ppm") {
×
2678
                inboundFee = &lnrpc.InboundFee{
×
2679
                        BaseFeeMsat: int32(inboundBaseFeeMsat),
×
2680
                        FeeRatePpm:  int32(inboundFeeRatePpm),
×
2681
                }
×
2682
        }
×
2683

2684
        createMissingEdge := ctx.Bool("create_missing_edge")
×
2685

×
2686
        req := &lnrpc.PolicyUpdateRequest{
×
2687
                BaseFeeMsat:       baseFee,
×
2688
                TimeLockDelta:     uint32(timeLockDelta),
×
2689
                MaxHtlcMsat:       ctx.Uint64("max_htlc_msat"),
×
2690
                InboundFee:        inboundFee,
×
2691
                CreateMissingEdge: createMissingEdge,
×
2692
        }
×
2693

×
2694
        if ctx.IsSet("min_htlc_msat") {
×
2695
                req.MinHtlcMsat = ctx.Uint64("min_htlc_msat")
×
2696
                req.MinHtlcMsatSpecified = true
×
2697
        }
×
2698

2699
        if chanPoint != nil {
×
2700
                req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
×
2701
                        ChanPoint: chanPoint,
×
2702
                }
×
2703
        } else {
×
2704
                req.Scope = &lnrpc.PolicyUpdateRequest_Global{
×
2705
                        Global: true,
×
2706
                }
×
2707
        }
×
2708

2709
        if feeRate != 0 {
×
2710
                req.FeeRate = feeRate
×
2711
        } else if feeRatePpm != 0 {
×
2712
                req.FeeRatePpm = uint32(feeRatePpm)
×
2713
        }
×
2714

2715
        resp, err := client.UpdateChannelPolicy(ctxc, req)
×
2716
        if err != nil {
×
2717
                return err
×
2718
        }
×
2719

2720
        // Parse the response into the final json object that will be printed
2721
        // to stdout. At the moment, this filters out the raw txid bytes from
2722
        // each failed update's outpoint and only prints the txid string.
2723
        var listFailedUpdateResp = struct {
×
2724
                FailedUpdates []*FailedUpdate `json:"failed_updates"`
×
2725
        }{
×
2726
                FailedUpdates: make([]*FailedUpdate, 0, len(resp.FailedUpdates)),
×
2727
        }
×
2728
        for _, protoUpdate := range resp.FailedUpdates {
×
2729
                failedUpdate := NewFailedUpdateFromProto(protoUpdate)
×
2730
                listFailedUpdateResp.FailedUpdates = append(
×
2731
                        listFailedUpdateResp.FailedUpdates, failedUpdate)
×
2732
        }
×
2733

2734
        printJSON(listFailedUpdateResp)
×
2735

×
2736
        return nil
×
2737
}
2738

2739
var fishCompletionCommand = cli.Command{
2740
        Name:   "fish-completion",
2741
        Hidden: true,
2742
        Action: func(c *cli.Context) error {
×
2743
                completion, err := c.App.ToFishCompletion()
×
2744
                if err != nil {
×
2745
                        return err
×
2746
                }
×
2747

2748
                // We don't want to suggest files, so we add this
2749
                // first line to the completions.
2750
                _, err = fmt.Printf("complete -c %q -f \n%s", c.App.Name, completion)
×
2751
                return err
×
2752
        },
2753
}
2754

2755
var exportChanBackupCommand = cli.Command{
2756
        Name:     "exportchanbackup",
2757
        Category: "Channels",
2758
        Usage: "Obtain a static channel back up for a selected channels, " +
2759
                "or all known channels.",
2760
        ArgsUsage: "[chan_point] [--all] [--output_file]",
2761
        Description: `
2762
        This command allows a user to export a Static Channel Backup (SCB) for
2763
        a selected channel. SCB's are encrypted backups of a channel's initial
2764
        state that are encrypted with a key derived from the seed of a user. In
2765
        the case of partial or complete data loss, the SCB will allow the user
2766
        to reclaim settled funds in the channel at its final state. The
2767
        exported channel backups can be restored at a later time using the
2768
        restorechanbackup command.
2769

2770
        This command will return one of two types of channel backups depending
2771
        on the set of passed arguments:
2772

2773
           * If a target channel point is specified, then a single channel
2774
             backup containing only the information for that channel will be
2775
             returned.
2776

2777
           * If the --all flag is passed, then a multi-channel backup will be
2778
             returned. A multi backup is a single encrypted blob (displayed in
2779
             hex encoding) that contains several channels in a single cipher
2780
             text.
2781

2782
        Both of the backup types can be restored using the restorechanbackup
2783
        command.
2784
        `,
2785
        Flags: []cli.Flag{
2786
                cli.StringFlag{
2787
                        Name:  "chan_point",
2788
                        Usage: "the target channel to obtain an SCB for",
2789
                },
2790
                cli.BoolFlag{
2791
                        Name: "all",
2792
                        Usage: "if specified, then a multi backup of all " +
2793
                                "active channels will be returned",
2794
                },
2795
                cli.StringFlag{
2796
                        Name: "output_file",
2797
                        Usage: `
2798
                        if specified, then rather than printing a JSON output
2799
                        of the static channel backup, a serialized version of
2800
                        the backup (either Single or Multi) will be written to
2801
                        the target file, this is the same format used by lnd in
2802
                        its channel.backup file `,
2803
                },
2804
        },
2805
        Action: actionDecorator(exportChanBackup),
2806
}
2807

2808
func exportChanBackup(ctx *cli.Context) error {
×
2809
        ctxc := getContext()
×
2810
        client, cleanUp := getClient(ctx)
×
2811
        defer cleanUp()
×
2812

×
2813
        // Show command help if no arguments provided
×
2814
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2815
                cli.ShowCommandHelp(ctx, "exportchanbackup")
×
2816
                return nil
×
2817
        }
×
2818

2819
        var (
×
2820
                err            error
×
2821
                chanPointStr   string
×
2822
                outputFileName string
×
2823
        )
×
2824
        args := ctx.Args()
×
2825

×
2826
        switch {
×
2827
        case ctx.IsSet("chan_point"):
×
2828
                chanPointStr = ctx.String("chan_point")
×
2829

2830
        case args.Present():
×
2831
                chanPointStr = args.First()
×
2832

2833
        case !ctx.IsSet("all"):
×
2834
                return fmt.Errorf("must specify chan_point if --all isn't set")
×
2835
        }
2836

2837
        if ctx.IsSet("output_file") {
×
2838
                outputFileName = ctx.String("output_file")
×
2839
        }
×
2840

2841
        if chanPointStr != "" {
×
2842
                chanPointRPC, err := parseChanPoint(chanPointStr)
×
2843
                if err != nil {
×
2844
                        return fmt.Errorf("unable to parse chan_point: %w", err)
×
2845
                }
×
2846

2847
                chanBackup, err := client.ExportChannelBackup(
×
2848
                        ctxc, &lnrpc.ExportChannelBackupRequest{
×
2849
                                ChanPoint: chanPointRPC,
×
2850
                        },
×
2851
                )
×
2852
                if err != nil {
×
2853
                        return err
×
2854
                }
×
2855

2856
                txid, err := chainhash.NewHash(
×
2857
                        chanPointRPC.GetFundingTxidBytes(),
×
2858
                )
×
2859
                if err != nil {
×
2860
                        return err
×
2861
                }
×
2862

2863
                chanPoint := wire.OutPoint{
×
2864
                        Hash:  *txid,
×
2865
                        Index: chanPointRPC.OutputIndex,
×
2866
                }
×
2867

×
2868
                if outputFileName != "" {
×
2869
                        return os.WriteFile(
×
2870
                                outputFileName,
×
2871
                                chanBackup.ChanBackup,
×
2872
                                0666,
×
2873
                        )
×
2874
                }
×
2875

2876
                printJSON(struct {
×
2877
                        ChanPoint  string `json:"chan_point"`
×
2878
                        ChanBackup string `json:"chan_backup"`
×
2879
                }{
×
2880
                        ChanPoint:  chanPoint.String(),
×
2881
                        ChanBackup: hex.EncodeToString(chanBackup.ChanBackup),
×
2882
                })
×
2883
                return nil
×
2884
        }
2885

2886
        if !ctx.IsSet("all") {
×
2887
                return fmt.Errorf("if a channel isn't specified, -all must be")
×
2888
        }
×
2889

2890
        chanBackup, err := client.ExportAllChannelBackups(
×
2891
                ctxc, &lnrpc.ChanBackupExportRequest{},
×
2892
        )
×
2893
        if err != nil {
×
2894
                return err
×
2895
        }
×
2896

2897
        if outputFileName != "" {
×
2898
                return os.WriteFile(
×
2899
                        outputFileName,
×
2900
                        chanBackup.MultiChanBackup.MultiChanBackup,
×
2901
                        0666,
×
2902
                )
×
2903
        }
×
2904

2905
        // TODO(roasbeef): support for export | restore ?
2906

2907
        var chanPoints []string
×
2908
        for _, chanPoint := range chanBackup.MultiChanBackup.ChanPoints {
×
2909
                txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes())
×
2910
                if err != nil {
×
2911
                        return err
×
2912
                }
×
2913

2914
                chanPoints = append(chanPoints, wire.OutPoint{
×
2915
                        Hash:  *txid,
×
2916
                        Index: chanPoint.OutputIndex,
×
2917
                }.String())
×
2918
        }
2919

2920
        printRespJSON(chanBackup)
×
2921

×
2922
        return nil
×
2923
}
2924

2925
var verifyChanBackupCommand = cli.Command{
2926
        Name:      "verifychanbackup",
2927
        Category:  "Channels",
2928
        Usage:     "Verify an existing channel backup.",
2929
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file]",
2930
        Description: `
2931
    This command allows a user to verify an existing Single or Multi channel
2932
    backup for integrity. This is useful when a user has a backup, but is
2933
    unsure as to if it's valid or for the target node.
2934

2935
    The command will accept backups in one of four forms:
2936

2937
       * A single channel packed SCB, which can be obtained from
2938
         exportchanbackup. This should be passed in hex encoded format.
2939

2940
       * A packed multi-channel SCB, which couples several individual
2941
         static channel backups in single blob.
2942

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

2946
       * A file path which points to a packed multi-channel backup within a
2947
         file, using the same format that lnd does in its channel.backup
2948
         file.
2949
    `,
2950
        Flags: []cli.Flag{
2951
                cli.StringFlag{
2952
                        Name: "single_backup",
2953
                        Usage: "a hex encoded single channel backup obtained " +
2954
                                "from exportchanbackup",
2955
                },
2956
                cli.StringFlag{
2957
                        Name: "multi_backup",
2958
                        Usage: "a hex encoded multi-channel backup obtained " +
2959
                                "from exportchanbackup",
2960
                },
2961

2962
                cli.StringFlag{
2963
                        Name:      "single_file",
2964
                        Usage:     "the path to a single-channel backup file",
2965
                        TakesFile: true,
2966
                },
2967

2968
                cli.StringFlag{
2969
                        Name:      "multi_file",
2970
                        Usage:     "the path to a multi-channel back up file",
2971
                        TakesFile: true,
2972
                },
2973
        },
2974
        Action: actionDecorator(verifyChanBackup),
2975
}
2976

2977
func verifyChanBackup(ctx *cli.Context) error {
×
2978
        ctxc := getContext()
×
2979
        client, cleanUp := getClient(ctx)
×
2980
        defer cleanUp()
×
2981

×
2982
        // Show command help if no arguments provided
×
2983
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
2984
                cli.ShowCommandHelp(ctx, "verifychanbackup")
×
2985
                return nil
×
2986
        }
×
2987

2988
        backups, err := parseChanBackups(ctx)
×
2989
        if err != nil {
×
2990
                return err
×
2991
        }
×
2992

2993
        verifyReq := lnrpc.ChanBackupSnapshot{}
×
2994

×
2995
        if backups.GetChanBackups() != nil {
×
2996
                verifyReq.SingleChanBackups = backups.GetChanBackups()
×
2997
        }
×
2998
        if backups.GetMultiChanBackup() != nil {
×
2999
                verifyReq.MultiChanBackup = &lnrpc.MultiChanBackup{
×
3000
                        MultiChanBackup: backups.GetMultiChanBackup(),
×
3001
                }
×
3002
        }
×
3003

3004
        resp, err := client.VerifyChanBackup(ctxc, &verifyReq)
×
3005
        if err != nil {
×
3006
                return err
×
3007
        }
×
3008

3009
        printRespJSON(resp)
×
3010
        return nil
×
3011
}
3012

3013
var restoreChanBackupCommand = cli.Command{
3014
        Name:     "restorechanbackup",
3015
        Category: "Channels",
3016
        Usage: "Restore an existing single or multi-channel static channel " +
3017
                "backup.",
3018
        ArgsUsage: "[--single_backup] [--multi_backup] [--multi_file=",
3019
        Description: `
3020
        Allows a user to restore a Static Channel Backup (SCB) that was
3021
        obtained either via the exportchanbackup command, or from lnd's
3022
        automatically managed channel.backup file. This command should be used
3023
        if a user is attempting to restore a channel due to data loss on a
3024
        running node restored with the same seed as the node that created the
3025
        channel. If successful, this command will allows the user to recover
3026
        the settled funds stored in the recovered channels.
3027

3028
        The command will accept backups in one of four forms:
3029

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

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

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

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

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

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

3071
// errMissingChanBackup is an error returned when we attempt to parse a channel
3072
// backup from a CLI command, and it is missing.
3073
var errMissingChanBackup = errors.New("missing channel backup")
3074

3075
func parseChanBackups(ctx *cli.Context) (*lnrpc.RestoreChanBackupRequest, error) {
×
3076
        switch {
×
3077
        case ctx.IsSet("single_backup"):
×
3078
                packedBackup, err := hex.DecodeString(
×
3079
                        ctx.String("single_backup"),
×
3080
                )
×
3081
                if err != nil {
×
3082
                        return nil, fmt.Errorf("unable to decode single packed "+
×
3083
                                "backup: %v", err)
×
3084
                }
×
3085

3086
                return &lnrpc.RestoreChanBackupRequest{
×
3087
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3088
                                ChanBackups: &lnrpc.ChannelBackups{
×
3089
                                        ChanBackups: []*lnrpc.ChannelBackup{
×
3090
                                                {
×
3091
                                                        ChanBackup: packedBackup,
×
3092
                                                },
×
3093
                                        },
×
3094
                                },
×
3095
                        },
×
3096
                }, nil
×
3097

3098
        case ctx.IsSet("multi_backup"):
×
3099
                packedMulti, err := hex.DecodeString(
×
3100
                        ctx.String("multi_backup"),
×
3101
                )
×
3102
                if err != nil {
×
3103
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3104
                                "backup: %v", err)
×
3105
                }
×
3106

3107
                return &lnrpc.RestoreChanBackupRequest{
×
3108
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3109
                                MultiChanBackup: packedMulti,
×
3110
                        },
×
3111
                }, nil
×
3112

3113
        case ctx.IsSet("single_file"):
×
3114
                packedSingle, err := os.ReadFile(ctx.String("single_file"))
×
3115
                if err != nil {
×
3116
                        return nil, fmt.Errorf("unable to decode single "+
×
3117
                                "packed backup: %v", err)
×
3118
                }
×
3119

3120
                return &lnrpc.RestoreChanBackupRequest{
×
3121
                        Backup: &lnrpc.RestoreChanBackupRequest_ChanBackups{
×
3122
                                ChanBackups: &lnrpc.ChannelBackups{
×
3123
                                        ChanBackups: []*lnrpc.ChannelBackup{{
×
3124
                                                ChanBackup: packedSingle,
×
3125
                                        }},
×
3126
                                },
×
3127
                        },
×
3128
                }, nil
×
3129

3130
        case ctx.IsSet("multi_file"):
×
3131
                packedMulti, err := os.ReadFile(ctx.String("multi_file"))
×
3132
                if err != nil {
×
3133
                        return nil, fmt.Errorf("unable to decode multi packed "+
×
3134
                                "backup: %v", err)
×
3135
                }
×
3136

3137
                return &lnrpc.RestoreChanBackupRequest{
×
3138
                        Backup: &lnrpc.RestoreChanBackupRequest_MultiChanBackup{
×
3139
                                MultiChanBackup: packedMulti,
×
3140
                        },
×
3141
                }, nil
×
3142

3143
        default:
×
3144
                return nil, errMissingChanBackup
×
3145
        }
3146
}
3147

3148
func restoreChanBackup(ctx *cli.Context) error {
×
3149
        ctxc := getContext()
×
3150
        client, cleanUp := getClient(ctx)
×
3151
        defer cleanUp()
×
3152

×
3153
        // Show command help if no arguments provided
×
3154
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
3155
                cli.ShowCommandHelp(ctx, "restorechanbackup")
×
3156
                return nil
×
3157
        }
×
3158

3159
        var req lnrpc.RestoreChanBackupRequest
×
3160

×
3161
        backups, err := parseChanBackups(ctx)
×
3162
        if err != nil {
×
3163
                return err
×
3164
        }
×
3165

3166
        req.Backup = backups.Backup
×
3167

×
3168
        resp, err := client.RestoreChannelBackups(ctxc, &req)
×
3169
        if err != nil {
×
3170
                return fmt.Errorf("unable to restore chan backups: %w", err)
×
3171
        }
×
3172

3173
        printRespJSON(resp)
×
3174

×
3175
        return nil
×
3176
}
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