|
2 | 2 |
|
3 | 3 | All notable changes to this project will be documented in this file. |
4 | 4 |
|
| 5 | +## [2.51.0] - 2026-06-22 |
| 6 | + |
| 7 | +New venue: **Hunch** (parimutuel prediction market on Base, x402 settlement). And a full assertion-based audit of the Hyperliquid SDK surface (both Python + TS) caught ten silent bugs that prior "did the call throw?" smoke tests had hidden — several since the venue was added. Read paths now have behavioral assertions on shape AND values; trading paths are verified end-to-end against a live mainnet wallet. Final HL state: every no-creds method behaviorally green in both SDKs (43/43 assertions), credentialed reads cross-checked vs HL raw API (open orders: 7/7 exact match; user trades: 2000/2000 match; synthesized closed orders: 104/104 match), and the full createOrder → openOrders → cancelOrder → openOrders-empty lifecycle proven live. |
| 8 | + |
| 9 | +### Added — Hunch venue (#1150) |
| 10 | + |
| 11 | +- **`core/src/exchanges/hunch/` — new adapter** for [playhunch.xyz](https://www.playhunch.xyz), a parimutuel (pool-based, not CLOB/AMM) prediction market on Base settling in USDC via x402 / EIP-3009. Inventory is crypto-native: token market-cap ladders, up/down, launchpad, date-window markets. Modeled on the Myriad adapter — market-orders only, `fetchOrderBook: 'emulated'` (single level synthesized from implied price + pool), `cancelOrder` / limit / sell unsupported. |
| 12 | +- **Surface**: `fetchMarkets` / `fetchEvents` (single-market wrap — Hunch has no event tier) / `fetchOHLCV` (flat candles from the odds tape) / `fetchOrderBook` (emulated) / `fetchTrades` / `fetchPositions` / `fetchBalance`. Binary markets → YES/NO outcomes; N-way (mcap ladders, date windows) → one outcome per bucket key. `outcomeId` encodes `${marketId}:${side}` and round-trips back to a Hunch trade side. |
| 13 | +- **`createOrder`** runs Hunch's x402 loop: `POST /trade` → 402 → sign EIP-3009 `TransferWithAuthorization` (viem) → re-POST with `X-PAYMENT` → 200 receipt. Orders > $10 fetch a price-locked quote first. Verified end-to-end against the live 402 challenge (no funds moved). |
| 14 | +- **Registration**: `core/src/index.ts` (export + registry + `Hunch`), `core/src/server/exchange-factory.ts` (`case "hunch"`), `core/src/server/openapi.yaml` (`ExchangeParam` enum), `COMPLIANCE.md`. |
| 15 | +- **Self-hosted creds**: `HUNCH_PRIVATE_KEY` / `HUNCH_WALLET_ADDRESS` (Base). Hosted-mode trading (Polygon escrow) is out of scope — Hunch settles on Base via x402 — so it lists as read-only hosted / tradeable self-hosted, like Kalshi. |
| 16 | +- **Tests**: `core/test/normalizers/hunch-normalizer.test.ts` — 47 tests covering binary + N-way mapping, the money-critical `outcomeId` ↔ `side` round-trip, position/balance/OHLCV/emulated-orderbook. `tsc --noEmit` clean. |
| 17 | +- **Known gap**: `volume24h` is `0` (Hunch reports no 24h split; total `volume` is the pool size). Fast-follow on our side can add it. |
| 18 | + |
| 19 | +### Fixed — Hyperliquid |
| 20 | + |
| 21 | +- **`core/src/exchanges/hyperliquid/index.ts` — `fetchOHLCV` `params` was typed as required but server dispatch passes a single merged object as `args[0]`, leaving `args[1]` undefined.** Result: any MCP/HTTP call to HL `fetchOHLCV` crashed with `Cannot read properties of undefined (reading 'start')`. Default to `{ resolution: '1h' }` so the type-check passes (resolution is required on `OHLCVParams`) and the call survives even without explicit params. (#1161, #1162) |
| 22 | +- **`sdks/python/pmxt/client.py` + `sdks/typescript/pmxt/client.ts` — SDKs sent `?id=...` for GET reads, but server `method-verbs.json` spec for `fetchOHLCV` / `fetchTrades` declares the first arg as `outcomeId`.** GET dispatcher (`queryToArgs` in `core/src/server/app.ts`) peels primitives by spec name, then bundles remaining query keys into the object arg — so `outcomeId` arrived `undefined` and the raw `id` ended up inside `params`. Downstream `fromMarketId(undefined)` crashed with `Cannot read properties of undefined (reading 'match')`. POST was unaffected (positional args). Fixed both SDKs. (#1162) |
| 23 | +- **`core/src/exchanges/hyperliquid/normalizer.ts` — `volume24h` was hardcoded to `0` for every HL market and event.** Fixed by pulling `dayNtlVlm` from `spotMetaAndAssetCtxs` (outcome legs appear as `coin: '#NNNN'`), summing Yes + No notional per outcome, and threading the map through `fetchRawMarkets` / `fetchEventsImpl`. One extra batched call, no N+1; fallback to 0 if `spotMetaAndAssetCtxs` is unreachable. (#1219) |
| 24 | +- **`core/src/exchanges/hyperliquid/utils.ts` — `fromMarketId` only matched `hl-outcome-{N}`.** Passing an actual outcome token (e.g. `100002000`, as returned by `UnifiedMarket.outcomes[].outcomeId`) threw `Invalid Hyperliquid market ID`. Now accepts either form via `decodeAssetId`. Affects `fetchOrderBook` / `fetchOHLCV` / `fetchTrades`. (#1252) |
| 25 | +- **`core/src/exchanges/hyperliquid/fetcher.ts` — `fetchRawMarkets` ignored `params.marketId`, and `fetchRawEvents` ignored `params.eventId`.** `fetchMarket(marketId=X)` returned the *first* market in the venue list instead of X; `fetchEvent(eventId=X)` always returned the first question. Both filters now applied via `fromMarketId` and direct id match. (#1254) |
| 26 | +- **`core/src/exchanges/hyperliquid/fetcher.ts` — `fetchRawOHLCV` and `fetchRawTrades` ignored `params.limit`.** OHLCV returned the entire window regardless; trades returned a fixed page. Now slices client-side after the venue call returns (HL's `candleSnapshot` / `recentTrades` have no native limit param). (#1254) |
| 27 | +- **`core/src/exchanges/hyperliquid/normalizer.ts` — `normalizeUserTrade` left `marketId` / `outcomeId` / `fee` undefined on every fill.** Consumers couldn't tell which market a fill was on. Now populated via the existing `coinToMarketId` / `coinToOutcomeId` helpers and the raw `fee` field. Widened `UnifiedUserTrade` with optional `marketId`, `fee`. Verified 2000/2000 trades fully populated on an active wallet. (#1255) |
| 28 | +- **`core/src/exchanges/hyperliquid/normalizer.ts` — `normalizeBalance` read `crossMarginSummary` (perp margin) and hardcoded the currency label as `'USDH'`.** Outcome markets quote against USDC on the spot account, so users with deposited funds saw `USDH: 0` and assumed empty. Now reads `spotClearinghouseState` alongside the perp account; spot balances surface with their real coin label (`USDC`, `USDH`, etc.); funded perp surfaces as `USDC_PERP` so callers can tell it apart. Verified $14.90 USDC deposit now correctly returns `USDC: 14.9`. (#1280) |
| 29 | +- **`core/src/exchanges/hyperliquid/index.ts` — `submitOrder` silently swallowed HL rejections.** When HL returned `statuses[0].error` (e.g. `"Order must have minimum value of 10 USDC"`), the SDK reported `status='filled', id='unknown'` — fake success with no way for the caller to know the order wasn't placed. Now raises a typed `PmxtError` carrying HL's message; also correctly populates `price` / `oid` on the `filled` branch (was checked but never read). (#1281) |
| 30 | +- **`core/src/exchanges/hyperliquid/index.ts` — `cancelOrder` hardcoded `a: 0` (asset id).** HL rejected every cancel with `User or API Wallet 0xfaf6... does not exist` because the action hash with the wrong asset recovers a different signer address. Now looks up the open order to derive the real asset id via `encodeAssetId`. Throws a typed "Order not found" when the supplied oid isn't open. (#1281) |
| 31 | +- **`core/src/exchanges/hyperliquid/auth.ts` — `msgpackr` encoded positive BigInts as int64 (`0xd3`), HL's server (Python `msgpack`) encodes the same values as uint64 (`0xcf`).** Identical bit pattern, different type byte → action hash mismatch → signature recovers to wrong address. Only manifested on cancel because order actions don't carry BigInt fields; cancel does (`oid`). Added `fixInt64ToUint64` post-processor: HL actions never carry negative ints (oids, nonces, asset ids are all ≥ 0), so flipping `d3 → cf` when the following byte is `< 0x80` is safe and produces byte-identical encoding to Python msgpack. (#1281) |
| 32 | + |
| 33 | +### Fixed — Python SDK (cross-venue) |
| 34 | + |
| 35 | +- **`sdks/python/pmxt/errors.py` — `NetworkError` / `ExchangeNotAvailable` rejected the `code` / `retryable` kwargs that `from_server_error` always sets.** Any 503 from Kalshi or another venue raised `TypeError: ExchangeNotAvailable.__init__() got an unexpected keyword argument 'code'` instead of a typed `PmxtError`. Both classes now accept `**kwargs` and default-fill their hardcoded values. Caught by the cross-venue Trump search test where Kalshi briefly errored. (#1219) |
| 36 | + |
| 37 | +### Added — Hyperliquid |
| 38 | + |
| 39 | +- **`fetchEventsPaginated`** wrappers in both SDKs. `BaseExchange.fetchEventsPaginated` existed but neither `client.py` nor `client.ts` exposed it. Added `fetch_events_paginated` / `fetchEventsPaginated` mirroring the existing `fetchMarketsPaginated` pattern. (#1252) |
| 40 | +- **`fetchOrderBooks`** on Hyperliquid. HL has no native batch order-book endpoint. Implemented as `Promise.all` over `fetchOrderBook`; inherits the outcome-token-or-marketId resolution from the single-fetch path. `has.fetchOrderBooks` auto-flips because `BaseExchange._deriveCapabilities` introspects overrides. `ponytail:` comment marks the unbounded concurrency for future capping. (#1253) |
| 41 | +- **`fetchClosedOrders` + `fetchAllOrders`** on Hyperliquid. HL exposes no closed-orders endpoint. Synthesize by grouping `userFills` by `oid` and excluding currently-open oids — VWAP price, summed size, summed fee, earliest fill time. `fetchAllOrders = openOrders ∪ closedOrders`. `ponytail:` comment marks the limitation: cancelled-with-no-fills orders aren't reconstructable from the public info API. Verified against an active wallet: 7 open + 104 derived closed = 111 all, exact-count match. (#1255) |
| 42 | + |
| 43 | +### Changed — types |
| 44 | + |
| 45 | +- **`core/src/types.ts` — `UnifiedUserTrade`** widened with optional `marketId` and `fee` fields. Existing consumers continue to work unchanged. (#1255) |
| 46 | + |
| 47 | +### Fixed — Python SDK (test-suite rot caught at version cut) |
| 48 | + |
| 49 | +Running the full `npm test` before tagging surfaced six pre-existing failures untouched by the HL audit. Two were the SDK silently broken since the 2.50.11–14 hosted-routing series; the rest were stale test fixtures from earlier intentional behavior changes. |
| 50 | + |
| 51 | +- **`sdks/python/pmxt/client.py` — `fetch_closed_orders` missing hosted-mode guard.** The 2.50.11–14 hosted-mode routing series added hosted branches to `fetch_balance` / `fetch_positions` / `fetch_order` / `fetch_my_trades` / `fetch_open_orders` / `cancel_order` but skipped `fetch_closed_orders`. Hosted-mode callers fell through to the sidecar `/api/{exchange}/fetchClosedOrders` path and received `invalid api key` (or whatever the sidecar returned) instead of the typed `NotSupported` the test asserts. Added an `if self.is_hosted` branch that raises `NotSupported` with a pointer to `fetch_my_trades` (which works in hosted mode and surfaces executed fills). |
| 52 | +- **`sdks/python/pmxt/client.py` — `fetch_all_orders` missing hosted-mode guard.** Identical class of bug to the above. Added the same `is_hosted` branch raising `NotSupported` with a pointer to `fetch_open_orders` + `fetch_my_trades`. |
| 53 | +- **`sdks/python/pmxt/client.py` — `unwatch_order_book` bypassed the WebSocket transport.** Every other `(un)watch*` method routes through `_watch_required_via_ws` / `_unwatch_required_via_ws`; `unwatch_order_book` alone hit `POST /api/{exchange}/unwatchOrderBook` over HTTP. The `_unwatch_required_via_ws` helper already existed and handles all the bookkeeping (find the active sub by `watchOrderBook:{outcomeId}` key, send unsubscribe over the WS, clean up `_active_subs` / `_subscriptions` / `_data_queues` / `_data_store`). The method now calls that helper, matching the other unwatch paths. |
| 54 | +- **`sdks/python/tests/test_feed_client.py:36` — assertion expected timeout 15, implementation has been 30 since the FeedClient default was bumped.** Updated the assertion to `30`. |
| 55 | +- **`sdks/python/tests/test_hosted_dispatch.py:285` — fixture sent `amount: 2.0` and expected `2.0` back, but per 2.50.13's `user_trade_from_v0` the v0 wire sends amounts in 6-dec micro-shares (the SDK divides by 1e6 to produce decimal shares). Updated the fixture to `2_000_000` micro-shares so the test asserts the real wire-to-model mapping. |
| 56 | + |
| 57 | +After these fixes the full pipeline (core unit + Python SDK + verification) is zero-failing — 28/28 Jest suites, 246/246 Python tests, "All SDK Integration Tests Passed". |
| 58 | + |
5 | 59 | ## [2.50.16] - 2026-06-18 |
6 | 60 |
|
7 | 61 | ### Fixed |
|
0 commit comments