lnd/cmd/lncli/cmd_payments.go
ziggie 4db5b2266f
lncli: remove default cltv delta for buildroute.
Now the default of the lnd backend is used instead of providing
the default value via the caller of the buildroute rpc cmd.
2024-01-16 14:10:22 +01:00

1825 lines
47 KiB
Go

package main
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli"
)
const (
// paymentTimeout is the default timeout for the payment loop in lnd.
// No new attempts will be started after the timeout.
paymentTimeout = time.Second * 60
)
var (
cltvLimitFlag = cli.UintFlag{
Name: "cltv_limit",
Usage: "the maximum time lock that may be used for " +
"this payment",
}
lastHopFlag = cli.StringFlag{
Name: "last_hop",
Usage: "pubkey of the last hop (penultimate node in the path) " +
"to route through for this payment",
}
dataFlag = cli.StringFlag{
Name: "data",
Usage: "attach custom data to the payment. The required " +
"format is: <record_id>=<hex_value>,<record_id>=" +
"<hex_value>,.. For example: --data 3438382=0a21ff. " +
"Custom record ids start from 65536.",
}
inflightUpdatesFlag = cli.BoolFlag{
Name: "inflight_updates",
Usage: "if set, intermediate payment state updates will be " +
"displayed. Only valid in combination with --json.",
}
maxPartsFlag = cli.UintFlag{
Name: "max_parts",
Usage: "the maximum number of partial payments that may be " +
"used",
Value: routerrpc.DefaultMaxParts,
}
jsonFlag = cli.BoolFlag{
Name: "json",
Usage: "if set, payment updates are printed as json " +
"messages. Set by default on Windows because table " +
"formatting is unsupported.",
}
maxShardSizeSatFlag = cli.UintFlag{
Name: "max_shard_size_sat",
Usage: "the largest payment split that should be attempted if " +
"payment splitting is required to attempt a payment, " +
"specified in satoshis",
}
maxShardSizeMsatFlag = cli.UintFlag{
Name: "max_shard_size_msat",
Usage: "the largest payment split that should be attempted if " +
"payment splitting is required to attempt a payment, " +
"specified in milli-satoshis",
}
ampFlag = cli.BoolFlag{
Name: "amp",
Usage: "if set to true, then AMP will be used to complete the " +
"payment",
}
timePrefFlag = cli.Float64Flag{
Name: "time_pref",
Usage: "(optional) expresses time preference (range -1 to 1)",
}
introductionNodeFlag = cli.StringFlag{
Name: "introduction_node",
Usage: "(blinded paths) the hex encoded, cleartext node ID " +
"of the node to use for queries to a blinded route",
}
blindingPointFlag = cli.StringFlag{
Name: "blinding_point",
Usage: "(blinded paths) the hex encoded blinding point to " +
"use if querying a route to a blinded path, this " +
"value *must* be set for queries to a blinded path",
}
blindedHopsFlag = cli.StringSliceFlag{
Name: "blinded_hops",
Usage: "(blinded paths) the blinded hops to include in the " +
"query, formatted as <blinded_node_id>:" +
"<hex_encrypted_data>. These hops must be provided " +
"*in order* starting with the introduction point and " +
"ending with the receiving node",
}
blindedBaseFlag = cli.Uint64Flag{
Name: "blinded_base_fee",
Usage: "(blinded paths) the aggregate base fee for the " +
"blinded portion of the route, expressed in msat",
}
blindedPPMFlag = cli.Uint64Flag{
Name: "blinded_ppm_fee",
Usage: "(blinded paths) the aggregate proportional fee for " +
"the blinded portion of the route, expressed in " +
"parts per million",
}
blindedCLTVFlag = cli.Uint64Flag{
Name: "blinded_cltv",
Usage: "(blinded paths) the total cltv delay for the " +
"blinded portion of the route",
}
)
// paymentFlags returns common flags for sendpayment and payinvoice.
func paymentFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: "pay_req",
Usage: "a zpay32 encoded payment request to fulfill",
},
cli.Int64Flag{
Name: "fee_limit",
Usage: "maximum fee allowed in satoshis when " +
"sending the payment",
},
cli.Int64Flag{
Name: "fee_limit_percent",
Usage: "percentage of the payment's amount used as " +
"the maximum fee allowed when sending the " +
"payment",
},
cli.DurationFlag{
Name: "timeout",
Usage: "the maximum amount of time we should spend " +
"trying to fulfill the payment, failing " +
"after the timeout has elapsed",
Value: paymentTimeout,
},
cltvLimitFlag,
lastHopFlag,
cli.Int64SliceFlag{
Name: "outgoing_chan_id",
Usage: "short channel id of the outgoing channel to " +
"use for the first hop of the payment; can " +
"be specified multiple times in the same " +
"command",
Value: &cli.Int64Slice{},
},
cli.BoolFlag{
Name: "force, f",
Usage: "will skip payment request confirmation",
},
cli.BoolFlag{
Name: "allow_self_payment",
Usage: "allow sending a circular payment to self",
},
dataFlag, inflightUpdatesFlag, maxPartsFlag, jsonFlag,
maxShardSizeSatFlag, maxShardSizeMsatFlag, ampFlag,
timePrefFlag,
}
}
var sendPaymentCommand = cli.Command{
Name: "sendpayment",
Category: "Payments",
Usage: "Send a payment over lightning.",
Description: `
Send a payment over Lightning. One can either specify the full
parameters of the payment, or just use a payment request which encodes
all the payment details.
If payment isn't manually specified, then only a payment request needs
to be passed using the --pay_req argument.
If the payment *is* manually specified, then the following arguments
need to be specified in order to complete the payment:
For invoice with keysend,
--dest=N --amt=A --final_cltv_delta=T --keysend
For invoice without payment address:
--dest=N --amt=A --payment_hash=H --final_cltv_delta=T
For invoice with payment address:
--dest=N --amt=A --payment_hash=H --final_cltv_delta=T --pay_addr=H
`,
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
"--pay_req=R [--pay_addr=H]",
Flags: append(paymentFlags(),
cli.StringFlag{
Name: "dest, d",
Usage: "the compressed identity pubkey of the " +
"payment recipient",
},
cli.Int64Flag{
Name: "amt, a",
Usage: "number of satoshis to send",
},
cli.StringFlag{
Name: "payment_hash, r",
Usage: "the hash to use within the payment's HTLC",
},
cli.Int64Flag{
Name: "final_cltv_delta",
Usage: "the number of blocks the last hop has to reveal the preimage",
},
cli.StringFlag{
Name: "pay_addr",
Usage: "the payment address of the generated invoice",
},
cli.BoolFlag{
Name: "keysend",
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
},
),
Action: sendPayment,
}
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
// flags passed. It always returns a value and doesn't rely on lnd applying a
// default.
func retrieveFeeLimit(ctx *cli.Context, amt int64) (int64, error) {
switch {
case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"):
return 0, fmt.Errorf("either fee_limit or fee_limit_percent " +
"can be set, but not both")
case ctx.IsSet("fee_limit"):
return ctx.Int64("fee_limit"), nil
case ctx.IsSet("fee_limit_percent"):
// Round up the fee limit to prevent hitting zero on small
// amounts.
feeLimitRoundedUp :=
(amt*ctx.Int64("fee_limit_percent") + 99) / 100
return feeLimitRoundedUp, nil
}
// If no fee limit is set, use a default value based on the amount.
amtMsat := lnwire.NewMSatFromSatoshis(btcutil.Amount(amt))
limitMsat := lnwallet.DefaultRoutingFeeLimitForAmount(amtMsat)
return int64(limitMsat.ToSatoshis()), nil
}
func confirmPayReq(resp *lnrpc.PayReq, amt, feeLimit int64) error {
fmt.Printf("Payment hash: %v\n", resp.GetPaymentHash())
fmt.Printf("Description: %v\n", resp.GetDescription())
fmt.Printf("Amount (in satoshis): %v\n", amt)
fmt.Printf("Fee limit (in satoshis): %v\n", feeLimit)
fmt.Printf("Destination: %v\n", resp.GetDestination())
confirm := promptForConfirmation("Confirm payment (yes/no): ")
if !confirm {
return fmt.Errorf("payment not confirmed")
}
return nil
}
func parsePayAddr(ctx *cli.Context, args cli.Args) ([]byte, error) {
var (
payAddr []byte
err error
)
switch {
case ctx.IsSet("pay_addr"):
payAddr, err = hex.DecodeString(ctx.String("pay_addr"))
case args.Present():
payAddr, err = hex.DecodeString(args.First())
}
if err != nil {
return nil, err
}
// payAddr may be not required if it's a legacy invoice.
if len(payAddr) != 0 && len(payAddr) != 32 {
return nil, fmt.Errorf("payment addr must be exactly 32 "+
"bytes, is instead %v", len(payAddr))
}
return payAddr, nil
}
func sendPayment(ctx *cli.Context) error {
// Show command help if no arguments provided
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
_ = cli.ShowCommandHelp(ctx, "sendpayment")
return nil
}
args := ctx.Args()
// If a payment request was provided, we can exit early since all of the
// details of the payment are encoded within the request.
if ctx.IsSet("pay_req") {
req := &routerrpc.SendPaymentRequest{
PaymentRequest: stripPrefix(ctx.String("pay_req")),
Amt: ctx.Int64("amt"),
DestCustomRecords: make(map[uint64][]byte),
}
// We'll attempt to parse a payment address as well, given that
// if the user is using an AMP invoice, then they may be trying
// to specify that value manually.
//
// Don't parse unnamed arguments to prevent confusion with the main
// unnamed argument format for non-AMP payments.
payAddr, err := parsePayAddr(ctx, nil)
if err != nil {
return err
}
req.PaymentAddr = payAddr
return sendPaymentRequest(ctx, req)
}
var (
destNode []byte
amount int64
err error
)
switch {
case ctx.IsSet("dest"):
destNode, err = hex.DecodeString(ctx.String("dest"))
case args.Present():
destNode, err = hex.DecodeString(args.First())
args = args.Tail()
default:
return fmt.Errorf("destination txid argument missing")
}
if err != nil {
return err
}
if len(destNode) != 33 {
return fmt.Errorf("dest node pubkey must be exactly 33 bytes, is "+
"instead: %v", len(destNode))
}
if ctx.IsSet("amt") {
amount = ctx.Int64("amt")
} else if args.Present() {
amount, err = strconv.ParseInt(args.First(), 10, 64)
args = args.Tail()
if err != nil {
return fmt.Errorf("unable to decode payment amount: %v", err)
}
}
req := &routerrpc.SendPaymentRequest{
Dest: destNode,
Amt: amount,
DestCustomRecords: make(map[uint64][]byte),
Amp: ctx.Bool(ampFlag.Name),
}
var rHash []byte
switch {
case ctx.Bool("keysend") && ctx.Bool(ampFlag.Name):
return errors.New("either keysend or amp may be set, but not both")
case ctx.Bool("keysend"):
if ctx.IsSet("payment_hash") {
return errors.New("cannot set payment hash when using " +
"keysend")
}
var preimage lntypes.Preimage
if _, err := rand.Read(preimage[:]); err != nil {
return err
}
// Set the preimage. If the user supplied a preimage with the
// data flag, the preimage that is set here will be overwritten
// later.
req.DestCustomRecords[record.KeySendType] = preimage[:]
hash := preimage.Hash()
rHash = hash[:]
case !ctx.Bool(ampFlag.Name):
switch {
case ctx.IsSet("payment_hash"):
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
case args.Present():
rHash, err = hex.DecodeString(args.First())
args = args.Tail()
default:
return fmt.Errorf("payment hash argument missing")
}
}
if err != nil {
return err
}
if !req.Amp && len(rHash) != 32 {
return fmt.Errorf("payment hash must be exactly 32 "+
"bytes, is instead %v", len(rHash))
}
req.PaymentHash = rHash
switch {
case ctx.IsSet("final_cltv_delta"):
req.FinalCltvDelta = int32(ctx.Int64("final_cltv_delta"))
case args.Present():
delta, err := strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return err
}
args = args.Tail()
req.FinalCltvDelta = int32(delta)
}
payAddr, err := parsePayAddr(ctx, args)
if err != nil {
return err
}
req.PaymentAddr = payAddr
return sendPaymentRequest(ctx, req)
}
func sendPaymentRequest(ctx *cli.Context,
req *routerrpc.SendPaymentRequest) error {
ctxc := getContext()
conn := getClientConn(ctx, false)
defer conn.Close()
client := lnrpc.NewLightningClient(conn)
routerClient := routerrpc.NewRouterClient(conn)
outChan := ctx.Int64Slice("outgoing_chan_id")
if len(outChan) != 0 {
req.OutgoingChanIds = make([]uint64, len(outChan))
for i, c := range outChan {
req.OutgoingChanIds[i] = uint64(c)
}
}
if ctx.IsSet(lastHopFlag.Name) {
lastHop, err := route.NewVertexFromStr(
ctx.String(lastHopFlag.Name),
)
if err != nil {
return err
}
req.LastHopPubkey = lastHop[:]
}
req.CltvLimit = int32(ctx.Int(cltvLimitFlag.Name))
pmtTimeout := ctx.Duration("timeout")
if pmtTimeout <= 0 {
return errors.New("payment timeout must be greater than zero")
}
req.TimeoutSeconds = int32(pmtTimeout.Seconds())
req.AllowSelfPayment = ctx.Bool("allow_self_payment")
req.MaxParts = uint32(ctx.Uint(maxPartsFlag.Name))
switch {
// If the max shard size is specified, then it should either be in sat
// or msat, but not both.
case ctx.Uint64(maxShardSizeMsatFlag.Name) != 0 &&
ctx.Uint64(maxShardSizeSatFlag.Name) != 0:
return fmt.Errorf("only --max_split_size_msat or " +
"--max_split_size_sat should be set, but not both")
case ctx.Uint64(maxShardSizeMsatFlag.Name) != 0:
req.MaxShardSizeMsat = ctx.Uint64(maxShardSizeMsatFlag.Name)
case ctx.Uint64(maxShardSizeSatFlag.Name) != 0:
req.MaxShardSizeMsat = uint64(lnwire.NewMSatFromSatoshis(
btcutil.Amount(ctx.Uint64(maxShardSizeSatFlag.Name)),
))
}
// Parse custom data records.
data := ctx.String(dataFlag.Name)
if data != "" {
records := strings.Split(data, ",")
for _, r := range records {
kv := strings.Split(r, "=")
if len(kv) != 2 {
return errors.New("invalid data format: " +
"multiple equal signs in record")
}
recordID, err := strconv.ParseUint(kv[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid data format: %v",
err)
}
hexValue, err := hex.DecodeString(kv[1])
if err != nil {
return fmt.Errorf("invalid data format: %v",
err)
}
req.DestCustomRecords[recordID] = hexValue
}
}
var feeLimit int64
if req.PaymentRequest != "" {
// Decode payment request to find out the amount.
decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
decodeResp, err := client.DecodePayReq(ctxc, decodeReq)
if err != nil {
return err
}
// If amount is present in the request, override the request
// amount.
amt := req.Amt
invoiceAmt := decodeResp.GetNumSatoshis()
if invoiceAmt != 0 {
amt = invoiceAmt
}
// Calculate fee limit based on the determined amount.
feeLimit, err = retrieveFeeLimit(ctx, amt)
if err != nil {
return err
}
// Ask for confirmation of amount and fee limit if payment is
// forced.
if !ctx.Bool("force") {
err := confirmPayReq(decodeResp, amt, feeLimit)
if err != nil {
return err
}
}
} else {
var err error
feeLimit, err = retrieveFeeLimit(ctx, req.Amt)
if err != nil {
return err
}
}
req.FeeLimitSat = feeLimit
// Set time pref.
req.TimePref = ctx.Float64(timePrefFlag.Name)
// Always print in-flight updates for the table output.
printJSON := ctx.Bool(jsonFlag.Name)
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
stream, err := routerClient.SendPaymentV2(ctxc, req)
if err != nil {
return err
}
finalState, err := printLivePayment(
ctxc, stream, client, printJSON,
)
if err != nil {
return err
}
// If we get a payment error back, we pass an error up
// to main which eventually calls fatal() and returns
// with a non-zero exit code.
if finalState.Status != lnrpc.Payment_SUCCEEDED {
return errors.New(finalState.Status.String())
}
return nil
}
var trackPaymentCommand = cli.Command{
Name: "trackpayment",
Category: "Payments",
Usage: "Track progress of an existing payment.",
Description: `
Pick up monitoring the progression of a previously initiated payment
specified by the hash argument.
`,
ArgsUsage: "hash",
Flags: []cli.Flag{
jsonFlag,
},
Action: actionDecorator(trackPayment),
}
func trackPayment(ctx *cli.Context) error {
ctxc := getContext()
args := ctx.Args()
conn := getClientConn(ctx, false)
defer conn.Close()
routerClient := routerrpc.NewRouterClient(conn)
if !args.Present() {
return fmt.Errorf("hash argument missing")
}
hash, err := hex.DecodeString(args.First())
if err != nil {
return err
}
req := &routerrpc.TrackPaymentRequest{
PaymentHash: hash,
}
stream, err := routerClient.TrackPaymentV2(ctxc, req)
if err != nil {
return err
}
client := lnrpc.NewLightningClient(conn)
_, err = printLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
return err
}
// printLivePayment receives payment updates from the given stream and either
// outputs them as json or as a more user-friendly formatted table. The table
// option uses terminal control codes to rewrite the output. This call
// terminates when the payment reaches a final state.
func printLivePayment(ctxc context.Context,
stream routerrpc.Router_TrackPaymentV2Client,
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
// Terminal escape codes aren't supported on Windows, fall back to json.
if !json && runtime.GOOS == "windows" {
json = true
}
aliases := newAliasCache(client)
first := true
var lastLineCount int
for {
payment, err := stream.Recv()
if err != nil {
return nil, err
}
if json {
// Delimit json messages by newlines (inspired by
// grpc over rest chunking).
if first {
first = false
} else {
fmt.Println()
}
// Write raw json to stdout.
printRespJSON(payment)
} else {
table := formatPayment(ctxc, payment, aliases)
// Clear all previously written lines and print the
// updated table.
clearLines(lastLineCount)
fmt.Print(table)
// Store the number of lines written for the next update
// pass.
lastLineCount = 0
for _, b := range table {
if b == '\n' {
lastLineCount++
}
}
}
// Terminate loop if payments state is final.
if payment.Status != lnrpc.Payment_IN_FLIGHT &&
payment.Status != lnrpc.Payment_INITIATED {
return payment, nil
}
}
}
// aliasCache allows cached retrieval of node aliases.
type aliasCache struct {
cache map[string]string
client lnrpc.LightningClient
}
func newAliasCache(client lnrpc.LightningClient) *aliasCache {
return &aliasCache{
client: client,
cache: make(map[string]string),
}
}
// get returns a node alias either from cache or freshly requested from lnd.
func (a *aliasCache) get(ctxc context.Context, pubkey string) string {
alias, ok := a.cache[pubkey]
if ok {
return alias
}
// Request node info.
resp, err := a.client.GetNodeInfo(
ctxc,
&lnrpc.NodeInfoRequest{
PubKey: pubkey,
},
)
if err != nil {
// If no info is available, use the
// pubkey as identifier.
alias = pubkey[:6]
} else {
alias = resp.Node.Alias
}
a.cache[pubkey] = alias
return alias
}
// formatMsat formats msat amounts as fractional sats.
func formatMsat(amt int64) string {
return strconv.FormatFloat(float64(amt)/1000.0, 'f', -1, 64)
}
// formatPayment formats the payment state as an ascii table.
func formatPayment(ctxc context.Context, payment *lnrpc.Payment,
aliases *aliasCache) string {
t := table.NewWriter()
// Build table header.
t.AppendHeader(table.Row{
"HTLC_STATE", "ATTEMPT_TIME", "RESOLVE_TIME", "RECEIVER_AMT",
"FEE", "TIMELOCK", "CHAN_OUT", "ROUTE",
})
t.SetColumnConfigs([]table.ColumnConfig{
{Name: "ATTEMPT_TIME", Align: text.AlignRight},
{Name: "RESOLVE_TIME", Align: text.AlignRight},
{Name: "CHAN_OUT", Align: text.AlignLeft,
AlignHeader: text.AlignLeft},
})
// Add all htlcs as rows.
createTime := time.Unix(0, payment.CreationTimeNs)
var totalPaid, totalFees int64
for _, htlc := range payment.Htlcs {
formatTime := func(timeNs int64) string {
if timeNs == 0 {
return "-"
}
resolveTime := time.Unix(0, timeNs)
resolveTimeDiff := resolveTime.Sub(createTime)
resolveTimeMs := resolveTimeDiff / time.Millisecond
return fmt.Sprintf(
"%.3f", float64(resolveTimeMs)/1000.0,
)
}
attemptTime := formatTime(htlc.AttemptTimeNs)
resolveTime := formatTime(htlc.ResolveTimeNs)
route := htlc.Route
lastHop := route.Hops[len(route.Hops)-1]
hops := []string{}
for _, h := range route.Hops {
alias := aliases.get(ctxc, h.PubKey)
hops = append(hops, alias)
}
state := htlc.Status.String()
if htlc.Failure != nil {
state = fmt.Sprintf(
"%v @ %s hop",
htlc.Failure.Code,
ordinalNumber(htlc.Failure.FailureSourceIndex),
)
}
t.AppendRow([]interface{}{
state, attemptTime, resolveTime,
formatMsat(lastHop.AmtToForwardMsat),
formatMsat(route.TotalFeesMsat),
route.TotalTimeLock, route.Hops[0].ChanId,
strings.Join(hops, "->")},
)
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
totalPaid += lastHop.AmtToForwardMsat
totalFees += route.TotalFeesMsat
}
}
// Render table.
b := &bytes.Buffer{}
t.SetOutputMirror(b)
t.Render()
// Add additional payment-level data.
fmt.Fprintf(b, "Amount + fee: %v + %v sat\n",
formatMsat(totalPaid), formatMsat(totalFees))
fmt.Fprintf(b, "Payment hash: %v\n", payment.PaymentHash)
fmt.Fprintf(b, "Payment status: %v", payment.Status)
switch payment.Status {
case lnrpc.Payment_SUCCEEDED:
fmt.Fprintf(b, ", preimage: %v", payment.PaymentPreimage)
case lnrpc.Payment_FAILED:
fmt.Fprintf(b, ", reason: %v", payment.FailureReason)
}
fmt.Fprintf(b, "\n")
return b.String()
}
var payInvoiceCommand = cli.Command{
Name: "payinvoice",
Category: "Payments",
Usage: "Pay an invoice over lightning.",
Description: `
This command is a shortcut for 'sendpayment --pay_req='.
`,
ArgsUsage: "pay_req",
Flags: append(paymentFlags(),
cli.Int64Flag{
Name: "amt",
Usage: "(optional) number of satoshis to fulfill the " +
"invoice",
},
),
Action: actionDecorator(payInvoice),
}
func payInvoice(ctx *cli.Context) error {
args := ctx.Args()
var payReq string
switch {
case ctx.IsSet("pay_req"):
payReq = ctx.String("pay_req")
case args.Present():
payReq = args.First()
default:
return fmt.Errorf("pay_req argument missing")
}
req := &routerrpc.SendPaymentRequest{
PaymentRequest: stripPrefix(payReq),
Amt: ctx.Int64("amt"),
DestCustomRecords: make(map[uint64][]byte),
}
return sendPaymentRequest(ctx, req)
}
var sendToRouteCommand = cli.Command{
Name: "sendtoroute",
Category: "Payments",
Usage: "Send a payment over a predefined route.",
Description: `
Send a payment over Lightning using a specific route. One must specify
the route to attempt and the payment hash. This command can even
be chained with the response to queryroutes or buildroute. This command
can be used to implement channel rebalancing by crafting a self-route,
or even atomic swaps using a self-route that crosses multiple chains.
There are three ways to specify a route:
* using the --routes parameter to manually specify a JSON encoded
route in the format of the return value of queryroutes or
buildroute:
(lncli sendtoroute --payment_hash=<pay_hash> --routes=<route>)
* passing the route as a positional argument:
(lncli sendtoroute --payment_hash=pay_hash <route>)
* or reading in the route from stdin, which can allow chaining the
response from queryroutes or buildroute, or even read in a file
with a pre-computed route:
(lncli queryroutes --args.. | lncli sendtoroute --payment_hash= -
notice the '-' at the end, which signals that lncli should read
the route in from stdin
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "payment_hash, pay_hash",
Usage: "the hash to use within the payment's HTLC",
},
cli.StringFlag{
Name: "routes, r",
Usage: "a json array string in the format of the response " +
"of queryroutes that denotes which routes to use",
},
cli.BoolFlag{
Name: "skip_temp_err",
Usage: "Whether the payment should be marked as " +
"failed when a temporary error occurred. Set " +
"it to true so the payment won't be failed " +
"unless a terminal error has occurred.",
},
},
Action: sendToRoute,
}
func sendToRoute(ctx *cli.Context) error {
// Show command help if no arguments provided.
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
_ = cli.ShowCommandHelp(ctx, "sendtoroute")
return nil
}
args := ctx.Args()
var (
rHash []byte
err error
)
switch {
case ctx.IsSet("payment_hash"):
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
case args.Present():
rHash, err = hex.DecodeString(args.First())
args = args.Tail()
default:
return fmt.Errorf("payment hash argument missing")
}
if err != nil {
return err
}
if len(rHash) != 32 {
return fmt.Errorf("payment hash must be exactly 32 "+
"bytes, is instead %d", len(rHash))
}
var jsonRoutes string
switch {
// The user is specifying the routes explicitly via the key word
// argument.
case ctx.IsSet("routes"):
jsonRoutes = ctx.String("routes")
// The user is specifying the routes as a positional argument.
case args.Present() && args.First() != "-":
jsonRoutes = args.First()
// The user is signalling that we should read stdin in order to parse
// the set of target routes.
case args.Present() && args.First() == "-":
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return err
}
if len(b) == 0 {
return fmt.Errorf("queryroutes output is empty")
}
jsonRoutes = string(b)
}
// Try to parse the provided json both in the legacy QueryRoutes format
// that contains a list of routes and the single route BuildRoute
// format.
var route *lnrpc.Route
routes := &lnrpc.QueryRoutesResponse{}
err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal([]byte(jsonRoutes), routes)
if err == nil {
if len(routes.Routes) == 0 {
return fmt.Errorf("no routes provided")
}
if len(routes.Routes) != 1 {
return fmt.Errorf("expected a single route, but got %v",
len(routes.Routes))
}
route = routes.Routes[0]
} else {
routes := &routerrpc.BuildRouteResponse{}
err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(
[]byte(jsonRoutes), routes,
)
if err != nil {
return fmt.Errorf("unable to unmarshal json string "+
"from incoming array of routes: %v", err)
}
route = routes.Route
}
req := &routerrpc.SendToRouteRequest{
PaymentHash: rHash,
Route: route,
SkipTempErr: ctx.Bool("skip_temp_err"),
}
return sendToRouteRequest(ctx, req)
}
func sendToRouteRequest(ctx *cli.Context, req *routerrpc.SendToRouteRequest) error {
ctxc := getContext()
conn := getClientConn(ctx, false)
defer conn.Close()
client := routerrpc.NewRouterClient(conn)
resp, err := client.SendToRouteV2(ctxc, req)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
var queryRoutesCommand = cli.Command{
Name: "queryroutes",
Category: "Payments",
Usage: "Query a route to a destination.",
Description: "Queries the channel router for a potential path to the destination that has sufficient flow for the amount including fees",
ArgsUsage: "dest amt",
Flags: []cli.Flag{
cli.StringFlag{
Name: "dest",
Usage: "the 33-byte hex-encoded public key for the payment " +
"destination",
},
cli.Int64Flag{
Name: "amt",
Usage: "the amount to send expressed in satoshis",
},
cli.Int64Flag{
Name: "fee_limit",
Usage: "maximum fee allowed in satoshis when sending " +
"the payment",
},
cli.Int64Flag{
Name: "fee_limit_percent",
Usage: "percentage of the payment's amount used as the " +
"maximum fee allowed when sending the payment",
},
cli.Int64Flag{
Name: "final_cltv_delta",
Usage: "(optional) number of blocks the last hop has to reveal " +
"the preimage",
},
cli.BoolFlag{
Name: "use_mc",
Usage: "use mission control probabilities",
},
cli.Uint64Flag{
Name: "outgoing_chan_id",
Usage: "(optional) the channel id of the channel " +
"that must be taken to the first hop",
},
cli.StringSliceFlag{
Name: "ignore_pair",
Usage: "ignore directional node pair " +
"<node1>:<node2>. This flag can be specified " +
"multiple times if multiple node pairs are " +
"to be ignored",
},
timePrefFlag,
cltvLimitFlag,
introductionNodeFlag,
blindingPointFlag,
blindedHopsFlag,
blindedBaseFlag,
blindedPPMFlag,
blindedCLTVFlag,
},
Action: actionDecorator(queryRoutes),
}
func queryRoutes(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()
var (
dest string
amt int64
err error
)
args := ctx.Args()
switch {
case ctx.IsSet("dest"):
dest = ctx.String("dest")
case args.Present():
dest = args.First()
args = args.Tail()
// If we have a blinded path set, we don't have to specify a
// destination.
case ctx.IsSet(introductionNodeFlag.Name):
default:
return fmt.Errorf("dest argument missing")
}
switch {
case ctx.IsSet("amt"):
amt = ctx.Int64("amt")
case args.Present():
amt, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode amt argument: %v", err)
}
default:
return fmt.Errorf("amt argument missing")
}
feeLimit, err := retrieveFeeLimitLegacy(ctx)
if err != nil {
return err
}
pairs := ctx.StringSlice("ignore_pair")
ignoredPairs := make([]*lnrpc.NodePair, len(pairs))
for i, pair := range pairs {
nodes := strings.Split(pair, ":")
if len(nodes) != 2 {
return fmt.Errorf("invalid node pair format. " +
"Expected <node1 pub key>:<node2 pub key>")
}
node1, err := hex.DecodeString(nodes[0])
if err != nil {
return err
}
node2, err := hex.DecodeString(nodes[1])
if err != nil {
return err
}
ignoredPairs[i] = &lnrpc.NodePair{
From: node1,
To: node2,
}
}
blindedRoutes, err := parseBlindedPaymentParameters(ctx)
if err != nil {
return err
}
req := &lnrpc.QueryRoutesRequest{
PubKey: dest,
Amt: amt,
FeeLimit: feeLimit,
FinalCltvDelta: int32(ctx.Int("final_cltv_delta")),
UseMissionControl: ctx.Bool("use_mc"),
CltvLimit: uint32(ctx.Uint64(cltvLimitFlag.Name)),
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
TimePref: ctx.Float64(timePrefFlag.Name),
IgnoredPairs: ignoredPairs,
BlindedPaymentPaths: blindedRoutes,
}
route, err := client.QueryRoutes(ctxc, req)
if err != nil {
return err
}
printRespJSON(route)
return nil
}
func parseBlindedPaymentParameters(ctx *cli.Context) (
[]*lnrpc.BlindedPaymentPath, error) {
// Return nil if we don't have a blinding set, as we don't have a
// blinded path.
if !ctx.IsSet(blindingPointFlag.Name) {
return nil, nil
}
// If any one of our blinding related flags is set, we expect the
// full set to be set and we'll error out accordingly.
introNode, err := route.NewVertexFromStr(
ctx.String(introductionNodeFlag.Name),
)
if err != nil {
return nil, fmt.Errorf("decode introduction node: %w", err)
}
blindingPoint, err := route.NewVertexFromStr(ctx.String(
blindingPointFlag.Name,
))
if err != nil {
return nil, fmt.Errorf("decode blinding point: %w", err)
}
blindedHops := ctx.StringSlice(blindedHopsFlag.Name)
pmt := &lnrpc.BlindedPaymentPath{
BlindedPath: &lnrpc.BlindedPath{
IntroductionNode: introNode[:],
BlindingPoint: blindingPoint[:],
BlindedHops: make(
[]*lnrpc.BlindedHop, len(blindedHops),
),
},
BaseFeeMsat: ctx.Uint64(
blindedBaseFlag.Name,
),
ProportionalFeeMsat: ctx.Uint64(
blindedPPMFlag.Name,
),
TotalCltvDelta: uint32(ctx.Uint64(
blindedCLTVFlag.Name,
)),
}
for i, hop := range blindedHops {
parts := strings.Split(hop, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("blinded hops should be "+
"expressed as "+
"blinded_node_id:hex_encrypted_data, got: %v",
hop)
}
hop, err := route.NewVertexFromStr(parts[0])
if err != nil {
return nil, fmt.Errorf("hop: %v node: %w", i, err)
}
data, err := hex.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("hop: %v data: %w", i, err)
}
pmt.BlindedPath.BlindedHops[i] = &lnrpc.BlindedHop{
BlindedNode: hop[:],
EncryptedData: data,
}
}
return []*lnrpc.BlindedPaymentPath{
pmt,
}, nil
}
// retrieveFeeLimitLegacy retrieves the fee limit based on the different fee
// limit flags passed. This function will eventually disappear in favor of
// retrieveFeeLimit and the new payment rpc.
func retrieveFeeLimitLegacy(ctx *cli.Context) (*lnrpc.FeeLimit, error) {
switch {
case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"):
return nil, fmt.Errorf("either fee_limit or fee_limit_percent " +
"can be set, but not both")
case ctx.IsSet("fee_limit"):
return &lnrpc.FeeLimit{
Limit: &lnrpc.FeeLimit_Fixed{
Fixed: ctx.Int64("fee_limit"),
},
}, nil
case ctx.IsSet("fee_limit_percent"):
feeLimitPercent := ctx.Int64("fee_limit_percent")
if feeLimitPercent < 0 {
return nil, errors.New("negative fee limit percentage " +
"provided")
}
return &lnrpc.FeeLimit{
Limit: &lnrpc.FeeLimit_Percent{
Percent: feeLimitPercent,
},
}, nil
}
// Since the fee limit flags aren't required, we don't return an error
// if they're not set.
return nil, nil
}
var listPaymentsCommand = cli.Command{
Name: "listpayments",
Category: "Payments",
Usage: "List all outgoing payments.",
Description: `
This command enables the retrieval of payments stored
in the database.
Pagination is supported by the usage of index_offset in combination with
the paginate_forwards flag.
Reversed pagination is enabled by default to receive current payments
first. Pagination can be resumed by using the returned last_index_offset
(for forwards order), or first_index_offset (for reversed order) as the
offset_index.
Because counting all payments in the payment database can take a long
time on systems with many payments, the count is not returned by
default. That feature can be turned on with the --count_total_payments
flag.
`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "include_incomplete",
Usage: "if set to true, payments still in flight (or " +
"failed) will be returned as well, keeping" +
"indices for payments the same as without " +
"the flag",
},
cli.UintFlag{
Name: "index_offset",
Usage: "The index of a payment that will be used as " +
"either the start (in forwards mode) or end " +
"(in reverse mode) of a query to determine " +
"which payments should be returned in the " +
"response, where the index_offset is " +
"excluded. If index_offset is set to zero in " +
"reversed mode, the query will end with the " +
"last payment made.",
},
cli.UintFlag{
Name: "max_payments",
Usage: "the max number of payments to return, by " +
"default, all completed payments are returned",
},
cli.BoolFlag{
Name: "paginate_forwards",
Usage: "if set, payments succeeding the " +
"index_offset will be returned, allowing " +
"forwards pagination",
},
cli.BoolFlag{
Name: "count_total_payments",
Usage: "if set, all payments (complete or incomplete, " +
"independent of max_payments parameter) will " +
"be counted; can take a long time on systems " +
"with many payments",
},
cli.Uint64Flag{
Name: "creation_date_start",
Usage: "timestamp in seconds, if set, filter " +
"payments with creation date greater than or " +
"equal to it",
},
cli.Uint64Flag{
Name: "creation_date_end",
Usage: "timestamp in seconds, if set, filter " +
"payments with creation date less than or " +
"equal to it",
},
},
Action: actionDecorator(listPayments),
}
func listPayments(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()
req := &lnrpc.ListPaymentsRequest{
IncludeIncomplete: ctx.Bool("include_incomplete"),
IndexOffset: uint64(ctx.Uint("index_offset")),
MaxPayments: uint64(ctx.Uint("max_payments")),
Reversed: !ctx.Bool("paginate_forwards"),
CountTotalPayments: ctx.Bool("count_total_payments"),
CreationDateStart: ctx.Uint64("creation_date_start"),
CreationDateEnd: ctx.Uint64("creation_date_end"),
}
payments, err := client.ListPayments(ctxc, req)
if err != nil {
return err
}
printRespJSON(payments)
return nil
}
var forwardingHistoryCommand = cli.Command{
Name: "fwdinghistory",
Category: "Payments",
Usage: "Query the history of all forwarded HTLCs.",
ArgsUsage: "start_time [end_time] [index_offset] [max_events]",
Description: `
Query the HTLC switch's internal forwarding log for all completed
payment circuits (HTLCs) over a particular time range (--start_time and
--end_time). The start and end times are meant to be expressed in
seconds since the Unix epoch.
Alternatively negative time ranges can be used, e.g. "-3d". Supports
s(seconds), m(minutes), h(ours), d(ays), w(eeks), M(onths), y(ears).
Month equals 30.44 days, year equals 365.25 days.
If --start_time isn't provided, then 24 hours ago is used. If
--end_time isn't provided, then the current time is used.
The max number of events returned is 50k. The default number is 100,
callers can use the --max_events param to modify this value.
Finally, callers can skip a series of events using the --index_offset
parameter. Each response will contain the offset index of the last
entry. Using this callers can manually paginate within a time slice.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "start_time",
Usage: "the starting time for the query " +
`as unix timestamp or relative e.g. "-1w"`,
},
cli.StringFlag{
Name: "end_time",
Usage: "the end time for the query " +
`as unix timestamp or relative e.g. "-1w"`,
},
cli.Int64Flag{
Name: "index_offset",
Usage: "the number of events to skip",
},
cli.Int64Flag{
Name: "max_events",
Usage: "the max number of events to return",
},
cli.BoolFlag{
Name: "skip_peer_alias_lookup",
Usage: "skip the peer alias lookup per forwarding " +
"event in order to improve performance",
},
},
Action: actionDecorator(forwardingHistory),
}
func forwardingHistory(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()
var (
startTime, endTime uint64
indexOffset, maxEvents uint32
err error
)
args := ctx.Args()
now := time.Now()
switch {
case ctx.IsSet("start_time"):
startTime, err = parseTime(ctx.String("start_time"), now)
case args.Present():
startTime, err = parseTime(args.First(), now)
args = args.Tail()
default:
now := time.Now()
startTime = uint64(now.Add(-time.Hour * 24).Unix())
}
if err != nil {
return fmt.Errorf("unable to decode start_time: %v", err)
}
switch {
case ctx.IsSet("end_time"):
endTime, err = parseTime(ctx.String("end_time"), now)
case args.Present():
endTime, err = parseTime(args.First(), now)
args = args.Tail()
default:
endTime = uint64(now.Unix())
}
if err != nil {
return fmt.Errorf("unable to decode end_time: %v", err)
}
switch {
case ctx.IsSet("index_offset"):
indexOffset = uint32(ctx.Int64("index_offset"))
case args.Present():
i, err := strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode index_offset: %v",
err)
}
indexOffset = uint32(i)
args = args.Tail()
}
switch {
case ctx.IsSet("max_events"):
maxEvents = uint32(ctx.Int64("max_events"))
case args.Present():
m, err := strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode max_events: %v",
err)
}
maxEvents = uint32(m)
}
// By default we will look up the peers' alias information unless the
// skip_peer_alias_lookup flag is specified.
lookupPeerAlias := !ctx.Bool("skip_peer_alias_lookup")
req := &lnrpc.ForwardingHistoryRequest{
StartTime: startTime,
EndTime: endTime,
IndexOffset: indexOffset,
NumMaxEvents: maxEvents,
PeerAliasLookup: lookupPeerAlias,
}
resp, err := client.ForwardingHistory(ctxc, req)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
var buildRouteCommand = cli.Command{
Name: "buildroute",
Category: "Payments",
Usage: "Build a route from a list of hop pubkeys.",
Description: `
Builds a sphinx route for the supplied hops (public keys). Make sure to
use a custom final_cltv_delta to create the route depending on the
restrictions in the invoice otherwise LND will use its default specified
via the bitcoin.timelockdelta setting (default 80).
If the final_cltv_delta mismatch you will likely see the error
INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS returned by the receiving node.
Moreover a payment_addr has to be provided if the invoice supplied it as
well otherwise the payment will be rejected by the receiving node.
`,
Action: actionDecorator(buildRoute),
Flags: []cli.Flag{
cli.Int64Flag{
Name: "amt",
Usage: "the amount to send expressed in satoshis. If" +
"not set, the minimum routable amount is used",
},
cli.Int64Flag{
Name: "final_cltv_delta",
Usage: "number of blocks the last hop has to reveal " +
"the preimage; if not set the default lnd " +
"final_cltv_delta is used",
},
cli.StringFlag{
Name: "hops",
Usage: "comma separated hex pubkeys",
},
cli.Uint64Flag{
Name: "outgoing_chan_id",
Usage: "short channel id of the outgoing channel to " +
"use for the first hop of the payment",
Value: 0,
},
cli.StringFlag{
Name: "payment_addr",
Usage: "hex encoded payment address to set in the " +
"last hop's mpp record",
},
},
}
func buildRoute(ctx *cli.Context) error {
ctxc := getContext()
conn := getClientConn(ctx, false)
defer conn.Close()
client := routerrpc.NewRouterClient(conn)
if !ctx.IsSet("hops") {
return errors.New("hops required")
}
// Build list of hop addresses for the rpc.
hops := strings.Split(ctx.String("hops"), ",")
rpcHops := make([][]byte, 0, len(hops))
for _, k := range hops {
pubkey, err := route.NewVertexFromStr(k)
if err != nil {
return fmt.Errorf("error parsing %v: %v", k, err)
}
rpcHops = append(rpcHops, pubkey[:])
}
var amtMsat int64
hasAmt := ctx.IsSet("amt")
if hasAmt {
amtMsat = ctx.Int64("amt") * 1000
if amtMsat == 0 {
return fmt.Errorf("non-zero amount required")
}
}
var (
payAddr []byte
err error
)
if ctx.IsSet("payment_addr") {
payAddr, err = hex.DecodeString(ctx.String("payment_addr"))
if err != nil {
return fmt.Errorf("error parsing payment_addr: %w", err)
}
}
// Call BuildRoute rpc.
req := &routerrpc.BuildRouteRequest{
AmtMsat: amtMsat,
FinalCltvDelta: int32(ctx.Int64("final_cltv_delta")),
HopPubkeys: rpcHops,
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
PaymentAddr: payAddr,
}
route, err := client.BuildRoute(ctxc, req)
if err != nil {
return err
}
printRespJSON(route)
return nil
}
var deletePaymentsCommand = cli.Command{
Name: "deletepayments",
Category: "Payments",
Usage: "Delete a single or multiple payments from the database.",
ArgsUsage: "--all [--failed_htlcs_only --include_non_failed] | " +
"--payment_hash hash [--failed_htlcs_only]",
Description: `
This command either deletes all failed payments or a single payment from
the database to reclaim disk space.
If the --all flag is used, then all failed payments are removed. If so
desired, _ALL_ payments (even the successful ones) can be deleted
by additionally specifying --include_non_failed.
If a --payment_hash is specified, that single payment is deleted,
independent of its state.
If --failed_htlcs_only is specified then the payments themselves (or the
single payment itself if used with --payment_hash) is not deleted, only
the information about any failed HTLC attempts during the payment.
NOTE: Removing payments from the database does free up disk space within
the internal bbolt database. But that disk space is only reclaimed after
compacting the database. Users might want to turn on auto compaction
(db.bolt.auto-compact=true in the config file or --db.bolt.auto-compact
as a command line flag) and restart lnd after deleting a large number of
payments to see a reduction in the file size of the channel.db file.
`,
Action: actionDecorator(deletePayments),
Flags: []cli.Flag{
cli.BoolFlag{
Name: "all",
Usage: "delete all failed payments",
},
cli.StringFlag{
Name: "payment_hash",
Usage: "delete a specific payment identified by its " +
"payment hash",
},
cli.BoolFlag{
Name: "failed_htlcs_only",
Usage: "only delete failed HTLCs from payments, not " +
"the payment itself",
},
cli.BoolFlag{
Name: "include_non_failed",
Usage: "delete ALL payments, not just the failed ones",
},
},
}
func deletePayments(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()
// Show command help if arguments or no flags are provided.
if ctx.NArg() > 0 || ctx.NumFlags() == 0 {
_ = cli.ShowCommandHelp(ctx, "deletepayments")
return nil
}
var (
paymentHash []byte
all = ctx.Bool("all")
singlePayment = ctx.IsSet("payment_hash")
failedHTLCsOnly = ctx.Bool("failed_htlcs_only")
includeNonFailed = ctx.Bool("include_non_failed")
err error
okMsg = struct {
OK bool `json:"ok"`
}{
OK: true,
}
)
// We pack two RPCs into the same CLI so there are a few non-valid
// combinations of the flags we need to filter out.
switch {
case all && singlePayment:
return fmt.Errorf("cannot use --all and --payment_hash at " +
"the same time")
case singlePayment && includeNonFailed:
return fmt.Errorf("cannot use --payment_hash and " +
"--include_non_failed at the same time, when using " +
"a payment hash the payment is deleted independent " +
"of its state")
}
// Deleting a single payment is implemented in a different RPC than
// removing all/multiple payments.
switch {
case singlePayment:
paymentHash, err = hex.DecodeString(ctx.String("payment_hash"))
if err != nil {
return fmt.Errorf("error decoding payment_hash: %v",
err)
}
_, err = client.DeletePayment(ctxc, &lnrpc.DeletePaymentRequest{
PaymentHash: paymentHash,
FailedHtlcsOnly: failedHTLCsOnly,
})
if err != nil {
return fmt.Errorf("error deleting single payment: %v",
err)
}
case all:
what := "failed"
if includeNonFailed {
what = "all"
}
if failedHTLCsOnly {
what = fmt.Sprintf("failed HTLCs from %s", what)
}
fmt.Printf("Removing %s payments, this might take a while...\n",
what)
_, err = client.DeleteAllPayments(
ctxc, &lnrpc.DeleteAllPaymentsRequest{
FailedPaymentsOnly: !includeNonFailed,
FailedHtlcsOnly: failedHTLCsOnly,
},
)
if err != nil {
return fmt.Errorf("error deleting payments: %v", err)
}
}
// Users are confused by empty JSON outputs so let's return a simple OK
// instead of just printing the empty response RPC message.
printJSON(okMsg)
return nil
}
// ESC is the ASCII code for escape character.
const ESC = 27
// clearCode defines a terminal escape code to clear the currently line and move
// the cursor up.
var clearCode = fmt.Sprintf("%c[%dA%c[2K", ESC, 1, ESC)
// clearLines erases the last count lines in the terminal window.
func clearLines(count int) {
_, _ = fmt.Print(strings.Repeat(clearCode, count))
}
// ordinalNumber returns the ordinal number as a string of a number.
func ordinalNumber(num uint32) string {
switch num {
case 1:
return "1st"
case 2:
return "2nd"
case 3:
return "3rd"
default:
return fmt.Sprintf("%dth", num)
}
}