cmd/lncli: modify sendtoroute cmd to make reading routes from stdin optional

In this commit we modify the existing sendtoroutes command such that
users can either specify the route over stdin (using the special '-'
flag), via the keyword argument, or via the positional argument.
This commit is contained in:
Olaoluwa Osuntokun 2018-06-06 20:44:41 -07:00
parent bc9eca32ab
commit 0177309dcf
No known key found for this signature in database
GPG Key ID: 964EA263DD637C21

@ -1809,31 +1809,43 @@ var sendToRouteCommand = cli.Command{
Usage: "send a payment over a predefined route", Usage: "send a payment over a predefined route",
Description: ` Description: `
Send a payment over Lightning using a specific route. One must specify Send a payment over Lightning using a specific route. One must specify
a list of routes to attempt and the payment hash. a list of routes to attempt and the payment hash. This command can even
be chained with the response to queryroutes. 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.
The --debug_send flag is provided for usage *purely* in test There are three ways to specify routes:
environments. If specified, then the payment hash isn't required, as * using the --routes parameter to manually specify a JSON encoded
it'll use the hash of all zeroes. This mode allows one to quickly test set of routes in the format of the return value of queryroutes:
payment connectivity without having to create an invoice at the (lncli sendtoroute --payment_hash=<pay_hash> --routes=<route>)
destination.
* passing the routes as a positional argument:
(lncli sendtoroute --payment_hash=pay_hash <route>)
* or reading in the routes from stdin, which can allow chaining the
response from queryroutes, or even read in a file with a set of
pre-computed routes:
(lncli queryroutes --args.. | lncli sendtoroute --payment_hash= -
notice the '-' at the end, which signals that lncli should read
the route in from stdin
`, `,
ArgsUsage: "(lncli queryroutes --dest=<pubkey> --amt=<amt> " +
"| lncli sendtoroute --payment_hash=<payment_hash>)",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{
Name: "payment_hash, r", Name: "payment_hash, pay_hash",
Usage: "the hash to use within the payment's HTLC", Usage: "the hash to use within the payment's HTLC",
}, },
cli.BoolFlag{ cli.StringFlag{
Name: "debug_send", Name: "routes, r",
Usage: "use the debug rHash when sending the HTLC", Usage: "a json array string in the format of the response " +
"of queryroutes that denotes which routes to use",
}, },
}, },
Action: sendToRoute, Action: sendToRoute,
} }
func sendToRoute(ctx *cli.Context) error { func sendToRoute(ctx *cli.Context) error {
// Show command help if no arguments provieded // Show command help if no arguments provided.
if ctx.NArg() == 0 && ctx.NumFlags() == 0 { if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
cli.ShowCommandHelp(ctx, "sendtoroute") cli.ShowCommandHelp(ctx, "sendtoroute")
return nil return nil
@ -1841,48 +1853,65 @@ func sendToRoute(ctx *cli.Context) error {
args := ctx.Args() args := ctx.Args()
b, err := ioutil.ReadAll(os.Stdin) 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 { if err != nil {
return err return err
} }
if len(b) == 0 {
return fmt.Errorf("queryroutes output is empty") if len(rHash) != 32 {
return fmt.Errorf("payment hash must be exactly 32 "+
"bytes, is instead %d", len(rHash))
} }
qroutes := &lnrpc.QueryRoutesResponse{} var jsonRoutes string
err = jsonpb.UnmarshalString(string(b), qroutes) switch {
if err != nil { // The user is specifying the routes explicitly via the key word
return fmt.Errorf("unable to unmarshal json string from "+ // argument.
"incoming array of routes: %v", err) case ctx.IsSet("routes"):
} jsonRoutes = ctx.String("routes")
var rHash []byte // The user is specifying the routes as a positional argument.
if ctx.Bool("debug_send") && case args.Present() && args.First() != "-":
(ctx.IsSet("payment_hash") || args.Present()) { jsonRoutes = args.First()
return fmt.Errorf("do not provide a payment hash with debug send")
} else if !ctx.Bool("debug_send") {
switch {
case ctx.IsSet("payment_hash"):
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
case args.Present():
rHash, err = hex.DecodeString(args.First())
default:
return fmt.Errorf("payment hash argument missing")
}
// 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 { if err != nil {
return err return err
} }
if len(b) == 0 {
if len(rHash) != 32 { return fmt.Errorf("queryroutes output is empty")
return fmt.Errorf("payment hash must be exactly 32 "+
"bytes, is instead %d", len(rHash))
} }
jsonRoutes = string(b)
}
routes := &lnrpc.QueryRoutesResponse{}
err = jsonpb.UnmarshalString(jsonRoutes, routes)
if err != nil {
return fmt.Errorf("unable to unmarshal json string "+
"from incoming array of routes: %v", err)
} }
req := &lnrpc.SendToRouteRequest{ req := &lnrpc.SendToRouteRequest{
PaymentHash: rHash, PaymentHash: rHash,
Routes: qroutes.Routes, Routes: routes.Routes,
} }
return sendToRouteRequest(ctx, req) return sendToRouteRequest(ctx, req)
@ -1906,8 +1935,6 @@ func sendToRouteRequest(ctx *cli.Context, req *lnrpc.SendToRouteRequest) error {
return err return err
} }
paymentStream.CloseSend()
printJSON(struct { printJSON(struct {
E string `json:"payment_error"` E string `json:"payment_error"`
P string `json:"payment_preimage"` P string `json:"payment_preimage"`