-
Notifications
You must be signed in to change notification settings - Fork 135
cmd/loop: fix route hint parsing #1209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,26 @@ package main | |
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/lightninglabs/loop/swapserverrpc" | ||
| "github.com/urfave/cli/v3" | ||
| "google.golang.org/protobuf/encoding/protojson" | ||
| ) | ||
|
|
||
| // commaSeparatedStringSlice restores comma parsing for other string slice | ||
| // flags on commands that disable it to preserve route hint JSON. | ||
| func commaSeparatedStringSlice(cmd *cli.Command, name string) []string { | ||
| values := cmd.StringSlice(name) | ||
| result := make([]string, 0, len(values)) | ||
| for _, value := range values { | ||
| result = append(result, strings.Split(value, ",")...) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
|
||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| // showCommandHelp prints help for the current command by delegating to the | ||
| // parent command when available. This ensures help output renders even when | ||
| // invoked from inside a subcommand's action. | ||
|
|
@@ -37,10 +50,10 @@ func validateRouteHints(cmd *cli.Command) ([]*swapserverrpc.RouteHint, error) { | |
|
|
||
| jsonHints := cmd.StringSlice(routeHintsFlag.Name) | ||
|
|
||
| hints := make([]*swapserverrpc.RouteHint, len(jsonHints)) | ||
| hints = make([]*swapserverrpc.RouteHint, len(jsonHints)) | ||
|
hieblmi marked this conversation as resolved.
|
||
| for i, jsonHint := range jsonHints { | ||
| var h swapserverrpc.RouteHint | ||
| err := json.Unmarshal([]byte(jsonHint), &h) | ||
| err := protojson.Unmarshal([]byte(jsonHint), &h) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unable to parse %d-th "+ | ||
| "hint json %v: %w", i, jsonHint, err) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/lightninglabs/loop/swapserverrpc" | ||
| "github.com/stretchr/testify/require" | ||
| "github.com/urfave/cli/v3" | ||
| ) | ||
|
|
||
| // TestRouteHintsCLIParsing verifies that the real commands preserve repeated | ||
| // JSON route hints containing commas and return the decoded values. | ||
| func TestRouteHintsCLIParsing(t *testing.T) { | ||
| const ( | ||
| firstHint = `{"hop_hints":[{"node_id":"node-1",` + | ||
| `"chan_id":1,"fee_base_msat":1000,` + | ||
| `"fee_proportional_millionths":1,` + | ||
| `"cltv_expiry_delta":80}]}` | ||
| secondHint = `{"hopHints":[{"nodeId":"node-2",` + | ||
| `"chanId":2,"feeBaseMsat":2000,` + | ||
| `"feeProportionalMillionths":2,` + | ||
| `"cltvExpiryDelta":81}]}` | ||
| ) | ||
|
|
||
| testCases := []struct { | ||
| name string | ||
| path []string | ||
| sliceFlag string | ||
| }{ | ||
| { | ||
| name: "loop in", | ||
| path: []string{"in"}, | ||
| }, | ||
| { | ||
| name: "quote in", | ||
| path: []string{"quote", "in"}, | ||
| sliceFlag: "deposit_outpoint", | ||
| }, | ||
| { | ||
| name: "static in", | ||
| path: []string{"static", "in"}, | ||
| sliceFlag: "utxo", | ||
| }, | ||
| } | ||
|
|
||
| for _, testCase := range testCases { | ||
| t.Run(testCase.name, func(t *testing.T) { | ||
| rootCommand := newRootCommandForReplay() | ||
| routeHintCommand := commandAtPath( | ||
| t, rootCommand, testCase.path, | ||
| ) | ||
|
|
||
| var ( | ||
| routeHints []*swapserverrpc.RouteHint | ||
| sliceValues []string | ||
| ) | ||
| routeHintCommand.Action = func(_ context.Context, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
|
||
| cmd *cli.Command) error { | ||
|
|
||
| if testCase.sliceFlag != "" { | ||
| sliceValues = commaSeparatedStringSlice( | ||
| cmd, | ||
| testCase.sliceFlag, | ||
| ) | ||
| } | ||
|
|
||
| var err error | ||
| routeHints, err = validateRouteHints(cmd) | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| args := append([]string{"loop"}, testCase.path...) | ||
| args = append( | ||
| args, "--route_hints", firstHint, | ||
| "--route_hints", secondHint, | ||
| ) | ||
| if testCase.sliceFlag != "" { | ||
| args = append( | ||
| args, "--"+testCase.sliceFlag, | ||
| "first:0,second:1", | ||
| ) | ||
| } | ||
|
|
||
| err := rootCommand.Run(t.Context(), args) | ||
| require.NoError(t, err) | ||
| require.Len(t, routeHints, 2) | ||
| if testCase.sliceFlag != "" { | ||
| require.Equal( | ||
| t, []string{"first:0", "second:1"}, | ||
| sliceValues, | ||
| ) | ||
| } | ||
|
|
||
| require.Len(t, routeHints[0].HopHints, 1) | ||
| require.Equal( | ||
| t, "node-1", routeHints[0].HopHints[0].NodeId, | ||
| ) | ||
| require.Equal( | ||
| t, uint64(1), routeHints[0].HopHints[0].ChanId, | ||
| ) | ||
| require.Equal( | ||
| t, uint32(1000), | ||
| routeHints[0].HopHints[0].FeeBaseMsat, | ||
| ) | ||
| require.Equal( | ||
| t, uint32(1), routeHints[0].HopHints[0]. | ||
| FeeProportionalMillionths, | ||
| ) | ||
| require.Equal( | ||
| t, uint32(80), | ||
| routeHints[0].HopHints[0].CltvExpiryDelta, | ||
| ) | ||
|
|
||
| require.Len(t, routeHints[1].HopHints, 1) | ||
| require.Equal( | ||
| t, "node-2", routeHints[1].HopHints[0].NodeId, | ||
| ) | ||
| require.Equal( | ||
| t, uint64(2), routeHints[1].HopHints[0].ChanId, | ||
| ) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestRouteHintsRejectUnknownFields verifies that misspelled protobuf fields | ||
| // fail at the CLI boundary instead of producing zero-valued route hints. | ||
| func TestRouteHintsRejectUnknownFields(t *testing.T) { | ||
| rootCommand := newRootCommandForReplay() | ||
| routeHintCommand := commandAtPath(t, rootCommand, []string{"in"}) | ||
| routeHintCommand.Action = func(_ context.Context, | ||
| cmd *cli.Command) error { | ||
|
|
||
| _, err := validateRouteHints(cmd) | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| err := rootCommand.Run(t.Context(), []string{ | ||
| "loop", "in", "--route_hints", | ||
| `{"hop_hints":[{"node_id":"node-1","chan_id":1,` + | ||
| `"cltv_expiry_delat":80}]}`, | ||
| }) | ||
| require.ErrorContains(t, err, "unknown field") | ||
| } | ||
|
|
||
| // commandAtPath returns a command from root by following path. | ||
| func commandAtPath(t *testing.T, root *cli.Command, | ||
| path []string) *cli.Command { | ||
|
|
||
| t.Helper() | ||
|
|
||
| command := root | ||
| for _, name := range path { | ||
| command = command.Command(name) | ||
| require.NotNil(t, command) | ||
| } | ||
|
|
||
| return command | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,10 @@ | |
|
|
||
| #### Bug Fixes | ||
|
|
||
| * Loop In commands and quotes now correctly parse, validate, and forward | ||
|
hieblmi marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The CLI half is resolved: with the toggle narrowed to three commands, no operator's existing comma-form invocation changes, so there is nothing to warn about there. What the note still omits is the server side — |
||
| explicit JSON route hints supplied by repeating `--route_hints`. Malformed | ||
| or empty hints are rejected before invoice creation. | ||
|
|
||
| * Loop Out requests now account for channel reserves when checking outbound | ||
| capacity, preventing swaps from starting when their off-chain payment cannot | ||
| be funded. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡
F7(Minor) — --route_hints usage text does not describe the input it now requires ·cmd/loop/loopin.go:56This is the first release in which
--route_hintsdoes anything, and disabling the slice separator makes repeating the flag the only way to pass more than one hint, yetrouteHintsFlag'sUsagestring is unchanged and documents neither that nor the accepted protojson object. Add a one-line example of the JSON shape and the repeat-the-flag requirement to the flag definition.