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
60 changes: 60 additions & 0 deletions cmd/obol/accept_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/ObolNetwork/obol-stack/internal/schemas"
"github.com/urfave/cli/v3"
)

const testPayTo = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
Expand Down Expand Up @@ -200,3 +201,62 @@ func TestBuildAcceptPayments(t *testing.T) {
t.Fatalf("expected duplicate error, got %v", err)
}
}

// TestSellAccept_CommaSeparatorDisabled is the regression guard for the
// multi-currency --accept bug: cli/v3 StringSliceFlag splits values on ","
// by default, so "--accept token=USDC,network=base,price=1" was shredded into
// three fragments and parsing failed with "network is required". The unit
// tests above never caught it because they call parseAcceptOption /
// buildAcceptPayments directly, bypassing cli/v3 argv parsing entirely.
func TestSellAccept_CommaSeparatorDisabled(t *testing.T) {
// (1) Structural guard: every sell command carrying --accept must keep the
// separator disabled, or multi-currency offers silently break again.
cfg := newTestConfig(t)
for _, name := range []string{"http", "agent", "update"} {
sub := findSubcommand(t, sellCommand(cfg), name)
if !sub.DisableSliceFlagSeparator {
t.Errorf("sell %s: DisableSliceFlagSeparator must be true so a comma-joined --accept stays one value", name)
}
}

// (2) Behavioral: drive real cli/v3 argv parsing. With the separator
// disabled each --accept arrives whole; with the default separator the same
// argv shreds, proving the field is load-bearing (not a no-op).
run := func(disable bool) []string {
var got []string
cmd := &cli.Command{
Name: "x",
DisableSliceFlagSeparator: disable,
Flags: []cli.Flag{&cli.StringSliceFlag{Name: "accept"}},
Action: func(_ context.Context, c *cli.Command) error {
got = c.StringSlice("accept")
return nil
},
}
err := cmd.Run(context.Background(), []string{
"x",
"--accept", "token=USDC,network=base,price=1",
"--accept", "token=OBOL,network=ethereum,price=10",
})
if err != nil {
t.Fatalf("run(disable=%v): %v", disable, err)
}
return got
}

whole := run(true)
if len(whole) != 2 {
t.Fatalf("disabled separator: got %d values %q, want 2 whole options", len(whole), whole)
}
if _, err := buildAcceptPayments(whole, testPayTo); err != nil {
t.Fatalf("whole --accept values should build payments, got: %v", err)
}

shredded := run(false)
if len(shredded) == 2 {
t.Fatal("default cli/v3 separator unexpectedly kept --accept whole; the fix may be a no-op")
}
if _, err := buildAcceptPayments(shredded, testPayTo); err == nil {
t.Error("shredded --accept fragments should fail to build payments (the original bug)")
}
}
11 changes: 11 additions & 0 deletions cmd/obol/sell.go
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,14 @@ func sellHTTPCommand(cfg *config.Config) *cli.Command {
Name: "http",
Usage: "Sell any local HTTP service with x402 payments",
ArgsUsage: "<name>",
// --accept carries a comma-separated key=value list per option
// (token=USDC,network=base,price=1). cli/v3 slice flags split on ","
// by default, which would shred one option into several fragments and
// fail with "network is required". Disable the separator so each
// --accept is one whole value; parseAcceptKV does its own "," split.
// The repeatable --register-skills/-domains/-metadata flags are
// unaffected in practice (each value is a single token / key=value).
DisableSliceFlagSeparator: true,
Description: `Publishes a payment gated HTTP API to any service within the stack.
By default it also registers the seller agent on ERC-8004 after the route is live.
Use --no-register to skip the on-chain registration step.
Expand Down Expand Up @@ -2732,6 +2740,9 @@ func sellUpdateCommand(cfg *config.Config) *cli.Command {
Name: "update",
Usage: "Update pricing or wallet on an existing ServiceOffer in place",
ArgsUsage: "<name>",
// See sell http: keep one --accept value intact (no cli/v3 "," split)
// so multi-currency updates parse. parseAcceptKV splits internally.
DisableSliceFlagSeparator: true,
Description: `Patches a live ServiceOffer without deleting it. Only the fields you pass
are changed; everything else is preserved. The serviceoffer-controller will
reconcile the new payment config automatically.
Expand Down
3 changes: 3 additions & 0 deletions cmd/obol/sell_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ func sellAgentCommand(cfg *config.Config) *cli.Command {
Name: "agent",
Usage: "Gate an existing Obol Stack agent with x402 payments",
ArgsUsage: "<name>",
// See sell http: keep one --accept value intact (no cli/v3 "," split)
// so multi-currency offers parse. parseAcceptKV splits internally.
DisableSliceFlagSeparator: true,
Description: `Wraps an existing Agent (created with ` + "`obol agent new <name>`" + `) with a
ServiceOffer of type=agent. The serviceoffer-controller resolves
spec.agent.ref into the agent's cluster endpoint, surfaces the agent's
Expand Down
Loading