diff --git a/cmd/lncli/cmd_build_route.go b/cmd/lncli/cmd_build_route.go deleted file mode 100644 index 81129b4d1..000000000 --- a/cmd/lncli/cmd_build_route.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "strings" - - "github.com/lightningnetwork/lnd/chainreg" - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/lightningnetwork/lnd/routing/route" - "github.com/urfave/cli" -) - -var buildRouteCommand = cli.Command{ - Name: "buildroute", - Category: "Payments", - Usage: "Build a route from a list of hop pubkeys.", - 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", - Value: chainreg.DefaultBitcoinTimeLockDelta, - }, - 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, - }, - }, -} - -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") - } - } - - // Call BuildRoute rpc. - req := &routerrpc.BuildRouteRequest{ - AmtMsat: amtMsat, - FinalCltvDelta: int32(ctx.Int64("final_cltv_delta")), - HopPubkeys: rpcHops, - OutgoingChanId: ctx.Uint64("outgoing_chan_id"), - } - - route, err := client.BuildRoute(ctxc, req) - if err != nil { - return err - } - - printRespJSON(route) - - return nil -} diff --git a/cmd/lncli/cmd_mc_cfg_get.go b/cmd/lncli/cmd_mc_cfg_get.go deleted file mode 100644 index 2062dcd92..000000000 --- a/cmd/lncli/cmd_mc_cfg_get.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/urfave/cli" -) - -var getCfgCommand = cli.Command{ - Name: "getmccfg", - Usage: "Display mission control's config.", - Description: ` - Returns the config currently being used by mission control. - `, - Action: actionDecorator(getCfg), -} - -func getCfg(ctx *cli.Context) error { - ctxc := getContext() - conn := getClientConn(ctx, false) - defer conn.Close() - - client := routerrpc.NewRouterClient(conn) - - resp, err := client.GetMissionControlConfig( - ctxc, &routerrpc.GetMissionControlConfigRequest{}, - ) - if err != nil { - return err - } - - printRespJSON(resp) - - return nil -} diff --git a/cmd/lncli/cmd_mc_cfg_set.go b/cmd/lncli/cmd_mc_cfg_set.go deleted file mode 100644 index 7e6fcc936..000000000 --- a/cmd/lncli/cmd_mc_cfg_set.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/urfave/cli" -) - -var setCfgCommand = cli.Command{ - Name: "setmccfg", - Usage: "Set mission control's config.", - Description: ` - Update the config values being used by mission control to calculate - the probability that payment routes will succeed. - `, - Flags: []cli.Flag{ - cli.DurationFlag{ - Name: "halflife", - Usage: "the amount of time taken to restore a node " + - "or channel to 50% probability of success.", - }, - cli.Float64Flag{ - Name: "hopprob", - Usage: "the probability of success assigned " + - "to hops that we have no information about", - }, - cli.Float64Flag{ - Name: "weight", - Usage: "the degree to which mission control should " + - "rely on historical results, expressed as " + - "value in [0;1]", - }, cli.UintFlag{ - Name: "pmtnr", - Usage: "the number of payments mission control " + - "should store", - }, - cli.DurationFlag{ - Name: "failrelax", - Usage: "the amount of time to wait after a failure " + - "before raising failure amount", - }, - }, - Action: actionDecorator(setCfg), -} - -func setCfg(ctx *cli.Context) error { - ctxc := getContext() - conn := getClientConn(ctx, false) - defer conn.Close() - - client := routerrpc.NewRouterClient(conn) - - resp, err := client.GetMissionControlConfig( - ctxc, &routerrpc.GetMissionControlConfigRequest{}, - ) - if err != nil { - return err - } - - var haveValue bool - - if ctx.IsSet("halflife") { - haveValue = true - resp.Config.HalfLifeSeconds = uint64(ctx.Duration( - "halflife", - ).Seconds()) - } - - if ctx.IsSet("hopprob") { - haveValue = true - resp.Config.HopProbability = float32(ctx.Float64("hopprob")) - } - - if ctx.IsSet("weight") { - haveValue = true - resp.Config.Weight = float32(ctx.Float64("weight")) - } - - if ctx.IsSet("pmtnr") { - haveValue = true - resp.Config.MaximumPaymentResults = uint32(ctx.Int("pmtnr")) - } - - if ctx.IsSet("failrelax") { - haveValue = true - resp.Config.MinimumFailureRelaxInterval = uint64(ctx.Duration( - "failrelax", - ).Seconds()) - } - - if !haveValue { - return cli.ShowCommandHelp(ctx, "setmccfg") - } - - _, err = client.SetMissionControlConfig( - ctxc, &routerrpc.SetMissionControlConfigRequest{ - Config: resp.Config, - }, - ) - return err -} diff --git a/cmd/lncli/cmd_mission_control.go b/cmd/lncli/cmd_mission_control.go new file mode 100644 index 000000000..92ce1269f --- /dev/null +++ b/cmd/lncli/cmd_mission_control.go @@ -0,0 +1,236 @@ +package main + +import ( + "fmt" + "strconv" + + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/urfave/cli" +) + +var getCfgCommand = cli.Command{ + Name: "getmccfg", + Category: "Mission Control", + Usage: "Display mission control's config.", + Description: ` + Returns the config currently being used by mission control. + `, + Action: actionDecorator(getCfg), +} + +func getCfg(ctx *cli.Context) error { + ctxc := getContext() + conn := getClientConn(ctx, false) + defer conn.Close() + + client := routerrpc.NewRouterClient(conn) + + resp, err := client.GetMissionControlConfig( + ctxc, &routerrpc.GetMissionControlConfigRequest{}, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +var setCfgCommand = cli.Command{ + Name: "setmccfg", + Category: "Mission Control", + Usage: "Set mission control's config.", + Description: ` + Update the config values being used by mission control to calculate + the probability that payment routes will succeed. + `, + Flags: []cli.Flag{ + cli.DurationFlag{ + Name: "halflife", + Usage: "the amount of time taken to restore a node " + + "or channel to 50% probability of success.", + }, + cli.Float64Flag{ + Name: "hopprob", + Usage: "the probability of success assigned " + + "to hops that we have no information about", + }, + cli.Float64Flag{ + Name: "weight", + Usage: "the degree to which mission control should " + + "rely on historical results, expressed as " + + "value in [0;1]", + }, cli.UintFlag{ + Name: "pmtnr", + Usage: "the number of payments mission control " + + "should store", + }, + cli.DurationFlag{ + Name: "failrelax", + Usage: "the amount of time to wait after a failure " + + "before raising failure amount", + }, + }, + Action: actionDecorator(setCfg), +} + +func setCfg(ctx *cli.Context) error { + ctxc := getContext() + conn := getClientConn(ctx, false) + defer conn.Close() + + client := routerrpc.NewRouterClient(conn) + + resp, err := client.GetMissionControlConfig( + ctxc, &routerrpc.GetMissionControlConfigRequest{}, + ) + if err != nil { + return err + } + + var haveValue bool + + if ctx.IsSet("halflife") { + haveValue = true + resp.Config.HalfLifeSeconds = uint64(ctx.Duration( + "halflife", + ).Seconds()) + } + + if ctx.IsSet("hopprob") { + haveValue = true + resp.Config.HopProbability = float32(ctx.Float64("hopprob")) + } + + if ctx.IsSet("weight") { + haveValue = true + resp.Config.Weight = float32(ctx.Float64("weight")) + } + + if ctx.IsSet("pmtnr") { + haveValue = true + resp.Config.MaximumPaymentResults = uint32(ctx.Int("pmtnr")) + } + + if ctx.IsSet("failrelax") { + haveValue = true + resp.Config.MinimumFailureRelaxInterval = uint64(ctx.Duration( + "failrelax", + ).Seconds()) + } + + if !haveValue { + return cli.ShowCommandHelp(ctx, "setmccfg") + } + + _, err = client.SetMissionControlConfig( + ctxc, &routerrpc.SetMissionControlConfigRequest{ + Config: resp.Config, + }, + ) + return err +} + +var queryMissionControlCommand = cli.Command{ + Name: "querymc", + Category: "Mission Control", + Usage: "Query the internal mission control state.", + Action: actionDecorator(queryMissionControl), +} + +func queryMissionControl(ctx *cli.Context) error { + ctxc := getContext() + conn := getClientConn(ctx, false) + defer conn.Close() + + client := routerrpc.NewRouterClient(conn) + + req := &routerrpc.QueryMissionControlRequest{} + snapshot, err := client.QueryMissionControl(ctxc, req) + if err != nil { + return err + } + + printRespJSON(snapshot) + + return nil +} + +var queryProbCommand = cli.Command{ + Name: "queryprob", + Category: "Mission Control", + Usage: "Estimate a success probability.", + ArgsUsage: "from-node to-node amt", + Action: actionDecorator(queryProb), +} + +func queryProb(ctx *cli.Context) error { + ctxc := getContext() + args := ctx.Args() + + if len(args) != 3 { + return cli.ShowCommandHelp(ctx, "queryprob") + } + + fromNode, err := route.NewVertexFromStr(args.Get(0)) + if err != nil { + return fmt.Errorf("invalid from node key: %v", err) + } + + toNode, err := route.NewVertexFromStr(args.Get(1)) + if err != nil { + return fmt.Errorf("invalid to node key: %v", err) + } + + amtSat, err := strconv.ParseUint(args.Get(2), 10, 64) + if err != nil { + return fmt.Errorf("invalid amt: %v", err) + } + + amtMsat := lnwire.NewMSatFromSatoshis( + btcutil.Amount(amtSat), + ) + + conn := getClientConn(ctx, false) + defer conn.Close() + + client := routerrpc.NewRouterClient(conn) + + req := &routerrpc.QueryProbabilityRequest{ + FromNode: fromNode[:], + ToNode: toNode[:], + AmtMsat: int64(amtMsat), + } + + response, err := client.QueryProbability(ctxc, req) + if err != nil { + return err + } + + printRespJSON(response) + + return nil +} + +var resetMissionControlCommand = cli.Command{ + Name: "resetmc", + Category: "Mission Control", + Usage: "Reset internal mission control state.", + Action: actionDecorator(resetMissionControl), +} + +func resetMissionControl(ctx *cli.Context) error { + ctxc := getContext() + conn := getClientConn(ctx, false) + defer conn.Close() + + client := routerrpc.NewRouterClient(conn) + + req := &routerrpc.ResetMissionControlRequest{} + _, err := client.ResetMissionControl(ctxc, req) + return err +} diff --git a/cmd/lncli/cmd_pay.go b/cmd/lncli/cmd_payments.go similarity index 62% rename from cmd/lncli/cmd_pay.go rename to cmd/lncli/cmd_payments.go index dcb7d96be..82808e3fc 100644 --- a/cmd/lncli/cmd_pay.go +++ b/cmd/lncli/cmd_payments.go @@ -18,6 +18,7 @@ import ( "github.com/jedib0t/go-pretty/table" "github.com/jedib0t/go-pretty/text" "github.com/lightninglabs/protobuf-hex-display/jsonpb" + "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntypes" @@ -987,6 +988,550 @@ func sendToRouteRequest(ctx *cli.Context, req *routerrpc.SendToRouteRequest) err 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_chanid", + Usage: "(optional) the channel id of the channel " + + "that must be taken to the first hop", + }, + cltvLimitFlag, + }, + 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() + 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 + } + + 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_chanid"), + } + + route, err := client.QueryRoutes(ctxc, req) + if err != nil { + return err + } + + printRespJSON(route) + return 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. ", + 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", + }, + }, + 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"), + } + + 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", + }, + }, + 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) + args = args.Tail() + } + + req := &lnrpc.ForwardingHistoryRequest{ + StartTime: startTime, + EndTime: endTime, + IndexOffset: indexOffset, + NumMaxEvents: maxEvents, + } + 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.", + 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", + Value: chainreg.DefaultBitcoinTimeLockDelta, + }, + 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, + }, + }, +} + +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") + } + } + + // Call BuildRoute rpc. + req := &routerrpc.BuildRouteRequest{ + AmtMsat: amtMsat, + FinalCltvDelta: int32(ctx.Int64("final_cltv_delta")), + HopPubkeys: rpcHops, + OutgoingChanId: ctx.Uint64("outgoing_chan_id"), + } + + 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 diff --git a/cmd/lncli/cmd_query_mission_control.go b/cmd/lncli/cmd_query_mission_control.go deleted file mode 100644 index 68007205c..000000000 --- a/cmd/lncli/cmd_query_mission_control.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - - "github.com/urfave/cli" -) - -var queryMissionControlCommand = cli.Command{ - Name: "querymc", - Category: "Payments", - Usage: "Query the internal mission control state.", - Action: actionDecorator(queryMissionControl), -} - -func queryMissionControl(ctx *cli.Context) error { - ctxc := getContext() - conn := getClientConn(ctx, false) - defer conn.Close() - - client := routerrpc.NewRouterClient(conn) - - req := &routerrpc.QueryMissionControlRequest{} - snapshot, err := client.QueryMissionControl(ctxc, req) - if err != nil { - return err - } - - printRespJSON(snapshot) - - return nil -} diff --git a/cmd/lncli/cmd_query_probability.go b/cmd/lncli/cmd_query_probability.go deleted file mode 100644 index c941d0890..000000000 --- a/cmd/lncli/cmd_query_probability.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - - "github.com/btcsuite/btcutil" - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/routing/route" - "github.com/urfave/cli" -) - -var queryProbCommand = cli.Command{ - Name: "queryprob", - Category: "Payments", - Usage: "Estimate a success probability.", - ArgsUsage: "from-node to-node amt", - Action: actionDecorator(queryProb), -} - -func queryProb(ctx *cli.Context) error { - ctxc := getContext() - args := ctx.Args() - - if len(args) != 3 { - return cli.ShowCommandHelp(ctx, "queryprob") - } - - fromNode, err := route.NewVertexFromStr(args.Get(0)) - if err != nil { - return fmt.Errorf("invalid from node key: %v", err) - } - - toNode, err := route.NewVertexFromStr(args.Get(1)) - if err != nil { - return fmt.Errorf("invalid to node key: %v", err) - } - - amtSat, err := strconv.ParseUint(args.Get(2), 10, 64) - if err != nil { - return fmt.Errorf("invalid amt: %v", err) - } - - amtMsat := lnwire.NewMSatFromSatoshis( - btcutil.Amount(amtSat), - ) - - conn := getClientConn(ctx, false) - defer conn.Close() - - client := routerrpc.NewRouterClient(conn) - - req := &routerrpc.QueryProbabilityRequest{ - FromNode: fromNode[:], - ToNode: toNode[:], - AmtMsat: int64(amtMsat), - } - - response, err := client.QueryProbability(ctxc, req) - if err != nil { - return err - } - - printRespJSON(response) - - return nil -} diff --git a/cmd/lncli/cmd_reset_mission_control.go b/cmd/lncli/cmd_reset_mission_control.go deleted file mode 100644 index f0fe9aa94..000000000 --- a/cmd/lncli/cmd_reset_mission_control.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - - "github.com/urfave/cli" -) - -var resetMissionControlCommand = cli.Command{ - Name: "resetmc", - Category: "Payments", - Usage: "Reset internal mission control state.", - Action: actionDecorator(resetMissionControl), -} - -func resetMissionControl(ctx *cli.Context) error { - ctxc := getContext() - conn := getClientConn(ctx, false) - defer conn.Close() - - client := routerrpc.NewRouterClient(conn) - - req := &routerrpc.ResetMissionControlRequest{} - _, err := client.ResetMissionControl(ctxc, req) - return err -} diff --git a/cmd/lncli/commands.go b/cmd/lncli/commands.go index fa3a1a8e9..68545d2c5 100644 --- a/cmd/lncli/commands.go +++ b/cmd/lncli/commands.go @@ -14,7 +14,6 @@ import ( "strconv" "strings" "sync" - "time" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -1550,72 +1549,6 @@ func getNodeMetrics(ctx *cli.Context) error { return 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. ", - 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", - }, - }, - 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"), - } - - payments, err := client.ListPayments(ctxc, req) - if err != nil { - return err - } - - printRespJSON(payments) - return nil -} - var getChanInfoCommand = cli.Command{ Name: "getchaninfo", Category: "Graph", @@ -1719,142 +1652,6 @@ func getNodeInfo(ctx *cli.Context) error { 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_chanid", - Usage: "(optional) the channel id of the channel " + - "that must be taken to the first hop", - }, - cltvLimitFlag, - }, - 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() - 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 - } - - 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_chanid"), - } - - route, err := client.QueryRoutes(ctxc, req) - if err != nil { - return err - } - - printRespJSON(route) - return 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 getNetworkInfoCommand = cli.Command{ Name: "getnetworkinfo", Category: "Channels", @@ -2330,131 +2127,6 @@ func updateChannelPolicy(ctx *cli.Context) error { 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", - }, - }, - 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) - args = args.Tail() - } - - req := &lnrpc.ForwardingHistoryRequest{ - StartTime: startTime, - EndTime: endTime, - IndexOffset: indexOffset, - NumMaxEvents: maxEvents, - } - resp, err := client.ForwardingHistory(ctxc, req) - if err != nil { - return err - } - - printRespJSON(resp) - return nil -} - var exportChanBackupCommand = cli.Command{ Name: "exportchanbackup", Category: "Channels", diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go index 05fbbae34..808b5d7eb 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -383,6 +383,7 @@ func main() { versionCommand, profileSubCommand, getStateCommand, + deletePaymentsCommand, } // Add any extra commands determined by build flags. diff --git a/docs/release-notes/release-notes-0.14.0.md b/docs/release-notes/release-notes-0.14.0.md index fd7941c8f..69d24c6c5 100644 --- a/docs/release-notes/release-notes-0.14.0.md +++ b/docs/release-notes/release-notes-0.14.0.md @@ -211,6 +211,11 @@ you. with the `addInvoice` rpc interface. However, now the function has been [exposed in the go package `invoicesrpc`](https://github.com/lightningnetwork/lnd/pull/5697). +* The `DeleteAllPayments` and `DeletePayment` RPC methods can now be called from + the command line with the [new + `lncli deletepayments`](https://github.com/lightningnetwork/lnd/pull/5699) + command. + ## Code Health ### Code cleanup, refactor, typo fixes