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

lightningnetwork / lnd / 14513053602

17 Apr 2025 09:56AM UTC coverage: 56.754% (-12.3%) from 69.035%
14513053602

Pull #9727

github

web-flow
Merge 5fb0f4317 into 24fdae7df
Pull Request #9727: Aux bandwidth manager: also pass HTLC blob to `ShouldHandleTraffic`

3 of 8 new or added lines in 2 files covered. (37.5%)

24357 existing lines in 290 files now uncovered.

107518 of 189445 relevant lines covered (56.75%)

22634.92 hits per line

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

4.5
/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
        Description: `
1479
        Query the HTLC switch's internal forwarding log for all completed
1480
        payment circuits (HTLCs) over a particular time range (--start_time and
1481
        --end_time). The start and end times are meant to be expressed in
1482
        seconds since the Unix epoch.
1483
        Alternatively negative time ranges can be used, e.g. "-3d". Supports
1484
        s(seconds), m(minutes), h(ours), d(ays), w(eeks), M(onths), y(ears).
1485
        Month equals 30.44 days, year equals 365.25 days.
1486
        If --start_time isn't provided, then 24 hours ago is used. If
1487
        --end_time isn't provided, then the current time is used.
1488

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

1492
        Finally, callers can skip a series of events using the --index_offset
1493
        parameter. Each response will contain the offset index of the last
1494
        entry. Using this callers can manually paginate within a time slice.
1495
        `,
1496
        Flags: []cli.Flag{
1497
                cli.StringFlag{
1498
                        Name: "start_time",
1499
                        Usage: "the starting time for the query " +
1500
                                `as unix timestamp or relative e.g. "-1w"`,
1501
                },
1502
                cli.StringFlag{
1503
                        Name: "end_time",
1504
                        Usage: "the end time for the query " +
1505
                                `as unix timestamp or relative e.g. "-1w"`,
1506
                },
1507
                cli.Int64Flag{
1508
                        Name:  "index_offset",
1509
                        Usage: "the number of events to skip",
1510
                },
1511
                cli.Int64Flag{
1512
                        Name:  "max_events",
1513
                        Usage: "the max number of events to return",
1514
                },
1515
                cli.BoolFlag{
1516
                        Name: "skip_peer_alias_lookup",
1517
                        Usage: "skip the peer alias lookup per forwarding " +
1518
                                "event in order to improve performance",
1519
                },
1520
        },
1521
        Action: actionDecorator(forwardingHistory),
1522
}
1523

1524
func forwardingHistory(ctx *cli.Context) error {
×
1525
        ctxc := getContext()
×
1526
        client, cleanUp := getClient(ctx)
×
1527
        defer cleanUp()
×
1528

×
1529
        var (
×
1530
                startTime, endTime     uint64
×
1531
                indexOffset, maxEvents uint32
×
1532
                err                    error
×
1533
        )
×
1534
        args := ctx.Args()
×
1535
        now := time.Now()
×
1536

×
1537
        switch {
×
1538
        case ctx.IsSet("start_time"):
×
1539
                startTime, err = parseTime(ctx.String("start_time"), now)
×
1540
        case args.Present():
×
1541
                startTime, err = parseTime(args.First(), now)
×
1542
                args = args.Tail()
×
1543
        default:
×
1544
                now := time.Now()
×
1545
                startTime = uint64(now.Add(-time.Hour * 24).Unix())
×
1546
        }
1547
        if err != nil {
×
1548
                return fmt.Errorf("unable to decode start_time: %w", err)
×
1549
        }
×
1550

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

1564
        switch {
×
1565
        case ctx.IsSet("index_offset"):
×
1566
                indexOffset = uint32(ctx.Int64("index_offset"))
×
1567
        case args.Present():
×
1568
                i, err := strconv.ParseInt(args.First(), 10, 64)
×
1569
                if err != nil {
×
1570
                        return fmt.Errorf("unable to decode index_offset: %w",
×
1571
                                err)
×
1572
                }
×
1573
                indexOffset = uint32(i)
×
1574
                args = args.Tail()
×
1575
        }
1576

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

1589
        // By default we will look up the peers' alias information unless the
1590
        // skip_peer_alias_lookup flag is specified.
1591
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1592

×
1593
        req := &lnrpc.ForwardingHistoryRequest{
×
1594
                StartTime:       startTime,
×
1595
                EndTime:         endTime,
×
1596
                IndexOffset:     indexOffset,
×
1597
                NumMaxEvents:    maxEvents,
×
1598
                PeerAliasLookup: lookupPeerAlias,
×
1599
        }
×
1600
        resp, err := client.ForwardingHistory(ctxc, req)
×
1601
        if err != nil {
×
1602
                return err
×
1603
        }
×
1604

1605
        printRespJSON(resp)
×
1606
        return nil
×
1607
}
1608

1609
var buildRouteCommand = cli.Command{
1610
        Name:     "buildroute",
1611
        Category: "Payments",
1612
        Usage:    "Build a route from a list of hop pubkeys.",
1613
        Description: `
1614
        Builds a sphinx route for the supplied hops (public keys). Make sure to
1615
        use a custom final_cltv_delta to create the route depending on the
1616
        restrictions in the invoice otherwise LND will use its default specified
1617
        via the bitcoin.timelockdelta setting (default 80).
1618
        If the final_cltv_delta mismatch you will likely see the error
1619
        INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS returned by the receiving node.
1620

1621
        Moreover a payment_addr has to be provided if the invoice supplied it as
1622
        well otherwise the payment will be rejected by the receiving node.
1623
        `,
1624
        Action: actionDecorator(buildRoute),
1625
        Flags: []cli.Flag{
1626
                cli.Int64Flag{
1627
                        Name: "amt",
1628
                        Usage: "the amount to send expressed in satoshis. If" +
1629
                                "not set, the minimum routable amount is used",
1630
                },
1631
                cli.Int64Flag{
1632
                        Name: "final_cltv_delta",
1633
                        Usage: "number of blocks the last hop has to reveal " +
1634
                                "the preimage; if not set the default lnd " +
1635
                                "final_cltv_delta is used",
1636
                },
1637
                cli.StringFlag{
1638
                        Name:  "hops",
1639
                        Usage: "comma separated hex pubkeys",
1640
                },
1641
                cli.Uint64Flag{
1642
                        Name: "outgoing_chan_id",
1643
                        Usage: "short channel id of the outgoing channel to " +
1644
                                "use for the first hop of the payment",
1645
                        Value: 0,
1646
                },
1647
                cli.StringFlag{
1648
                        Name: "payment_addr",
1649
                        Usage: "hex encoded payment address to set in the " +
1650
                                "last hop's mpp record",
1651
                },
1652
        },
1653
}
1654

1655
func buildRoute(ctx *cli.Context) error {
×
1656
        ctxc := getContext()
×
1657
        conn := getClientConn(ctx, false)
×
1658
        defer conn.Close()
×
1659

×
1660
        client := routerrpc.NewRouterClient(conn)
×
1661

×
1662
        if !ctx.IsSet("hops") {
×
1663
                return errors.New("hops required")
×
1664
        }
×
1665

1666
        // Build list of hop addresses for the rpc.
1667
        hops := strings.Split(ctx.String("hops"), ",")
×
1668
        rpcHops := make([][]byte, 0, len(hops))
×
1669
        for _, k := range hops {
×
1670
                pubkey, err := route.NewVertexFromStr(k)
×
1671
                if err != nil {
×
1672
                        return fmt.Errorf("error parsing %v: %w", k, err)
×
1673
                }
×
1674
                rpcHops = append(rpcHops, pubkey[:])
×
1675
        }
1676

1677
        var amtMsat int64
×
1678
        hasAmt := ctx.IsSet("amt")
×
1679
        if hasAmt {
×
1680
                amtMsat = ctx.Int64("amt") * 1000
×
1681
                if amtMsat == 0 {
×
1682
                        return fmt.Errorf("non-zero amount required")
×
1683
                }
×
1684
        }
1685

1686
        var (
×
1687
                payAddr []byte
×
1688
                err     error
×
1689
        )
×
1690

×
1691
        if ctx.IsSet("payment_addr") {
×
1692
                payAddr, err = hex.DecodeString(ctx.String("payment_addr"))
×
1693
                if err != nil {
×
1694
                        return fmt.Errorf("error parsing payment_addr: %w", err)
×
1695
                }
×
1696
        }
1697

1698
        // Call BuildRoute rpc.
1699
        req := &routerrpc.BuildRouteRequest{
×
1700
                AmtMsat:        amtMsat,
×
1701
                FinalCltvDelta: int32(ctx.Int64("final_cltv_delta")),
×
1702
                HopPubkeys:     rpcHops,
×
1703
                OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
×
1704
                PaymentAddr:    payAddr,
×
1705
        }
×
1706

×
1707
        route, err := client.BuildRoute(ctxc, req)
×
1708
        if err != nil {
×
1709
                return err
×
1710
        }
×
1711

1712
        printRespJSON(route)
×
1713

×
1714
        return nil
×
1715
}
1716

1717
var deletePaymentsCommand = cli.Command{
1718
        Name:     "deletepayments",
1719
        Category: "Payments",
1720
        Usage:    "Delete a single or multiple payments from the database.",
1721
        ArgsUsage: "--all [--failed_htlcs_only --include_non_failed] | " +
1722
                "--payment_hash hash [--failed_htlcs_only]",
1723
        Description: `
1724
        This command either deletes all failed payments or a single payment from
1725
        the database to reclaim disk space.
1726

1727
        If the --all flag is used, then all failed payments are removed. If so
1728
        desired, _ALL_ payments (even the successful ones) can be deleted
1729
        by additionally specifying --include_non_failed.
1730

1731
        If a --payment_hash is specified, that single payment is deleted,
1732
        independent of its state.
1733

1734
        If --failed_htlcs_only is specified then the payments themselves (or the
1735
        single payment itself if used with --payment_hash) is not deleted, only
1736
        the information about any failed HTLC attempts during the payment.
1737

1738
        NOTE: Removing payments from the database does free up disk space within
1739
        the internal bbolt database. But that disk space is only reclaimed after
1740
        compacting the database. Users might want to turn on auto compaction
1741
        (db.bolt.auto-compact=true in the config file or --db.bolt.auto-compact
1742
        as a command line flag) and restart lnd after deleting a large number of
1743
        payments to see a reduction in the file size of the channel.db file.
1744
        `,
1745
        Action: actionDecorator(deletePayments),
1746
        Flags: []cli.Flag{
1747
                cli.BoolFlag{
1748
                        Name:  "all",
1749
                        Usage: "delete all failed payments",
1750
                },
1751
                cli.StringFlag{
1752
                        Name: "payment_hash",
1753
                        Usage: "delete a specific payment identified by its " +
1754
                                "payment hash",
1755
                },
1756
                cli.BoolFlag{
1757
                        Name: "failed_htlcs_only",
1758
                        Usage: "only delete failed HTLCs from payments, not " +
1759
                                "the payment itself",
1760
                },
1761
                cli.BoolFlag{
1762
                        Name:  "include_non_failed",
1763
                        Usage: "delete ALL payments, not just the failed ones",
1764
                },
1765
        },
1766
}
1767

1768
func deletePayments(ctx *cli.Context) error {
×
1769
        ctxc := getContext()
×
1770
        client, cleanUp := getClient(ctx)
×
1771
        defer cleanUp()
×
1772

×
1773
        // Show command help if arguments or no flags are provided.
×
1774
        if ctx.NArg() > 0 || ctx.NumFlags() == 0 {
×
1775
                _ = cli.ShowCommandHelp(ctx, "deletepayments")
×
1776
                return nil
×
1777
        }
×
1778

1779
        var (
×
1780
                paymentHash      []byte
×
1781
                all              = ctx.Bool("all")
×
1782
                singlePayment    = ctx.IsSet("payment_hash")
×
1783
                failedHTLCsOnly  = ctx.Bool("failed_htlcs_only")
×
1784
                includeNonFailed = ctx.Bool("include_non_failed")
×
1785
                err              error
×
1786
                resp             proto.Message
×
1787
        )
×
1788

×
1789
        // We pack two RPCs into the same CLI so there are a few non-valid
×
1790
        // combinations of the flags we need to filter out.
×
1791
        switch {
×
1792
        case all && singlePayment:
×
1793
                return fmt.Errorf("cannot use --all and --payment_hash at " +
×
1794
                        "the same time")
×
1795

1796
        case singlePayment && includeNonFailed:
×
1797
                return fmt.Errorf("cannot use --payment_hash and " +
×
1798
                        "--include_non_failed at the same time, when using " +
×
1799
                        "a payment hash the payment is deleted independent " +
×
1800
                        "of its state")
×
1801
        }
1802

1803
        // Deleting a single payment is implemented in a different RPC than
1804
        // removing all/multiple payments.
1805
        switch {
×
1806
        case singlePayment:
×
1807
                paymentHash, err = hex.DecodeString(ctx.String("payment_hash"))
×
1808
                if err != nil {
×
1809
                        return fmt.Errorf("error decoding payment_hash: %w",
×
1810
                                err)
×
1811
                }
×
1812

1813
                resp, err = client.DeletePayment(
×
1814
                        ctxc, &lnrpc.DeletePaymentRequest{
×
1815
                                PaymentHash:     paymentHash,
×
1816
                                FailedHtlcsOnly: failedHTLCsOnly,
×
1817
                        },
×
1818
                )
×
1819
                if err != nil {
×
1820
                        return fmt.Errorf("error deleting single payment: %w",
×
1821
                                err)
×
1822
                }
×
1823

1824
        case all:
×
1825
                what := "failed"
×
1826
                if includeNonFailed {
×
1827
                        what = "all"
×
1828
                }
×
1829
                if failedHTLCsOnly {
×
1830
                        what = fmt.Sprintf("failed HTLCs from %s", what)
×
1831
                }
×
1832

1833
                fmt.Printf("Removing %s payments, this might take a while...\n",
×
1834
                        what)
×
1835
                resp, err = client.DeleteAllPayments(
×
1836
                        ctxc, &lnrpc.DeleteAllPaymentsRequest{
×
1837
                                AllPayments:        includeNonFailed,
×
1838
                                FailedPaymentsOnly: !includeNonFailed,
×
1839
                                FailedHtlcsOnly:    failedHTLCsOnly,
×
1840
                        },
×
1841
                )
×
1842
                if err != nil {
×
1843
                        return fmt.Errorf("error deleting payments: %w", err)
×
1844
                }
×
1845
        }
UNCOV
1846

×
1847
        printJSON(resp)
×
1848

1849
        return nil
UNCOV
1850
}
×
UNCOV
1851

×
UNCOV
1852
var estimateRouteFeeCommand = cli.Command{
×
1853
        Name:     "estimateroutefee",
1854
        Category: "Payments",
1855
        Usage:    "Estimate routing fees based on a destination or an invoice.",
1856
        Action:   actionDecorator(estimateRouteFee),
1857
        Flags: []cli.Flag{
1858
                cli.StringFlag{
1859
                        Name: "dest",
1860
                        Usage: "the 33-byte hex-encoded public key for the " +
1861
                                "probe destination. If it is specified then " +
1862
                                "the amt flag is required. If it isn't " +
1863
                                "specified then the pay_req field has to.",
1864
                },
1865
                cli.Int64Flag{
1866
                        Name: "amt",
1867
                        Usage: "the payment amount expressed in satoshis " +
1868
                                "that should be probed for. This field is " +
1869
                                "mandatory if dest is specified.",
1870
                },
1871
                cli.StringFlag{
1872
                        Name: "pay_req",
1873
                        Usage: "a zpay32 encoded payment request which is " +
1874
                                "used to probe. If the destination is " +
1875
                                "not public then route hints are scanned for " +
1876
                                "a public node.",
1877
                },
1878
                cli.DurationFlag{
1879
                        Name: "timeout",
1880
                        Usage: "a deadline for the probe attempt. Only " +
1881
                                "applicable if pay_req is specified.",
1882
                        Value: paymentTimeout,
1883
                },
1884
        },
1885
}
1886

1887
func estimateRouteFee(ctx *cli.Context) error {
1888
        ctxc := getContext()
1889
        conn := getClientConn(ctx, false)
1890
        defer conn.Close()
×
1891

×
1892
        client := routerrpc.NewRouterClient(conn)
×
1893

×
1894
        req := &routerrpc.RouteFeeRequest{}
×
1895

×
1896
        switch {
×
1897
        case ctx.IsSet("dest") && ctx.IsSet("pay_req"):
×
1898
                return fmt.Errorf("either dest or pay_req can be set")
×
UNCOV
1899

×
1900
        case ctx.IsSet("dest") && !ctx.IsSet("amt"):
×
1901
                return fmt.Errorf("amt is required when dest is set")
×
1902

1903
        case ctx.IsSet("dest"):
×
1904
                dest, err := hex.DecodeString(ctx.String("dest"))
×
1905
                if err != nil {
1906
                        return err
×
1907
                }
×
UNCOV
1908

×
1909
                if len(dest) != 33 {
×
1910
                        return fmt.Errorf("dest node pubkey must be exactly "+
×
1911
                                "33 bytes, is instead: %v", len(dest))
1912
                }
×
UNCOV
1913

×
1914
                amtSat := ctx.Int64("amt")
×
1915
                if amtSat == 0 {
×
1916
                        return fmt.Errorf("non-zero amount required")
1917
                }
×
UNCOV
1918

×
1919
                req.Dest = dest
×
1920
                req.AmtSat = amtSat
×
1921

1922
        case ctx.IsSet("pay_req"):
×
1923
                req.PaymentRequest = StripPrefix(ctx.String("pay_req"))
×
1924
                req.Timeout = uint32(ctx.Duration("timeout").Seconds())
UNCOV
1925

×
1926
        default:
×
1927
                return fmt.Errorf("fee estimation arguments missing")
×
1928
        }
UNCOV
1929

×
1930
        resp, err := client.EstimateRouteFee(ctxc, req)
×
1931
        if err != nil {
1932
                return err
1933
        }
×
UNCOV
1934

×
1935
        printRespJSON(resp)
×
1936

×
1937
        return nil
UNCOV
1938
}
×
UNCOV
1939

×
UNCOV
1940
// ESC is the ASCII code for escape character.
×
1941
const ESC = 27
1942

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

1947
// clearLines erases the last count lines in the terminal window.
1948
func clearLines(count int) {
1949
        _, _ = fmt.Print(strings.Repeat(clearCode, count))
1950
}
UNCOV
1951

×
UNCOV
1952
// ordinalNumber returns the ordinal number as a string of a number.
×
1953
func ordinalNumber(num uint32) string {
×
1954
        switch num {
1955
        case 1:
1956
                return "1st"
×
1957
        case 2:
×
1958
                return "2nd"
×
1959
        case 3:
×
1960
                return "3rd"
×
1961
        default:
×
1962
                return fmt.Sprintf("%dth", num)
×
UNCOV
1963
        }
×
UNCOV
1964
}
×
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