@pulsepairs/sdk 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DOCUMENTATION.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Version:** 0.7.0 · **License:** UNLICENSED · **Runtime:** Node ≥ 18 or a modern browser
4
4
 
5
- Complete reference for the UpDown (PulsePairs) TypeScript SDK: every export, its
5
+ Complete reference for the UpDown TypeScript SDK: every export, its
6
6
  units, its failure modes, and the protocol it speaks.
7
7
 
8
8
  The [`README.md`](./README.md) is the quickstart. **This** document is the
@@ -61,19 +61,28 @@ on-chain fill. You never pay gas to place, amend or cancel an order — only for
61
61
  the one-time ERC-20 `approve` (and, on the smart-account path, the one-time
62
62
  account deploy).
63
63
 
64
- ### The demo environment
64
+ ### The environments
65
65
 
66
- A full stack runs 24/7 on Arbitrum One with a mock oracle and mintable test USDT:
66
+ Two stacks, both running 24/7 on **Arbitrum One** (dev is not a testnet — it is
67
+ mainnet with a mock oracle and play money):
67
68
 
68
69
  ```
69
- UI https://demo-pulsepairs.rainwins.com
70
- REST https://api.demo-pulsepairs.rainwins.com
71
- WS wss://api.demo-pulsepairs.rainwins.com/stream
72
- Faucet POST /test/devmint (10k cap, 1 mint / address / 5 min; also seeds gas ETH)
70
+ DEV REST https://dev-api-updown.rain.trade
71
+ WS wss://dev-api-updown.rain.trade/stream
72
+ Money mock USDT, mintable
73
+ Oracle mock Data Streams; chart shows the CEX composite
74
+ Faucet POST /test/devmint (10k cap, 1 mint / address / 5 min; also seeds gas ETH)
75
+
76
+ PROD REST https://prod-api-updown.rain.trade
77
+ WS wss://prod-api-updown.rain.trade/stream
78
+ Money real USDT0
79
+ Oracle real Chainlink Data Streams; chart shows the DON benchmark it settles on
80
+ Faucet none — /test/devmint 404s
73
81
  ```
74
82
 
75
83
  Mint to the **smart-account** address if you are on the Account Kit path, not the
76
- owner EOA.
84
+ owner EOA. Read `GET /config` on whichever host you point at for chain id and
85
+ contract addresses; never hardcode them.
77
86
 
78
87
  ---
79
88
 
@@ -204,8 +213,25 @@ signed message. `PostOrderBody` additionally accepts the string forms
204
213
  (`"BUY"`, `"LIMIT"`, …) for `side`/`type`, but the *signature* is always over the
205
214
  numbers.
206
215
 
207
- `MARKET` orders carry `price: 0` (both in the signed message as `0n` and in the
208
- POST body as `0`).
216
+ A `MARKET` order's `price` is its **slippage bound**, not a fill price: the worst
217
+ price the order will accept — a CAP on a buy, a FLOOR on a sell. It is enforced
218
+ twice, so it is not advisory: the engine breaks its match loop on it (an order
219
+ whose depth sat outside the bound is cancelled `PRICE_THROUGH_LIMIT`, not
220
+ `NO_LIQUIDITY`), and `UpDownSettlement.enterPosition` reverts `OrdersNotCrossed()`
221
+ if a fill would cross it.
222
+
223
+ `price: 0` is **rejected at submit** — *"Orders must specify a non-zero price.
224
+ Market orders should be signed at the worst-acceptable slippage price."* There is
225
+ no unbounded market order. Earlier versions of this document said MARKET orders
226
+ carry `price: 0`; that was true of an older protocol and following it now
227
+ produces an error on every order.
228
+
229
+ Express the pad in **cents**, not as a percentage. A percentage bound is worth
230
+ almost nothing at long-shot prices, which is where these markets spend their
231
+ final minutes: 5% of an 8c option is 0.4c, against a book quoting a 7.39c
232
+ spread — one production order missed a fill by 0.23c with depth to spare that
233
+ way. `centsToBps` / `bpsToCents` are exported for the conversion; 100 bps is 1
234
+ cent exactly.
209
235
 
210
236
  ### 3.4 Fees and `maxFee`
211
237
 
@@ -306,7 +332,7 @@ otherwise be replayable forever.
306
332
 
307
333
  ```ts
308
334
  import { UpDownHttpClient } from "@pulsepairs/sdk";
309
- const api = new UpDownHttpClient("https://api.demo-pulsepairs.rainwins.com");
335
+ const api = new UpDownHttpClient("https://dev-api-updown.rain.trade");
310
336
  ```
311
337
 
312
338
  A thin, dependency-free wrapper over `fetch`. Trailing slashes on the base URL
@@ -331,7 +357,7 @@ runtimes) — they are not normalised.
331
357
  | `getVersion()` | `GET /version` | `Version` — `{ commit, bootedAt, env, nodeVersion }` |
332
358
  | `getHealth()` | `GET /health` | `{ status, relayer?, uptime? }` |
333
359
  | `getConfig()` | `GET /config` | `ApiConfig` — **fetch this first, never hardcode addresses** |
334
- | `getMarkets(opts?)` | `GET /markets` | `MarketListItem[]`; `opts.timeframe` ∈ `300 \| 900 \| 3600`, `opts.pair` ∈ `"BTC-USD" \| "ETH-USD"` |
360
+ | `getMarkets(opts?)` | `GET /markets` | `MarketListItem[]`; `opts.timeframe` ∈ `300 \| 900` (60m retired 2026-08-18 — no new 3600s markets; the API still accepts `3600` and returns the historical ones), `opts.pair` ∈ `"BTC-USD" \| "ETH-USD"` |
335
361
  | `getMarket(address)` | `GET /markets/:address` | `MarketDetail` — list item + `timeRemainingSeconds` + top-of-book |
336
362
  | `getOrderbook(market)` | `GET /orderbook/:address` | `OrderBookFull` — full depth, both options, bids and asks |
337
363
  | `getBalance(wallet)` | `GET /balance/:wallet` | `Balance` — `available`, `inOrders`, `cachedBalance`, `withdrawNonce` |
@@ -428,8 +454,8 @@ the book asynchronously.
428
454
  ### `wsUrlFromHttpBase(httpBase)`
429
455
 
430
456
  ```ts
431
- wsUrlFromHttpBase("https://api.demo-pulsepairs.rainwins.com")
432
- // → "wss://api.demo-pulsepairs.rainwins.com/stream"
457
+ wsUrlFromHttpBase("https://dev-api-updown.rain.trade")
458
+ // → "wss://dev-api-updown.rain.trade/stream"
433
459
  ```
434
460
 
435
461
  Swaps `https:`→`wss:` / `http:`→`ws:`, forces the path to `/stream`, and strips
@@ -703,9 +729,14 @@ assertStakeBounds(amountAtomic: bigint, side?: "BUY" | "SELL"): void
703
729
  feeAtomic(notionalAtomic, priceBps, cfg): bigint
704
730
 
705
731
  MIN_STAKE_ATOMIC = 1_000_000n // $1 of share FACE value (BUY only)
706
- MAX_STAKE_ATOMIC = 500_000_000n // $500 of share FACE value
707
732
  ```
708
733
 
734
+ > **`MAX_STAKE_ATOMIC` was removed in `0.8.0`** — the venue no longer caps order
735
+ > size, so importing it is a compile error. If you were relying on it, pick your
736
+ > own ceiling; nothing server-side will stop an oversized `amount` except your
737
+ > own collateral (`CollateralGate` checks balance and allowance on-chain before
738
+ > matching).
739
+
709
740
  Both `centsToBps` and `bpsToCents` are exclusive at both ends — 0¢ and 100¢ are
710
741
  not tradeable prices, and passing either throws.
711
742
 
@@ -721,8 +752,8 @@ Two things about the window that bite integrators:
721
752
  shares first (`cash / price`), which is always ≥ the cash figure, so a
722
753
  cash-denominated gate at $1 sits safely above this bound at every price.
723
754
  - **The minimum is entry-only.** Pass `side: "SELL"` for exits — a holder left
724
- with 0.42 shares after a partial fill must be able to sell them. The maximum
725
- still applies to both sides.
755
+ with 0.42 shares after a partial fill must be able to sell them. There is no
756
+ maximum on either side any more.
726
757
 
727
758
  > Changed 2026-07-23 (SDK 0.7.0): minimum was `5_000_000n` ($5, both sides).
728
759
 
package/README.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # @pulsepairs/sdk
2
2
 
3
- Standalone SDK for the **UpDown** (PulsePairs) up/down prediction markets —
3
+ Standalone SDK for the **UpDown** up/down prediction markets —
4
4
  BTC-USD / ETH-USD, UP or DOWN, on 5-minute / 15-minute / 1-hour cycles.
5
5
 
6
+ > **On the name:** the product is **UpDown**. "PulsePairs" is a retired name that
7
+ > survives in two places for compatibility reasons only — this npm package's name
8
+ > (`@pulsepairs/sdk`, renaming it would break every consumer) and two EIP-712 domain
9
+ > strings that are hashed into signatures (`PulsePairs WebSocket Auth`,
10
+ > `PulsePairsAuthDomain`). Neither can be changed without breaking deployed clients.
11
+
6
12
  This is the package [rain.trade](https://rain.trade) integrates to surface
7
13
  UpDown markets alongside Rain's own markets ("satellite" integration, Option A).
8
14
  It is deliberately **segregated** from `rain-sdk-v2`: bump this package's version
@@ -23,13 +29,20 @@ It gives you three things:
23
29
  > its units, its failure modes, the WS protocol, the full type surface and a
24
30
  > troubleshooting table — see **[`DOCUMENTATION.md`](./DOCUMENTATION.md)**.
25
31
 
26
- > **Live integration environment:** a public demo of the full stack (markets
27
- > cycling 24/7 on Arbitrum One, mock oracle, mintable test USDT) runs at
28
- > **https://demo-pulsepairs.rainwins.com** with the matcher API/WS at
29
- > **`https://api.demo-pulsepairs.rainwins.com`** (`wss://…/stream`). Every
30
- > example below targets it by default. Test funds: `POST /test/devmint`
31
- > (10k cap, 1 mint per address per 5 min — mint to the **smart-account**
32
- > address, not the owner EOA; it also seeds a little gas ETH).
32
+ > **Live environments — there are two, both on Arbitrum One:**
33
+ >
34
+ > | | REST | WebSocket |
35
+ > |---|---|---|
36
+ > | **Dev** — mock oracle, mock USDT, faucet on | `https://dev-api-updown.rain.trade` | `wss://dev-api-updown.rain.trade/stream` |
37
+ > | **Prod** — real Chainlink DON, real USDT0 | `https://prod-api-updown.rain.trade` | `wss://prod-api-updown.rain.trade/stream` |
38
+ >
39
+ > Examples below target **dev** by default — markets cycle 24/7 there and a
40
+ > mistake costs play money. Test funds: `POST /test/devmint` (10k cap, 1 mint per
41
+ > address per 5 min — mint to the **smart-account** address, not the owner EOA; it
42
+ > also seeds a little gas ETH). The faucet 404s on prod.
43
+ >
44
+ > The older `demo-pulsepairs.rainwins.com` / `testnet-pulsepairs.rainwins.com`
45
+ > hosts are retired; there is no Arbitrum Sepolia deployment.
33
46
 
34
47
  ---
35
48
 
@@ -96,15 +109,28 @@ await ensureSettlementAllowance({
96
109
 
97
110
  const maxFee = (amount * BigInt(cfg.platformFeeBps + cfg.makerFeeBps)) / 10000n; // peak fee
98
111
  const nonce = freshNonce(); // CSPRNG; never Math.random/Date.now
112
+
113
+ // A MARKET order's signed `price` is its SLIPPAGE BOUND — the worst price it
114
+ // will accept — not a fill price. `price: 0` is REJECTED at submit ("Orders must
115
+ // specify a non-zero price"), so there is no unbounded market order to sign.
116
+ // Derive the bound from the current quote and pad it in CENTS: a percentage pad
117
+ // is worth almost nothing at long-shot prices, where these markets spend their
118
+ // final minutes (5% of an 8c option is 0.4c, against a 7.39c spread). 100 bps =
119
+ // 1 cent exactly. On a BUY the bound is reference + pad (the most you will pay);
120
+ // on a SELL it is reference − pad, because there the price is a FLOOR.
121
+ const reference = live.upPrice; // bps; use downPrice for DOWN
122
+ if (reference === null) throw new Error("no quote to bound against — refusing to guess");
123
+ const price = BigInt(Math.min(9999, Math.max(1, reference + 5 * 100))); // +5c
124
+
99
125
  const typedData = buildOrderTypedData({
100
126
  cfg, settlementAddress,
101
127
  message: { maker: account.address, market: BigInt(marketId), option: BigInt(Option.UP),
102
- side: OrderSide.BUY, type: OrderType.MARKET, price: 0n, amount, maxFee, nonce,
128
+ side: OrderSide.BUY, type: OrderType.MARKET, price, amount, maxFee, nonce,
103
129
  expiry: BigInt(live.endTime) },
104
130
  });
105
131
  const signature = await account.signTypedData(typedData);
106
132
  await api.postOrder({ maker: account.address, market: live.address, option: Option.UP,
107
- side: OrderSide.BUY, type: OrderType.MARKET, price: 0, amount: amount.toString(),
133
+ side: OrderSide.BUY, type: OrderType.MARKET, price: Number(price), amount: amount.toString(),
108
134
  maxFee: maxFee.toString(), nonce: Number(nonce), expiry: live.endTime, signature });
109
135
  ```
110
136
 
@@ -330,7 +356,7 @@ number. `getPnl` assembles one, marking each position:
330
356
  flagged `markSource: "cost"` so you can tell it apart).
331
357
 
332
358
  ```ts
333
- const client = new UpDownHttpClient("https://api.demo-pulsepairs.rainwins.com");
359
+ const client = new UpDownHttpClient("https://dev-api-updown.rain.trade");
334
360
 
335
361
  const pnl = await client.getPnl(wallet, { includeFees: true });
336
362
  console.log(pnl.unrealizedPnlUsdt, pnl.roiPct); // e.g. -3, -30
package/dist/eip712.d.ts CHANGED
@@ -225,19 +225,24 @@ export declare function bpsToCents(bps: number): number;
225
225
  * 51¢. Lowered from $5 → $1 on 2026-07-23 (Polymarket parity).
226
226
  */
227
227
  export declare const MIN_STAKE_ATOMIC = 1000000n;
228
- export declare const MAX_STAKE_ATOMIC = 500000000n;
229
228
  /** Parse a USD string (e.g. "5", "5.50", "12.345") into atomic USDT.
230
229
  * Rejects negatives + non-finite. */
231
230
  export declare function parseStake(usd: string | number): bigint;
232
231
  /**
233
232
  * Defense-in-depth stake clamp. Throws if `amountAtomic` (share face value)
234
- * is outside the documented `$1 ≤ stake ≤ $500` window; the backend applies
235
- * the same window at the API boundary. SDK callers should validate before
236
- * signing so a bad stake never produces a signed payload at all.
233
+ * is below the documented `$1` entry floor; the backend applies the same
234
+ * floor at the API boundary. SDK callers should validate before signing so a
235
+ * bad stake never produces a signed payload at all.
237
236
  *
238
237
  * Pass `side: "SELL"` for exits: the minimum is ENTRY-ONLY, so a holder left
239
238
  * with a sub-$1 position (routine after a partial fill) can always sell it.
240
239
  * The default stays "BUY" so existing callers keep the strict branch.
240
+ *
241
+ * 2026-08-03: the $500 ceiling was REMOVED (backend `lib/stakeBounds.ts` did
242
+ * the same). Size is bounded by collateral — the venue's pre-match gate reads
243
+ * balance AND allowance on-chain and refuses a cross it cannot settle — so the
244
+ * cap was never what limited risk, only what limited legitimate size. Callers
245
+ * upgrading from <=0.7.0 will find previously-rejected amounts now sign.
241
246
  */
242
247
  export declare function assertStakeBounds(amountAtomic: bigint, side?: "BUY" | "SELL"): void;
243
248
  /**
package/dist/eip712.js CHANGED
@@ -227,7 +227,6 @@ export function bpsToCents(bps) {
227
227
  * 51¢. Lowered from $5 → $1 on 2026-07-23 (Polymarket parity).
228
228
  */
229
229
  export const MIN_STAKE_ATOMIC = 1000000n;
230
- export const MAX_STAKE_ATOMIC = 500000000n;
231
230
  /** Parse a USD string (e.g. "5", "5.50", "12.345") into atomic USDT.
232
231
  * Rejects negatives + non-finite. */
233
232
  export function parseStake(usd) {
@@ -239,21 +238,24 @@ export function parseStake(usd) {
239
238
  }
240
239
  /**
241
240
  * Defense-in-depth stake clamp. Throws if `amountAtomic` (share face value)
242
- * is outside the documented `$1 ≤ stake ≤ $500` window; the backend applies
243
- * the same window at the API boundary. SDK callers should validate before
244
- * signing so a bad stake never produces a signed payload at all.
241
+ * is below the documented `$1` entry floor; the backend applies the same
242
+ * floor at the API boundary. SDK callers should validate before signing so a
243
+ * bad stake never produces a signed payload at all.
245
244
  *
246
245
  * Pass `side: "SELL"` for exits: the minimum is ENTRY-ONLY, so a holder left
247
246
  * with a sub-$1 position (routine after a partial fill) can always sell it.
248
247
  * The default stays "BUY" so existing callers keep the strict branch.
248
+ *
249
+ * 2026-08-03: the $500 ceiling was REMOVED (backend `lib/stakeBounds.ts` did
250
+ * the same). Size is bounded by collateral — the venue's pre-match gate reads
251
+ * balance AND allowance on-chain and refuses a cross it cannot settle — so the
252
+ * cap was never what limited risk, only what limited legitimate size. Callers
253
+ * upgrading from <=0.7.0 will find previously-rejected amounts now sign.
249
254
  */
250
255
  export function assertStakeBounds(amountAtomic, side = "BUY") {
251
256
  if (side === "BUY" && amountAtomic < MIN_STAKE_ATOMIC) {
252
257
  throw new Error(`stake below $${Number(MIN_STAKE_ATOMIC) / 1e6} minimum`);
253
258
  }
254
- if (amountAtomic > MAX_STAKE_ATOMIC) {
255
- throw new Error(`stake above $${Number(MAX_STAKE_ATOMIC) / 1e6} maximum`);
256
- }
257
259
  }
258
260
  /**
259
261
  * Probability-weighted fee in atomic USDT. Mirrors the backend / contract
package/dist/http.d.ts CHANGED
@@ -10,8 +10,20 @@ export declare class UpDownHttpClient {
10
10
  uptime?: number;
11
11
  }>;
12
12
  getConfig(): Promise<ApiConfig>;
13
+ /**
14
+ * List markets.
15
+ *
16
+ * `timeframe` narrowed from `300 | 900 | 3600` to `300 | 900`: 60m was
17
+ * disabled on the cyclers 2026-08-18, so no new 3600s markets exist to list.
18
+ *
19
+ * The narrowing is a COMPILE-TIME steer, not a runtime break. The backend
20
+ * still accepts `?timeframe=3600` and returns the historical 60m markets, so
21
+ * a caller pinned to an older SDK keeps working and anything reading history
22
+ * can pass 3600 through a cast. New code gets a type error instead of a
23
+ * silently empty list, which is the failure that is hard to diagnose.
24
+ */
13
25
  getMarkets(opts?: {
14
- timeframe?: 300 | 900 | 3600;
26
+ timeframe?: 300 | 900;
15
27
  pair?: PairSymbol;
16
28
  }): Promise<MarketListItem[]>;
17
29
  getMarket(address: string): Promise<MarketDetail>;
package/dist/http.js CHANGED
@@ -46,6 +46,18 @@ export class UpDownHttpClient {
46
46
  const res = await fetch(buildUrl(this.baseUrl, "/config"));
47
47
  return parseJson(res);
48
48
  }
49
+ /**
50
+ * List markets.
51
+ *
52
+ * `timeframe` narrowed from `300 | 900 | 3600` to `300 | 900`: 60m was
53
+ * disabled on the cyclers 2026-08-18, so no new 3600s markets exist to list.
54
+ *
55
+ * The narrowing is a COMPILE-TIME steer, not a runtime break. The backend
56
+ * still accepts `?timeframe=3600` and returns the historical 60m markets, so
57
+ * a caller pinned to an older SDK keeps working and anything reading history
58
+ * can pass 3600 through a cast. New code gets a type error instead of a
59
+ * silently empty list, which is the failure that is hard to diagnose.
60
+ */
49
61
  async getMarkets(opts) {
50
62
  const res = await fetch(buildUrl(this.baseUrl, "/markets", {
51
63
  timeframe: opts?.timeframe,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { UpDownHttpClient, wsUrlFromHttpBase } from "./http.js";
2
2
  export { UpDownWsClient, type UpDownWsMessage, type SubscribePayload, type WsAuthCredentials, type ConnectAuthedOptions, } from "./ws.js";
3
- export { ORDER_TYPES, CANCEL_TYPES, WS_AUTH_TYPES, buildOrderTypedData, buildCancelTypedData, buildWsAuthTypedData, freshSessionId, freshNonce, domainForSettlement, findPairBySettlement, parseCompositeMarketKey, centsToBps, bpsToCents, parseStake, assertStakeBounds, feeAtomic, MIN_STAKE_ATOMIC, MAX_STAKE_ATOMIC, type OrderSignMessage, type CancelSignMessage, type WsAuthMessage, type ParsedComposite, } from "./eip712.js";
3
+ export { ORDER_TYPES, CANCEL_TYPES, WS_AUTH_TYPES, buildOrderTypedData, buildCancelTypedData, buildWsAuthTypedData, freshSessionId, freshNonce, domainForSettlement, findPairBySettlement, parseCompositeMarketKey, centsToBps, bpsToCents, parseStake, assertStakeBounds, feeAtomic, MIN_STAKE_ATOMIC, type OrderSignMessage, type CancelSignMessage, type WsAuthMessage, type ParsedComposite, } from "./eip712.js";
4
4
  export { CLOB_AUTH_TYPES, CLOB_AUTH_MESSAGE, buildClobAuthTypedData, buildHmacSignature, HMAC_HEADERS, type ClobAuthDomain, type ClobAuthMessage, } from "./auth.js";
5
5
  export { ensureSettlementAllowance, MAX_UINT256, DEFAULT_APPROVAL_AMOUNT, type EnsureAllowanceResult, } from "./approve.js";
6
6
  export { readOnChainHolderShares, reconcileFills, reportedFromPositions, type OnChainHolderShares, type ShareReconciliation, type ReconcileStatus, type FillReconciliationReport, } from "./reconcile.js";
package/dist/index.js CHANGED
@@ -2,7 +2,11 @@
2
2
  export { UpDownHttpClient, wsUrlFromHttpBase } from "./http.js";
3
3
  export { UpDownWsClient, } from "./ws.js";
4
4
  // EIP-712 helpers
5
- export { ORDER_TYPES, CANCEL_TYPES, WS_AUTH_TYPES, buildOrderTypedData, buildCancelTypedData, buildWsAuthTypedData, freshSessionId, freshNonce, domainForSettlement, findPairBySettlement, parseCompositeMarketKey, centsToBps, bpsToCents, parseStake, assertStakeBounds, feeAtomic, MIN_STAKE_ATOMIC, MAX_STAKE_ATOMIC, } from "./eip712.js";
5
+ export { ORDER_TYPES, CANCEL_TYPES, WS_AUTH_TYPES, buildOrderTypedData, buildCancelTypedData, buildWsAuthTypedData, freshSessionId, freshNonce, domainForSettlement, findPairBySettlement, parseCompositeMarketKey, centsToBps, bpsToCents, parseStake, assertStakeBounds, feeAtomic,
6
+ // MAX_STAKE_ATOMIC was removed in 0.8.0 — the venue no longer caps order
7
+ // size. Importing it is now a compile error rather than a silent behaviour
8
+ // change, which is the intended way for a consumer to learn the cap is gone.
9
+ MIN_STAKE_ATOMIC, } from "./eip712.js";
6
10
  // Phase 3 / Gate 1 — L2 HMAC auth helpers
7
11
  export { CLOB_AUTH_TYPES, CLOB_AUTH_MESSAGE, buildClobAuthTypedData, buildHmacSignature, HMAC_HEADERS, } from "./auth.js";
8
12
  // Approve helper
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * UpDown SDK type surface — mirrors the backend's REST + WS response shapes
3
- * one-to-one. Keep field names and casing in sync with `docs/api.md`; if a
3
+ * one-to-one. Keep field names and casing in sync with `docs/archive/api-superseded-2026-08-12.md`; if a
4
4
  * shape drifts, update both in the same commit.
5
5
  */
6
6
  export type PairSymbol = "BTC-USD" | "ETH-USD";
package/dist/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * UpDown SDK type surface — mirrors the backend's REST + WS response shapes
3
- * one-to-one. Keep field names and casing in sync with `docs/api.md`; if a
3
+ * one-to-one. Keep field names and casing in sync with `docs/archive/api-superseded-2026-08-12.md`; if a
4
4
  * shape drifts, update both in the same commit.
5
5
  */
6
6
  /** Numeric enum values match the backend's on-chain encoding. */
package/dist/ws.js CHANGED
@@ -212,7 +212,48 @@ export class UpDownWsClient {
212
212
  const delay = attempt > 12 ? 30_000 : Math.min(30_000, 1000 * 2 ** Math.min(attempt, 5));
213
213
  this.timer = setTimeout(() => this.openSocket(), delay);
214
214
  };
215
- ws.onerror = () => ws.close();
215
+ // NEVER call close() synchronously from here.
216
+ //
217
+ // `onerror = () => ws.close()` stood here through 0.8.0 and took
218
+ // LedgerCore's process down twice against our 502s:
219
+ // "RangeError: Maximum call stack size exceeded" at dist/ws.js:215. On a
220
+ // WebSocket implementation that fails a CONNECTING socket SYNCHRONOUSLY,
221
+ // close() dispatches `error` before it returns, which re-enters this
222
+ // handler, which calls close() again — unbounded recursion on the stack
223
+ // rather than a loop that backoff could ever slow down. A handshake that
224
+ // fails fast, like a 502 from a load balancer, is exactly the trigger.
225
+ //
226
+ // Node's built-in WebSocket and the `ws` package both defer that error to a
227
+ // later tick and so survive it, which is why this went unnoticed here: the
228
+ // bug is invisible on the runtimes we test on and fatal on the ones we
229
+ // don't. The guard below therefore does not depend on knowing which
230
+ // implementation is underneath.
231
+ //
232
+ // The close is also redundant on every conforming implementation: a socket
233
+ // that emits `error` emits `close` afterwards, and `onclose` already owns
234
+ // reconnect. It is kept only for an implementation that errors without ever
235
+ // closing, deferred to a later tick where re-entry cannot grow the stack.
236
+ let errorHandled = false;
237
+ ws.onerror = () => {
238
+ if (errorHandled)
239
+ return; // re-entry is the bug; make it impossible
240
+ errorHandled = true;
241
+ if (this.closed || this.ws !== ws)
242
+ return;
243
+ setTimeout(() => {
244
+ // `onclose` normally fires on its own and has already scheduled the
245
+ // reconnect. Only force it if this socket errored and then just sat
246
+ // there, which would otherwise strand the client with no retry.
247
+ if (ws.readyState === 3 /* CLOSED */)
248
+ return;
249
+ try {
250
+ ws.close();
251
+ }
252
+ catch {
253
+ /* ignore */
254
+ }
255
+ }, 0);
256
+ };
216
257
  }
217
258
  async startHandshake() {
218
259
  if (!this.mode || !this.ws)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulsepairs/sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Standalone SDK for the UpDown (PulsePairs) up/down prediction markets \u2014 matcher REST/WS client, EIP-712 order signing, trade-math, and Alchemy Account Kit (smart-account) order signing for rain.trade integration.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "scripts": {
35
35
  "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
36
36
  "build": "npm run clean && tsc",
37
- "test": "node scripts/eip712-golden.test.mjs && node scripts/rawtx-tier.test.mjs && node scripts/hot-key-safety.test.mjs && node scripts/reconcile.test.mjs && node scripts/pnl.test.mjs && node scripts/ws-subscribe.test.mjs",
37
+ "test": "node scripts/eip712-golden.test.mjs && node scripts/rawtx-tier.test.mjs && node scripts/hot-key-safety.test.mjs && node scripts/reconcile.test.mjs && node scripts/pnl.test.mjs && node scripts/ws-subscribe.test.mjs && node scripts/ws-onerror.test.mjs",
38
38
  "prepublishOnly": "npm run build && npm test",
39
39
  "example:taker": "npx tsx examples/simple-taker.ts",
40
40
  "example:maker": "npx tsx examples/simple-maker.ts",
@@ -62,6 +62,10 @@
62
62
  }
63
63
  },
64
64
  "devDependencies": {
65
+ "@aa-sdk/core": "^4.88.5",
66
+ "@account-kit/infra": "^4.88.5",
67
+ "@account-kit/smart-contracts": "^4.88.5",
68
+ "@account-kit/wallet-client": "^4.88.5",
65
69
  "@types/node": "^22.10.0",
66
70
  "typescript": "^5.9.3",
67
71
  "viem": "^2.47.0"