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

lightningnetwork / lnd / 15575490320

11 Jun 2025 03:40AM UTC coverage: 68.499% (+10.2%) from 58.306%
15575490320

Pull #9356

github

web-flow
Merge bfe1edf4e into 92a5d35cf
Pull Request #9356: lnrpc: add incoming/outgoing channel ids filter to forwarding history request

34 of 48 new or added lines in 3 files covered. (70.83%)

24 existing lines in 7 files now uncovered.

134421 of 196239 relevant lines covered (68.5%)

22265.74 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

271
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
272
// flags passed. It always returns a value and doesn't rely on lnd applying a
273
// default.
274
func retrieveFeeLimit(ctx *cli.Context, amt int64) (int64, error) {
×
275
        switch {
×
276
        case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"):
×
277
                return 0, fmt.Errorf("either fee_limit or fee_limit_percent " +
×
278
                        "can be set, but not both")
×
279

280
        case ctx.IsSet("fee_limit"):
×
281
                return ctx.Int64("fee_limit"), nil
×
282

283
        case ctx.IsSet("fee_limit_percent"):
×
284
                // Round up the fee limit to prevent hitting zero on small
×
285
                // amounts.
×
286
                feeLimitRoundedUp :=
×
287
                        (amt*ctx.Int64("fee_limit_percent") + 99) / 100
×
288

×
289
                return feeLimitRoundedUp, nil
×
290
        }
291

292
        // If no fee limit is set, use a default value based on the amount.
293
        amtMsat := lnwire.NewMSatFromSatoshis(btcutil.Amount(amt))
×
294
        limitMsat := lnwallet.DefaultRoutingFeeLimitForAmount(amtMsat)
×
295
        return int64(limitMsat.ToSatoshis()), nil
×
296
}
297

298
func confirmPayReq(resp *lnrpc.PayReq, amt, feeLimit int64) error {
×
299
        fmt.Printf("Payment hash: %v\n", resp.GetPaymentHash())
×
300
        fmt.Printf("Description: %v\n", resp.GetDescription())
×
301
        fmt.Printf("Amount (in satoshis): %v\n", amt)
×
302
        fmt.Printf("Fee limit (in satoshis): %v\n", feeLimit)
×
303
        fmt.Printf("Destination: %v\n", resp.GetDestination())
×
304

×
305
        confirm := promptForConfirmation("Confirm payment (yes/no): ")
×
306
        if !confirm {
×
307
                return fmt.Errorf("payment not confirmed")
×
308
        }
×
309

310
        return nil
×
311
}
312

313
func parsePayAddr(ctx *cli.Context, args cli.Args) ([]byte, error) {
×
314
        var (
×
315
                payAddr []byte
×
316
                err     error
×
317
        )
×
318
        switch {
×
319
        case ctx.IsSet("pay_addr"):
×
320
                payAddr, err = hex.DecodeString(ctx.String("pay_addr"))
×
321

322
        case args.Present():
×
323
                payAddr, err = hex.DecodeString(args.First())
×
324
        }
325

326
        if err != nil {
×
327
                return nil, err
×
328
        }
×
329

330
        // payAddr may be not required if it's a legacy invoice.
331
        if len(payAddr) != 0 && len(payAddr) != 32 {
×
332
                return nil, fmt.Errorf("payment addr must be exactly 32 "+
×
333
                        "bytes, is instead %v", len(payAddr))
×
334
        }
×
335

336
        return payAddr, nil
×
337
}
338

339
func SendPayment(ctx *cli.Context) error {
×
340
        // Show command help if no arguments provided
×
341
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
342
                _ = cli.ShowCommandHelp(ctx, "sendpayment")
×
343
                return nil
×
344
        }
×
345

346
        conn := getClientConn(ctx, false)
×
347
        defer conn.Close()
×
348

×
349
        args := ctx.Args()
×
350

×
351
        // If a payment request was provided, we can exit early since all of the
×
352
        // details of the payment are encoded within the request.
×
353
        if ctx.IsSet("pay_req") {
×
354
                req := &routerrpc.SendPaymentRequest{
×
355
                        PaymentRequest:    StripPrefix(ctx.String("pay_req")),
×
356
                        Amt:               ctx.Int64("amt"),
×
357
                        DestCustomRecords: make(map[uint64][]byte),
×
358
                        Amp:               ctx.Bool(ampFlag.Name),
×
359
                        Cancelable:        ctx.Bool(cancelableFlag.Name),
×
360
                }
×
361

×
362
                // We'll attempt to parse a payment address as well, given that
×
363
                // if the user is using an AMP invoice, then they may be trying
×
364
                // to specify that value manually.
×
365
                //
×
366
                // Don't parse unnamed arguments to prevent confusion with the
×
367
                // main unnamed argument format for non-AMP payments.
×
368
                payAddr, err := parsePayAddr(ctx, nil)
×
369
                if err != nil {
×
370
                        return err
×
371
                }
×
372

373
                req.PaymentAddr = payAddr
×
374

×
375
                return SendPaymentRequest(
×
376
                        ctx, req, conn, conn, routerRPCSendPayment,
×
377
                )
×
378
        }
379

380
        var (
×
381
                destNode []byte
×
382
                amount   int64
×
383
                err      error
×
384
        )
×
385

×
386
        switch {
×
387
        case ctx.IsSet("dest"):
×
388
                destNode, err = hex.DecodeString(ctx.String("dest"))
×
389
        case args.Present():
×
390
                destNode, err = hex.DecodeString(args.First())
×
391
                args = args.Tail()
×
392
        default:
×
393
                return fmt.Errorf("destination txid argument missing")
×
394
        }
395
        if err != nil {
×
396
                return err
×
397
        }
×
398

399
        if len(destNode) != 33 {
×
400
                return fmt.Errorf("dest node pubkey must be exactly 33 bytes, is "+
×
401
                        "instead: %v", len(destNode))
×
402
        }
×
403

404
        if ctx.IsSet("amt") {
×
405
                amount = ctx.Int64("amt")
×
406
        } else if args.Present() {
×
407
                amount, err = strconv.ParseInt(args.First(), 10, 64)
×
408
                args = args.Tail()
×
409
                if err != nil {
×
410
                        return fmt.Errorf("unable to decode payment amount: %w",
×
411
                                err)
×
412
                }
×
413
        }
414

415
        req := &routerrpc.SendPaymentRequest{
×
416
                Dest:              destNode,
×
417
                Amt:               amount,
×
418
                DestCustomRecords: make(map[uint64][]byte),
×
419
                Amp:               ctx.Bool(ampFlag.Name),
×
420
                Cancelable:        ctx.Bool(cancelableFlag.Name),
×
421
        }
×
422

×
423
        var rHash []byte
×
424

×
425
        switch {
×
426
        case ctx.Bool("keysend") && ctx.Bool(ampFlag.Name):
×
427
                return errors.New("either keysend or amp may be set, but not both")
×
428

429
        case ctx.Bool("keysend"):
×
430
                if ctx.IsSet("payment_hash") {
×
431
                        return errors.New("cannot set payment hash when using " +
×
432
                                "keysend")
×
433
                }
×
434
                var preimage lntypes.Preimage
×
435
                if _, err := rand.Read(preimage[:]); err != nil {
×
436
                        return err
×
437
                }
×
438

439
                // Set the preimage. If the user supplied a preimage with the
440
                // data flag, the preimage that is set here will be overwritten
441
                // later.
442
                req.DestCustomRecords[record.KeySendType] = preimage[:]
×
443

×
444
                hash := preimage.Hash()
×
445
                rHash = hash[:]
×
446
        case !ctx.Bool(ampFlag.Name):
×
447
                switch {
×
448
                case ctx.IsSet("payment_hash"):
×
449
                        rHash, err = hex.DecodeString(ctx.String("payment_hash"))
×
450
                case args.Present():
×
451
                        rHash, err = hex.DecodeString(args.First())
×
452
                        args = args.Tail()
×
453
                default:
×
454
                        return fmt.Errorf("payment hash argument missing")
×
455
                }
456
        }
457

458
        if err != nil {
×
459
                return err
×
460
        }
×
461
        if !req.Amp && len(rHash) != 32 {
×
462
                return fmt.Errorf("payment hash must be exactly 32 "+
×
463
                        "bytes, is instead %v", len(rHash))
×
464
        }
×
465
        req.PaymentHash = rHash
×
466

×
467
        switch {
×
468
        case ctx.IsSet("final_cltv_delta"):
×
469
                req.FinalCltvDelta = int32(ctx.Int64("final_cltv_delta"))
×
470
        case args.Present():
×
471
                delta, err := strconv.ParseInt(args.First(), 10, 64)
×
472
                if err != nil {
×
473
                        return err
×
474
                }
×
475
                args = args.Tail()
×
476
                req.FinalCltvDelta = int32(delta)
×
477
        }
478

479
        payAddr, err := parsePayAddr(ctx, args)
×
480
        if err != nil {
×
481
                return err
×
482
        }
×
483

484
        req.PaymentAddr = payAddr
×
485

×
486
        if ctx.IsSet("route_hints") {
×
487
                // Parse the route hints JSON.
×
488
                routeHintsJSON := ctx.String("route_hints")
×
489
                var routeHints []*lnrpc.RouteHint
×
490

×
491
                err := json.Unmarshal([]byte(routeHintsJSON), &routeHints)
×
492
                if err != nil {
×
493
                        return fmt.Errorf("error unmarshaling route_hints "+
×
494
                                "json: %w", err)
×
495
                }
×
496

497
                req.RouteHints = routeHints
×
498
        }
499

500
        return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
×
501
}
502

503
// SendPaymentFn is a function type that abstracts the SendPaymentV2 call of the
504
// router client.
505
type SendPaymentFn func(ctx context.Context, payConn grpc.ClientConnInterface,
506
        req *routerrpc.SendPaymentRequest) (PaymentResultStream, error)
507

508
// routerRPCSendPayment is the default implementation of the SendPaymentFn type
509
// that uses the lnd routerrpc.SendPaymentV2 call.
510
func routerRPCSendPayment(ctx context.Context, payConn grpc.ClientConnInterface,
511
        req *routerrpc.SendPaymentRequest) (PaymentResultStream, error) {
×
512

×
513
        return routerrpc.NewRouterClient(payConn).SendPaymentV2(ctx, req)
×
514
}
×
515

516
func SendPaymentRequest(ctx *cli.Context, req *routerrpc.SendPaymentRequest,
517
        lnConn, paymentConn grpc.ClientConnInterface,
518
        callSendPayment SendPaymentFn) error {
×
519

×
520
        ctxc := getContext()
×
521

×
522
        lnClient := lnrpc.NewLightningClient(lnConn)
×
523

×
524
        outChan := ctx.Int64Slice("outgoing_chan_id")
×
525
        if len(outChan) != 0 {
×
526
                req.OutgoingChanIds = make([]uint64, len(outChan))
×
527
                for i, c := range outChan {
×
528
                        req.OutgoingChanIds[i] = uint64(c)
×
529
                }
×
530
        }
531

532
        if ctx.IsSet(lastHopFlag.Name) {
×
533
                lastHop, err := route.NewVertexFromStr(
×
534
                        ctx.String(lastHopFlag.Name),
×
535
                )
×
536
                if err != nil {
×
537
                        return err
×
538
                }
×
539
                req.LastHopPubkey = lastHop[:]
×
540
        }
541

542
        req.CltvLimit = int32(ctx.Int(cltvLimitFlag.Name))
×
543

×
544
        pmtTimeout := ctx.Duration("timeout")
×
545
        if pmtTimeout <= 0 {
×
546
                return errors.New("payment timeout must be greater than zero")
×
547
        }
×
548
        req.TimeoutSeconds = int32(pmtTimeout.Seconds())
×
549

×
550
        req.AllowSelfPayment = ctx.Bool("allow_self_payment")
×
551

×
552
        req.MaxParts = uint32(ctx.Uint(maxPartsFlag.Name))
×
553

×
554
        switch {
×
555
        // If the max shard size is specified, then it should either be in sat
556
        // or msat, but not both.
557
        case ctx.Uint64(maxShardSizeMsatFlag.Name) != 0 &&
558
                ctx.Uint64(maxShardSizeSatFlag.Name) != 0:
×
559
                return fmt.Errorf("only --max_split_size_msat or " +
×
560
                        "--max_split_size_sat should be set, but not both")
×
561

562
        case ctx.Uint64(maxShardSizeMsatFlag.Name) != 0:
×
563
                req.MaxShardSizeMsat = ctx.Uint64(maxShardSizeMsatFlag.Name)
×
564

565
        case ctx.Uint64(maxShardSizeSatFlag.Name) != 0:
×
566
                req.MaxShardSizeMsat = uint64(lnwire.NewMSatFromSatoshis(
×
567
                        btcutil.Amount(ctx.Uint64(maxShardSizeSatFlag.Name)),
×
568
                ))
×
569
        }
570

571
        // Parse custom data records.
572
        data := ctx.String(dataFlag.Name)
×
573
        if data != "" {
×
574
                records := strings.Split(data, ",")
×
575
                for _, r := range records {
×
576
                        kv := strings.Split(r, "=")
×
577
                        if len(kv) != 2 {
×
578
                                return errors.New("invalid data format: " +
×
579
                                        "multiple equal signs in record")
×
580
                        }
×
581

582
                        recordID, err := strconv.ParseUint(kv[0], 10, 64)
×
583
                        if err != nil {
×
584
                                return fmt.Errorf("invalid data format: %w",
×
585
                                        err)
×
586
                        }
×
587

588
                        hexValue, err := hex.DecodeString(kv[1])
×
589
                        if err != nil {
×
590
                                return fmt.Errorf("invalid data format: %w",
×
591
                                        err)
×
592
                        }
×
593

594
                        req.DestCustomRecords[recordID] = hexValue
×
595
                }
596
        }
597

598
        var feeLimit int64
×
599
        if req.PaymentRequest != "" {
×
600
                // Decode payment request to find out the amount.
×
601
                decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
×
602
                decodeResp, err := lnClient.DecodePayReq(ctxc, decodeReq)
×
603
                if err != nil {
×
604
                        return err
×
605
                }
×
606

607
                // If amount is present in the request, override the request
608
                // amount.
609
                amt := req.Amt
×
610
                invoiceAmt := decodeResp.GetNumSatoshis()
×
611
                if invoiceAmt != 0 {
×
612
                        amt = invoiceAmt
×
613
                }
×
614

615
                // Calculate fee limit based on the determined amount.
616
                feeLimit, err = retrieveFeeLimit(ctx, amt)
×
617
                if err != nil {
×
618
                        return err
×
619
                }
×
620

621
                // Ask for confirmation of amount and fee limit if payment is
622
                // forced.
623
                if !ctx.Bool("force") {
×
624
                        err := confirmPayReq(decodeResp, amt, feeLimit)
×
625
                        if err != nil {
×
626
                                return err
×
627
                        }
×
628
                }
629
        } else {
×
630
                var err error
×
631
                feeLimit, err = retrieveFeeLimit(ctx, req.Amt)
×
632
                if err != nil {
×
633
                        return err
×
634
                }
×
635
        }
636

637
        req.FeeLimitSat = feeLimit
×
638

×
639
        // Set time pref.
×
640
        req.TimePref = ctx.Float64(timePrefFlag.Name)
×
641

×
642
        // Always print in-flight updates for the table output.
×
643
        printJSON := ctx.Bool(jsonFlag.Name)
×
644
        req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
×
645

×
646
        stream, err := callSendPayment(ctxc, paymentConn, req)
×
647
        if err != nil {
×
648
                return err
×
649
        }
×
650

651
        finalState, err := PrintLivePayment(ctxc, stream, lnClient, printJSON)
×
652
        if err != nil {
×
653
                return err
×
654
        }
×
655

656
        // If we get a payment error back, we pass an error up
657
        // to main which eventually calls fatal() and returns
658
        // with a non-zero exit code.
659
        if finalState.Status != lnrpc.Payment_SUCCEEDED {
×
660
                return errors.New(finalState.Status.String())
×
661
        }
×
662

663
        return nil
×
664
}
665

666
var trackPaymentCommand = cli.Command{
667
        Name:     "trackpayment",
668
        Category: "Payments",
669
        Usage:    "Track progress of an existing payment.",
670
        Description: `
671
        Pick up monitoring the progression of a previously initiated payment
672
        specified by the hash argument.
673
        `,
674
        ArgsUsage: "hash",
675
        Flags: []cli.Flag{
676
                jsonFlag,
677
        },
678
        Action: actionDecorator(trackPayment),
679
}
680

681
func trackPayment(ctx *cli.Context) error {
×
682
        ctxc := getContext()
×
683
        args := ctx.Args()
×
684

×
685
        conn := getClientConn(ctx, false)
×
686
        defer conn.Close()
×
687

×
688
        routerClient := routerrpc.NewRouterClient(conn)
×
689

×
690
        if !args.Present() {
×
691
                return fmt.Errorf("hash argument missing")
×
692
        }
×
693

694
        hash, err := hex.DecodeString(args.First())
×
695
        if err != nil {
×
696
                return err
×
697
        }
×
698

699
        req := &routerrpc.TrackPaymentRequest{
×
700
                PaymentHash: hash,
×
701
        }
×
702

×
703
        stream, err := routerClient.TrackPaymentV2(ctxc, req)
×
704
        if err != nil {
×
705
                return err
×
706
        }
×
707

708
        client := lnrpc.NewLightningClient(conn)
×
709
        _, err = PrintLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
×
710
        return err
×
711
}
712

713
// PaymentResultStream is an interface that abstracts the Recv method of the
714
// SendPaymentV2 or TrackPaymentV2 client stream.
715
type PaymentResultStream interface {
716
        Recv() (*lnrpc.Payment, error)
717
}
718

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

×
726
        // Terminal escape codes aren't supported on Windows, fall back to json.
×
727
        if !json && runtime.GOOS == "windows" {
×
728
                json = true
×
729
        }
×
730

731
        aliases := newAliasCache(lnClient)
×
732

×
733
        first := true
×
734
        var lastLineCount int
×
735
        for {
×
736
                payment, err := stream.Recv()
×
737
                if err != nil {
×
738
                        return nil, err
×
739
                }
×
740

741
                if json {
×
742
                        // Delimit json messages by newlines (inspired by
×
743
                        // grpc over rest chunking).
×
744
                        if first {
×
745
                                first = false
×
746
                        } else {
×
747
                                fmt.Println()
×
748
                        }
×
749

750
                        // Write raw json to stdout.
751
                        printRespJSON(payment)
×
752
                } else {
×
753
                        resultTable := formatPayment(ctxc, payment, aliases)
×
754

×
755
                        // Clear all previously written lines and print the
×
756
                        // updated table.
×
757
                        clearLines(lastLineCount)
×
758
                        fmt.Print(resultTable)
×
759

×
760
                        // Store the number of lines written for the next update
×
761
                        // pass.
×
762
                        lastLineCount = 0
×
763
                        for _, b := range resultTable {
×
764
                                if b == '\n' {
×
765
                                        lastLineCount++
×
766
                                }
×
767
                        }
768
                }
769

770
                // Terminate loop if payments state is final.
771
                if payment.Status != lnrpc.Payment_IN_FLIGHT &&
×
772
                        payment.Status != lnrpc.Payment_INITIATED {
×
773

×
774
                        return payment, nil
×
775
                }
×
776
        }
777
}
778

779
// aliasCache allows cached retrieval of node aliases.
780
type aliasCache struct {
781
        cache  map[string]string
782
        client lnrpc.LightningClient
783
}
784

785
func newAliasCache(client lnrpc.LightningClient) *aliasCache {
×
786
        return &aliasCache{
×
787
                client: client,
×
788
                cache:  make(map[string]string),
×
789
        }
×
790
}
×
791

792
// get returns a node alias either from cache or freshly requested from lnd.
793
func (a *aliasCache) get(ctxc context.Context, pubkey string) string {
×
794
        alias, ok := a.cache[pubkey]
×
795
        if ok {
×
796
                return alias
×
797
        }
×
798

799
        // Request node info.
800
        resp, err := a.client.GetNodeInfo(
×
801
                ctxc,
×
802
                &lnrpc.NodeInfoRequest{
×
803
                        PubKey: pubkey,
×
804
                },
×
805
        )
×
806
        if err != nil {
×
807
                // If no info is available, use the
×
808
                // pubkey as identifier.
×
809
                alias = pubkey[:6]
×
810
        } else {
×
811
                alias = resp.Node.Alias
×
812
        }
×
813
        a.cache[pubkey] = alias
×
814

×
815
        return alias
×
816
}
817

818
// formatMsat formats msat amounts as fractional sats.
819
func formatMsat(amt int64) string {
×
820
        return strconv.FormatFloat(float64(amt)/1000.0, 'f', -1, 64)
×
821
}
×
822

823
// formatPayment formats the payment state as an ascii table.
824
func formatPayment(ctxc context.Context, payment *lnrpc.Payment,
825
        aliases *aliasCache) string {
×
826

×
827
        t := table.NewWriter()
×
828

×
829
        // Build table header.
×
830
        t.AppendHeader(table.Row{
×
831
                "HTLC_STATE", "ATTEMPT_TIME", "RESOLVE_TIME", "RECEIVER_AMT",
×
832
                "FEE", "TIMELOCK", "CHAN_OUT", "ROUTE",
×
833
        })
×
834
        t.SetColumnConfigs([]table.ColumnConfig{
×
835
                {Name: "ATTEMPT_TIME", Align: text.AlignRight},
×
836
                {Name: "RESOLVE_TIME", Align: text.AlignRight},
×
837
                {Name: "CHAN_OUT", Align: text.AlignLeft,
×
838
                        AlignHeader: text.AlignLeft},
×
839
        })
×
840

×
841
        // Add all htlcs as rows.
×
842
        createTime := time.Unix(0, payment.CreationTimeNs)
×
843
        var totalPaid, totalFees int64
×
844
        for _, htlc := range payment.Htlcs {
×
845
                formatTime := func(timeNs int64) string {
×
846
                        if timeNs == 0 {
×
847
                                return "-"
×
848
                        }
×
849
                        resolveTime := time.Unix(0, timeNs)
×
850
                        resolveTimeDiff := resolveTime.Sub(createTime)
×
851
                        resolveTimeMs := resolveTimeDiff / time.Millisecond
×
852
                        return fmt.Sprintf(
×
853
                                "%.3f", float64(resolveTimeMs)/1000.0,
×
854
                        )
×
855
                }
856

857
                attemptTime := formatTime(htlc.AttemptTimeNs)
×
858
                resolveTime := formatTime(htlc.ResolveTimeNs)
×
859

×
860
                route := htlc.Route
×
861
                lastHop := route.Hops[len(route.Hops)-1]
×
862

×
863
                hops := []string{}
×
864
                for _, h := range route.Hops {
×
865
                        alias := aliases.get(ctxc, h.PubKey)
×
866
                        hops = append(hops, alias)
×
867
                }
×
868

869
                state := htlc.Status.String()
×
870
                if htlc.Failure != nil {
×
871
                        state = fmt.Sprintf(
×
872
                                "%v @ %s hop",
×
873
                                htlc.Failure.Code,
×
874
                                ordinalNumber(htlc.Failure.FailureSourceIndex),
×
875
                        )
×
876
                }
×
877

878
                t.AppendRow([]interface{}{
×
879
                        state, attemptTime, resolveTime,
×
880
                        formatMsat(lastHop.AmtToForwardMsat),
×
881
                        formatMsat(route.TotalFeesMsat),
×
882
                        route.TotalTimeLock, route.Hops[0].ChanId,
×
883
                        strings.Join(hops, "->")},
×
884
                )
×
885

×
886
                if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
×
887
                        totalPaid += lastHop.AmtToForwardMsat
×
888
                        totalFees += route.TotalFeesMsat
×
889
                }
×
890
        }
891

892
        // Render table.
893
        b := &bytes.Buffer{}
×
894
        t.SetOutputMirror(b)
×
895
        t.Render()
×
896

×
897
        // Add additional payment-level data.
×
898
        fmt.Fprintf(b, "Amount + fee:   %v + %v sat\n",
×
899
                formatMsat(totalPaid), formatMsat(totalFees))
×
900
        fmt.Fprintf(b, "Payment hash:   %v\n", payment.PaymentHash)
×
901
        fmt.Fprintf(b, "Payment status: %v", payment.Status)
×
902
        switch payment.Status {
×
903
        case lnrpc.Payment_SUCCEEDED:
×
904
                fmt.Fprintf(b, ", preimage: %v", payment.PaymentPreimage)
×
905
        case lnrpc.Payment_FAILED:
×
906
                fmt.Fprintf(b, ", reason: %v", payment.FailureReason)
×
907
        }
908
        fmt.Fprintf(b, "\n")
×
909

×
910
        return b.String()
×
911
}
912

913
var payInvoiceCommand = cli.Command{
914
        Name:     "payinvoice",
915
        Category: "Payments",
916
        Usage:    "Pay an invoice over lightning.",
917
        Description: `
918
        This command is a shortcut for 'sendpayment --pay_req='.
919
        `,
920
        ArgsUsage: "pay_req",
921
        Flags: append(PaymentFlags(),
922
                cli.Int64Flag{
923
                        Name: "amt",
924
                        Usage: "(optional) number of satoshis to fulfill the " +
925
                                "invoice",
926
                },
927
        ),
928
        Action: actionDecorator(payInvoice),
929
}
930

931
func payInvoice(ctx *cli.Context) error {
×
932
        conn := getClientConn(ctx, false)
×
933
        defer conn.Close()
×
934

×
935
        args := ctx.Args()
×
936

×
937
        var payReq string
×
938
        switch {
×
939
        case ctx.IsSet("pay_req"):
×
940
                payReq = ctx.String("pay_req")
×
941
        case args.Present():
×
942
                payReq = args.First()
×
943
        default:
×
944
                return fmt.Errorf("pay_req argument missing")
×
945
        }
946

947
        req := &routerrpc.SendPaymentRequest{
×
948
                PaymentRequest:    StripPrefix(payReq),
×
949
                Amt:               ctx.Int64("amt"),
×
950
                DestCustomRecords: make(map[uint64][]byte),
×
951
                Amp:               ctx.Bool(ampFlag.Name),
×
952
                Cancelable:        ctx.Bool(cancelableFlag.Name),
×
953
        }
×
954

×
955
        return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
×
956
}
957

958
var sendToRouteCommand = cli.Command{
959
        Name:     "sendtoroute",
960
        Category: "Payments",
961
        Usage:    "Send a payment over a predefined route.",
962
        Description: `
963
        Send a payment over Lightning using a specific route. One must specify
964
        the route to attempt and the payment hash. This command can even
965
        be chained with the response to queryroutes or buildroute. This command
966
        can be used to implement channel rebalancing by crafting a self-route,
967
        or even atomic swaps using a self-route that crosses multiple chains.
968

969
        There are three ways to specify a route:
970
           * using the --routes parameter to manually specify a JSON encoded
971
             route in the format of the return value of queryroutes or
972
             buildroute:
973
                 (lncli sendtoroute --payment_hash=<pay_hash> --routes=<route>)
974

975
           * passing the route as a positional argument:
976
                 (lncli sendtoroute --payment_hash=pay_hash <route>)
977

978
           * or reading in the route from stdin, which can allow chaining the
979
             response from queryroutes or buildroute, or even read in a file
980
             with a pre-computed route:
981
                 (lncli queryroutes --args.. | lncli sendtoroute --payment_hash= -
982

983
             notice the '-' at the end, which signals that lncli should read
984
             the route in from stdin
985
        `,
986
        Flags: []cli.Flag{
987
                cli.StringFlag{
988
                        Name:  "payment_hash, pay_hash",
989
                        Usage: "the hash to use within the payment's HTLC",
990
                },
991
                cli.StringFlag{
992
                        Name: "routes, r",
993
                        Usage: "a json array string in the format of the response " +
994
                                "of queryroutes that denotes which routes to use",
995
                },
996
                cli.BoolFlag{
997
                        Name: "skip_temp_err",
998
                        Usage: "Whether the payment should be marked as " +
999
                                "failed when a temporary error occurred. Set " +
1000
                                "it to true so the payment won't be failed " +
1001
                                "unless a terminal error has occurred.",
1002
                },
1003
        },
1004
        Action: sendToRoute,
1005
}
1006

1007
func sendToRoute(ctx *cli.Context) error {
×
1008
        // Show command help if no arguments provided.
×
1009
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
1010
                _ = cli.ShowCommandHelp(ctx, "sendtoroute")
×
1011
                return nil
×
1012
        }
×
1013

1014
        args := ctx.Args()
×
1015

×
1016
        var (
×
1017
                rHash []byte
×
1018
                err   error
×
1019
        )
×
1020
        switch {
×
1021
        case ctx.IsSet("payment_hash"):
×
1022
                rHash, err = hex.DecodeString(ctx.String("payment_hash"))
×
1023
        case args.Present():
×
1024
                rHash, err = hex.DecodeString(args.First())
×
1025

×
1026
                args = args.Tail()
×
1027
        default:
×
1028
                return fmt.Errorf("payment hash argument missing")
×
1029
        }
1030

1031
        if err != nil {
×
1032
                return err
×
1033
        }
×
1034

1035
        if len(rHash) != 32 {
×
1036
                return fmt.Errorf("payment hash must be exactly 32 "+
×
1037
                        "bytes, is instead %d", len(rHash))
×
1038
        }
×
1039

1040
        var jsonRoutes string
×
1041
        switch {
×
1042
        // The user is specifying the routes explicitly via the key word
1043
        // argument.
1044
        case ctx.IsSet("routes"):
×
1045
                jsonRoutes = ctx.String("routes")
×
1046

1047
        // The user is specifying the routes as a positional argument.
1048
        case args.Present() && args.First() != "-":
×
1049
                jsonRoutes = args.First()
×
1050

1051
        // The user is signalling that we should read stdin in order to parse
1052
        // the set of target routes.
1053
        case args.Present() && args.First() == "-":
×
1054
                b, err := io.ReadAll(os.Stdin)
×
1055
                if err != nil {
×
1056
                        return err
×
1057
                }
×
1058
                if len(b) == 0 {
×
1059
                        return fmt.Errorf("queryroutes output is empty")
×
1060
                }
×
1061

1062
                jsonRoutes = string(b)
×
1063
        }
1064

1065
        // Try to parse the provided json both in the legacy QueryRoutes format
1066
        // that contains a list of routes and the single route BuildRoute
1067
        // format.
1068
        var route *lnrpc.Route
×
1069
        routes := &lnrpc.QueryRoutesResponse{}
×
1070
        err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal([]byte(jsonRoutes), routes)
×
1071
        if err == nil {
×
1072
                if len(routes.Routes) == 0 {
×
1073
                        return fmt.Errorf("no routes provided")
×
1074
                }
×
1075

1076
                if len(routes.Routes) != 1 {
×
1077
                        return fmt.Errorf("expected a single route, but got %v",
×
1078
                                len(routes.Routes))
×
1079
                }
×
1080

1081
                route = routes.Routes[0]
×
1082
        } else {
×
1083
                routes := &routerrpc.BuildRouteResponse{}
×
1084
                err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(
×
1085
                        []byte(jsonRoutes), routes,
×
1086
                )
×
1087
                if err != nil {
×
1088
                        return fmt.Errorf("unable to unmarshal json string "+
×
1089
                                "from incoming array of routes: %v", err)
×
1090
                }
×
1091

1092
                route = routes.Route
×
1093
        }
1094

1095
        req := &routerrpc.SendToRouteRequest{
×
1096
                PaymentHash: rHash,
×
1097
                Route:       route,
×
1098
                SkipTempErr: ctx.Bool("skip_temp_err"),
×
1099
        }
×
1100

×
1101
        return sendToRouteRequest(ctx, req)
×
1102
}
1103

1104
func sendToRouteRequest(ctx *cli.Context, req *routerrpc.SendToRouteRequest) error {
×
1105
        ctxc := getContext()
×
1106
        conn := getClientConn(ctx, false)
×
1107
        defer conn.Close()
×
1108

×
1109
        client := routerrpc.NewRouterClient(conn)
×
1110

×
1111
        resp, err := client.SendToRouteV2(ctxc, req)
×
1112
        if err != nil {
×
1113
                return err
×
1114
        }
×
1115

1116
        printRespJSON(resp)
×
1117

×
1118
        return nil
×
1119
}
1120

1121
var queryRoutesCommand = cli.Command{
1122
        Name:        "queryroutes",
1123
        Category:    "Payments",
1124
        Usage:       "Query a route to a destination.",
1125
        Description: "Queries the channel router for a potential path to the destination that has sufficient flow for the amount including fees",
1126
        ArgsUsage:   "dest amt",
1127
        Flags: []cli.Flag{
1128
                cli.StringFlag{
1129
                        Name: "dest",
1130
                        Usage: "the 33-byte hex-encoded public key for the payment " +
1131
                                "destination",
1132
                },
1133
                cli.Int64Flag{
1134
                        Name:  "amt",
1135
                        Usage: "the amount to send expressed in satoshis",
1136
                },
1137
                cli.Int64Flag{
1138
                        Name: "fee_limit",
1139
                        Usage: "maximum fee allowed in satoshis when sending " +
1140
                                "the payment",
1141
                },
1142
                cli.Int64Flag{
1143
                        Name: "fee_limit_percent",
1144
                        Usage: "percentage of the payment's amount used as the " +
1145
                                "maximum fee allowed when sending the payment",
1146
                },
1147
                cli.Int64Flag{
1148
                        Name: "final_cltv_delta",
1149
                        Usage: "(optional) number of blocks the last hop has " +
1150
                                "to reveal the preimage. Note that this " +
1151
                                "should not be set in the case where the " +
1152
                                "path includes a blinded path since in " +
1153
                                "that case, the receiver will already have " +
1154
                                "accounted for this value in the " +
1155
                                "blinded_cltv value",
1156
                },
1157
                cli.BoolFlag{
1158
                        Name:  "use_mc",
1159
                        Usage: "use mission control probabilities",
1160
                },
1161
                cli.Uint64Flag{
1162
                        Name: "outgoing_chan_id",
1163
                        Usage: "(optional) the channel id of the channel " +
1164
                                "that must be taken to the first hop",
1165
                },
1166
                cli.StringSliceFlag{
1167
                        Name: "ignore_pair",
1168
                        Usage: "ignore directional node pair " +
1169
                                "<node1>:<node2>. This flag can be specified " +
1170
                                "multiple times if multiple node pairs are " +
1171
                                "to be ignored",
1172
                },
1173
                timePrefFlag,
1174
                cltvLimitFlag,
1175
                introductionNodeFlag,
1176
                blindingPointFlag,
1177
                blindedHopsFlag,
1178
                blindedBaseFlag,
1179
                blindedPPMFlag,
1180
                blindedCLTVFlag,
1181
                cli.StringFlag{
1182
                        Name: "route_hints",
1183
                        Usage: `route hints for searching through private ` +
1184
                                `channels (and no blinded paths set). eg: ` +
1185
                                `'[{"hop_hints":[{"node_id":"A","chan_id":1,` +
1186
                                `"fee_base_msat":2,` +
1187
                                `"fee_proportional_millionths":3,` +
1188
                                `"cltv_expiry_delta":4}]}]'`,
1189
                },
1190
        },
1191
        Action: actionDecorator(queryRoutes),
1192
}
1193

1194
func queryRoutes(ctx *cli.Context) error {
×
1195
        ctxc := getContext()
×
1196
        client, cleanUp := getClient(ctx)
×
1197
        defer cleanUp()
×
1198

×
1199
        var (
×
1200
                dest string
×
1201
                amt  int64
×
1202
                err  error
×
1203
        )
×
1204

×
1205
        args := ctx.Args()
×
1206

×
1207
        switch {
×
1208
        case ctx.IsSet("dest"):
×
1209
                dest = ctx.String("dest")
×
1210

1211
        case args.Present():
×
1212
                dest = args.First()
×
1213
                args = args.Tail()
×
1214

1215
        // If we have a blinded path set, we don't have to specify a
1216
        // destination.
1217
        case ctx.IsSet(introductionNodeFlag.Name):
×
1218

1219
        default:
×
1220
                return fmt.Errorf("dest argument missing")
×
1221
        }
1222

1223
        switch {
×
1224
        case ctx.IsSet("amt"):
×
1225
                amt = ctx.Int64("amt")
×
1226
        case args.Present():
×
1227
                amt, err = strconv.ParseInt(args.First(), 10, 64)
×
1228
                if err != nil {
×
1229
                        return fmt.Errorf("unable to decode amt argument: %w",
×
1230
                                err)
×
1231
                }
×
1232
        default:
×
1233
                return fmt.Errorf("amt argument missing")
×
1234
        }
1235

1236
        feeLimit, err := retrieveFeeLimitLegacy(ctx)
×
1237
        if err != nil {
×
1238
                return err
×
1239
        }
×
1240

1241
        pairs := ctx.StringSlice("ignore_pair")
×
1242
        ignoredPairs := make([]*lnrpc.NodePair, len(pairs))
×
1243
        for i, pair := range pairs {
×
1244
                nodes := strings.Split(pair, ":")
×
1245
                if len(nodes) != 2 {
×
1246
                        return fmt.Errorf("invalid node pair format. " +
×
1247
                                "Expected <node1 pub key>:<node2 pub key>")
×
1248
                }
×
1249

1250
                node1, err := hex.DecodeString(nodes[0])
×
1251
                if err != nil {
×
1252
                        return err
×
1253
                }
×
1254

1255
                node2, err := hex.DecodeString(nodes[1])
×
1256
                if err != nil {
×
1257
                        return err
×
1258
                }
×
1259

1260
                ignoredPairs[i] = &lnrpc.NodePair{
×
1261
                        From: node1,
×
1262
                        To:   node2,
×
1263
                }
×
1264
        }
1265

1266
        blindedRoutes, err := parseBlindedPaymentParameters(ctx)
×
1267
        if err != nil {
×
1268
                return err
×
1269
        }
×
1270

1271
        req := &lnrpc.QueryRoutesRequest{
×
1272
                PubKey:              dest,
×
1273
                Amt:                 amt,
×
1274
                FeeLimit:            feeLimit,
×
1275
                FinalCltvDelta:      int32(ctx.Int("final_cltv_delta")),
×
1276
                UseMissionControl:   ctx.Bool("use_mc"),
×
1277
                CltvLimit:           uint32(ctx.Uint64(cltvLimitFlag.Name)),
×
1278
                OutgoingChanId:      ctx.Uint64("outgoing_chan_id"),
×
1279
                TimePref:            ctx.Float64(timePrefFlag.Name),
×
1280
                IgnoredPairs:        ignoredPairs,
×
1281
                BlindedPaymentPaths: blindedRoutes,
×
1282
        }
×
1283

×
1284
        if ctx.IsSet("route_hints") {
×
1285
                if len(blindedRoutes) > 0 {
×
1286
                        return fmt.Errorf("--route_hints should not be used " +
×
1287
                                "if blinded paths are set")
×
1288
                }
×
1289
                routeHintsJSON := ctx.String("route_hints")
×
1290
                var routeHints []*lnrpc.RouteHint
×
1291

×
1292
                err := json.Unmarshal([]byte(routeHintsJSON), &routeHints)
×
1293
                if err != nil {
×
1294
                        return fmt.Errorf("error unmarshaling route_hints "+
×
1295
                                "json: %w", err)
×
1296
                }
×
1297

1298
                req.RouteHints = routeHints
×
1299
        }
1300

1301
        route, err := client.QueryRoutes(ctxc, req)
×
1302
        if err != nil {
×
1303
                return err
×
1304
        }
×
1305

1306
        printRespJSON(route)
×
1307

×
1308
        return nil
×
1309
}
1310

1311
func parseBlindedPaymentParameters(ctx *cli.Context) (
1312
        []*lnrpc.BlindedPaymentPath, error) {
×
1313

×
1314
        // Return nil if we don't have a blinding set, as we don't have a
×
1315
        // blinded path.
×
1316
        if !ctx.IsSet(blindingPointFlag.Name) {
×
1317
                return nil, nil
×
1318
        }
×
1319

1320
        // If a blinded path has been provided, then the final_cltv_delta flag
1321
        // should not be provided since this value will be ignored.
1322
        if ctx.IsSet("final_cltv_delta") {
×
1323
                return nil, fmt.Errorf("`final_cltv_delta` should not be " +
×
1324
                        "provided if a blinded path is provided")
×
1325
        }
×
1326

1327
        // If any one of our blinding related flags is set, we expect the
1328
        // full set to be set and we'll error out accordingly.
1329
        introNode, err := route.NewVertexFromStr(
×
1330
                ctx.String(introductionNodeFlag.Name),
×
1331
        )
×
1332
        if err != nil {
×
1333
                return nil, fmt.Errorf("decode introduction node: %w", err)
×
1334
        }
×
1335

1336
        blindingPoint, err := route.NewVertexFromStr(ctx.String(
×
1337
                blindingPointFlag.Name,
×
1338
        ))
×
1339
        if err != nil {
×
1340
                return nil, fmt.Errorf("decode blinding point: %w", err)
×
1341
        }
×
1342

1343
        blindedHops := ctx.StringSlice(blindedHopsFlag.Name)
×
1344

×
1345
        pmt := &lnrpc.BlindedPaymentPath{
×
1346
                BlindedPath: &lnrpc.BlindedPath{
×
1347
                        IntroductionNode: introNode[:],
×
1348
                        BlindingPoint:    blindingPoint[:],
×
1349
                        BlindedHops: make(
×
1350
                                []*lnrpc.BlindedHop, len(blindedHops),
×
1351
                        ),
×
1352
                },
×
1353
                BaseFeeMsat: ctx.Uint64(
×
1354
                        blindedBaseFlag.Name,
×
1355
                ),
×
1356
                ProportionalFeeRate: uint32(ctx.Uint64(
×
1357
                        blindedPPMFlag.Name,
×
1358
                )),
×
1359
                TotalCltvDelta: uint32(ctx.Uint64(
×
1360
                        blindedCLTVFlag.Name,
×
1361
                )),
×
1362
        }
×
1363

×
1364
        for i, hop := range blindedHops {
×
1365
                parts := strings.Split(hop, ":")
×
1366
                if len(parts) != 2 {
×
1367
                        return nil, fmt.Errorf("blinded hops should be "+
×
1368
                                "expressed as "+
×
1369
                                "blinded_node_id:hex_encrypted_data, got: %v",
×
1370
                                hop)
×
1371
                }
×
1372

1373
                hop, err := route.NewVertexFromStr(parts[0])
×
1374
                if err != nil {
×
1375
                        return nil, fmt.Errorf("hop: %v node: %w", i, err)
×
1376
                }
×
1377

1378
                data, err := hex.DecodeString(parts[1])
×
1379
                if err != nil {
×
1380
                        return nil, fmt.Errorf("hop: %v data: %w", i, err)
×
1381
                }
×
1382

1383
                pmt.BlindedPath.BlindedHops[i] = &lnrpc.BlindedHop{
×
1384
                        BlindedNode:   hop[:],
×
1385
                        EncryptedData: data,
×
1386
                }
×
1387
        }
1388

1389
        return []*lnrpc.BlindedPaymentPath{
×
1390
                pmt,
×
1391
        }, nil
×
1392
}
1393

1394
// retrieveFeeLimitLegacy retrieves the fee limit based on the different fee
1395
// limit flags passed. This function will eventually disappear in favor of
1396
// retrieveFeeLimit and the new payment rpc.
1397
func retrieveFeeLimitLegacy(ctx *cli.Context) (*lnrpc.FeeLimit, error) {
×
1398
        switch {
×
1399
        case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"):
×
1400
                return nil, fmt.Errorf("either fee_limit or fee_limit_percent " +
×
1401
                        "can be set, but not both")
×
1402
        case ctx.IsSet("fee_limit"):
×
1403
                return &lnrpc.FeeLimit{
×
1404
                        Limit: &lnrpc.FeeLimit_Fixed{
×
1405
                                Fixed: ctx.Int64("fee_limit"),
×
1406
                        },
×
1407
                }, nil
×
1408
        case ctx.IsSet("fee_limit_percent"):
×
1409
                feeLimitPercent := ctx.Int64("fee_limit_percent")
×
1410
                if feeLimitPercent < 0 {
×
1411
                        return nil, errors.New("negative fee limit percentage " +
×
1412
                                "provided")
×
1413
                }
×
1414
                return &lnrpc.FeeLimit{
×
1415
                        Limit: &lnrpc.FeeLimit_Percent{
×
1416
                                Percent: feeLimitPercent,
×
1417
                        },
×
1418
                }, nil
×
1419
        }
1420

1421
        // Since the fee limit flags aren't required, we don't return an error
1422
        // if they're not set.
1423
        return nil, nil
×
1424
}
1425

1426
var listPaymentsCommand = cli.Command{
1427
        Name:     "listpayments",
1428
        Category: "Payments",
1429
        Usage:    "List all outgoing payments.",
1430
        Description: `
1431
        This command enables the retrieval of payments stored
1432
        in the database.
1433

1434
        Pagination is supported by the usage of index_offset in combination with
1435
        the paginate_forwards flag.
1436
        Reversed pagination is enabled by default to receive current payments
1437
        first. Pagination can be resumed by using the returned last_index_offset
1438
        (for forwards order), or first_index_offset (for reversed order) as the
1439
        offset_index.
1440

1441
        Because counting all payments in the payment database can take a long
1442
        time on systems with many payments, the count is not returned by
1443
        default. That feature can be turned on with the --count_total_payments
1444
        flag.
1445
        `,
1446
        Flags: []cli.Flag{
1447
                cli.BoolFlag{
1448
                        Name: "include_incomplete",
1449
                        Usage: "if set to true, payments still in flight (or " +
1450
                                "failed) will be returned as well, keeping" +
1451
                                "indices for payments the same as without " +
1452
                                "the flag",
1453
                },
1454
                cli.UintFlag{
1455
                        Name: "index_offset",
1456
                        Usage: "The index of a payment that will be used as " +
1457
                                "either the start (in forwards mode) or end " +
1458
                                "(in reverse mode) of a query to determine " +
1459
                                "which payments should be returned in the " +
1460
                                "response, where the index_offset is " +
1461
                                "excluded. If index_offset is set to zero in " +
1462
                                "reversed mode, the query will end with the " +
1463
                                "last payment made.",
1464
                },
1465
                cli.UintFlag{
1466
                        Name: "max_payments",
1467
                        Usage: "the max number of payments to return, by " +
1468
                                "default, all completed payments are returned",
1469
                },
1470
                cli.BoolFlag{
1471
                        Name: "paginate_forwards",
1472
                        Usage: "if set, payments succeeding the " +
1473
                                "index_offset will be returned, allowing " +
1474
                                "forwards pagination",
1475
                },
1476
                cli.BoolFlag{
1477
                        Name: "count_total_payments",
1478
                        Usage: "if set, all payments (complete or incomplete, " +
1479
                                "independent of max_payments parameter) will " +
1480
                                "be counted; can take a long time on systems " +
1481
                                "with many payments",
1482
                },
1483
                cli.Uint64Flag{
1484
                        Name: "creation_date_start",
1485
                        Usage: "timestamp in seconds, if set, filter " +
1486
                                "payments with creation date greater than or " +
1487
                                "equal to it",
1488
                },
1489
                cli.Uint64Flag{
1490
                        Name: "creation_date_end",
1491
                        Usage: "timestamp in seconds, if set, filter " +
1492
                                "payments with creation date less than or " +
1493
                                "equal to it",
1494
                },
1495
        },
1496
        Action: actionDecorator(listPayments),
1497
}
1498

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

×
1504
        req := &lnrpc.ListPaymentsRequest{
×
1505
                IncludeIncomplete:  ctx.Bool("include_incomplete"),
×
1506
                IndexOffset:        uint64(ctx.Uint("index_offset")),
×
1507
                MaxPayments:        uint64(ctx.Uint("max_payments")),
×
1508
                Reversed:           !ctx.Bool("paginate_forwards"),
×
1509
                CountTotalPayments: ctx.Bool("count_total_payments"),
×
1510
                CreationDateStart:  ctx.Uint64("creation_date_start"),
×
1511
                CreationDateEnd:    ctx.Uint64("creation_date_end"),
×
1512
        }
×
1513

×
1514
        payments, err := client.ListPayments(ctxc, req)
×
1515
        if err != nil {
×
1516
                return err
×
1517
        }
×
1518

1519
        printRespJSON(payments)
×
1520
        return nil
×
1521
}
1522

1523
var forwardingHistoryCommand = cli.Command{
1524
        Name:     "fwdinghistory",
1525
        Category: "Payments",
1526
        Usage:    "Query the history of all forwarded HTLCs.",
1527
        ArgsUsage: "start_time [end_time] [index_offset] [max_events]" +
1528
                "[--incoming_channel_ids] [--outgoing_channel_ids]",
1529
        Description: `
1530
        Query the HTLC switch's internal forwarding log for all completed
1531
        payment circuits (HTLCs) over a particular time range (--start_time and
1532
        --end_time). The start and end times are meant to be expressed in
1533
        seconds since the Unix epoch.
1534
        Alternatively negative time ranges can be used, e.g. "-3d". Supports
1535
        s(seconds), m(minutes), h(ours), d(ays), w(eeks), M(onths), y(ears).
1536
        Month equals 30.44 days, year equals 365.25 days.
1537
        If --start_time isn't provided, then 24 hours ago is used. If
1538
        --end_time isn't provided, then the current time is used.
1539

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

1543
        Incoming and outgoing channel IDs can be provided to further filter
1544
        the events. If not provided, all events will be returned.
1545

1546
        Finally, callers can skip a series of events using the --index_offset
1547
        parameter. Each response will contain the offset index of the last
1548
        entry. Using this callers can manually paginate within a time slice.
1549
        `,
1550
        Flags: []cli.Flag{
1551
                cli.StringFlag{
1552
                        Name: "start_time",
1553
                        Usage: "the starting time for the query " +
1554
                                `as unix timestamp or relative e.g. "-1w"`,
1555
                },
1556
                cli.StringFlag{
1557
                        Name: "end_time",
1558
                        Usage: "the end time for the query " +
1559
                                `as unix timestamp or relative e.g. "-1w"`,
1560
                },
1561
                cli.Int64Flag{
1562
                        Name:  "index_offset",
1563
                        Usage: "the number of events to skip",
1564
                },
1565
                cli.Int64Flag{
1566
                        Name:  "max_events",
1567
                        Usage: "the max number of events to return",
1568
                },
1569
                cli.BoolFlag{
1570
                        Name: "skip_peer_alias_lookup",
1571
                        Usage: "skip the peer alias lookup per forwarding " +
1572
                                "event in order to improve performance",
1573
                },
1574
                cli.Int64SliceFlag{
1575
                        Name: "incoming_chan_ids",
1576
                        Usage: "the short channel id of the incoming " +
1577
                                "channel to filter events by; can be " +
1578
                                "specified multiple times in the same command",
1579
                },
1580
                cli.Int64SliceFlag{
1581
                        Name: "outgoing_chan_ids",
1582
                        Usage: "the short channel id of the outgoing " +
1583
                                "channel to filter events by; can be " +
1584
                                "specified multiple times in the same command",
1585
                },
1586
        },
1587
        Action: actionDecorator(forwardingHistory),
1588
}
1589

1590
func forwardingHistory(ctx *cli.Context) error {
×
1591
        ctxc := getContext()
×
1592
        client, cleanUp := getClient(ctx)
×
1593
        defer cleanUp()
×
1594

×
1595
        var (
×
1596
                startTime, endTime     uint64
×
1597
                indexOffset, maxEvents uint32
×
1598
                err                    error
×
1599
        )
×
1600
        args := ctx.Args()
×
1601
        now := time.Now()
×
1602

×
1603
        switch {
×
1604
        case ctx.IsSet("start_time"):
×
1605
                startTime, err = parseTime(ctx.String("start_time"), now)
×
1606
        case args.Present():
×
1607
                startTime, err = parseTime(args.First(), now)
×
1608
                args = args.Tail()
×
1609
        default:
×
1610
                now := time.Now()
×
1611
                startTime = uint64(now.Add(-time.Hour * 24).Unix())
×
1612
        }
1613
        if err != nil {
×
1614
                return fmt.Errorf("unable to decode start_time: %w", err)
×
1615
        }
×
1616

1617
        switch {
×
1618
        case ctx.IsSet("end_time"):
×
1619
                endTime, err = parseTime(ctx.String("end_time"), now)
×
1620
        case args.Present():
×
1621
                endTime, err = parseTime(args.First(), now)
×
1622
                args = args.Tail()
×
1623
        default:
×
1624
                endTime = uint64(now.Unix())
×
1625
        }
1626
        if err != nil {
×
1627
                return fmt.Errorf("unable to decode end_time: %w", err)
×
1628
        }
×
1629

1630
        switch {
×
1631
        case ctx.IsSet("index_offset"):
×
1632
                indexOffset = uint32(ctx.Int64("index_offset"))
×
1633
        case args.Present():
×
1634
                i, err := strconv.ParseInt(args.First(), 10, 64)
×
1635
                if err != nil {
×
1636
                        return fmt.Errorf("unable to decode index_offset: %w",
×
1637
                                err)
×
1638
                }
×
1639
                indexOffset = uint32(i)
×
1640
                args = args.Tail()
×
1641
        }
1642

1643
        switch {
×
1644
        case ctx.IsSet("max_events"):
×
1645
                maxEvents = uint32(ctx.Int64("max_events"))
×
1646
        case args.Present():
×
1647
                m, err := strconv.ParseInt(args.First(), 10, 64)
×
1648
                if err != nil {
×
1649
                        return fmt.Errorf("unable to decode max_events: %w",
×
1650
                                err)
×
1651
                }
×
1652
                maxEvents = uint32(m)
×
1653
        }
1654

1655
        // By default we will look up the peers' alias information unless the
1656
        // skip_peer_alias_lookup flag is specified.
1657
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1658

×
1659
        req := &lnrpc.ForwardingHistoryRequest{
×
1660
                StartTime:       startTime,
×
1661
                EndTime:         endTime,
×
1662
                IndexOffset:     indexOffset,
×
1663
                NumMaxEvents:    maxEvents,
×
1664
                PeerAliasLookup: lookupPeerAlias,
×
1665
        }
×
NEW
1666
        outgoingChannelIDs := ctx.Int64Slice("outgoing_chan_ids")
×
NEW
1667
        if len(outgoingChannelIDs) != 0 {
×
NEW
1668
                req.OutgoingChanIds = make([]uint64, len(outgoingChannelIDs))
×
NEW
1669
                for i, c := range outgoingChannelIDs {
×
NEW
1670
                        req.OutgoingChanIds[i] = uint64(c)
×
NEW
1671
                }
×
1672
        }
1673

NEW
1674
        incomingChannelIDs := ctx.Int64Slice("incoming_chan_ids")
×
NEW
1675
        if len(incomingChannelIDs) != 0 {
×
NEW
1676
                req.IncomingChanIds = make([]uint64, len(incomingChannelIDs))
×
NEW
1677
                for i, c := range incomingChannelIDs {
×
NEW
1678
                        req.IncomingChanIds[i] = uint64(c)
×
NEW
1679
                }
×
1680
        }
1681
        resp, err := client.ForwardingHistory(ctxc, req)
×
1682
        if err != nil {
×
1683
                return err
×
1684
        }
×
1685

1686
        printRespJSON(resp)
×
1687
        return nil
×
1688
}
1689

1690
var buildRouteCommand = cli.Command{
1691
        Name:     "buildroute",
1692
        Category: "Payments",
1693
        Usage:    "Build a route from a list of hop pubkeys.",
1694
        Description: `
1695
        Builds a sphinx route for the supplied hops (public keys). Make sure to
1696
        use a custom final_cltv_delta to create the route depending on the
1697
        restrictions in the invoice otherwise LND will use its default specified
1698
        via the bitcoin.timelockdelta setting (default 80).
1699
        If the final_cltv_delta mismatch you will likely see the error
1700
        INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS returned by the receiving node.
1701

1702
        Moreover a payment_addr has to be provided if the invoice supplied it as
1703
        well otherwise the payment will be rejected by the receiving node.
1704
        `,
1705
        Action: actionDecorator(buildRoute),
1706
        Flags: []cli.Flag{
1707
                cli.Int64Flag{
1708
                        Name: "amt",
1709
                        Usage: "the amount to send expressed in satoshis. If" +
1710
                                "not set, the minimum routable amount is used",
1711
                },
1712
                cli.Int64Flag{
1713
                        Name: "final_cltv_delta",
1714
                        Usage: "number of blocks the last hop has to reveal " +
1715
                                "the preimage; if not set the default lnd " +
1716
                                "final_cltv_delta is used",
1717
                },
1718
                cli.StringFlag{
1719
                        Name:  "hops",
1720
                        Usage: "comma separated hex pubkeys",
1721
                },
1722
                cli.Uint64Flag{
1723
                        Name: "outgoing_chan_id",
1724
                        Usage: "short channel id of the outgoing channel to " +
1725
                                "use for the first hop of the payment",
1726
                        Value: 0,
1727
                },
1728
                cli.StringFlag{
1729
                        Name: "payment_addr",
1730
                        Usage: "hex encoded payment address to set in the " +
1731
                                "last hop's mpp record",
1732
                },
1733
        },
1734
}
1735

1736
func buildRoute(ctx *cli.Context) error {
×
1737
        ctxc := getContext()
×
1738
        conn := getClientConn(ctx, false)
×
1739
        defer conn.Close()
×
1740

×
1741
        client := routerrpc.NewRouterClient(conn)
×
1742

×
1743
        if !ctx.IsSet("hops") {
×
1744
                return errors.New("hops required")
×
1745
        }
×
1746

1747
        // Build list of hop addresses for the rpc.
1748
        hops := strings.Split(ctx.String("hops"), ",")
×
1749
        rpcHops := make([][]byte, 0, len(hops))
×
1750
        for _, k := range hops {
×
1751
                pubkey, err := route.NewVertexFromStr(k)
×
1752
                if err != nil {
×
1753
                        return fmt.Errorf("error parsing %v: %w", k, err)
×
1754
                }
×
1755
                rpcHops = append(rpcHops, pubkey[:])
×
1756
        }
1757

1758
        var amtMsat int64
×
1759
        hasAmt := ctx.IsSet("amt")
×
1760
        if hasAmt {
×
1761
                amtMsat = ctx.Int64("amt") * 1000
×
1762
                if amtMsat == 0 {
×
1763
                        return fmt.Errorf("non-zero amount required")
×
1764
                }
×
1765
        }
1766

1767
        var (
×
1768
                payAddr []byte
×
1769
                err     error
×
1770
        )
×
1771

×
1772
        if ctx.IsSet("payment_addr") {
×
1773
                payAddr, err = hex.DecodeString(ctx.String("payment_addr"))
×
1774
                if err != nil {
×
1775
                        return fmt.Errorf("error parsing payment_addr: %w", err)
×
1776
                }
×
1777
        }
1778

1779
        // Call BuildRoute rpc.
1780
        req := &routerrpc.BuildRouteRequest{
×
1781
                AmtMsat:        amtMsat,
×
1782
                FinalCltvDelta: int32(ctx.Int64("final_cltv_delta")),
×
1783
                HopPubkeys:     rpcHops,
×
1784
                OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
×
1785
                PaymentAddr:    payAddr,
×
1786
        }
×
1787

×
1788
        route, err := client.BuildRoute(ctxc, req)
×
1789
        if err != nil {
×
1790
                return err
×
1791
        }
×
1792

1793
        printRespJSON(route)
×
1794

×
1795
        return nil
×
1796
}
1797

1798
var deletePaymentsCommand = cli.Command{
1799
        Name:     "deletepayments",
1800
        Category: "Payments",
1801
        Usage:    "Delete a single or multiple payments from the database.",
1802
        ArgsUsage: "--all [--failed_htlcs_only --include_non_failed] | " +
1803
                "--payment_hash hash [--failed_htlcs_only]",
1804
        Description: `
1805
        This command either deletes all failed payments or a single payment from
1806
        the database to reclaim disk space.
1807

1808
        If the --all flag is used, then all failed payments are removed. If so
1809
        desired, _ALL_ payments (even the successful ones) can be deleted
1810
        by additionally specifying --include_non_failed.
1811

1812
        If a --payment_hash is specified, that single payment is deleted,
1813
        independent of its state.
1814

1815
        If --failed_htlcs_only is specified then the payments themselves (or the
1816
        single payment itself if used with --payment_hash) is not deleted, only
1817
        the information about any failed HTLC attempts during the payment.
1818

1819
        NOTE: Removing payments from the database does free up disk space within
1820
        the internal bbolt database. But that disk space is only reclaimed after
1821
        compacting the database. Users might want to turn on auto compaction
1822
        (db.bolt.auto-compact=true in the config file or --db.bolt.auto-compact
1823
        as a command line flag) and restart lnd after deleting a large number of
1824
        payments to see a reduction in the file size of the channel.db file.
1825
        `,
1826
        Action: actionDecorator(deletePayments),
1827
        Flags: []cli.Flag{
1828
                cli.BoolFlag{
1829
                        Name:  "all",
1830
                        Usage: "delete all failed payments",
1831
                },
1832
                cli.StringFlag{
1833
                        Name: "payment_hash",
1834
                        Usage: "delete a specific payment identified by its " +
1835
                                "payment hash",
1836
                },
1837
                cli.BoolFlag{
1838
                        Name: "failed_htlcs_only",
1839
                        Usage: "only delete failed HTLCs from payments, not " +
1840
                                "the payment itself",
1841
                },
1842
                cli.BoolFlag{
1843
                        Name:  "include_non_failed",
1844
                        Usage: "delete ALL payments, not just the failed ones",
1845
                },
1846
        },
1847
}
1848

1849
func deletePayments(ctx *cli.Context) error {
×
1850
        ctxc := getContext()
×
1851
        client, cleanUp := getClient(ctx)
×
1852
        defer cleanUp()
×
1853

×
1854
        // Show command help if arguments or no flags are provided.
×
1855
        if ctx.NArg() > 0 || ctx.NumFlags() == 0 {
×
1856
                _ = cli.ShowCommandHelp(ctx, "deletepayments")
×
1857
                return nil
×
1858
        }
×
1859

1860
        var (
×
1861
                paymentHash      []byte
×
1862
                all              = ctx.Bool("all")
×
1863
                singlePayment    = ctx.IsSet("payment_hash")
×
1864
                failedHTLCsOnly  = ctx.Bool("failed_htlcs_only")
×
1865
                includeNonFailed = ctx.Bool("include_non_failed")
×
1866
                err              error
×
1867
                resp             proto.Message
×
1868
        )
×
1869

×
1870
        // We pack two RPCs into the same CLI so there are a few non-valid
×
1871
        // combinations of the flags we need to filter out.
×
1872
        switch {
×
1873
        case all && singlePayment:
×
1874
                return fmt.Errorf("cannot use --all and --payment_hash at " +
×
1875
                        "the same time")
×
1876

1877
        case singlePayment && includeNonFailed:
×
1878
                return fmt.Errorf("cannot use --payment_hash and " +
×
1879
                        "--include_non_failed at the same time, when using " +
×
1880
                        "a payment hash the payment is deleted independent " +
×
1881
                        "of its state")
×
1882
        }
1883

1884
        // Deleting a single payment is implemented in a different RPC than
1885
        // removing all/multiple payments.
1886
        switch {
×
1887
        case singlePayment:
×
1888
                paymentHash, err = hex.DecodeString(ctx.String("payment_hash"))
×
1889
                if err != nil {
×
1890
                        return fmt.Errorf("error decoding payment_hash: %w",
×
1891
                                err)
×
1892
                }
×
1893

1894
                resp, err = client.DeletePayment(
×
1895
                        ctxc, &lnrpc.DeletePaymentRequest{
×
1896
                                PaymentHash:     paymentHash,
×
1897
                                FailedHtlcsOnly: failedHTLCsOnly,
×
1898
                        },
×
1899
                )
×
1900
                if err != nil {
×
1901
                        return fmt.Errorf("error deleting single payment: %w",
×
1902
                                err)
×
1903
                }
×
1904

1905
        case all:
×
1906
                what := "failed"
×
1907
                if includeNonFailed {
×
1908
                        what = "all"
×
1909
                }
×
1910
                if failedHTLCsOnly {
×
1911
                        what = fmt.Sprintf("failed HTLCs from %s", what)
×
1912
                }
×
1913

1914
                fmt.Printf("Removing %s payments, this might take a while...\n",
×
1915
                        what)
×
1916
                resp, err = client.DeleteAllPayments(
×
1917
                        ctxc, &lnrpc.DeleteAllPaymentsRequest{
×
1918
                                AllPayments:        includeNonFailed,
×
1919
                                FailedPaymentsOnly: !includeNonFailed,
×
1920
                                FailedHtlcsOnly:    failedHTLCsOnly,
×
1921
                        },
×
1922
                )
×
1923
                if err != nil {
×
1924
                        return fmt.Errorf("error deleting payments: %w", err)
×
1925
                }
×
1926

1927
        default:
×
1928
                return fmt.Errorf("either --all or --payment_hash must be set")
×
1929
        }
1930

1931
        printJSON(resp)
×
1932

×
1933
        return nil
×
1934
}
1935

1936
var estimateRouteFeeCommand = cli.Command{
1937
        Name:     "estimateroutefee",
1938
        Category: "Payments",
1939
        Usage:    "Estimate routing fees based on a destination or an invoice.",
1940
        Action:   actionDecorator(estimateRouteFee),
1941
        Flags: []cli.Flag{
1942
                cli.StringFlag{
1943
                        Name: "dest",
1944
                        Usage: "the 33-byte hex-encoded public key for the " +
1945
                                "probe destination. If it is specified then " +
1946
                                "the amt flag is required. If it isn't " +
1947
                                "specified then the pay_req field has to.",
1948
                },
1949
                cli.Int64Flag{
1950
                        Name: "amt",
1951
                        Usage: "the payment amount expressed in satoshis " +
1952
                                "that should be probed for. This field is " +
1953
                                "mandatory if dest is specified.",
1954
                },
1955
                cli.StringFlag{
1956
                        Name: "pay_req",
1957
                        Usage: "a zpay32 encoded payment request which is " +
1958
                                "used to probe. If the destination is " +
1959
                                "not public then route hints are scanned for " +
1960
                                "a public node.",
1961
                },
1962
                cli.DurationFlag{
1963
                        Name: "timeout",
1964
                        Usage: "a deadline for the probe attempt. Only " +
1965
                                "applicable if pay_req is specified.",
1966
                        Value: paymentTimeout,
1967
                },
1968
        },
1969
}
1970

1971
func estimateRouteFee(ctx *cli.Context) error {
×
1972
        ctxc := getContext()
×
1973
        conn := getClientConn(ctx, false)
×
1974
        defer conn.Close()
×
1975

×
1976
        client := routerrpc.NewRouterClient(conn)
×
1977

×
1978
        req := &routerrpc.RouteFeeRequest{}
×
1979

×
1980
        switch {
×
1981
        case ctx.IsSet("dest") && ctx.IsSet("pay_req"):
×
1982
                return fmt.Errorf("either dest or pay_req can be set")
×
1983

1984
        case ctx.IsSet("dest") && !ctx.IsSet("amt"):
×
1985
                return fmt.Errorf("amt is required when dest is set")
×
1986

1987
        case ctx.IsSet("dest"):
×
1988
                dest, err := hex.DecodeString(ctx.String("dest"))
×
1989
                if err != nil {
×
1990
                        return err
×
1991
                }
×
1992

1993
                if len(dest) != 33 {
×
1994
                        return fmt.Errorf("dest node pubkey must be exactly "+
×
1995
                                "33 bytes, is instead: %v", len(dest))
×
1996
                }
×
1997

1998
                amtSat := ctx.Int64("amt")
×
1999
                if amtSat == 0 {
×
2000
                        return fmt.Errorf("non-zero amount required")
×
2001
                }
×
2002

2003
                req.Dest = dest
×
2004
                req.AmtSat = amtSat
×
2005

2006
        case ctx.IsSet("pay_req"):
×
2007
                req.PaymentRequest = StripPrefix(ctx.String("pay_req"))
×
2008
                req.Timeout = uint32(ctx.Duration("timeout").Seconds())
×
2009

2010
        default:
×
2011
                return fmt.Errorf("fee estimation arguments missing")
×
2012
        }
2013

2014
        resp, err := client.EstimateRouteFee(ctxc, req)
×
2015
        if err != nil {
×
2016
                return err
×
2017
        }
×
2018

2019
        printRespJSON(resp)
×
2020

×
2021
        return nil
×
2022
}
2023

2024
// ESC is the ASCII code for escape character.
2025
const ESC = 27
2026

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

2031
// clearLines erases the last count lines in the terminal window.
2032
func clearLines(count int) {
×
2033
        _, _ = fmt.Print(strings.Repeat(clearCode, count))
×
2034
}
×
2035

2036
// ordinalNumber returns the ordinal number as a string of a number.
2037
func ordinalNumber(num uint32) string {
×
2038
        switch num {
×
2039
        case 1:
×
2040
                return "1st"
×
2041
        case 2:
×
2042
                return "2nd"
×
2043
        case 3:
×
2044
                return "3rd"
×
2045
        default:
×
2046
                return fmt.Sprintf("%dth", num)
×
2047
        }
2048
}
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