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" 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');