Skip to content

Commit 1e39b91

Browse files
committed
release: 2.50.2 — Rain works in both SDKs + restore hosted submitOrder
Three fixes that together make Rain usable from pmxtjs and pmxt-py: 1. Function-wrap ESM import() to defeat tsc's commonjs downlevel 2. Stringify bigints in sourceMetadata before HTTP serialisation 3. Restore _hostedSubmitOrder helper that PR #1058 orphaned (3) was a pre-existing regression on main that broke hosted createOrder for every venue, not just Rain.
1 parent 59498b9 commit 1e39b91

6 files changed

Lines changed: 131 additions & 5 deletions

File tree

changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.50.2] - 2026-06-15
6+
7+
Rain now actually works end-to-end through both `pmxtjs` and the Python SDK, and restores hosted-mode `createOrder` for every other venue (`polymarket`, `opinion`, `limitless`) which had been broken on `main` since PR #1058. Verified live: `new pmxt.Rain().fetchMarkets({ limit: 3 })` and `Rain().fetch_markets(limit=3)` both round-trip real markets (Khamenei binary, Bond actor 6-way, FIFA 16-way) through the sidecar.
8+
9+
### Fixed
10+
11+
- **`core/src/exchanges/rain/fetcher.ts` + `core/src/exchanges/rain/websocket.ts`**: `@buidlrrr/rain-sdk` is ESM-only (`"type": "module"`, no CJS export) and `tsc` with `module: "commonjs"` silently rewrites `await import('@buidlrrr/rain-sdk')` into `Promise.resolve().then(() => require('@buidlrrr/rain-sdk'))`, which throws `ERR_PACKAGE_PATH_NOT_EXPORTED` at runtime. Wrapping the loader in `new Function('return import("@buidlrrr/rain-sdk")')` keeps the real ESM `import()` opaque to the downleveller, so Node executes it natively. The same trap applies to the Opinion adapter on paper; its read path appears to escape it via existing bundling, but the Rain path lit it up because the trade-tx builders are called from the cold server start.
12+
- **`core/src/exchanges/rain/utils.ts` + `core/src/exchanges/rain/normalizer.ts`**: New `bigintsToStrings()` recursive converter is applied to the spread that feeds `buildSourceMetadata`. Rain's `MarketDetails` carries `bigint` for `startTime`, `endTime`, `oracleEndTime`, `allFunds`, `allVotes`, `totalLiquidity`, `numberOfOptions`, `winner`, `baseTokenDecimals`, etc., and the PMXT sidecar `JSON.stringify`s the response over HTTP — the unconverted spread threw "Do not know how to serialize a BigInt" and dropped every Rain market on the floor before it reached the SDKs.
13+
- **`sdks/typescript/pmxt/client.ts`**: Restored `_hostedSubmitOrder` (typed-data sign + economic validation + `/v0/trade/submit-order` POST) and `_hostedTypedDataRoute` / `_hostedCancelTypedDataRoute` helpers. PR #1058 (Limitless hosted wire-up, commit `e96801b`) removed all three methods but left their call sites and signing imports intact; this made `npx tsc` fail with `TS2551: Property '_hostedSubmitOrder' does not exist on type 'Exchange'` and silently broke hosted-mode `createOrder` on every venue at runtime (it would have thrown "this._hostedSubmitOrder is not a function" the moment a hosted user placed an order). The restored helper now also routes `limitless` through `limitless_buy` / `limitless_sell_polygon` / `limitless_sell_base_pull` / `cancel_limitless_*` schemas, matching the PR's stated scope.
14+
515
## [2.50.1] - 2026-06-15
616

717
Follow-up to 2.50.0. Rain is now wired through every consumer surface — the openapi enum source, the TS SDK class export, and the Python SDK class export — so `pmxtjs.Rain` and `pmxt.Rain` (Python) actually exist instead of being missing from the published packages. The CI exchange-drift check caught this on the 2.50.0 push; `ADDING_AN_EXCHANGE.md` only documents the core-server registration sites, not these SDK-facing ones.

core/src/exchanges/rain/fetcher.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@
55
import { rainErrorMapper } from './errors';
66
import { logger } from '../../utils/logger';
77

8-
// ESM dynamic import (same pattern as Opinion adapter).
8+
// @buidlrrr/rain-sdk is ESM-only. TSC with module:"commonjs" rewrites a plain
9+
// `await import(...)` to `Promise.resolve().then(() => require(...))`, which
10+
// blows up at runtime on `"type": "module"` packages with no CJS export
11+
// (`ERR_PACKAGE_PATH_NOT_EXPORTED`). The Function-wrapped string keeps the
12+
// real ESM `import()` opaque to TSC's downleveller so Node executes it natively.
913
type RainSdk = typeof import('@buidlrrr/rain-sdk');
1014
type RainClient = InstanceType<RainSdk['Rain']>;
1115

16+
const esmImportRainSdk: () => Promise<RainSdk> = new Function(
17+
'return import("@buidlrrr/rain-sdk")',
18+
) as () => Promise<RainSdk>;
19+
1220
let sdkPromise: Promise<RainSdk> | undefined;
1321
function loadSdk(): Promise<RainSdk> {
14-
if (!sdkPromise) sdkPromise = import('@buidlrrr/rain-sdk');
22+
if (!sdkPromise) sdkPromise = esmImportRainSdk();
1523
return sdkPromise;
1624
}
1725

core/src/exchanges/rain/normalizer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
RainRawBalance, RainRawPriceHistory, RainRawTransactions, RainRawMarketTransactions,
1010
} from './fetcher';
1111
import {
12-
priceBigIntToNumber, weiToNumber, mapRainStatus, rainMarketUrl, USDT_DECIMALS, resolveDecimals,
12+
priceBigIntToNumber, weiToNumber, mapRainStatus, rainMarketUrl, USDT_DECIMALS, resolveDecimals, bigintsToStrings,
1313
} from './utils';
1414

1515
const PROMOTED_MARKET_KEYS = [
@@ -83,7 +83,7 @@ export class RainNormalizer {
8383
contractAddress,
8484
tags,
8585
sourceMetadata: buildSourceMetadata(
86-
{ ...(details ?? {}), ...m } as Record<string, unknown>,
86+
bigintsToStrings({ ...(details ?? {}), ...m }) as Record<string, unknown>,
8787
PROMOTED_MARKET_KEYS,
8888
),
8989
};

core/src/exchanges/rain/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,25 @@ export function mapRainStatus(status?: string): string | undefined {
5959
export function rainMarketUrl(marketId: string): string {
6060
return `https://rain.one/markets/${marketId}`;
6161
}
62+
63+
/**
64+
* Recursively convert bigints to strings so the value is JSON-serialisable.
65+
* Rain's SDK returns bigints all over (timestamps, fund totals, prices), and
66+
* the PMXT server JSON.stringifies sourceMetadata before returning over HTTP.
67+
*/
68+
export function bigintsToStrings<T>(value: T): T {
69+
if (typeof value === 'bigint') {
70+
return value.toString() as unknown as T;
71+
}
72+
if (Array.isArray(value)) {
73+
return value.map(bigintsToStrings) as unknown as T;
74+
}
75+
if (value && typeof value === 'object') {
76+
const out: Record<string, unknown> = {};
77+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
78+
out[k] = bigintsToStrings(v);
79+
}
80+
return out as T;
81+
}
82+
return value;
83+
}

core/src/exchanges/rain/websocket.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ type RainSdk = typeof import('@buidlrrr/rain-sdk');
99
type RainClient = InstanceType<RainSdk['Rain']>;
1010
type Unsubscribe = () => void;
1111

12+
// See note in fetcher.ts on why we Function-wrap the import().
13+
const esmImportRainSdk: () => Promise<RainSdk> = new Function(
14+
'return import("@buidlrrr/rain-sdk")',
15+
) as () => Promise<RainSdk>;
16+
1217
export interface RainWebSocketConfig {
1318
wsRpcUrl: string;
1419
environment?: 'development' | 'stage' | 'production';
@@ -26,7 +31,7 @@ export class RainWebSocket {
2631

2732
private async getClient(): Promise<RainClient> {
2833
if (!this.client) {
29-
const sdk = await import('@buidlrrr/rain-sdk');
34+
const sdk = await esmImportRainSdk();
3035
this.client = new sdk.Rain({
3136
environment: this.config.environment ?? 'production',
3237
wsRpcUrl: this.config.wsRpcUrl,

sdks/typescript/pmxt/client.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2237,6 +2237,87 @@ export abstract class Exchange {
22372237
return this._hostedSubmitOrder(built);
22382238
}
22392239

2240+
/**
2241+
* Hosted-mode submitOrder: validate the stored build response, sign the
2242+
* typed_data (and pull_typed_data for cross-chain venue sells), then
2243+
* POST to `/v0/trade/submit-order`.
2244+
*
2245+
* Restored after PR #1058 (Limitless hosted wire-up) accidentally
2246+
* removed it but left the call site + signing imports intact.
2247+
*/
2248+
private async _hostedSubmitOrder(built: BuiltOrder): Promise<Order> {
2249+
const signer = this.requireHostedSigner();
2250+
if (!this.walletAddress) {
2251+
throw new MissingWalletAddress(
2252+
"hosted submitOrder requires walletAddress",
2253+
);
2254+
}
2255+
const payload = built as unknown as Record<string, unknown>;
2256+
const typedData = payload["typed_data"] as TypedData | undefined;
2257+
if (!typedData) {
2258+
throw new HostedInvalidSignature(0, "typed_data missing from built order");
2259+
}
2260+
const buildRequest = (payload["build_request"] as Record<string, unknown> | undefined)
2261+
?? ((payload["params"] as Record<string, unknown> | undefined)?.["build_request"] as Record<string, unknown> | undefined);
2262+
2263+
const side = String(buildRequest?.["side"] ?? "buy");
2264+
const primaryRoute = this._hostedTypedDataRoute(side, false);
2265+
validateTypedData(typedData, primaryRoute, this.walletAddress);
2266+
if (buildRequest) {
2267+
validateEconomics(typedData, primaryRoute, buildRequest, payload);
2268+
}
2269+
2270+
const signature = await signer.signTypedData(typedData);
2271+
verifySignature(typedData, signature, signer.address);
2272+
2273+
const body: Record<string, unknown> = {
2274+
built_order_id: payload["built_order_id"],
2275+
signature,
2276+
};
2277+
2278+
const pullTypedData = payload["pull_typed_data"] as TypedData | undefined;
2279+
if (pullTypedData) {
2280+
const pullRoute = this._hostedTypedDataRoute(side, true);
2281+
if (pullRoute) {
2282+
validateTypedData(pullTypedData, pullRoute, this.walletAddress);
2283+
}
2284+
const pullSig = await signer.signTypedData(pullTypedData);
2285+
verifySignature(pullTypedData, pullSig, signer.address);
2286+
body["pull_signature"] = pullSig;
2287+
}
2288+
2289+
const route = HOSTED_METHOD_ROUTES.get("submitOrder")!;
2290+
const data = await _tradingRequest(this, { method: route.method, path: route.path, body });
2291+
return orderFromV0(data as Record<string, unknown>);
2292+
}
2293+
2294+
/**
2295+
* Resolve the per-(venue, side, pull) typed-data schema route used by
2296+
* `validateTypedData` / `validateEconomics`. Returns the cross-chain
2297+
* pull-leg route name for Opinion sells and Limitless cross-chain orders.
2298+
*/
2299+
private _hostedTypedDataRoute(side: string, isPull: boolean): string {
2300+
const venue = this.exchangeName;
2301+
const sideLower = side.toLowerCase();
2302+
if (venue === "polymarket") {
2303+
return sideLower === "sell" ? "polymarket_sell" : "polymarket_buy";
2304+
}
2305+
if (venue === "limitless") {
2306+
if (sideLower === "buy") return "limitless_buy";
2307+
return isPull ? "limitless_sell_base_pull" : "limitless_sell_polygon";
2308+
}
2309+
// opinion
2310+
if (sideLower === "buy") return "opinion_buy";
2311+
return isPull ? "opinion_sell_bsc_pull" : "opinion_sell_polygon";
2312+
}
2313+
2314+
private _hostedCancelTypedDataRoute(isPull: boolean): string {
2315+
const venue = this.exchangeName;
2316+
if (venue === "polymarket") return "cancel_polymarket";
2317+
if (venue === "limitless") return isPull ? "cancel_limitless_base_pull" : "cancel_limitless_polygon";
2318+
return isPull ? "cancel_opinion_bsc_pull" : "cancel_opinion_polygon";
2319+
}
2320+
22402321
/**
22412322
* Construct the hosted build-order request body and validate inputs
22422323
* locally per the v0 contract (denom/side compatibility, > 6-decimal

0 commit comments

Comments
 (0)