Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions cmd/loop/loopin.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ var (
}

loopInCommand = &cli.Command{
Name: "in",
Usage: "perform an on-chain to off-chain swap (loop in)",
ArgsUsage: "amt",
Name: "in",
Usage: "perform an on-chain to off-chain swap (loop in)",
ArgsUsage: "amt",
DisableSliceFlagSeparator: true,

Copy link
Copy Markdown

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:56

This is the first release in which --route_hints does anything, and disabling the slice separator makes repeating the flag the only way to pass more than one hint, yet routeHintsFlag's Usage string 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.

Description: `
Send the amount in satoshis specified by the amt argument
off-chain.
Expand Down
11 changes: 7 additions & 4 deletions cmd/loop/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ var quoteCommand = &cli.Command{
}

var quoteInCommand = &cli.Command{
Name: "in",
Usage: "get a quote for the cost of a loop in swap",
ArgsUsage: "amt",
Name: "in",
Usage: "get a quote for the cost of a loop in swap",
ArgsUsage: "amt",
DisableSliceFlagSeparator: true,
Comment thread
hieblmi marked this conversation as resolved.
Description: "Allows to determine the cost of a swap up front." +
"Either specify an amount or deposit outpoints.",
Flags: []cli.Flag{
Expand Down Expand Up @@ -86,7 +87,9 @@ func quoteIn(ctx context.Context, cmd *cli.Command) error {
}

if cmd.IsSet("deposit_outpoint") {
depositOutpoints = cmd.StringSlice("deposit_outpoint")
depositOutpoints = commaSeparatedStringSlice(
cmd, "deposit_outpoint",
)
depositAmt, err = depositAmount(ctx, client, depositOutpoints)
if err != nil {
return err
Expand Down
9 changes: 5 additions & 4 deletions cmd/loop/staticaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,9 +416,10 @@ func summary(ctx context.Context, cmd *cli.Command) error {
}

var staticAddressLoopInCommand = &cli.Command{
Name: "in",
Usage: "Loop in funds from static address deposits.",
ArgsUsage: "[amt] [--all | --utxo xxx:xx]",
Name: "in",
Usage: "Loop in funds from static address deposits.",
ArgsUsage: "[amt] [--all | --utxo xxx:xx]",
DisableSliceFlagSeparator: true,
Description: `
Requests a loop-in swap based on static address deposits. After the
creation of a static address funds can be sent to it. Once the funds are
Expand Down Expand Up @@ -568,7 +569,7 @@ func staticAddressLoopIn(ctx context.Context, cmd *cli.Command) error {
depositOutpoints = depositsToOutpoints(allDeposits)

case isUtxoSelected:
depositOutpoints = cmd.StringSlice("utxo")
depositOutpoints = commaSeparatedStringSlice(cmd, "utxo")

case selectedAmount > 0:
// If only an amount is selected, we will trigger coin
Expand Down
19 changes: 16 additions & 3 deletions cmd/loop/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ",")...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 F8 (Minor) — Empty and untrimmed tokens reach deposit-outpoint selection · cmd/loop/utils.go:19

strings.Split keeps empty fields and does not trim, so --utxo "a:0," yields an "" entry and --deposit_outpoint "a:0, b:1" yields " b:1"; both flow through containsDuplicates() into QuoteRequest.DepositOutpoints / StaticAddressLoopInRequest.Outpoints, where an outpoint the server cannot resolve quietly shrinks the quoted deposit total instead of erroring, because depositAmount only sums FilteredDeposits. I cannot confirm whether urfave's native splitting behaved identically, but this helper is now the single point every slice flag on these three commands must go through, so it is the right place to trim and drop empties.

}

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.
Expand Down Expand Up @@ -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))
Comment thread
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)
Expand Down
161 changes: 161 additions & 0 deletions cmd/loop/utils_test.go
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 F9 (Minor) — Test leaves a stub Action on shared command singletons · cmd/loop/utils_test.go:58

loopInCommand, quoteInCommand and staticAddressLoopInCommand are package-level vars, so unless newRootCommandForReplay returns copies — its definition is not in the provided context — this assignment leaves the stub Action in package state for any later test in package main that runs those commands. Restoring the original with t.Cleanup removes the coupling.

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
}
4 changes: 4 additions & 0 deletions docs/release-notes/release-notes-next.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

#### Bug Fixes

* Loop In commands and quotes now correctly parse, validate, and forward
Comment thread
hieblmi marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 F5 (Minor) · docs/release-notes/release-notes-next.md:19 · partially_addressed

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 — unmarshallRouteHints now rejects route hints that LoopIn, Probe and GetLoopInQuote previously accepted (F6), which affects every gRPC client and not just loop. "Malformed or empty hints are rejected before invoice creation" under Bug Fixes reads as client-side hygiene; the file already has a Breaking Changes section carrying exactly this kind of entry.

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.
Expand Down
Loading
Loading