Skip to content

fix(market): use decimal-string bigints for all price fields - #483

Merged
vrogojin merged 1 commit into
mainfrom
fix/market-bigint-string-prices
Jun 10, 2026
Merged

fix(market): use decimal-string bigints for all price fields#483
vrogojin merged 1 commit into
mainfrom
fix/market-bigint-string-prices

Conversation

@vrogojin

Copy link
Copy Markdown
Contributor

Summary

`MarketModule`'s `PostIntentRequest.price`, `SearchIntentResult.price`, and `SearchFilters.{min,max}Price` were typed as `number` — inconsistent with `MarketIntent.price` (already `string`) and inconsistent with the SDK's established bigint-serialization convention everywhere else (TXF amount fields, transfer payloads, every token amount in the wire layer: bigint internally, decimal-string on the wire).

Why this matters

The trader-service intent engine (`trader-service/src/trader/intent-engine.ts:908`) takes a bigint midpoint rate `(rate_min + rate_max) / 2n` in 18-decimal smallest units and casts to `Number(...)` before passing to `MarketModule.postIntent`. For the trader-roundtrip soak's default 0.08–0.12 ETH/UCT band:

  • midpoint = `(80_000_000_000_000_000n + 120_000_000_000_000_000n) / 2n` = `100_000_000_000_000_000n` = 10¹⁷
  • `Number.MAX_SAFE_INTEGER` = 2⁵³ ≈ 9.007 × 10¹⁵
  • 10¹⁷ → JavaScript Number stores a close-but-not-exact IEEE 754 double
  • Serialized as JSON number → market-api server returns HTTP 500 with non-JSON body, so the trader logs only the status code with no actionable diagnostic

The end-to-end soak in sphere-sdk#475 hits this consistently in §6 (intent post). Two consecutive runs both failed at the same line, with the same error, on the same payload.

Fix

Field Before After
`PostIntentRequest.price` `number` `string`
`SearchIntentResult.price` `number` `string`
`SearchFilters.minPrice` `number` `string`
`SearchFilters.maxPrice` `number` `string`
`MarketIntent.price` `string` (already correct) unchanged

The wire serialization in `toSnakeCaseIntent` / `toSnakeCaseFilters` is a direct pass-through, so changing the input type changes the wire shape from JSON number to JSON string without any new conversion logic.

`MarketIntent.price` was already `string`, confirming the server's read shape uses strings — the server should also accept strings on the write side without server changes (this is the existing convention for TIP-0 / MarketModule round-tripping). If the server unexpectedly rejects, a follow-up issue would track the server side.

Test plan

  • `npx tsc --noEmit` — clean
  • `npm test -- --run modules/market` — 125 / 125 pass
  • Existing test inputs (`price: 100`, `price: 99.99`, `minPrice: 10`, `maxPrice: 200`) migrated to string form. `99.99` becomes `'99990000000000000000'` — i.e. 99.99 displayed in human units, 18-decimal smallest units on the wire. This also documents the canonical UI ↔ wire conversion the trader-service should adopt.
  • New `preserves bigint precision past Number.MAX_SAFE_INTEGER` test exercises the failure mode directly: `'100000000000000000'` (10¹⁷) round-trips through JSON without loss, proving the string convention is what trader-service needs.

Coordinating change

`trader-service/src/trader/intent-engine.ts:908` will need a follow-up to drop the `Number(...)` cast and pass `.toString()` instead. That's a one-line change. Tracked as the next step in the conversation that surfaced this.

Related

MarketModule's PostIntentRequest.price, SearchIntentResult.price, and
SearchFilters.{min,max}Price were typed as `number` — inconsistent with
MarketIntent.price (already `string`) and inconsistent with the SDK's
established bigint-serialization convention (TXF amount fields, transfer
payloads, token amounts everywhere else: bigint internally, decimal-string
on the wire).

This bites real callers. The trader-service intent engine
(trader-service/src/trader/intent-engine.ts:908) takes its bigint
midpoint rate (rate_min + rate_max) / 2n in 18-decimal smallest units and
casts to `Number(...)` before passing to MarketModule.postIntent. For
the trader-roundtrip soak's default 0.08-0.12 ETH/UCT band the midpoint
is 1e17 = 100_000_000_000_000_000, well past Number.MAX_SAFE_INTEGER
(2^53 ≈ 9.007e15). JavaScript Number stores 1e17 as a close-but-not-exact
double, and the market-api server responds with HTTP 500 — non-JSON, so
the trader logs only the status code with no diagnostic.

Fix:
- PostIntentRequest.price          number → string
- SearchIntentResult.price         number → string
- SearchFilters.{minPrice,maxPrice} number → string

The wire serialization in toSnakeCaseIntent / toSnakeCaseFilters is a
direct pass-through, so changing the input type changes the wire shape
from JSON number to JSON string without any new conversion logic.

MarketIntent.price was already `string`, confirming the server's read
shape uses strings — the server should also accept strings on the write
side without server changes (this is the existing convention for
TIP-0 / MarketModule round-tripping).

Tests:
- Existing test inputs `price: 100`, `price: 99.99`, `minPrice: 10`,
  `maxPrice: 200` migrated to string form (`'100'`, `'99990000000000000000'`
  i.e. 99.99 in 18-decimal smallest units, `'10'`, `'200'`).
- New precision-preservation test demonstrates the failure mode:
  '100000000000000000' (10^17) round-trips through JSON without loss,
  proving the string convention is what trader-service should use.

Surfaced by: #475 (trader-roundtrip soak)
end-to-end run §6 — market-api HTTP 500 on every attempt to post the
trader's intent.

Coordinating: trader-service/src/trader/intent-engine.ts will need a
follow-up to drop the `Number(...)` cast and pass `.toString()` instead.
@vrogojin
vrogojin merged commit b2fd028 into main Jun 10, 2026
3 checks passed
@vrogojin
vrogojin deleted the fix/market-bigint-string-prices branch June 10, 2026 21:20
@vrogojin
vrogojin restored the fix/market-bigint-string-prices branch July 15, 2026 13:41
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