From d904facc2db92c7bfcc0c85ee248416d2b177d18 Mon Sep 17 00:00:00 2001 From: Vladimir Rogojin Date: Wed, 10 Jun 2026 23:19:19 +0200 Subject: [PATCH 1/2] fix(market): pass intent price as decimal-string bigint, not Number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit intent-engine.ts:908 computed the bigint midpoint of the trader's rate band and cast to Number before calling MarketModule.postIntent. For a typical 18-decimal quote asset, the midpoint of a realistic rate band (e.g. 0.08–0.12 ETH/UCT) is 1e17 = 100_000_000_000_000_000 — past Number.MAX_SAFE_INTEGER (2^53 ≈ 9e15). The IEEE 754 double stored a close-but-not-exact value, JSON serialised it as a number, and the market-api server rejected with HTTP 500 (non-JSON body, so the trader only logged the opaque status code with no actionable diagnostic). Fix: - intent-engine.ts: drop `Number(...)` cast; pass `midpointRate.toString()` - types.ts MarketPostRequest.price: number → string - types.ts MarketSearchResult.price: number → string - types.ts MarketSearchFilters.{min,max}Price: number → string Aligns with sphere-sdk PR #483 which makes the same change on the SDK side (PostIntentRequest.price, SearchIntentResult.price, SearchFilters.{min,max}Price all become `string`). This is the established bigint serialisation convention everywhere else in the SDK (TXF amount fields, transfer payloads, all token amounts) — bigint internally, decimal-string on the wire. Surfaced by: unicity-sphere/sphere-sdk#475 (trader-roundtrip soak) §6 which failed with `HTTP 500 — unexpected response (not JSON)` on two consecutive runs. Tests: e2e/trader-intent-lifecycle.e2e.test.ts T1.1 expectation updated from `postCall.price === 475` to `postCall.price === '475'`. 698/698 test suite passes. The market-api server SHOULD accept string prices (MarketIntent.price in the SDK was ALREADY `string`, so the read shape uses strings — the server clearly round-trips strings on reads). If a server-side change is also needed, that's a follow-up; this is the client-side correctness fix. Related: sphere-sdk PR #483 (SDK type alignment). --- src/trader/intent-engine.ts | 13 +++++++++++-- src/trader/types.ts | 20 ++++++++++++++++---- test/e2e/trader-intent-lifecycle.e2e.test.ts | 6 ++++-- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/trader/intent-engine.ts b/src/trader/intent-engine.ts index cbf5d4c..c362cef 100644 --- a/src/trader/intent-engine.ts +++ b/src/trader/intent-engine.ts @@ -893,7 +893,16 @@ export function createIntentEngine(deps: IntentEngineDeps): IntentEngine { // Transition DRAFT → ACTIVE (validates transition) // Build description for market posting const description = encodeDescription(tradingIntent); - const midpointRate = Number((rateMin + rateMax) / 2n); + // Bigint midpoint of the rate band, serialized to a decimal + // string for the wire. NEVER cast to Number — for a typical + // 18-decimal quote asset, the midpoint of a 0.08–0.12 band is + // 1e17 which is past Number.MAX_SAFE_INTEGER (≈9e15). Coercing + // loses precision, the market-api server rejects with HTTP 500, + // and the trader logs only an opaque status code. The SDK's + // PostIntentRequest.price field is `string` for exactly this + // reason (see sphere-sdk PR #483); pass `.toString()` through + // and the JSON serialization is correct end-to-end. + const midpointRate = (rateMin + rateMax) / 2n; const expiresInDays = Math.max( 1, Math.ceil((expiryMs - nowMs()) / 86_400_000), @@ -905,7 +914,7 @@ export function createIntentEngine(deps: IntentEngineDeps): IntentEngine { description, intentType: params.direction, category: `${params.base_asset}/${params.quote_asset}`, - price: midpointRate, + price: midpointRate.toString(), currency: params.quote_asset, // Prefer @nametag for contactHandle — it resolves faster than DIRECT:// // which requires a binding event lookup that may not have propagated yet. diff --git a/src/trader/types.ts b/src/trader/types.ts index 9473fac..008548e 100644 --- a/src/trader/types.ts +++ b/src/trader/types.ts @@ -268,7 +268,16 @@ export interface MarketPostRequest { readonly description: string; readonly intentType: 'buy' | 'sell'; readonly category: string; - readonly price: number; + /** + * Price as a decimal-string bigint in the quote currency's smallest + * units. Same convention as token amounts in sphere-sdk (TXF amount + * fields, transfer payloads): internally bigint, over the wire + * decimal-string. Avoids JavaScript's `Number` precision loss for + * values above 2^53 (an 18-decimal coin hits that ceiling around + * 0.09 in human units). See sphere-sdk PR #483 for the SDK-side + * type that aligns with this. + */ + readonly price: string; readonly currency: string; readonly contactHandle: string; readonly expiresInDays: number; @@ -282,8 +291,10 @@ export interface MarketSearchOptions { export interface MarketSearchFilters { readonly intentType?: 'buy' | 'sell'; readonly category?: string; - readonly minPrice?: number; - readonly maxPrice?: number; + /** Decimal-string bigint — see {@link MarketPostRequest.price}. */ + readonly minPrice?: string; + /** Decimal-string bigint — see {@link MarketPostRequest.price}. */ + readonly maxPrice?: string; readonly minScore?: number; } @@ -295,7 +306,8 @@ export interface MarketSearchResult { readonly description: string; readonly intentType: 'buy' | 'sell'; readonly category?: string; - readonly price?: number; + /** Decimal-string bigint — see {@link MarketPostRequest.price}. */ + readonly price?: string; readonly currency: string; readonly contactHandle?: string; readonly createdAt: string; diff --git a/test/e2e/trader-intent-lifecycle.e2e.test.ts b/test/e2e/trader-intent-lifecycle.e2e.test.ts index 135436e..a15f29c 100644 --- a/test/e2e/trader-intent-lifecycle.e2e.test.ts +++ b/test/e2e/trader-intent-lifecycle.e2e.test.ts @@ -249,8 +249,10 @@ describe('E2E: Trader Intent Lifecycle (T1 + T10)', () => { expect(postCall.category).toBe('ALPHA/USDC'); expect(postCall.currency).toBe('USDC'); expect(postCall.contactHandle).toBe(AGENT_ADDRESS); - // Midpoint of 450-500 = 475 - expect(postCall.price).toBe(475); + // Midpoint of 450-500 = 475, serialized as decimal-string bigint + // per sphere-sdk PostIntentRequest.price convention (no Number + // cast — see trader/intent-engine.ts comment block). + expect(postCall.price).toBe('475'); // Description must match canonical format expect(postCall.description).toContain('Selling 100-1000 ALPHA for USDC'); expect(postCall.description).toContain('Rate: 450-500 USDC per ALPHA'); From 1c88a5cc69a2644b1abd62f39904920eaacfae55 Mon Sep 17 00:00:00 2001 From: Vladimir Rogojin Date: Thu, 11 Jun 2026 09:45:55 +0200 Subject: [PATCH 2/2] ci: bump SPHERE_SDK_SHA to b2fd028 (price-as-string fix) The price-as-string types from sphere-sdk PR #483 land at b2fd028; this PR's intent-engine + types.ts changes need that SDK build to typecheck. Both CI and docker-publish workflow pins moved in lockstep so the next v0.3 release tag builds against the same SHA. --- .github/workflows/ci.yml | 2 +- .github/workflows/docker-publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a311798..a7410cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: # /#447 rotations needed for agentic_hosting#26 (trader:v0.2 image # rebuild). Keep the docker-publish.yml pin aligned on every bump. env: - SPHERE_SDK_SHA: 550114c75e0b47151488d0c5d656475fd5a5077b + SPHERE_SDK_SHA: b2fd028fdf8dc86a598e7bf34c069dce50442b59 run: | git clone https://github.com/unicity-sphere/sphere-sdk.git ../sphere-sdk git -C ../sphere-sdk checkout --detach "$SPHERE_SDK_SHA" diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index ee9edef..c9e7e68 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -31,7 +31,7 @@ jobs: # counterparty transport pubkey), #464 PR #465 (MuxAdapter dispatch # awaits async handler), #447 PR #461 (terminal swap visibility). env: - SPHERE_SDK_SHA: 550114c75e0b47151488d0c5d656475fd5a5077b + SPHERE_SDK_SHA: b2fd028fdf8dc86a598e7bf34c069dce50442b59 run: | git clone https://github.com/unicity-sphere/sphere-sdk.git sphere-sdk git -C sphere-sdk checkout --detach "$SPHERE_SDK_SHA"