Skip to content

fix(trader): align ACP wire shape with trader-service (expiry, volume, status)#7

Closed
vrogojin wants to merge 3 commits intofix/encrypt-decrypt-namespacefrom
fix/trader-acp-wire-shape
Closed

fix(trader): align ACP wire shape with trader-service (expiry, volume, status)#7
vrogojin wants to merge 3 commits intofix/encrypt-decrypt-namespacefrom
fix/trader-acp-wire-shape

Conversation

@vrogojin
Copy link
Copy Markdown
Contributor

@vrogojin vrogojin commented May 3, 2026

Summary

Three upstream wire-shape mismatches between sphere-cli's sphere trader … namespace and trader-service's actual ACP-0 wire schema. All three surfaced as live-testnet failures in trader-service's HMA-orchestrated e2e suite (vrogojin/trader-service#15) and block the full create-intent → cancel-intent → settlement flow from being testable end-to-end.

Base: fix/encrypt-decrypt-namespace (PR #6). Re-target to main once #6 lands.

Three fixes

1. --volume-total--volume-max flag

The trader's CREATE_INTENT ACP param is volume_max per trader-service/src/trader/acp-types.ts:23. sphere-cli was sending volume_total over the wire which the trader silently dropped. Renames CLI flag, internal opt name, and wire param.

2. expiry_ms wire param → expiry_sec

The trader validates params['expiry_sec'] as a finite positive integer ≤ 7 days (trader-service/src/trader/trader-command-handler.ts:331-342). sphere-cli was sending expiry_ms which the trader rejected with INVALID_PARAM ("expiry_sec must be a finite number").

The --expiry-ms CLI flag stays (ergonomically consistent with other timeout flags); we convert at the wire boundary: Math.floor(opts.expiryMs / 1000). Sub-1000ms expiries floor to 0 and fail the trader's positive-int check — correctly, since sub-second expiries make no sense for trade intents.

3. sphere trader status removed

STATUS is SYSTEM-scoped per the Unicity architecture: system commands (STATUS, SHUTDOWN_GRACEFUL, SET_LOG_LEVEL, EXEC) route through the tenant's host manager via HMCP, not direct controller→tenant ACP. The trader correctly rejected direct STATUS calls with UNAUTHORIZED ("System command STATUS can only be sent by the host manager").

The subcommand is removed; controllers should use sphere host inspect <instance> (HMCP) for trader liveness probes, or rely on sphere trader portfolio / list-intents succeeding as an implicit liveness signal.

The subcommand-tree test renames from "exposes all 7 expected subcommands" to "exposes the 6 controller-scoped trader subcommands" with a comment documenting the architectural distinction.

Verification

$ npm run check                       → 95/95 tests passing
$ node bin/sphere.mjs trader --help   → 6 subcommands (no status)

Live-testnet verification downstream (vrogojin/trader-service#15 with this branch's built sphere-cli):

$ TRADER_E2E_SKIP_PREFLIGHT=1 npx vitest run --config vitest.e2e-live.config.ts \
    test/e2e-live/hma-trade-flow.e2e-live.test.ts
✓ drives the full trader CLI surface against HMA-spawned tenants (48s)

The full flow now passes: HMA spawns escrow + 2 traders → set-strategy on each → portfolio reads → list-intents (empty) → create-intent on Alice → list-intents (CREATED) → cancel-intent → list-intents (CANCELLED).

Test plan

Out of scope

  • Faucet-funded swap settlement test — separate trader-service PR after this lands.
  • Adding STATUS routing through HMCP (i.e. sphere host inspect's implementation) — that's already wired in sphere-cli's host namespace.

Vladimir Rogojin added 3 commits May 4, 2026 00:28
…, status)

Three upstream-discovered mismatches between sphere-cli's `sphere
trader …` namespace and trader-service's actual ACP-0 wire schema.
All three surfaced as failures in trader-service's HMA-orchestrated
e2e tests (vrogojin/trader-service#15) and block the full
create-intent → cancel-intent → settlement flow from being
testable end-to-end.

1. `--volume-total` flag → `--volume-max` flag.

   The trader's CREATE_INTENT ACP param is `volume_max`
   (`/home/vrogojin/trader-service/src/trader/acp-types.ts:23`).
   sphere-cli was sending `volume_total` over the wire, which the
   trader silently dropped (parameter not in the schema). This
   PR was already partially staged in the working tree of the
   `fix/encrypt-decrypt-namespace` branch — landing it cleanly here.

2. `expiry_ms` wire param → `expiry_sec`.

   The trader validates `params['expiry_sec']` as a finite positive
   integer ≤ 7 days
   (`trader-service/src/trader/trader-command-handler.ts:331-342`).
   sphere-cli was sending `expiry_ms` which the trader rejected
   with INVALID_PARAM ("expiry_sec must be a finite number"). The
   `--expiry-ms` CLI flag stays — converting to seconds at the
   wire boundary keeps the flag ergonomic (matches other timeout
   flags) while fixing the wire shape. `Math.floor(ms / 1000)` so
   sub-1000ms expiries floor to 0 and fail the trader's
   positive-int check (correctly: sub-second expiries make no
   sense for trade intents).

3. `sphere trader status` removed.

   STATUS is a SYSTEM-scoped ACP command per the Unicity
   architecture (system commands like STATUS / SHUTDOWN_GRACEFUL
   / SET_LOG_LEVEL / EXEC route through the tenant's host manager
   via HMCP, not direct controller→tenant ACP). The trader
   correctly rejects direct STATUS calls with UNAUTHORIZED. The
   subcommand is removed; controllers should use
   `sphere host inspect <instance>` (HMCP) for trader liveness
   probes, or rely on `sphere trader portfolio` / `list-intents`
   succeeding as an implicit liveness signal.

   The trader-commands.test.ts subcommand-tree assertion drops
   `status` and renames the test to "exposes the 6
   controller-scoped trader subcommands" with a comment
   explaining the architectural distinction.

Verified:
  $ npm run check                       → 95/95 tests passing
  $ node bin/sphere.mjs trader --help   → 6 subcommands listed (no status)

Verified downstream against live testnet:
  $ cd /home/vrogojin/trader-service
  $ TRADER_E2E_SKIP_PREFLIGHT=1 npx vitest run --config vitest.e2e-live.config.ts \
      test/e2e-live/hma-trade-flow.e2e-live.test.ts
  ✓ drives the full trader CLI surface against HMA-spawned tenants (48s)
  Set-strategy + portfolio + list-intents + create-intent +
  list-intents + cancel-intent + verify-CANCELLED — all green.

Depends on PR #6 (encrypt/decrypt L1 fix) for the typecheck baseline.
Steelman round 1 on PR #7 found three issues:

WARNING — Sub-1000ms expiry passed the CLI's `n <= 0` guard, then
`Math.floor(n / 1000) = 0`, then the trader rejected with the opaque
"expiry_sec must be positive" — confusing UX for what was a
positive millisecond value. Add CLI-layer guard `n < 1000` with
clear message before the conversion. Same approach for the 7-day
upper bound (matches trader's own validation; saves a network
round-trip on misuse).

NOTE — No unit test for the wire conversion (`expiry_ms`→`expiry_sec`
or `--volume-max` → `volume_max`). The PR's correctness rested
entirely on the live e2e test in trader-service. Refactor:
extract `buildCreateIntentParams(opts): { params } | { error }` as
a pure function. The runWithTransport wrapper now just calls it,
emits stderr on error-shape, and forwards on success. Add 10
targeted unit tests covering:
  - volume_max present (not volume_total)
  - --expiry-ms 5000 → expiry_sec=5
  - --expiry-ms 90500 → 90 (floor)
  - omit --expiry-ms → no expiry_sec / expiry_ms in payload
  - --expiry-ms 999 → error mentioning "1 second"
  - --expiry-ms > 7d → error mentioning "7 days"
  - boundary cases (exactly 1000, exactly 7 days) — accepted
  - non-numeric/zero/negative — rejected
  - invalid direction — rejected
  - bigint-string fields pass through verbatim (no precision loss)

NOTE (carry-forward) — trader-service has its own CLI shim
(src/cli/main.ts) that ALSO had volume_total/expiry_ms — fixed in
trader-service PR (vrogojin/trader-service#15) so direct-docker
tests don't break either. Cross-repo coverage now consistent.

Verified: 105/105 tests pass (was 95, +10 new wire-shape unit
tests). Live e2e in trader-service still passes against testnet.
Round 2 of steelman loop on PR #7 returned ROUND CLEAN. One
NOTE-level test gap was worth adding as polish:

`Number.parseInt('1e6', 10)` returns 1 (parses '1', stops at 'e')
rather than 1_000_000. A user passing `--expiry-ms 1e6` wanted
1 000 000 ms (16.7 minutes) but gets the truncated value 1, which
the < 1000ms guard then rejects with a confusing message saying
"got 1". The value is REJECTED (not silently misused), so this
isn't a correctness bug — but the surprising parse behavior is
worth pinning in a test so a future strict-parse refactor is
tracked as a deliberate change.

The two WARNING-level findings from round 2 (parseInt prefix-
truncation accepts `'1000abc'` as 1000) are a pre-existing
file-wide pattern affecting --limit, --max-concurrent, --timeout
in addition to --expiry-ms. Tracked as a follow-up: introduce
a strict-positive-int helper and apply uniformly. Out of scope
for this PR.

Verified: 106/106 tests pass (was 105, +1 new pinning test).
@vrogojin vrogojin deleted the branch fix/encrypt-decrypt-namespace May 4, 2026 06:17
@vrogojin vrogojin closed this May 4, 2026
vrogojin pushed a commit that referenced this pull request May 4, 2026
Steelman round 1 on PR #7 found three issues:

WARNING — Sub-1000ms expiry passed the CLI's `n <= 0` guard, then
`Math.floor(n / 1000) = 0`, then the trader rejected with the opaque
"expiry_sec must be positive" — confusing UX for what was a
positive millisecond value. Add CLI-layer guard `n < 1000` with
clear message before the conversion. Same approach for the 7-day
upper bound (matches trader's own validation; saves a network
round-trip on misuse).

NOTE — No unit test for the wire conversion (`expiry_ms`→`expiry_sec`
or `--volume-max` → `volume_max`). The PR's correctness rested
entirely on the live e2e test in trader-service. Refactor:
extract `buildCreateIntentParams(opts): { params } | { error }` as
a pure function. The runWithTransport wrapper now just calls it,
emits stderr on error-shape, and forwards on success. Add 10
targeted unit tests covering:
  - volume_max present (not volume_total)
  - --expiry-ms 5000 → expiry_sec=5
  - --expiry-ms 90500 → 90 (floor)
  - omit --expiry-ms → no expiry_sec / expiry_ms in payload
  - --expiry-ms 999 → error mentioning "1 second"
  - --expiry-ms > 7d → error mentioning "7 days"
  - boundary cases (exactly 1000, exactly 7 days) — accepted
  - non-numeric/zero/negative — rejected
  - invalid direction — rejected
  - bigint-string fields pass through verbatim (no precision loss)

NOTE (carry-forward) — trader-service has its own CLI shim
(src/cli/main.ts) that ALSO had volume_total/expiry_ms — fixed in
trader-service PR (vrogojin/trader-service#15) so direct-docker
tests don't break either. Cross-repo coverage now consistent.

Verified: 105/105 tests pass (was 95, +10 new wire-shape unit
tests). Live e2e in trader-service still passes against testnet.
vrogojin pushed a commit that referenced this pull request May 4, 2026
Round 2 of steelman loop on PR #7 returned ROUND CLEAN. One
NOTE-level test gap was worth adding as polish:

`Number.parseInt('1e6', 10)` returns 1 (parses '1', stops at 'e')
rather than 1_000_000. A user passing `--expiry-ms 1e6` wanted
1 000 000 ms (16.7 minutes) but gets the truncated value 1, which
the < 1000ms guard then rejects with a confusing message saying
"got 1". The value is REJECTED (not silently misused), so this
isn't a correctness bug — but the surprising parse behavior is
worth pinning in a test so a future strict-parse refactor is
tracked as a deliberate change.

The two WARNING-level findings from round 2 (parseInt prefix-
truncation accepts `'1000abc'` as 1000) are a pre-existing
file-wide pattern affecting --limit, --max-concurrent, --timeout
in addition to --expiry-ms. Tracked as a follow-up: introduce
a strict-positive-int helper and apply uniformly. Out of scope
for this PR.

Verified: 106/106 tests pass (was 105, +1 new pinning test).
vrogojin added a commit that referenced this pull request May 4, 2026
…, drop status) (#8)

* fix(trader): align ACP wire shape with trader-service (expiry, volume, status)

Three upstream-discovered mismatches between sphere-cli's `sphere
trader …` namespace and trader-service's actual ACP-0 wire schema.
All three surfaced as failures in trader-service's HMA-orchestrated
e2e tests (vrogojin/trader-service#15) and block the full
create-intent → cancel-intent → settlement flow from being
testable end-to-end.

1. `--volume-total` flag → `--volume-max` flag.

   The trader's CREATE_INTENT ACP param is `volume_max`
   (`/home/vrogojin/trader-service/src/trader/acp-types.ts:23`).
   sphere-cli was sending `volume_total` over the wire, which the
   trader silently dropped (parameter not in the schema). This
   PR was already partially staged in the working tree of the
   `fix/encrypt-decrypt-namespace` branch — landing it cleanly here.

2. `expiry_ms` wire param → `expiry_sec`.

   The trader validates `params['expiry_sec']` as a finite positive
   integer ≤ 7 days
   (`trader-service/src/trader/trader-command-handler.ts:331-342`).
   sphere-cli was sending `expiry_ms` which the trader rejected
   with INVALID_PARAM ("expiry_sec must be a finite number"). The
   `--expiry-ms` CLI flag stays — converting to seconds at the
   wire boundary keeps the flag ergonomic (matches other timeout
   flags) while fixing the wire shape. `Math.floor(ms / 1000)` so
   sub-1000ms expiries floor to 0 and fail the trader's
   positive-int check (correctly: sub-second expiries make no
   sense for trade intents).

3. `sphere trader status` removed.

   STATUS is a SYSTEM-scoped ACP command per the Unicity
   architecture (system commands like STATUS / SHUTDOWN_GRACEFUL
   / SET_LOG_LEVEL / EXEC route through the tenant's host manager
   via HMCP, not direct controller→tenant ACP). The trader
   correctly rejects direct STATUS calls with UNAUTHORIZED. The
   subcommand is removed; controllers should use
   `sphere host inspect <instance>` (HMCP) for trader liveness
   probes, or rely on `sphere trader portfolio` / `list-intents`
   succeeding as an implicit liveness signal.

   The trader-commands.test.ts subcommand-tree assertion drops
   `status` and renames the test to "exposes the 6
   controller-scoped trader subcommands" with a comment
   explaining the architectural distinction.

Verified:
  $ npm run check                       → 95/95 tests passing
  $ node bin/sphere.mjs trader --help   → 6 subcommands listed (no status)

Verified downstream against live testnet:
  $ cd /home/vrogojin/trader-service
  $ TRADER_E2E_SKIP_PREFLIGHT=1 npx vitest run --config vitest.e2e-live.config.ts \
      test/e2e-live/hma-trade-flow.e2e-live.test.ts
  ✓ drives the full trader CLI surface against HMA-spawned tenants (48s)
  Set-strategy + portfolio + list-intents + create-intent +
  list-intents + cancel-intent + verify-CANCELLED — all green.

Depends on PR #6 (encrypt/decrypt L1 fix) for the typecheck baseline.

* review: round 1 steelman fixes (CLI guards, extracted pure fn, tests)

Steelman round 1 on PR #7 found three issues:

WARNING — Sub-1000ms expiry passed the CLI's `n <= 0` guard, then
`Math.floor(n / 1000) = 0`, then the trader rejected with the opaque
"expiry_sec must be positive" — confusing UX for what was a
positive millisecond value. Add CLI-layer guard `n < 1000` with
clear message before the conversion. Same approach for the 7-day
upper bound (matches trader's own validation; saves a network
round-trip on misuse).

NOTE — No unit test for the wire conversion (`expiry_ms`→`expiry_sec`
or `--volume-max` → `volume_max`). The PR's correctness rested
entirely on the live e2e test in trader-service. Refactor:
extract `buildCreateIntentParams(opts): { params } | { error }` as
a pure function. The runWithTransport wrapper now just calls it,
emits stderr on error-shape, and forwards on success. Add 10
targeted unit tests covering:
  - volume_max present (not volume_total)
  - --expiry-ms 5000 → expiry_sec=5
  - --expiry-ms 90500 → 90 (floor)
  - omit --expiry-ms → no expiry_sec / expiry_ms in payload
  - --expiry-ms 999 → error mentioning "1 second"
  - --expiry-ms > 7d → error mentioning "7 days"
  - boundary cases (exactly 1000, exactly 7 days) — accepted
  - non-numeric/zero/negative — rejected
  - invalid direction — rejected
  - bigint-string fields pass through verbatim (no precision loss)

NOTE (carry-forward) — trader-service has its own CLI shim
(src/cli/main.ts) that ALSO had volume_total/expiry_ms — fixed in
trader-service PR (vrogojin/trader-service#15) so direct-docker
tests don't break either. Cross-repo coverage now consistent.

Verified: 105/105 tests pass (was 95, +10 new wire-shape unit
tests). Live e2e in trader-service still passes against testnet.

* review: round 2 polish — pin parseInt('1e6') surprise behavior

Round 2 of steelman loop on PR #7 returned ROUND CLEAN. One
NOTE-level test gap was worth adding as polish:

`Number.parseInt('1e6', 10)` returns 1 (parses '1', stops at 'e')
rather than 1_000_000. A user passing `--expiry-ms 1e6` wanted
1 000 000 ms (16.7 minutes) but gets the truncated value 1, which
the < 1000ms guard then rejects with a confusing message saying
"got 1". The value is REJECTED (not silently misused), so this
isn't a correctness bug — but the surprising parse behavior is
worth pinning in a test so a future strict-parse refactor is
tracked as a deliberate change.

The two WARNING-level findings from round 2 (parseInt prefix-
truncation accepts `'1000abc'` as 1000) are a pre-existing
file-wide pattern affecting --limit, --max-concurrent, --timeout
in addition to --expiry-ms. Tracked as a follow-up: introduce
a strict-positive-int helper and apply uniformly. Out of scope
for this PR.

Verified: 106/106 tests pass (was 105, +1 new pinning test).

---------

Co-authored-by: Vladimir Rogojin <vrogojin@blockyinnovations.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant