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

lightningnetwork / lnd / 16649448520

31 Jul 2025 12:49PM UTC coverage: 67.047% (+0.003%) from 67.044%
16649448520

push

github

web-flow
Merge pull request #10100 from Abdulkbk/fix-chanid-flag

commands: fix how we parse chan ids args at CLI level

15 of 32 new or added lines in 1 file covered. (46.88%)

58 existing lines in 19 files now uncovered.

135567 of 202196 relevant lines covered (67.05%)

21677.36 hits per line

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

5.45
/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.StringSliceFlag{
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.StringSlice{},
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

×
NEW
524
        var err error
×
NEW
525
        outChan := ctx.StringSlice("outgoing_chan_id")
×
NEW
526
        req.OutgoingChanIds, err = parseChanIDs(outChan)
×
NEW
527
        if err != nil {
×
NEW
528
                return fmt.Errorf("unable to decode outgoing_chan_ids: %w", err)
×
UNCOV
529
        }
×
530

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

636
        req.FeeLimitSat = feeLimit
×
637

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

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

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

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

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

662
        return nil
×
663
}
664

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

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

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

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

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

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

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

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

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

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

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

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

730
        aliases := newAliasCache(lnClient)
×
731

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

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

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

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

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

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

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

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

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

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

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

×
814
        return alias
×
815
}
816

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1013
        args := ctx.Args()
×
1014

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

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

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

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

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

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

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

1061
                jsonRoutes = string(b)
×
1062
        }
1063

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

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

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

1091
                route = routes.Route
×
1092
        }
1093

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

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

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

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

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

1115
        printRespJSON(resp)
×
1116

×
1117
        return nil
×
1118
}
1119

1120
var queryRoutesCommand = cli.Command{
1121
        Name:        "queryroutes",
1122
        Category:    "Payments",
1123
        Usage:       "Query a route to a destination.",
1124
        Description: "Queries the channel router for a potential path to the destination that has sufficient flow for the amount including fees",
1125
        ArgsUsage:   "dest amt",
1126
        Flags: []cli.Flag{
1127
                cli.StringFlag{
1128
                        Name: "dest",
1129
                        Usage: "the 33-byte hex-encoded public key for the payment " +
1130
                                "destination",
1131
                },
1132
                cli.Int64Flag{
1133
                        Name:  "amt",
1134
                        Usage: "the amount to send expressed in satoshis",
1135
                },
1136
                cli.Int64Flag{
1137
                        Name: "fee_limit",
1138
                        Usage: "maximum fee allowed in satoshis when sending " +
1139
                                "the payment",
1140
                },
1141
                cli.Int64Flag{
1142
                        Name: "fee_limit_percent",
1143
                        Usage: "percentage of the payment's amount used as the " +
1144
                                "maximum fee allowed when sending the payment",
1145
                },
1146
                cli.Int64Flag{
1147
                        Name: "final_cltv_delta",
1148
                        Usage: "(optional) number of blocks the last hop has " +
1149
                                "to reveal the preimage. Note that this " +
1150
                                "should not be set in the case where the " +
1151
                                "path includes a blinded path since in " +
1152
                                "that case, the receiver will already have " +
1153
                                "accounted for this value in the " +
1154
                                "blinded_cltv value",
1155
                },
1156
                cli.BoolFlag{
1157
                        Name:  "use_mc",
1158
                        Usage: "use mission control probabilities",
1159
                },
1160
                cli.StringSliceFlag{
1161
                        Name: "outgoing_chan_id",
1162
                        Usage: "(optional) the channel id of the channel " +
1163
                                "to use as the first hop. This flag can be " +
1164
                                "specified multiple times in the same command.",
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
                TimePref:            ctx.Float64(timePrefFlag.Name),
×
1279
                IgnoredPairs:        ignoredPairs,
×
1280
                BlindedPaymentPaths: blindedRoutes,
×
1281
        }
×
1282

×
1283
        outgoingChanIds := ctx.StringSlice("outgoing_chan_id")
×
NEW
1284
        req.OutgoingChanIds, err = parseChanIDs(outgoingChanIds)
×
NEW
1285
        if err != nil {
×
NEW
1286
                return fmt.Errorf("unable to decode outgoing_chan_id: %w", err)
×
UNCOV
1287
        }
×
1288

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

×
1297
                err := json.Unmarshal([]byte(routeHintsJSON), &routeHints)
×
1298
                if err != nil {
×
1299
                        return fmt.Errorf("error unmarshaling route_hints "+
×
1300
                                "json: %w", err)
×
1301
                }
×
1302

1303
                req.RouteHints = routeHints
×
1304
        }
1305

1306
        route, err := client.QueryRoutes(ctxc, req)
×
1307
        if err != nil {
×
1308
                return err
×
1309
        }
×
1310

1311
        printRespJSON(route)
×
1312

×
1313
        return nil
×
1314
}
1315

1316
func parseBlindedPaymentParameters(ctx *cli.Context) (
1317
        []*lnrpc.BlindedPaymentPath, error) {
×
1318

×
1319
        // Return nil if we don't have a blinding set, as we don't have a
×
1320
        // blinded path.
×
1321
        if !ctx.IsSet(blindingPointFlag.Name) {
×
1322
                return nil, nil
×
1323
        }
×
1324

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

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

1341
        blindingPoint, err := route.NewVertexFromStr(ctx.String(
×
1342
                blindingPointFlag.Name,
×
1343
        ))
×
1344
        if err != nil {
×
1345
                return nil, fmt.Errorf("decode blinding point: %w", err)
×
1346
        }
×
1347

1348
        blindedHops := ctx.StringSlice(blindedHopsFlag.Name)
×
1349

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

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

1378
                hop, err := route.NewVertexFromStr(parts[0])
×
1379
                if err != nil {
×
1380
                        return nil, fmt.Errorf("hop: %v node: %w", i, err)
×
1381
                }
×
1382

1383
                data, err := hex.DecodeString(parts[1])
×
1384
                if err != nil {
×
1385
                        return nil, fmt.Errorf("hop: %v data: %w", i, err)
×
1386
                }
×
1387

1388
                pmt.BlindedPath.BlindedHops[i] = &lnrpc.BlindedHop{
×
1389
                        BlindedNode:   hop[:],
×
1390
                        EncryptedData: data,
×
1391
                }
×
1392
        }
1393

1394
        return []*lnrpc.BlindedPaymentPath{
×
1395
                pmt,
×
1396
        }, nil
×
1397
}
1398

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

1426
        // Since the fee limit flags aren't required, we don't return an error
1427
        // if they're not set.
1428
        return nil, nil
×
1429
}
1430

1431
var listPaymentsCommand = cli.Command{
1432
        Name:     "listpayments",
1433
        Category: "Payments",
1434
        Usage:    "List all outgoing payments.",
1435
        Description: `
1436
        This command enables the retrieval of payments stored
1437
        in the database.
1438

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

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

1504
func listPayments(ctx *cli.Context) error {
×
1505
        ctxc := getContext()
×
1506
        client, cleanUp := getClient(ctx)
×
1507
        defer cleanUp()
×
1508

×
1509
        req := &lnrpc.ListPaymentsRequest{
×
1510
                IncludeIncomplete:  ctx.Bool("include_incomplete"),
×
1511
                IndexOffset:        uint64(ctx.Uint("index_offset")),
×
1512
                MaxPayments:        uint64(ctx.Uint("max_payments")),
×
1513
                Reversed:           !ctx.Bool("paginate_forwards"),
×
1514
                CountTotalPayments: ctx.Bool("count_total_payments"),
×
1515
                CreationDateStart:  ctx.Uint64("creation_date_start"),
×
1516
                CreationDateEnd:    ctx.Uint64("creation_date_end"),
×
1517
        }
×
1518

×
1519
        payments, err := client.ListPayments(ctxc, req)
×
1520
        if err != nil {
×
1521
                return err
×
1522
        }
×
1523

1524
        printRespJSON(payments)
×
1525
        return nil
×
1526
}
1527

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

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

1548
        Incoming and outgoing channel IDs can be provided to further filter
1549
        the events. If not provided, all events will be returned.
1550

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

1595
func forwardingHistory(ctx *cli.Context) error {
×
1596
        ctxc := getContext()
×
1597
        client, cleanUp := getClient(ctx)
×
1598
        defer cleanUp()
×
1599

×
1600
        var (
×
1601
                startTime, endTime     uint64
×
1602
                indexOffset, maxEvents uint32
×
1603
                err                    error
×
1604
        )
×
1605
        args := ctx.Args()
×
1606
        now := time.Now()
×
1607

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

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

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

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

1660
        // By default we will look up the peers' alias information unless the
1661
        // skip_peer_alias_lookup flag is specified.
1662
        lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
×
1663

×
1664
        req := &lnrpc.ForwardingHistoryRequest{
×
1665
                StartTime:       startTime,
×
1666
                EndTime:         endTime,
×
1667
                IndexOffset:     indexOffset,
×
1668
                NumMaxEvents:    maxEvents,
×
1669
                PeerAliasLookup: lookupPeerAlias,
×
1670
        }
×
NEW
1671

×
NEW
1672
        outgoingChannelIDs := ctx.StringSlice("outgoing_chan_ids")
×
NEW
1673
        req.OutgoingChanIds, err = parseChanIDs(outgoingChannelIDs)
×
NEW
1674
        if err != nil {
×
NEW
1675
                return fmt.Errorf("unable to decode outgoing_chan_ids: %w", err)
×
UNCOV
1676
        }
×
1677

NEW
1678
        incomingChannelIDs := ctx.StringSlice("incoming_chan_ids")
×
NEW
1679
        req.IncomingChanIds, err = parseChanIDs(incomingChannelIDs)
×
NEW
1680
        if err != nil {
×
NEW
1681
                return fmt.Errorf("unable to decode incoming_chan_ids: %w", err)
×
1682
        }
×
1683

1684
        resp, err := client.ForwardingHistory(ctxc, req)
×
1685
        if err != nil {
×
1686
                return err
×
1687
        }
×
1688

1689
        printRespJSON(resp)
×
1690
        return nil
×
1691
}
1692

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

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

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

×
1744
        client := routerrpc.NewRouterClient(conn)
×
1745

×
1746
        if !ctx.IsSet("hops") {
×
1747
                return errors.New("hops required")
×
1748
        }
×
1749

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

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

1770
        var (
×
1771
                payAddr []byte
×
1772
                err     error
×
1773
        )
×
1774

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

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

×
1791
        route, err := client.BuildRoute(ctxc, req)
×
1792
        if err != nil {
×
1793
                return err
×
1794
        }
×
1795

1796
        printRespJSON(route)
×
1797

×
1798
        return nil
×
1799
}
1800

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

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

1815
        If a --payment_hash is specified, that single payment is deleted,
1816
        independent of its state.
1817

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

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

1852
func deletePayments(ctx *cli.Context) error {
×
1853
        ctxc := getContext()
×
1854
        client, cleanUp := getClient(ctx)
×
1855
        defer cleanUp()
×
1856

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

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

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

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

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

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

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

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

1930
        default:
×
1931
                return fmt.Errorf("either --all or --payment_hash must be set")
×
1932
        }
1933

1934
        printJSON(resp)
×
1935

×
1936
        return nil
×
1937
}
1938

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

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

×
1979
        client := routerrpc.NewRouterClient(conn)
×
1980

×
1981
        req := &routerrpc.RouteFeeRequest{}
×
1982

×
1983
        switch {
×
1984
        case ctx.IsSet("dest") && ctx.IsSet("pay_req"):
×
1985
                return fmt.Errorf("either dest or pay_req can be set")
×
1986

1987
        case ctx.IsSet("dest") && !ctx.IsSet("amt"):
×
1988
                return fmt.Errorf("amt is required when dest is set")
×
1989

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

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

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

2006
                req.Dest = dest
×
2007
                req.AmtSat = amtSat
×
2008

2009
        case ctx.IsSet("pay_req"):
×
2010
                req.PaymentRequest = StripPrefix(ctx.String("pay_req"))
×
2011
                req.Timeout = uint32(ctx.Duration("timeout").Seconds())
×
2012

2013
        default:
×
2014
                return fmt.Errorf("fee estimation arguments missing")
×
2015
        }
2016

2017
        resp, err := client.EstimateRouteFee(ctxc, req)
×
2018
        if err != nil {
×
2019
                return err
×
2020
        }
×
2021

2022
        printRespJSON(resp)
×
2023

×
2024
        return nil
×
2025
}
2026

2027
// ESC is the ASCII code for escape character.
2028
const ESC = 27
2029

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

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

2039
// ordinalNumber returns the ordinal number as a string of a number.
2040
func ordinalNumber(num uint32) string {
×
2041
        switch num {
×
2042
        case 1:
×
2043
                return "1st"
×
2044
        case 2:
×
2045
                return "2nd"
×
2046
        case 3:
×
2047
                return "3rd"
×
2048
        default:
×
2049
                return fmt.Sprintf("%dth", num)
×
2050
        }
2051
}
2052

2053
// parseChanIDs parses a slice of strings containing short channel IDs into a
2054
// slice of uint64 values.
2055
func parseChanIDs(idStrings []string) ([]uint64, error) {
4✔
2056
        // Return early if no chan IDs are passed.
4✔
2057
        if len(idStrings) == 0 {
5✔
2058
                return nil, nil
1✔
2059
        }
1✔
2060

2061
        chanIDs := make([]uint64, len(idStrings))
3✔
2062
        for i, idStr := range idStrings {
7✔
2063
                scid, err := strconv.ParseUint(idStr, 10, 64)
4✔
2064
                if err != nil {
6✔
2065
                        return nil, err
2✔
2066
                }
2✔
2067

2068
                chanIDs[i] = scid
2✔
2069
        }
2070

2071
        return chanIDs, nil
1✔
2072
}
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