perp-cli 0.14.2 → 0.15.0

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.
Files changed (313) hide show
  1. package/README.md +152 -13
  2. package/dist/agent-wallet/store.d.ts +3 -2
  3. package/dist/agent-wallet/store.js +2 -2
  4. package/dist/api/public/hyperliquid.js +4 -0
  5. package/dist/api/public/pacifica.js +83 -13
  6. package/dist/api/public/urls.d.ts +2 -0
  7. package/dist/api/public/urls.js +3 -0
  8. package/dist/arb/utils.d.ts +1 -2
  9. package/dist/arb/utils.js +1 -2
  10. package/dist/bot/apex/pulse.js +3 -0
  11. package/dist/bot/apex/radar.js +2 -1
  12. package/dist/bot/engine.d.ts +10 -1
  13. package/dist/bot/engine.js +14 -6
  14. package/dist/bot/indicators.d.ts +7 -0
  15. package/dist/bot/indicators.js +16 -0
  16. package/dist/bot/perp-order.d.ts +4 -4
  17. package/dist/bot/perp-order.js +5 -5
  18. package/dist/bot/quarantine.d.ts +1 -1
  19. package/dist/bot/quarantine.js +1 -1
  20. package/dist/bot/strategies/apex-strategy.js +3 -2
  21. package/dist/bot/strategies/engine-mm.js +6 -5
  22. package/dist/bot/strategies/funding-arb-strategy.js +9 -9
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
  24. package/dist/bot/strategies/funding-arb-utils.js +43 -24
  25. package/dist/bot/strategies/funding-auto.js +2 -2
  26. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  27. package/dist/bot/strategies/liquidation-mm.js +8 -2
  28. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  29. package/dist/bot/strategies/momentum-breakout.js +11 -3
  30. package/dist/bot/strategies/spot-perp-arb-strategy.js +23 -12
  31. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  32. package/dist/bot/strategies/spot-perp-executor.js +1 -1
  33. package/dist/bot/strategy-types.d.ts +2 -1
  34. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  35. package/dist/bot/tui/BotDashboard.js +1 -1
  36. package/dist/cache.js +12 -6
  37. package/dist/cli-program.d.ts +15 -0
  38. package/dist/cli-program.js +125 -5
  39. package/dist/commands/agent-import.d.ts +7 -0
  40. package/dist/commands/agent-import.js +30 -1
  41. package/dist/commands/agent.d.ts +6 -3
  42. package/dist/commands/agent.js +160 -12
  43. package/dist/commands/arb/index.js +4 -4
  44. package/dist/commands/arb-auto.js +21 -17
  45. package/dist/commands/arb.d.ts +12 -0
  46. package/dist/commands/arb.js +61 -143
  47. package/dist/commands/bot.js +1 -1
  48. package/dist/commands/builder.js +1 -1
  49. package/dist/commands/funds.js +113 -3
  50. package/dist/commands/init.d.ts +1 -1
  51. package/dist/commands/init.js +27 -7
  52. package/dist/commands/manage.js +23 -21
  53. package/dist/commands/market.js +168 -36
  54. package/dist/commands/options.d.ts +9 -0
  55. package/dist/commands/options.js +134 -0
  56. package/dist/commands/pacifica-earn.d.ts +5 -0
  57. package/dist/commands/pacifica-earn.js +468 -0
  58. package/dist/commands/pacifica-orders.d.ts +14 -0
  59. package/dist/commands/pacifica-orders.js +142 -0
  60. package/dist/commands/portfolio.js +32 -47
  61. package/dist/commands/rebalance.d.ts +1 -0
  62. package/dist/commands/rebalance.js +15 -0
  63. package/dist/commands/risk.d.ts +11 -2
  64. package/dist/commands/risk.js +39 -44
  65. package/dist/commands/trade.d.ts +1 -0
  66. package/dist/commands/trade.js +63 -58
  67. package/dist/commands/wallet.js +1 -1
  68. package/dist/config.d.ts +2 -2
  69. package/dist/config.js +15 -4
  70. package/dist/constants.js +6 -0
  71. package/dist/dashboard/server.js +1 -1
  72. package/dist/dashboard/ui.js +1 -1
  73. package/dist/errors.d.ts +24 -1
  74. package/dist/errors.js +25 -3
  75. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  76. package/dist/exchanges/arcus/adapter.js +12 -12
  77. package/dist/exchanges/arcus/agent.d.ts +1 -1
  78. package/dist/exchanges/arcus/agent.js +1 -1
  79. package/dist/exchanges/arcus/client.js +1 -1
  80. package/dist/exchanges/arcus/signing.d.ts +2 -3
  81. package/dist/exchanges/arcus/signing.js +1 -1
  82. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  83. package/dist/exchanges/arcus/spot-chain.js +1 -1
  84. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  85. package/dist/exchanges/arcus/spot-router.js +2 -2
  86. package/dist/exchanges/arcus/spot.d.ts +2 -3
  87. package/dist/exchanges/arcus/spot.js +2 -3
  88. package/dist/exchanges/aster-spot.d.ts +3 -3
  89. package/dist/exchanges/aster-spot.js +6 -9
  90. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  91. package/dist/exchanges/aster-typed-data.js +1 -1
  92. package/dist/exchanges/aster.d.ts +9 -4
  93. package/dist/exchanges/aster.js +125 -55
  94. package/dist/exchanges/builder-fee.d.ts +1 -1
  95. package/dist/exchanges/builder-fee.js +13 -2
  96. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  97. package/dist/exchanges/canonical-symbols.js +26 -2
  98. package/dist/exchanges/derive/client.d.ts +37 -0
  99. package/dist/exchanges/derive/client.js +135 -0
  100. package/dist/exchanges/derive/options.d.ts +68 -0
  101. package/dist/exchanges/derive/options.js +360 -0
  102. package/dist/exchanges/extended/adapter.d.ts +82 -0
  103. package/dist/exchanges/extended/adapter.js +179 -0
  104. package/dist/exchanges/extended/client.d.ts +38 -0
  105. package/dist/exchanges/extended/client.js +88 -0
  106. package/dist/exchanges/extended/public.d.ts +130 -0
  107. package/dist/exchanges/extended/public.js +429 -0
  108. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  109. package/dist/exchanges/grvt/adapter.js +206 -0
  110. package/dist/exchanges/grvt/client.d.ts +25 -0
  111. package/dist/exchanges/grvt/client.js +86 -0
  112. package/dist/exchanges/grvt/public.d.ts +151 -0
  113. package/dist/exchanges/grvt/public.js +438 -0
  114. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  115. package/dist/exchanges/hyperliquid-spot.js +5 -5
  116. package/dist/exchanges/hyperliquid.d.ts +22 -16
  117. package/dist/exchanges/hyperliquid.js +80 -55
  118. package/dist/exchanges/index.d.ts +1 -1
  119. package/dist/exchanges/index.js +1 -1
  120. package/dist/exchanges/interface.d.ts +97 -8
  121. package/dist/exchanges/interface.js +19 -2
  122. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  123. package/dist/exchanges/ioc-outcome.js +1 -1
  124. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  125. package/dist/exchanges/legacy-trading-v2.js +7 -2
  126. package/dist/exchanges/lighter-public.d.ts +2 -2
  127. package/dist/exchanges/lighter-public.js +2 -2
  128. package/dist/exchanges/lighter-signer.d.ts +1 -1
  129. package/dist/exchanges/lighter-signer.js +1 -1
  130. package/dist/exchanges/lighter-spot.d.ts +3 -4
  131. package/dist/exchanges/lighter-spot.js +6 -7
  132. package/dist/exchanges/lighter.d.ts +11 -11
  133. package/dist/exchanges/lighter.js +28 -14
  134. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  135. package/dist/exchanges/ondo/adapter.js +16 -14
  136. package/dist/exchanges/ondo/agent.d.ts +1 -1
  137. package/dist/exchanges/ondo/agent.js +1 -1
  138. package/dist/exchanges/ondo/client.d.ts +1 -1
  139. package/dist/exchanges/ondo/client.js +2 -2
  140. package/dist/exchanges/ondo/public.d.ts +8 -1
  141. package/dist/exchanges/ondo/public.js +13 -2
  142. package/dist/exchanges/ondo/spot.d.ts +2 -2
  143. package/dist/exchanges/ondo/spot.js +3 -3
  144. package/dist/exchanges/options-interface.d.ts +158 -0
  145. package/dist/exchanges/options-interface.js +17 -0
  146. package/dist/exchanges/options-venues.d.ts +12 -0
  147. package/dist/exchanges/options-venues.js +35 -0
  148. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  149. package/dist/exchanges/pacifica-atomic.js +440 -0
  150. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  151. package/dist/exchanges/pacifica-decimal.js +51 -0
  152. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  153. package/dist/exchanges/pacifica-presigned.js +141 -0
  154. package/dist/exchanges/pacifica-print.d.ts +200 -0
  155. package/dist/exchanges/pacifica-print.js +417 -0
  156. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  157. package/dist/exchanges/pacifica-spot.js +3 -3
  158. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  159. package/dist/exchanges/pacifica-typed-data.js +1 -1
  160. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  161. package/dist/exchanges/pacifica-vaults.js +264 -0
  162. package/dist/exchanges/pacifica.d.ts +32 -5
  163. package/dist/exchanges/pacifica.js +114 -48
  164. package/dist/exchanges/precision.d.ts +3 -1
  165. package/dist/exchanges/precision.js +9 -2
  166. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  167. package/dist/exchanges/qfex/adapter.js +32 -12
  168. package/dist/exchanges/qfex/agent.d.ts +1 -1
  169. package/dist/exchanges/qfex/agent.js +1 -1
  170. package/dist/exchanges/qfex/client.d.ts +1 -1
  171. package/dist/exchanges/qfex/client.js +1 -1
  172. package/dist/exchanges/qfex/errors.d.ts +1 -1
  173. package/dist/exchanges/qfex/errors.js +2 -2
  174. package/dist/exchanges/qfex/public.d.ts +3 -3
  175. package/dist/exchanges/qfex/public.js +3 -3
  176. package/dist/exchanges/qfex/signing.d.ts +2 -2
  177. package/dist/exchanges/qfex/signing.js +4 -5
  178. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  179. package/dist/exchanges/qfex/ws-transport.js +3 -3
  180. package/dist/exchanges/rate-limiter.d.ts +1 -1
  181. package/dist/exchanges/rate-limiter.js +5 -2
  182. package/dist/exchanges/registry.d.ts +1 -1
  183. package/dist/exchanges/registry.js +1 -1
  184. package/dist/exchanges/risex/adapter.d.ts +148 -0
  185. package/dist/exchanges/risex/adapter.js +796 -0
  186. package/dist/exchanges/risex/agent.d.ts +154 -0
  187. package/dist/exchanges/risex/agent.js +384 -0
  188. package/dist/exchanges/risex/client.d.ts +47 -0
  189. package/dist/exchanges/risex/client.js +174 -0
  190. package/dist/exchanges/risex/nonce.d.ts +43 -0
  191. package/dist/exchanges/risex/nonce.js +212 -0
  192. package/dist/exchanges/risex/public.d.ts +64 -0
  193. package/dist/exchanges/risex/public.js +150 -0
  194. package/dist/exchanges/risex/session.d.ts +71 -0
  195. package/dist/exchanges/risex/session.js +171 -0
  196. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  197. package/dist/exchanges/risex/typed-data.js +240 -0
  198. package/dist/exchanges/risex/units.d.ts +29 -0
  199. package/dist/exchanges/risex/units.js +104 -0
  200. package/dist/exchanges/slippage.d.ts +1 -1
  201. package/dist/exchanges/slippage.js +1 -1
  202. package/dist/exchanges/spot-factory.d.ts +3 -3
  203. package/dist/exchanges/spot-factory.js +7 -7
  204. package/dist/exchanges/spot-interface.d.ts +5 -5
  205. package/dist/exchanges/spot-interface.js +2 -2
  206. package/dist/exchanges/spot-result.d.ts +3 -3
  207. package/dist/exchanges/spot-result.js +3 -3
  208. package/dist/exchanges/trading-v2.d.ts +3 -3
  209. package/dist/exchanges/trading-v2.js +9 -6
  210. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  211. package/dist/exchanges/txflow/adapter.js +118 -0
  212. package/dist/exchanges/txflow/client.d.ts +47 -0
  213. package/dist/exchanges/txflow/client.js +153 -0
  214. package/dist/exchanges/txflow/public.d.ts +76 -0
  215. package/dist/exchanges/txflow/public.js +284 -0
  216. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  217. package/dist/exchanges/unknown-symbol.js +51 -0
  218. package/dist/exchanges/variational/adapter.d.ts +71 -0
  219. package/dist/exchanges/variational/adapter.js +137 -0
  220. package/dist/exchanges/variational/client.d.ts +21 -0
  221. package/dist/exchanges/variational/client.js +66 -0
  222. package/dist/exchanges/variational/public.d.ts +126 -0
  223. package/dist/exchanges/variational/public.js +430 -0
  224. package/dist/exchanges/venue-types.d.ts +6 -7
  225. package/dist/exchanges/venue-types.js +2 -2
  226. package/dist/exchanges/venue.d.ts +51 -8
  227. package/dist/exchanges/venue.js +54 -4
  228. package/dist/exchanges/venues/arcus.d.ts +1 -2
  229. package/dist/exchanges/venues/arcus.js +5 -4
  230. package/dist/exchanges/venues/aster.d.ts +2 -2
  231. package/dist/exchanges/venues/aster.js +6 -4
  232. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  233. package/dist/exchanges/venues/create-helpers.js +3 -3
  234. package/dist/exchanges/venues/extended.d.ts +21 -0
  235. package/dist/exchanges/venues/extended.js +96 -0
  236. package/dist/exchanges/venues/grvt.d.ts +30 -0
  237. package/dist/exchanges/venues/grvt.js +98 -0
  238. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  239. package/dist/exchanges/venues/hyperliquid.js +10 -8
  240. package/dist/exchanges/venues/index.d.ts +7 -2
  241. package/dist/exchanges/venues/index.js +8 -2
  242. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  243. package/dist/exchanges/venues/lighter-family.js +9 -11
  244. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  245. package/dist/exchanges/venues/lighter-rh.js +7 -5
  246. package/dist/exchanges/venues/lighter.d.ts +1 -1
  247. package/dist/exchanges/venues/lighter.js +5 -3
  248. package/dist/exchanges/venues/ondo.d.ts +1 -1
  249. package/dist/exchanges/venues/ondo.js +5 -3
  250. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  251. package/dist/exchanges/venues/pacifica.js +6 -4
  252. package/dist/exchanges/venues/qfex.d.ts +1 -1
  253. package/dist/exchanges/venues/qfex.js +4 -2
  254. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  255. package/dist/exchanges/venues/risex-release.js +14 -0
  256. package/dist/exchanges/venues/risex.d.ts +31 -0
  257. package/dist/exchanges/venues/risex.js +116 -0
  258. package/dist/exchanges/venues/txflow.d.ts +18 -0
  259. package/dist/exchanges/venues/txflow.js +77 -0
  260. package/dist/exchanges/venues/variational.d.ts +21 -0
  261. package/dist/exchanges/venues/variational.js +85 -0
  262. package/dist/execution-log.d.ts +1 -1
  263. package/dist/funding/legacy-public.d.ts +1 -1
  264. package/dist/funding/legacy-public.js +9 -4
  265. package/dist/funding/normalize.d.ts +1 -1
  266. package/dist/funding/normalize.js +1 -1
  267. package/dist/funding/rates.d.ts +44 -4
  268. package/dist/funding/rates.js +56 -12
  269. package/dist/index.js +7 -6
  270. package/dist/jobs.d.ts +1 -0
  271. package/dist/jobs.js +25 -1
  272. package/dist/landing.d.ts +2 -0
  273. package/dist/landing.js +1 -1
  274. package/dist/mcp-pacifica-earn.d.ts +9 -0
  275. package/dist/mcp-pacifica-earn.js +58 -0
  276. package/dist/mcp-server.js +113 -59
  277. package/dist/pacifica/client.d.ts +67 -4
  278. package/dist/pacifica/client.js +170 -8
  279. package/dist/pacifica/rate-limit.d.ts +144 -0
  280. package/dist/pacifica/rate-limit.js +267 -0
  281. package/dist/pacifica/types/order.d.ts +2 -1
  282. package/dist/plan-executor.js +7 -2
  283. package/dist/rebalance.js +1 -1
  284. package/dist/risk.d.ts +2 -1
  285. package/dist/risk.js +2 -4
  286. package/dist/settings.d.ts +32 -3
  287. package/dist/signer/ows-evm.d.ts +1 -1
  288. package/dist/signer/ows-evm.js +1 -1
  289. package/dist/smart-order.d.ts +1 -1
  290. package/dist/smart-order.js +1 -1
  291. package/dist/strategies/funding-arb.d.ts +1 -1
  292. package/dist/strategies/funding-arb.js +1 -1
  293. package/dist/trade-validator.d.ts +1 -1
  294. package/dist/trade-validator.js +9 -4
  295. package/dist/utils.d.ts +12 -3
  296. package/dist/utils.js +11 -3
  297. package/dist/venue-health.d.ts +40 -0
  298. package/dist/venue-health.js +75 -0
  299. package/dist/withdraw-gate.d.ts +1 -1
  300. package/dist/withdraw-gate.js +1 -1
  301. package/dist/ws/index.d.ts +959 -0
  302. package/dist/ws/index.js +4295 -0
  303. package/dist/ws/node.d.ts +975 -0
  304. package/dist/ws/node.js +4366 -0
  305. package/dist/ws/pacifica-instrument.d.ts +8 -0
  306. package/dist/ws/pacifica-instrument.js +8 -0
  307. package/dist/ws-cli/credentials.d.ts +62 -0
  308. package/dist/ws-cli/credentials.js +126 -0
  309. package/package.json +58 -10
  310. package/skills/perp-cli/SKILL.md +62 -9
  311. package/skills/perp-cli/references/agent-operations.md +7 -0
  312. package/skills/perp-cli/references/commands.md +76 -5
  313. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -0,0 +1,4366 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ // src/ws/node.ts
4
+
5
+ // src/ws/errors.ts
6
+ var WS_ERROR_CODES = {
7
+ WS_PROTOCOL_ERROR: { status: 502, retryable: false },
8
+ WS_AUTH_FAILED: { status: 401, retryable: false },
9
+ SUBSCRIPTION_LIMIT: { status: 400, retryable: false },
10
+ STREAM_OVERFLOW: { status: 507, retryable: false },
11
+ CAPABILITY_UNSUPPORTED: { status: 400, retryable: false },
12
+ EXCHANGE_UNREACHABLE: { status: 503, retryable: true },
13
+ INVALID_PARAMS: { status: 400, retryable: false },
14
+ RATE_LIMITED: { status: 429, retryable: true },
15
+ EXCHANGE_ERROR: { status: 502, retryable: true },
16
+ SYMBOL_NOT_FOUND: { status: 404, retryable: false }
17
+ };
18
+ var WsError = class extends Error {
19
+ constructor(code, message, opts) {
20
+ super(message);
21
+ this.name = "WsError";
22
+ this.structured = {
23
+ code,
24
+ message,
25
+ status: WS_ERROR_CODES[code].status,
26
+ retryable: WS_ERROR_CODES[code].retryable,
27
+ ...opts?.exchange !== void 0 ? { exchange: opts.exchange } : {},
28
+ ...opts?.remediation !== void 0 ? { remediation: opts.remediation } : {},
29
+ ...opts?.details !== void 0 ? { details: opts.details } : {}
30
+ };
31
+ }
32
+ };
33
+ function isPerpError(e) {
34
+ if (e === null || typeof e !== "object") return false;
35
+ const s = e.structured;
36
+ if (s === null || typeof s !== "object") return false;
37
+ return typeof s.code === "string" && typeof s.message === "string";
38
+ }
39
+
40
+ // src/ws/canonical.ts
41
+ var USD_LIKE_QUOTES = /* @__PURE__ */ new Set(["USDT", "USD1", "USDC", "USD", "USDG"]);
42
+ var defaultCanonicalize = (_venue, m) => {
43
+ const base = m.base.toUpperCase();
44
+ if (m.dex !== null && m.dex !== "") return `${m.dex}:${base}`;
45
+ if (m.quote === null) return null;
46
+ return USD_LIKE_QUOTES.has(m.quote.toUpperCase()) ? base : null;
47
+ };
48
+
49
+ // src/ws/transport.ts
50
+ function resolveTransport(opts, venue) {
51
+ if (opts.transport !== void 0) return opts.transport;
52
+ const Ctor = globalThis.WebSocket;
53
+ if (typeof Ctor !== "function") {
54
+ throw new WsError("INVALID_PARAMS", "No global WebSocket in this runtime (Node 20 has none)", {
55
+ exchange: venue,
56
+ remediation: "import from the /node entry (perp-cli/ws/node or @hypurrquant/perp-ws/node) or pass transport"
57
+ });
58
+ }
59
+ return (url) => {
60
+ const ws = new Ctor(url);
61
+ ws.binaryType = "arraybuffer";
62
+ return ws;
63
+ };
64
+ }
65
+ function resolveFetch(opts, venue) {
66
+ if (opts.fetch !== void 0) return opts.fetch;
67
+ const f = globalThis.fetch;
68
+ if (typeof f !== "function") {
69
+ throw new WsError("INVALID_PARAMS", "No global fetch in this runtime (needed for the market directory bootstrap)", {
70
+ exchange: venue,
71
+ remediation: "pass opts.fetch"
72
+ });
73
+ }
74
+ return (input, init) => f(input, init);
75
+ }
76
+ function frameText(data2) {
77
+ if (typeof data2 === "string") return data2;
78
+ const TD = globalThis.TextDecoder;
79
+ if (TD === void 0) return null;
80
+ if (data2 instanceof ArrayBuffer) return new TD().decode(data2);
81
+ if (ArrayBuffer.isView(data2)) return new TD().decode(new Uint8Array(data2.buffer, data2.byteOffset, data2.byteLength));
82
+ return null;
83
+ }
84
+ function defaultVisibility() {
85
+ const doc = globalThis.document;
86
+ if (doc === void 0 || typeof doc.visibilityState !== "string" || typeof doc.addEventListener !== "function") return null;
87
+ return {
88
+ hidden: () => doc.visibilityState === "hidden",
89
+ onChange: (cb) => {
90
+ doc.addEventListener?.("visibilitychange", cb);
91
+ return () => doc.removeEventListener?.("visibilitychange", cb);
92
+ }
93
+ };
94
+ }
95
+
96
+ // src/ws/connection.ts
97
+ var HANDSHAKE_REMEDIATION = "connection refused before open \u2014 network, venue outage, or Origin/CORS block (see capability matrix: browser)";
98
+ var MAX_PAYLOAD_REMEDIATION = "Raise the cap: createVenueStream(codec, { transport: createNodeTransport({ maxPayload: bytes }) }) from the /node entry";
99
+ function isMaxPayloadError(e) {
100
+ return e !== null && typeof e === "object" && e.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH";
101
+ }
102
+ var STALE_TICK_MS = 1e3;
103
+ var Connection = class {
104
+ constructor(o, h) {
105
+ this.o = o;
106
+ this.h = h;
107
+ this.state = { status: "idle" };
108
+ this.ws = null;
109
+ this.opened = false;
110
+ /** Consecutive failed attempts. Reset by the first data frame (markHealthy), not by open: a venue
111
+ * that accepts and then drops the socket must still exhaust reconnect.maxAttempts. */
112
+ this.attempt = 0;
113
+ this.sendTimes = [];
114
+ this.sendQueue = [];
115
+ this.sendTimer = null;
116
+ this.reconnectTimer = null;
117
+ this.staleTimer = null;
118
+ this.pingTimer = null;
119
+ this.awaitingPongSince = null;
120
+ this.visibilityOff = null;
121
+ this.terminal = false;
122
+ if (o.visibility !== null) {
123
+ const vis = o.visibility;
124
+ this.visibilityOff = vis.onChange(() => {
125
+ if (!vis.hidden()) this.onVisible();
126
+ });
127
+ }
128
+ }
129
+ get isOpen() {
130
+ return this.ws !== null && this.opened;
131
+ }
132
+ /** Open the socket when idle (lazily, on the first wire subscription). */
133
+ ensureOpen() {
134
+ if (this.terminal) return;
135
+ if (this.state.status === "idle") this.connectWithBudget();
136
+ }
137
+ send(msg) {
138
+ if (!this.isOpen) return;
139
+ this.sendQueue.push(msg);
140
+ this.flush();
141
+ }
142
+ /** A data frame arrived on this socket: the connection works, reset the failure count. */
143
+ markHealthy() {
144
+ if (this.isOpen) this.attempt = 0;
145
+ }
146
+ notePong() {
147
+ this.awaitingPongSince = null;
148
+ }
149
+ /** Re-check staleness now (also called when a stale channel receives a frame). */
150
+ evaluate() {
151
+ if (!this.isOpen) return;
152
+ const now = this.o.clock();
153
+ let worst = null;
154
+ for (const w of this.h.liveness()) {
155
+ if (w.staleAfterMs === null) continue;
156
+ const silent = now - w.lastAt;
157
+ const ratio = silent / w.staleAfterMs;
158
+ if (worst === null || ratio > worst.ratio) worst = { key: w.key, silent, ratio, stale: w.staleAfterMs };
159
+ }
160
+ if (worst !== null && worst.ratio > 2) {
161
+ this.forceReconnect({
162
+ code: "EXCHANGE_UNREACHABLE",
163
+ message: `No message on ${worst.key} for ${worst.silent} ms (> 2 \xD7 staleAfterMs ${worst.stale})`,
164
+ wsCode: null,
165
+ remediation: "Reconnecting automatically; check the network or venue status if it persists"
166
+ });
167
+ return;
168
+ }
169
+ if (worst !== null && worst.ratio > 1) {
170
+ if (this.state.status !== "stale" || this.state.channel !== worst.key) {
171
+ this.setState({ status: "stale", since: now - worst.silent + worst.stale, silentMs: worst.silent, channel: worst.key });
172
+ }
173
+ return;
174
+ }
175
+ if (this.state.status === "stale") this.setState({ status: "live", since: now, sockets: 1 });
176
+ }
177
+ /** Idle, with a connect deferred by the new-connection budget (ensureOpen while the budget was spent). */
178
+ get waiting() {
179
+ return !this.terminal && this.state.status === "idle" && this.reconnectTimer !== null;
180
+ }
181
+ /** Close the socket because no wire subscription is left; state → idle. Idle with a deferred
182
+ * connect (waiting): the connect is cancelled, so no socket opens without channels. */
183
+ closeIdle() {
184
+ if (this.terminal) return;
185
+ if (this.state.status === "idle") {
186
+ if (this.reconnectTimer !== null) {
187
+ clearTimeout(this.reconnectTimer);
188
+ this.reconnectTimer = null;
189
+ }
190
+ return;
191
+ }
192
+ this.teardown(1e3, "idle");
193
+ this.attempt = 0;
194
+ this.setState({ status: "idle" });
195
+ }
196
+ close() {
197
+ if (this.terminal) return;
198
+ this.terminal = true;
199
+ this.teardown(1e3, "client close");
200
+ if (this.visibilityOff !== null) {
201
+ this.visibilityOff();
202
+ this.visibilityOff = null;
203
+ }
204
+ this.setState({ status: "closed", reason: { code: "CLIENT_CLOSE", message: "closed by the client", wsCode: null, remediation: null } });
205
+ }
206
+ /** A fatal stop that is not a client close (e.g. reconnect attempts exhausted). */
207
+ fail(reason) {
208
+ if (this.terminal) return;
209
+ this.terminal = true;
210
+ this.teardown(1e3, "fatal");
211
+ if (this.visibilityOff !== null) {
212
+ this.visibilityOff();
213
+ this.visibilityOff = null;
214
+ }
215
+ this.setState({ status: "closed", reason });
216
+ }
217
+ // ── internals ──
218
+ setState(s) {
219
+ this.state = s;
220
+ this.h.log(
221
+ s.status === "closed" && s.reason.code !== "CLIENT_CLOSE" ? "error" : s.status === "reconnecting" || s.status === "stale" ? "warn" : "info",
222
+ "state",
223
+ `state \u2192 ${s.status}`,
224
+ stateDetails(s)
225
+ );
226
+ this.h.onState(s);
227
+ }
228
+ budgetDelay(now) {
229
+ const log = this.o.connectLog;
230
+ while (log.length > 0 && now - log[0] >= 6e4) log.shift();
231
+ if (log.length < this.o.newConnectionsPerMin) return 0;
232
+ return log[0] + 6e4 - now;
233
+ }
234
+ connectWithBudget() {
235
+ const now = this.o.clock();
236
+ const wait = this.budgetDelay(now);
237
+ if (wait > 0) {
238
+ this.h.log("warn", "connection-budget", `new-connection budget (${this.o.newConnectionsPerMin}/min) reached; delaying ${wait} ms`);
239
+ this.scheduleConnect(wait);
240
+ return;
241
+ }
242
+ this.connect();
243
+ }
244
+ scheduleConnect(ms) {
245
+ if (this.reconnectTimer !== null) clearTimeout(this.reconnectTimer);
246
+ this.reconnectTimer = setTimeout(() => {
247
+ this.reconnectTimer = null;
248
+ if (this.terminal) return;
249
+ this.connectWithBudget();
250
+ }, ms);
251
+ }
252
+ connect() {
253
+ this.o.connectLog.push(this.o.clock());
254
+ this.setState({ status: "connecting", attempt: this.attempt + 1 });
255
+ this.opened = false;
256
+ let ws;
257
+ try {
258
+ ws = this.o.transport(this.o.url);
259
+ } catch (e) {
260
+ if (isPerpError(e) && e.structured.retryable === false) {
261
+ const st = e.structured;
262
+ const code = st.code in WS_ERROR_CODES ? st.code : "INVALID_PARAMS";
263
+ this.fail({
264
+ code,
265
+ message: this.o.redact(`WebSocket transport failed: ${st.message}`),
266
+ wsCode: null,
267
+ remediation: typeof st.remediation === "string" ? this.o.redact(st.remediation) : null
268
+ });
269
+ return;
270
+ }
271
+ this.handleClose({
272
+ code: "EXCHANGE_UNREACHABLE",
273
+ message: this.o.redact(`WebSocket constructor failed: ${e instanceof Error ? e.message : String(e)}`),
274
+ wsCode: null,
275
+ remediation: HANDSHAKE_REMEDIATION
276
+ });
277
+ return;
278
+ }
279
+ this.ws = ws;
280
+ let socketError = null;
281
+ ws.onopen = () => {
282
+ if (this.ws !== ws) return;
283
+ this.opened = true;
284
+ this.awaitingPongSince = null;
285
+ this.setState({ status: "live", since: this.o.clock(), sockets: 1 });
286
+ this.startTimers();
287
+ this.h.onOpen();
288
+ };
289
+ ws.onmessage = (ev) => {
290
+ if (this.ws !== ws) return;
291
+ const recvTs = this.o.clock();
292
+ const text = frameText(ev.data);
293
+ if (text === null) this.h.onUnreadableFrame(recvTs);
294
+ else this.h.onText(text, recvTs);
295
+ };
296
+ ws.onerror = (ev) => {
297
+ if (this.ws !== ws) return;
298
+ socketError = ev;
299
+ this.h.log("warn", "socket-error", isMaxPayloadError(ev) ? "inbound message exceeded the transport's maxPayload (WS_ERR_UNSUPPORTED_MESSAGE_LENGTH); a close follows" : "socket error event (a close follows)");
300
+ };
301
+ ws.onclose = (ev) => {
302
+ if (this.ws !== ws) return;
303
+ const wasOpen = this.opened;
304
+ this.detach();
305
+ const tooLarge = isMaxPayloadError(socketError);
306
+ const reason = tooLarge ? {
307
+ code: "WS_PROTOCOL_ERROR",
308
+ message: `Socket closed: an inbound message exceeded the transport's maxPayload (WS_ERR_UNSUPPORTED_MESSAGE_LENGTH, sent close 1009)`,
309
+ wsCode: 1009,
310
+ remediation: MAX_PAYLOAD_REMEDIATION
311
+ } : wasOpen ? {
312
+ code: "EXCHANGE_UNREACHABLE",
313
+ message: this.o.redact(`Socket closed by the venue (code ${ev.code}${ev.reason ? `, ${ev.reason}` : ""})`),
314
+ wsCode: ev.code,
315
+ remediation: "Reconnecting automatically"
316
+ } : {
317
+ code: "EXCHANGE_UNREACHABLE",
318
+ message: this.o.redact(`Handshake failed before open (code ${ev.code}${ev.reason ? `, ${ev.reason}` : ""})`),
319
+ wsCode: ev.code,
320
+ remediation: HANDSHAKE_REMEDIATION
321
+ };
322
+ if (this.h.onUnexpectedClose(reason, wasOpen, tooLarge)) return;
323
+ this.handleClose(reason);
324
+ };
325
+ }
326
+ handleClose(reason) {
327
+ if (this.terminal) return;
328
+ if (this.state.status === "idle") return;
329
+ this.attempt++;
330
+ const max = this.o.reconnect.maxAttempts;
331
+ if (max !== null && this.attempt > max) {
332
+ this.fail({ ...reason, message: `${reason.message}; reconnect attempts exhausted (${max})` });
333
+ return;
334
+ }
335
+ const exp = Math.min(this.o.reconnect.maxMs, this.o.reconnect.baseMs * Math.pow(2, this.attempt - 1));
336
+ const backoff = Math.round(exp / 2 + this.o.random() * (exp / 2));
337
+ const inMs = Math.max(backoff, this.budgetDelay(this.o.clock()));
338
+ this.setState({ status: "reconnecting", attempt: this.attempt, inMs, reason });
339
+ this.scheduleConnect(inMs);
340
+ }
341
+ /** Stale / missed pong: drop the socket ourselves (HL never answers a client close) and reconnect. */
342
+ forceReconnect(reason) {
343
+ if (this.terminal || this.ws === null) return;
344
+ this.h.log("warn", "force-reconnect", reason.message);
345
+ this.teardown(4e3, "stale");
346
+ this.handleClose(reason);
347
+ }
348
+ startTimers() {
349
+ this.staleTimer = setInterval(() => this.evaluate(), STALE_TICK_MS);
350
+ const ping = this.o.ping;
351
+ if (ping !== null) {
352
+ this.pingTimer = setInterval(() => this.pingTick(), ping.everyMs);
353
+ }
354
+ }
355
+ pingTick() {
356
+ if (!this.isOpen || this.o.ping === null) return;
357
+ if (this.awaitingPongSince !== null) {
358
+ this.forceReconnect({
359
+ code: "EXCHANGE_UNREACHABLE",
360
+ message: `No pong within ${this.o.ping.everyMs} ms`,
361
+ wsCode: null,
362
+ remediation: "Reconnecting automatically"
363
+ });
364
+ return;
365
+ }
366
+ this.awaitingPongSince = this.o.clock();
367
+ this.send(this.o.ping.message);
368
+ }
369
+ onVisible() {
370
+ if (!this.isOpen) return;
371
+ this.h.log("info", "visible", "tab visible: re-evaluating staleness");
372
+ this.evaluate();
373
+ if (this.isOpen && this.o.ping !== null && this.awaitingPongSince === null) {
374
+ this.awaitingPongSince = this.o.clock();
375
+ this.send(this.o.ping.message);
376
+ }
377
+ }
378
+ flush() {
379
+ if (this.sendTimer !== null) return;
380
+ const ws = this.ws;
381
+ while (this.sendQueue.length > 0 && ws !== null && this.opened) {
382
+ const now = this.o.clock();
383
+ this.sendTimes = this.sendTimes.filter((t) => now - t < 6e4);
384
+ if (this.sendTimes.length >= this.o.outboundPerMin) {
385
+ const wait = this.sendTimes[0] + 6e4 - now;
386
+ this.h.log("warn", "outbound-limit", `outbound limit (${this.o.outboundPerMin}/min) reached; ${this.sendQueue.length} queued for ${wait} ms`);
387
+ this.sendTimer = setTimeout(() => {
388
+ this.sendTimer = null;
389
+ this.flush();
390
+ }, wait);
391
+ return;
392
+ }
393
+ const msg = this.sendQueue.shift();
394
+ this.sendTimes.push(now);
395
+ try {
396
+ ws.send(msg);
397
+ } catch (e) {
398
+ this.h.log("warn", "send-failed", this.o.redact(`send failed: ${e instanceof Error ? e.message : String(e)}`));
399
+ }
400
+ }
401
+ }
402
+ detach() {
403
+ const ws = this.ws;
404
+ if (ws !== null) {
405
+ ws.onopen = null;
406
+ ws.onmessage = null;
407
+ ws.onerror = null;
408
+ ws.onclose = null;
409
+ }
410
+ const wasSocket = ws !== null;
411
+ this.ws = null;
412
+ this.opened = false;
413
+ this.awaitingPongSince = null;
414
+ this.sendQueue = [];
415
+ if (this.sendTimer !== null) {
416
+ clearTimeout(this.sendTimer);
417
+ this.sendTimer = null;
418
+ }
419
+ if (this.staleTimer !== null) {
420
+ clearInterval(this.staleTimer);
421
+ this.staleTimer = null;
422
+ }
423
+ if (this.pingTimer !== null) {
424
+ clearInterval(this.pingTimer);
425
+ this.pingTimer = null;
426
+ }
427
+ if (wasSocket) this.h.onSocketDown();
428
+ }
429
+ teardown(code, reason) {
430
+ if (this.reconnectTimer !== null) {
431
+ clearTimeout(this.reconnectTimer);
432
+ this.reconnectTimer = null;
433
+ }
434
+ const ws = this.ws;
435
+ this.detach();
436
+ if (ws !== null) {
437
+ try {
438
+ ws.close(code, reason);
439
+ } catch {
440
+ }
441
+ }
442
+ }
443
+ };
444
+ function stateDetails(s) {
445
+ const { status: _status, ...rest } = s;
446
+ return rest;
447
+ }
448
+ function closedError(venue) {
449
+ return new WsError("INVALID_PARAMS", `${venue} stream is closed`, { exchange: venue, remediation: "Create a new stream" });
450
+ }
451
+
452
+ // src/ws/ids.ts
453
+ var VENUE_IDS = [
454
+ "pacifica",
455
+ "hyperliquid",
456
+ "lighter",
457
+ "aster",
458
+ "lighter-rh",
459
+ "ondo",
460
+ "arcus",
461
+ "qfex"
462
+ ];
463
+
464
+ // src/ws/market-state.ts
465
+ var PATCH_FIELDS = [
466
+ "mark",
467
+ "index",
468
+ "bestBid",
469
+ "bestAsk",
470
+ "last",
471
+ "rate",
472
+ "predictedRate",
473
+ "nextFundingTime",
474
+ "openInterest",
475
+ "volume24hNotional"
476
+ ];
477
+ var MarketStore = class {
478
+ constructor(codec, dir) {
479
+ this.codec = codec;
480
+ this.dir = dir;
481
+ this.rows = /* @__PURE__ */ new Map();
482
+ this.bySymbol = new Map(dir.entries.map((e) => [e.symbol, e]));
483
+ }
484
+ entry(symbol) {
485
+ return this.bySymbol.get(symbol);
486
+ }
487
+ /** Apply one patch; undefined when the symbol is not in the directory (caller reports it). */
488
+ apply(patch, recvTs, source) {
489
+ const entry = this.bySymbol.get(patch.symbol);
490
+ if (entry === void 0) return void 0;
491
+ let row = this.rows.get(patch.symbol);
492
+ if (row === void 0) {
493
+ row = {
494
+ entry,
495
+ values: {
496
+ mark: null,
497
+ index: null,
498
+ bestBid: null,
499
+ bestAsk: null,
500
+ last: null,
501
+ rate: null,
502
+ predictedRate: null,
503
+ nextFundingTime: null,
504
+ openInterest: null,
505
+ volume24hNotional: null
506
+ },
507
+ intervalKey: null,
508
+ venueTs: null,
509
+ asOf: {}
510
+ };
511
+ this.rows.set(patch.symbol, row);
512
+ }
513
+ const changed = [];
514
+ for (const f of PATCH_FIELDS) {
515
+ if (!(f in patch)) continue;
516
+ const v2 = patch[f];
517
+ if (v2 === void 0) continue;
518
+ if (row.values[f] !== v2) changed.push(f);
519
+ row.values[f] = v2;
520
+ row.asOf[f] = recvTs;
521
+ }
522
+ const meta = this.codec.fundingMeta(entry, this.dir);
523
+ const intervalKey = `${meta.rateIntervalHours}|${meta.settlementIntervalHours}|${meta.intervalSource}`;
524
+ if (meta.intervalAsOf !== null) row.asOf.fundingInterval = meta.intervalAsOf;
525
+ if (row.intervalKey !== intervalKey) {
526
+ if (row.intervalKey !== null || meta.rateIntervalHours !== null) changed.push("fundingInterval");
527
+ row.intervalKey = intervalKey;
528
+ }
529
+ if (patch.venueTs !== null) row.venueTs = patch.venueTs;
530
+ const v = row.values;
531
+ return {
532
+ type: "market",
533
+ schema: 1,
534
+ venue: entry.venue,
535
+ dex: entry.dex,
536
+ symbol: entry.symbol,
537
+ venueSymbol: entry.venueSymbol,
538
+ quote: entry.quote,
539
+ canonical: entry.canonical,
540
+ mark: v.mark,
541
+ index: v.index,
542
+ bestBid: v.bestBid,
543
+ bestAsk: v.bestAsk,
544
+ last: v.last,
545
+ funding: {
546
+ rate: v.rate,
547
+ rateKind: meta.rateKind,
548
+ rateSource: meta.rateSource,
549
+ rateIntervalHours: meta.rateIntervalHours,
550
+ settlementIntervalHours: meta.settlementIntervalHours,
551
+ intervalSource: meta.intervalSource,
552
+ nextFundingTime: v.nextFundingTime,
553
+ predictedRate: v.predictedRate,
554
+ marketClosed: meta.marketClosed
555
+ },
556
+ openInterest: {
557
+ value: v.openInterest,
558
+ unit: this.codec.openInterest.unit,
559
+ sides: this.codec.openInterest.sides
560
+ },
561
+ volume24hNotional: v.volume24hNotional,
562
+ venueTs: row.venueTs,
563
+ recvTs,
564
+ source,
565
+ changed,
566
+ asOf: { ...row.asOf }
567
+ };
568
+ }
569
+ };
570
+
571
+ // src/ws/redact.ts
572
+ var BASE_SECRET_KEYS = [
573
+ "auth",
574
+ "authorization",
575
+ "token",
576
+ "authToken",
577
+ "auth-token",
578
+ "auth_token",
579
+ "access-token",
580
+ "access_token",
581
+ "accessToken",
582
+ "listenKey",
583
+ "listen-key",
584
+ "signature",
585
+ "sig",
586
+ "hmac",
587
+ "apiKey",
588
+ "api_key",
589
+ "api-key",
590
+ "x-api-key",
591
+ "secret",
592
+ "secretKey",
593
+ "secret_key",
594
+ "secret-key",
595
+ "privateKey",
596
+ "private_key",
597
+ "private-key",
598
+ "password",
599
+ "passphrase"
600
+ ];
601
+ var REDACTED = "<redacted>";
602
+ var RAW_EXCERPT_MAX = 200;
603
+ function escapeRe(s) {
604
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
605
+ }
606
+ function createRedactor(secretKeys, urlPatterns = []) {
607
+ const keys = Array.from(/* @__PURE__ */ new Set([...BASE_SECRET_KEYS, ...secretKeys])).map(escapeRe).join("|");
608
+ const K = `(?<![A-Za-z0-9_])(?:${keys})(?![A-Za-z0-9_])`;
609
+ const nested = `\\[(?:[^\\[\\]]|\\[[^\\[\\]]*\\])*\\]|\\{(?:[^{}]|\\{[^{}]*\\})*\\}`;
610
+ const json = new RegExp(`("(?:${keys})"\\s*:\\s*)("(?:[^"\\\\]|\\\\.)*"|${nested}|-?\\d[\\d.eE+-]*|true|false)`, "gi");
611
+ const escaped = new RegExp(`((\\\\+)"(?:${keys})\\\\+"\\s*:\\s*)(?:\\\\+"(?:(?!\\\\+").)*\\\\+"|\\[[^\\]]*\\]|\\{[^}]*\\}|-?\\d[\\d.eE+-]*|true|false)`, "gi");
612
+ const query = new RegExp(`([?&]${K}=)[^&#\\s"'\\\\]*`, "gi");
613
+ const encoded = new RegExp(`(${K}%3D)(?:[A-Za-z0-9._~+/-]|%(?!26)[0-9A-Fa-f]{2})*`, "gi");
614
+ const quotedText = new RegExp(`(${K}\\s*[=:]\\s*)(\\\\*["'])(?:(?!\\2).)*\\2`, "gi");
615
+ const text = new RegExp(`(${K}\\s*[=:]\\s*)(?![\\s"'\\\\<])[^\\s,;&"'}\\]\\\\]+`, "gi");
616
+ const bearer = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi;
617
+ return (s) => {
618
+ let out = s.replace(json, `$1"${REDACTED}"`).replace(escaped, (_m, prefix, slashes) => `${prefix}${slashes}"${REDACTED}${slashes}"`).replace(query, `$1${REDACTED}`).replace(encoded, `$1${REDACTED}`).replace(bearer, `$1 ${REDACTED}`).replace(quotedText, `$1$2${REDACTED}$2`).replace(text, `$1${REDACTED}`);
619
+ for (const p of urlPatterns) out = out.replace(p, REDACTED);
620
+ return out;
621
+ };
622
+ }
623
+ function excerpt(redact, s) {
624
+ const r = redact(s);
625
+ return r.length <= RAW_EXCERPT_MAX ? r : `${r.slice(0, RAW_EXCERPT_MAX - 1)}\u2026`;
626
+ }
627
+
628
+ // src/ws/subscription.ts
629
+ function coalesceKey(e) {
630
+ switch (e.type) {
631
+ case "market": {
632
+ const m = e;
633
+ return `market|${m.dex === null ? "" : m.dex}|${m.symbol}`;
634
+ }
635
+ case "bbo": {
636
+ const b = e;
637
+ return `bbo|${b.dex === null ? "" : b.dex}|${b.symbol}`;
638
+ }
639
+ case "account": {
640
+ const a = e;
641
+ return `account|${a.dex === null ? "" : a.dex}`;
642
+ }
643
+ case "positions":
644
+ return "positions";
645
+ case "orders":
646
+ return "orders";
647
+ default:
648
+ return null;
649
+ }
650
+ }
651
+ function evictable(e) {
652
+ if (e.type === "state") return true;
653
+ if (e.type !== "error") return false;
654
+ const c = e;
655
+ return !c.fatal && c.code !== "SYMBOL_NOT_FOUND";
656
+ }
657
+ function merge(prev, next) {
658
+ if (next.type === "market") {
659
+ const p = prev;
660
+ const n = next;
661
+ const changed = Array.from(/* @__PURE__ */ new Set([...p.changed, ...n.changed]));
662
+ const source = p.source === "snapshot" || n.source === "snapshot" ? "snapshot" : n.source;
663
+ const merged = { ...n, changed, source };
664
+ return merged;
665
+ }
666
+ if (next.type === "positions" || next.type === "orders") {
667
+ const p = prev;
668
+ const n = next;
669
+ const snapshot = p.snapshot || n.snapshot;
670
+ const merged = { ...n, snapshot, source: snapshot ? "snapshot" : n.source };
671
+ return merged;
672
+ }
673
+ return next;
674
+ }
675
+ var SubscriptionImpl = class {
676
+ constructor(venue, bufferSize, onUnsubscribe, onListenerError, onOverflow) {
677
+ this.venue = venue;
678
+ this.bufferSize = bufferSize;
679
+ this.onUnsubscribe = onUnsubscribe;
680
+ this.onListenerError = onListenerError;
681
+ this.onOverflow = onOverflow;
682
+ this.status = "pending";
683
+ this.listeners = /* @__PURE__ */ new Set();
684
+ this.iterating = false;
685
+ this.buffering = true;
686
+ this.queue = [];
687
+ this.keyed = /* @__PURE__ */ new Map();
688
+ // coalesce key → queued box
689
+ this.controlsQueued = 0;
690
+ /** Control events discarded from the pre-iterator ring. */
691
+ this.droppedBeforeIterator = 0;
692
+ this.waiters = [];
693
+ this.ready = new Promise((resolve, reject) => {
694
+ this.resolveReady = resolve;
695
+ this.rejectReady = reject;
696
+ });
697
+ this.ready.catch(() => void 0);
698
+ }
699
+ markActive() {
700
+ if (this.status !== "pending") return;
701
+ this.status = "active";
702
+ this.resolveReady();
703
+ }
704
+ /** Reject `ready` (if pending), emit a fatal error event and end the subscription. */
705
+ fail(err) {
706
+ if (this.status === "closed" || this.status === "rejected") return;
707
+ const wasPending = this.status === "pending";
708
+ this.deliver({
709
+ type: "error",
710
+ venue: this.venue,
711
+ code: err.structured.code,
712
+ message: err.message,
713
+ remediation: err.structured.remediation === void 0 ? null : err.structured.remediation,
714
+ raw: null,
715
+ fatal: true
716
+ }, true);
717
+ this.status = wasPending ? "rejected" : "closed";
718
+ if (wasPending) this.rejectReady(err);
719
+ this.finish();
720
+ }
721
+ /** End without error (client unsubscribe / stream close). */
722
+ end() {
723
+ if (this.status === "closed" || this.status === "rejected") return;
724
+ const wasPending = this.status === "pending";
725
+ this.status = "closed";
726
+ if (wasPending) this.rejectReady(new WsError("INVALID_PARAMS", "Subscription closed before it became ready", { exchange: this.venue }));
727
+ this.finish();
728
+ }
729
+ get closed() {
730
+ return this.status === "closed" || this.status === "rejected";
731
+ }
732
+ emit(e) {
733
+ this.deliver(e, false);
734
+ }
735
+ /** `force`: a terminal error event is queued even when the buffer is full. */
736
+ deliver(e, force) {
737
+ if (this.closed) return;
738
+ for (const l of this.listeners) {
739
+ try {
740
+ l(e);
741
+ } catch (err) {
742
+ this.onListenerError(err);
743
+ }
744
+ }
745
+ if (!this.buffering) return;
746
+ const w = this.waiters.shift();
747
+ if (w !== void 0) {
748
+ w({ value: e, done: false });
749
+ return;
750
+ }
751
+ const key = coalesceKey(e);
752
+ if (key !== null) {
753
+ const box = this.keyed.get(key);
754
+ if (box !== void 0) {
755
+ box.v = merge(box.v, e);
756
+ return;
757
+ }
758
+ const nb = { v: e, key };
759
+ this.keyed.set(key, nb);
760
+ this.queue.push(nb);
761
+ return;
762
+ }
763
+ if (!force && !this.iterating && this.controlsQueued >= this.bufferSize) {
764
+ const oldest = this.queue.findIndex((b) => b.key === null && evictable(b.v));
765
+ if (oldest >= 0) {
766
+ this.queue.splice(oldest, 1);
767
+ this.controlsQueued--;
768
+ this.droppedBeforeIterator++;
769
+ }
770
+ }
771
+ if (!force && this.controlsQueued >= this.bufferSize) {
772
+ this.onOverflow(this, new WsError("STREAM_OVERFLOW", `Subscription buffer full (${this.bufferSize} events not consumed)`, {
773
+ exchange: this.venue,
774
+ remediation: "Consume the iterator faster, use on() callbacks, or raise opts.buffer.size"
775
+ }));
776
+ return;
777
+ }
778
+ this.controlsQueued++;
779
+ this.queue.push({ v: e, key: null });
780
+ }
781
+ on(cb) {
782
+ if (!this.iterating) this.clearBuffer(false);
783
+ this.listeners.add(cb);
784
+ return () => {
785
+ this.listeners.delete(cb);
786
+ };
787
+ }
788
+ unsubscribe() {
789
+ if (this.closed) return Promise.resolve();
790
+ return this.onUnsubscribe(this);
791
+ }
792
+ [Symbol.asyncIterator]() {
793
+ if (!this.iterating && this.droppedBeforeIterator > 0) {
794
+ const oldest = this.queue.findIndex((b) => b.key === null && evictable(b.v));
795
+ if (this.controlsQueued >= this.bufferSize && oldest >= 0) {
796
+ this.queue.splice(oldest, 1);
797
+ this.controlsQueued--;
798
+ this.droppedBeforeIterator++;
799
+ }
800
+ const n = this.droppedBeforeIterator;
801
+ this.droppedBeforeIterator = 0;
802
+ this.queue.unshift({
803
+ v: {
804
+ type: "error",
805
+ venue: this.venue,
806
+ code: "STREAM_OVERFLOW",
807
+ message: `${n} older control event(s) were discarded before an iterator attached (latest ${this.bufferSize} kept)`,
808
+ remediation: "Create the iterator (or attach on()) right after subscribe()",
809
+ raw: null,
810
+ fatal: false
811
+ },
812
+ key: null
813
+ });
814
+ this.controlsQueued++;
815
+ }
816
+ this.iterating = true;
817
+ this.buffering = true;
818
+ const stop = async () => {
819
+ this.clearBuffer(true);
820
+ await this.unsubscribe();
821
+ return { value: void 0, done: true };
822
+ };
823
+ return {
824
+ next: () => {
825
+ const box = this.queue.shift();
826
+ if (box !== void 0) {
827
+ if (box.key === null) this.controlsQueued--;
828
+ else this.keyed.delete(box.key);
829
+ return Promise.resolve({ value: box.v, done: false });
830
+ }
831
+ if (this.closed) return Promise.resolve({ value: void 0, done: true });
832
+ return new Promise((resolve) => this.waiters.push(resolve));
833
+ },
834
+ return: stop,
835
+ throw: async (err) => {
836
+ await stop();
837
+ throw err;
838
+ }
839
+ };
840
+ }
841
+ clearBuffer(keepBuffering) {
842
+ this.queue = [];
843
+ this.keyed.clear();
844
+ this.controlsQueued = 0;
845
+ this.droppedBeforeIterator = 0;
846
+ this.buffering = keepBuffering;
847
+ }
848
+ finish() {
849
+ for (const w of this.waiters.splice(0)) w({ value: void 0, done: true });
850
+ }
851
+ };
852
+
853
+ // src/ws/user-state.ts
854
+ var FILL_DEDUPE_WINDOW = 2e4;
855
+ var UserStore = class {
856
+ constructor(venue) {
857
+ this.venue = venue;
858
+ this.positions = /* @__PURE__ */ new Map();
859
+ this.orders = /* @__PURE__ */ new Map();
860
+ /** Order scopes that received a venue snapshot since they were (re)created. */
861
+ this.ordersComplete = /* @__PURE__ */ new Set();
862
+ this.seenFills = /* @__PURE__ */ new Map();
863
+ }
864
+ apply(d, recvTs) {
865
+ const source = d.kind !== "fills" && d.snapshot ? "snapshot" : "ws";
866
+ switch (d.kind) {
867
+ case "account":
868
+ return {
869
+ fillsGap: false,
870
+ updates: [{
871
+ type: "account",
872
+ schema: 1,
873
+ venue: this.venue,
874
+ dex: d.dex,
875
+ equity: d.equity === void 0 ? null : d.equity,
876
+ available: d.available === void 0 ? null : d.available,
877
+ marginUsed: d.marginUsed === void 0 ? null : d.marginUsed,
878
+ unrealizedPnl: d.unrealizedPnl === void 0 ? null : d.unrealizedPnl,
879
+ venueTs: d.venueTs,
880
+ source,
881
+ recvTs
882
+ }]
883
+ };
884
+ case "positions":
885
+ mergeScope(this.positions, d.scope, d.snapshot, d.rows.map((r) => [posKey(r), r]), d.remove);
886
+ return { fillsGap: false, updates: [{ type: "positions", schema: 1, venue: this.venue, snapshot: d.snapshot, positions: flatten(this.positions), source, recvTs }] };
887
+ case "orders":
888
+ mergeScope(this.orders, d.scope, d.snapshot, d.rows.map((r) => [r.orderId, r]), d.remove);
889
+ if (d.snapshot) this.ordersComplete.add(d.scope);
890
+ return { fillsGap: false, updates: [this.ordersEvent(d.snapshot, recvTs)] };
891
+ case "fills":
892
+ return this.applyFills(d.snapshot, d.fills, recvTs);
893
+ }
894
+ }
895
+ /** A non-replayable order channel had a gap: forget its (now unknown) open orders. */
896
+ clearOrders(recvTs) {
897
+ this.orders.clear();
898
+ this.ordersComplete.clear();
899
+ return this.ordersEvent(false, recvTs);
900
+ }
901
+ currentOrders(recvTs) {
902
+ return this.ordersEvent(false, recvTs);
903
+ }
904
+ ordersEvent(snapshot, recvTs) {
905
+ const scopes = Array.from(this.orders.keys());
906
+ const complete = scopes.length > 0 && scopes.every((s) => this.ordersComplete.has(s));
907
+ return { type: "orders", schema: 1, venue: this.venue, snapshot, complete, orders: flatten(this.orders), source: snapshot ? "snapshot" : "ws", recvTs };
908
+ }
909
+ applyFills(snapshot, fills, recvTs) {
910
+ const hadHistory = this.seenFills.size > 0;
911
+ const fresh = [];
912
+ let oldest = null;
913
+ for (const f of fills) {
914
+ if (oldest === null || f.time !== null && (oldest.time === null || f.time < oldest.time)) oldest = f;
915
+ }
916
+ const oldestUnseen = oldest !== null && !this.seenFills.has(fillKey(oldest));
917
+ for (const f of fills) {
918
+ const k = fillKey(f);
919
+ if (this.seenFills.has(k)) continue;
920
+ this.remember(k);
921
+ fresh.push(f);
922
+ }
923
+ const fillsGap = snapshot && hadHistory && oldestUnseen;
924
+ if (fresh.length === 0) return { fillsGap, updates: [] };
925
+ if (snapshot) {
926
+ return { fillsGap, updates: [{ type: "fills", schema: 1, venue: this.venue, snapshot: true, fills: fresh, source: "snapshot", recvTs }] };
927
+ }
928
+ return {
929
+ fillsGap,
930
+ updates: fresh.map((fill) => ({ type: "fill", schema: 1, venue: this.venue, snapshot: false, fillId: fill.fillId, fill, source: "ws", recvTs }))
931
+ };
932
+ }
933
+ remember(k) {
934
+ this.seenFills.set(k, true);
935
+ if (this.seenFills.size > FILL_DEDUPE_WINDOW) {
936
+ const first = this.seenFills.keys().next();
937
+ if (first.done !== true) this.seenFills.delete(first.value);
938
+ }
939
+ }
940
+ };
941
+ function fillKey(f) {
942
+ return `${f.fillId}|${f.side === null ? "" : f.side}|${f.orderId === null ? "" : f.orderId}`;
943
+ }
944
+ function posKey(r) {
945
+ return `${r.dex === null ? "" : r.dex}|${r.symbol}`;
946
+ }
947
+ function mergeScope(all, scope, snapshot, rows, remove) {
948
+ let m = all.get(scope);
949
+ if (m === void 0 || snapshot) {
950
+ m = /* @__PURE__ */ new Map();
951
+ all.set(scope, m);
952
+ }
953
+ for (const k of remove) {
954
+ for (const key of Array.from(m.keys())) if (key === k || key.endsWith(`|${k}`)) m.delete(key);
955
+ }
956
+ for (const [k, v] of rows) m.set(k, v);
957
+ }
958
+ function flatten(all) {
959
+ const out = [];
960
+ for (const m of all.values()) for (const v of m.values()) out.push(v);
961
+ return out;
962
+ }
963
+
964
+ // src/ws/stream.ts
965
+ var USER_CHANNELS = ["account", "positions", "orders", "fills"];
966
+ var MARKET_CHANNELS = ["market", "bbo", "book", "trades"];
967
+ var LATER_CHANNELS = ["book", "trades"];
968
+ var NETWORKS = ["mainnet", "testnet"];
969
+ var MAX_PAYLOAD_CLOSES = 3;
970
+ var MAX_PAYLOAD_WINDOW_MS = 10 * 6e4;
971
+ var CODEC_REMEDIATION = "Pass a codec export, e.g. import { hyperliquid } from '@hypurrquant/perp-ws' (or 'perp-cli/ws')";
972
+ function checkCodec(codec) {
973
+ const c = codec;
974
+ const problem = c === null || typeof c !== "object" ? `got ${c === null ? "null" : typeof codec}${typeof codec === "string" ? ` ${JSON.stringify(codec)}` : ""}` : typeof c.venue !== "string" || !VENUE_IDS.includes(c.venue) ? `unknown venue ${JSON.stringify(c.venue)}` : !Array.isArray(c.secretKeys) || !Array.isArray(c.secretUrlPatterns) || typeof c.url !== "function" || typeof c.restUrl !== "function" || typeof c.loadDirectory !== "function" || typeof c.wiresFor !== "function" || c.capabilities === null || typeof c.capabilities !== "object" ? `${String(c.venue)} codec is missing required members` : null;
975
+ if (problem !== null) {
976
+ throw new WsError("INVALID_PARAMS", `createVenueStream: first argument must be a venue codec (${problem})`, { remediation: CODEC_REMEDIATION });
977
+ }
978
+ }
979
+ function suggestCanonicals(missing, entries) {
980
+ const m = missing.toUpperCase();
981
+ const out = entries.filter((e) => e.canonical !== null && (e.canonical.toUpperCase() === m || e.base.toUpperCase() === m || e.canonical.toUpperCase().endsWith(`:${m}`))).map((e) => e.canonical);
982
+ return Array.from(new Set(out)).slice(0, 5);
983
+ }
984
+ function suggestSymbols(missing, entries) {
985
+ const m = missing.toUpperCase();
986
+ const out = entries.filter((e) => e.symbol.toUpperCase() === m || e.canonical !== null && e.canonical.toUpperCase() === m || e.base.toUpperCase() === m).map((e) => e.symbol);
987
+ return Array.from(new Set(out)).slice(0, 5);
988
+ }
989
+ var EVENT_CHANNEL = { account: "account", positions: "positions", orders: "orders", fill: "fills", fills: "fills" };
990
+ var TOKEN_REDACT_MARGIN_MS = 15 * 6e4;
991
+ var TOKEN_RETRY_BASE_MS = 1e3;
992
+ var TOKEN_RETRY_MAX_MS = 3e4;
993
+ var TOKEN_RETRIES_WITHOUT_VALID = 5;
994
+ var BAD_SUBSCRIBE_WINDOW_MS = 1500;
995
+ var DEFAULT_BUFFER = 1024;
996
+ var EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
997
+ function userRefProblem(venue, ref) {
998
+ if (ref === null || typeof ref !== "object") return "not an object";
999
+ if (ref.venue !== venue) return `venue is "${String(ref.venue)}", expected "${venue}"`;
1000
+ switch (ref.venue) {
1001
+ case "hyperliquid":
1002
+ return EVM_ADDRESS.test(ref.address) ? null : "address is not a 0x EVM address";
1003
+ case "pacifica":
1004
+ return typeof ref.account === "string" && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(ref.account) ? null : "account is not a base58 Solana address";
1005
+ case "lighter":
1006
+ case "lighter-rh":
1007
+ return Number.isInteger(ref.accountIndex) && ref.accountIndex >= 0 ? null : "accountIndex is not a non-negative integer";
1008
+ case "arcus":
1009
+ if (!EVM_ADDRESS.test(ref.address)) return "address is not a 0x EVM address";
1010
+ return ref.subaccount === null || Number.isInteger(ref.subaccount) && ref.subaccount >= 0 ? null : "subaccount is not null or a non-negative integer";
1011
+ default:
1012
+ return null;
1013
+ }
1014
+ }
1015
+ function isBrowserLike() {
1016
+ const g = globalThis;
1017
+ return g.window !== void 0 && g.document !== void 0;
1018
+ }
1019
+ function createVenueStream(codec, opts = {}) {
1020
+ checkCodec(codec);
1021
+ const venue = codec.venue;
1022
+ if (opts === null || typeof opts !== "object") {
1023
+ throw new WsError("INVALID_PARAMS", `${venue}: options must be an object (got ${opts === null ? "null" : typeof opts})`, { exchange: venue });
1024
+ }
1025
+ if (opts.network !== void 0 && !NETWORKS.includes(opts.network)) {
1026
+ throw new WsError("INVALID_PARAMS", `${venue}: unknown network ${JSON.stringify(opts.network)}`, {
1027
+ exchange: venue,
1028
+ remediation: `Use network ${NETWORKS.map((n) => `"${n}"`).join(" or ")} (or omit it for mainnet)`
1029
+ });
1030
+ }
1031
+ const network = opts.network === void 0 ? "mainnet" : opts.network;
1032
+ const transport = resolveTransport(opts, venue);
1033
+ const fetchFn = resolveFetch(opts, venue);
1034
+ const clock = opts.clock === void 0 ? () => Date.now() : opts.clock;
1035
+ const baseRedact = createRedactor(codec.secretKeys, codec.secretUrlPatterns);
1036
+ const liveSecrets = [];
1037
+ const redact = (s) => {
1038
+ let out = baseRedact(s);
1039
+ for (const { secret } of liveSecrets) if (out.includes(secret)) out = out.split(secret).join(REDACTED);
1040
+ return out;
1041
+ };
1042
+ const bufferSize = opts.buffer === void 0 ? DEFAULT_BUFFER : opts.buffer.size;
1043
+ if (!Number.isInteger(bufferSize) || bufferSize < 1) {
1044
+ throw new WsError("INVALID_PARAMS", `${venue}: buffer.size must be a positive integer (got ${String(bufferSize)})`, {
1045
+ exchange: venue,
1046
+ remediation: "Pass buffer: { size: n } with n \u2265 1, or omit it (default 1024)"
1047
+ });
1048
+ }
1049
+ const url = codec.url(network);
1050
+ const restUrl = codec.restUrl(network);
1051
+ const subs = /* @__PURE__ */ new Map();
1052
+ const wires = /* @__PURE__ */ new Map();
1053
+ const stateListeners = /* @__PURE__ */ new Set();
1054
+ const reportedUnknown = /* @__PURE__ */ new Set();
1055
+ let nextReqId = 1;
1056
+ let closed = false;
1057
+ let directory = null;
1058
+ let dir = null;
1059
+ let store = null;
1060
+ let refreshTimer = null;
1061
+ let directoryWarnings = [];
1062
+ const userStore = new UserStore(venue);
1063
+ let userRef = null;
1064
+ const tokens = /* @__PURE__ */ new Map();
1065
+ const tokenFetches = /* @__PURE__ */ new Map();
1066
+ const tokenRetryTimers = /* @__PURE__ */ new Set();
1067
+ const tokenLoops = /* @__PURE__ */ new Map();
1068
+ function log(level2, event, message, details) {
1069
+ if (opts.logger === void 0) return;
1070
+ const entry = { ts: clock(), venue, level: level2, event, message: redact(message) };
1071
+ if (details !== void 0) entry.details = JSON.parse(redact(JSON.stringify(details)));
1072
+ try {
1073
+ opts.logger(entry);
1074
+ } catch {
1075
+ }
1076
+ }
1077
+ function errorEvent(code, message, remediation, raw, fatal, details) {
1078
+ return {
1079
+ type: "error",
1080
+ venue,
1081
+ code,
1082
+ message: redact(message),
1083
+ remediation: remediation === null ? null : redact(remediation),
1084
+ raw: raw === null ? null : excerpt(redact, raw),
1085
+ fatal,
1086
+ ...details === void 0 ? {} : { details }
1087
+ };
1088
+ }
1089
+ function wsError(code, message, remediation, details) {
1090
+ return new WsError(code, redact(message), {
1091
+ exchange: venue,
1092
+ ...remediation !== void 0 ? { remediation: redact(remediation) } : {},
1093
+ ...details !== void 0 ? { details } : {}
1094
+ });
1095
+ }
1096
+ const shards = [];
1097
+ const connectLog = [];
1098
+ let terminalReason = null;
1099
+ let lastState = null;
1100
+ const maxSockets = Math.min(
1101
+ codec.limits.maxSockets === void 0 ? 1 : codec.limits.maxSockets,
1102
+ opts.budget?.maxSockets === void 0 ? Infinity : opts.budget.maxSockets
1103
+ );
1104
+ function wiresOn(shard) {
1105
+ return Array.from(shard.wires);
1106
+ }
1107
+ function newShard() {
1108
+ const shard = { id: shards.length, wires: /* @__PURE__ */ new Set(), fresh: true, payloadCloses: [] };
1109
+ shard.conn = new Connection({
1110
+ venue,
1111
+ url,
1112
+ transport,
1113
+ clock,
1114
+ redact,
1115
+ ping: codec.ping,
1116
+ reconnect: opts.reconnect === void 0 ? { maxAttempts: null, baseMs: 1e3, maxMs: 3e4 } : opts.reconnect,
1117
+ newConnectionsPerMin: Math.min(codec.limits.newConnectionsPerMin, opts.budget?.newConnectionsPerMin === void 0 ? Infinity : opts.budget.newConnectionsPerMin),
1118
+ connectLog,
1119
+ outboundPerMin: codec.limits.outboundPerMin,
1120
+ visibility: opts.visibility === void 0 ? defaultVisibility() : opts.visibility,
1121
+ random: Math.random
1122
+ }, {
1123
+ onOpen: () => {
1124
+ const now = clock();
1125
+ shard.fresh = false;
1126
+ for (const w of wiresOn(shard)) {
1127
+ const g = w.wire.gaps?.onReconnect;
1128
+ if (w.everAcked && g !== void 0) {
1129
+ for (const ch of g) {
1130
+ emitGap(w, ch, "reconnected; the venue does not replay this channel");
1131
+ if (ch === "orders") dispatchUser(w, userStore.clearOrders(now));
1132
+ }
1133
+ }
1134
+ w.firstFrameSeen = false;
1135
+ w.lastAt = null;
1136
+ if (w.suspect) {
1137
+ w.sentAt = null;
1138
+ continue;
1139
+ }
1140
+ sendSubscribe(w, now, false);
1141
+ }
1142
+ probeNextSuspect(shard);
1143
+ },
1144
+ // Ack timers belong to one socket: a reconnect delay must not fail a pending subscription.
1145
+ onSocketDown: () => {
1146
+ for (const w of wiresOn(shard)) clearAckTimer(w);
1147
+ },
1148
+ onText: (text, recvTs) => onText(shard, text, recvTs),
1149
+ onUnreadableFrame: () => {
1150
+ const ev = errorEvent("WS_PROTOCOL_ERROR", "Unreadable (non-text) frame", null, null, false);
1151
+ log("warn", "protocol-error", ev.type === "error" ? ev.message : "");
1152
+ broadcast(ev);
1153
+ },
1154
+ onState: (s) => {
1155
+ if (s.status === "idle") shard.fresh = true;
1156
+ onShardState(s);
1157
+ },
1158
+ onUnexpectedClose: (reason, opened, maxPayloadExceeded) => maxPayloadExceeded ? handleMaxPayloadClose(shard, reason) : handleBadSubscribeClose(shard, reason, opened),
1159
+ liveness: () => {
1160
+ const out = [];
1161
+ for (const w of wiresOn(shard)) {
1162
+ if (w.sentAt === null) continue;
1163
+ out.push({ key: w.wire.key, staleAfterMs: w.wire.staleAfterMs, lastAt: Math.max(w.sentAt, w.lastAt === null ? 0 : w.lastAt) });
1164
+ }
1165
+ return out;
1166
+ },
1167
+ log
1168
+ });
1169
+ shards.push(shard);
1170
+ return shard;
1171
+ }
1172
+ if (url !== null) newShard();
1173
+ function shardState(sh) {
1174
+ return sh.conn.waiting && sh.wires.size > 0 ? { status: "connecting", attempt: 1 } : sh.conn.state;
1175
+ }
1176
+ function aggregateState() {
1177
+ if (shards.length === 1) return shardState(shards[0]);
1178
+ const active = shards.map((sh) => ({ sh, st: shardState(sh) })).filter((x) => x.st.status !== "idle");
1179
+ const closedHit = active.find((x) => x.st.status === "closed");
1180
+ if (closedHit !== void 0) return closedHit.st;
1181
+ for (const status of ["reconnecting", "stale"]) {
1182
+ const hit = active.find((x) => x.st.status === status && !(status === "reconnecting" && x.sh.fresh));
1183
+ if (hit !== void 0) return hit.st;
1184
+ }
1185
+ const down = active.find((x) => x.st.status === "connecting" && !x.sh.fresh);
1186
+ if (down !== void 0) return down.st;
1187
+ const live = active.filter((x) => x.st.status === "live");
1188
+ const opening = active.filter((x) => x.sh.fresh && (x.st.status === "connecting" || x.st.status === "reconnecting"));
1189
+ if (live.length > 0) {
1190
+ const since = Math.max(...live.map((x) => x.st.status === "live" ? x.st.since : 0));
1191
+ return { status: "live", since, sockets: live.length, pending: opening.length };
1192
+ }
1193
+ return opening.length > 0 ? opening[0].st : { status: "idle" };
1194
+ }
1195
+ function reportState() {
1196
+ const agg = aggregateState();
1197
+ const key = JSON.stringify(agg);
1198
+ if (key === lastState) return;
1199
+ lastState = key;
1200
+ for (const l of stateListeners) {
1201
+ try {
1202
+ l(agg);
1203
+ } catch (e) {
1204
+ log("warn", "listener-error", `onState listener threw: ${e instanceof Error ? e.message : String(e)}`);
1205
+ }
1206
+ }
1207
+ broadcast({ type: "state", venue, state: agg });
1208
+ }
1209
+ function onShardState(s) {
1210
+ const failed = s.status === "closed" && s.reason.code !== "CLIENT_CLOSE" && terminalReason === null;
1211
+ if (failed) terminalReason = s.reason;
1212
+ reportState();
1213
+ if (failed && s.status === "closed" && s.reason.code !== "CLIENT_CLOSE") {
1214
+ for (const sh of shards) sh.conn.fail(s.reason);
1215
+ const err = wsError(
1216
+ s.reason.code,
1217
+ s.reason.message,
1218
+ s.reason.remediation === null ? void 0 : s.reason.remediation
1219
+ );
1220
+ for (const sub of Array.from(subs.keys())) dropSub(sub, err);
1221
+ }
1222
+ }
1223
+ function broadcast(e) {
1224
+ for (const sub of subs.keys()) sub.emit(e);
1225
+ }
1226
+ function ctx() {
1227
+ return {
1228
+ network,
1229
+ fetch: fetchFn,
1230
+ restUrl,
1231
+ hip3Dexes: opts.hip3Dexes === void 0 ? [] : opts.hip3Dexes,
1232
+ canonicalize: opts.canonicalize === void 0 ? defaultCanonicalize : opts.canonicalize,
1233
+ clock
1234
+ };
1235
+ }
1236
+ function loadDirectory() {
1237
+ if (directory !== null) return directory;
1238
+ if (restUrl === null) {
1239
+ return Promise.reject(wsError("CAPABILITY_UNSUPPORTED", `${venue} has no REST host on ${network} for the market directory`));
1240
+ }
1241
+ const p = codec.loadDirectory(ctx()).then((d) => {
1242
+ dir = d;
1243
+ store = new MarketStore(codec, d);
1244
+ directoryWarnings = d.warnings === void 0 ? [] : d.warnings;
1245
+ log("info", "directory", `market directory loaded (${d.entries.length} markets)`);
1246
+ for (const w of directoryWarnings) log("warn", "directory-warning", w);
1247
+ if (!closed) startRefresh();
1248
+ return d;
1249
+ });
1250
+ directory = p;
1251
+ p.catch((e) => {
1252
+ directory = null;
1253
+ log("error", "directory-failed", e instanceof Error ? e.message : String(e));
1254
+ });
1255
+ return p;
1256
+ }
1257
+ function startRefresh() {
1258
+ const r = codec.refresh;
1259
+ if (r === null || refreshTimer !== null || closed) return;
1260
+ refreshTimer = setInterval(() => {
1261
+ if (dir === null || closed) return;
1262
+ const d = dir;
1263
+ r.run(ctx(), d).then(
1264
+ (warnings) => {
1265
+ log("info", "labelled-refresh", `${r.source} refreshed`);
1266
+ for (const w of warnings) {
1267
+ log("warn", "labelled-refresh-warning", w);
1268
+ broadcast(errorEvent("EXCHANGE_ERROR", w, "The venue REST response had malformed rows; affected markets keep a null interval", null, false));
1269
+ }
1270
+ },
1271
+ (e) => {
1272
+ const msg = `${r.source} refresh failed: ${e instanceof Error ? e.message : String(e)}; interval values keep their previous asOf`;
1273
+ log("warn", "labelled-refresh-failed", msg);
1274
+ broadcast(errorEvent(e instanceof WsError ? e.structured.code : "EXCHANGE_ERROR", msg, "Retried on the next refresh", null, false));
1275
+ }
1276
+ );
1277
+ }, r.everyMs);
1278
+ }
1279
+ function sendSubscribe(w, now, probe) {
1280
+ const connection = w.shard.conn;
1281
+ if (!connection.isOpen) return;
1282
+ let auth = null;
1283
+ const need = w.wire.auth;
1284
+ if (need !== void 0) {
1285
+ const t = tokens.get(need.purpose);
1286
+ if (t === void 0 || clock() >= t.expiresAt) {
1287
+ clearAckTimer(w);
1288
+ w.acked = false;
1289
+ w.sentAt = null;
1290
+ w.lastSeq = null;
1291
+ acquireToken(
1292
+ need,
1293
+ () => {
1294
+ if (!closed && wires.get(w.wire.key) === w && connection.isOpen && w.sentAt === null) sendSubscribe(w, clock(), probe);
1295
+ },
1296
+ (err) => failWire(w, err)
1297
+ );
1298
+ return;
1299
+ }
1300
+ auth = t.token;
1301
+ if (clock() >= t.refreshAt && t.timer === null) Promise.resolve().then(() => refreshToken(need));
1302
+ }
1303
+ w.lastSeq = null;
1304
+ w.reqId = nextReqId++;
1305
+ w.sentAt = now;
1306
+ w.acked = false;
1307
+ w.probe = probe;
1308
+ connection.send(codec.encodeSubscribe(w.wire, w.reqId, auth));
1309
+ armAckTimeout(w);
1310
+ }
1311
+ function failWire(w, err) {
1312
+ if (w.refs.size === 0) return;
1313
+ log("error", "wire-failed", err.message, { wire: w.wire.key });
1314
+ for (const sub of Array.from(w.refs)) dropSub(sub, err);
1315
+ }
1316
+ function dispatchUser(w, u, only) {
1317
+ const ch = EVENT_CHANNEL[u.type];
1318
+ for (const sub of only === void 0 ? w.refs : [only]) {
1319
+ const meta = subs.get(sub);
1320
+ if (meta !== void 0 && meta.userChannels !== null && meta.userChannels.has(ch)) sub.emit(u);
1321
+ }
1322
+ }
1323
+ function emitGap(w, channel, why) {
1324
+ log("warn", "gap", `${w.wire.key}: ${channel} ${why} \u2192 refetch-required`);
1325
+ for (const sub of w.refs) {
1326
+ const meta = subs.get(sub);
1327
+ if (meta === void 0 || meta.userChannels === null || !meta.userChannels.has(channel)) continue;
1328
+ sub.emit({ type: "gap", venue, channel, symbol: null, expected: null, got: null, action: "refetch-required" });
1329
+ }
1330
+ }
1331
+ function toAuthError(e) {
1332
+ if (e instanceof WsError && e.structured.code === "WS_AUTH_FAILED") return e;
1333
+ const definite = isPerpError(e) && e.structured.retryable === false;
1334
+ return wsError(
1335
+ "WS_AUTH_FAILED",
1336
+ `${venue} authToken() failed: ${e instanceof Error ? e.message : String(e)}`,
1337
+ "Check the credential provider (token for the right account / API key; a read-only ro: token in browsers)",
1338
+ definite ? void 0 : { transient: true }
1339
+ );
1340
+ }
1341
+ function usesPurpose(purpose) {
1342
+ for (const w of wires.values()) if (w.wire.auth !== void 0 && w.wire.auth.purpose === purpose) return true;
1343
+ return false;
1344
+ }
1345
+ function acquireToken(need, onOk, onFail) {
1346
+ const waiting = tokenLoops.get(need.purpose);
1347
+ if (waiting !== void 0) {
1348
+ waiting.push({ onOk, onFail });
1349
+ return;
1350
+ }
1351
+ tokenLoops.set(need.purpose, [{ onOk, onFail }]);
1352
+ const settle = (f) => {
1353
+ const list = tokenLoops.get(need.purpose);
1354
+ tokenLoops.delete(need.purpose);
1355
+ if (list !== void 0) for (const cb of list) f(cb);
1356
+ };
1357
+ const attempt = (n) => {
1358
+ getToken(need).then((t) => settle((cb) => cb.onOk(t)), (e) => {
1359
+ const err = toAuthError(e);
1360
+ if (closed) {
1361
+ tokenLoops.delete(need.purpose);
1362
+ return;
1363
+ }
1364
+ const delay = Math.min(TOKEN_RETRY_MAX_MS, TOKEN_RETRY_BASE_MS * Math.pow(2, n));
1365
+ const cur = tokens.get(need.purpose);
1366
+ const stillValid = cur !== void 0 && clock() + delay < cur.expiresAt;
1367
+ const transient = err.structured.details?.transient === true;
1368
+ if (!transient || !(stillValid || n < TOKEN_RETRIES_WITHOUT_VALID)) {
1369
+ settle((cb) => cb.onFail(err));
1370
+ return;
1371
+ }
1372
+ const msg = `${err.message}; retrying in ${delay} ms${stillValid ? " (current token still valid)" : ""}`;
1373
+ log("warn", "auth-token-retry", msg);
1374
+ for (const w of wires.values()) {
1375
+ if (w.wire.auth === void 0 || w.wire.auth.purpose !== need.purpose) continue;
1376
+ for (const sub of w.refs) sub.emit(errorEvent("WS_AUTH_FAILED", msg, "Retried automatically; fails once the token expires", null, false));
1377
+ }
1378
+ const timer = setTimeout(() => {
1379
+ tokenRetryTimers.delete(timer);
1380
+ if (closed) return;
1381
+ if (!usesPurpose(need.purpose)) {
1382
+ tokenLoops.delete(need.purpose);
1383
+ log("info", "auth-token-retry-stopped", `${need.purpose}: no channel uses this token any more`);
1384
+ return;
1385
+ }
1386
+ attempt(n + 1);
1387
+ }, delay);
1388
+ tokenRetryTimers.add(timer);
1389
+ });
1390
+ };
1391
+ attempt(0);
1392
+ }
1393
+ function getToken(need) {
1394
+ const have = tokens.get(need.purpose);
1395
+ if (have !== void 0 && clock() < have.refreshAt) return Promise.resolve(have);
1396
+ const inflight = tokenFetches.get(need.purpose);
1397
+ if (inflight !== void 0) return inflight;
1398
+ const p = (async () => {
1399
+ const provider = opts.credentials;
1400
+ if (provider === void 0 || provider.authToken === void 0) {
1401
+ throw wsError(
1402
+ "WS_AUTH_FAILED",
1403
+ `${need.purpose} needs an auth token but the credential provider has no authToken()`,
1404
+ "Pass credentials.authToken (Lighter: a read-only ro: token is recommended in browsers)"
1405
+ );
1406
+ }
1407
+ let r;
1408
+ try {
1409
+ r = await provider.authToken({ purpose: need.purpose, maxTtlSec: need.maxTtlSec });
1410
+ } catch (e) {
1411
+ throw toAuthError(e);
1412
+ }
1413
+ const now = clock();
1414
+ if (r === null || typeof r !== "object" || typeof r.token !== "string" || r.token === "" || typeof r.expiresAt !== "number" || !(r.expiresAt > now)) {
1415
+ throw wsError("WS_AUTH_FAILED", `${venue} authToken() returned no usable token (need { token, expiresAt: epoch ms in the future })`);
1416
+ }
1417
+ for (let i = liveSecrets.length - 1; i >= 0; i--) if (liveSecrets[i].until < now) liveSecrets.splice(i, 1);
1418
+ liveSecrets.push({ secret: r.token, until: r.expiresAt + TOKEN_REDACT_MARGIN_MS });
1419
+ const old = tokens.get(need.purpose);
1420
+ if (old !== void 0 && old.timer !== null) clearTimeout(old.timer);
1421
+ const refreshAt = now + Math.floor(0.8 * (r.expiresAt - now));
1422
+ const st = { token: r.token, expiresAt: r.expiresAt, refreshAt, timer: null };
1423
+ if (!closed) st.timer = setTimeout(() => {
1424
+ st.timer = null;
1425
+ refreshToken(need);
1426
+ }, refreshAt - now);
1427
+ tokens.set(need.purpose, st);
1428
+ log("info", "auth-token", `${need.purpose} token obtained (refresh in ${refreshAt - now} ms)`);
1429
+ return st;
1430
+ })();
1431
+ tokenFetches.set(need.purpose, p);
1432
+ p.then(() => tokenFetches.delete(need.purpose), () => tokenFetches.delete(need.purpose));
1433
+ return p;
1434
+ }
1435
+ function refreshToken(need) {
1436
+ if (closed) return;
1437
+ const users = () => Array.from(wires.values()).filter((w) => w.wire.auth !== void 0 && w.wire.auth.purpose === need.purpose);
1438
+ if (users().length === 0) {
1439
+ tokens.delete(need.purpose);
1440
+ return;
1441
+ }
1442
+ if (!users().some((w) => w.shard.conn.isOpen)) return;
1443
+ if (tokenLoops.has(need.purpose)) return;
1444
+ acquireToken(
1445
+ need,
1446
+ () => {
1447
+ for (const w of users()) {
1448
+ if (!w.shard.conn.isOpen || w.sentAt === null) continue;
1449
+ w.shard.conn.send(codec.encodeUnsubscribe(w.wire, nextReqId++));
1450
+ w.firstFrameSeen = false;
1451
+ sendSubscribe(w, clock(), false);
1452
+ }
1453
+ log("info", "auth-token-refreshed", `${need.purpose}: resubscribed with a fresh token`);
1454
+ },
1455
+ (err) => {
1456
+ for (const w of users()) failWire(w, err);
1457
+ }
1458
+ );
1459
+ }
1460
+ function armAckTimeout(w) {
1461
+ clearAckTimer(w);
1462
+ const reqId = w.reqId;
1463
+ const ms = w.wire.staleAfterMs === null ? codec.staleAfterMs : w.wire.staleAfterMs;
1464
+ w.ackTimer = setTimeout(() => {
1465
+ w.ackTimer = null;
1466
+ if (closed || !w.shard.conn.isOpen || wires.get(w.wire.key) !== w || w.reqId !== reqId || w.acked) return;
1467
+ const msg = `${venue}: no acknowledgement or data for ${w.wire.key} within ${ms} ms of subscribing`;
1468
+ log("error", "ack-timeout", msg);
1469
+ const err = wsError("WS_PROTOCOL_ERROR", msg, "The venue ignored the subscription; check the channel/symbol or retry later");
1470
+ const wasSuspect = w.suspect;
1471
+ w.suspect = false;
1472
+ for (const sub of Array.from(w.refs)) {
1473
+ if (sub.status === "pending") dropSub(sub, err);
1474
+ else sub.emit(errorEvent("WS_PROTOCOL_ERROR", msg, null, null, false));
1475
+ }
1476
+ if (wasSuspect) probeNextSuspect(w.shard);
1477
+ }, ms);
1478
+ }
1479
+ function clearAckTimer(w) {
1480
+ if (w.ackTimer !== null) {
1481
+ clearTimeout(w.ackTimer);
1482
+ w.ackTimer = null;
1483
+ }
1484
+ }
1485
+ function maybeReady(sub) {
1486
+ const meta = subs.get(sub);
1487
+ if (meta === void 0 || sub.status !== "pending") return;
1488
+ if (meta.wires.every((k) => wires.get(k)?.acked === true)) sub.markActive();
1489
+ }
1490
+ function markAcked(w) {
1491
+ if (w.acked) return;
1492
+ w.acked = true;
1493
+ w.everAcked = true;
1494
+ w.probe = false;
1495
+ clearAckTimer(w);
1496
+ if (w.suspect) {
1497
+ w.suspect = false;
1498
+ probeNextSuspect(w.shard);
1499
+ }
1500
+ for (const sub of w.refs) maybeReady(sub);
1501
+ }
1502
+ function wireOn(shard, key) {
1503
+ const w = wires.get(key);
1504
+ if (w === void 0) log("info", "late-frame", `frame for ${key} after its unsubscribe (in flight)`);
1505
+ else if (w.shard !== shard) log("info", "late-frame", `frame for ${key} on socket ${shard.id}; it lives on socket ${w.shard.id}`);
1506
+ else return w;
1507
+ return void 0;
1508
+ }
1509
+ function findWire(ev) {
1510
+ if (ev.wire !== null) return wires.get(ev.wire);
1511
+ if (ev.reqId !== null) {
1512
+ for (const w of wires.values()) if (w.reqId === ev.reqId) return w;
1513
+ }
1514
+ return void 0;
1515
+ }
1516
+ function onText(shard, text, recvTs) {
1517
+ if (dir === null || store === null) {
1518
+ broadcast(errorEvent("WS_PROTOCOL_ERROR", "Frame received before the market directory loaded", null, text, false));
1519
+ return;
1520
+ }
1521
+ let events;
1522
+ try {
1523
+ events = codec.decode(text, dir, recvTs);
1524
+ } catch (e) {
1525
+ events = [{ t: "error", code: "WS_PROTOCOL_ERROR", message: `Codec failed: ${e instanceof Error ? e.message : String(e)}`, wire: null, fatal: false, remediation: null }];
1526
+ }
1527
+ for (const ev of events) handleEvent(shard, ev, text, recvTs);
1528
+ }
1529
+ function handleEvent(shard, ev, text, recvTs) {
1530
+ const connection = shard.conn;
1531
+ switch (ev.t) {
1532
+ case "pong":
1533
+ connection.notePong();
1534
+ return;
1535
+ case "ignore":
1536
+ return;
1537
+ case "ack": {
1538
+ if (ev.unsubscribe) return;
1539
+ const w = findWire(ev);
1540
+ if (w === void 0 || w.shard !== shard) {
1541
+ log("info", "late-ack", w === void 0 ? "ack for a channel that is no longer subscribed" : `ack for ${w.wire.key} on socket ${shard.id}; it lives on socket ${w.shard.id}`, { wire: ev.wire, reqId: ev.reqId });
1542
+ return;
1543
+ }
1544
+ markAcked(w);
1545
+ return;
1546
+ }
1547
+ case "error": {
1548
+ const w = ev.wire !== null ? wires.get(ev.wire) : ev.reqId !== void 0 ? findWire({ wire: null, reqId: ev.reqId }) : void 0;
1549
+ if (w !== void 0 && w.shard !== shard) {
1550
+ log("info", "late-frame", `error for ${w.wire.key} on socket ${shard.id}; it lives on socket ${w.shard.id}`, { code: ev.code, fatal: ev.fatal });
1551
+ return;
1552
+ }
1553
+ const e = errorEvent(ev.code, ev.message, ev.remediation, text, ev.fatal);
1554
+ log(ev.fatal ? "error" : "warn", "protocol-error", e.type === "error" ? e.message : "", { wire: ev.wire });
1555
+ if (w === void 0) {
1556
+ broadcast(e.type === "error" && e.fatal ? { ...e, fatal: false } : e);
1557
+ return;
1558
+ }
1559
+ if (ev.fatal) {
1560
+ const err = wsError(ev.code, ev.message, ev.remediation === null ? void 0 : ev.remediation);
1561
+ for (const sub of Array.from(w.refs)) dropSub(sub, err);
1562
+ } else {
1563
+ for (const sub of w.refs) sub.emit(e);
1564
+ }
1565
+ return;
1566
+ }
1567
+ case "user": {
1568
+ const w = wireOn(shard, ev.wire);
1569
+ if (w === void 0) return;
1570
+ w.lastAt = recvTs;
1571
+ markAcked(w);
1572
+ connection.markHealthy();
1573
+ if (ev.seq !== void 0 && ev.seq !== null) {
1574
+ if (w.lastSeq !== null && ev.seq < w.lastSeq) {
1575
+ log("info", "stale-snapshot", `${w.wire.key}: frame seq ${ev.seq} is older than ${w.lastSeq}; dropped`);
1576
+ return;
1577
+ }
1578
+ w.lastSeq = ev.seq;
1579
+ }
1580
+ for (const d of ev.deltas) {
1581
+ const r = userStore.apply(d, recvTs);
1582
+ if (r.fillsGap) emitGap(w, "fills", "replayed snapshot does not overlap the fills seen before (outage longer than the replay window)");
1583
+ for (const u of r.updates) dispatchUser(w, u);
1584
+ }
1585
+ return;
1586
+ }
1587
+ case "bbo": {
1588
+ const w = wireOn(shard, ev.wire);
1589
+ if (w === void 0) return;
1590
+ w.lastAt = recvTs;
1591
+ markAcked(w);
1592
+ connection.markHealthy();
1593
+ if (ev.seq !== null) {
1594
+ if (w.lastSeq !== null && ev.seq <= w.lastSeq) return;
1595
+ w.lastSeq = ev.seq;
1596
+ }
1597
+ const entry = store.entry(ev.symbol);
1598
+ if (entry === void 0) {
1599
+ for (const sub of w.refs) sub.emit(errorEvent("WS_PROTOCOL_ERROR", `bbo for ${ev.symbol} which is not in the market directory`, null, null, false));
1600
+ return;
1601
+ }
1602
+ const u = {
1603
+ type: "bbo",
1604
+ schema: 1,
1605
+ venue,
1606
+ dex: entry.dex,
1607
+ symbol: entry.symbol,
1608
+ bid: ev.bid,
1609
+ bidSize: ev.bidSize,
1610
+ ask: ev.ask,
1611
+ askSize: ev.askSize,
1612
+ venueTs: ev.venueTs,
1613
+ recvTs
1614
+ };
1615
+ for (const sub of w.refs) sub.emit(u);
1616
+ return;
1617
+ }
1618
+ case "market": {
1619
+ const w = wireOn(shard, ev.wire);
1620
+ if (w === void 0) return;
1621
+ const s = store;
1622
+ w.lastAt = recvTs;
1623
+ markAcked(w);
1624
+ connection.markHealthy();
1625
+ const source = w.firstFrameSeen ? "ws" : "snapshot";
1626
+ w.firstFrameSeen = true;
1627
+ const unknown = [];
1628
+ for (const patch of ev.patches) {
1629
+ const u = s.apply(patch, recvTs, source);
1630
+ if (u === void 0) {
1631
+ if (!reportedUnknown.has(patch.symbol)) {
1632
+ reportedUnknown.add(patch.symbol);
1633
+ unknown.push(patch.symbol);
1634
+ }
1635
+ continue;
1636
+ }
1637
+ for (const sub of w.refs) {
1638
+ const meta = subs.get(sub);
1639
+ if (meta !== void 0 && (meta.symbols === null || meta.symbols.has(u.symbol))) sub.emit(u);
1640
+ }
1641
+ }
1642
+ if (unknown.length > 0) {
1643
+ const msg = `${unknown.length} market(s) on ${ev.wire} are not in the market directory (new listing?): ${unknown.slice(0, 10).join(", ")}${unknown.length > 10 ? ", \u2026" : ""}`;
1644
+ const e = errorEvent("WS_PROTOCOL_ERROR", msg, "Recreate the stream to reload the market directory", null, false);
1645
+ log("warn", "unknown-market", msg);
1646
+ for (const sub of w.refs) sub.emit(e);
1647
+ }
1648
+ if (connection.state.status === "stale") connection.evaluate();
1649
+ return;
1650
+ }
1651
+ }
1652
+ }
1653
+ function handleMaxPayloadClose(shard, reason) {
1654
+ const now = clock();
1655
+ const closes = shard.payloadCloses;
1656
+ while (closes.length > 0 && now - closes[0] > MAX_PAYLOAD_WINDOW_MS) closes.shift();
1657
+ closes.push(now);
1658
+ log("warn", "max-payload", reason.message, { count: closes.length, limit: MAX_PAYLOAD_CLOSES, socket: shard.id });
1659
+ if (closes.length < MAX_PAYLOAD_CLOSES) return false;
1660
+ closes.length = 0;
1661
+ const err = wsError(
1662
+ "WS_PROTOCOL_ERROR",
1663
+ `${venue}: inbound messages exceeded the transport's maxPayload ${MAX_PAYLOAD_CLOSES} times (WS_ERR_UNSUPPORTED_MESSAGE_LENGTH); stopped reconnecting`,
1664
+ reason.remediation === null ? void 0 : reason.remediation,
1665
+ { wsCode: 1009 }
1666
+ );
1667
+ log("error", "max-payload-repeated", err.message);
1668
+ const onShard = new Set(wiresOn(shard).flatMap((w) => Array.from(w.refs)));
1669
+ for (const sub of onShard) dropSub(sub, err);
1670
+ return wiresOn(shard).length === 0;
1671
+ }
1672
+ function handleBadSubscribeClose(shard, reason, opened) {
1673
+ if (!codec.closesOnBadSubscribe || !opened) return false;
1674
+ const now = clock();
1675
+ const suspects = wiresOn(shard).filter((w2) => w2.probe && !w2.acked && w2.sentAt !== null && now - w2.sentAt <= BAD_SUBSCRIBE_WINDOW_MS);
1676
+ if (suspects.length > 1) {
1677
+ for (const w2 of suspects) w2.suspect = true;
1678
+ log(
1679
+ "warn",
1680
+ "bad-subscribe-ambiguous",
1681
+ `${venue} dropped the socket with ${suspects.length} new channels pending; probing them one at a time`,
1682
+ { channels: suspects.map((w2) => w2.wire.key) }
1683
+ );
1684
+ return false;
1685
+ }
1686
+ if (suspects.length !== 1) return false;
1687
+ const w = suspects[0];
1688
+ const err = wsError(
1689
+ "INVALID_PARAMS",
1690
+ `${venue} closed the socket right after subscribing to ${w.wire.key} (the venue drops the socket on an invalid subscription)`,
1691
+ "Check the symbol against stream.markets(); the other subscriptions reconnect"
1692
+ );
1693
+ log("error", "bad-subscribe", err.message, { wsCode: reason.wsCode });
1694
+ for (const sub of Array.from(w.refs)) dropSub(sub, err);
1695
+ return wiresOn(shard).length === 0;
1696
+ }
1697
+ function probeNextSuspect(shard) {
1698
+ if (!shard.conn.isOpen) return;
1699
+ const held = wiresOn(shard).filter((w) => w.suspect);
1700
+ if (held.length === 0 || held.some((w) => w.sentAt !== null)) return;
1701
+ sendSubscribe(held[0], clock(), true);
1702
+ }
1703
+ function releaseWires(sub) {
1704
+ const meta = subs.get(sub);
1705
+ subs.delete(sub);
1706
+ if (meta === void 0) return;
1707
+ const touched = /* @__PURE__ */ new Set();
1708
+ const releasedSuspect = /* @__PURE__ */ new Set();
1709
+ for (const key of meta.wires) {
1710
+ const w = wires.get(key);
1711
+ if (w === void 0) continue;
1712
+ w.refs.delete(sub);
1713
+ if (w.refs.size > 0) continue;
1714
+ clearAckTimer(w);
1715
+ wires.delete(key);
1716
+ w.shard.wires.delete(w);
1717
+ touched.add(w.shard);
1718
+ if (w.suspect) releasedSuspect.add(w.shard);
1719
+ if (w.shard.conn.isOpen && w.sentAt !== null) {
1720
+ w.shard.conn.send(codec.encodeUnsubscribe(w.wire, nextReqId++));
1721
+ }
1722
+ }
1723
+ for (const sh of touched) {
1724
+ if (sh.wires.size === 0 && !closed && sh.conn.state.status !== "closed") sh.conn.closeIdle();
1725
+ }
1726
+ for (const sh of releasedSuspect) probeNextSuspect(sh);
1727
+ if (!closed && touched.size > 0) reportState();
1728
+ }
1729
+ function dropSub(sub, err) {
1730
+ releaseWires(sub);
1731
+ sub.fail(err);
1732
+ }
1733
+ async function activate(sub, target) {
1734
+ if (closed) throw closedError(venue);
1735
+ terminalCheck();
1736
+ if (url === null) {
1737
+ throw wsError(
1738
+ "CAPABILITY_UNSUPPORTED",
1739
+ `${venue} has no WebSocket on ${network}`,
1740
+ codec.capabilities.notes[`network:${network}`] === void 0 ? void 0 : codec.capabilities.notes[`network:${network}`]
1741
+ );
1742
+ }
1743
+ if (target.kind === "market") {
1744
+ const channels = target.channels;
1745
+ const valid = `Use channels ${MARKET_CHANNELS.map((c) => `"${c}"`).join(" / ")} (book and trades arrive in 0.15.1)`;
1746
+ if (!Array.isArray(channels) || channels.length === 0) {
1747
+ throw wsError("INVALID_PARAMS", 'kind "market" needs at least one channel', valid);
1748
+ }
1749
+ const unknown = channels.filter((c) => typeof c !== "string" || !MARKET_CHANNELS.includes(c));
1750
+ if (unknown.length > 0) throw wsError("INVALID_PARAMS", `${venue}: unknown channel(s) ${unknown.map((c) => JSON.stringify(c)).join(", ")}`, valid);
1751
+ const later = target.channels.filter((c) => LATER_CHANNELS.includes(c));
1752
+ if (later.length > 0) {
1753
+ throw wsError(
1754
+ "CAPABILITY_UNSUPPORTED",
1755
+ `${venue} channel(s) ${later.join(", ")} are not available in this release`,
1756
+ 'Use channels "market" / "bbo" (book and trades arrive in 0.15.1)'
1757
+ );
1758
+ }
1759
+ if (target.channels.includes("bbo") && !codec.capabilities.channels.bbo) {
1760
+ throw wsError("CAPABILITY_UNSUPPORTED", `${venue} has no bbo channel`, 'Use channel "market"');
1761
+ }
1762
+ }
1763
+ if (target.kind === "allMarkets") checkAllMarketsTarget(target);
1764
+ if (target.kind === "user") checkUserTarget(target.channels);
1765
+ const d = await loadDirectory();
1766
+ if (closed) throw closedError(venue);
1767
+ terminalCheck();
1768
+ if (sub.closed) return;
1769
+ let list;
1770
+ let wanted = null;
1771
+ let canonicalWarning = null;
1772
+ if (target.kind === "user") {
1773
+ const ref = await resolveUser();
1774
+ list = codec.user.wires(ref, target.channels, d);
1775
+ for (const w of list) if (w.auth !== void 0) await getToken(w.auth);
1776
+ if (closed) throw closedError(venue);
1777
+ if (sub.closed) return;
1778
+ } else {
1779
+ const known = new Set(d.entries.map((e) => e.symbol));
1780
+ if (target.kind === "allMarkets" && target.canonicals !== void 0) {
1781
+ const r = symbolsForCanonicals(target.canonicals, d.entries);
1782
+ wanted = r.symbols;
1783
+ canonicalWarning = r.warning;
1784
+ } else {
1785
+ wanted = target.kind === "allMarkets" ? target.symbols === void 0 ? null : target.symbols : [target.symbol];
1786
+ }
1787
+ if (wanted !== null) {
1788
+ const missing = wanted.filter((s) => !known.has(s));
1789
+ if (missing.length > 0) {
1790
+ const hints = missing.slice(0, 20).map((s) => ({ s, c: suggestSymbols(s, d.entries) })).filter((h) => h.c.length > 0);
1791
+ const hint = hints.length === 0 ? "" : ` (did you mean ${hints.map((h) => `${h.c.join(" / ")} for ${h.s}`).join("; ")}?)`;
1792
+ const base = "Symbols are this venue's directory symbols (case-sensitive): use stream.markets(), MarketUpdate.symbol or FundingLeg.symbol";
1793
+ throw wsError(
1794
+ "INVALID_PARAMS",
1795
+ `${venue}: unknown symbol(s) ${missing.slice(0, 20).join(", ")}${hint}`,
1796
+ venue === "hyperliquid" ? `${base}; HIP-3 markets need opts.hip3Dexes` : base,
1797
+ hints.length === 0 ? void 0 : { suggestions: Object.fromEntries(hints.map((h) => [h.s, h.c])) }
1798
+ );
1799
+ }
1800
+ }
1801
+ list = codec.wiresFor(target, d, opts.staleAfterMs === void 0 ? null : opts.staleAfterMs);
1802
+ }
1803
+ const fresh = list.filter((w) => !wires.has(w.key));
1804
+ if (opts.budget?.maxSockets !== void 0 && opts.budget.maxSockets < 1) {
1805
+ throw wsError("SUBSCRIPTION_LIMIT", `${venue} needs 1 socket; budget.maxSockets is ${opts.budget.maxSockets}`, "Raise budget.maxSockets", { needed: 1, allowed: opts.budget.maxSockets });
1806
+ }
1807
+ const allowed = Math.min(codec.limits.subscriptionsPerSocket * maxSockets, opts.budget?.maxSubscriptions === void 0 ? Infinity : opts.budget.maxSubscriptions);
1808
+ if (wires.size + fresh.length > allowed) {
1809
+ throw wsError(
1810
+ "SUBSCRIPTION_LIMIT",
1811
+ `${venue}: ${wires.size + fresh.length} wire subscriptions needed, ${allowed} allowed`,
1812
+ "Unsubscribe something, narrow the symbols, or raise budget.maxSubscriptions (\u2264 the venue limit)",
1813
+ { needed: wires.size + fresh.length, allowed }
1814
+ );
1815
+ }
1816
+ const placement = placeWires(fresh);
1817
+ subs.set(sub, {
1818
+ symbols: wanted === null ? null : new Set(wanted),
1819
+ wires: list.map((w) => w.key),
1820
+ userChannels: target.kind === "user" ? new Set(target.channels) : null
1821
+ });
1822
+ const now = clock();
1823
+ for (const w of list) {
1824
+ let st = wires.get(w.key);
1825
+ if (st === void 0) {
1826
+ const slot = placement.get(w.key);
1827
+ st = {
1828
+ wire: w,
1829
+ shard: slot < shards.length ? shards[slot] : newShard(),
1830
+ refs: /* @__PURE__ */ new Set(),
1831
+ reqId: 0,
1832
+ sentAt: null,
1833
+ lastAt: null,
1834
+ acked: false,
1835
+ firstFrameSeen: false,
1836
+ probe: false,
1837
+ suspect: false,
1838
+ ackTimer: null,
1839
+ everAcked: false,
1840
+ lastSeq: null
1841
+ };
1842
+ wires.set(w.key, st);
1843
+ st.shard.wires.add(st);
1844
+ sendSubscribe(st, now, true);
1845
+ }
1846
+ st.refs.add(sub);
1847
+ }
1848
+ for (const w of directoryWarnings) sub.emit(errorEvent("EXCHANGE_ERROR", w, null, null, false));
1849
+ if (canonicalWarning !== null && canonicalWarning.type === "error") {
1850
+ log("warn", "canonicals-missing", canonicalWarning.message, { missing: canonicalWarning.details?.missing });
1851
+ sub.emit(canonicalWarning);
1852
+ }
1853
+ if (target.kind === "user") {
1854
+ for (const w of list) {
1855
+ const g = w.gaps?.onSubscribe;
1856
+ if (g === void 0) continue;
1857
+ for (const ch of g) {
1858
+ if (!target.channels.includes(ch)) continue;
1859
+ sub.emit({ type: "gap", venue, channel: ch, symbol: null, expected: null, got: null, action: "refetch-required" });
1860
+ const st = wires.get(w.key);
1861
+ if (ch === "orders" && st !== void 0) dispatchUser(st, userStore.currentOrders(clock()), sub);
1862
+ }
1863
+ }
1864
+ }
1865
+ maybeReady(sub);
1866
+ for (const sh of new Set(list.map((w) => wires.get(w.key).shard))) sh.conn.ensureOpen();
1867
+ reportState();
1868
+ }
1869
+ function placeWires(fresh) {
1870
+ const perGroup = codec.limits.perGroup === void 0 ? {} : codec.limits.perGroup;
1871
+ const inGroup = (groups, g) => {
1872
+ const n = groups.get(g);
1873
+ return n === void 0 ? 0 : n;
1874
+ };
1875
+ const load = shards.map((sh) => {
1876
+ const on = wiresOn(sh);
1877
+ const groups = /* @__PURE__ */ new Map();
1878
+ for (const w of on) if (w.wire.group !== void 0) groups.set(w.wire.group, inGroup(groups, w.wire.group) + 1);
1879
+ return { total: on.length, groups };
1880
+ });
1881
+ const out = /* @__PURE__ */ new Map();
1882
+ for (const w of fresh) {
1883
+ const cap = w.group === void 0 ? void 0 : perGroup[w.group];
1884
+ const fits = (l) => l.total < codec.limits.subscriptionsPerSocket && (cap === void 0 || inGroup(l.groups, w.group) < cap);
1885
+ let i = load.findIndex(fits);
1886
+ if (i === -1 && load.length < maxSockets) {
1887
+ load.push({ total: 0, groups: /* @__PURE__ */ new Map() });
1888
+ i = load.length - 1;
1889
+ }
1890
+ if (i === -1) {
1891
+ if (cap !== void 0) {
1892
+ const needed = Array.from(wires.values()).filter((x) => x.wire.group === w.group).length + fresh.filter((x) => x.group === w.group).length;
1893
+ const max = cap * maxSockets;
1894
+ throw wsError(
1895
+ "SUBSCRIPTION_LIMIT",
1896
+ `${venue}: ${needed} "${w.group}" channels needed, ${cap} per connection \xD7 ${maxSockets} connection(s) = ${max} allowed`,
1897
+ `Unsubscribe some ${w.group} channels${maxSockets < (codec.limits.maxSockets === void 0 ? 1 : codec.limits.maxSockets) ? ", or raise budget.maxSockets (\u2264 the venue limit)" : ""}`,
1898
+ { needed, allowed: max }
1899
+ );
1900
+ }
1901
+ throw wsError(
1902
+ "SUBSCRIPTION_LIMIT",
1903
+ `${venue}: no connection has room for ${w.key} (${codec.limits.subscriptionsPerSocket} per connection \xD7 ${maxSockets} connection(s))`,
1904
+ "Unsubscribe something or narrow the symbols",
1905
+ { needed: wires.size + fresh.length, allowed: codec.limits.subscriptionsPerSocket * maxSockets }
1906
+ );
1907
+ }
1908
+ load[i].total++;
1909
+ if (w.group !== void 0) load[i].groups.set(w.group, inGroup(load[i].groups, w.group) + 1);
1910
+ out.set(w.key, i);
1911
+ }
1912
+ return out;
1913
+ }
1914
+ function checkAllMarketsTarget(t) {
1915
+ const listOf = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
1916
+ if (t.symbols !== void 0 && t.canonicals !== void 0) {
1917
+ throw wsError(
1918
+ "INVALID_PARAMS",
1919
+ 'kind "allMarkets": pass either symbols or canonicals, not both',
1920
+ "canonicals for the same markets on several venues; symbols for this venue's exact directory symbols"
1921
+ );
1922
+ }
1923
+ for (const k of ["symbols", "canonicals"]) {
1924
+ const v = t[k];
1925
+ if (v !== void 0 && (!listOf(v) || v.length === 0)) {
1926
+ throw wsError("INVALID_PARAMS", `kind "allMarkets": ${k} must be a non-empty array of strings`, `Omit ${k} for every market`);
1927
+ }
1928
+ }
1929
+ }
1930
+ function symbolsForCanonicals(canonicals, entries) {
1931
+ const want = new Set(canonicals);
1932
+ const symbols = entries.filter((e) => e.canonical !== null && want.has(e.canonical)).map((e) => e.symbol);
1933
+ const missing = Array.from(new Set(canonicals.filter((c) => !entries.some((e) => e.canonical === c))));
1934
+ if (missing.length === 0) return { symbols, warning: null };
1935
+ const pairs = missing.map((c) => ({ c, s: suggestCanonicals(c, entries) }));
1936
+ const suggestions = Object.fromEntries(pairs.map((p) => [p.c, p.s]));
1937
+ const withHints = pairs.filter((p) => p.s.length > 0);
1938
+ const hint = withHints.length === 0 ? "" : ` (did you mean ${withHints.slice(0, 20).map((p) => `${p.s.join(" / ")} for ${p.c}`).join("; ")}?)`;
1939
+ const remediation = `Canonicals are "<BASE>" (USD-like quotes) or "<dex>:<BASE>" (HIP-3${venue === "hyperliquid" ? ", needs opts.hip3Dexes" : ""}); see MarketDirectoryEntry.canonical in stream.markets()`;
1940
+ const list = missing.slice(0, 20).join(", ");
1941
+ if (symbols.length === 0) {
1942
+ throw wsError(
1943
+ "INVALID_PARAMS",
1944
+ `${venue}: no market with canonical(s) ${list}${hint}`,
1945
+ remediation,
1946
+ withHints.length === 0 ? void 0 : { suggestions: Object.fromEntries(withHints.map((p) => [p.c, p.s])) }
1947
+ );
1948
+ }
1949
+ const message = `${venue}: no market with canonical(s) ${list}${hint}; subscribed to the other ${canonicals.length - missing.length}`;
1950
+ return { symbols, warning: errorEvent("SYMBOL_NOT_FOUND", message, remediation, null, false, { missing, suggestions }) };
1951
+ }
1952
+ function checkUserTarget(channels) {
1953
+ if (codec.user === null) {
1954
+ const note = codec.capabilities.notes.user;
1955
+ throw wsError("CAPABILITY_UNSUPPORTED", `${venue} user channels are not available in this release`, note === void 0 ? void 0 : note);
1956
+ }
1957
+ if (codec.user.inPageSecrets && isBrowserLike() && opts.allowInPageSecrets !== true) {
1958
+ throw wsError(
1959
+ "CAPABILITY_UNSUPPORTED",
1960
+ `${venue} user channels need a signing secret in the page`,
1961
+ "Pass allowInPageSecrets: true to accept that trade-off, or run the stream server-side (the /node entry)"
1962
+ );
1963
+ }
1964
+ if (!Array.isArray(channels)) throw wsError("INVALID_PARAMS", 'kind "user" needs a channels array', `Use ${USER_CHANNELS.join(" / ")}`);
1965
+ const bad = channels.filter((c) => !USER_CHANNELS.includes(c));
1966
+ if (channels.length === 0 || bad.length > 0) {
1967
+ throw wsError(
1968
+ "INVALID_PARAMS",
1969
+ channels.length === 0 ? 'kind "user" needs at least one channel' : `unknown user channel(s) ${bad.join(", ")}`,
1970
+ `Use ${USER_CHANNELS.join(" / ")}`
1971
+ );
1972
+ }
1973
+ if (opts.credentials === void 0) {
1974
+ throw wsError(
1975
+ "WS_AUTH_FAILED",
1976
+ `${venue} user channels need opts.credentials (CredentialProvider.user())`,
1977
+ "Pass credentials: { user: async () => ({ venue, \u2026 }) }; the library never holds keys"
1978
+ );
1979
+ }
1980
+ }
1981
+ async function resolveUser() {
1982
+ if (userRef !== null) return userRef;
1983
+ const provider = opts.credentials;
1984
+ if (provider === void 0) throw wsError("WS_AUTH_FAILED", `${venue}: no credential provider`);
1985
+ let ref;
1986
+ try {
1987
+ ref = await provider.user();
1988
+ } catch (e) {
1989
+ throw toAuthError(e);
1990
+ }
1991
+ const problem = userRefProblem(venue, ref);
1992
+ if (problem !== null) throw wsError("INVALID_PARAMS", `${venue} credentials.user(): ${problem}`, "Return the UserRef shape documented for this venue");
1993
+ if (userRef === null) userRef = ref;
1994
+ return userRef;
1995
+ }
1996
+ function terminalCheck() {
1997
+ if (url === null) return;
1998
+ const agg = aggregateState();
1999
+ if (agg.status !== "closed") return;
2000
+ const r = agg.reason;
2001
+ if (r.code === "CLIENT_CLOSE") throw closedError(venue);
2002
+ throw wsError(r.code, `${venue} stream is closed: ${r.message}`, r.remediation === null ? "Create a new stream" : r.remediation);
2003
+ }
2004
+ function subscribe(target) {
2005
+ const sub = new SubscriptionImpl(
2006
+ venue,
2007
+ bufferSize,
2008
+ async (s) => {
2009
+ releaseWires(s);
2010
+ s.end();
2011
+ },
2012
+ (e) => log("warn", "listener-error", `subscription listener threw: ${e instanceof Error ? e.message : String(e)}`),
2013
+ (s, err) => {
2014
+ log("error", "overflow", err.message);
2015
+ dropSub(s, err);
2016
+ }
2017
+ );
2018
+ activate(sub, target).catch((e) => {
2019
+ const err = e instanceof WsError ? e : wsError("EXCHANGE_ERROR", e instanceof Error ? e.message : String(e));
2020
+ log("warn", "subscribe-rejected", err.message, { code: err.structured.code });
2021
+ dropSub(sub, err);
2022
+ });
2023
+ return sub;
2024
+ }
2025
+ const stream = {
2026
+ venue,
2027
+ network,
2028
+ capabilities: codec.capabilities,
2029
+ staleAfterMs: opts.staleAfterMs === void 0 ? codec.staleAfterMs : opts.staleAfterMs,
2030
+ get state() {
2031
+ if (url === null) {
2032
+ return { status: "closed", reason: { code: "CAPABILITY_UNSUPPORTED", message: `${venue} has no WebSocket on ${network}`, wsCode: null, remediation: null } };
2033
+ }
2034
+ return aggregateState();
2035
+ },
2036
+ subscribe,
2037
+ onState(cb) {
2038
+ stateListeners.add(cb);
2039
+ return () => {
2040
+ stateListeners.delete(cb);
2041
+ };
2042
+ },
2043
+ async markets() {
2044
+ const d = await loadDirectory();
2045
+ return d.entries.slice();
2046
+ },
2047
+ async close() {
2048
+ if (closed) return;
2049
+ closed = true;
2050
+ if (refreshTimer !== null) {
2051
+ clearInterval(refreshTimer);
2052
+ refreshTimer = null;
2053
+ }
2054
+ for (const t of tokens.values()) if (t.timer !== null) {
2055
+ clearTimeout(t.timer);
2056
+ t.timer = null;
2057
+ }
2058
+ for (const t of tokenRetryTimers) clearTimeout(t);
2059
+ tokenRetryTimers.clear();
2060
+ tokenLoops.clear();
2061
+ for (const w of wires.values()) clearAckTimer(w);
2062
+ wires.clear();
2063
+ for (const sh of shards) sh.wires.clear();
2064
+ for (const sh of shards) sh.conn.close();
2065
+ for (const sub of Array.from(subs.keys())) {
2066
+ subs.delete(sub);
2067
+ sub.end();
2068
+ }
2069
+ stateListeners.clear();
2070
+ }
2071
+ };
2072
+ return stream;
2073
+ }
2074
+
2075
+ // src/ws/venue-static.ts
2076
+ var VENUE_STATIC = {
2077
+ hyperliquid: {
2078
+ wsUrl: { mainnet: "wss://api.hyperliquid.xyz/ws", testnet: "wss://api.hyperliquid-testnet.xyz/ws" },
2079
+ restUrl: { mainnet: "https://api.hyperliquid.xyz", testnet: "https://api.hyperliquid-testnet.xyz" },
2080
+ rateIntervalHours: 1,
2081
+ settlementIntervalHours: 1,
2082
+ fundingVerified: true,
2083
+ // Idle limit 60 s (Phase 0: closed at 60.1 s, no close frame).
2084
+ ping: { message: JSON.stringify({ method: "ping" }), everyMs: 2e4 },
2085
+ // allDexsAssetCtxs cadence ~11–15 s (Phase 0) → 30 s; per-coin activeAssetCtx uses 10 s.
2086
+ staleAfterMs: 3e4,
2087
+ // Per IP: 1000 subs, 10 conns, 30 new conns/min, 2000 msgs/min.
2088
+ limits: { subscriptionsPerSocket: 1e3, newConnectionsPerMin: 30, outboundPerMin: 2e3 }
2089
+ },
2090
+ pacifica: {
2091
+ wsUrl: { mainnet: "wss://ws.pacifica.fi/ws", testnet: "wss://test-ws.pacifica.fi/ws" },
2092
+ restUrl: { mainnet: "https://api.pacifica.fi/api/v1", testnet: "https://test-api.pacifica.fi/api/v1" },
2093
+ rateIntervalHours: 1,
2094
+ settlementIntervalHours: 1,
2095
+ fundingVerified: true,
2096
+ ping: { message: JSON.stringify({ method: "ping" }), everyMs: 2e4 },
2097
+ staleAfterMs: 1e4,
2098
+ // `prices` every 3 s
2099
+ // docs.pacifica.fi/api-documentation/api/rate-limits: max 100 concurrent connections per IP, max
2100
+ // 20 subscriptions per channel per connection → perGroup (one group per channel `source`) and up
2101
+ // to 50 sockets per stream (half the per-IP cap: other streams / processes share the IP). The
2102
+ // server closes a socket after 60 s without a client message (ping every 20 s) and at 24 h
2103
+ // (reconnect + resubscribe). No documented total per socket, new-connection rate or outbound
2104
+ // rate: those are library self-limits, not venue values.
2105
+ limits: {
2106
+ subscriptionsPerSocket: 100,
2107
+ newConnectionsPerMin: 60,
2108
+ outboundPerMin: 1e3,
2109
+ maxSockets: 50,
2110
+ perGroup: { prices: 20, bbo: 20, account_info: 20, account_positions: 20, account_order_updates: 20, account_trades: 20 }
2111
+ }
2112
+ },
2113
+ lighter: {
2114
+ wsUrl: { mainnet: "wss://mainnet.zklighter.elliot.ai/stream", testnet: "wss://testnet.zklighter.elliot.ai/stream" },
2115
+ restUrl: { mainnet: "https://mainnet.zklighter.elliot.ai", testnet: "https://testnet.zklighter.elliot.ai" },
2116
+ // WS funding_rate / current_funding_rate are PERCENT per 1 h (Phase 0); the CLI REST basis
2117
+ // (perHours 8) applies to REST /api/v1/funding-rates only.
2118
+ rateIntervalHours: 1,
2119
+ settlementIntervalHours: 1,
2120
+ fundingVerified: true,
2121
+ ping: { message: JSON.stringify({ type: "ping" }), everyMs: 3e4 },
2122
+ // idle limit 120 s
2123
+ staleAfterMs: 1e4,
2124
+ // market_stats/all updates ~1 s
2125
+ limits: { subscriptionsPerSocket: 500, newConnectionsPerMin: 255, outboundPerMin: 200 }
2126
+ },
2127
+ "lighter-rh": {
2128
+ // Testnet WS answers HTTP 403 (CloudFront, Phase 0) → unsupported.
2129
+ wsUrl: { mainnet: "wss://api.rh.lighter.xyz/stream", testnet: null },
2130
+ restUrl: { mainnet: "https://api.rh.lighter.xyz", testnet: "https://api.rh-testnet.lighter.xyz" },
2131
+ rateIntervalHours: 1,
2132
+ settlementIntervalHours: 1,
2133
+ fundingVerified: true,
2134
+ ping: { message: JSON.stringify({ type: "ping" }), everyMs: 3e4 },
2135
+ staleAfterMs: 1e4,
2136
+ limits: { subscriptionsPerSocket: 500, newConnectionsPerMin: 255, outboundPerMin: 200 }
2137
+ },
2138
+ aster: {
2139
+ wsUrl: { mainnet: "wss://fstream.asterdex.com", testnet: "wss://fstream.asterdex-testnet.com" },
2140
+ restUrl: { mainnet: "https://fapi.asterdex.com", testnet: "https://fapi.asterdex-testnet.com" },
2141
+ rateIntervalHours: "perMarket",
2142
+ // rest:/fapi/v3/fundingInfo
2143
+ settlementIntervalHours: "perMarket",
2144
+ fundingVerified: true,
2145
+ ping: null,
2146
+ // server protocol pings, answered by the runtime
2147
+ staleAfterMs: 5e3,
2148
+ // markPrice@1s
2149
+ // 200 streams/conn, 10 client→server msgs/s. No documented new-connection rate: a library self-limit.
2150
+ limits: { subscriptionsPerSocket: 200, newConnectionsPerMin: 60, outboundPerMin: 600 }
2151
+ },
2152
+ arcus: {
2153
+ wsUrl: { mainnet: "wss://api.arcus.xyz/v1/ws", testnet: "wss://api.testnet.arcus.xyz/v1/ws" },
2154
+ restUrl: { mainnet: "https://api.arcus.xyz", testnet: "https://api.testnet.arcus.xyz" },
2155
+ rateIntervalHours: 1,
2156
+ settlementIntervalHours: 1,
2157
+ fundingVerified: true,
2158
+ ping: { message: JSON.stringify({ type: "ping" }), everyMs: 3e4 },
2159
+ staleAfterMs: 15e3,
2160
+ // `markets` every ~5 s
2161
+ limits: { subscriptionsPerSocket: 100, newConnectionsPerMin: 50, outboundPerMin: 1e3 }
2162
+ }
2163
+ };
2164
+
2165
+ // src/ws/funding.ts
2166
+ var COMPARATOR_KINDS = ["current", "next", "predicted", "lastSettled", "accruing"];
2167
+ function toHourly(rate, rateIntervalHours) {
2168
+ if (rate === null || rateIntervalHours === null || !(rateIntervalHours > 0)) return null;
2169
+ const n = Number(rate);
2170
+ return Number.isFinite(n) ? n / rateIntervalHours : null;
2171
+ }
2172
+ function toApr(rate, rateIntervalHours) {
2173
+ const h = toHourly(rate, rateIntervalHours);
2174
+ return h === null ? null : h * 24 * 365;
2175
+ }
2176
+ function pick(u, kind, predictedPublished) {
2177
+ const f = u.funding;
2178
+ const predicted = () => {
2179
+ if (f.rateKind === "predicted") return { rate: f.rate, rateKind: "predicted", field: "rate", unavailable: false };
2180
+ return predictedPublished ? { rate: f.predictedRate, rateKind: "predicted", field: "predictedRate", unavailable: false } : { rate: null, rateKind: null, field: "predictedRate", unavailable: true };
2181
+ };
2182
+ switch (kind) {
2183
+ case "current":
2184
+ return { rate: f.rate, rateKind: f.rateKind, field: "rate", unavailable: false };
2185
+ case "predicted":
2186
+ return predicted();
2187
+ case "lastSettled":
2188
+ case "accruing":
2189
+ return f.rateKind === kind ? { rate: f.rate, rateKind: kind, field: "rate", unavailable: false } : { rate: null, rateKind: null, field: "rate", unavailable: true };
2190
+ case "next":
2191
+ if (f.rateKind === "accruing") return { rate: f.rate, rateKind: "accruing", field: "rate", unavailable: false };
2192
+ return predicted();
2193
+ }
2194
+ }
2195
+ function checkOptions(opts) {
2196
+ const bad = (message, remediation) => {
2197
+ throw new WsError("INVALID_PARAMS", `createFundingComparator: ${message}`, { remediation });
2198
+ };
2199
+ if (opts === null || typeof opts !== "object") bad("options must be an object", "Pass { streams: [...] }");
2200
+ if (!Array.isArray(opts.streams) || opts.streams.some((s) => s === null || typeof s !== "object" || typeof s.subscribe !== "function" || typeof s.markets !== "function")) {
2201
+ bad("streams must be an array of VenueStream", "Pass streams created with createVenueStream()");
2202
+ }
2203
+ if (opts.kind !== void 0 && !COMPARATOR_KINDS.includes(opts.kind)) {
2204
+ bad(`unknown kind ${JSON.stringify(opts.kind)}`, `Use kind ${COMPARATOR_KINDS.map((k) => `"${k}"`).join(" / ")}`);
2205
+ }
2206
+ if (opts.minVenues !== void 0 && !(Number.isInteger(opts.minVenues) && opts.minVenues >= 1)) {
2207
+ bad(`minVenues must be a positive integer (got ${String(opts.minVenues)})`, "Pass minVenues \u2265 1, or omit it (default 2)");
2208
+ }
2209
+ if (opts.include !== void 0 && !(Array.isArray(opts.include) && opts.include.every((c) => typeof c === "string"))) {
2210
+ bad("include must be an array of canonical symbols", 'Pass e.g. include: ["BTC", "ETH"], or omit it');
2211
+ }
2212
+ if (opts.staleAfterMs !== void 0 && !(typeof opts.staleAfterMs === "number" && Number.isFinite(opts.staleAfterMs) && opts.staleAfterMs > 0)) {
2213
+ bad(`staleAfterMs must be a positive number (got ${String(opts.staleAfterMs)})`, "Pass milliseconds > 0, or omit it (venue default)");
2214
+ }
2215
+ for (const k of ["canonicalize", "onControl", "clock"]) {
2216
+ if (opts[k] !== void 0 && typeof opts[k] !== "function") bad(`${k} must be a function`, `Pass a function or omit ${k}`);
2217
+ }
2218
+ }
2219
+ function createFundingComparator(opts) {
2220
+ checkOptions(opts);
2221
+ const kind = opts.kind === void 0 ? "current" : opts.kind;
2222
+ const minVenues = opts.minVenues === void 0 ? 2 : opts.minVenues;
2223
+ const include = opts.include === void 0 ? null : new Set(opts.include);
2224
+ const clock = opts.clock === void 0 ? () => Date.now() : opts.clock;
2225
+ const latest = /* @__PURE__ */ new Map();
2226
+ const dirs = /* @__PURE__ */ new Map();
2227
+ const listeners = /* @__PURE__ */ new Set();
2228
+ const subs = [];
2229
+ const pending = /* @__PURE__ */ new Set();
2230
+ let scheduled = false;
2231
+ let closed = false;
2232
+ function canonicalOf(stream, u) {
2233
+ if (opts.canonicalize === void 0) return u.canonical;
2234
+ const e = dirs.get(stream)?.get(u.symbol);
2235
+ if (e === void 0) return null;
2236
+ return opts.canonicalize(u.venue, { venueSymbol: u.venueSymbol, dex: u.dex, base: e.base, quote: u.quote });
2237
+ }
2238
+ function threshold(stream) {
2239
+ if (opts.staleAfterMs !== void 0) return opts.staleAfterMs;
2240
+ return (stream.venue === "arcus" ? 3 : 2) * stream.staleAfterMs;
2241
+ }
2242
+ function leg(stream, u, now) {
2243
+ const p = pick(u, kind, stream.capabilities.fields.predictedRate);
2244
+ const at = u.asOf[p.field];
2245
+ const asOf = at === void 0 ? null : at;
2246
+ const st = VENUE_STATIC[u.venue];
2247
+ return {
2248
+ venue: u.venue,
2249
+ dex: u.dex,
2250
+ symbol: u.symbol,
2251
+ quote: u.quote,
2252
+ mark: u.mark,
2253
+ rate: p.rate,
2254
+ rateKind: p.rateKind,
2255
+ kindUnavailable: p.unavailable,
2256
+ funding: u.funding,
2257
+ fundingVerified: st === void 0 ? false : st.fundingVerified,
2258
+ asOf,
2259
+ stale: stream.state.status !== "live" || asOf === null || now - asOf > threshold(stream)
2260
+ };
2261
+ }
2262
+ function rows() {
2263
+ const now = clock();
2264
+ const groups = /* @__PURE__ */ new Map();
2265
+ for (const { stream, u } of latest.values()) {
2266
+ const c = canonicalOf(stream, u);
2267
+ if (c === null || include !== null && !include.has(c)) continue;
2268
+ let g = groups.get(c);
2269
+ if (g === void 0) {
2270
+ g = [];
2271
+ groups.set(c, g);
2272
+ }
2273
+ g.push(leg(stream, u, now));
2274
+ }
2275
+ const out = [];
2276
+ for (const [canonical, legs] of groups) {
2277
+ const withRate = legs.filter((l) => l.rate !== null);
2278
+ if (new Set(withRate.map((l) => l.venue)).size < minVenues) continue;
2279
+ const kinds = new Set(withRate.map((l) => l.rateKind));
2280
+ legs.sort((a, b) => a.venue < b.venue ? -1 : a.venue > b.venue ? 1 : a.symbol < b.symbol ? -1 : 1);
2281
+ out.push({ canonical, mixedKinds: kinds.size > 1, legs });
2282
+ }
2283
+ out.sort((a, b) => a.canonical < b.canonical ? -1 : a.canonical > b.canonical ? 1 : 0);
2284
+ return out;
2285
+ }
2286
+ function schedule(changed) {
2287
+ pending.add(changed);
2288
+ if (scheduled || listeners.size === 0) return;
2289
+ scheduled = true;
2290
+ Promise.resolve().then(() => {
2291
+ scheduled = false;
2292
+ if (closed) return;
2293
+ const ch = Array.from(pending);
2294
+ pending.clear();
2295
+ const r = rows();
2296
+ for (const l of listeners) {
2297
+ try {
2298
+ l(r, ch);
2299
+ } catch (e) {
2300
+ const message = `funding comparator listener threw: ${e instanceof Error ? e.message : String(e)}`;
2301
+ if (opts.onControl !== void 0) {
2302
+ try {
2303
+ opts.onControl({ type: "listener-error", message });
2304
+ } catch {
2305
+ }
2306
+ }
2307
+ }
2308
+ }
2309
+ });
2310
+ }
2311
+ for (const stream of opts.streams) {
2312
+ stream.markets().then(
2313
+ (entries) => {
2314
+ dirs.set(stream, new Map(entries.map((e) => [e.symbol, e])));
2315
+ },
2316
+ () => void 0
2317
+ // the subscription below reports the same failure as an error control
2318
+ );
2319
+ const sub = stream.subscribe({ kind: "allMarkets" });
2320
+ subs.push(sub);
2321
+ sub.on((e) => {
2322
+ if (e.type === "market") {
2323
+ latest.set(`${e.venue}|${e.symbol}`, { stream, u: e });
2324
+ const c = canonicalOf(stream, e);
2325
+ if (c !== null) schedule(c);
2326
+ return;
2327
+ }
2328
+ if (opts.onControl !== void 0) opts.onControl(e);
2329
+ if (e.type === "state") {
2330
+ for (const { stream: s, u } of latest.values()) {
2331
+ if (s !== stream) continue;
2332
+ const c = canonicalOf(s, u);
2333
+ if (c !== null) schedule(c);
2334
+ }
2335
+ }
2336
+ });
2337
+ }
2338
+ return {
2339
+ rows,
2340
+ subscribe(cb) {
2341
+ listeners.add(cb);
2342
+ return () => {
2343
+ listeners.delete(cb);
2344
+ };
2345
+ },
2346
+ close() {
2347
+ if (closed) return;
2348
+ closed = true;
2349
+ listeners.clear();
2350
+ for (const s of subs) void s.unsubscribe();
2351
+ }
2352
+ };
2353
+ }
2354
+
2355
+ // src/ws/directory.ts
2356
+ async function fetchJson(fetch, venue, url, init) {
2357
+ let res;
2358
+ try {
2359
+ res = await fetch(url, init);
2360
+ } catch (e) {
2361
+ throw new WsError("EXCHANGE_UNREACHABLE", `${venue} REST ${pathOf(url)} unreachable: ${e instanceof Error ? e.message : String(e)}`, {
2362
+ exchange: venue,
2363
+ remediation: "Check network access to the venue REST host (and CORS in a browser), then subscribe again"
2364
+ });
2365
+ }
2366
+ if (!res.ok) {
2367
+ throw new WsError(res.status === 429 ? "RATE_LIMITED" : "EXCHANGE_ERROR", `${venue} REST ${pathOf(url)} returned HTTP ${res.status}`, {
2368
+ exchange: venue,
2369
+ remediation: res.status === 429 ? "Wait and subscribe again (REST rate limit)" : "Retry later; the venue REST endpoint failed"
2370
+ });
2371
+ }
2372
+ try {
2373
+ return await res.json();
2374
+ } catch {
2375
+ throw new WsError("EXCHANGE_ERROR", `${venue} REST ${pathOf(url)} returned a non-JSON body`, { exchange: venue });
2376
+ }
2377
+ }
2378
+ function badShape(venue, what) {
2379
+ return new WsError("EXCHANGE_ERROR", `${venue} ${what}`, {
2380
+ exchange: venue,
2381
+ remediation: "The venue response shape changed; report it (perp-cli/ws cannot build the market directory)"
2382
+ });
2383
+ }
2384
+ function pathOf(url) {
2385
+ const m = /^[a-z]+:\/\/[^/]+(\/[^?#]*)?/i.exec(url);
2386
+ return m !== null && m[1] !== void 0 ? m[1] : url;
2387
+ }
2388
+
2389
+ // src/ws/dec.ts
2390
+ var DECIMAL_RE = /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/;
2391
+ function toDec(v) {
2392
+ if (v === null || v === void 0) return null;
2393
+ if (typeof v === "string") {
2394
+ const m = DECIMAL_RE.exec(v);
2395
+ return m !== null && (m[2] !== "" || m[3] !== void 0 && m[3] !== "") ? v : void 0;
2396
+ }
2397
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : void 0;
2398
+ return void 0;
2399
+ }
2400
+ function shiftDecimal(s, places) {
2401
+ const m = DECIMAL_RE.exec(s);
2402
+ if (m === null || m[2] === "" && (m[3] === void 0 || m[3] === "")) return void 0;
2403
+ const sign = m[1] === "-" ? "-" : "";
2404
+ const intPart = m[2];
2405
+ const fracPart = m[3] === void 0 ? "" : m[3];
2406
+ const exp = (m[4] === void 0 ? 0 : parseInt(m[4], 10)) + places;
2407
+ let digits = intPart + fracPart;
2408
+ let point = intPart.length + exp;
2409
+ if (point <= 0) {
2410
+ digits = "0".repeat(1 - point) + digits;
2411
+ point = 1;
2412
+ }
2413
+ if (point > digits.length) digits = digits + "0".repeat(point - digits.length);
2414
+ let int = digits.slice(0, point).replace(/^0+(?=\d)/, "");
2415
+ const frac = digits.slice(point).replace(/0+$/, "");
2416
+ if (int === "") int = "0";
2417
+ const out = frac === "" ? int : `${int}.${frac}`;
2418
+ return /^0(\.0*)?$/.test(out) ? "0" : sign + out;
2419
+ }
2420
+ function mulDecimalInt(s, n, shift) {
2421
+ if (!Number.isInteger(n)) return void 0;
2422
+ const m = DECIMAL_RE.exec(s);
2423
+ if (m === null || m[4] !== void 0 || m[2] === "" && (m[3] === void 0 || m[3] === "")) return void 0;
2424
+ const frac = m[3] === void 0 ? "" : m[3];
2425
+ const digits = `${m[2]}${frac}`.replace(/^0+(?=\d)/, "");
2426
+ const product = BigInt(digits === "" ? "0" : digits) * BigInt(n) * (m[1] === "-" ? -1n : 1n);
2427
+ const neg = product < 0n;
2428
+ const abs2 = (neg ? -product : product).toString();
2429
+ return shiftDecimal(`${neg ? "-" : ""}${abs2}`, shift - frac.length);
2430
+ }
2431
+
2432
+ // src/ws/codecs/common.ts
2433
+ function isObj(v) {
2434
+ return v !== null && typeof v === "object" && !Array.isArray(v);
2435
+ }
2436
+ function parseFrame(text) {
2437
+ try {
2438
+ return { ok: true, v: JSON.parse(text) };
2439
+ } catch {
2440
+ return { ok: false, ev: protocolError("Frame is not valid JSON") };
2441
+ }
2442
+ }
2443
+ function protocolError(message, wire = null) {
2444
+ return { t: "error", code: "WS_PROTOCOL_ERROR", message, wire, fatal: false, remediation: null };
2445
+ }
2446
+ function venueError(code, message, wire, fatal, remediation) {
2447
+ return { t: "error", code, message, wire, fatal, remediation };
2448
+ }
2449
+ function unrecognised(venue) {
2450
+ return protocolError(`Unrecognised ${venue} frame`);
2451
+ }
2452
+ function readDec(o, key, bad) {
2453
+ if (!(key in o)) return void 0;
2454
+ const d = toDec(o[key]);
2455
+ if (d === void 0) bad.push(key);
2456
+ return d;
2457
+ }
2458
+ function readTs(o, key, bad, scale = 1) {
2459
+ if (!(key in o)) return void 0;
2460
+ const v = o[key];
2461
+ if (v === null) return null;
2462
+ if (typeof v === "number" && Number.isFinite(v)) return v * scale;
2463
+ bad.push(key);
2464
+ return void 0;
2465
+ }
2466
+ function compact(o) {
2467
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
2468
+ return o;
2469
+ }
2470
+ function badFieldsError(venue, wire, bad) {
2471
+ const list = bad.slice(0, 5).map((b) => `${b.symbol}: ${b.fields.join("/")}`).join("; ");
2472
+ return protocolError(`${venue} ${wire}: unparseable field value(s) left unchanged (${list}${bad.length > 5 ? "; \u2026" : ""})`, wire);
2473
+ }
2474
+ function level(o, bad, name) {
2475
+ if (o === null || o === void 0) return [null, null];
2476
+ if (!isObj(o)) {
2477
+ bad.push(name);
2478
+ return [null, null];
2479
+ }
2480
+ const p = readDec(o, "price", bad);
2481
+ const s = readDec(o, "size", bad);
2482
+ return [p === void 0 ? null : p, s === void 0 ? null : s];
2483
+ }
2484
+ function pick2(m, v) {
2485
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(m, v) ? m[v] : null;
2486
+ }
2487
+ function idOf(v) {
2488
+ if (typeof v === "string" && v !== "") return v;
2489
+ if (typeof v === "number" && Number.isFinite(v)) return String(v);
2490
+ return null;
2491
+ }
2492
+ function dec(o, key, bad) {
2493
+ const v = readDec(o, key, bad);
2494
+ return v === void 0 ? null : v;
2495
+ }
2496
+ function ts(o, key, bad, scale = 1) {
2497
+ const v = readTs(o, key, bad, scale);
2498
+ return v === void 0 || v === null ? null : Math.floor(v);
2499
+ }
2500
+ function nextHour(ts2) {
2501
+ return Math.floor(ts2 / 36e5) * 36e5 + 36e5;
2502
+ }
2503
+ function capabilities(p) {
2504
+ return {
2505
+ browser: true,
2506
+ // Phase 0: no WS server checks Origin; REST CORS is `*`
2507
+ networks: { mainnet: true, testnet: p.testnet },
2508
+ channels: { allMarkets: true, market: true, bbo: true, book: false, trades: false, user: p.user },
2509
+ fields: p.fields,
2510
+ notes: {
2511
+ book: "Not in this release (0.15.1)",
2512
+ trades: "Not in this release (0.15.1)",
2513
+ ...p.user ? {} : { user: "Not in this release (0.15.1)" },
2514
+ ...p.notes
2515
+ }
2516
+ };
2517
+ }
2518
+
2519
+ // src/ws/codecs/hyperliquid.ts
2520
+ var S = VENUE_STATIC.hyperliquid;
2521
+ var ALL = "allDexsAssetCtxs";
2522
+ var PER_COIN_STALE_MS = 1e4;
2523
+ function data(dir) {
2524
+ return dir.data;
2525
+ }
2526
+ function universeOf(v, dex) {
2527
+ if (!isObj(v) || !Array.isArray(v.universe)) throw badShape("hyperliquid", `meta${dex ? `{dex:${dex}}` : ""} has no universe array`);
2528
+ const out = v.universe.map((u) => {
2529
+ if (!isObj(u) || typeof u.name !== "string") throw badShape("hyperliquid", `meta${dex ? `{dex:${dex}}` : ""} universe entry has no name`);
2530
+ return { name: u.name, delisted: u.isDelisted === true };
2531
+ });
2532
+ return Object.assign(out, { collateralToken: v.collateralToken });
2533
+ }
2534
+ function patchFromCtx(symbol, ctx, recvTs, bad) {
2535
+ return compact({
2536
+ symbol,
2537
+ venueTs: null,
2538
+ // ctx frames carry no timestamp
2539
+ mark: readDec(ctx, "markPx", bad),
2540
+ index: readDec(ctx, "oraclePx", bad),
2541
+ rate: readDec(ctx, "funding", bad),
2542
+ openInterest: readDec(ctx, "openInterest", bad),
2543
+ volume24hNotional: readDec(ctx, "dayNtlVlm", bad),
2544
+ nextFundingTime: nextHour(recvTs)
2545
+ // hourly settlement (venue-definition)
2546
+ });
2547
+ }
2548
+ function subscriptionKey(sub) {
2549
+ if (!isObj(sub)) return null;
2550
+ if (sub.type === ALL) return ALL;
2551
+ if (sub.type === "activeAssetCtx" && typeof sub.coin === "string") return `activeAssetCtx:${sub.coin}`;
2552
+ if (sub.type === "bbo" && typeof sub.coin === "string") return `bbo:${sub.coin}`;
2553
+ if (sub.type === "clearinghouseState" || sub.type === "openOrders") return `${sub.type}:${typeof sub.dex === "string" ? sub.dex : ""}`;
2554
+ if (sub.type === "orderUpdates" || sub.type === "userFills") return sub.type;
2555
+ return null;
2556
+ }
2557
+ var hyperliquid = {
2558
+ venue: "hyperliquid",
2559
+ capabilities: capabilities({
2560
+ testnet: true,
2561
+ user: true,
2562
+ fields: {
2563
+ mark: true,
2564
+ index: true,
2565
+ bestBid: false,
2566
+ bestAsk: false,
2567
+ last: false,
2568
+ predictedRate: false,
2569
+ nextFundingTime: true,
2570
+ fundingInterval: true,
2571
+ openInterest: true,
2572
+ volume24hNotional: true
2573
+ },
2574
+ notes: {
2575
+ bestBid: "Not in asset ctxs; use the bbo channel (Phase 2)",
2576
+ bestAsk: "Not in asset ctxs; use the bbo channel (Phase 2)",
2577
+ last: "Not in asset ctxs",
2578
+ predictedRate: "HL publishes no predicted rate; `rate` (ctx.funding) is the accruing current-hour rate",
2579
+ nextFundingTime: "Hourly settlement rule (venue-definition)",
2580
+ openInterest: "Base units, one side",
2581
+ user: "By address, no auth. clearinghouseState/openOrders per selected dex (opts.hip3Dexes); userFills is replayable (deduped by tid)",
2582
+ clientId: "orders: cloid (null when the order has none). fills: cloid only when the frame carries it (WsFill documents none) \u2014 else null; join OrderRow.clientId by orderId",
2583
+ liquidationPrice: "liquidationPx (null when the venue sends null, e.g. no liquidation risk)",
2584
+ fee: "fee in feeToken (negative = rebate)",
2585
+ bbo: "bbo{coin}: full top of book per frame, no sequence"
2586
+ }
2587
+ }),
2588
+ url: (n) => S.wsUrl[n],
2589
+ restUrl: (n) => S.restUrl[n],
2590
+ secretKeys: [],
2591
+ secretUrlPatterns: [],
2592
+ ping: S.ping,
2593
+ limits: S.limits,
2594
+ closesOnBadSubscribe: true,
2595
+ staleAfterMs: S.staleAfterMs,
2596
+ openInterest: { unit: "base", sides: "one" },
2597
+ refresh: null,
2598
+ async loadDirectory(ctx) {
2599
+ const info = (body) => fetchJson(ctx.fetch, "hyperliquid", `${ctx.restUrl}/info`, {
2600
+ method: "POST",
2601
+ headers: { "Content-Type": "application/json" },
2602
+ body: JSON.stringify(body)
2603
+ });
2604
+ let hip3 = [];
2605
+ if (ctx.hip3Dexes === "all" || ctx.hip3Dexes.length > 0) {
2606
+ const raw = await info({ type: "perpDexs" });
2607
+ if (!Array.isArray(raw)) throw badShape("hyperliquid", "perpDexs is not an array");
2608
+ const names = raw.filter(isObj).map((d) => d.name).filter((n) => typeof n === "string");
2609
+ if (ctx.hip3Dexes === "all") hip3 = names;
2610
+ else {
2611
+ const unknown = ctx.hip3Dexes.filter((d) => !names.includes(d));
2612
+ if (unknown.length > 0) {
2613
+ throw new WsError("INVALID_PARAMS", `hyperliquid: unknown HIP-3 dex(es) ${unknown.join(", ")}`, {
2614
+ exchange: "hyperliquid",
2615
+ remediation: `Known dexes: ${names.join(", ")}`
2616
+ });
2617
+ }
2618
+ hip3 = ctx.hip3Dexes.slice();
2619
+ }
2620
+ }
2621
+ const [spot, ...metas] = await Promise.all([
2622
+ info({ type: "spotMeta" }),
2623
+ info({ type: "meta" }),
2624
+ ...hip3.map((dex) => info({ type: "meta", dex }))
2625
+ ]);
2626
+ const tokens = /* @__PURE__ */ new Map();
2627
+ if (isObj(spot) && Array.isArray(spot.tokens)) {
2628
+ for (const t of spot.tokens) if (isObj(t) && typeof t.index === "number" && typeof t.name === "string") tokens.set(t.index, t.name);
2629
+ }
2630
+ const dexes = /* @__PURE__ */ new Map();
2631
+ const entries = [];
2632
+ ["", ...hip3].forEach((dex, i) => {
2633
+ const uni = universeOf(metas[i], dex);
2634
+ const ct = uni.collateralToken;
2635
+ const quoteName = typeof ct === "number" ? tokens.get(ct) : void 0;
2636
+ const quote = quoteName === void 0 ? null : quoteName;
2637
+ dexes.set(dex, { names: uni.map((u) => u.delisted ? null : u.name) });
2638
+ for (const u of uni) {
2639
+ if (u.delisted) continue;
2640
+ const base = dex === "" ? u.name : u.name.slice(u.name.indexOf(":") + 1);
2641
+ const d = dex === "" ? null : dex;
2642
+ entries.push({
2643
+ venue: "hyperliquid",
2644
+ dex: d,
2645
+ symbol: u.name,
2646
+ venueSymbol: u.name,
2647
+ base,
2648
+ quote,
2649
+ canonical: ctx.canonicalize("hyperliquid", { venueSymbol: u.name, dex: d, base, quote }),
2650
+ rateIntervalHours: 1,
2651
+ settlementIntervalHours: 1,
2652
+ source: "rest:/info"
2653
+ });
2654
+ }
2655
+ });
2656
+ return { entries, data: { dexes } };
2657
+ },
2658
+ fundingMeta: () => ({
2659
+ rateKind: "accruing",
2660
+ rateSource: "ws",
2661
+ rateIntervalHours: 1,
2662
+ settlementIntervalHours: 1,
2663
+ intervalSource: "venue-definition",
2664
+ intervalAsOf: null,
2665
+ marketClosed: null
2666
+ }),
2667
+ wiresFor(target, _dir, staleOverrideMs) {
2668
+ if (target.kind === "allMarkets") {
2669
+ return [{ key: ALL, staleAfterMs: staleOverrideMs === null ? S.staleAfterMs : staleOverrideMs, params: { type: ALL } }];
2670
+ }
2671
+ const out = [];
2672
+ if (target.channels.includes("market")) {
2673
+ out.push({
2674
+ key: `activeAssetCtx:${target.symbol}`,
2675
+ staleAfterMs: staleOverrideMs === null ? PER_COIN_STALE_MS : staleOverrideMs,
2676
+ params: { type: "activeAssetCtx", coin: target.symbol }
2677
+ });
2678
+ }
2679
+ if (target.channels.includes("bbo")) out.push({ key: `bbo:${target.symbol}`, staleAfterMs: null, params: { type: "bbo", coin: target.symbol } });
2680
+ return out;
2681
+ },
2682
+ user: {
2683
+ inPageSecrets: false,
2684
+ wires(ref, channels, dir) {
2685
+ if (ref.venue !== "hyperliquid") return [];
2686
+ const user = ref.address;
2687
+ const dexes = Array.from(data(dir).dexes.keys());
2688
+ const dexParam = (dex) => dex === "" ? {} : { dex };
2689
+ const out = [];
2690
+ if (channels.includes("account") || channels.includes("positions")) {
2691
+ for (const dex of dexes) out.push({ key: `clearinghouseState:${dex}`, staleAfterMs: null, params: { type: "clearinghouseState", user, ...dexParam(dex) } });
2692
+ }
2693
+ if (channels.includes("orders")) {
2694
+ for (const dex of dexes) out.push({ key: `openOrders:${dex}`, staleAfterMs: null, params: { type: "openOrders", user, ...dexParam(dex) } });
2695
+ out.push({ key: "orderUpdates", staleAfterMs: null, params: { type: "orderUpdates", user } });
2696
+ }
2697
+ if (channels.includes("fills")) out.push({ key: "userFills", staleAfterMs: null, params: { type: "userFills", user } });
2698
+ return out;
2699
+ }
2700
+ },
2701
+ encodeSubscribe: (w) => JSON.stringify({ method: "subscribe", subscription: w.params }),
2702
+ encodeUnsubscribe: (w) => JSON.stringify({ method: "unsubscribe", subscription: w.params }),
2703
+ decode(text, dir, recvTs) {
2704
+ const p = parseFrame(text);
2705
+ if (!p.ok) return [p.ev];
2706
+ const f = p.v;
2707
+ if (!isObj(f) || typeof f.channel !== "string") return [unrecognised("hyperliquid")];
2708
+ switch (f.channel) {
2709
+ case "pong":
2710
+ return [{ t: "pong" }];
2711
+ case "subscriptionResponse": {
2712
+ if (!isObj(f.data)) return [unrecognised("hyperliquid")];
2713
+ return [{ t: "ack", wire: subscriptionKey(f.data.subscription), reqId: null, unsubscribe: f.data.method === "unsubscribe" }];
2714
+ }
2715
+ case "error": {
2716
+ const text2 = typeof f.data === "string" ? f.data : JSON.stringify(f.data);
2717
+ const wire = rejectedWire(text2);
2718
+ return [{ t: "error", code: "WS_PROTOCOL_ERROR", message: `hyperliquid error: ${text2}`, wire, fatal: wire !== null, remediation: null }];
2719
+ }
2720
+ case ALL:
2721
+ return decodeAll(f, data(dir), recvTs);
2722
+ case "clearinghouseState":
2723
+ return decodeClearinghouse(f.data);
2724
+ case "openOrders":
2725
+ return decodeOpenOrders(f.data);
2726
+ case "orderUpdates":
2727
+ return decodeOrderUpdates(f.data, data(dir));
2728
+ case "userFills":
2729
+ return decodeFills(f.data);
2730
+ case "bbo":
2731
+ return decodeBbo(f.data);
2732
+ case "activeAssetCtx": {
2733
+ if (!isObj(f.data) || typeof f.data.coin !== "string" || !isObj(f.data.ctx)) return [protocolError("hyperliquid activeAssetCtx frame without coin/ctx")];
2734
+ const wire = `activeAssetCtx:${f.data.coin}`;
2735
+ const bad = [];
2736
+ const out = [{ t: "market", wire, patches: [patchFromCtx(f.data.coin, f.data.ctx, recvTs, bad)] }];
2737
+ if (bad.length > 0) out.push(badFieldsError("hyperliquid", wire, [{ symbol: f.data.coin, fields: bad }]));
2738
+ return out;
2739
+ }
2740
+ default:
2741
+ return [unrecognised("hyperliquid")];
2742
+ }
2743
+ }
2744
+ };
2745
+ function dexOf(coin) {
2746
+ const i = coin.indexOf(":");
2747
+ return i < 0 ? null : coin.slice(0, i);
2748
+ }
2749
+ var SIDE = { B: "buy", A: "sell" };
2750
+ function abs(d) {
2751
+ return d === null ? null : d.replace(/^-/, "");
2752
+ }
2753
+ function decodeClearinghouse(d) {
2754
+ if (!isObj(d) || typeof d.dex !== "string" || !isObj(d.clearinghouseState)) return [protocolError("hyperliquid clearinghouseState frame without dex/clearinghouseState")];
2755
+ const wire = `clearinghouseState:${d.dex}`;
2756
+ const cs = d.clearinghouseState;
2757
+ const bad = [];
2758
+ const ms = isObj(cs.marginSummary) ? cs.marginSummary : null;
2759
+ if (ms === null) bad.push("marginSummary");
2760
+ const venueTs = ts(cs, "time", bad);
2761
+ const rows = [];
2762
+ if (!Array.isArray(cs.assetPositions)) bad.push("assetPositions");
2763
+ else for (const ap of cs.assetPositions) {
2764
+ const p = isObj(ap) && isObj(ap.position) ? ap.position : null;
2765
+ if (p === null || typeof p.coin !== "string") {
2766
+ bad.push("assetPositions[]");
2767
+ continue;
2768
+ }
2769
+ const szi = dec(p, "szi", bad);
2770
+ const lev = isObj(p.leverage) ? dec(p.leverage, "value", bad) : null;
2771
+ rows.push({
2772
+ symbol: p.coin,
2773
+ dex: dexOf(p.coin),
2774
+ side: szi === null ? null : szi.startsWith("-") ? "short" : "long",
2775
+ size: abs(szi),
2776
+ entryPrice: dec(p, "entryPx", bad),
2777
+ markPrice: null,
2778
+ positionValue: dec(p, "positionValue", bad),
2779
+ unrealizedPnl: dec(p, "unrealizedPnl", bad),
2780
+ liquidationPrice: dec(p, "liquidationPx", bad),
2781
+ marginUsed: dec(p, "marginUsed", bad),
2782
+ leverage: lev,
2783
+ venueTs
2784
+ });
2785
+ }
2786
+ const deltas = [
2787
+ {
2788
+ kind: "account",
2789
+ snapshot: true,
2790
+ dex: d.dex === "" ? null : d.dex,
2791
+ venueTs,
2792
+ equity: ms === null ? null : dec(ms, "accountValue", bad),
2793
+ available: dec(cs, "withdrawable", bad),
2794
+ marginUsed: ms === null ? null : dec(ms, "totalMarginUsed", bad),
2795
+ unrealizedPnl: null
2796
+ // not in clearinghouseState (per-position only)
2797
+ },
2798
+ { kind: "positions", snapshot: true, scope: d.dex, rows, remove: [] }
2799
+ ];
2800
+ const out = [{ t: "user", wire, deltas }];
2801
+ if (bad.length > 0) out.push(badFieldsError("hyperliquid", wire, [{ symbol: "account", fields: bad }]));
2802
+ return out;
2803
+ }
2804
+ function orderRow(o, bad, status) {
2805
+ const id = idOf(o.oid);
2806
+ if (id === null || typeof o.coin !== "string") {
2807
+ bad.push("oid/coin");
2808
+ return null;
2809
+ }
2810
+ const sz = dec(o, "sz", bad);
2811
+ const orig = dec(o, "origSz", bad);
2812
+ return {
2813
+ orderId: id,
2814
+ clientId: typeof o.cloid === "string" ? o.cloid : null,
2815
+ symbol: o.coin,
2816
+ dex: dexOf(o.coin),
2817
+ side: pick2(SIDE, o.side),
2818
+ type: typeof o.orderType === "string" ? o.orderType : null,
2819
+ status,
2820
+ price: dec(o, "limitPx", bad),
2821
+ size: orig,
2822
+ filled: null,
2823
+ remaining: sz,
2824
+ reduceOnly: typeof o.reduceOnly === "boolean" ? o.reduceOnly : null,
2825
+ createdAt: ts(o, "timestamp", bad),
2826
+ updatedAt: null
2827
+ };
2828
+ }
2829
+ function decodeOpenOrders(d) {
2830
+ if (!isObj(d) || typeof d.dex !== "string" || !Array.isArray(d.orders)) return [protocolError("hyperliquid openOrders frame without dex/orders")];
2831
+ const wire = `openOrders:${d.dex}`;
2832
+ const bad = [];
2833
+ const rows = d.orders.map((o) => isObj(o) ? orderRow(o, bad, "open") : null).filter((r) => r !== null);
2834
+ const out = [{ t: "user", wire, deltas: [{ kind: "orders", snapshot: true, scope: d.dex, rows, remove: [] }] }];
2835
+ if (bad.length > 0) out.push(badFieldsError("hyperliquid", wire, [{ symbol: "orders", fields: bad }]));
2836
+ return out;
2837
+ }
2838
+ function decodeOrderUpdates(d, hl) {
2839
+ if (!Array.isArray(d)) return [protocolError("hyperliquid orderUpdates data is not an array", "orderUpdates")];
2840
+ const bad = [];
2841
+ const byScope = /* @__PURE__ */ new Map();
2842
+ for (const u of d) {
2843
+ if (!isObj(u) || !isObj(u.order) || typeof u.status !== "string") {
2844
+ bad.push("update");
2845
+ continue;
2846
+ }
2847
+ const row = orderRow(u.order, bad, u.status);
2848
+ if (row === null) continue;
2849
+ const scope = row.dex === null ? "" : row.dex;
2850
+ if (!hl.dexes.has(scope)) continue;
2851
+ let g = byScope.get(scope);
2852
+ if (g === void 0) {
2853
+ g = { rows: [], remove: [] };
2854
+ byScope.set(scope, g);
2855
+ }
2856
+ if (u.status === "open") g.rows.push({ ...row, updatedAt: ts(u, "statusTimestamp", bad) });
2857
+ else g.remove.push(row.orderId);
2858
+ }
2859
+ const deltas = Array.from(byScope, ([scope, g]) => ({ kind: "orders", snapshot: false, scope, rows: g.rows, remove: g.remove }));
2860
+ const out = [{ t: "user", wire: "orderUpdates", deltas }];
2861
+ if (bad.length > 0) out.push(badFieldsError("hyperliquid", "orderUpdates", [{ symbol: "orders", fields: bad }]));
2862
+ return out;
2863
+ }
2864
+ function decodeFills(d) {
2865
+ if (!isObj(d) || !Array.isArray(d.fills)) return [protocolError("hyperliquid userFills frame without fills", "userFills")];
2866
+ const bad = [];
2867
+ const fills = [];
2868
+ for (const f of d.fills) {
2869
+ const id = isObj(f) ? idOf(f.tid) : null;
2870
+ if (!isObj(f) || id === null || typeof f.coin !== "string") {
2871
+ bad.push("tid/coin");
2872
+ continue;
2873
+ }
2874
+ fills.push({
2875
+ // WsFill documents no cloid; read it when the frame carries one (join OrderRow.clientId by orderId otherwise).
2876
+ fillId: id,
2877
+ clientId: typeof f.cloid === "string" ? f.cloid : null,
2878
+ symbol: f.coin,
2879
+ dex: dexOf(f.coin),
2880
+ side: pick2(SIDE, f.side),
2881
+ price: dec(f, "px", bad),
2882
+ size: dec(f, "sz", bad),
2883
+ fee: dec(f, "fee", bad),
2884
+ feeToken: typeof f.feeToken === "string" ? f.feeToken : null,
2885
+ closedPnl: dec(f, "closedPnl", bad),
2886
+ orderId: idOf(f.oid),
2887
+ liquidity: typeof f.crossed === "boolean" ? f.crossed ? "taker" : "maker" : null,
2888
+ time: ts(f, "time", bad)
2889
+ });
2890
+ }
2891
+ const out = [{ t: "user", wire: "userFills", deltas: [{ kind: "fills", snapshot: d.isSnapshot === true, fills }] }];
2892
+ if (bad.length > 0) out.push(badFieldsError("hyperliquid", "userFills", [{ symbol: "fills", fields: bad }]));
2893
+ return out;
2894
+ }
2895
+ function decodeBbo(d) {
2896
+ if (!isObj(d) || typeof d.coin !== "string" || !Array.isArray(d.bbo) || d.bbo.length !== 2) return [protocolError("hyperliquid bbo frame without coin/bbo[2]")];
2897
+ const wire = `bbo:${d.coin}`;
2898
+ const bad = [];
2899
+ const side = (l) => {
2900
+ if (l === null) return [null, null];
2901
+ if (!isObj(l)) {
2902
+ bad.push("level");
2903
+ return [null, null];
2904
+ }
2905
+ return [dec(l, "px", bad), dec(l, "sz", bad)];
2906
+ };
2907
+ const [bid, bidSize] = side(d.bbo[0]);
2908
+ const [ask, askSize] = side(d.bbo[1]);
2909
+ const out = [{ t: "bbo", wire, symbol: d.coin, seq: null, bid, bidSize, ask, askSize, venueTs: ts(d, "time", bad) }];
2910
+ if (bad.length > 0) out.push(badFieldsError("hyperliquid", wire, [{ symbol: d.coin, fields: bad }]));
2911
+ return out;
2912
+ }
2913
+ function rejectedWire(text) {
2914
+ const at = text.indexOf("{");
2915
+ if (at < 0) return null;
2916
+ try {
2917
+ const req = JSON.parse(text.slice(at));
2918
+ return isObj(req) && req.method === "subscribe" ? subscriptionKey(req.subscription) : null;
2919
+ } catch {
2920
+ return null;
2921
+ }
2922
+ }
2923
+ function decodeAll(f, d, recvTs) {
2924
+ if (!isObj(f.data) || !Array.isArray(f.data.ctxs)) return [protocolError(`hyperliquid ${ALL} frame without data.ctxs`, ALL)];
2925
+ const out = [];
2926
+ const patches = [];
2927
+ const badRows = [];
2928
+ const seen = /* @__PURE__ */ new Set();
2929
+ for (const pair of f.data.ctxs) {
2930
+ if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string" || !Array.isArray(pair[1])) {
2931
+ out.push(protocolError(`hyperliquid ${ALL}: malformed [dex, ctxs] entry`, ALL));
2932
+ continue;
2933
+ }
2934
+ const dex = pair[0];
2935
+ const sel = d.dexes.get(dex);
2936
+ if (sel === void 0) continue;
2937
+ seen.add(dex);
2938
+ const ctxs = pair[1];
2939
+ if (ctxs.length !== sel.names.length) {
2940
+ out.push(protocolError(
2941
+ `hyperliquid ${ALL}: dex "${dex}" has ${ctxs.length} ctxs but the directory has ${sel.names.length} markets \u2014 not assigned (recreate the stream to reload meta)`,
2942
+ ALL
2943
+ ));
2944
+ continue;
2945
+ }
2946
+ ctxs.forEach((ctx, i) => {
2947
+ const name = sel.names[i];
2948
+ if (name === null) return;
2949
+ if (!isObj(ctx)) {
2950
+ badRows.push({ symbol: name, fields: ["ctx"] });
2951
+ return;
2952
+ }
2953
+ const bad = [];
2954
+ patches.push(patchFromCtx(name, ctx, recvTs, bad));
2955
+ if (bad.length > 0) badRows.push({ symbol: name, fields: bad });
2956
+ });
2957
+ }
2958
+ for (const dex of d.dexes.keys()) {
2959
+ if (!seen.has(dex)) out.push(protocolError(`hyperliquid ${ALL}: selected dex "${dex}" missing from the frame`, ALL));
2960
+ }
2961
+ if (badRows.length > 0) out.push(badFieldsError("hyperliquid", ALL, badRows));
2962
+ return [{ t: "market", wire: ALL, patches }, ...out];
2963
+ }
2964
+
2965
+ // src/ws/pacifica-instrument.ts
2966
+ function isPacificaPerpetual(row) {
2967
+ return row.instrument_type === "perpetual";
2968
+ }
2969
+
2970
+ // src/ws/codecs/pacifica.ts
2971
+ var S2 = VENUE_STATIC.pacifica;
2972
+ var PRICES = "prices";
2973
+ function sourceKey(d) {
2974
+ if (!isObj(d) || typeof d.source !== "string") return null;
2975
+ return typeof d.symbol === "string" ? `${d.source}:${d.symbol}` : d.source;
2976
+ }
2977
+ var pacifica = {
2978
+ venue: "pacifica",
2979
+ capabilities: capabilities({
2980
+ testnet: true,
2981
+ user: true,
2982
+ fields: {
2983
+ mark: true,
2984
+ index: true,
2985
+ bestBid: false,
2986
+ bestAsk: false,
2987
+ last: false,
2988
+ predictedRate: true,
2989
+ nextFundingTime: false,
2990
+ fundingInterval: true,
2991
+ openInterest: true,
2992
+ volume24hNotional: true
2993
+ },
2994
+ notes: {
2995
+ bestBid: "`prices` has no bid/ask; use the bbo channel (Phase 2)",
2996
+ bestAsk: "`prices` has no bid/ask; use the bbo channel (Phase 2)",
2997
+ last: "Not in `prices`",
2998
+ nextFundingTime: "Not in `prices`",
2999
+ openInterest: "Base units",
3000
+ user: "By account (Solana address), no auth. account_order_updates / account_trades send no snapshot \u2192 gap refetch-required for orders (at subscribe and after a reconnect) and fills (after a reconnect)",
3001
+ bbo: "bbo{symbol} \u2264 20 per connection (more are spread over further sockets); `li` orders frames (non-contiguous)",
3002
+ positions: "account_positions envelope `li` orders snapshots: an older one than the last applied is dropped",
3003
+ clientId: "orders and fills: `I` (client order id; null when the order has none)",
3004
+ liquidationPrice: "`l` (null when not applicable, per the venue)",
3005
+ fee: "trade `f`; feeToken not stated \u2192 null"
3006
+ }
3007
+ }),
3008
+ url: (n) => S2.wsUrl[n],
3009
+ restUrl: (n) => S2.restUrl[n],
3010
+ secretKeys: [],
3011
+ secretUrlPatterns: [/PF-API-KEY=[^&\s"']+/gi],
3012
+ ping: S2.ping,
3013
+ limits: S2.limits,
3014
+ closesOnBadSubscribe: false,
3015
+ staleAfterMs: S2.staleAfterMs,
3016
+ openInterest: { unit: "base", sides: null },
3017
+ refresh: null,
3018
+ async loadDirectory(ctx) {
3019
+ const raw = await fetchJson(ctx.fetch, "pacifica", `${ctx.restUrl}/info`);
3020
+ if (!isObj(raw) || !Array.isArray(raw.data)) throw badShape("pacifica", "GET /info has no data array");
3021
+ const entries = [];
3022
+ const nonPerp = /* @__PURE__ */ new Set();
3023
+ const unclassified = [];
3024
+ for (const m of raw.data) {
3025
+ if (!isObj(m) || typeof m.symbol !== "string") throw badShape("pacifica", "GET /info market without symbol");
3026
+ if (typeof m.instrument_type !== "string") {
3027
+ unclassified.push(m.symbol);
3028
+ nonPerp.add(m.symbol);
3029
+ continue;
3030
+ }
3031
+ if (!isPacificaPerpetual(m)) {
3032
+ nonPerp.add(m.symbol);
3033
+ continue;
3034
+ }
3035
+ const base = m.symbol;
3036
+ const quote = "USDC";
3037
+ entries.push({
3038
+ venue: "pacifica",
3039
+ dex: null,
3040
+ symbol: m.symbol,
3041
+ venueSymbol: m.symbol,
3042
+ base,
3043
+ quote,
3044
+ canonical: ctx.canonicalize("pacifica", { venueSymbol: m.symbol, dex: null, base, quote }),
3045
+ rateIntervalHours: 1,
3046
+ settlementIntervalHours: 1,
3047
+ source: "rest:/api/v1/info"
3048
+ });
3049
+ }
3050
+ const warnings = unclassified.length === 0 ? [] : [
3051
+ `pacifica GET /info: ${unclassified.length} market(s) without instrument_type left out (cannot tell perp from spot): ${unclassified.slice(0, 10).join(", ")}${unclassified.length > 10 ? ", \u2026" : ""}`
3052
+ ];
3053
+ return { entries, data: { nonPerp }, warnings };
3054
+ },
3055
+ fundingMeta: () => ({
3056
+ rateKind: "lastSettled",
3057
+ rateSource: "ws",
3058
+ rateIntervalHours: 1,
3059
+ settlementIntervalHours: 1,
3060
+ intervalSource: "venue-definition",
3061
+ intervalAsOf: null,
3062
+ marketClosed: null
3063
+ }),
3064
+ wiresFor(target, _dir, staleOverrideMs) {
3065
+ const prices = { key: PRICES, staleAfterMs: staleOverrideMs === null ? S2.staleAfterMs : staleOverrideMs, group: PRICES, params: { source: PRICES } };
3066
+ if (target.kind === "allMarkets") return [prices];
3067
+ const out = [];
3068
+ if (target.channels.includes("market")) out.push(prices);
3069
+ if (target.channels.includes("bbo")) out.push({ key: `bbo:${target.symbol}`, staleAfterMs: null, group: "bbo", params: { source: "bbo", symbol: target.symbol } });
3070
+ return out;
3071
+ },
3072
+ user: {
3073
+ inPageSecrets: false,
3074
+ wires(ref, channels) {
3075
+ if (ref.venue !== "pacifica") return [];
3076
+ const w = (source, gaps) => ({ key: source, staleAfterMs: null, group: source, params: { source, account: ref.account }, ...gaps === void 0 ? {} : { gaps } });
3077
+ const out = [];
3078
+ if (channels.includes("account")) out.push(w("account_info"));
3079
+ if (channels.includes("positions")) out.push(w("account_positions"));
3080
+ if (channels.includes("orders")) out.push(w("account_order_updates", { onReconnect: ["orders"], onSubscribe: ["orders"] }));
3081
+ if (channels.includes("fills")) out.push(w("account_trades", { onReconnect: ["fills"] }));
3082
+ return out;
3083
+ }
3084
+ },
3085
+ encodeSubscribe: (w) => JSON.stringify({ method: "subscribe", params: w.params }),
3086
+ encodeUnsubscribe: (w) => JSON.stringify({ method: "unsubscribe", params: w.params }),
3087
+ decode(text, dir) {
3088
+ const p = parseFrame(text);
3089
+ if (!p.ok) return [p.ev];
3090
+ const f = p.v;
3091
+ if (!isObj(f) || typeof f.channel !== "string") return [unrecognised("pacifica")];
3092
+ switch (f.channel) {
3093
+ case "pong":
3094
+ return [{ t: "pong" }];
3095
+ case "subscribe":
3096
+ case "unsubscribe":
3097
+ return [{ t: "ack", wire: sourceKey(f.data), reqId: null, unsubscribe: f.channel === "unsubscribe" }];
3098
+ case "account_info":
3099
+ return decodeAccount(f.data);
3100
+ case "account_positions":
3101
+ return decodePositions(f.data, f.li);
3102
+ case "account_order_updates":
3103
+ return decodeOrders(f.data);
3104
+ case "account_trades":
3105
+ return decodeTrades(f.data);
3106
+ case "bbo":
3107
+ return decodeBbo2(f.data);
3108
+ case PRICES: {
3109
+ if (!Array.isArray(f.data)) return [unrecognised("pacifica")];
3110
+ const patches = [];
3111
+ const badRows = [];
3112
+ const out = [];
3113
+ const nonPerp = dir.data.nonPerp;
3114
+ for (const r of f.data) {
3115
+ if (!isObj(r) || typeof r.symbol !== "string") {
3116
+ badRows.push({ symbol: "?", fields: ["symbol"] });
3117
+ continue;
3118
+ }
3119
+ if (nonPerp.has(r.symbol)) continue;
3120
+ const bad = [];
3121
+ patches.push(compact({
3122
+ symbol: r.symbol,
3123
+ venueTs: (() => {
3124
+ const t = readTs(r, "timestamp", bad);
3125
+ return t === void 0 ? null : t;
3126
+ })(),
3127
+ mark: readDec(r, "mark", bad),
3128
+ index: readDec(r, "oracle", bad),
3129
+ rate: readDec(r, "funding", bad),
3130
+ predictedRate: readDec(r, "next_funding", bad),
3131
+ openInterest: readDec(r, "open_interest", bad),
3132
+ volume24hNotional: readDec(r, "volume_24h", bad)
3133
+ }));
3134
+ if (bad.length > 0) badRows.push({ symbol: r.symbol, fields: bad });
3135
+ }
3136
+ if (badRows.length > 0) out.push(badFieldsError("pacifica", PRICES, badRows));
3137
+ return [{ t: "market", wire: PRICES, patches }, ...out];
3138
+ }
3139
+ default:
3140
+ return [unrecognised("pacifica")];
3141
+ }
3142
+ }
3143
+ };
3144
+ var BOOK_SIDE = { bid: "buy", ask: "sell" };
3145
+ var TRADE_SIDE = { open_long: "buy", close_short: "buy", open_short: "sell", close_long: "sell" };
3146
+ var OPEN_STATUSES = /* @__PURE__ */ new Set(["open", "partially_filled"]);
3147
+ function decodeAccount(d) {
3148
+ if (!isObj(d)) return [protocolError("pacifica account_info frame without data", "account_info")];
3149
+ const bad = [];
3150
+ const out = [{
3151
+ t: "user",
3152
+ wire: "account_info",
3153
+ deltas: [{
3154
+ kind: "account",
3155
+ snapshot: true,
3156
+ dex: null,
3157
+ venueTs: ts(d, "t", bad),
3158
+ // every frame carries the full account
3159
+ equity: dec(d, "ae", bad),
3160
+ available: dec(d, "as", bad),
3161
+ marginUsed: dec(d, "mu", bad),
3162
+ unrealizedPnl: null
3163
+ // not in account_info
3164
+ }]
3165
+ }];
3166
+ if (bad.length > 0) out.push(badFieldsError("pacifica", "account_info", [{ symbol: "account", fields: bad }]));
3167
+ return out;
3168
+ }
3169
+ function decodePositions(d, li) {
3170
+ if (!Array.isArray(d)) return [protocolError("pacifica account_positions data is not an array", "account_positions")];
3171
+ const bad = [];
3172
+ const rows = [];
3173
+ for (const p of d) {
3174
+ if (!isObj(p) || typeof p.s !== "string") {
3175
+ bad.push("position");
3176
+ continue;
3177
+ }
3178
+ const side = pick2(BOOK_SIDE, p.d);
3179
+ rows.push({
3180
+ symbol: p.s,
3181
+ dex: null,
3182
+ side: side === null ? null : side === "buy" ? "long" : "short",
3183
+ size: dec(p, "a", bad),
3184
+ entryPrice: dec(p, "p", bad),
3185
+ markPrice: null,
3186
+ positionValue: null,
3187
+ unrealizedPnl: null,
3188
+ liquidationPrice: dec(p, "l", bad),
3189
+ marginUsed: dec(p, "m", bad),
3190
+ leverage: null,
3191
+ venueTs: ts(p, "t", bad)
3192
+ });
3193
+ }
3194
+ const out = [{
3195
+ t: "user",
3196
+ wire: "account_positions",
3197
+ seq: typeof li === "number" ? li : null,
3198
+ deltas: [{ kind: "positions", snapshot: true, scope: "", rows, remove: [] }]
3199
+ }];
3200
+ if (bad.length > 0) out.push(badFieldsError("pacifica", "account_positions", [{ symbol: "positions", fields: bad }]));
3201
+ return out;
3202
+ }
3203
+ function decodeOrders(d) {
3204
+ if (!Array.isArray(d)) return [protocolError("pacifica account_order_updates data is not an array", "account_order_updates")];
3205
+ const bad = [];
3206
+ const rows = [];
3207
+ const remove = [];
3208
+ for (const o of d) {
3209
+ const id = isObj(o) ? idOf(o.i) : null;
3210
+ if (!isObj(o) || id === null || typeof o.s !== "string") {
3211
+ bad.push("order");
3212
+ continue;
3213
+ }
3214
+ if (!OPEN_STATUSES.has(String(o.os))) {
3215
+ remove.push(id);
3216
+ continue;
3217
+ }
3218
+ const size = dec(o, "a", bad);
3219
+ const filled = dec(o, "f", bad);
3220
+ rows.push({
3221
+ orderId: id,
3222
+ clientId: typeof o.I === "string" ? o.I : null,
3223
+ symbol: o.s,
3224
+ dex: null,
3225
+ side: pick2(BOOK_SIDE, o.d),
3226
+ type: typeof o.ot === "string" ? o.ot : null,
3227
+ status: typeof o.os === "string" ? o.os : null,
3228
+ price: dec(o, "ip", bad),
3229
+ size,
3230
+ filled,
3231
+ remaining: null,
3232
+ reduceOnly: typeof o.r === "boolean" ? o.r : null,
3233
+ createdAt: ts(o, "ct", bad),
3234
+ updatedAt: ts(o, "ut", bad)
3235
+ });
3236
+ }
3237
+ const out = [{ t: "user", wire: "account_order_updates", deltas: [{ kind: "orders", snapshot: false, scope: "", rows, remove }] }];
3238
+ if (bad.length > 0) out.push(badFieldsError("pacifica", "account_order_updates", [{ symbol: "orders", fields: bad }]));
3239
+ return out;
3240
+ }
3241
+ function decodeTrades(d) {
3242
+ if (!Array.isArray(d)) return [protocolError("pacifica account_trades data is not an array", "account_trades")];
3243
+ const bad = [];
3244
+ const fills = [];
3245
+ for (const t of d) {
3246
+ const h = isObj(t) ? idOf(t.h) : null;
3247
+ const i = isObj(t) ? idOf(t.i) : null;
3248
+ if (!isObj(t) || h === null || typeof t.s !== "string") {
3249
+ bad.push("h/s");
3250
+ continue;
3251
+ }
3252
+ fills.push({
3253
+ fillId: i === null ? h : `${h}:${i}`,
3254
+ clientId: typeof t.I === "string" ? t.I : null,
3255
+ symbol: t.s,
3256
+ dex: null,
3257
+ side: pick2(TRADE_SIDE, t.ts),
3258
+ price: dec(t, "p", bad),
3259
+ size: dec(t, "a", bad),
3260
+ fee: dec(t, "f", bad),
3261
+ feeToken: null,
3262
+ closedPnl: dec(t, "n", bad),
3263
+ orderId: i,
3264
+ liquidity: t.te === "fulfill_maker" ? "maker" : t.te === "fulfill_taker" ? "taker" : null,
3265
+ time: ts(t, "t", bad)
3266
+ });
3267
+ }
3268
+ const out = [{ t: "user", wire: "account_trades", deltas: [{ kind: "fills", snapshot: false, fills }] }];
3269
+ if (bad.length > 0) out.push(badFieldsError("pacifica", "account_trades", [{ symbol: "fills", fields: bad }]));
3270
+ return out;
3271
+ }
3272
+ function decodeBbo2(d) {
3273
+ if (!isObj(d) || typeof d.s !== "string") return [protocolError("pacifica bbo frame without symbol")];
3274
+ const wire = `bbo:${d.s}`;
3275
+ const bad = [];
3276
+ const out = [{
3277
+ t: "bbo",
3278
+ wire,
3279
+ symbol: d.s,
3280
+ seq: typeof d.li === "number" ? d.li : null,
3281
+ bid: dec(d, "b", bad),
3282
+ bidSize: dec(d, "B", bad),
3283
+ ask: dec(d, "a", bad),
3284
+ askSize: dec(d, "A", bad),
3285
+ venueTs: ts(d, "t", bad)
3286
+ }];
3287
+ if (bad.length > 0) out.push(badFieldsError("pacifica", wire, [{ symbol: d.s, fields: bad }]));
3288
+ return out;
3289
+ }
3290
+
3291
+ // src/ws/codecs/lighter.ts
3292
+ var ALL2 = "market_stats/all";
3293
+ var ORDERS_TOKEN_TTL_SEC = 3600;
3294
+ var OPEN_ORDER = /* @__PURE__ */ new Set(["open", "pending", "in-progress"]);
3295
+ var ZERO = /^-?0*(\.0*)?$/;
3296
+ function bookSide(o, key, bad) {
3297
+ return o[key] === "" ? null : readDec(o, key, bad);
3298
+ }
3299
+ function percentToFraction(o, key, bad) {
3300
+ const v = readDec(o, key, bad);
3301
+ if (v === void 0 || v === null) return v;
3302
+ const f = shiftDecimal(v, -2);
3303
+ if (f === void 0) {
3304
+ bad.push(key);
3305
+ return void 0;
3306
+ }
3307
+ return f;
3308
+ }
3309
+ function makeLighter(venue, quote) {
3310
+ const S5 = VENUE_STATIC[venue];
3311
+ return {
3312
+ venue,
3313
+ capabilities: capabilities({
3314
+ testnet: S5.wsUrl.testnet !== null,
3315
+ user: true,
3316
+ fields: {
3317
+ mark: true,
3318
+ index: true,
3319
+ bestBid: true,
3320
+ bestAsk: true,
3321
+ last: true,
3322
+ predictedRate: true,
3323
+ nextFundingTime: true,
3324
+ fundingInterval: true,
3325
+ openInterest: true,
3326
+ volume24hNotional: true
3327
+ },
3328
+ notes: {
3329
+ rate: "WS funding is PERCENT per 1 h on the wire; emitted as a fraction per 1 h",
3330
+ nextFundingTime: "Hourly settlement rule (venue-definition)",
3331
+ openInterest: "USD notional (Phase 0), sides not stated by the venue",
3332
+ user: "By accountIndex. user_stats / account_all_positions / account_all_trades need no auth; account_all_orders needs CredentialProvider.authToken (re-called at 80 % TTL). account_all_trades does not replay \u2192 gap refetch-required for fills after a reconnect",
3333
+ clientId: "orders: client_order_id; fills: ask_/bid_client_id_str of our side; 0 = no client id \u2192 null",
3334
+ liquidationPrice: 'liquidation_price; "0" (no liquidation price, e.g. well-covered cross margin) \u2192 null, as in the CLI REST path',
3335
+ fee: "taker_fee / maker_fee are a rate in 1e-6 of usd_amount (omitted when 0): fee = usd_amount \xD7 rate \xD7 1e-6 in the collateral token",
3336
+ bbo: "ticker/{market_id}; `nonce` orders frames (non-contiguous)",
3337
+ ...S5.wsUrl.testnet === null ? { "network:testnet": "Testnet WS answers HTTP 403 (CloudFront)" } : {}
3338
+ }
3339
+ }),
3340
+ url: (n) => S5.wsUrl[n],
3341
+ restUrl: (n) => S5.restUrl[n],
3342
+ secretKeys: ["auth"],
3343
+ secretUrlPatterns: [],
3344
+ ping: S5.ping,
3345
+ limits: S5.limits,
3346
+ closesOnBadSubscribe: false,
3347
+ staleAfterMs: S5.staleAfterMs,
3348
+ openInterest: { unit: "usd", sides: null },
3349
+ refresh: null,
3350
+ async loadDirectory(ctx) {
3351
+ const raw = await fetchJson(ctx.fetch, venue, `${ctx.restUrl}/api/v1/orderBooks`);
3352
+ if (!isObj(raw) || !Array.isArray(raw.order_books)) throw badShape(venue, "GET /api/v1/orderBooks has no order_books array");
3353
+ const entries = [];
3354
+ const idToSymbol = /* @__PURE__ */ new Map();
3355
+ const nonPerp = /* @__PURE__ */ new Set();
3356
+ for (const m of raw.order_books) {
3357
+ if (!isObj(m) || typeof m.symbol !== "string" || typeof m.market_id !== "number") {
3358
+ throw badShape(venue, "orderBooks entry without symbol/market_id");
3359
+ }
3360
+ const id = String(m.market_id);
3361
+ if (m.market_type !== "perp") {
3362
+ nonPerp.add(id);
3363
+ continue;
3364
+ }
3365
+ idToSymbol.set(id, m.symbol);
3366
+ entries.push({
3367
+ venue,
3368
+ dex: null,
3369
+ symbol: m.symbol,
3370
+ venueSymbol: m.symbol,
3371
+ base: m.symbol,
3372
+ quote,
3373
+ canonical: ctx.canonicalize(venue, { venueSymbol: m.symbol, dex: null, base: m.symbol, quote }),
3374
+ rateIntervalHours: 1,
3375
+ settlementIntervalHours: 1,
3376
+ source: "rest:/api/v1/orderBooks"
3377
+ });
3378
+ }
3379
+ const symbolToId = new Map(Array.from(idToSymbol, ([id, sym]) => [sym, id]));
3380
+ return { entries, data: { idToSymbol, nonPerp, symbolToId } };
3381
+ },
3382
+ fundingMeta: () => ({
3383
+ rateKind: "lastSettled",
3384
+ rateSource: "ws",
3385
+ rateIntervalHours: 1,
3386
+ settlementIntervalHours: 1,
3387
+ intervalSource: "venue-definition",
3388
+ intervalAsOf: null,
3389
+ marketClosed: null
3390
+ }),
3391
+ wiresFor(target, dir, staleOverrideMs) {
3392
+ const all = { key: ALL2, staleAfterMs: staleOverrideMs === null ? S5.staleAfterMs : staleOverrideMs, params: { channel: ALL2 } };
3393
+ if (target.kind === "allMarkets") return [all];
3394
+ const out = [];
3395
+ if (target.channels.includes("market")) out.push(all);
3396
+ if (target.channels.includes("bbo")) {
3397
+ const id = dir.data.symbolToId.get(target.symbol);
3398
+ out.push({ key: `ticker/${id}`, staleAfterMs: null, params: { channel: `ticker/${id}` } });
3399
+ }
3400
+ return out;
3401
+ },
3402
+ user: {
3403
+ inPageSecrets: false,
3404
+ wires(ref, channels) {
3405
+ if (ref.venue !== venue) return [];
3406
+ const a = ref.accountIndex;
3407
+ const w = (ch, extra = {}) => ({ key: `${ch}/${a}`, staleAfterMs: null, params: { channel: `${ch}/${a}` }, ...extra });
3408
+ const out = [];
3409
+ if (channels.includes("account")) out.push(w("user_stats"));
3410
+ if (channels.includes("positions")) out.push(w("account_all_positions"));
3411
+ if (channels.includes("orders")) out.push(w("account_all_orders", { auth: { purpose: `${venue}:account_all_orders`, maxTtlSec: ORDERS_TOKEN_TTL_SEC } }));
3412
+ if (channels.includes("fills")) out.push(w("account_all_trades", { gaps: { onReconnect: ["fills"] } }));
3413
+ return out;
3414
+ }
3415
+ },
3416
+ encodeSubscribe: (w, _reqId, auth) => JSON.stringify({ type: "subscribe", channel: w.params.channel, ...auth === null ? {} : { auth } }),
3417
+ encodeUnsubscribe: (w) => JSON.stringify({ type: "unsubscribe", channel: w.params.channel }),
3418
+ decode(text, dir, recvTs) {
3419
+ const p = parseFrame(text);
3420
+ if (!p.ok) return [p.ev];
3421
+ const f = p.v;
3422
+ if (!isObj(f)) return [unrecognised(venue)];
3423
+ if (isObj(f.error)) return [decodeError(venue, f.error)];
3424
+ switch (f.type) {
3425
+ case "connected":
3426
+ return [{ t: "ignore", what: "connected" }];
3427
+ case "pong":
3428
+ return [{ t: "pong" }];
3429
+ case "unsubscribed":
3430
+ return [{ t: "ack", wire: channelToWire(f.channel), reqId: null, unsubscribe: true }];
3431
+ case "subscribed/market_stats":
3432
+ case "update/market_stats":
3433
+ if (f.channel !== "market_stats:all") return [unrecognised(venue)];
3434
+ return decodeStats(venue, f, dir.data, recvTs);
3435
+ default: {
3436
+ const ty = typeof f.type === "string" ? /^(subscribed|update)\/(user_stats|account_all_positions|account_all_orders|account_all_trades|ticker)$/.exec(f.type) : null;
3437
+ const wire = channelToWire(f.channel);
3438
+ if (ty === null || wire === null) return [unrecognised(venue)];
3439
+ const snapshot = ty[1] === "subscribed";
3440
+ const d = dir.data;
3441
+ switch (ty[2]) {
3442
+ case "user_stats":
3443
+ return decodeUserStats(venue, wire, f, snapshot);
3444
+ case "account_all_positions":
3445
+ return decodePositions2(venue, wire, f, d, snapshot);
3446
+ case "account_all_orders":
3447
+ return decodeOrders2(venue, wire, f, d, snapshot);
3448
+ case "account_all_trades":
3449
+ return decodeTrades2(venue, wire, f, d, snapshot, quote);
3450
+ default:
3451
+ return decodeTicker(venue, wire, f, d);
3452
+ }
3453
+ }
3454
+ }
3455
+ }
3456
+ };
3457
+ }
3458
+ function channelToWire(ch) {
3459
+ if (typeof ch !== "string") return null;
3460
+ const m = /^(market_stats|user_stats|account_all_positions|account_all_orders|account_all_trades|ticker):(\w+)$/.exec(ch);
3461
+ return m === null ? null : `${m[1]}/${m[2]}`;
3462
+ }
3463
+ function decodeError(venue, e) {
3464
+ const code = typeof e.code === "number" ? e.code : null;
3465
+ const message = typeof e.message === "string" ? e.message : JSON.stringify(e);
3466
+ const m = /(market_stats|user_stats|account_all_positions|account_all_orders|account_all_trades|ticker)[:/](\w+)/.exec(message);
3467
+ const wire = m === null ? null : `${m[1]}/${m[2]}`;
3468
+ if (code === 20001) {
3469
+ return venueError(
3470
+ "WS_AUTH_FAILED",
3471
+ `${venue} error 20001: ${message}`,
3472
+ wire,
3473
+ wire !== null,
3474
+ "account_all_orders needs a valid auth token from CredentialProvider.authToken (expired, wrong account, or wrong API key)"
3475
+ );
3476
+ }
3477
+ if (code === 30005) return venueError("INVALID_PARAMS", `${venue} error 30005: ${message}`, wire, wire !== null, null);
3478
+ return protocolError(`${venue} error ${code === null ? "?" : code}: ${message}`, wire);
3479
+ }
3480
+ function decodeStats(venue, f, d, recvTs) {
3481
+ if (!isObj(f.market_stats)) return [protocolError(`${venue} ${ALL2} frame without market_stats`, ALL2)];
3482
+ const venueTs = typeof f.timestamp === "number" ? f.timestamp : null;
3483
+ const patches = [];
3484
+ const badRows = [];
3485
+ for (const [id, r] of Object.entries(f.market_stats)) {
3486
+ if (d.nonPerp.has(id)) continue;
3487
+ if (!isObj(r)) {
3488
+ badRows.push({ symbol: `#${id}`, fields: ["entry"] });
3489
+ continue;
3490
+ }
3491
+ const known = d.idToSymbol.get(id);
3492
+ const symbol = known !== void 0 ? known : typeof r.symbol === "string" ? r.symbol : `#${id}`;
3493
+ const bad = [];
3494
+ patches.push(compact({
3495
+ symbol,
3496
+ venueTs,
3497
+ mark: readDec(r, "mark_price", bad),
3498
+ index: readDec(r, "index_price", bad),
3499
+ bestBid: bookSide(r, "best_bid_price", bad),
3500
+ bestAsk: bookSide(r, "best_ask_price", bad),
3501
+ last: readDec(r, "last_trade_price", bad),
3502
+ rate: percentToFraction(r, "funding_rate", bad),
3503
+ predictedRate: percentToFraction(r, "current_funding_rate", bad),
3504
+ openInterest: readDec(r, "open_interest", bad),
3505
+ volume24hNotional: readDec(r, "daily_quote_token_volume", bad),
3506
+ nextFundingTime: nextHour(recvTs)
3507
+ // hourly settlement (venue-definition)
3508
+ }));
3509
+ if (bad.length > 0) badRows.push({ symbol, fields: bad });
3510
+ }
3511
+ const out = [{ t: "market", wire: ALL2, patches }];
3512
+ if (badRows.length > 0) out.push(badFieldsError(venue, ALL2, badRows));
3513
+ return out;
3514
+ }
3515
+ var lighter = makeLighter("lighter", "USDC");
3516
+ var lighterRh = makeLighter("lighter-rh", "USDG");
3517
+ function accountOf(wire) {
3518
+ return wire.slice(wire.indexOf("/") + 1);
3519
+ }
3520
+ function positivePrice(v) {
3521
+ return v !== null && Number(v) > 0 ? v : null;
3522
+ }
3523
+ function clientIdOf(str, num) {
3524
+ const id = idOf(str) === null ? idOf(num) : idOf(str);
3525
+ return id === null || /^0+$/.test(id) ? null : id;
3526
+ }
3527
+ function decodeUserStats(venue, wire, f, snapshot) {
3528
+ if (!isObj(f.stats)) return [protocolError(`${venue} ${wire} frame without stats`, wire)];
3529
+ const bad = [];
3530
+ const out = [{
3531
+ t: "user",
3532
+ wire,
3533
+ deltas: [{
3534
+ kind: "account",
3535
+ snapshot,
3536
+ dex: null,
3537
+ venueTs: ts(f, "timestamp", bad),
3538
+ equity: dec(f.stats, "portfolio_value", bad),
3539
+ available: dec(f.stats, "available_balance", bad),
3540
+ marginUsed: null,
3541
+ // margin_usage is a ratio, not an amount
3542
+ unrealizedPnl: null
3543
+ }]
3544
+ }];
3545
+ if (bad.length > 0) out.push(badFieldsError(venue, wire, [{ symbol: "account", fields: bad }]));
3546
+ return out;
3547
+ }
3548
+ function decodePositions2(venue, wire, f, d, snapshot) {
3549
+ if (!isObj(f.positions)) return [protocolError(`${venue} ${wire} frame without positions`, wire)];
3550
+ const bad = [];
3551
+ const rows = [];
3552
+ const remove = [];
3553
+ const venueTs = ts(f, "transaction_time", bad, 1e-3);
3554
+ for (const [id, p] of Object.entries(f.positions)) {
3555
+ if (!isObj(p)) {
3556
+ bad.push(`#${id}`);
3557
+ continue;
3558
+ }
3559
+ const known = d.idToSymbol.get(id);
3560
+ const symbol = known !== void 0 ? known : typeof p.symbol === "string" ? p.symbol : `#${id}`;
3561
+ const size = dec(p, "position", bad);
3562
+ if (size === null || ZERO.test(size)) {
3563
+ remove.push(symbol);
3564
+ continue;
3565
+ }
3566
+ rows.push({
3567
+ symbol,
3568
+ dex: null,
3569
+ side: p.sign === 1 ? "long" : p.sign === -1 ? "short" : null,
3570
+ size: size.replace(/^-/, ""),
3571
+ entryPrice: dec(p, "avg_entry_price", bad),
3572
+ markPrice: null,
3573
+ positionValue: dec(p, "position_value", bad),
3574
+ unrealizedPnl: dec(p, "unrealized_pnl", bad),
3575
+ liquidationPrice: positivePrice(dec(p, "liquidation_price", bad)),
3576
+ marginUsed: dec(p, "allocated_margin", bad),
3577
+ leverage: null,
3578
+ venueTs
3579
+ });
3580
+ }
3581
+ const out = [{ t: "user", wire, deltas: [{ kind: "positions", snapshot, scope: "", rows, remove: snapshot ? [] : remove }] }];
3582
+ if (bad.length > 0) out.push(badFieldsError(venue, wire, [{ symbol: "positions", fields: bad }]));
3583
+ return out;
3584
+ }
3585
+ function decodeOrders2(venue, wire, f, d, snapshot) {
3586
+ if (!isObj(f.orders)) return [protocolError(`${venue} ${wire} frame without orders`, wire)];
3587
+ const bad = [];
3588
+ const rows = [];
3589
+ const remove = [];
3590
+ for (const [mkt, list] of Object.entries(f.orders)) {
3591
+ if (!Array.isArray(list)) {
3592
+ bad.push(`orders[${mkt}]`);
3593
+ continue;
3594
+ }
3595
+ for (const o of list) {
3596
+ const id = isObj(o) ? idOf(o.order_id) === null ? idOf(o.order_index) : idOf(o.order_id) : null;
3597
+ if (!isObj(o) || id === null) {
3598
+ bad.push("order_id");
3599
+ continue;
3600
+ }
3601
+ if (!OPEN_ORDER.has(String(o.status))) {
3602
+ remove.push(id);
3603
+ continue;
3604
+ }
3605
+ const known = d.idToSymbol.get(String(o.market_index));
3606
+ rows.push({
3607
+ orderId: id,
3608
+ clientId: clientIdOf(o.client_order_id, o.client_order_index),
3609
+ symbol: known !== void 0 ? known : `#${String(o.market_index)}`,
3610
+ dex: null,
3611
+ side: typeof o.is_ask === "boolean" ? o.is_ask ? "sell" : "buy" : null,
3612
+ type: typeof o.type === "string" ? o.type : null,
3613
+ status: typeof o.status === "string" ? o.status : null,
3614
+ price: dec(o, "price", bad),
3615
+ size: dec(o, "initial_base_amount", bad),
3616
+ filled: dec(o, "filled_base_amount", bad),
3617
+ remaining: dec(o, "remaining_base_amount", bad),
3618
+ reduceOnly: typeof o.reduce_only === "boolean" ? o.reduce_only : null,
3619
+ createdAt: ts(o, "created_at", bad),
3620
+ updatedAt: ts(o, "updated_at", bad)
3621
+ });
3622
+ }
3623
+ }
3624
+ const out = [{ t: "user", wire, deltas: [{ kind: "orders", snapshot, scope: "", rows, remove: snapshot ? [] : remove }] }];
3625
+ if (bad.length > 0) out.push(badFieldsError(venue, wire, [{ symbol: "orders", fields: bad }]));
3626
+ return out;
3627
+ }
3628
+ function feeOf(t, maker, bad) {
3629
+ if (maker === null) return null;
3630
+ const rate = maker ? t.maker_fee : t.taker_fee;
3631
+ if (rate !== void 0 && (typeof rate !== "number" || !Number.isInteger(rate))) {
3632
+ bad.push(maker ? "maker_fee" : "taker_fee");
3633
+ return null;
3634
+ }
3635
+ const usd = dec(t, "usd_amount", bad);
3636
+ if (usd === null) return null;
3637
+ const fee = mulDecimalInt(usd, rate === void 0 ? 0 : rate, -6);
3638
+ if (fee === void 0) {
3639
+ bad.push("usd_amount");
3640
+ return null;
3641
+ }
3642
+ return fee;
3643
+ }
3644
+ function decodeTrades2(venue, wire, f, d, snapshot, quote) {
3645
+ const groups = Array.isArray(f.trades) ? [f.trades] : isObj(f.trades) ? Object.values(f.trades) : [];
3646
+ if (!Array.isArray(f.trades) && !isObj(f.trades)) return [protocolError(`${venue} ${wire} frame without trades`, wire)];
3647
+ const acct = accountOf(wire);
3648
+ const bad = [];
3649
+ const fills = [];
3650
+ for (const g of groups) {
3651
+ if (!Array.isArray(g)) {
3652
+ bad.push("trades[]");
3653
+ continue;
3654
+ }
3655
+ for (const t of g) {
3656
+ const id = isObj(t) ? idOf(t.trade_id_str) === null ? idOf(t.trade_id) : idOf(t.trade_id_str) : null;
3657
+ if (!isObj(t) || id === null) {
3658
+ bad.push("trade_id");
3659
+ continue;
3660
+ }
3661
+ const isAsk = String(t.ask_account_id) === acct;
3662
+ const isBid = String(t.bid_account_id) === acct;
3663
+ const known = d.idToSymbol.get(String(t.market_id));
3664
+ const base = {
3665
+ fillId: id,
3666
+ symbol: known !== void 0 ? known : `#${String(t.market_id)}`,
3667
+ dex: null,
3668
+ price: dec(t, "price", bad),
3669
+ size: dec(t, "size", bad),
3670
+ closedPnl: null,
3671
+ time: ts(t, "timestamp", bad)
3672
+ };
3673
+ const leg = (ask) => {
3674
+ const maker = typeof t.is_maker_ask === "boolean" ? t.is_maker_ask === ask : null;
3675
+ const fee = feeOf(t, maker, bad);
3676
+ return {
3677
+ ...base,
3678
+ side: ask ? "sell" : "buy",
3679
+ orderId: ask ? idOf(t.ask_id_str) === null ? idOf(t.ask_id) : idOf(t.ask_id_str) : idOf(t.bid_id_str) === null ? idOf(t.bid_id) : idOf(t.bid_id_str),
3680
+ clientId: ask ? clientIdOf(t.ask_client_id_str, t.ask_client_id) : clientIdOf(t.bid_client_id_str, t.bid_client_id),
3681
+ liquidity: maker === null ? null : maker ? "maker" : "taker",
3682
+ fee,
3683
+ feeToken: fee === null ? null : quote
3684
+ };
3685
+ };
3686
+ if (isAsk) fills.push(leg(true));
3687
+ if (isBid) fills.push(leg(false));
3688
+ if (!isAsk && !isBid) fills.push({ ...base, side: null, orderId: null, clientId: null, liquidity: null, fee: null, feeToken: null });
3689
+ }
3690
+ }
3691
+ const out = [{ t: "user", wire, deltas: [{ kind: "fills", snapshot, fills }] }];
3692
+ if (bad.length > 0) out.push(badFieldsError(venue, wire, [{ symbol: "fills", fields: bad }]));
3693
+ return out;
3694
+ }
3695
+ function decodeTicker(venue, wire, f, d) {
3696
+ if (!isObj(f.ticker)) return [protocolError(`${venue} ${wire} frame without ticker`, wire)];
3697
+ const id = accountOf(wire);
3698
+ const known = d.idToSymbol.get(id);
3699
+ const bad = [];
3700
+ const [ask, askSize] = level(f.ticker.a, bad, "a");
3701
+ const [bid, bidSize] = level(f.ticker.b, bad, "b");
3702
+ const out = [{
3703
+ t: "bbo",
3704
+ wire,
3705
+ symbol: known !== void 0 ? known : `#${id}`,
3706
+ seq: typeof f.nonce === "number" ? f.nonce : null,
3707
+ bid,
3708
+ bidSize,
3709
+ ask,
3710
+ askSize,
3711
+ venueTs: ts(f, "timestamp", bad)
3712
+ }];
3713
+ if (bad.length > 0) out.push(badFieldsError(venue, wire, [{ symbol: known !== void 0 ? known : id, fields: bad }]));
3714
+ return out;
3715
+ }
3716
+
3717
+ // src/ws/codecs/aster.ts
3718
+ var S3 = VENUE_STATIC.aster;
3719
+ var MARK = "!markPrice@arr@1s";
3720
+ var TICKER = "!ticker@arr";
3721
+ var TICKER_STALE_MS = 15e3;
3722
+ var FUNDING_INFO = "rest:/fapi/v3/fundingInfo";
3723
+ async function fetchIntervals(ctx) {
3724
+ const raw = await fetchJson(ctx.fetch, "aster", `${ctx.restUrl}/fapi/v3/fundingInfo`);
3725
+ if (!Array.isArray(raw)) throw badShape("aster", "GET /fapi/v3/fundingInfo is not an array");
3726
+ const m = /* @__PURE__ */ new Map();
3727
+ const bad = [];
3728
+ for (const r of raw) {
3729
+ if (isObj(r) && typeof r.symbol === "string" && typeof r.fundingIntervalHours === "number" && r.fundingIntervalHours > 0) {
3730
+ m.set(r.symbol, r.fundingIntervalHours);
3731
+ } else {
3732
+ bad.push(isObj(r) && typeof r.symbol === "string" ? r.symbol : "?");
3733
+ }
3734
+ }
3735
+ const warnings = bad.length === 0 ? [] : [
3736
+ `aster ${FUNDING_INFO}: ${bad.length} malformed row(s) (${bad.slice(0, 5).join(", ")}${bad.length > 5 ? ", \u2026" : ""}); their funding interval is null`
3737
+ ];
3738
+ return { intervals: m, warnings };
3739
+ }
3740
+ var aster = {
3741
+ venue: "aster",
3742
+ capabilities: capabilities({
3743
+ testnet: true,
3744
+ user: false,
3745
+ fields: {
3746
+ mark: true,
3747
+ index: true,
3748
+ bestBid: false,
3749
+ bestAsk: false,
3750
+ last: true,
3751
+ predictedRate: false,
3752
+ nextFundingTime: true,
3753
+ fundingInterval: true,
3754
+ openInterest: false,
3755
+ volume24hNotional: true
3756
+ },
3757
+ notes: {
3758
+ bestBid: "Use the bookTicker bbo channel (Phase 2)",
3759
+ bestAsk: "Use the bookTicker bbo channel (Phase 2)",
3760
+ predictedRate: "Not published; `rate` (r) is the accruing current-period rate",
3761
+ openInterest: "Not on WS (REST only)",
3762
+ fundingInterval: `Labelled REST source ${FUNDING_INFO}, refreshed hourly; missing \u2192 null`,
3763
+ volume24hNotional: "!ticker@arr q (quote-asset volume)",
3764
+ user: 'KNOWN GAP in 0.15.0: no Aster user channels (signed USER_STREAM listenKey) \u2014 kind:"user" rejects CAPABILITY_UNSUPPORTED; arrives in 0.15.1',
3765
+ bbo: "<symbol>@bookTicker; `u` orders frames (non-contiguous)"
3766
+ }
3767
+ }),
3768
+ url: (n) => `${S3.wsUrl[n]}/stream`,
3769
+ restUrl: (n) => S3.restUrl[n],
3770
+ secretKeys: ["listenKey"],
3771
+ // A user-data stream URL ends in the listenKey (/ws/<listenKey>).
3772
+ secretUrlPatterns: [/(?<=asterdex(?:-testnet)?\.com\/(?:ws|stream)\/)[A-Za-z0-9]{20,}/g],
3773
+ ping: S3.ping,
3774
+ limits: S3.limits,
3775
+ closesOnBadSubscribe: false,
3776
+ staleAfterMs: S3.staleAfterMs,
3777
+ openInterest: { unit: null, sides: null },
3778
+ refresh: {
3779
+ source: FUNDING_INFO,
3780
+ everyMs: 36e5,
3781
+ async run(ctx, dir) {
3782
+ const d = dir.data;
3783
+ const r = await fetchIntervals(ctx);
3784
+ d.intervals = r.intervals;
3785
+ d.intervalsAt = ctx.clock();
3786
+ return r.warnings;
3787
+ }
3788
+ },
3789
+ async loadDirectory(ctx) {
3790
+ const [info, fi, premium] = await Promise.all([
3791
+ fetchJson(ctx.fetch, "aster", `${ctx.restUrl}/fapi/v3/exchangeInfo`),
3792
+ fetchIntervals(ctx),
3793
+ fetchJson(ctx.fetch, "aster", `${ctx.restUrl}/fapi/v3/premiumIndex`)
3794
+ ]);
3795
+ if (!isObj(info) || !Array.isArray(info.symbols)) throw badShape("aster", "GET /fapi/v3/exchangeInfo has no symbols array");
3796
+ if (!Array.isArray(premium)) throw badShape("aster", "GET /fapi/v3/premiumIndex is not an array");
3797
+ const intervals = fi.intervals;
3798
+ const entries = [];
3799
+ const listed = /* @__PURE__ */ new Set();
3800
+ for (const s of info.symbols) {
3801
+ if (!isObj(s) || typeof s.symbol !== "string" || typeof s.baseAsset !== "string") {
3802
+ throw badShape("aster", "exchangeInfo symbol without symbol/baseAsset");
3803
+ }
3804
+ const quote = typeof s.quoteAsset === "string" && s.quoteAsset !== "" ? s.quoteAsset : null;
3805
+ listed.add(s.symbol);
3806
+ const hours = intervals.get(s.symbol);
3807
+ entries.push({
3808
+ venue: "aster",
3809
+ dex: null,
3810
+ symbol: s.symbol,
3811
+ venueSymbol: s.symbol,
3812
+ base: s.baseAsset,
3813
+ quote,
3814
+ canonical: ctx.canonicalize("aster", { venueSymbol: s.symbol, dex: null, base: s.baseAsset, quote }),
3815
+ rateIntervalHours: hours === void 0 ? null : hours,
3816
+ settlementIntervalHours: hours === void 0 ? null : hours,
3817
+ source: "rest:/fapi/v3/exchangeInfo"
3818
+ });
3819
+ }
3820
+ const unlisted = /* @__PURE__ */ new Set();
3821
+ for (const p of premium) if (isObj(p) && typeof p.symbol === "string" && !listed.has(p.symbol)) unlisted.add(p.symbol);
3822
+ return { entries, data: { intervals, intervalsAt: ctx.clock(), unlisted }, warnings: fi.warnings };
3823
+ },
3824
+ fundingMeta(entry, dir) {
3825
+ const d = dir.data;
3826
+ const h = d.intervals.get(entry.symbol);
3827
+ return {
3828
+ rateKind: "accruing",
3829
+ rateSource: "ws",
3830
+ rateIntervalHours: h === void 0 ? null : h,
3831
+ settlementIntervalHours: h === void 0 ? null : h,
3832
+ intervalSource: h === void 0 ? null : FUNDING_INFO,
3833
+ intervalAsOf: h === void 0 ? null : d.intervalsAt,
3834
+ marketClosed: null
3835
+ };
3836
+ },
3837
+ wiresFor(target, _dir, staleOverrideMs) {
3838
+ const all = [
3839
+ { key: MARK, staleAfterMs: staleOverrideMs === null ? S3.staleAfterMs : staleOverrideMs, params: {} },
3840
+ { key: TICKER, staleAfterMs: staleOverrideMs === null ? TICKER_STALE_MS : staleOverrideMs, params: {} }
3841
+ ];
3842
+ if (target.kind === "allMarkets") return all;
3843
+ const out = target.channels.includes("market") ? all : [];
3844
+ if (target.channels.includes("bbo")) out.push({ key: `${target.symbol.toLowerCase()}@bookTicker`, staleAfterMs: null, params: { symbol: target.symbol } });
3845
+ return out;
3846
+ },
3847
+ user: null,
3848
+ encodeSubscribe: (w, reqId) => JSON.stringify({ method: "SUBSCRIBE", params: [w.key], id: reqId }),
3849
+ encodeUnsubscribe: (w, reqId) => JSON.stringify({ method: "UNSUBSCRIBE", params: [w.key], id: reqId }),
3850
+ decode(text, dir) {
3851
+ const p = parseFrame(text);
3852
+ if (!p.ok) return [p.ev];
3853
+ const f = p.v;
3854
+ if (!isObj(f)) return [unrecognised("aster")];
3855
+ if ("result" in f && typeof f.id === "number") {
3856
+ return f.result === null ? [{ t: "ack", wire: null, reqId: f.id, unsubscribe: false }] : [{ t: "ignore", what: "list-subscriptions" }];
3857
+ }
3858
+ if (isObj(f.error)) {
3859
+ const msg = typeof f.error.msg === "string" ? f.error.msg : JSON.stringify(f.error);
3860
+ const message = `aster error${typeof f.error.code === "number" ? ` ${f.error.code}` : ""}: ${msg}`;
3861
+ if (typeof f.id === "number") return [{ ...protocolError(message), fatal: true, reqId: f.id }];
3862
+ return [protocolError(message)];
3863
+ }
3864
+ if (typeof f.stream !== "string") return [unrecognised("aster")];
3865
+ const d = dir.data;
3866
+ if (f.stream === MARK) return decodeArr(MARK, f.data, d, markPatch);
3867
+ if (f.stream === TICKER) return decodeArr(TICKER, f.data, d, tickerPatch);
3868
+ if (f.stream.endsWith("@bookTicker")) return decodeBookTicker(f.stream, f.data);
3869
+ return [unrecognised("aster")];
3870
+ }
3871
+ };
3872
+ function decodeBookTicker(wire, d) {
3873
+ if (!isObj(d) || d.e !== "bookTicker" || typeof d.s !== "string") return [protocolError(`aster ${wire} frame without bookTicker data`, wire)];
3874
+ const bad = [];
3875
+ const out = [{
3876
+ t: "bbo",
3877
+ wire,
3878
+ symbol: d.s,
3879
+ seq: typeof d.u === "number" ? d.u : null,
3880
+ bid: dec(d, "b", bad),
3881
+ bidSize: dec(d, "B", bad),
3882
+ ask: dec(d, "a", bad),
3883
+ askSize: dec(d, "A", bad),
3884
+ venueTs: ts(d, "E", bad)
3885
+ }];
3886
+ if (bad.length > 0) out.push(badFieldsError("aster", wire, [{ symbol: d.s, fields: bad }]));
3887
+ return out;
3888
+ }
3889
+ function markPatch(r, bad) {
3890
+ if (r.e !== "markPriceUpdate" || typeof r.s !== "string") return null;
3891
+ const venueTs = readTs(r, "E", bad);
3892
+ return compact({
3893
+ symbol: r.s,
3894
+ venueTs: venueTs === void 0 ? null : venueTs,
3895
+ mark: readDec(r, "p", bad),
3896
+ index: readDec(r, "i", bad),
3897
+ rate: readDec(r, "r", bad),
3898
+ nextFundingTime: readTs(r, "T", bad)
3899
+ });
3900
+ }
3901
+ function tickerPatch(r, bad) {
3902
+ if (r.e !== "24hrTicker" || typeof r.s !== "string") return null;
3903
+ const venueTs = readTs(r, "E", bad);
3904
+ return compact({
3905
+ symbol: r.s,
3906
+ venueTs: venueTs === void 0 ? null : venueTs,
3907
+ last: readDec(r, "c", bad),
3908
+ volume24hNotional: readDec(r, "q", bad)
3909
+ });
3910
+ }
3911
+ function decodeArr(wire, data2, d, fn) {
3912
+ if (!Array.isArray(data2)) return [protocolError(`aster ${wire} data is not an array`, wire)];
3913
+ const patches = [];
3914
+ const badRows = [];
3915
+ let unrecognisedRows = 0;
3916
+ for (const r of data2) {
3917
+ if (!isObj(r)) {
3918
+ unrecognisedRows++;
3919
+ continue;
3920
+ }
3921
+ const bad = [];
3922
+ const patch = fn(r, bad);
3923
+ if (patch === null) {
3924
+ unrecognisedRows++;
3925
+ continue;
3926
+ }
3927
+ if (d.unlisted.has(patch.symbol)) continue;
3928
+ patches.push(patch);
3929
+ if (bad.length > 0) badRows.push({ symbol: patch.symbol, fields: bad });
3930
+ }
3931
+ const out = [{ t: "market", wire, patches }];
3932
+ if (unrecognisedRows > 0) out.push(protocolError(`aster ${wire}: ${unrecognisedRows} unrecognised row(s)`, wire));
3933
+ if (badRows.length > 0) out.push(badFieldsError("aster", wire, badRows));
3934
+ return out;
3935
+ }
3936
+
3937
+ // src/ws/codecs/arcus.ts
3938
+ var S4 = VENUE_STATIC.arcus;
3939
+ var MARKETS = "markets";
3940
+ var arcus = {
3941
+ venue: "arcus",
3942
+ capabilities: capabilities({
3943
+ testnet: true,
3944
+ user: true,
3945
+ fields: {
3946
+ mark: true,
3947
+ index: true,
3948
+ bestBid: false,
3949
+ bestAsk: false,
3950
+ last: true,
3951
+ predictedRate: true,
3952
+ nextFundingTime: true,
3953
+ fundingInterval: true,
3954
+ openInterest: true,
3955
+ volume24hNotional: true
3956
+ },
3957
+ notes: {
3958
+ bestBid: "Not in `markets`; use the bbo channel (Phase 2)",
3959
+ bestAsk: "Not in `markets`; use the bbo channel (Phase 2)",
3960
+ openInterest: "Base units, one side (~60 s venue cadence)",
3961
+ user: "By address, no auth (subaccount \u2192 accountIndex). account re-snapshots every 5 s; userFills replays on resubscribe (deduped by tradeId). Streaming `account` order events carry no balances and are not emitted",
3962
+ bbo: "bbo{market}; lastSequenceId orders frames (non-contiguous)",
3963
+ clientId: "orders and fills: clientId (omitted by the venue when the order had none \u2192 null)",
3964
+ liquidationPrice: "not published (WS or REST) \u2192 always null",
3965
+ fee: "fill `fee`; feeToken not stated \u2192 null"
3966
+ }
3967
+ }),
3968
+ url: (n) => S4.wsUrl[n],
3969
+ restUrl: (n) => S4.restUrl[n],
3970
+ secretKeys: [],
3971
+ secretUrlPatterns: [],
3972
+ ping: S4.ping,
3973
+ limits: S4.limits,
3974
+ closesOnBadSubscribe: false,
3975
+ staleAfterMs: S4.staleAfterMs,
3976
+ openInterest: { unit: "base", sides: "one" },
3977
+ refresh: null,
3978
+ async loadDirectory(ctx) {
3979
+ const raw = await fetchJson(ctx.fetch, "arcus", `${ctx.restUrl}/v1/markets`);
3980
+ if (!isObj(raw) || !Array.isArray(raw.markets)) throw badShape("arcus", "GET /v1/markets has no markets array");
3981
+ const entries = [];
3982
+ const idToSymbol = /* @__PURE__ */ new Map();
3983
+ for (const m of raw.markets) {
3984
+ if (!isObj(m) || typeof m.marketDisplayName !== "string" || typeof m.marketId !== "number" || typeof m.baseAsset !== "string") {
3985
+ throw badShape("arcus", "market without marketDisplayName/marketId/baseAsset");
3986
+ }
3987
+ const quote = typeof m.quoteAsset === "string" ? m.quoteAsset : null;
3988
+ idToSymbol.set(String(m.marketId), m.marketDisplayName);
3989
+ entries.push({
3990
+ venue: "arcus",
3991
+ dex: null,
3992
+ symbol: m.marketDisplayName,
3993
+ venueSymbol: m.marketDisplayName,
3994
+ base: m.baseAsset,
3995
+ quote,
3996
+ canonical: ctx.canonicalize("arcus", { venueSymbol: m.marketDisplayName, dex: null, base: m.baseAsset, quote }),
3997
+ rateIntervalHours: 1,
3998
+ settlementIntervalHours: 1,
3999
+ source: "rest:/v1/markets"
4000
+ });
4001
+ }
4002
+ return { entries, data: { idToSymbol } };
4003
+ },
4004
+ fundingMeta: () => ({
4005
+ rateKind: "lastSettled",
4006
+ rateSource: "ws",
4007
+ rateIntervalHours: 1,
4008
+ settlementIntervalHours: 1,
4009
+ intervalSource: "venue-definition",
4010
+ intervalAsOf: null,
4011
+ marketClosed: null
4012
+ }),
4013
+ wiresFor(target, _dir, staleOverrideMs) {
4014
+ const markets = { key: MARKETS, staleAfterMs: staleOverrideMs === null ? S4.staleAfterMs : staleOverrideMs, params: { channel: MARKETS } };
4015
+ if (target.kind === "allMarkets") return [markets];
4016
+ const out = [];
4017
+ if (target.channels.includes("market")) out.push(markets);
4018
+ if (target.channels.includes("bbo")) out.push({ key: `bbo:${target.symbol}`, staleAfterMs: null, params: { channel: "bbo", id: target.symbol } });
4019
+ return out;
4020
+ },
4021
+ user: {
4022
+ inPageSecrets: false,
4023
+ wires(ref, channels) {
4024
+ if (ref.venue !== "arcus") return [];
4025
+ const w = (channel) => ({
4026
+ key: channel,
4027
+ staleAfterMs: null,
4028
+ params: { channel, id: ref.address, ...ref.subaccount === null ? {} : { accountIndex: ref.subaccount } }
4029
+ });
4030
+ const out = [];
4031
+ if (channels.includes("account")) out.push(w("account"));
4032
+ if (channels.includes("positions")) out.push(w("positions"));
4033
+ if (channels.includes("orders")) out.push(w("orders"));
4034
+ if (channels.includes("fills")) out.push(w("userFills"));
4035
+ return out;
4036
+ }
4037
+ },
4038
+ encodeSubscribe: (w) => JSON.stringify({ type: "subscribe", ...w.params }),
4039
+ encodeUnsubscribe: (w) => JSON.stringify({ type: "unsubscribe", ...w.params }),
4040
+ decode(text, dir) {
4041
+ const p = parseFrame(text);
4042
+ if (!p.ok) return [p.ev];
4043
+ const f = p.v;
4044
+ if (!isObj(f) || typeof f.type !== "string") return [unrecognised("arcus")];
4045
+ switch (f.type) {
4046
+ case "connected":
4047
+ return [{ t: "ignore", what: "connected" }];
4048
+ case "pong":
4049
+ return [{ t: "pong" }];
4050
+ case "unsubscribed":
4051
+ return [{ t: "ack", wire: wireOf(f), reqId: null, unsubscribe: true }];
4052
+ case "error":
4053
+ return [protocolError(`arcus error: ${typeof f.message === "string" ? f.message : JSON.stringify(f)}`)];
4054
+ case "subscribed":
4055
+ case "channel_data": {
4056
+ const d = dir.data;
4057
+ switch (f.channel) {
4058
+ case MARKETS:
4059
+ return decodeMarkets(f, d);
4060
+ case "bbo":
4061
+ return decodeBbo3(f);
4062
+ case "account":
4063
+ return decodeAccount2(f);
4064
+ case "positions":
4065
+ return decodePositions3(f, d);
4066
+ case "orders":
4067
+ return decodeOrders3(f, d);
4068
+ case "userFills":
4069
+ return decodeFills2(f, d);
4070
+ default:
4071
+ return [unrecognised("arcus")];
4072
+ }
4073
+ }
4074
+ default:
4075
+ return [unrecognised("arcus")];
4076
+ }
4077
+ }
4078
+ };
4079
+ function decodeMarkets(f, d) {
4080
+ if (!isObj(f.contents) || !isObj(f.contents.markets)) return [protocolError("arcus markets frame without contents.markets", MARKETS)];
4081
+ const patches = [];
4082
+ const badRows = [];
4083
+ for (const [id, m] of Object.entries(f.contents.markets)) {
4084
+ if (!isObj(m)) {
4085
+ badRows.push({ symbol: `#${id}`, fields: ["entry"] });
4086
+ continue;
4087
+ }
4088
+ const known = d.idToSymbol.get(id);
4089
+ const symbol = known !== void 0 ? known : typeof m.marketDisplayName === "string" ? m.marketDisplayName : `#${id}`;
4090
+ const bad = [];
4091
+ patches.push(compact({
4092
+ symbol,
4093
+ venueTs: null,
4094
+ // `markets` carries no frame timestamp
4095
+ mark: readDec(m, "markPrice", bad),
4096
+ index: readDec(m, "oraclePrice", bad),
4097
+ last: readDec(m, "lastTradePrice", bad),
4098
+ rate: readDec(m, "fundingRate", bad),
4099
+ predictedRate: readDec(m, "nextFundingRate", bad),
4100
+ nextFundingTime: readTs(m, "nextFundingAt", bad, 1e3),
4101
+ openInterest: readDec(m, "openInterest", bad),
4102
+ volume24hNotional: readDec(m, "volume24hNotional", bad)
4103
+ }));
4104
+ if (bad.length > 0) badRows.push({ symbol, fields: bad });
4105
+ }
4106
+ const out = [{ t: "market", wire: MARKETS, patches }];
4107
+ if (badRows.length > 0) out.push(badFieldsError("arcus", MARKETS, badRows));
4108
+ return out;
4109
+ }
4110
+ function wireOf(f) {
4111
+ if (f.channel === MARKETS) return MARKETS;
4112
+ if (f.channel === "bbo" && typeof f.id === "string") return `bbo:${f.id}`;
4113
+ if (f.channel === "account" || f.channel === "positions" || f.channel === "orders" || f.channel === "userFills") return f.channel;
4114
+ return null;
4115
+ }
4116
+ var SIDE2 = { BUY: "buy", SELL: "sell" };
4117
+ var POS_SIDE = { LONG: "long", SHORT: "short" };
4118
+ var OPEN_STATUS = /* @__PURE__ */ new Set(["OPEN", "TPSL_PLACED"]);
4119
+ var ZERO2 = /^-?0*(\.0*)?$/;
4120
+ function symbolOf(o, d) {
4121
+ if (typeof o.marketId === "number") {
4122
+ const s = d.idToSymbol.get(String(o.marketId));
4123
+ if (s !== void 0) return s;
4124
+ }
4125
+ if (typeof o.marketDisplayName === "string") return o.marketDisplayName;
4126
+ if (typeof o.market === "string") return o.market;
4127
+ return null;
4128
+ }
4129
+ function decodeBbo3(f) {
4130
+ if (typeof f.id !== "string" || !isObj(f.contents)) return [protocolError("arcus bbo frame without id/contents")];
4131
+ const wire = `bbo:${f.id}`;
4132
+ const c = f.contents;
4133
+ const bad = [];
4134
+ const [bid, bidSize] = level(c.bestBid, bad, "bestBid");
4135
+ const [ask, askSize] = level(c.bestAsk, bad, "bestAsk");
4136
+ const out = [{
4137
+ t: "bbo",
4138
+ wire,
4139
+ symbol: f.id,
4140
+ seq: typeof c.lastSequenceId === "number" ? c.lastSequenceId : null,
4141
+ bid,
4142
+ bidSize,
4143
+ ask,
4144
+ askSize,
4145
+ venueTs: ts(c, "timestamp", bad, 1e-3)
4146
+ // µs
4147
+ }];
4148
+ if (bad.length > 0) out.push(badFieldsError("arcus", wire, [{ symbol: f.id, fields: bad }]));
4149
+ return out;
4150
+ }
4151
+ function decodeAccount2(f) {
4152
+ if (!isObj(f.contents)) return [protocolError("arcus account frame without contents", "account")];
4153
+ const c = f.contents;
4154
+ if (c.isSnapshot !== true) return [{ t: "ignore", what: "account order event" }];
4155
+ if (c.noActivity === true) return [{ t: "ack", wire: "account", reqId: null, unsubscribe: false }];
4156
+ const bad = [];
4157
+ const out = [{
4158
+ t: "user",
4159
+ wire: "account",
4160
+ deltas: [{
4161
+ kind: "account",
4162
+ snapshot: true,
4163
+ dex: null,
4164
+ venueTs: null,
4165
+ equity: dec(c, "accountEquity", bad),
4166
+ available: dec(c, "freeCollateral", bad),
4167
+ marginUsed: null,
4168
+ unrealizedPnl: null
4169
+ }]
4170
+ }];
4171
+ if (bad.length > 0) out.push(badFieldsError("arcus", "account", [{ symbol: "account", fields: bad }]));
4172
+ return out;
4173
+ }
4174
+ function positionRow(p, d, bad) {
4175
+ const symbol = symbolOf(p, d);
4176
+ if (symbol === null) {
4177
+ bad.push("market");
4178
+ return null;
4179
+ }
4180
+ const size = dec(p, "size", bad);
4181
+ return {
4182
+ symbol,
4183
+ dex: null,
4184
+ side: pick2(POS_SIDE, p.side),
4185
+ size: size === null ? null : size.replace(/^-/, ""),
4186
+ entryPrice: dec(p, "averageEntryPrice", bad),
4187
+ markPrice: dec(p, "markPx", bad),
4188
+ positionValue: dec(p, "positionValueNotional", bad),
4189
+ unrealizedPnl: dec(p, "unrealizedPnl", bad),
4190
+ liquidationPrice: null,
4191
+ // Arcus publishes no liquidation price (REST GET /v1/positions has none either)
4192
+ marginUsed: dec(p, "marginUsed", bad),
4193
+ leverage: null,
4194
+ venueTs: null
4195
+ };
4196
+ }
4197
+ function decodePositions3(f, d) {
4198
+ if (!isObj(f.contents)) return [protocolError("arcus positions frame without contents", "positions")];
4199
+ const c = f.contents;
4200
+ const bad = [];
4201
+ let delta;
4202
+ if (c.isSnapshot === true) {
4203
+ if (!isObj(c.positions)) return [protocolError("arcus positions snapshot without positions", "positions")];
4204
+ const rows = Object.values(c.positions).map((p) => isObj(p) ? positionRow(p, d, bad) : null).filter((r) => r !== null);
4205
+ delta = { kind: "positions", snapshot: true, scope: "", rows, remove: [] };
4206
+ } else {
4207
+ const row = positionRow(c, d, bad);
4208
+ if (row === null) return [badFieldsError("arcus", "positions", [{ symbol: "position", fields: bad }])];
4209
+ const flat = c.side === "FLAT" || row.size === null || ZERO2.test(row.size);
4210
+ delta = { kind: "positions", snapshot: false, scope: "", rows: flat ? [] : [row], remove: flat ? [row.symbol] : [] };
4211
+ }
4212
+ const out = [{ t: "user", wire: "positions", deltas: [delta] }];
4213
+ if (bad.length > 0) out.push(badFieldsError("arcus", "positions", [{ symbol: "positions", fields: bad }]));
4214
+ return out;
4215
+ }
4216
+ function orderRow2(o, d, bad) {
4217
+ const id = idOf(o.orderId);
4218
+ const symbol = symbolOf(o, d);
4219
+ if (id === null || symbol === null) {
4220
+ bad.push("orderId/market");
4221
+ return null;
4222
+ }
4223
+ const size = dec(o, "originalSize", bad);
4224
+ return {
4225
+ orderId: id,
4226
+ clientId: idOf(o.clientId),
4227
+ symbol,
4228
+ dex: null,
4229
+ side: pick2(SIDE2, o.side),
4230
+ type: typeof o.type === "string" ? o.type : null,
4231
+ status: typeof o.status === "string" ? o.status : null,
4232
+ price: dec(o, "price", bad),
4233
+ size,
4234
+ filled: dec(o, "filledSize", bad),
4235
+ remaining: dec(o, "remainingSize", bad),
4236
+ reduceOnly: typeof o.reduceOnly === "boolean" ? o.reduceOnly : null,
4237
+ createdAt: ts(o, "createdAt", bad, 1e-3),
4238
+ updatedAt: ts(o, "updatedAt", bad, 1e-3)
4239
+ // µs
4240
+ };
4241
+ }
4242
+ function decodeOrders3(f, d) {
4243
+ if (!isObj(f.contents)) return [protocolError("arcus orders frame without contents", "orders")];
4244
+ const c = f.contents;
4245
+ const bad = [];
4246
+ let delta;
4247
+ if (c.isSnapshot === true) {
4248
+ if (!Array.isArray(c.openOrders)) return [protocolError("arcus orders snapshot without openOrders", "orders")];
4249
+ const rows = c.openOrders.map((o) => isObj(o) ? orderRow2(o, d, bad) : null).filter((r) => r !== null);
4250
+ delta = { kind: "orders", snapshot: true, scope: "", rows, remove: [] };
4251
+ } else {
4252
+ const row = orderRow2(c, d, bad);
4253
+ if (row === null) return [badFieldsError("arcus", "orders", [{ symbol: "order", fields: bad }])];
4254
+ const open = row.status !== null && OPEN_STATUS.has(row.status);
4255
+ delta = { kind: "orders", snapshot: false, scope: "", rows: open ? [row] : [], remove: open ? [] : [row.orderId] };
4256
+ }
4257
+ const out = [{ t: "user", wire: "orders", deltas: [delta] }];
4258
+ if (bad.length > 0) out.push(badFieldsError("arcus", "orders", [{ symbol: "orders", fields: bad }]));
4259
+ return out;
4260
+ }
4261
+ function fillRow(x, d, bad) {
4262
+ const id = idOf(x.tradeId);
4263
+ const symbol = symbolOf(x, d);
4264
+ if (id === null || symbol === null) {
4265
+ bad.push("tradeId/market");
4266
+ return null;
4267
+ }
4268
+ const price = "price" in x ? dec(x, "price", bad) : dec(x, "fillPrice", bad);
4269
+ const size = "size" in x ? dec(x, "size", bad) : dec(x, "fillSize", bad);
4270
+ return {
4271
+ fillId: id,
4272
+ clientId: idOf(x.clientId),
4273
+ symbol,
4274
+ dex: null,
4275
+ side: pick2(SIDE2, x.side),
4276
+ price,
4277
+ size,
4278
+ fee: dec(x, "fee", bad),
4279
+ feeToken: null,
4280
+ closedPnl: dec(x, "closedPnl", bad),
4281
+ orderId: idOf(x.orderId),
4282
+ liquidity: x.role === "MAKER" ? "maker" : x.role === "TAKER" ? "taker" : null,
4283
+ time: ts(x, "createdAt", bad, 1e-3)
4284
+ // µs
4285
+ };
4286
+ }
4287
+ function decodeFills2(f, d) {
4288
+ if (!isObj(f.contents)) return [protocolError("arcus userFills frame without contents", "userFills")];
4289
+ const c = f.contents;
4290
+ const bad = [];
4291
+ const list = c.isSnapshot === true ? Array.isArray(c.fills) ? c.fills : null : [c];
4292
+ if (list === null) return [protocolError("arcus userFills snapshot without fills", "userFills")];
4293
+ const fills = list.map((x) => isObj(x) ? fillRow(x, d, bad) : null).filter((r) => r !== null);
4294
+ const out = [{ t: "user", wire: "userFills", deltas: [{ kind: "fills", snapshot: c.isSnapshot === true, fills }] }];
4295
+ if (bad.length > 0) out.push(badFieldsError("arcus", "userFills", [{ symbol: "fills", fields: bad }]));
4296
+ return out;
4297
+ }
4298
+
4299
+ // src/ws/node.ts
4300
+ var wsModule = null;
4301
+ function isWsNotFound(e) {
4302
+ if (e === null || typeof e !== "object" || e.code !== "MODULE_NOT_FOUND") return false;
4303
+ const first = String(e.message).split("\n")[0];
4304
+ if (!/^Cannot find module 'ws'$/.test(first)) return false;
4305
+ const stack = e.requireStack;
4306
+ return !Array.isArray(stack) || !stack.some((p) => typeof p === "string" && /[\\/]node_modules[\\/]ws[\\/]/.test(p));
4307
+ }
4308
+ function loadWs() {
4309
+ if (wsModule !== null) return wsModule;
4310
+ try {
4311
+ wsModule = createRequire(import.meta.url)("ws");
4312
+ } catch (e) {
4313
+ if (isWsNotFound(e)) {
4314
+ throw new WsError("INVALID_PARAMS", "The /node entry needs the `ws` package (an optional peer dependency), and it is not installed", {
4315
+ remediation: "npm install ws@^8.21.0 \u2014 or, on Node \u2265 22 / in a browser, import the browser entry (global WebSocket) or pass opts.transport"
4316
+ });
4317
+ }
4318
+ const err = new Error(`The /node entry could not load the \`ws\` package: ${e instanceof Error ? e.message : String(e)}`);
4319
+ err.cause = e;
4320
+ throw err;
4321
+ }
4322
+ return wsModule;
4323
+ }
4324
+ var DEFAULT_MAX_PAYLOAD = 16 * 1024 * 1024;
4325
+ function createNodeTransport(opts = {}) {
4326
+ const maxPayload = opts.maxPayload ?? DEFAULT_MAX_PAYLOAD;
4327
+ if (!Number.isSafeInteger(maxPayload) || maxPayload <= 0) {
4328
+ throw new RangeError(`createNodeTransport: maxPayload must be a positive integer (got ${String(opts.maxPayload)})`);
4329
+ }
4330
+ const WebSocket = loadWs();
4331
+ return (url) => {
4332
+ const ws = new WebSocket(url, { maxPayload });
4333
+ ws.on("error", () => void 0);
4334
+ const like = {
4335
+ get readyState() {
4336
+ return ws.readyState;
4337
+ },
4338
+ send: (data2) => ws.send(data2),
4339
+ close: (code, reason) => ws.close(code, reason),
4340
+ onopen: null,
4341
+ onmessage: null,
4342
+ onclose: null,
4343
+ onerror: null
4344
+ };
4345
+ ws.on("open", () => like.onopen?.());
4346
+ ws.on("message", (data2, isBinary) => {
4347
+ const text = isBinary ? null : Array.isArray(data2) ? Buffer.concat(data2).toString("utf8") : Buffer.from(data2).toString("utf8");
4348
+ like.onmessage?.({ data: text === null ? data2 : text });
4349
+ });
4350
+ ws.on("close", (code, reason) => like.onclose?.({ code, reason: reason.toString("utf8"), wasClean: code === 1e3 }));
4351
+ ws.on("error", (err) => like.onerror?.(err));
4352
+ return like;
4353
+ };
4354
+ }
4355
+ var defaultTransport = null;
4356
+ var nodeTransport = (url) => {
4357
+ if (defaultTransport === null) defaultTransport = createNodeTransport();
4358
+ return defaultTransport(url);
4359
+ };
4360
+ function createVenueStream2(codec, opts = {}) {
4361
+ const useDefault = opts.transport === void 0 || opts.transport === nodeTransport;
4362
+ if (useDefault) loadWs();
4363
+ return createVenueStream(codec, opts.transport === void 0 ? { ...opts, transport: nodeTransport } : opts);
4364
+ }
4365
+
4366
+ export { DEFAULT_MAX_PAYLOAD, USD_LIKE_QUOTES, VENUE_IDS, WS_ERROR_CODES, WsError, arcus, aster, createFundingComparator, createNodeTransport, createVenueStream2 as createVenueStream, defaultCanonicalize, hyperliquid, isPerpError, lighter, lighterRh, nodeTransport, pacifica, toApr, toHourly };