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

lightningnetwork / lnd / 14227452255

02 Apr 2025 07:01PM UTC coverage: 69.045% (+10.4%) from 58.637%
14227452255

Pull #9356

github

web-flow
Merge 12f979a13 into 6a3845b79
Pull Request #9356: lnrpc: add incoming/outgoing channel ids filter to forwarding history request

43 of 61 new or added lines in 3 files covered. (70.49%)

36 existing lines in 11 files now uncovered.

133471 of 193310 relevant lines covered (69.05%)

22169.88 hits per line

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

4.46
/cmd/commands/cmd_payments.go
1
package commands
2

3
import (
4
        "bytes"
5
        "context"
6
        "crypto/rand"
7
        "encoding/hex"
8
        "errors"
9
        "fmt"
10
        "io"
11
        "os"
12
        "runtime"
13
        "strconv"
14
        "strings"
15
        "time"
16

17
        "github.com/btcsuite/btcd/btcutil"
18
        "github.com/jedib0t/go-pretty/v6/table"
19
        "github.com/jedib0t/go-pretty/v6/text"
20
        "github.com/lightningnetwork/lnd/lnrpc"
21
        "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
22
        "github.com/lightningnetwork/lnd/lntypes"
23
        "github.com/lightningnetwork/lnd/lnwallet"
24
        "github.com/lightningnetwork/lnd/lnwire"
25
        "github.com/lightningnetwork/lnd/record"
26
        "github.com/lightningnetwork/lnd/routing/route"
27
        "github.com/urfave/cli"
28
        "google.golang.org/grpc"
29
        "google.golang.org/protobuf/proto"
30
)
31

32
const (
33
        // paymentTimeout is the default timeout for the payment loop in lnd.
34
        // No new attempts will be started after the timeout.
35
        paymentTimeout = time.Second * 60
36
)
37

38
var (
39
        cltvLimitFlag = cli.UintFlag{
40
                Name: "cltv_limit",
41
                Usage: "the maximum time lock that may be used for " +
42
                        "this payment",
43
        }
44

45
        lastHopFlag = cli.StringFlag{
46
                Name: "last_hop",
47
                Usage: "pubkey of the last hop (penultimate node in the path) " +
48
                        "to route through for this payment",
49
        }
50

51
        dataFlag = cli.StringFlag{
52
                Name: "data",
53
                Usage: "attach custom data to the payment. The required " +
54
                        "format is: <record_id>=<hex_value>,<record_id>=" +
55
                        "<hex_value>,.. For example: --data 3438382=0a21ff. " +
56
                        "Custom record ids start from 65536.",
57
        }
58

59
        inflightUpdatesFlag = cli.BoolFlag{
60
                Name: "inflight_updates",
61
                Usage: "if set, intermediate payment state updates will be " +
62
                        "displayed. Only valid in combination with --json.",
63
        }
64

65
        maxPartsFlag = cli.UintFlag{
66
                Name: "max_parts",
67
                Usage: "the maximum number of partial payments that may be " +
68
                        "used",
69
                Value: routerrpc.DefaultMaxParts,
70
        }
71

72
        jsonFlag = cli.BoolFlag{
73
                Name: "json",
74
                Usage: "if set, payment updates are printed as json " +
75
                        "messages. Set by default on Windows because table " +
76
                        "formatting is unsupported.",
77
        }
78

79
        maxShardSizeSatFlag = cli.UintFlag{
80
                Name: "max_shard_size_sat",
81
                Usage: "the largest payment split that should be attempted if " +
82
                        "payment splitting is required to attempt a payment, " +
83
                        "specified in satoshis",
84
        }
85

86
        maxShardSizeMsatFlag = cli.UintFlag{
87
                Name: "max_shard_size_msat",
88
                Usage: "the largest payment split that should be attempted if " +
89
                        "payment splitting is required to attempt a payment, " +
90
                        "specified in milli-satoshis",
91
        }
92

93
        ampFlag = cli.BoolFlag{
94
                Name: "amp",
95
                Usage: "if set to true, then AMP will be used to complete the " +
96
                        "payment",
97
        }
98

99
        timePrefFlag = cli.Float64Flag{
100
                Name:  "time_pref",
101
                Usage: "(optional) expresses time preference (range -1 to 1)",
102
        }
103

104
        introductionNodeFlag = cli.StringFlag{
105
                Name: "introduction_node",
106
                Usage: "(blinded paths) the hex encoded, cleartext node ID " +
107
                        "of the node to use for queries to a blinded route",
108
        }
109

110
        blindingPointFlag = cli.StringFlag{
111
                Name: "blinding_point",
112
                Usage: "(blinded paths) the hex encoded blinding point to " +
113
                        "use if querying a route to a blinded path, this " +
114
                        "value *must* be set for queries to a blinded path",
115
        }
116

117
        blindedHopsFlag = cli.StringSliceFlag{
118
                Name: "blinded_hops",
119
                Usage: "(blinded paths) the blinded hops to include in the " +
120
                        "query, formatted as <blinded_node_id>:" +
121
                        "<hex_encrypted_data>. These hops must be provided " +
122
                        "*in order* starting with the introduction point and " +
123
                        "ending with the receiving node",
124
        }
125

126
        blindedBaseFlag = cli.Uint64Flag{
127
                Name: "blinded_base_fee",
128
                Usage: "(blinded paths) the aggregate base fee for the " +
129
                        "blinded portion of the route, expressed in msat",
130
        }
131

132
        blindedPPMFlag = cli.Uint64Flag{
133
                Name: "blinded_ppm_fee",
134
                Usage: "(blinded paths) the aggregate proportional fee for " +
135
                        "the blinded portion of the route, expressed in " +
136
                        "parts per million",
137
        }
138

139
        blindedCLTVFlag = cli.Uint64Flag{
140
                Name: "blinded_cltv",
141
                Usage: "(blinded paths) the total cltv delay for the " +
142
                        "blinded portion of the route",
143
        }
144

145
        cancelableFlag = cli.BoolFlag{
146
                Name: "cancelable",
147
                Usage: "if set to true, the payment loop can be interrupted " +
148
                        "by manually canceling the payment context, even " +
149
                        "before the payment timeout is reached. Note that " +
150
                        "the payment may still succeed after cancellation, " +
151
                        "as in-flight attempts can still settle afterwards. " +
152
                        "Canceling will only prevent further attempts from " +
153
                        "being sent",
154
        }
155
)
156

157
// PaymentFlags returns common flags for sendpayment and payinvoice.
158
func PaymentFlags() []cli.Flag {
2✔
159
        return []cli.Flag{
2✔
160
                cli.StringFlag{
2✔
161
                        Name:  "pay_req",
2✔
162
                        Usage: "a zpay32 encoded payment request to fulfill",
2✔
163
                },
2✔
164
                cli.Int64Flag{
2✔
165
                        Name: "fee_limit",
2✔
166
                        Usage: "maximum fee allowed in satoshis when " +
2✔
167
                                "sending the payment",
2✔
168
                },
2✔
169
                cli.Int64Flag{
2✔
170
                        Name: "fee_limit_percent",
2✔
171
                        Usage: "percentage of the payment's amount used as " +
2✔
172
                                "the maximum fee allowed when sending the " +
2✔
173
                                "payment",
2✔
174
                },
2✔
175
                cli.DurationFlag{
2✔
176
                        Name: "timeout",
2✔
177
                        Usage: "the maximum amount of time we should spend " +
2✔
178
                                "trying to fulfill the payment, failing " +
2✔
179
                                "after the timeout has elapsed",
2✔
180
                        Value: paymentTimeout,
2✔
181
                },
2✔
182
                cancelableFlag,
2✔
183
                cltvLimitFlag,
2✔
184
                lastHopFlag,
2✔
185
                cli.Int64SliceFlag{
2✔
186
                        Name: "outgoing_chan_id",
2✔
187
                        Usage: "short channel id of the outgoing channel to " +
2✔
188
                                "use for the first hop of the payment; can " +
2✔
189
                                "be specified multiple times in the same " +
2✔
190
                                "command",
2✔
191
                        Value: &cli.Int64Slice{},
2✔
192
                },
2✔
193
                cli.BoolFlag{
2✔
194
                        Name:  "force, f",
2✔
195
                        Usage: "will skip payment request confirmation",
2✔
196
                },
2✔
197
                cli.BoolFlag{
2✔
198
                        Name:  "allow_self_payment",
2✔
199
                        Usage: "allow sending a circular payment to self",
2✔
200
                },
2✔
201
                dataFlag, inflightUpdatesFlag, maxPartsFlag, jsonFlag,
2✔
202
                maxShardSizeSatFlag, maxShardSizeMsatFlag, ampFlag,
2✔
203
                timePrefFlag,
2✔
204
        }
2✔
205
}
2✔
206

207
var SendPaymentCommand = cli.Command{
208
        Name:     "sendpayment",
209
        Category: "Payments",
210
        Usage:    "Send a payment over lightning.",
211
        Description: `
212
        Send a payment over Lightning. One can either specify the full
213
        parameters of the payment, or just use a payment request which encodes
214
        all the payment details.
215

216
        If payment isn't manually specified, then only a payment request needs
217
        to be passed using the --pay_req argument.
218

219
        If the payment *is* manually specified, then the following arguments
220
        need to be specified in order to complete the payment:
221

222
        For invoice with keysend,
223
            --dest=N --amt=A --final_cltv_delta=T --keysend
224
        For invoice without payment address:
225
            --dest=N --amt=A --payment_hash=H --final_cltv_delta=T
226
        For invoice with payment address:
227
            --dest=N --amt=A --payment_hash=H --final_cltv_delta=T --pay_addr=H
228
        `,
229
        ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
230
                "--pay_req=R [--pay_addr=H]",
231
        Flags: append(PaymentFlags(),
232
                cli.StringFlag{
233
                        Name: "dest, d",
234
                        Usage: "the compressed identity pubkey of the " +
235
                                "payment recipient",
236
                },
237
                cli.Int64Flag{
238
                        Name:  "amt, a",
239
                        Usage: "number of satoshis to send",
240
                },
241
                cli.StringFlag{
242
                        Name:  "payment_hash, r",
243
                        Usage: "the hash to use within the payment's HTLC",
244
                },
245
                cli.Int64Flag{
246
                        Name:  "final_cltv_delta",
247
                        Usage: "the number of blocks the last hop has to reveal the preimage",
248
                },
249
                cli.StringFlag{
250
                        Name:  "pay_addr",
251
                        Usage: "the payment address of the generated invoice",
252
                },
253
                cli.BoolFlag{
254
                        Name:  "keysend",
255
                        Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
256
                },
257
        ),
258
        Action: SendPayment,
259
}
260

261
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
262
// flags passed. It always returns a value and doesn't rely on lnd applying a
263
// default.
264
func retrieveFeeLimit(ctx *cli.Context, amt int64) (int64, error) {
×
265
        switch {
×
266
        case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"):
×
267
                return 0, fmt.Errorf("either fee_limit or fee_limit_percent " +
×
268
                        "can be set, but not both")
×
269

270
        case ctx.IsSet("fee_limit"):
×
271
                return ctx.Int64("fee_limit"), nil
×
272

273
        case ctx.IsSet("fee_limit_percent"):
×
274
                // Round up the fee limit to prevent hitting zero on small
×
275
                // amounts.
×
276
                feeLimitRoundedUp :=
×
277
                        (amt*ctx.Int64("fee_limit_percent") + 99) / 100
×
278

×
279
                return feeLimitRoundedUp, nil
×
280
        }
281

282
        // If no fee limit is set, use a default value based on the amount.
283
        amtMsat := lnwire.NewMSatFromSatoshis(btcutil.Amount(amt))
×
284
        limitMsat := lnwallet.DefaultRoutingFeeLimitForAmount(amtMsat)
×
285
        return int64(limitMsat.ToSatoshis()), nil
×
286
}
287

288
func confirmPayReq(resp *lnrpc.PayReq, amt, feeLimit int64) error {
×
289
        fmt.Printf("Payment hash: %v\n", resp.GetPaymentHash())
×
290
        fmt.Printf("Description: %v\n", resp.GetDescription())
×
291
        fmt.Printf("Amount (in satoshis): %v\n", amt)
×
292
        fmt.Printf("Fee limit (in satoshis): %v\n", feeLimit)
×
293
        fmt.Printf("Destination: %v\n", resp.GetDestination())
×
294

×
295
        confirm := promptForConfirmation("Confirm payment (yes/no): ")
×
296
        if !confirm {
×
297
                return fmt.Errorf("payment not confirmed")
×
298
        }
×
299

300
        return nil
×
301
}
302

303
func parsePayAddr(ctx *cli.Context, args cli.Args) ([]byte, error) {
×
304
        var (
×
305
                payAddr []byte
×
306
                err     error
×
307
        )
×
308
        switch {
×
309
        case ctx.IsSet("pay_addr"):
×
310
                payAddr, err = hex.DecodeString(ctx.String("pay_addr"))
×
311

312
        case args.Present():
×
313
                payAddr, err = hex.DecodeString(args.First())
×
314
        }
315

316
        if err != nil {
×
317
                return nil, err
×
318
        }
×
319

320
        // payAddr may be not required if it's a legacy invoice.
321
        if len(payAddr) != 0 && len(payAddr) != 32 {
×
322
                return nil, fmt.Errorf("payment addr must be exactly 32 "+
×
323
                        "bytes, is instead %v", len(payAddr))
×
324
        }
×
325

326
        return payAddr, nil
×
327
}
328

329
func SendPayment(ctx *cli.Context) error {
×
330
        // Show command help if no arguments provided
×
331
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
332
                _ = cli.ShowCommandHelp(ctx, "sendpayment")
×
333
                return nil
×
334
        }
×
335

336
        conn := getClientConn(ctx, false)
×
337
        defer conn.Close()
×
338

×
339
        args := ctx.Args()
×
340

×
341
        // If a payment request was provided, we can exit early since all of the
×
342
        // details of the payment are encoded within the request.
×
343
        if ctx.IsSet("pay_req") {
×
344
                req := &routerrpc.SendPaymentRequest{
×
345
                        PaymentRequest:    StripPrefix(ctx.String("pay_req")),
×
346
                        Amt:               ctx.Int64("amt"),
×
347
                        DestCustomRecords: make(map[uint64][]byte),
×
348
                        Amp:               ctx.Bool(ampFlag.Name),
×
349
                        Cancelable:        ctx.Bool(cancelableFlag.Name),
×
350
                }
×
351

×
352
                // We'll attempt to parse a payment address as well, given that
×
353
                // if the user is using an AMP invoice, then they may be trying
×
354
                // to specify that value manually.
×
355
                //
×
356
                // Don't parse unnamed arguments to prevent confusion with the
×
357
                // main unnamed argument format for non-AMP payments.
×
358
                payAddr, err := parsePayAddr(ctx, nil)
×
359
                if err != nil {
×
360
                        return err
×
361
                }
×
362

363
                req.PaymentAddr = payAddr
×
364

×
365
                return SendPaymentRequest(
×
366
                        ctx, req, conn, conn, routerRPCSendPayment,
×
367
                )
×
368
        }
369

370
        var (
×
371
                destNode []byte
×
372
                amount   int64
×
373
                err      error
×
374
        )
×
375

×
376
        switch {
×
377
        case ctx.IsSet("dest"):
×
378
                destNode, err = hex.DecodeString(ctx.String("dest"))
×
379
        case args.Present():
×
380
                destNode, err = hex.DecodeString(args.First())
×
381
                args = args.Tail()
×
382
        default:
×
383
                return fmt.Errorf("destination txid argument missing")
×
384
        }
385
        if err != nil {
×
386
                return err
×
387
        }
×
388

389
        if len(destNode) != 33 {
×
390
                return fmt.Errorf("dest node pubkey must be exactly 33 bytes, is "+
×
391
                        "instead: %v", len(destNode))
×
392
        }
×
393

394
        if ctx.IsSet("amt") {
×
395
                amount = ctx.Int64("amt")
×
396
        } else if args.Present() {
×
397
                amount, err = strconv.ParseInt(args.First(), 10, 64)
×
398
                args = args.Tail()
×
399
                if err != nil {
×
400
                        return fmt.Errorf("unable to decode payment amount: %w",
×
401
                                err)
×
402
                }
×
403
        }
404

405
        req := &routerrpc.SendPaymentRequest{
×
406
                Dest:              destNode,
×
407
                Amt:               amount,
×
408
                DestCustomRecords: make(map[uint64][]byte),
×
409
                Amp:               ctx.Bool(ampFlag.Name),
×
410
                Cancelable:        ctx.Bool(cancelableFlag.Name),
×
411
        }
×
412

×
413
        var rHash []byte
×
414

×
415
        switch {
×
416
        case ctx.Bool("keysend") && ctx.Bool(ampFlag.Name):
×
417
                return errors.New("either keysend or amp may be set, but not both")
×
418

419
        case ctx.Bool("keysend"):
×
420
                if ctx.IsSet("payment_hash") {
×
421
                        return errors.New("cannot set payment hash when using " +
×
422
                                "keysend")
×
423
                }
×
424
                var preimage lntypes.Preimage
×
425
                if _, err := rand.Read(preimage[:]); err != nil {
×
426
                        return err
×
427
                }
×
428

429
                // Set the preimage. If the user supplied a preimage with the
430
                // data flag, the preimage that is set here will be overwritten
431
                // later.
432
                req.DestCustomRecords[record.KeySendType] = preimage[:]
×
433

×
434
                hash := preimage.Hash()
×
435
                rHash = hash[:]
×
436
        case !ctx.Bool(ampFlag.Name):
×
437
                switch {
×
438
                case ctx.IsSet("payment_hash"):
×
439
                        rHash, err = hex.DecodeString(ctx.String("payment_hash"))
×
440
                case args.Present():
×
441
                        rHash, err = hex.DecodeString(args.First())
×
442
                        args = args.Tail()
×
443
                default:
×
444
                        return fmt.Errorf("payment hash argument missing")
×
445
                }
446
        }
447

448
        if err != nil {
×
449
                return err
×
450
        }
×
451
        if !req.Amp && len(rHash) != 32 {
×
452
                return fmt.Errorf("payment hash must be exactly 32 "+
×
453
                        "bytes, is instead %v", len(rHash))
×
454
        }
×
455
        req.PaymentHash = rHash
×
456

×
457
        switch {
×
458
        case ctx.IsSet("final_cltv_delta"):
×
459
                req.FinalCltvDelta = int32(ctx.Int64("final_cltv_delta"))
×
460
        case args.Present():
×
461
                delta, err := strconv.ParseInt(args.First(), 10, 64)
×
462
                if err != nil {
×
463
                        return err
×
464
                }
×
465
                args = args.Tail()
×
466
                req.FinalCltvDelta = int32(delta)
×
467
        }
468

469
        payAddr, err := parsePayAddr(ctx, args)
×
470
        if err != nil {
×
471
                return err
×
472
        }
×
473

474
        req.PaymentAddr = payAddr
×
475

×
476
        return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
×
477
}
478

479
// SendPaymentFn is a function type that abstracts the SendPaymentV2 call of the
480
// router client.
481
type SendPaymentFn func(ctx context.Context, payConn grpc.ClientConnInterface,
482
        req *routerrpc.SendPaymentRequest) (PaymentResultStream, error)
483

484
// routerRPCSendPayment is the default implementation of the SendPaymentFn type
485
// that uses the lnd routerrpc.SendPaymentV2 call.
486
func routerRPCSendPayment(ctx context.Context, payConn grpc.ClientConnInterface,
487
        req *routerrpc.SendPaymentRequest) (PaymentResultStream, error) {
×
488

×
489
        return routerrpc.NewRouterClient(payConn).SendPaymentV2(ctx, req)
×
490
}
×
491

492
func SendPaymentRequest(ctx *cli.Context, req *routerrpc.SendPaymentRequest,
493
        lnConn, paymentConn grpc.ClientConnInterface,
494
        callSendPayment SendPaymentFn) error {
×
495

×
496
        ctxc := getContext()
×
497

×
498
        lnClient := lnrpc.NewLightningClient(lnConn)
×
499

×
500
        outChan := ctx.Int64Slice("outgoing_chan_id")
×
501
        if len(outChan) != 0 {
×
502
                req.OutgoingChanIds = make([]uint64, len(outChan))
×
503
                for i, c := range outChan {
×
504
                        req.OutgoingChanIds[i] = uint64(c)
×
505
                }
×
506
        }
507

508
        if ctx.IsSet(lastHopFlag.Name) {
×
509
                lastHop, err := route.NewVertexFromStr(
×
510
                        ctx.String(lastHopFlag.Name),
×
511
                )
×
512
                if err != nil {
×
513
                        return err
×
514
                }
×
515
                req.LastHopPubkey = lastHop[:]
×
516
        }
517

518
        req.CltvLimit = int32(ctx.Int(cltvLimitFlag.Name))
×
519

×
520
        pmtTimeout := ctx.Duration("timeout")
×
521
        if pmtTimeout <= 0 {
×
522
                return errors.New("payment timeout must be greater than zero")
×
523
        }
×
524
        req.TimeoutSeconds = int32(pmtTimeout.Seconds())
×
525

×
526
        req.AllowSelfPayment = ctx.Bool("allow_self_payment")
×
527

×
528
        req.MaxParts = uint32(ctx.Uint(maxPartsFlag.Name))
×
529

×
530
        switch {
×
531
        // If the max shard size is specified, then it should either be in sat
532
        // or msat, but not both.
533
        case ctx.Uint64(maxShardSizeMsatFlag.Name) != 0 &&
534
                ctx.Uint64(maxShardSizeSatFlag.Name) != 0:
×
535
                return fmt.Errorf("only --max_split_size_msat or " +
×
536
                        "--max_split_size_sat should be set, but not both")
×
537

538
        case ctx.Uint64(maxShardSizeMsatFlag.Name) != 0:
×
539
                req.MaxShardSizeMsat = ctx.Uint64(maxShardSizeMsatFlag.Name)
×
540

541
        case ctx.Uint64(maxShardSizeSatFlag.Name) != 0:
×
542
                req.MaxShardSizeMsat = uint64(lnwire.NewMSatFromSatoshis(
×
543
                        btcutil.Amount(ctx.Uint64(maxShardSizeSatFlag.Name)),
×
544
                ))
×
545
        }
546

547
        // Parse custom data records.
548
        data := ctx.String(dataFlag.Name)
×
549
        if data != "" {
×
550
                records := strings.Split(data, ",")
×
551
                for _, r := range records {
×
552
                        kv := strings.Split(r, "=")
×
553
                        if len(kv) != 2 {
×
554
                                return errors.New("invalid data format: " +
×
555
                                        "multiple equal signs in record")
×
556
                        }
×
557

558
                        recordID, err := strconv.ParseUint(kv[0], 10, 64)
×
559
                        if err != nil {
×
560
                                return fmt.Errorf("invalid data format: %w",
×
561
                                        err)
×
562
                        }
×
563

564
                        hexValue, err := hex.DecodeString(kv[1])
×
565
                        if err != nil {
×
566
                                return fmt.Errorf("invalid data format: %w",
×
567
                                        err)
×
568
                        }
×
569

570
                        req.DestCustomRecords[recordID] = hexValue
×
571
                }
572
        }
573

574
        var feeLimit int64
×
575
        if req.PaymentRequest != "" {
×
576
                // Decode payment request to find out the amount.
×
577
                decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
×
578
                decodeResp, err := lnClient.DecodePayReq(ctxc, decodeReq)
×
579
                if err != nil {
×
580
                        return err
×
581
                }
×
582

583
                // If amount is present in the request, override the request
584
                // amount.
585
                amt := req.Amt
×
586
                invoiceAmt := decodeResp.GetNumSatoshis()
×
587
                if invoiceAmt != 0 {
×
588
                        amt = invoiceAmt
×
589
                }
×
590

591
                // Calculate fee limit based on the determined amount.
592
                feeLimit, err = retrieveFeeLimit(ctx, amt)
×
593
                if err != nil {
×
594
                        return err
×
595
                }
×
596

597
                // Ask for confirmation of amount and fee limit if payment is
598
                // forced.
599
                if !ctx.Bool("force") {
×
600
                        err := confirmPayReq(decodeResp, amt, feeLimit)
×
601
                        if err != nil {
×
602
                                return err
×
603
                        }
×
604
                }
605
        } else {
×
606
                var err error
×
607
                feeLimit, err = retrieveFeeLimit(ctx, req.Amt)
×
608
                if err != nil {
×
609
                        return err
×
610
                }
×
611
        }
612

613
        req.FeeLimitSat = feeLimit
×
614

×
615
        // Set time pref.
×
616
        req.TimePref = ctx.Float64(timePrefFlag.Name)
×
617

×
618
        // Always print in-flight updates for the table output.
×
619
        printJSON := ctx.Bool(jsonFlag.Name)
×
620
        req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
×
621

×
622
        stream, err := callSendPayment(ctxc, paymentConn, req)
×
623
        if err != nil {
×
624
                return err
×
625
        }
×
626

627
        finalState, err := PrintLivePayment(ctxc, stream, lnClient, printJSON)
×
628
        if err != nil {
×
629
                return err
×
630
        }
×
631

632
        // If we get a payment error back, we pass an error up
633
        // to main which eventually calls fatal() and returns
634
        // with a non-zero exit code.
635
        if finalState.Status != lnrpc.Payment_SUCCEEDED {
×
636
                return errors.New(finalState.Status.String())
×
637
        }
×
638

639
        return nil
×
640
}
641

642
var trackPaymentCommand = cli.Command{
643
        Name:     "trackpayment",
644
        Category: "Payments",
645
        Usage:    "Track progress of an existing payment.",
646
        Description: `
647
        Pick up monitoring the progression of a previously initiated payment
648
        specified by the hash argument.
649
        `,
650
        ArgsUsage: "hash",
651
        Flags: []cli.Flag{
652
                jsonFlag,
653
        },
654
        Action: actionDecorator(trackPayment),
655
}
656

657
func trackPayment(ctx *cli.Context) error {
×
658
        ctxc := getContext()
×
659
        args := ctx.Args()
×
660

×
661
        conn := getClientConn(ctx, false)
×
662
        defer conn.Close()
×
663

×
664
        routerClient := routerrpc.NewRouterClient(conn)
×
665

×
666
        if !args.Present() {
×
667
                return fmt.Errorf("hash argument missing")
×
668
        }
×
669

670
        hash, err := hex.DecodeString(args.First())
×
671
        if err != nil {
×
672
                return err
×
673
        }
×
674

675
        req := &routerrpc.TrackPaymentRequest{
×
676
                PaymentHash: hash,
×
677
        }
×
678

×
679
        stream, err := routerClient.TrackPaymentV2(ctxc, req)
×
680
        if err != nil {
×
681
                return err
×
682
        }
×
683

684
        client := lnrpc.NewLightningClient(conn)
×
685
        _, err = PrintLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
×
686
        return err
×
687
}
688

689
// PaymentResultStream is an interface that abstracts the Recv method of the
690
// SendPaymentV2 or TrackPaymentV2 client stream.
691
type PaymentResultStream interface {
692
        Recv() (*lnrpc.Payment, error)
693
}
694

695
// PrintLivePayment receives payment updates from the given stream and either
696
// outputs them as json or as a more user-friendly formatted table. The table
697
// option uses terminal control codes to rewrite the output. This call
698
// terminates when the payment reaches a final state.
699
func PrintLivePayment(ctxc context.Context, stream PaymentResultStream,
700
        lnClient lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
×
701

×
702
        // Terminal escape codes aren't supported on Windows, fall back to json.
×
703
        if !json && runtime.GOOS == "windows" {
×
704
                json = true
×
705
        }
×
706

707
        aliases := newAliasCache(lnClient)
×
708

×
709
        first := true
×
710
        var lastLineCount int
×
711
        for {
×
712
                payment, err := stream.Recv()
×
713
                if err != nil {
×
714
                        return nil, err
×
715
                }
×
716

717
                if json {
×
718
                        // Delimit json messages by newlines (inspired by
×
719
                        // grpc over rest chunking).
×
720
                        if first {
×
721
                                first = false
×
722
                        } else {
×
723
                                fmt.Println()
×
724
                        }
×
725

726
                        // Write raw json to stdout.
727
                        printRespJSON(payment)
×
728
                } else {
×
729
                        resultTable := formatPayment(ctxc, payment, aliases)
×
730

×
731
                        // Clear all previously written lines and print the
×
732
                        // updated table.
×
733
                        clearLines(lastLineCount)
×
734
                        fmt.Print(resultTable)
×
735

×
736
                        // Store the number of lines written for the next update
×
737
                        // pass.
×
738
                        lastLineCount = 0
×
739
                        for _, b := range resultTable {
×
740
                                if b == '\n' {
×
741
                                        lastLineCount++
×
742
                                }
×
743
                        }
744
                }
745

746
                // Terminate loop if payments state is final.
747
                if payment.Status != lnrpc.Payment_IN_FLIGHT &&
×
748
                        payment.Status != lnrpc.Payment_INITIATED {
×
749

×
750
                        return payment, nil
×
751
                }
×
752
        }
753
}
754

755
// aliasCache allows cached retrieval of node aliases.
756
type aliasCache struct {
757
        cache  map[string]string
758
        client lnrpc.LightningClient
759
}
760

761
func newAliasCache(client lnrpc.LightningClient) *aliasCache {
×
762
        return &aliasCache{
×
763
                client: client,
×
764
                cache:  make(map[string]string),
×
765
        }
×
766
}
×
767

768
// get returns a node alias either from cache or freshly requested from lnd.
769
func (a *aliasCache) get(ctxc context.Context, pubkey string) string {
×
770
        alias, ok := a.cache[pubkey]
×
771
        if ok {
×
772
                return alias
×
773
        }
×
774

775
        // Request node info.
776
        resp, err := a.client.GetNodeInfo(
×
777
                ctxc,
×
778
                &lnrpc.NodeInfoRequest{
×
779
                        PubKey: pubkey,
×
780
                },
×
781
        )
×
782
        if err != nil {
×
783
                // If no info is available, use the
×
784
                // pubkey as identifier.
×
785
                alias = pubkey[:6]
×
786
        } else {
×
787
                alias = resp.Node.Alias
×
788
        }
×
789
        a.cache[pubkey] = alias
×
790

×
791
        return alias
×
792
}
793

794
// formatMsat formats msat amounts as fractional sats.
795
func formatMsat(amt int64) string {
×
796
        return strconv.FormatFloat(float64(amt)/1000.0, 'f', -1, 64)
×
797
}
×
798

799
// formatPayment formats the payment state as an ascii table.
800
func formatPayment(ctxc context.Context, payment *lnrpc.Payment,
801
        aliases *aliasCache) string {
×
802

×
803
        t := table.NewWriter()
×
804

×
805
        // Build table header.
×
806
        t.AppendHeader(table.Row{
×
807
                "HTLC_STATE", "ATTEMPT_TIME", "RESOLVE_TIME", "RECEIVER_AMT",
×
808
                "FEE", "TIMELOCK", "CHAN_OUT", "ROUTE",
×
809
        })
×
810
        t.SetColumnConfigs([]table.ColumnConfig{
×
811
                {Name: "ATTEMPT_TIME", Align: text.AlignRight},
×
812
                {Name: "RESOLVE_TIME", Align: text.AlignRight},
×
813
                {Name: "CHAN_OUT", Align: text.AlignLeft,
×
814
                        AlignHeader: text.AlignLeft},
×
815
        })
×
816

×
817
        // Add all htlcs as rows.
×
818
        createTime := time.Unix(0, payment.CreationTimeNs)
×
819
        var totalPaid, totalFees int64
×
820
        for _, htlc := range payment.Htlcs {
×
821
                formatTime := func(timeNs int64) string {
×
822
                        if timeNs == 0 {
×
823
                                return "-"
×
824
                        }
×
825
                        resolveTime := time.Unix(0, timeNs)
×
826
                        resolveTimeDiff := resolveTime.Sub(createTime)
×
827
                        resolveTimeMs := resolveTimeDiff / time.Millisecond
×
828
                        return fmt.Sprintf(
×
829
                                "%.3f", float64(resolveTimeMs)/1000.0,
×
830
                        )
×
831
                }
832

833
                attemptTime := formatTime(htlc.AttemptTimeNs)
×
834
                resolveTime := formatTime(htlc.ResolveTimeNs)
×
835

×
836
                route := htlc.Route
×
837
                lastHop := route.Hops[len(route.Hops)-1]
×
838

×
839
                hops := []string{}
×
840
                for _, h := range route.Hops {
×
841
                        alias := aliases.get(ctxc, h.PubKey)
×
842
                        hops = append(hops, alias)
×
843
                }
×
844

845
                state := htlc.Status.String()
×
846
                if htlc.Failure != nil {
×
847
                        state = fmt.Sprintf(
×
848
                                "%v @ %s hop",
×
849
                                htlc.Failure.Code,
×
850
                                ordinalNumber(htlc.Failure.FailureSourceIndex),
×
851
                        )
×
852
                }
×
853

854
                t.AppendRow([]interface{}{
×
855
                        state, attemptTime, resolveTime,
×
856
                        formatMsat(lastHop.AmtToForwardMsat),
×
857
                        formatMsat(route.TotalFeesMsat),
×
858
                        route.TotalTimeLock, route.Hops[0].ChanId,
×
859
                        strings.Join(hops, "->")},
×
860
                )
×
861

×
862
                if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
×
863
                        totalPaid += lastHop.AmtToForwardMsat
×
864
                        totalFees += route.TotalFeesMsat
×
865
                }
×
866
        }
867

868
        // Render table.
869
        b := &bytes.Buffer{}
×
870
        t.SetOutputMirror(b)
×
871
        t.Render()
×
872

×
873
        // Add additional payment-level data.
×
874
        fmt.Fprintf(b, "Amount + fee:   %v + %v sat\n",
×
875
                formatMsat(totalPaid), formatMsat(totalFees))
×
876
        fmt.Fprintf(b, "Payment hash:   %v\n", payment.PaymentHash)
×
877
        fmt.Fprintf(b, "Payment status: %v", payment.Status)
×
878
        switch payment.Status {
×
879
        case lnrpc.Payment_SUCCEEDED:
×
880
                fmt.Fprintf(b, ", preimage: %v", payment.PaymentPreimage)
×
881
        case lnrpc.Payment_FAILED:
×
882
                fmt.Fprintf(b, ", reason: %v", payment.FailureReason)
×
883
        }
884
        fmt.Fprintf(b, "\n")
×
885

×
886
        return b.String()
×
887
}
888

889
var payInvoiceCommand = cli.Command{
890
        Name:     "payinvoice",
891
        Category: "Payments",
892
        Usage:    "Pay an invoice over lightning.",
893
        Description: `
894
        This command is a shortcut for 'sendpayment --pay_req='.
895
        `,
896
        ArgsUsage: "pay_req",
897
        Flags: append(PaymentFlags(),
898
                cli.Int64Flag{
899
                        Name: "amt",
900
                        Usage: "(optional) number of satoshis to fulfill the " +
901
                                "invoice",
902
                },
903
        ),
904
        Action: actionDecorator(payInvoice),
905
}
906

907
func payInvoice(ctx *cli.Context) error {
×
908
        conn := getClientConn(ctx, false)
×
909
        defer conn.Close()
×
910

×
911
        args := ctx.Args()
×
912

×
913
        var payReq string
×
914
        switch {
×
915
        case ctx.IsSet("pay_req"):
×
916
                payReq = ctx.String("pay_req")
×
917
        case args.Present():
×
918
                payReq = args.First()
×
919
        default:
×
920
                return fmt.Errorf("pay_req argument missing")
×
921
        }
922

923
        req := &routerrpc.SendPaymentRequest{
×
924
                PaymentRequest:    StripPrefix(payReq),
×
925
                Amt:               ctx.Int64("amt"),
×
926
                DestCustomRecords: make(map[uint64][]byte),
×
927
                Amp:               ctx.Bool(ampFlag.Name),
×
928
                Cancelable:        ctx.Bool(cancelableFlag.Name),
×
929
        }
×
930

×
931
        return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
×
932
}
933

934
var sendToRouteCommand = cli.Command{
935
        Name:     "sendtoroute",
936
        Category: "Payments",
937
        Usage:    "Send a payment over a predefined route.",
938
        Description: `
939
        Send a payment over Lightning using a specific route. One must specify
940
        the route to attempt and the payment hash. This command can even
941
        be chained with the response to queryroutes or buildroute. This command
942
        can be used to implement channel rebalancing by crafting a self-route,
943
        or even atomic swaps using a self-route that crosses multiple chains.
944

945
        There are three ways to specify a route:
946
           * using the --routes parameter to manually specify a JSON encoded
947
             route in the format of the return value of queryroutes or
948
             buildroute:
949
                 (lncli sendtoroute --payment_hash=<pay_hash> --routes=<route>)
950

951
           * passing the route as a positional argument:
952
                 (lncli sendtoroute --payment_hash=pay_hash <route>)
953

954
           * or reading in the route from stdin, which can allow chaining the
955
             response from queryroutes or buildroute, or even read in a file
956
             with a pre-computed route:
957
                 (lncli queryroutes --args.. | lncli sendtoroute --payment_hash= -
958

959
             notice the '-' at the end, which signals that lncli should read
960
             the route in from stdin
961
        `,
962
        Flags: []cli.Flag{
963
                cli.StringFlag{
964
                        Name:  "payment_hash, pay_hash",
965
                        Usage: "the hash to use within the payment's HTLC",
966
                },
967
                cli.StringFlag{
968
                        Name: "routes, r",
969
                        Usage: "a json array string in the format of the response " +
970
                                "of queryroutes that denotes which routes to use",
971
                },
972
                cli.BoolFlag{
973
                        Name: "skip_temp_err",
974
                        Usage: "Whether the payment should be marked as " +
975
                                "failed when a temporary error occurred. Set " +
976
                                "it to true so the payment won't be failed " +
977
                                "unless a terminal error has occurred.",
978
                },
979
        },
980
        Action: sendToRoute,
981
}
982

983
func sendToRoute(ctx *cli.Context) error {
×
984
        // Show command help if no arguments provided.
×
985
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
986
                _ = cli.ShowCommandHelp(ctx, "sendtoroute")
×
987
                return nil
×
988
        }
×
989

990
        args := ctx.Args()
×
991

×
992
        var (
×
993
                rHash []byte
×
994
                err   error
×
995
        )
×
996
        switch {
×
997
        case ctx.IsSet("payment_hash"):
×
998
                rHash, err = hex.DecodeString(ctx.String("payment_hash"))
×
999
        case args.Present():
×
1000
                rHash, err = hex.DecodeString(args.First())
×
1001

×
1002
                args = args.Tail()
×
1003
        default:
×
1004
                return fmt.Errorf("payment hash argument missing")
×
1005
        }
1006

1007
        if err != nil {
×
1008
                return err
×
1009
        }
×
1010

1011
        if len(rHash) != 32 {
×
1012
                return fmt.Errorf("payment hash must be exactly 32 "+
×
1013
                        "bytes, is instead %d", len(rHash))
×
1014
        }
×
1015

1016
        var jsonRoutes string
×
1017
        switch {
×
1018
        // The user is specifying the routes explicitly via the key word
1019
        // argument.
1020
        case ctx.IsSet("routes"):
×
1021
                jsonRoutes = ctx.String("routes")
×
1022

1023
        // The user is specifying the routes as a positional argument.
1024
        case args.Present() && args.First() != "-":
×
1025
                jsonRoutes = args.First()
×
1026

1027
        // The user is signalling that we should read stdin in order to parse
1028
        // the set of target routes.
1029
        case args.Present() && args.First() == "-":
×
1030
                b, err := io.ReadAll(os.Stdin)
×
1031
                if err != nil {
×
1032
                        return err
×
1033
                }
×
1034
                if len(b) == 0 {
×
1035
                        return fmt.Errorf("queryroutes output is empty")
×
1036
                }
×
1037

1038
                jsonRoutes = string(b)
×
1039
        }
1040

1041
        // Try to parse the provided json both in the legacy QueryRoutes format
1042
        // that contains a list of routes and the single route BuildRoute
1043
        // format.
1044
        var route *lnrpc.Route
×
1045
        routes := &lnrpc.QueryRoutesResponse{}
×
1046
        err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal([]byte(jsonRoutes), routes)
×
1047
        if err == nil {
×
1048
                if len(routes.Routes) == 0 {
×
1049
                        return fmt.Errorf("no routes provided")
×
1050
                }
×
1051

1052
                if len(routes.Routes) != 1 {
×
1053
                        return fmt.Errorf("expected a single route, but got %v",
×
1054
                                len(routes.Routes))
×
1055
                }
×
1056

1057
                route = routes.Routes[0]
×
1058
        } else {
×
1059
                routes := &routerrpc.BuildRouteResponse{}
×
1060
                err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(
×
1061
                        []byte(jsonRoutes), routes,
×
1062
                )
×
1063
                if err != nil {
×
1064
                        return fmt.Errorf("unable to unmarshal json string "+
×
1065
                                "from incoming array of routes: %v", err)
×
1066
                }
×
1067

1068
                route = routes.Route
×
1069
        }
1070

1071
        req := &routerrpc.SendToRouteRequest{
×
1072
                PaymentHash: rHash,
×
1073
                Route:       route,
×
1074
                SkipTempErr: ctx.Bool("skip_temp_err"),
×
1075
        }
×
1076

×
1077
        return sendToRouteRequest(ctx, req)
×
1078
}
1079

1080
func sendToRouteRequest(ctx *cli.Context, req *routerrpc.SendToRouteRequest) error {
×
1081
        ctxc := getContext()
×
1082
        conn := getClientConn(ctx, false)
×
1083
        defer conn.Close()
×
1084

×
1085
        client := routerrpc.NewRouterClient(conn)
×
1086

×
1087
        resp, err := client.SendToRouteV2(ctxc, req)
×
1088
        if err != nil {
×
1089
                return err
×
1090
        }
×
1091

1092
        printRespJSON(resp)
×
1093

×
1094
        return nil
×
1095
}
1096

1097
var queryRoutesCommand = cli.Command{
1098
        Name:        "queryroutes",
1099
        Category:    "Payments",
1100
        Usage:       "Query a route to a destination.",
1101
        Description: "Queries the channel router for a potential path to the destination that has sufficient flow for the amount including fees",
1102
        ArgsUsage:   "dest amt",
1103
        Flags: []cli.Flag{
1104
                cli.StringFlag{
1105
                        Name: "dest",
1106
                        Usage: "the 33-byte hex-encoded public key for the payment " +
1107
                                "destination",
1108
                },
1109
                cli.Int64Flag{
1110
                        Name:  "amt",
1111
                        Usage: "the amount to send expressed in satoshis",
1112
                },
1113
                cli.Int64Flag{
1114
                        Name: "fee_limit",
1115
                        Usage: "maximum fee allowed in satoshis when sending " +
1116
                                "the payment",
1117
                },
1118
                cli.Int64Flag{
1119
                        Name: "fee_limit_percent",
1120
                        Usage: "percentage of the payment's amount used as the " +
1121
                                "maximum fee allowed when sending the payment",
1122
                },
1123
                cli.Int64Flag{
1124
                        Name: "final_cltv_delta",
1125
                        Usage: "(optional) number of blocks the last hop has " +
1126
                                "to reveal the preimage. Note that this " +
1127
                                "should not be set in the case where the " +
1128
                                "path includes a blinded path since in " +
1129
                                "that case, the receiver will already have " +
1130
                                "accounted for this value in the " +
1131
                                "blinded_cltv value",
1132
                },
1133
                cli.BoolFlag{
1134
                        Name:  "use_mc",
1135
                        Usage: "use mission control probabilities",
1136
                },
1137
                cli.Uint64Flag{
1138
                        Name: "outgoing_chan_id",
1139
                        Usage: "(optional) the channel id of the channel " +
1140
                                "that must be taken to the first hop",
1141
                },
1142
                cli.StringSliceFlag{
1143
                        Name: "ignore_pair",
1144
                        Usage: "ignore directional node pair " +
1145
                                "<node1>:<node2>. This flag can be specified " +
1146
                                "multiple times if multiple node pairs are " +
1147
                                "to be ignored",
1148
                },
1149
                timePrefFlag,
1150
                cltvLimitFlag,
1151
                introductionNodeFlag,
1152
                blindingPointFlag,
1153
                blindedHopsFlag,
1154
                blindedBaseFlag,
1155
                blindedPPMFlag,
1156
                blindedCLTVFlag,
1157
        },
1158
        Action: actionDecorator(queryRoutes),
1159
}
1160

1161
func queryRoutes(ctx *cli.Context) error {
×
1162
        ctxc := getContext()
×
1163
        client, cleanUp := getClient(ctx)
×
1164
        defer cleanUp()
×
1165

×
1166
        var (
×
1167
                dest string
×
1168
                amt  int64
×
1169
                err  error
×
1170
        )
×
1171

×
1172
        args := ctx.Args()
×
1173

×
1174
        switch {
×
1175
        case ctx.IsSet("dest"):
×
1176
                dest = ctx.String("dest")
×
1177

1178
        case args.Present():
×
1179
                dest = args.First()
×
1180
                args = args.Tail()
×
1181

1182
        // If we have a blinded path set, we don't have to specify a
1183
        // destination.
1184
        case ctx.IsSet(introductionNodeFlag.Name):
×
1185

1186
        default:
×
1187
                return fmt.Errorf("dest argument missing")
×
1188
        }
1189

1190
        switch {
×
1191
        case ctx.IsSet("amt"):
×
1192
                amt = ctx.Int64("amt")
×
1193
        case args.Present():
×
1194
                amt, err = strconv.ParseInt(args.First(), 10, 64)
×
1195
                if err != nil {
×
1196
                        return fmt.Errorf("unable to decode amt argument: %w",
×
1197
                                err)
×
1198
                }
×
1199
        default:
×
1200
                return fmt.Errorf("amt argument missing")
×
1201
        }
1202

1203
        feeLimit, err := retrieveFeeLimitLegacy(ctx)
×
1204
        if err != nil {
×
1205
                return err
×
1206
        }
×
1207

1208
        pairs := ctx.StringSlice("ignore_pair")
×
1209
        ignoredPairs := make([]*lnrpc.NodePair, len(pairs))
×
1210
        for i, pair := range pairs {
×
1211
                nodes := strings.Split(pair, ":")
×
1212
                if len(nodes) != 2 {
×
1213
                        return fmt.Errorf("invalid node pair format. " +
×
1214
                                "Expected <node1 pub key>:<node2 pub key>")
×
1215
                }
×
1216

1217
                node1, err := hex.DecodeString(nodes[0])
×
1218
                if err != nil {
×
1219
                        return err
×
1220
                }
×
1221

1222
                node2, err := hex.DecodeString(nodes[1])
×
1223
                if err != nil {
×
1224
                        return err
×
1225
                }
×
1226

1227
                ignoredPairs[i] = &lnrpc.NodePair{
×
1228
                        From: node1,
×
1229
                        To:   node2,
×
1230
                }
×
1231
        }
1232

1233
        blindedRoutes, err := parseBlindedPaymentParameters(ctx)
×
1234
        if err != nil {
×
1235
                return err
×
1236
        }
×
1237

1238
        req := &lnrpc.QueryRoutesRequest{
×
1239
                PubKey:              dest,
×
1240
                Amt:                 amt,
×
1241
                FeeLimit:            feeLimit,
×
1242
                FinalCltvDelta:      int32(ctx.Int("final_cltv_delta")),
×
1243
                UseMissionControl:   ctx.Bool("use_mc"),
×
1244
                CltvLimit:           uint32(ctx.Uint64(cltvLimitFlag.Name)),
×
1245
                OutgoingChanId:      ctx.Uint64("outgoing_chan_id"),
×
1246
                TimePref:            ctx.Float64(timePrefFlag.Name),
×
1247
                IgnoredPairs:        ignoredPairs,
×
1248
                BlindedPaymentPaths: blindedRoutes,
×
1249
        }
×
1250

×
1251
        route, err := client.QueryRoutes(ctxc, req)
×
1252
        if err != nil {
×
1253
                return err
×
1254
        }
×
1255

1256
        printRespJSON(route)
×
1257

×
1258
        return nil
×
1259
}
1260

1261
func parseBlindedPaymentParameters(ctx *cli.Context) (
1262
        []*lnrpc.BlindedPaymentPath, error) {
×
1263

×
1264
        // Return nil if we don't have a blinding set, as we don't have a
×
1265
        // blinded path.
×
1266
        if !ctx.IsSet(blindingPointFlag.Name) {
×
1267
                return nil, nil
×
1268
        }
×
1269

1270
        // If a blinded path has been provided, then the final_cltv_delta flag
1271
        // should not be provided since this value will be ignored.
1272
        if ctx.IsSet("final_cltv_delta") {
×
1273
                return nil, fmt.Errorf("`final_cltv_delta` should not be " +
×
1274
                        "provided if a blinded path is provided")
×
1275
        }
×
1276

1277
        // If any one of our blinding related flags is set, we expect the
1278
        // full set to be set and we'll error out accordingly.
1279
        introNode, err := route.NewVertexFromStr(
×
1280
                ctx.String(introductionNodeFlag.Name),
×
1281
        )
×
1282
        if err != nil {
×
1283
                return nil, fmt.Errorf("decode introduction node: %w", err)
×
1284
        }
×
1285

1286
        blindingPoint, err := route.NewVertexFromStr(ctx.String(
×
1287
                blindingPointFlag.Name,
×
1288
        ))
×
1289
        if err != nil {
×
1290
                return nil, fmt.Errorf("decode blinding point: %w", err)
×
1291
        }
×
1292

1293
        blindedHops := ctx.StringSlice(blindedHopsFlag.Name)
×
1294

×
1295
        pmt := &lnrpc.BlindedPaymentPath{
×
1296
                BlindedPath: &lnrpc.BlindedPath{
×
1297
                        IntroductionNode: introNode[:],
×
1298
                        BlindingPoint:    blindingPoint[:],
×
1299
                        BlindedHops: make(
×
1300
                                []*lnrpc.BlindedHop, len(blindedHops),
×
1301
                        ),
×
1302
                },
×
1303
                BaseFeeMsat: ctx.Uint64(
×
1304
                        blindedBaseFlag.Name,
×
1305
                ),
×
1306
                ProportionalFeeRate: uint32(ctx.Uint64(
×
1307
                        blindedPPMFlag.Name,
×
1308
                )),
×
1309
                TotalCltvDelta: uint32(ctx.Uint64(
×
1310
                        blindedCLTVFlag.Name,
×
1311
                )),
×
1312
        }
×
1313

×
1314
        for i, hop := range blindedHops {
×
1315
                parts := strings.Split(hop, ":")
×
1316
                if len(parts) != 2 {
×
1317
                        return nil, fmt.Errorf("blinded hops should be "+
×
1318
                                "expressed as "+
×
1319
                                "blinded_node_id:hex_encrypted_data, got: %v",
×
1320
                                hop)
×
1321
                }
×
1322

1323
                hop, err := route.NewVertexFromStr(parts[0])
×
1324
                if err != nil {
×
1325
                        return nil, fmt.Errorf("hop: %v node: %w", i, err)
×
1326
                }
×
1327

1328
                data, err := hex.DecodeString(parts[1])
×
1329
                if err != nil {
×
1330
                        return nil, fmt.Errorf("hop: %v data: %w", i, err)
×
1331
                }
×
1332

1333
                pmt.BlindedPath.BlindedHops[i] = &lnrpc.BlindedHop{
×
1334
                        BlindedNode:   hop[:],
×
1335
                        EncryptedData: data,
×
1336
                }
×
1337
        }
1338

1339
        return []*lnrpc.BlindedPaymentPath{
×
1340
                pmt,
×
1341
        }, nil
×
1342
}
1343

1344
// retrieveFeeLimitLegacy retrieves the fee limit based on the different fee
1345
// limit flags passed. This function will eventually disappear in favor of
1346
// retrieveFeeLimit and the new payment rpc.
1347
func retrieveFeeLimitLegacy(ctx *cli.Context) (*lnrpc.FeeLimit, error) {
×
1348
        switch {
×
1349
        case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"):
×
1350
                return nil, fmt.Errorf("either fee_limit or fee_limit_percent " +
×
1351
                        "can be set, but not both")
×
1352
        case ctx.IsSet("fee_limit"):
×
1353
                return &lnrpc.FeeLimit{
×
1354
                        Limit: &lnrpc.FeeLimit_Fixed{
×
1355
                                Fixed: ctx.Int64("fee_limit"),
×
1356
                        },
×
1357
                }, nil
×
1358
        case ctx.IsSet("fee_limit_percent"):
×
1359
                feeLimitPercent := ctx.Int64("fee_limit_percent")
×
1360
                if feeLimitPercent < 0 {
×
1361
                        return nil, errors.New("negative fee limit percentage " +
×
1362
                                "provided")
×
1363
                }
×
1364
                return &lnrpc.FeeLimit{
×
1365
                        Limit: &lnrpc.FeeLimit_Percent{
×
1366
                                Percent: feeLimitPercent,
×
1367
                        },
×
1368
                }, nil
×
1369
        }
1370

1371
        // Since the fee limit flags aren't required, we don't return an error
1372
        // if they're not set.
1373
        return nil, nil
×
1374
}
1375

1376
var listPaymentsCommand = cli.Command{
1377
        Name:     "listpayments",
1378
        Category: "Payments",
1379
        Usage:    "List all outgoing payments.",
1380
        Description: `
1381
        This command enables the retrieval of payments stored
1382
        in the database.
1383

1384
        Pagination is supported by the usage of index_offset in combination with
1385
        the paginate_forwards flag.
1386
        Reversed pagination is enabled by default to receive current payments
1387
        first. Pagination can be resumed by using the returned last_index_offset
1388
        (for forwards order), or first_index_offset (for reversed order) as the
1389
        offset_index.
1390

1391
        Because counting all payments in the payment database can take a long
1392
        time on systems with many payments, the count is not returned by
1393
        default. That feature can be turned on with the --count_total_payments
1394
        flag.
1395
        `,
1396
        Flags: []cli.Flag{
1397
                cli.BoolFlag{
1398
                        Name: "include_incomplete",
1399
                        Usage: "if set to true, payments still in flight (or " +
1400
                                "failed) will be returned as well, keeping" +
1401
                                "indices for payments the same as without " +
1402
                                "the flag",
1403
                },
1404
                cli.UintFlag{
1405
                        Name: "index_offset",
1406
                        Usage: "The index of a payment that will be used as " +
1407
                                "either the start (in forwards mode) or end " +
1408
                                "(in reverse mode) of a query to determine " +
1409
                                "which payments should be returned in the " +
1410
                                "response, where the index_offset is " +
1411
                                "excluded. If index_offset is set to zero in " +
1412
                                "reversed mode, the query will end with the " +
1413
                                "last payment made.",
1414
                },
1415
                cli.UintFlag{
1416
                        Name: "max_payments",
1417
                        Usage: "the max number of payments to return, by " +
1418
                                "default, all completed payments are returned",
1419
                },
1420
                cli.BoolFlag{
1421
                        Name: "paginate_forwards",
1422
                        Usage: "if set, payments succeeding the " +
1423
                                "index_offset will be returned, allowing " +
1424
                                "forwards pagination",
1425
                },
1426
                cli.BoolFlag{
1427
                        Name: "count_total_payments",
1428
                        Usage: "if set, all payments (complete or incomplete, " +
1429
                                "independent of max_payments parameter) will " +
1430
                                "be counted; can take a long time on systems " +
1431
                                "with many payments",
1432
                },
1433
                cli.Uint64Flag{
1434
                        Name: "creation_date_start",
1435
                        Usage: "timestamp in seconds, if set, filter " +
1436
                                "payments with creation date greater than or " +
1437
                                "equal to it",
1438
                },
1439
                cli.Uint64Flag{
1440
                        Name: "creation_date_end",
1441
                        Usage: "timestamp in seconds, if set, filter " +
1442
                                "payments with creation date less than or " +
1443
                                "equal to it",
1444
                },
1445
        },
1446
        Action: actionDecorator(listPayments),
1447
}
1448

1449
func listPayments(ctx *cli.Context) error {
×
1450
        ctxc := getContext()
×
1451
        client, cleanUp := getClient(ctx)
×
1452
        defer cleanUp()
×
1453

×
1454
        req := &lnrpc.ListPaymentsRequest{
×
1455
                IncludeIncomplete:  ctx.Bool("include_incomplete"),
×
1456
                IndexOffset:        uint64(ctx.Uint("index_offset")),
×
1457
                MaxPayments:        uint64(ctx.Uint("max_payments")),
×
1458
                Reversed:           !ctx.Bool("paginate_forwards"),
×
1459
                CountTotalPayments: ctx.Bool("count_total_payments"),
×
1460
                CreationDateStart:  ctx.Uint64("creation_date_start"),
×
1461
                CreationDateEnd:    ctx.Uint64("creation_date_end"),
×
1462
        }
×
1463

×
1464
        payments, err := client.ListPayments(ctxc, req)
×
1465
        if err != nil {
×
1466
                return err
×
1467
        }
×
1468

1469
        printRespJSON(payments)
×
1470
        return nil
×
1471
}
1472

1473
var forwardingHistoryCommand = cli.Command{
1474
        Name:     "fwdinghistory",
1475
        Category: "Payments",
1476
        Usage:    "Query the history of all forwarded HTLCs.",
1477
        ArgsUsage: "start_time [end_time] [index_offset] [max_events] " +
1478
                "[incoming_channel_ids] [outgoing_channel_ids]",
1479
        Description: `
1480
        Query the HTLC switch's internal forwarding log for all completed
1481
        payment circuits (HTLCs) over a particular time range (--start_time and
1482
        --end_time). The start and end times are meant to be expressed in
1483
        seconds since the Unix epoch.
1484
        Alternatively negative time ranges can be used, e.g. "-3d". Supports
1485
        s(seconds), m(minutes), h(ours), d(ays), w(eeks), M(onths), y(ears).
1486
        Month equals 30.44 days, year equals 365.25 days.
1487
        If --start_time isn't provided, then 24 hours ago is used. If
1488
        --end_time isn't provided, then the current time is used.
1489

1490
        The max number of events returned is 50k. The default number is 100,
1491
        callers can use the --max_events param to modify this value.
1492

1493
        Incoming and outgoing channel IDs can be provided to further filter
1494
        the events. If not provided, all events will be returned.
1495

1496
        Finally, callers can skip a series of events using the --index_offset
1497
        parameter. Each response will contain the offset index of the last
1498
        entry. Using this callers can manually paginate within a time slice.
1499
        `,
1500
        Flags: []cli.Flag{
1501
                cli.StringFlag{
1502
                        Name: "start_time",
1503
                        Usage: "the starting time for the query " +
1504
                                `as unix timestamp or relative e.g. "-1w"`,
1505
                },
1506
                cli.StringFlag{
1507
                        Name: "end_time",
1508
                        Usage: "the end time for the query " +
1509
                                `as unix timestamp or relative e.g. "-1w"`,
1510
                },
1511
                cli.Int64Flag{
1512
                        Name:  "index_offset",
1513
                        Usage: "the number of events to skip",
1514
                },
1515
                cli.Int64Flag{
1516
                        Name:  "max_events",
1517
                        Usage: "the max number of events to return",
1518
                },
1519
                cli.BoolFlag{
1520
                        Name: "skip_peer_alias_lookup",
1521
                        Usage: "skip the peer alias lookup per forwarding " +
1522
                                "event in order to improve performance",
1523
                },
1524
                cli.Int64SliceFlag{
1525
                        Name: "incoming_chan_ids",
1526
                        Usage: "the short channel ids of the incoming " +
1527
                                "channels to filter events by",
1528
                },
1529
                cli.Int64SliceFlag{
1530
                        Name: "outgoing_chan_ids",
1531
                        Usage: "the short channel ids of the outgoing " +
1532
                                "channels to filter events by",
1533
                },
1534
        },
1535
        Action: actionDecorator(forwardingHistory),
1536
}
1537

1538
func forwardingHistory(ctx *cli.Context) error {
×
1539
        ctxc := getContext()
×
1540
        client, cleanUp := getClient(ctx)
×
1541
        defer cleanUp()
×
1542

×
1543
        var (
×
1544
                startTime, endTime     uint64
×
1545
                indexOffset, maxEvents uint32
×
1546
                err                    error
×
1547
        )
×
1548
        args := ctx.Args()
×
1549
        now := time.Now()
×
1550

×
1551
        switch {
×
1552
        case ctx.IsSet("start_time"):
×
1553
                startTime, err = parseTime(ctx.String("start_time"), now)
×
1554
        case args.Present():
×
1555
                startTime, err = parseTime(args.First(), now)
×
1556
                args = args.Tail()
×
1557
        default:
×
1558
                now := time.Now()
×
1559
                startTime = uint64(now.Add(-time.Hour * 24).Unix())
×
1560
        }
1561
        if err != nil {
×
1562
                return fmt.Errorf("unable to decode start_time: %w", err)
×
1563
        }
×
1564

1565
        switch {
×
1566
        case ctx.IsSet("end_time"):
×
1567
                endTime, err = parseTime(ctx.String("end_time"), now)
×
1568
        case args.Present():
×
1569
                endTime, err = parseTime(args.First(), now)
×
1570
                args = args.Tail()
×
1571
        default:
×
1572
                endTime = uint64(now.Unix())
×
1573
        }
1574
        if err != nil {
×
1575
                return fmt.Errorf("unable to decode end_time: %w", err)
×
1576
        }
×
1577

1578
        switch {
×
1579
        case ctx.IsSet("index_offset"):
×
1580
                indexOffset = uint32(ctx.Int64("index_offset"))
×
1581
        case args.Present():
×
1582
                i, err := strconv.ParseInt(args.First(), 10, 64)
×
1583
                if err != nil {
×
1584
                        return fmt.Errorf("unable to decode index_offset: %w",
×
1585
                                err)
×
1586
                }
×
1587
                indexOffset = uint32(i)
×
1588
                args = args.Tail()
×
1589
        }
1590

1591
        switch {
×
1592
        case ctx.IsSet("max_events"):
×
1593
                maxEvents = uint32(ctx.Int64("max_events"))
×
1594
        case args.Present():
×
1595
                m, err := strconv.ParseInt(args.First(), 10, 64)
×
1596
                if err != nil {
×
1597
                        return fmt.Errorf("unable to decode max_events: %w",
×
1598
                                err)
×
1599
                }
×
1600
                maxEvents = uint32(m)
×
1601
        }
1602

1603
        // By default we will look up the peers' alias information unless the
1604
        // skip_peer_alias_lookup flag is specified.
1605
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1606

×
1607
        req := &lnrpc.ForwardingHistoryRequest{
×
1608
                StartTime:       startTime,
×
1609
                EndTime:         endTime,
×
1610
                IndexOffset:     indexOffset,
×
1611
                NumMaxEvents:    maxEvents,
×
1612
                PeerAliasLookup: lookupPeerAlias,
×
1613
        }
×
NEW
1614
        outChan := ctx.Int64Slice("outgoing_chan_ids")
×
NEW
1615
        if len(outChan) != 0 {
×
NEW
1616
                req.OutgoingChanIds = make([]uint64, len(outChan))
×
NEW
1617
                for i, c := range outChan {
×
NEW
1618
                        req.OutgoingChanIds[i] = uint64(c)
×
NEW
1619
                }
×
1620
        }
1621

NEW
1622
        inChan := ctx.Int64Slice("incoming_chan_ids")
×
NEW
1623
        if len(inChan) != 0 {
×
NEW
1624
                req.IncomingChanIds = make([]uint64, len(inChan))
×
NEW
1625
                for i, c := range inChan {
×
NEW
1626
                        req.IncomingChanIds[i] = uint64(c)
×
NEW
1627
                }
×
1628
        }
1629
        resp, err := client.ForwardingHistory(ctxc, req)
×
1630
        if err != nil {
×
1631
                return err
×
1632
        }
×
1633

1634
        printRespJSON(resp)
×
1635
        return nil
×
1636
}
1637

1638
var buildRouteCommand = cli.Command{
1639
        Name:     "buildroute",
1640
        Category: "Payments",
1641
        Usage:    "Build a route from a list of hop pubkeys.",
1642
        Description: `
1643
        Builds a sphinx route for the supplied hops (public keys). Make sure to
1644
        use a custom final_cltv_delta to create the route depending on the
1645
        restrictions in the invoice otherwise LND will use its default specified
1646
        via the bitcoin.timelockdelta setting (default 80).
1647
        If the final_cltv_delta mismatch you will likely see the error
1648
        INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS returned by the receiving node.
1649

1650
        Moreover a payment_addr has to be provided if the invoice supplied it as
1651
        well otherwise the payment will be rejected by the receiving node.
1652
        `,
1653
        Action: actionDecorator(buildRoute),
1654
        Flags: []cli.Flag{
1655
                cli.Int64Flag{
1656
                        Name: "amt",
1657
                        Usage: "the amount to send expressed in satoshis. If" +
1658
                                "not set, the minimum routable amount is used",
1659
                },
1660
                cli.Int64Flag{
1661
                        Name: "final_cltv_delta",
1662
                        Usage: "number of blocks the last hop has to reveal " +
1663
                                "the preimage; if not set the default lnd " +
1664
                                "final_cltv_delta is used",
1665
                },
1666
                cli.StringFlag{
1667
                        Name:  "hops",
1668
                        Usage: "comma separated hex pubkeys",
1669
                },
1670
                cli.Uint64Flag{
1671
                        Name: "outgoing_chan_id",
1672
                        Usage: "short channel id of the outgoing channel to " +
1673
                                "use for the first hop of the payment",
1674
                        Value: 0,
1675
                },
1676
                cli.StringFlag{
1677
                        Name: "payment_addr",
1678
                        Usage: "hex encoded payment address to set in the " +
1679
                                "last hop's mpp record",
1680
                },
1681
        },
1682
}
1683

1684
func buildRoute(ctx *cli.Context) error {
×
1685
        ctxc := getContext()
×
1686
        conn := getClientConn(ctx, false)
×
1687
        defer conn.Close()
×
1688

×
1689
        client := routerrpc.NewRouterClient(conn)
×
1690

×
1691
        if !ctx.IsSet("hops") {
×
1692
                return errors.New("hops required")
×
1693
        }
×
1694

1695
        // Build list of hop addresses for the rpc.
1696
        hops := strings.Split(ctx.String("hops"), ",")
×
1697
        rpcHops := make([][]byte, 0, len(hops))
×
1698
        for _, k := range hops {
×
1699
                pubkey, err := route.NewVertexFromStr(k)
×
1700
                if err != nil {
×
1701
                        return fmt.Errorf("error parsing %v: %w", k, err)
×
1702
                }
×
1703
                rpcHops = append(rpcHops, pubkey[:])
×
1704
        }
1705

1706
        var amtMsat int64
×
1707
        hasAmt := ctx.IsSet("amt")
×
1708
        if hasAmt {
×
1709
                amtMsat = ctx.Int64("amt") * 1000
×
1710
                if amtMsat == 0 {
×
1711
                        return fmt.Errorf("non-zero amount required")
×
1712
                }
×
1713
        }
1714

1715
        var (
×
1716
                payAddr []byte
×
1717
                err     error
×
1718
        )
×
1719

×
1720
        if ctx.IsSet("payment_addr") {
×
1721
                payAddr, err = hex.DecodeString(ctx.String("payment_addr"))
×
1722
                if err != nil {
×
1723
                        return fmt.Errorf("error parsing payment_addr: %w", err)
×
1724
                }
×
1725
        }
1726

1727
        // Call BuildRoute rpc.
1728
        req := &routerrpc.BuildRouteRequest{
×
1729
                AmtMsat:        amtMsat,
×
1730
                FinalCltvDelta: int32(ctx.Int64("final_cltv_delta")),
×
1731
                HopPubkeys:     rpcHops,
×
1732
                OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
×
1733
                PaymentAddr:    payAddr,
×
1734
        }
×
1735

×
1736
        route, err := client.BuildRoute(ctxc, req)
×
1737
        if err != nil {
×
1738
                return err
×
1739
        }
×
1740

1741
        printRespJSON(route)
×
1742

×
1743
        return nil
×
1744
}
1745

1746
var deletePaymentsCommand = cli.Command{
1747
        Name:     "deletepayments",
1748
        Category: "Payments",
1749
        Usage:    "Delete a single or multiple payments from the database.",
1750
        ArgsUsage: "--all [--failed_htlcs_only --include_non_failed] | " +
1751
                "--payment_hash hash [--failed_htlcs_only]",
1752
        Description: `
1753
        This command either deletes all failed payments or a single payment from
1754
        the database to reclaim disk space.
1755

1756
        If the --all flag is used, then all failed payments are removed. If so
1757
        desired, _ALL_ payments (even the successful ones) can be deleted
1758
        by additionally specifying --include_non_failed.
1759

1760
        If a --payment_hash is specified, that single payment is deleted,
1761
        independent of its state.
1762

1763
        If --failed_htlcs_only is specified then the payments themselves (or the
1764
        single payment itself if used with --payment_hash) is not deleted, only
1765
        the information about any failed HTLC attempts during the payment.
1766

1767
        NOTE: Removing payments from the database does free up disk space within
1768
        the internal bbolt database. But that disk space is only reclaimed after
1769
        compacting the database. Users might want to turn on auto compaction
1770
        (db.bolt.auto-compact=true in the config file or --db.bolt.auto-compact
1771
        as a command line flag) and restart lnd after deleting a large number of
1772
        payments to see a reduction in the file size of the channel.db file.
1773
        `,
1774
        Action: actionDecorator(deletePayments),
1775
        Flags: []cli.Flag{
1776
                cli.BoolFlag{
1777
                        Name:  "all",
1778
                        Usage: "delete all failed payments",
1779
                },
1780
                cli.StringFlag{
1781
                        Name: "payment_hash",
1782
                        Usage: "delete a specific payment identified by its " +
1783
                                "payment hash",
1784
                },
1785
                cli.BoolFlag{
1786
                        Name: "failed_htlcs_only",
1787
                        Usage: "only delete failed HTLCs from payments, not " +
1788
                                "the payment itself",
1789
                },
1790
                cli.BoolFlag{
1791
                        Name:  "include_non_failed",
1792
                        Usage: "delete ALL payments, not just the failed ones",
1793
                },
1794
        },
1795
}
1796

1797
func deletePayments(ctx *cli.Context) error {
×
1798
        ctxc := getContext()
×
1799
        client, cleanUp := getClient(ctx)
×
1800
        defer cleanUp()
×
1801

×
1802
        // Show command help if arguments or no flags are provided.
×
1803
        if ctx.NArg() > 0 || ctx.NumFlags() == 0 {
×
1804
                _ = cli.ShowCommandHelp(ctx, "deletepayments")
×
1805
                return nil
×
1806
        }
×
1807

1808
        var (
×
1809
                paymentHash      []byte
×
1810
                all              = ctx.Bool("all")
×
1811
                singlePayment    = ctx.IsSet("payment_hash")
×
1812
                failedHTLCsOnly  = ctx.Bool("failed_htlcs_only")
×
1813
                includeNonFailed = ctx.Bool("include_non_failed")
×
1814
                err              error
×
1815
                resp             proto.Message
×
1816
        )
×
1817

×
1818
        // We pack two RPCs into the same CLI so there are a few non-valid
×
1819
        // combinations of the flags we need to filter out.
×
1820
        switch {
×
1821
        case all && singlePayment:
×
1822
                return fmt.Errorf("cannot use --all and --payment_hash at " +
×
1823
                        "the same time")
×
1824

1825
        case singlePayment && includeNonFailed:
×
1826
                return fmt.Errorf("cannot use --payment_hash and " +
×
1827
                        "--include_non_failed at the same time, when using " +
×
1828
                        "a payment hash the payment is deleted independent " +
×
1829
                        "of its state")
×
1830
        }
1831

1832
        // Deleting a single payment is implemented in a different RPC than
1833
        // removing all/multiple payments.
1834
        switch {
×
1835
        case singlePayment:
×
1836
                paymentHash, err = hex.DecodeString(ctx.String("payment_hash"))
×
1837
                if err != nil {
×
1838
                        return fmt.Errorf("error decoding payment_hash: %w",
×
1839
                                err)
×
1840
                }
×
1841

1842
                resp, err = client.DeletePayment(
×
1843
                        ctxc, &lnrpc.DeletePaymentRequest{
×
1844
                                PaymentHash:     paymentHash,
×
1845
                                FailedHtlcsOnly: failedHTLCsOnly,
×
1846
                        },
×
1847
                )
×
1848
                if err != nil {
×
1849
                        return fmt.Errorf("error deleting single payment: %w",
×
1850
                                err)
×
1851
                }
×
1852

1853
        case all:
×
1854
                what := "failed"
×
1855
                if includeNonFailed {
×
1856
                        what = "all"
×
1857
                }
×
1858
                if failedHTLCsOnly {
×
1859
                        what = fmt.Sprintf("failed HTLCs from %s", what)
×
1860
                }
×
1861

1862
                fmt.Printf("Removing %s payments, this might take a while...\n",
×
1863
                        what)
×
1864
                resp, err = client.DeleteAllPayments(
×
1865
                        ctxc, &lnrpc.DeleteAllPaymentsRequest{
×
1866
                                AllPayments:        includeNonFailed,
×
1867
                                FailedPaymentsOnly: !includeNonFailed,
×
1868
                                FailedHtlcsOnly:    failedHTLCsOnly,
×
1869
                        },
×
1870
                )
×
1871
                if err != nil {
×
1872
                        return fmt.Errorf("error deleting payments: %w", err)
×
1873
                }
×
1874
        }
1875

1876
        printJSON(resp)
×
1877

×
1878
        return nil
×
1879
}
1880

1881
var estimateRouteFeeCommand = cli.Command{
1882
        Name:     "estimateroutefee",
1883
        Category: "Payments",
1884
        Usage:    "Estimate routing fees based on a destination or an invoice.",
1885
        Action:   actionDecorator(estimateRouteFee),
1886
        Flags: []cli.Flag{
1887
                cli.StringFlag{
1888
                        Name: "dest",
1889
                        Usage: "the 33-byte hex-encoded public key for the " +
1890
                                "probe destination. If it is specified then " +
1891
                                "the amt flag is required. If it isn't " +
1892
                                "specified then the pay_req field has to.",
1893
                },
1894
                cli.Int64Flag{
1895
                        Name: "amt",
1896
                        Usage: "the payment amount expressed in satoshis " +
1897
                                "that should be probed for. This field is " +
1898
                                "mandatory if dest is specified.",
1899
                },
1900
                cli.StringFlag{
1901
                        Name: "pay_req",
1902
                        Usage: "a zpay32 encoded payment request which is " +
1903
                                "used to probe. If the destination is " +
1904
                                "not public then route hints are scanned for " +
1905
                                "a public node.",
1906
                },
1907
                cli.DurationFlag{
1908
                        Name: "timeout",
1909
                        Usage: "a deadline for the probe attempt. Only " +
1910
                                "applicable if pay_req is specified.",
1911
                        Value: paymentTimeout,
1912
                },
1913
        },
1914
}
1915

1916
func estimateRouteFee(ctx *cli.Context) error {
×
1917
        ctxc := getContext()
×
1918
        conn := getClientConn(ctx, false)
×
1919
        defer conn.Close()
×
1920

×
1921
        client := routerrpc.NewRouterClient(conn)
×
1922

×
1923
        req := &routerrpc.RouteFeeRequest{}
×
1924

×
1925
        switch {
×
1926
        case ctx.IsSet("dest") && ctx.IsSet("pay_req"):
×
1927
                return fmt.Errorf("either dest or pay_req can be set")
×
1928

1929
        case ctx.IsSet("dest") && !ctx.IsSet("amt"):
×
1930
                return fmt.Errorf("amt is required when dest is set")
×
1931

1932
        case ctx.IsSet("dest"):
×
1933
                dest, err := hex.DecodeString(ctx.String("dest"))
×
1934
                if err != nil {
×
1935
                        return err
×
1936
                }
×
1937

1938
                if len(dest) != 33 {
×
1939
                        return fmt.Errorf("dest node pubkey must be exactly "+
×
1940
                                "33 bytes, is instead: %v", len(dest))
×
1941
                }
×
1942

1943
                amtSat := ctx.Int64("amt")
×
1944
                if amtSat == 0 {
×
1945
                        return fmt.Errorf("non-zero amount required")
×
1946
                }
×
1947

1948
                req.Dest = dest
×
1949
                req.AmtSat = amtSat
×
1950

1951
        case ctx.IsSet("pay_req"):
×
1952
                req.PaymentRequest = StripPrefix(ctx.String("pay_req"))
×
1953
                req.Timeout = uint32(ctx.Duration("timeout").Seconds())
×
1954

1955
        default:
×
1956
                return fmt.Errorf("fee estimation arguments missing")
×
1957
        }
1958

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

1964
        printRespJSON(resp)
×
1965

×
1966
        return nil
×
1967
}
1968

1969
// ESC is the ASCII code for escape character.
1970
const ESC = 27
1971

1972
// clearCode defines a terminal escape code to clear the current line and move
1973
// the cursor up.
1974
var clearCode = fmt.Sprintf("%c[%dA%c[2K", ESC, 1, ESC)
1975

1976
// clearLines erases the last count lines in the terminal window.
1977
func clearLines(count int) {
×
1978
        _, _ = fmt.Print(strings.Repeat(clearCode, count))
×
1979
}
×
1980

1981
// ordinalNumber returns the ordinal number as a string of a number.
1982
func ordinalNumber(num uint32) string {
×
1983
        switch num {
×
1984
        case 1:
×
1985
                return "1st"
×
1986
        case 2:
×
1987
                return "2nd"
×
1988
        case 3:
×
1989
                return "3rd"
×
1990
        default:
×
1991
                return fmt.Sprintf("%dth", num)
×
1992
        }
1993
}
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