github.com/decred/dcrlnd@v0.7.6/cmd/dcrlncli/cmd_payments.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/rand" 7 "encoding/hex" 8 "errors" 9 "fmt" 10 "io/ioutil" 11 "os" 12 "runtime" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/decred/dcrd/dcrutil/v4" 18 "github.com/decred/dcrlnd/chainreg" 19 "github.com/decred/dcrlnd/lnrpc" 20 "github.com/decred/dcrlnd/lnrpc/routerrpc" 21 "github.com/decred/dcrlnd/lntypes" 22 "github.com/decred/dcrlnd/lnwallet" 23 "github.com/decred/dcrlnd/lnwire" 24 "github.com/decred/dcrlnd/record" 25 "github.com/decred/dcrlnd/routing/route" 26 "github.com/jedib0t/go-pretty/table" 27 "github.com/jedib0t/go-pretty/text" 28 "github.com/matheusd/protobuf-hex-display/jsonpb" 29 "github.com/urfave/cli" 30 ) 31 32 const ( 33 // paymentTimeout is the default timeout for the payment loop in lnd. 34 // No new attempts will be started after the timeout. 35 paymentTimeout = time.Second * 60 36 ) 37 38 var ( 39 cltvLimitFlag = cli.UintFlag{ 40 Name: "cltv_limit", 41 Usage: "the maximum time lock that may be used for " + 42 "this payment", 43 } 44 45 lastHopFlag = cli.StringFlag{ 46 Name: "last_hop", 47 Usage: "pubkey of the last hop (penultimate node in the path) " + 48 "to route through for this payment", 49 } 50 51 dataFlag = cli.StringFlag{ 52 Name: "data", 53 Usage: "attach custom data to the payment. The required " + 54 "format is: <record_id>=<hex_value>,<record_id>=" + 55 "<hex_value>,.. For example: --data 3438382=0a21ff. " + 56 "Custom record ids start from 65536.", 57 } 58 59 inflightUpdatesFlag = cli.BoolFlag{ 60 Name: "inflight_updates", 61 Usage: "if set, intermediate payment state updates will be " + 62 "displayed. Only valid in combination with --json.", 63 } 64 65 maxPartsFlag = cli.UintFlag{ 66 Name: "max_parts", 67 Usage: "the maximum number of partial payments that may be " + 68 "used", 69 Value: routerrpc.DefaultMaxParts, 70 } 71 72 jsonFlag = cli.BoolFlag{ 73 Name: "json", 74 Usage: "if set, payment updates are printed as json " + 75 "messages. Set by default on Windows because table " + 76 "formatting is unsupported.", 77 } 78 79 maxShardSizeAtomsFlag = cli.UintFlag{ 80 Name: "max_shard_size_atoms", 81 Usage: "the largest payment split that should be attempted if " + 82 "payment splitting is required to attempt a payment, " + 83 "specified in atoms", 84 } 85 86 maxShardSizeMatomsFlag = cli.UintFlag{ 87 Name: "max_shard_size_matoms", 88 Usage: "the largest payment split that should be attempted if " + 89 "payment splitting is required to attempt a payment, " + 90 "specified in milli-atoms", 91 } 92 93 ampFlag = cli.BoolFlag{ 94 Name: "amp", 95 Usage: "if set to true, then AMP will be used to complete the " + 96 "payment", 97 } 98 ) 99 100 // paymentFlags returns common flags for sendpayment and payinvoice. 101 func paymentFlags() []cli.Flag { 102 return []cli.Flag{ 103 cli.StringFlag{ 104 Name: "pay_req", 105 Usage: "A zpay32 encoded payment request to fulfill", 106 }, 107 cli.Int64Flag{ 108 Name: "fee_limit", 109 Usage: "Maximum fee allowed in atoms when " + 110 "sending the payment", 111 }, 112 cli.Int64Flag{ 113 Name: "fee_limit_percent", 114 Usage: "Percentage of the payment's amount used as " + 115 "the maximum fee allowed when sending the " + 116 "payment", 117 }, 118 cli.DurationFlag{ 119 Name: "timeout", 120 Usage: "the maximum amount of time we should spend " + 121 "trying to fulfill the payment, failing " + 122 "after the timeout has elapsed", 123 Value: paymentTimeout, 124 }, 125 cltvLimitFlag, 126 lastHopFlag, 127 cli.Uint64Flag{ 128 Name: "outgoing_chan_id", 129 Usage: "Short channel id of the outgoing channel to " + 130 "use for the first hop of the payment", 131 Value: 0, 132 }, 133 cli.BoolFlag{ 134 Name: "force, f", 135 Usage: "Will skip payment request confirmation", 136 }, 137 cli.BoolFlag{ 138 Name: "allow_self_payment", 139 Usage: "Allow sending a circular payment to self", 140 }, 141 dataFlag, inflightUpdatesFlag, maxPartsFlag, jsonFlag, 142 maxShardSizeAtomsFlag, maxShardSizeMatomsFlag, ampFlag, 143 } 144 } 145 146 var sendPaymentCommand = cli.Command{ 147 Name: "sendpayment", 148 Category: "Payments", 149 Usage: "Send a payment over lightning.", 150 Description: ` 151 Send a payment over Lightning. One can either specify the full 152 parameters of the payment, or just use a payment request which encodes 153 all the payment details. 154 155 If payment isn't manually specified, then only a payment request needs 156 to be passed using the '--pay_req' argument. 157 158 If the payment *is* manually specified, then the following arguments 159 need to be specified in order to complete the payment: 160 161 For invoice with keysend, 162 --dest=N --amt=A --final_cltv_delta=T --keysend 163 For invoice without payment address: 164 --dest=N --amt=A --payment_hash=H --final_cltv_delta=T 165 For invoice with payment address: 166 --dest=N --amt=A --payment_hash=H --final_cltv_delta=T --pay_addr=H 167 `, 168 ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | --pay_req=[payment request]", 169 Flags: append(paymentFlags(), 170 cli.StringFlag{ 171 Name: "dest, d", 172 Usage: "The compressed identity pubkey of the " + 173 "payment recipient", 174 }, 175 cli.Int64Flag{ 176 Name: "amt, a", 177 Usage: "Number of atoms to send", 178 }, 179 cli.StringFlag{ 180 Name: "payment_hash, r", 181 Usage: "The hash to use within the payment's HTLC", 182 }, 183 cli.Int64Flag{ 184 Name: "final_cltv_delta", 185 Usage: "The number of blocks the last hop has to reveal the preimage", 186 }, 187 cli.StringFlag{ 188 Name: "pay_addr", 189 Usage: "the payment address of the generated invoice", 190 }, 191 cli.BoolFlag{ 192 Name: "keysend", 193 Usage: "Will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]", 194 }, 195 ), 196 Action: sendPayment, 197 } 198 199 // retrieveFeeLimit retrieves the fee limit based on the different fee limit 200 // flags passed. It always returns a value and doesn't rely on lnd applying a 201 // default. 202 func retrieveFeeLimit(ctx *cli.Context, amt int64) (int64, error) { 203 switch { 204 205 case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"): 206 return 0, fmt.Errorf("either fee_limit or fee_limit_percent " + 207 "can be set, but not both") 208 209 case ctx.IsSet("fee_limit"): 210 return ctx.Int64("fee_limit"), nil 211 212 case ctx.IsSet("fee_limit_percent"): 213 // Round up the fee limit to prevent hitting zero on small 214 // amounts. 215 feeLimitRoundedUp := 216 (amt*ctx.Int64("fee_limit_percent") + 99) / 100 217 218 return feeLimitRoundedUp, nil 219 } 220 221 // If no fee limit is set, use a default value based on the amount. 222 amtMAtoms := lnwire.NewMAtomsFromAtoms(dcrutil.Amount(amt)) 223 limitMsat := lnwallet.DefaultRoutingFeeLimitForAmount(amtMAtoms) 224 return int64(limitMsat.ToAtoms()), nil 225 } 226 227 func confirmPayReq(resp *lnrpc.PayReq, amt, feeLimit int64) error { 228 fmt.Printf("Payment hash: %v\n", resp.GetPaymentHash()) 229 fmt.Printf("Description: %v\n", resp.GetDescription()) 230 fmt.Printf("Amount (in atoms): %v\n", amt) 231 fmt.Printf("Fee limit (in atoms): %v\n", feeLimit) 232 fmt.Printf("Destination: %v\n", resp.GetDestination()) 233 234 confirm := promptForConfirmation("Confirm payment (yes/no): ") 235 if !confirm { 236 return fmt.Errorf("payment not confirmed") 237 } 238 239 return nil 240 } 241 242 func parsePayAddr(ctx *cli.Context) ([]byte, error) { 243 var ( 244 payAddr []byte 245 err error 246 ) 247 switch { 248 case ctx.IsSet("pay_addr"): 249 payAddr, err = hex.DecodeString(ctx.String("pay_addr")) 250 251 case ctx.Args().Present(): 252 payAddr, err = hex.DecodeString(ctx.Args().First()) 253 } 254 255 if err != nil { 256 return nil, err 257 } 258 259 // payAddr may be not required if it's a legacy invoice. 260 if len(payAddr) != 0 && len(payAddr) != 32 { 261 return nil, fmt.Errorf("payment addr must be exactly 32 "+ 262 "bytes, is instead %v", len(payAddr)) 263 } 264 265 return payAddr, nil 266 } 267 268 func sendPayment(ctx *cli.Context) error { 269 // Show command help if no arguments provided 270 if ctx.NArg() == 0 && ctx.NumFlags() == 0 { 271 cli.ShowCommandHelp(ctx, "sendpayment") 272 return nil 273 } 274 275 args := ctx.Args() 276 277 // If a payment request was provided, we can exit early since all of the 278 // details of the payment are encoded within the request. 279 if ctx.IsSet("pay_req") { 280 req := &routerrpc.SendPaymentRequest{ 281 PaymentRequest: ctx.String("pay_req"), 282 Amt: ctx.Int64("amt"), 283 DestCustomRecords: make(map[uint64][]byte), 284 } 285 286 // We'll attempt to parse a payment address as well, given that 287 // if the user is using an AMP invoice, then they may be trying 288 // to specify that value manually. 289 payAddr, err := parsePayAddr(ctx) 290 if err != nil { 291 return err 292 } 293 294 req.PaymentAddr = payAddr 295 296 return sendPaymentRequest(ctx, req) 297 } 298 299 var ( 300 destNode []byte 301 amount int64 302 err error 303 ) 304 305 switch { 306 case ctx.IsSet("dest"): 307 destNode, err = hex.DecodeString(ctx.String("dest")) 308 case args.Present(): 309 destNode, err = hex.DecodeString(args.First()) 310 args = args.Tail() 311 default: 312 return fmt.Errorf("destination txid argument missing") 313 } 314 if err != nil { 315 return err 316 } 317 318 if len(destNode) != 33 { 319 return fmt.Errorf("dest node pubkey must be exactly 33 bytes, is "+ 320 "instead: %v", len(destNode)) 321 } 322 323 if ctx.IsSet("amt") { 324 amount = ctx.Int64("amt") 325 } else if args.Present() { 326 amount, err = strconv.ParseInt(args.First(), 10, 64) 327 args = args.Tail() 328 if err != nil { 329 return fmt.Errorf("unable to decode payment amount: %v", err) 330 } 331 } 332 333 if ctx.Bool("ignore_max_outbound_amt") { 334 return fmt.Errorf("plz re-implement") 335 } 336 337 req := &routerrpc.SendPaymentRequest{ 338 Dest: destNode, 339 Amt: amount, 340 DestCustomRecords: make(map[uint64][]byte), 341 Amp: ctx.Bool(ampFlag.Name), 342 } 343 344 var rHash []byte 345 346 switch { 347 case ctx.Bool("keysend") && ctx.Bool(ampFlag.Name): 348 return errors.New("either keysend or amp may be set, but not both") 349 350 case ctx.Bool("keysend"): 351 if ctx.IsSet("payment_hash") { 352 return errors.New("cannot set payment hash when using " + 353 "keysend") 354 } 355 var preimage lntypes.Preimage 356 if _, err := rand.Read(preimage[:]); err != nil { 357 return err 358 } 359 360 // Set the preimage. If the user supplied a preimage with the 361 // data flag, the preimage that is set here will be overwritten 362 // later. 363 req.DestCustomRecords[record.KeySendType] = preimage[:] 364 365 hash := preimage.Hash() 366 rHash = hash[:] 367 case !ctx.Bool(ampFlag.Name): 368 switch { 369 case ctx.IsSet("payment_hash"): 370 rHash, err = hex.DecodeString(ctx.String("payment_hash")) 371 case args.Present(): 372 rHash, err = hex.DecodeString(args.First()) 373 args = args.Tail() 374 default: 375 return fmt.Errorf("payment hash argument missing") 376 } 377 } 378 379 if err != nil { 380 return err 381 } 382 if !req.Amp && len(rHash) != 32 { 383 return fmt.Errorf("payment hash must be exactly 32 "+ 384 "bytes, is instead %v", len(rHash)) 385 } 386 req.PaymentHash = rHash 387 388 switch { 389 case ctx.IsSet("final_cltv_delta"): 390 req.FinalCltvDelta = int32(ctx.Int64("final_cltv_delta")) 391 case args.Present(): 392 delta, err := strconv.ParseInt(args.First(), 10, 64) 393 if err != nil { 394 return err 395 } 396 args = args.Tail() 397 req.FinalCltvDelta = int32(delta) 398 } 399 400 payAddr, err := parsePayAddr(ctx) 401 if err != nil { 402 return err 403 } 404 405 req.PaymentAddr = payAddr 406 407 return sendPaymentRequest(ctx, req) 408 } 409 410 func sendPaymentRequest(ctx *cli.Context, 411 req *routerrpc.SendPaymentRequest) error { 412 ctxc := getContext() 413 414 conn := getClientConn(ctx, false) 415 defer conn.Close() 416 417 client := lnrpc.NewLightningClient(conn) 418 routerClient := routerrpc.NewRouterClient(conn) 419 420 outChan := ctx.Uint64("outgoing_chan_id") 421 if outChan != 0 { 422 req.OutgoingChanIds = []uint64{outChan} 423 } 424 if ctx.IsSet(lastHopFlag.Name) { 425 lastHop, err := route.NewVertexFromStr( 426 ctx.String(lastHopFlag.Name), 427 ) 428 if err != nil { 429 return err 430 } 431 req.LastHopPubkey = lastHop[:] 432 } 433 434 req.CltvLimit = int32(ctx.Int(cltvLimitFlag.Name)) 435 436 pmtTimeout := ctx.Duration("timeout") 437 if pmtTimeout <= 0 { 438 return errors.New("payment timeout must be greater than zero") 439 } 440 req.TimeoutSeconds = int32(pmtTimeout.Seconds()) 441 442 req.AllowSelfPayment = ctx.Bool("allow_self_payment") 443 444 req.MaxParts = uint32(ctx.Uint(maxPartsFlag.Name)) 445 446 switch { 447 // If the max shard size is specified, then it should either be in sat 448 // or msat, but not both. 449 case ctx.Uint64(maxShardSizeMatomsFlag.Name) != 0 && 450 ctx.Uint64(maxShardSizeAtomsFlag.Name) != 0: 451 return fmt.Errorf("only --max_split_size_matoms or " + 452 "--max_split_size_atoms should be set, but not both") 453 454 case ctx.Uint64(maxShardSizeMatomsFlag.Name) != 0: 455 req.MaxShardSizeMatoms = ctx.Uint64(maxShardSizeMatomsFlag.Name) 456 457 case ctx.Uint64(maxShardSizeAtomsFlag.Name) != 0: 458 req.MaxShardSizeMatoms = uint64(lnwire.NewMAtomsFromAtoms( 459 dcrutil.Amount(ctx.Uint64(maxShardSizeAtomsFlag.Name)), 460 )) 461 } 462 463 // Parse custom data records. 464 data := ctx.String(dataFlag.Name) 465 if data != "" { 466 records := strings.Split(data, ",") 467 for _, r := range records { 468 kv := strings.Split(r, "=") 469 if len(kv) != 2 { 470 return errors.New("invalid data format: " + 471 "multiple equal signs in record") 472 } 473 474 recordID, err := strconv.ParseUint(kv[0], 10, 64) 475 if err != nil { 476 return fmt.Errorf("invalid data format: %v", 477 err) 478 } 479 480 hexValue, err := hex.DecodeString(kv[1]) 481 if err != nil { 482 return fmt.Errorf("invalid data format: %v", 483 err) 484 } 485 486 req.DestCustomRecords[recordID] = hexValue 487 } 488 } 489 490 var feeLimit int64 491 if req.PaymentRequest != "" { 492 // Decode payment request to find out the amount. 493 decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest} 494 decodeResp, err := client.DecodePayReq(ctxc, decodeReq) 495 if err != nil { 496 return err 497 } 498 499 // If amount is present in the request, override the request 500 // amount. 501 amt := req.Amt 502 invoiceAmt := decodeResp.GetNumAtoms() 503 if invoiceAmt != 0 { 504 amt = invoiceAmt 505 } 506 507 // Calculate fee limit based on the determined amount. 508 feeLimit, err = retrieveFeeLimit(ctx, amt) 509 if err != nil { 510 return err 511 } 512 513 // Ask for confirmation of amount and fee limit if payment is 514 // forced. 515 if !ctx.Bool("force") { 516 err := confirmPayReq(decodeResp, amt, feeLimit) 517 if err != nil { 518 return err 519 } 520 } 521 } else { 522 var err error 523 feeLimit, err = retrieveFeeLimit(ctx, req.Amt) 524 if err != nil { 525 return err 526 } 527 } 528 529 req.FeeLimitAtoms = feeLimit 530 531 // Always print in-flight updates for the table output. 532 printJSON := ctx.Bool(jsonFlag.Name) 533 req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON 534 535 stream, err := routerClient.SendPaymentV2(ctxc, req) 536 if err != nil { 537 return err 538 } 539 540 finalState, err := printLivePayment( 541 ctxc, stream, client, printJSON, 542 ) 543 if err != nil { 544 return err 545 } 546 547 // If we get a payment error back, we pass an error up 548 // to main which eventually calls fatal() and returns 549 // with a non-zero exit code. 550 if finalState.Status != lnrpc.Payment_SUCCEEDED { 551 return errors.New(finalState.Status.String()) 552 } 553 554 return nil 555 } 556 557 var trackPaymentCommand = cli.Command{ 558 Name: "trackpayment", 559 Category: "Payments", 560 Usage: "Track progress of an existing payment.", 561 Description: ` 562 Pick up monitoring the progression of a previously initiated payment 563 specified by the hash argument. 564 `, 565 ArgsUsage: "hash", 566 Flags: []cli.Flag{ 567 jsonFlag, 568 }, 569 Action: actionDecorator(trackPayment), 570 } 571 572 func trackPayment(ctx *cli.Context) error { 573 ctxc := getContext() 574 args := ctx.Args() 575 576 conn := getClientConn(ctx, false) 577 defer conn.Close() 578 579 routerClient := routerrpc.NewRouterClient(conn) 580 581 if !args.Present() { 582 return fmt.Errorf("hash argument missing") 583 } 584 585 hash, err := hex.DecodeString(args.First()) 586 if err != nil { 587 return err 588 } 589 590 req := &routerrpc.TrackPaymentRequest{ 591 PaymentHash: hash, 592 } 593 594 stream, err := routerClient.TrackPaymentV2(ctxc, req) 595 if err != nil { 596 return err 597 } 598 599 client := lnrpc.NewLightningClient(conn) 600 _, err = printLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name)) 601 return err 602 } 603 604 // printLivePayment receives payment updates from the given stream and either 605 // outputs them as json or as a more user-friendly formatted table. The table 606 // option uses terminal control codes to rewrite the output. This call 607 // terminates when the payment reaches a final state. 608 func printLivePayment(ctxc context.Context, 609 stream routerrpc.Router_TrackPaymentV2Client, 610 client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) { 611 612 // Terminal escape codes aren't supported on Windows, fall back to json. 613 if !json && runtime.GOOS == "windows" { 614 json = true 615 } 616 617 aliases := newAliasCache(client) 618 619 first := true 620 var lastLineCount int 621 for { 622 payment, err := stream.Recv() 623 if err != nil { 624 return nil, err 625 } 626 627 if json { 628 // Delimit json messages by newlines (inspired by 629 // grpc over rest chunking). 630 if first { 631 first = false 632 } else { 633 fmt.Println() 634 } 635 636 // Write raw json to stdout. 637 printRespJSON(payment) 638 } else { 639 table := formatPayment(ctxc, payment, aliases) 640 641 // Clear all previously written lines and print the 642 // updated table. 643 clearLines(lastLineCount) 644 fmt.Print(table) 645 646 // Store the number of lines written for the next update 647 // pass. 648 lastLineCount = 0 649 for _, b := range table { 650 if b == '\n' { 651 lastLineCount++ 652 } 653 } 654 } 655 656 // Terminate loop if payments state is final. 657 if payment.Status != lnrpc.Payment_IN_FLIGHT { 658 return payment, nil 659 } 660 } 661 } 662 663 // aliasCache allows cached retrieval of node aliases. 664 type aliasCache struct { 665 cache map[string]string 666 client lnrpc.LightningClient 667 } 668 669 func newAliasCache(client lnrpc.LightningClient) *aliasCache { 670 return &aliasCache{ 671 client: client, 672 cache: make(map[string]string), 673 } 674 } 675 676 // get returns a node alias either from cache or freshly requested from lnd. 677 func (a *aliasCache) get(ctxc context.Context, pubkey string) string { 678 alias, ok := a.cache[pubkey] 679 if ok { 680 return alias 681 } 682 683 // Request node info. 684 resp, err := a.client.GetNodeInfo( 685 ctxc, 686 &lnrpc.NodeInfoRequest{ 687 PubKey: pubkey, 688 }, 689 ) 690 if err != nil { 691 // If no info is available, use the 692 // pubkey as identifier. 693 alias = pubkey[:6] 694 } else { 695 alias = resp.Node.Alias 696 } 697 a.cache[pubkey] = alias 698 699 return alias 700 } 701 702 // formatMAtoms formats msat amounts as fractional sats. 703 func formatMAtoms(amt int64) string { 704 return strconv.FormatFloat(float64(amt)/1000.0, 'f', -1, 64) 705 } 706 707 // formatPayment formats the payment state as an ascii table. 708 func formatPayment(ctxc context.Context, payment *lnrpc.Payment, 709 aliases *aliasCache) string { 710 t := table.NewWriter() 711 712 // Build table header. 713 t.AppendHeader(table.Row{ 714 "HTLC_STATE", "ATTEMPT_TIME", "RESOLVE_TIME", "RECEIVER_AMT", 715 "FEE", "TIMELOCK", "CHAN_OUT", "ROUTE", 716 }) 717 t.SetColumnConfigs([]table.ColumnConfig{ 718 {Name: "ATTEMPT_TIME", Align: text.AlignRight}, 719 {Name: "RESOLVE_TIME", Align: text.AlignRight}, 720 {Name: "CHAN_OUT", Align: text.AlignLeft, 721 AlignHeader: text.AlignLeft}, 722 }) 723 724 // Add all htlcs as rows. 725 createTime := time.Unix(0, payment.CreationTimeNs) 726 var totalPaid, totalFees int64 727 for _, htlc := range payment.Htlcs { 728 formatTime := func(timeNs int64) string { 729 if timeNs == 0 { 730 return "-" 731 } 732 resolveTime := time.Unix(0, timeNs) 733 resolveTimeDiff := resolveTime.Sub(createTime) 734 resolveTimeMs := resolveTimeDiff / time.Millisecond 735 return fmt.Sprintf( 736 "%.3f", float64(resolveTimeMs)/1000.0, 737 ) 738 } 739 740 attemptTime := formatTime(htlc.AttemptTimeNs) 741 resolveTime := formatTime(htlc.ResolveTimeNs) 742 743 route := htlc.Route 744 lastHop := route.Hops[len(route.Hops)-1] 745 746 hops := []string{} 747 for _, h := range route.Hops { 748 alias := aliases.get(ctxc, h.PubKey) 749 hops = append(hops, alias) 750 } 751 752 state := htlc.Status.String() 753 if htlc.Failure != nil { 754 state = fmt.Sprintf( 755 "%v @ %v", 756 htlc.Failure.Code, 757 htlc.Failure.FailureSourceIndex, 758 ) 759 } 760 761 t.AppendRow([]interface{}{ 762 state, attemptTime, resolveTime, 763 formatMAtoms(lastHop.AmtToForwardMAtoms), 764 formatMAtoms(route.TotalFeesMAtoms), 765 route.TotalTimeLock, route.Hops[0].ChanId, 766 strings.Join(hops, "->")}, 767 ) 768 769 if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED { 770 totalPaid += lastHop.AmtToForwardMAtoms 771 totalFees += route.TotalFeesMAtoms 772 } 773 } 774 775 // Render table. 776 b := &bytes.Buffer{} 777 t.SetOutputMirror(b) 778 t.Render() 779 780 // Add additional payment-level data. 781 fmt.Fprintf(b, "Amount + fee: %v + %v atoms\n", 782 formatMAtoms(totalPaid), formatMAtoms(totalFees)) 783 fmt.Fprintf(b, "Payment hash: %v\n", payment.PaymentHash) 784 fmt.Fprintf(b, "Payment status: %v", payment.Status) 785 switch payment.Status { 786 case lnrpc.Payment_SUCCEEDED: 787 fmt.Fprintf(b, ", preimage: %v", payment.PaymentPreimage) 788 case lnrpc.Payment_FAILED: 789 fmt.Fprintf(b, ", reason: %v", payment.FailureReason) 790 } 791 fmt.Fprintf(b, "\n") 792 793 return b.String() 794 } 795 796 var payInvoiceCommand = cli.Command{ 797 Name: "payinvoice", 798 Category: "Payments", 799 Usage: "Pay an invoice over lightning.", 800 ArgsUsage: "pay_req", 801 Flags: append(paymentFlags(), 802 cli.Int64Flag{ 803 Name: "amt", 804 Usage: "Number of atoms to fulfill the " + 805 "invoice (optional)", 806 }, 807 ), 808 Action: actionDecorator(payInvoice), 809 } 810 811 func payInvoice(ctx *cli.Context) error { 812 args := ctx.Args() 813 814 var payReq string 815 switch { 816 case ctx.IsSet("pay_req"): 817 payReq = ctx.String("pay_req") 818 case args.Present(): 819 payReq = args.First() 820 default: 821 return fmt.Errorf("pay_req argument missing") 822 } 823 824 req := &routerrpc.SendPaymentRequest{ 825 PaymentRequest: payReq, 826 Amt: ctx.Int64("amt"), 827 DestCustomRecords: make(map[uint64][]byte), 828 } 829 830 return sendPaymentRequest(ctx, req) 831 } 832 833 var sendToRouteCommand = cli.Command{ 834 Name: "sendtoroute", 835 Category: "Payments", 836 Usage: "Send a payment over a predefined route.", 837 Description: ` 838 Send a payment over Lightning using a specific route. One must specify 839 the route to attempt and the payment hash. This command can even 840 be chained with the response to 'queryroutes' or 'buildroute'. This command 841 can be used to implement channel rebalancing by crafting a self-route, 842 or even atomic swaps using a self-route that crosses multiple chains. 843 844 There are three ways to specify a route: 845 * using the '--routes' parameter to manually specify a JSON encoded 846 route in the format of the return value of queryroutes or 847 buildroute: 848 'dcrlncli sendtoroute --payment_hash=<pay_hash> --routes=<route>' 849 850 * passing the route as a positional argument: 851 ('dcrlncli sendtoroute --payment_hash=pay_hash <route>' 852 853 * or reading in the route from stdin, which can allow chaining the 854 response from 'queryroutes' or 'buildroute', or even read in a file 855 with a pre-computed route: 856 'dcrlncli queryroutes --args.. | dcrlncli sendtoroute --payment_hash= -' 857 858 notice the '-' at the end, which signals that dcrlncli should read 859 the route in from stdin 860 `, 861 Flags: []cli.Flag{ 862 cli.StringFlag{ 863 Name: "payment_hash, pay_hash", 864 Usage: "The hash to use within the payment's HTLC", 865 }, 866 cli.StringFlag{ 867 Name: "routes, r", 868 Usage: "A json array string in the format of the response " + 869 "of 'queryroutes' that denotes which routes to use", 870 }, 871 }, 872 Action: sendToRoute, 873 } 874 875 func sendToRoute(ctx *cli.Context) error { 876 // Show command help if no arguments provided. 877 if ctx.NArg() == 0 && ctx.NumFlags() == 0 { 878 cli.ShowCommandHelp(ctx, "sendtoroute") 879 return nil 880 } 881 882 args := ctx.Args() 883 884 var ( 885 rHash []byte 886 err error 887 ) 888 switch { 889 case ctx.IsSet("payment_hash"): 890 rHash, err = hex.DecodeString(ctx.String("payment_hash")) 891 case args.Present(): 892 rHash, err = hex.DecodeString(args.First()) 893 894 args = args.Tail() 895 default: 896 return fmt.Errorf("payment hash argument missing") 897 } 898 899 if err != nil { 900 return err 901 } 902 903 if len(rHash) != 32 { 904 return fmt.Errorf("payment hash must be exactly 32 "+ 905 "bytes, is instead %d", len(rHash)) 906 } 907 908 var jsonRoutes string 909 switch { 910 // The user is specifying the routes explicitly via the key word 911 // argument. 912 case ctx.IsSet("routes"): 913 jsonRoutes = ctx.String("routes") 914 915 // The user is specifying the routes as a positional argument. 916 case args.Present() && args.First() != "-": 917 jsonRoutes = args.First() 918 919 // The user is signalling that we should read stdin in order to parse 920 // the set of target routes. 921 case args.Present() && args.First() == "-": 922 b, err := ioutil.ReadAll(os.Stdin) 923 if err != nil { 924 return err 925 } 926 if len(b) == 0 { 927 return fmt.Errorf("queryroutes output is empty") 928 } 929 930 jsonRoutes = string(b) 931 } 932 933 // Try to parse the provided json both in the legacy QueryRoutes format 934 // that contains a list of routes and the single route BuildRoute 935 // format. 936 var route *lnrpc.Route 937 routes := &lnrpc.QueryRoutesResponse{} 938 err = jsonpb.UnmarshalString(jsonRoutes, routes) 939 if err == nil { 940 if len(routes.Routes) == 0 { 941 return fmt.Errorf("no routes provided") 942 } 943 944 if len(routes.Routes) != 1 { 945 return fmt.Errorf("expected a single route, but got %v", 946 len(routes.Routes)) 947 } 948 949 route = routes.Routes[0] 950 } else { 951 routes := &routerrpc.BuildRouteResponse{} 952 err = jsonpb.UnmarshalString(jsonRoutes, routes) 953 if err != nil { 954 return fmt.Errorf("unable to unmarshal json string "+ 955 "from incoming array of routes: %v", err) 956 } 957 958 route = routes.Route 959 } 960 961 req := &routerrpc.SendToRouteRequest{ 962 PaymentHash: rHash, 963 Route: route, 964 } 965 966 return sendToRouteRequest(ctx, req) 967 } 968 969 func sendToRouteRequest(ctx *cli.Context, req *routerrpc.SendToRouteRequest) error { 970 ctxc := getContext() 971 conn := getClientConn(ctx, false) 972 defer conn.Close() 973 974 client := routerrpc.NewRouterClient(conn) 975 976 resp, err := client.SendToRouteV2(ctxc, req) 977 if err != nil { 978 return err 979 } 980 981 printRespJSON(resp) 982 983 return nil 984 } 985 986 var queryRoutesCommand = cli.Command{ 987 Name: "queryroutes", 988 Category: "Payments", 989 Usage: "Query a route to a destination.", 990 Description: "Queries the channel router for a potential path to the destination that has sufficient flow for the amount including fees", 991 ArgsUsage: "dest amt", 992 Flags: []cli.Flag{ 993 cli.StringFlag{ 994 Name: "dest", 995 Usage: "The 33-byte hex-encoded public key for the payment " + 996 "destination", 997 }, 998 cli.Int64Flag{ 999 Name: "amt", 1000 Usage: "The amount to send expressed in atoms", 1001 }, 1002 cli.Int64Flag{ 1003 Name: "fee_limit", 1004 Usage: "Maximum fee allowed in atoms when sending " + 1005 "the payment", 1006 }, 1007 cli.Int64Flag{ 1008 Name: "fee_limit_percent", 1009 Usage: "Percentage of the payment's amount used as the " + 1010 "maximum fee allowed when sending the payment", 1011 }, 1012 cli.Int64Flag{ 1013 Name: "final_cltv_delta", 1014 Usage: "Number of blocks the last hop has to reveal " + 1015 "the preimage (optional)", 1016 }, 1017 cli.BoolFlag{ 1018 Name: "use_mc", 1019 Usage: "Use mission control probabilities", 1020 }, 1021 cli.Uint64Flag{ 1022 Name: "outgoing_chanid", 1023 Usage: "(optional) the channel id of the channel " + 1024 "that must be taken to the first hop", 1025 }, 1026 cltvLimitFlag, 1027 }, 1028 Action: actionDecorator(queryRoutes), 1029 } 1030 1031 func queryRoutes(ctx *cli.Context) error { 1032 ctxc := getContext() 1033 client, cleanUp := getClient(ctx) 1034 defer cleanUp() 1035 1036 var ( 1037 dest string 1038 amt int64 1039 err error 1040 ) 1041 1042 args := ctx.Args() 1043 1044 switch { 1045 case ctx.IsSet("dest"): 1046 dest = ctx.String("dest") 1047 case args.Present(): 1048 dest = args.First() 1049 args = args.Tail() 1050 default: 1051 return fmt.Errorf("dest argument missing") 1052 } 1053 1054 switch { 1055 case ctx.IsSet("amt"): 1056 amt = ctx.Int64("amt") 1057 case args.Present(): 1058 amt, err = strconv.ParseInt(args.First(), 10, 64) 1059 if err != nil { 1060 return fmt.Errorf("unable to decode amt argument: %v", err) 1061 } 1062 default: 1063 return fmt.Errorf("amt argument missing") 1064 } 1065 1066 feeLimit, err := retrieveFeeLimitLegacy(ctx) 1067 if err != nil { 1068 return err 1069 } 1070 1071 req := &lnrpc.QueryRoutesRequest{ 1072 PubKey: dest, 1073 Amt: amt, 1074 FeeLimit: feeLimit, 1075 FinalCltvDelta: int32(ctx.Int("final_cltv_delta")), 1076 UseMissionControl: ctx.Bool("use_mc"), 1077 CltvLimit: uint32(ctx.Uint64(cltvLimitFlag.Name)), 1078 OutgoingChanId: ctx.Uint64("outgoing_chanid"), 1079 } 1080 1081 route, err := client.QueryRoutes(ctxc, req) 1082 if err != nil { 1083 return err 1084 } 1085 1086 printRespJSON(route) 1087 return nil 1088 } 1089 1090 // retrieveFeeLimitLegacy retrieves the fee limit based on the different fee 1091 // limit flags passed. This function will eventually disappear in favor of 1092 // retrieveFeeLimit and the new payment rpc. 1093 func retrieveFeeLimitLegacy(ctx *cli.Context) (*lnrpc.FeeLimit, error) { 1094 switch { 1095 case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"): 1096 return nil, fmt.Errorf("either fee_limit or fee_limit_percent " + 1097 "can be set, but not both") 1098 case ctx.IsSet("fee_limit"): 1099 return &lnrpc.FeeLimit{ 1100 Limit: &lnrpc.FeeLimit_Fixed{ 1101 Fixed: ctx.Int64("fee_limit"), 1102 }, 1103 }, nil 1104 case ctx.IsSet("fee_limit_percent"): 1105 feeLimitPercent := ctx.Int64("fee_limit_percent") 1106 if feeLimitPercent < 0 { 1107 return nil, errors.New("negative fee limit percentage " + 1108 "provided") 1109 } 1110 return &lnrpc.FeeLimit{ 1111 Limit: &lnrpc.FeeLimit_Percent{ 1112 Percent: feeLimitPercent, 1113 }, 1114 }, nil 1115 } 1116 1117 // Since the fee limit flags aren't required, we don't return an error 1118 // if they're not set. 1119 return nil, nil 1120 } 1121 1122 var listPaymentsCommand = cli.Command{ 1123 Name: "listpayments", 1124 Category: "Payments", 1125 Usage: "List all outgoing payments.", 1126 Description: "This command enables the retrieval of payments stored " + 1127 "in the database. Pagination is supported by the usage of " + 1128 "index_offset in combination with the paginate_forwards flag. " + 1129 "Reversed pagination is enabled by default to receive " + 1130 "current payments first. Pagination can be resumed by using " + 1131 "the returned last_index_offset (for forwards order), or " + 1132 "first_index_offset (for reversed order) as the offset_index. ", 1133 Flags: []cli.Flag{ 1134 cli.BoolFlag{ 1135 Name: "include_incomplete", 1136 Usage: "If set to true, payments still in flight (or " + 1137 "failed) will be returned as well, keeping" + 1138 "indices for payments the same as without " + 1139 "the flag.", 1140 }, 1141 cli.UintFlag{ 1142 Name: "index_offset", 1143 Usage: "The index of a payment that will be used as " + 1144 "either the start (in forwards mode) or end " + 1145 "(in reverse mode) of a query to determine " + 1146 "which payments should be returned in the " + 1147 "response, where the index_offset is " + 1148 "excluded. If index_offset is set to zero in " + 1149 "reversed mode, the query will end with the " + 1150 "last payment made.", 1151 }, 1152 cli.UintFlag{ 1153 Name: "max_payments", 1154 Usage: "the max number of payments to return, by " + 1155 "default, all completed payments are returned", 1156 }, 1157 cli.BoolFlag{ 1158 Name: "paginate_forwards", 1159 Usage: "if set, payments succeeding the " + 1160 "index_offset will be returned, allowing " + 1161 "forwards pagination", 1162 }, 1163 }, 1164 Action: actionDecorator(listPayments), 1165 } 1166 1167 func listPayments(ctx *cli.Context) error { 1168 ctxc := getContext() 1169 client, cleanUp := getClient(ctx) 1170 defer cleanUp() 1171 1172 req := &lnrpc.ListPaymentsRequest{ 1173 IncludeIncomplete: ctx.Bool("include_incomplete"), 1174 IndexOffset: uint64(ctx.Uint("index_offset")), 1175 MaxPayments: uint64(ctx.Uint("max_payments")), 1176 Reversed: !ctx.Bool("paginate_forwards"), 1177 } 1178 1179 payments, err := client.ListPayments(ctxc, req) 1180 if err != nil { 1181 return err 1182 } 1183 1184 printRespJSON(payments) 1185 return nil 1186 } 1187 1188 var forwardingHistoryCommand = cli.Command{ 1189 Name: "fwdinghistory", 1190 Category: "Payments", 1191 Usage: "Query the history of all forwarded HTLCs.", 1192 ArgsUsage: "start_time [end_time] [index_offset] [max_events]", 1193 Description: ` 1194 Query the HTLC switch's internal forwarding log for all completed 1195 payment circuits (HTLCs) over a particular time range '--start_time' and 1196 '--end_time'. The start and end times are meant to be expressed in 1197 seconds since the Unix epoch. 1198 Alternatively negative time ranges can be used, e.g. '-3d'. Supports 1199 s(seconds), m(minutes), h(ours), d(ays), w(eeks), M(onths), y(ears). 1200 Month equals 30.44 days, year equals 365.25 days. 1201 If '--start_time' isn't provided, then 24 hours ago is used. If 1202 '--end_time' isn't provided, then the current time is used. 1203 1204 The max number of events returned is 50k. The default number is 100, 1205 callers can use the '--max_events' param to modify this value. 1206 1207 Finally, callers can skip a series of events using the '--index_offset' 1208 parameter. Each response will contain the offset index of the last 1209 entry. Using this callers can manually paginate within a time slice. 1210 `, 1211 Flags: []cli.Flag{ 1212 cli.StringFlag{ 1213 Name: "start_time", 1214 Usage: "The starting time for the query " + 1215 `as unix timestamp or relative e.g. "-1w"`, 1216 }, 1217 cli.StringFlag{ 1218 Name: "end_time", 1219 Usage: "The end time for the query " + 1220 `as unix timestamp or relative e.g. "-1w"`, 1221 }, 1222 cli.Int64Flag{ 1223 Name: "index_offset", 1224 Usage: "The number of events to skip", 1225 }, 1226 cli.Int64Flag{ 1227 Name: "max_events", 1228 Usage: "The max number of events to return", 1229 }, 1230 }, 1231 Action: actionDecorator(forwardingHistory), 1232 } 1233 1234 func forwardingHistory(ctx *cli.Context) error { 1235 ctxc := getContext() 1236 client, cleanUp := getClient(ctx) 1237 defer cleanUp() 1238 1239 var ( 1240 startTime, endTime uint64 1241 indexOffset, maxEvents uint32 1242 err error 1243 ) 1244 args := ctx.Args() 1245 now := time.Now() 1246 1247 switch { 1248 case ctx.IsSet("start_time"): 1249 startTime, err = parseTime(ctx.String("start_time"), now) 1250 case args.Present(): 1251 startTime, err = parseTime(args.First(), now) 1252 args = args.Tail() 1253 default: 1254 now := time.Now() 1255 startTime = uint64(now.Add(-time.Hour * 24).Unix()) 1256 } 1257 if err != nil { 1258 return fmt.Errorf("unable to decode start_time: %v", err) 1259 } 1260 1261 switch { 1262 case ctx.IsSet("end_time"): 1263 endTime, err = parseTime(ctx.String("end_time"), now) 1264 case args.Present(): 1265 endTime, err = parseTime(args.First(), now) 1266 args = args.Tail() 1267 default: 1268 endTime = uint64(now.Unix()) 1269 } 1270 if err != nil { 1271 return fmt.Errorf("unable to decode end_time: %v", err) 1272 } 1273 1274 switch { 1275 case ctx.IsSet("index_offset"): 1276 indexOffset = uint32(ctx.Int64("index_offset")) 1277 case args.Present(): 1278 i, err := strconv.ParseInt(args.First(), 10, 64) 1279 if err != nil { 1280 return fmt.Errorf("unable to decode index_offset: %v", err) 1281 } 1282 indexOffset = uint32(i) 1283 args = args.Tail() 1284 } 1285 1286 switch { 1287 case ctx.IsSet("max_events"): 1288 maxEvents = uint32(ctx.Int64("max_events")) 1289 case args.Present(): 1290 m, err := strconv.ParseInt(args.First(), 10, 64) 1291 if err != nil { 1292 return fmt.Errorf("unable to decode max_events: %v", err) 1293 } 1294 maxEvents = uint32(m) 1295 args = args.Tail() 1296 } 1297 1298 req := &lnrpc.ForwardingHistoryRequest{ 1299 StartTime: startTime, 1300 EndTime: endTime, 1301 IndexOffset: indexOffset, 1302 NumMaxEvents: maxEvents, 1303 } 1304 resp, err := client.ForwardingHistory(ctxc, req) 1305 if err != nil { 1306 return err 1307 } 1308 1309 printRespJSON(resp) 1310 return nil 1311 } 1312 1313 var buildRouteCommand = cli.Command{ 1314 Name: "buildroute", 1315 Category: "Payments", 1316 Usage: "Build a route from a list of hop pubkeys.", 1317 Action: actionDecorator(buildRoute), 1318 Flags: []cli.Flag{ 1319 cli.Int64Flag{ 1320 Name: "amt", 1321 Usage: "the amount to send expressed in satoshis. If" + 1322 "not set, the minimum routable amount is used", 1323 }, 1324 cli.Int64Flag{ 1325 Name: "final_cltv_delta", 1326 Usage: "number of blocks the last hop has to reveal " + 1327 "the preimage", 1328 Value: chainreg.DefaultDecredTimeLockDelta, 1329 }, 1330 cli.StringFlag{ 1331 Name: "hops", 1332 Usage: "comma separated hex pubkeys", 1333 }, 1334 cli.Uint64Flag{ 1335 Name: "outgoing_chan_id", 1336 Usage: "short channel id of the outgoing channel to " + 1337 "use for the first hop of the payment", 1338 Value: 0, 1339 }, 1340 }, 1341 } 1342 1343 func buildRoute(ctx *cli.Context) error { 1344 ctxc := getContext() 1345 conn := getClientConn(ctx, false) 1346 defer conn.Close() 1347 1348 client := routerrpc.NewRouterClient(conn) 1349 1350 if !ctx.IsSet("hops") { 1351 return errors.New("hops required") 1352 } 1353 1354 // Build list of hop addresses for the rpc. 1355 hops := strings.Split(ctx.String("hops"), ",") 1356 rpcHops := make([][]byte, 0, len(hops)) 1357 for _, k := range hops { 1358 pubkey, err := route.NewVertexFromStr(k) 1359 if err != nil { 1360 return fmt.Errorf("error parsing %v: %v", k, err) 1361 } 1362 rpcHops = append(rpcHops, pubkey[:]) 1363 } 1364 1365 var amtMAtoms int64 1366 hasAmt := ctx.IsSet("amt") 1367 if hasAmt { 1368 amtMAtoms = ctx.Int64("amt") * 1000 1369 if amtMAtoms == 0 { 1370 return fmt.Errorf("non-zero amount required") 1371 } 1372 } 1373 1374 // Call BuildRoute rpc. 1375 req := &routerrpc.BuildRouteRequest{ 1376 AmtMAtoms: amtMAtoms, 1377 FinalCltvDelta: int32(ctx.Int64("final_cltv_delta")), 1378 HopPubkeys: rpcHops, 1379 OutgoingChanId: ctx.Uint64("outgoing_chan_id"), 1380 } 1381 1382 route, err := client.BuildRoute(ctxc, req) 1383 if err != nil { 1384 return err 1385 } 1386 1387 printRespJSON(route) 1388 1389 return nil 1390 } 1391 1392 var deletePaymentsCommand = cli.Command{ 1393 Name: "deletepayments", 1394 Category: "Payments", 1395 Usage: "Delete a single or multiple payments from the database.", 1396 ArgsUsage: "--all [--failed_htlcs_only --include_non_failed] | " + 1397 "--payment_hash hash [--failed_htlcs_only]", 1398 Description: ` 1399 This command either deletes all failed payments or a single payment from 1400 the database to reclaim disk space. 1401 1402 If the --all flag is used, then all failed payments are removed. If so 1403 desired, _ALL_ payments (even the successful ones) can be deleted 1404 by additionally specifying --include_non_failed. 1405 1406 If a --payment_hash is specified, that single payment is deleted, 1407 independent of its state. 1408 1409 If --failed_htlcs_only is specified then the payments themselves (or the 1410 single payment itself if used with --payment_hash) is not deleted, only 1411 the information about any failed HTLC attempts during the payment. 1412 1413 NOTE: Removing payments from the database does free up disk space within 1414 the internal bbolt database. But that disk space is only reclaimed after 1415 compacting the database. Users might want to turn on auto compaction 1416 (db.bolt.auto-compact=true in the config file or --db.bolt.auto-compact 1417 as a command line flag) and restart lnd after deleting a large number of 1418 payments to see a reduction in the file size of the channel.db file. 1419 `, 1420 Action: actionDecorator(deletePayments), 1421 Flags: []cli.Flag{ 1422 cli.BoolFlag{ 1423 Name: "all", 1424 Usage: "delete all failed payments", 1425 }, 1426 cli.StringFlag{ 1427 Name: "payment_hash", 1428 Usage: "delete a specific payment identified by its " + 1429 "payment hash", 1430 }, 1431 cli.BoolFlag{ 1432 Name: "failed_htlcs_only", 1433 Usage: "only delete failed HTLCs from payments, not " + 1434 "the payment itself", 1435 }, 1436 cli.BoolFlag{ 1437 Name: "include_non_failed", 1438 Usage: "delete ALL payments, not just the failed ones", 1439 }, 1440 }, 1441 } 1442 1443 func deletePayments(ctx *cli.Context) error { 1444 ctxc := getContext() 1445 client, cleanUp := getClient(ctx) 1446 defer cleanUp() 1447 1448 // Show command help if arguments or no flags are provided. 1449 if ctx.NArg() > 0 || ctx.NumFlags() == 0 { 1450 _ = cli.ShowCommandHelp(ctx, "deletepayments") 1451 return nil 1452 } 1453 1454 var ( 1455 paymentHash []byte 1456 all = ctx.Bool("all") 1457 singlePayment = ctx.IsSet("payment_hash") 1458 failedHTLCsOnly = ctx.Bool("failed_htlcs_only") 1459 includeNonFailed = ctx.Bool("include_non_failed") 1460 err error 1461 okMsg = struct { 1462 OK bool `json:"ok"` 1463 }{ 1464 OK: true, 1465 } 1466 ) 1467 1468 // We pack two RPCs into the same CLI so there are a few non-valid 1469 // combinations of the flags we need to filter out. 1470 switch { 1471 case all && singlePayment: 1472 return fmt.Errorf("cannot use --all and --payment_hash at " + 1473 "the same time") 1474 1475 case singlePayment && includeNonFailed: 1476 return fmt.Errorf("cannot use --payment_hash and " + 1477 "--include_non_failed at the same time, when using " + 1478 "a payment hash the payment is deleted independent " + 1479 "of its state") 1480 } 1481 1482 // Deleting a single payment is implemented in a different RPC than 1483 // removing all/multiple payments. 1484 switch { 1485 case singlePayment: 1486 paymentHash, err = hex.DecodeString(ctx.String("payment_hash")) 1487 if err != nil { 1488 return fmt.Errorf("error decoding payment_hash: %v", 1489 err) 1490 } 1491 1492 _, err = client.DeletePayment(ctxc, &lnrpc.DeletePaymentRequest{ 1493 PaymentHash: paymentHash, 1494 FailedHtlcsOnly: failedHTLCsOnly, 1495 }) 1496 if err != nil { 1497 return fmt.Errorf("error deleting single payment: %v", 1498 err) 1499 } 1500 1501 case all: 1502 what := "failed" 1503 if includeNonFailed { 1504 what = "all" 1505 } 1506 if failedHTLCsOnly { 1507 what = fmt.Sprintf("failed HTLCs from %s", what) 1508 } 1509 1510 fmt.Printf("Removing %s payments, this might take a while...\n", 1511 what) 1512 _, err = client.DeleteAllPayments( 1513 ctxc, &lnrpc.DeleteAllPaymentsRequest{ 1514 FailedPaymentsOnly: !includeNonFailed, 1515 FailedHtlcsOnly: failedHTLCsOnly, 1516 }, 1517 ) 1518 if err != nil { 1519 return fmt.Errorf("error deleting payments: %v", err) 1520 } 1521 } 1522 1523 // Users are confused by empty JSON outputs so let's return a simple OK 1524 // instead of just printing the empty response RPC message. 1525 printJSON(okMsg) 1526 1527 return nil 1528 } 1529 1530 // ESC is the ASCII code for escape character 1531 const ESC = 27 1532 1533 // clearCode defines a terminal escape code to clear the currently line and move 1534 // the cursor up. 1535 var clearCode = fmt.Sprintf("%c[%dA%c[2K", ESC, 1, ESC) 1536 1537 // clearLines erases the last count lines in the terminal window. 1538 func clearLines(count int) { 1539 _, _ = fmt.Print(strings.Repeat(clearCode, count)) 1540 }