pmxt-core 2.44.6 → 2.45.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 (41) hide show
  1. package/dist/exchanges/kalshi/api.d.ts +1 -1
  2. package/dist/exchanges/kalshi/api.js +1 -1
  3. package/dist/exchanges/limitless/api.d.ts +1 -1
  4. package/dist/exchanges/limitless/api.js +1 -1
  5. package/dist/exchanges/mock/index.js +13 -2
  6. package/dist/exchanges/myriad/api.d.ts +1 -1
  7. package/dist/exchanges/myriad/api.js +1 -1
  8. package/dist/exchanges/opinion/api.d.ts +1 -1
  9. package/dist/exchanges/opinion/api.js +1 -1
  10. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  11. package/dist/exchanges/polymarket/api-clob.js +1 -1
  12. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  13. package/dist/exchanges/polymarket/api-data.js +1 -1
  14. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  15. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  16. package/dist/exchanges/polymarket/websocket.d.ts +12 -0
  17. package/dist/exchanges/polymarket/websocket.js +120 -14
  18. package/dist/exchanges/probable/api.d.ts +1 -1
  19. package/dist/exchanges/probable/api.js +1 -1
  20. package/dist/exchanges/suibets/api.d.ts +15 -0
  21. package/dist/exchanges/suibets/api.js +17 -0
  22. package/dist/exchanges/suibets/config.d.ts +16 -0
  23. package/dist/exchanges/suibets/config.js +34 -0
  24. package/dist/exchanges/suibets/errors.d.ts +16 -0
  25. package/dist/exchanges/suibets/errors.js +71 -0
  26. package/dist/exchanges/suibets/fetcher.d.ts +64 -0
  27. package/dist/exchanges/suibets/fetcher.js +128 -0
  28. package/dist/exchanges/suibets/index.d.ts +54 -0
  29. package/dist/exchanges/suibets/index.js +114 -0
  30. package/dist/exchanges/suibets/normalizer.d.ts +8 -0
  31. package/dist/exchanges/suibets/normalizer.js +102 -0
  32. package/dist/exchanges/suibets/utils.d.ts +63 -0
  33. package/dist/exchanges/suibets/utils.js +124 -0
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.js +5 -1
  36. package/dist/router/Router.js +12 -3
  37. package/dist/server/app.js +76 -1
  38. package/dist/server/exchange-factory.js +6 -0
  39. package/dist/server/openapi.yaml +7 -0
  40. package/dist/server/ws-handler.js +196 -23
  41. package/package.json +6 -6
@@ -217,7 +217,10 @@ class Router extends BaseExchange_1.PredictionMarketExchange {
217
217
  }
218
218
  // Lookup mode: find matches for a specific market.
219
219
  const response = await this.client.getMarketMatches(params);
220
- const matches = response.matches ?? [];
220
+ if (!response || !Array.isArray(response.matches)) {
221
+ throw new Error('fetchMarketMatches returned an unexpected response shape: missing matches array');
222
+ }
223
+ const matches = response.matches;
221
224
  return matches.map((m) => ({
222
225
  market: m.market,
223
226
  relation: m.relation,
@@ -262,14 +265,20 @@ class Router extends BaseExchange_1.PredictionMarketExchange {
262
265
  const hasIdentifier = params.eventId || params.slug;
263
266
  if (!hasIdentifier) {
264
267
  const results = await this.client.browseEventMatches(params);
265
- return Array.isArray(results) ? results : [];
268
+ if (!Array.isArray(results)) {
269
+ throw new Error(`browseEventMatches returned unexpected type '${typeof results}'`);
270
+ }
271
+ return results;
266
272
  }
267
273
  if (await this.resolveLocalMockEventLookup(params)) {
268
274
  return [];
269
275
  }
270
276
  // Lookup mode: find matches for a specific event.
271
277
  const response = await this.client.getEventMatches(params);
272
- return response.matches ?? [];
278
+ if (!response || !Array.isArray(response.matches)) {
279
+ throw new Error('fetchEventMatches returned an unexpected response shape: missing matches array');
280
+ }
281
+ return response.matches;
273
282
  }
274
283
  // -----------------------------------------------------------------------
275
284
  // Price comparison: identity matches with live prices
@@ -134,6 +134,80 @@ function queryToArgs(query, spec) {
134
134
  }
135
135
  return args;
136
136
  }
137
+ function isRecord(value) {
138
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
139
+ }
140
+ function toDateParam(value, field, exchange) {
141
+ if (value instanceof Date) {
142
+ if (Number.isNaN(value.getTime())) {
143
+ throw new errors_1.ValidationError(`${field} must be a valid date-time value.`, field, exchange);
144
+ }
145
+ return value;
146
+ }
147
+ if (typeof value === "string" || typeof value === "number") {
148
+ const parsed = new Date(value);
149
+ if (!Number.isNaN(parsed.getTime())) {
150
+ return parsed;
151
+ }
152
+ }
153
+ throw new errors_1.ValidationError(`${field} must be a valid date-time string or timestamp.`, field, exchange);
154
+ }
155
+ function normalizeDateFields(params, fields, exchange) {
156
+ const normalized = { ...params };
157
+ for (const field of fields) {
158
+ const value = normalized[field];
159
+ if (value === undefined || value === null || value === "")
160
+ continue;
161
+ normalized[field] = toDateParam(value, field, exchange);
162
+ }
163
+ return normalized;
164
+ }
165
+ function isMissing(value) {
166
+ return value === undefined || value === null || value === "";
167
+ }
168
+ function assertFiniteNumber(params, field, exchange) {
169
+ const value = params[field];
170
+ if (typeof value !== "number" || !Number.isFinite(value)) {
171
+ throw new errors_1.ValidationError(`createOrder params.${field} must be a finite number.`, field, exchange);
172
+ }
173
+ }
174
+ function validateCreateOrderParams(params, exchange) {
175
+ if (!isRecord(params)) {
176
+ throw new errors_1.ValidationError("createOrder requires an order parameter object.", "params", exchange);
177
+ }
178
+ const required = ["marketId", "outcomeId", "side", "type", "amount"];
179
+ const missing = required.filter((field) => isMissing(params[field]));
180
+ if (missing.length > 0) {
181
+ throw new errors_1.ValidationError(`createOrder requires ${missing.map((field) => `params.${field}`).join(", ")}.`, missing[0], exchange);
182
+ }
183
+ if (params.side !== "buy" && params.side !== "sell") {
184
+ throw new errors_1.ValidationError('createOrder params.side must be "buy" or "sell".', "side", exchange);
185
+ }
186
+ if (params.type !== "market" && params.type !== "limit") {
187
+ throw new errors_1.ValidationError('createOrder params.type must be "market" or "limit".', "type", exchange);
188
+ }
189
+ assertFiniteNumber(params, "amount", exchange);
190
+ if (params.type === "limit" && isMissing(params.price)) {
191
+ throw new errors_1.ValidationError("createOrder params.price is required for limit orders.", "price", exchange);
192
+ }
193
+ if (!isMissing(params.price)) {
194
+ assertFiniteNumber(params, "price", exchange);
195
+ }
196
+ if (!isMissing(params.fee)) {
197
+ assertFiniteNumber(params, "fee", exchange);
198
+ }
199
+ return params;
200
+ }
201
+ function normalizeDispatchArgs(methodName, args, exchange) {
202
+ const normalized = [...args];
203
+ if (methodName === "fetchOHLCV" && isRecord(normalized[1])) {
204
+ normalized[1] = normalizeDateFields(normalized[1], ["start", "end"], exchange);
205
+ }
206
+ if (methodName === "createOrder") {
207
+ normalized[0] = validateCreateOrderParams(normalized[0], exchange);
208
+ }
209
+ return normalized;
210
+ }
137
211
  // Singleton instances for local usage (when no credentials provided)
138
212
  const defaultExchanges = {
139
213
  polymarket: null,
@@ -275,7 +349,8 @@ function createApp(options = {}) {
275
349
  });
276
350
  return;
277
351
  }
278
- const result = await exchange[methodName](...args);
352
+ const normalizedArgs = normalizeDispatchArgs(methodName, args, exchangeName);
353
+ const result = await exchange[methodName](...normalizedArgs);
279
354
  res.json({ success: true, data: result });
280
355
  }
281
356
  catch (error) {
@@ -14,6 +14,7 @@ const smarkets_1 = require("../exchanges/smarkets");
14
14
  const polymarket_us_1 = require("../exchanges/polymarket_us");
15
15
  const hyperliquid_1 = require("../exchanges/hyperliquid");
16
16
  const gemini_titan_1 = require("../exchanges/gemini-titan");
17
+ const suibets_1 = require("../exchanges/suibets");
17
18
  const mock_1 = require("../exchanges/mock");
18
19
  const router_1 = require("../router");
19
20
  function createExchange(name, credentials, bearerToken) {
@@ -103,6 +104,11 @@ function createExchange(name, credentials, bearerToken) {
103
104
  apiKey: credentials?.apiKey || process.env.GEMINI_API_KEY,
104
105
  apiSecret: credentials?.apiSecret || process.env.GEMINI_API_SECRET,
105
106
  });
107
+ case "suibets":
108
+ return new suibets_1.SuiBetsExchange({
109
+ walletAddress: credentials?.walletAddress || process.env.SUIBETS_WALLET_ADDRESS,
110
+ baseUrl: credentials?.baseUrl || process.env.SUIBETS_BASE_URL,
111
+ });
106
112
  case "mock":
107
113
  return new mock_1.MockExchange();
108
114
  case "router":
@@ -4159,6 +4159,13 @@ x-sdk-constructors:
4159
4159
  tsName: apiSecret
4160
4160
  type: string
4161
4161
  description: API secret for authentication
4162
+ suibets:
4163
+ className: Suibets
4164
+ params:
4165
+ - name: pmxt_api_key
4166
+ tsName: pmxtApiKey
4167
+ type: string
4168
+ description: PMXT API key for hosted access
4162
4169
  mock:
4163
4170
  className: Mock
4164
4171
  params:
@@ -4,6 +4,7 @@ exports.createWebSocketHandler = createWebSocketHandler;
4
4
  const ws_1 = require("ws");
5
5
  const errors_1 = require("../errors");
6
6
  const exchange_factory_1 = require("./exchange-factory");
7
+ const feed_factory_1 = require("./feed-factory");
7
8
  const logger_1 = require("../utils/logger");
8
9
  // ---------------------------------------------------------------------------
9
10
  // Streaming methods
@@ -14,6 +15,10 @@ const WATCH_METHODS = new Set([
14
15
  "watchOrderBooks",
15
16
  "watchTrades",
16
17
  ]);
18
+ /** Data-feed methods that produce streaming data. */
19
+ const FEED_WATCH_METHODS = new Set([
20
+ "watchTicker",
21
+ ]);
17
22
  /** Methods for unsubscribing. */
18
23
  const UNWATCH_METHODS = {
19
24
  unwatchOrderBook: "watchOrderBook",
@@ -30,7 +35,10 @@ function sendError(ws, id, message, code) {
30
35
  * Build a unique key for a subscription so we can cancel it.
31
36
  */
32
37
  function subscriptionKey(msg) {
33
- return `${msg.exchange}:${msg.method}:${JSON.stringify(msg.args)}`;
38
+ const target = msg.targetKind === "feed"
39
+ ? `feed:${msg.feed}`
40
+ : `exchange:${msg.exchange}`;
41
+ return `${target}:${msg.method}:${JSON.stringify(msg.args)}`;
34
42
  }
35
43
  /**
36
44
  * Start a streaming loop for a single-ticker watch method
@@ -101,6 +109,78 @@ async function streamBatch(exchange, method, args, id, ws, signal) {
101
109
  }
102
110
  }
103
111
  }
112
+ /**
113
+ * Start a streaming loop for feed watch methods.
114
+ *
115
+ * Data feeds expose callback-driven watch methods rather than the exchange
116
+ * layer's await-next-update methods, so lifecycle is tied to the returned
117
+ * unsubscribe callback and the client's AbortSignal.
118
+ */
119
+ async function streamFeedSingle(feed, feedName, method, args, id, ws, signal) {
120
+ const symbol = typeof args[0] === "string" ? args[0] : String(args[0] ?? "");
121
+ let unsubscribe;
122
+ let settled = false;
123
+ let resolveDone = () => { };
124
+ const done = new Promise((resolve) => {
125
+ resolveDone = resolve;
126
+ });
127
+ const cleanup = () => {
128
+ if (settled)
129
+ return;
130
+ settled = true;
131
+ signal.removeEventListener("abort", cleanup);
132
+ ws.removeListener("close", cleanup);
133
+ if (unsubscribe) {
134
+ try {
135
+ unsubscribe();
136
+ }
137
+ catch (err) {
138
+ logger_1.logger.warn('ws-handler: feed unsubscribe failed', {
139
+ feed: feedName,
140
+ method,
141
+ id,
142
+ error: err instanceof Error ? err.message : String(err),
143
+ });
144
+ }
145
+ }
146
+ resolveDone();
147
+ };
148
+ signal.addEventListener("abort", cleanup, { once: true });
149
+ ws.once("close", cleanup);
150
+ try {
151
+ if (typeof feed.connect === "function") {
152
+ await feed.connect();
153
+ }
154
+ if (signal.aborted || ws.readyState !== ws_1.WebSocket.OPEN) {
155
+ cleanup();
156
+ return;
157
+ }
158
+ if (method !== "watchTicker" || typeof feed.watchTicker !== "function") {
159
+ sendError(ws, id, `Method '${method}' not found on ${feedName}`);
160
+ cleanup();
161
+ return;
162
+ }
163
+ unsubscribe = feed.watchTicker(symbol, (data) => {
164
+ if (!signal.aborted && ws.readyState === ws_1.WebSocket.OPEN) {
165
+ send(ws, { id, event: "data", method, symbol, source: feedName, data });
166
+ }
167
+ });
168
+ send(ws, { event: "subscribed", id });
169
+ await done;
170
+ }
171
+ catch (err) {
172
+ if (!signal.aborted) {
173
+ const message = err instanceof errors_1.BaseError
174
+ ? err.message
175
+ : err instanceof Error
176
+ ? err.message
177
+ : "Unknown streaming error";
178
+ const code = err instanceof errors_1.BaseError ? err.code : undefined;
179
+ sendError(ws, id, message, code);
180
+ }
181
+ cleanup();
182
+ }
183
+ }
104
184
  // ---------------------------------------------------------------------------
105
185
  // Public API
106
186
  // ---------------------------------------------------------------------------
@@ -158,32 +238,75 @@ function createWebSocketHandler(options = {}) {
158
238
  const id = parsed.id;
159
239
  const action = parsed.action;
160
240
  const exchange = parsed.exchange;
241
+ const feed = parsed.feed;
161
242
  const method = parsed.method;
162
- if (!id || !action || !exchange || !method) {
163
- sendError(ws, id, "Missing required fields: id, action, exchange, method");
243
+ if (!id || !action) {
244
+ sendError(ws, id, "Missing required fields: id, action");
245
+ return;
246
+ }
247
+ if (action === "unsubscribe" && !method && !exchange && !feed) {
248
+ handleUnsubscribeById(ws, state, id);
249
+ return;
250
+ }
251
+ if (!method) {
252
+ sendError(ws, id, "Missing required field: method");
253
+ return;
254
+ }
255
+ if ((exchange && feed) || (!exchange && !feed)) {
256
+ sendError(ws, id, "Missing required target: provide exactly one of exchange or feed");
164
257
  return;
165
258
  }
166
- const exchangeName = exchange.toLowerCase();
167
259
  if (action === "subscribe") {
168
- const msg = {
169
- id,
170
- action: "subscribe",
171
- exchange: exchangeName,
172
- method,
173
- args: parsed.args || [],
174
- credentials: parsed.credentials,
175
- };
176
- handleSubscribe(ws, state, msg, exchangeName);
260
+ if (feed) {
261
+ const feedName = feed.toLowerCase();
262
+ const msg = {
263
+ id,
264
+ action: "subscribe",
265
+ targetKind: "feed",
266
+ feed: feedName,
267
+ method,
268
+ args: parsed.args || [],
269
+ };
270
+ handleFeedSubscribe(ws, state, msg, feedName);
271
+ }
272
+ else {
273
+ const exchangeName = exchange.toLowerCase();
274
+ const msg = {
275
+ id,
276
+ action: "subscribe",
277
+ targetKind: "exchange",
278
+ exchange: exchangeName,
279
+ method,
280
+ args: parsed.args || [],
281
+ credentials: parsed.credentials,
282
+ };
283
+ handleSubscribe(ws, state, msg, exchangeName);
284
+ }
177
285
  }
178
286
  else if (action === "unsubscribe") {
179
- const msg = {
180
- id,
181
- action: "unsubscribe",
182
- exchange: exchangeName,
183
- method,
184
- args: parsed.args || [],
185
- };
186
- handleUnsubscribe(ws, state, msg, exchangeName);
287
+ if (feed) {
288
+ const msg = {
289
+ id,
290
+ action: "unsubscribe",
291
+ targetKind: "feed",
292
+ feed: feed.toLowerCase(),
293
+ method,
294
+ args: parsed.args || [],
295
+ };
296
+ handleUnsubscribe(ws, state, msg);
297
+ }
298
+ else {
299
+ const exchangeName = exchange.toLowerCase();
300
+ const msg = {
301
+ id,
302
+ action: "unsubscribe",
303
+ targetKind: "exchange",
304
+ exchange: exchangeName,
305
+ method,
306
+ args: parsed.args || [],
307
+ };
308
+ handleUnsubscribe(ws, state, msg);
309
+ }
187
310
  }
188
311
  else {
189
312
  sendError(ws, id, `Unknown action: ${action}`);
@@ -257,7 +380,7 @@ function handleSubscribe(ws, state, msg, exchangeName) {
257
380
  return;
258
381
  }
259
382
  const abortController = new AbortController();
260
- state.subscriptions.set(key, { abortController });
383
+ state.subscriptions.set(key, { id, abortController });
261
384
  const streamFn = method === "watchOrderBooks" ? streamBatch : streamSingle;
262
385
  // Fire and forget -- the loop runs until aborted or WS closes
263
386
  streamFn(exchange, method, args || [], id, ws, abortController.signal).catch((err) => {
@@ -270,10 +393,45 @@ function handleSubscribe(ws, state, msg, exchangeName) {
270
393
  error: err instanceof Error ? err.message : String(err),
271
394
  });
272
395
  sendError(ws, id, err instanceof Error ? err.message : 'Streaming error');
396
+ }).finally(() => {
273
397
  state.subscriptions.delete(key);
274
398
  });
275
399
  }
276
- function handleUnsubscribe(ws, state, msg, exchangeName) {
400
+ function handleFeedSubscribe(ws, state, msg, feedName) {
401
+ const { id, method, args } = msg;
402
+ if (!FEED_WATCH_METHODS.has(method)) {
403
+ sendError(ws, id, `Method '${method}' is not a feed streaming method. Use the HTTP API for non-streaming calls.`);
404
+ return;
405
+ }
406
+ const key = subscriptionKey(msg);
407
+ if (state.subscriptions.has(key)) {
408
+ sendError(ws, id, `Already subscribed to ${key}`);
409
+ return;
410
+ }
411
+ let feed;
412
+ try {
413
+ feed = (0, feed_factory_1.getFeed)(feedName);
414
+ }
415
+ catch (err) {
416
+ const message = err instanceof Error ? err.message : "Failed to create feed";
417
+ sendError(ws, id, message);
418
+ return;
419
+ }
420
+ const abortController = new AbortController();
421
+ state.subscriptions.set(key, { id, abortController });
422
+ streamFeedSingle(feed, feedName, method, args || [], id, ws, abortController.signal).catch((err) => {
423
+ logger_1.logger.warn('ws-handler: feed stream ended with unexpected error', {
424
+ feed: feedName,
425
+ method,
426
+ id,
427
+ error: err instanceof Error ? err.message : String(err),
428
+ });
429
+ sendError(ws, id, err instanceof Error ? err.message : 'Streaming error');
430
+ }).finally(() => {
431
+ state.subscriptions.delete(key);
432
+ });
433
+ }
434
+ function handleUnsubscribe(ws, state, msg) {
277
435
  const { id } = msg;
278
436
  // Build the key of the subscription to cancel.
279
437
  // For unsubscribe actions, the corresponding subscribe key uses the watch method.
@@ -289,3 +447,18 @@ function handleUnsubscribe(ws, state, msg, exchangeName) {
289
447
  state.subscriptions.delete(key);
290
448
  send(ws, { event: "subscribed", id }); // Acknowledge unsubscribe
291
449
  }
450
+ function handleUnsubscribeById(ws, state, id) {
451
+ let cancelled = false;
452
+ for (const [key, sub] of state.subscriptions) {
453
+ if (sub.id === id) {
454
+ sub.abortController.abort();
455
+ state.subscriptions.delete(key);
456
+ cancelled = true;
457
+ }
458
+ }
459
+ if (!cancelled) {
460
+ sendError(ws, id, `No active subscription for id ${id}`);
461
+ return;
462
+ }
463
+ send(ws, { event: "subscribed", id });
464
+ }
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "pmxt-core",
3
- "version": "2.44.6",
3
+ "version": "2.45.0",
4
4
  "description": "pmxt is a unified prediction market data API. The ccxt for prediction markets.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/pmxt-dev/pmxt.git"
9
+ "url": "git+https://github.com/pmxt-dev/pmxt.git"
10
10
  },
11
11
  "bin": {
12
- "pmxt-server": "./dist/server/index.js",
13
- "pmxt-ensure-server": "./bin/pmxt-ensure-server"
12
+ "pmxt-server": "dist/server/index.js",
13
+ "pmxt-ensure-server": "bin/pmxt-ensure-server"
14
14
  },
15
15
  "files": [
16
16
  "dist",
@@ -29,8 +29,8 @@
29
29
  "test": "jest -c jest.config.js",
30
30
  "server": "tsx watch src/server/index.ts",
31
31
  "server:prod": "node dist/server/index.js",
32
- "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=2.44.6,library=urllib3",
33
- "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=2.44.6,supportsES6=true,typescriptThreePlus=true && node ../sdks/typescript/scripts/fix-generated.js",
32
+ "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=2.45.0,library=urllib3",
33
+ "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=2.45.0,supportsES6=true,typescriptThreePlus=true && node ../sdks/typescript/scripts/fix-generated.js",
34
34
  "fetch:openapi": "node scripts/fetch-openapi-specs.js",
35
35
  "extract:jsdoc": "node ../scripts/extract-jsdoc.js",
36
36
  "generate:docs": "npm run extract:jsdoc && node ../scripts/generate-api-docs.js",