pmxt-core 2.35.30 → 2.35.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BaseExchange.d.ts +13 -0
- package/dist/BaseExchange.js +27 -2
- package/dist/exchanges/kalshi/api.d.ts +1 -1
- package/dist/exchanges/kalshi/api.js +1 -1
- package/dist/exchanges/kalshi/index.d.ts +1 -0
- package/dist/exchanges/kalshi/index.js +12 -0
- package/dist/exchanges/kalshi/websocket.d.ts +1 -0
- package/dist/exchanges/kalshi/websocket.js +36 -2
- package/dist/exchanges/limitless/api.d.ts +1 -1
- package/dist/exchanges/limitless/api.js +1 -1
- package/dist/exchanges/myriad/api.d.ts +1 -1
- package/dist/exchanges/myriad/api.js +1 -1
- package/dist/exchanges/opinion/api.d.ts +1 -1
- package/dist/exchanges/opinion/api.js +1 -1
- package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
- package/dist/exchanges/polymarket/api-clob.js +1 -1
- package/dist/exchanges/polymarket/api-data.d.ts +1 -1
- package/dist/exchanges/polymarket/api-data.js +1 -1
- package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
- package/dist/exchanges/polymarket/api-gamma.js +1 -1
- package/dist/exchanges/probable/api.d.ts +1 -1
- package/dist/exchanges/probable/api.js +1 -1
- package/dist/server/app.d.ts +6 -0
- package/dist/server/app.js +15 -101
- package/dist/server/exchange-factory.d.ts +2 -0
- package/dist/server/exchange-factory.js +100 -0
- package/dist/server/method-verbs.json +15 -0
- package/dist/server/openapi.yaml +45 -0
- package/dist/server/ws-handler.d.ts +20 -0
- package/dist/server/ws-handler.js +268 -0
- package/package.json +3 -3
package/dist/BaseExchange.d.ts
CHANGED
|
@@ -217,6 +217,8 @@ export interface ExchangeHas {
|
|
|
217
217
|
unwatchAddress: ExchangeCapability;
|
|
218
218
|
/** Whether this exchange supports streaming order book updates. */
|
|
219
219
|
watchOrderBook: ExchangeCapability;
|
|
220
|
+
/** Whether this exchange supports batch-subscribing to multiple order book streams. */
|
|
221
|
+
watchOrderBooks: ExchangeCapability;
|
|
220
222
|
/** Whether this exchange supports unsubscribing from an order book stream. */
|
|
221
223
|
unwatchOrderBook: ExchangeCapability;
|
|
222
224
|
/** Whether this exchange supports streaming trade updates. */
|
|
@@ -574,6 +576,17 @@ export declare abstract class PredictionMarketExchange {
|
|
|
574
576
|
* @returns Promise that resolves with the current orderbook state
|
|
575
577
|
*/
|
|
576
578
|
watchOrderBook(id: string, limit?: number): Promise<OrderBook>;
|
|
579
|
+
/**
|
|
580
|
+
* Watch multiple order books simultaneously via WebSocket.
|
|
581
|
+
* Returns a promise that resolves with a record of order book snapshots keyed by ID.
|
|
582
|
+
* Exchanges with native batch support (e.g. Kalshi) send a single subscribe message
|
|
583
|
+
* for all tickers; others fall back to individual watchOrderBook calls.
|
|
584
|
+
*
|
|
585
|
+
* @param ids - Array of Outcome IDs to watch
|
|
586
|
+
* @param limit - Optional limit for orderbook depth
|
|
587
|
+
* @returns Promise that resolves with order books keyed by ID
|
|
588
|
+
*/
|
|
589
|
+
watchOrderBooks(ids: string[], limit?: number): Promise<Record<string, OrderBook>>;
|
|
577
590
|
/**
|
|
578
591
|
* Unsubscribe from a previously watched order book stream.
|
|
579
592
|
*
|
package/dist/BaseExchange.js
CHANGED
|
@@ -764,6 +764,30 @@ class PredictionMarketExchange {
|
|
|
764
764
|
async watchOrderBook(id, limit) {
|
|
765
765
|
throw new Error(`watchOrderBook() is not supported by ${this.name}`);
|
|
766
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Watch multiple order books simultaneously via WebSocket.
|
|
769
|
+
* Returns a promise that resolves with a record of order book snapshots keyed by ID.
|
|
770
|
+
* Exchanges with native batch support (e.g. Kalshi) send a single subscribe message
|
|
771
|
+
* for all tickers; others fall back to individual watchOrderBook calls.
|
|
772
|
+
*
|
|
773
|
+
* @param ids - Array of Outcome IDs to watch
|
|
774
|
+
* @param limit - Optional limit for orderbook depth
|
|
775
|
+
* @returns Promise that resolves with order books keyed by ID
|
|
776
|
+
*/
|
|
777
|
+
async watchOrderBooks(ids, limit) {
|
|
778
|
+
// Default implementation: subscribe to each ID individually.
|
|
779
|
+
// Exchanges with native batch support (e.g. Kalshi) override this
|
|
780
|
+
// to send a single subscribe message for all tickers.
|
|
781
|
+
const entries = await Promise.all(ids.map(async (id) => {
|
|
782
|
+
const book = await this.watchOrderBook(id, limit);
|
|
783
|
+
return [id, book];
|
|
784
|
+
}));
|
|
785
|
+
const result = {};
|
|
786
|
+
for (const [id, book] of entries) {
|
|
787
|
+
result[id] = book;
|
|
788
|
+
}
|
|
789
|
+
return result;
|
|
790
|
+
}
|
|
767
791
|
/**
|
|
768
792
|
* Unsubscribe from a previously watched order book stream.
|
|
769
793
|
*
|
|
@@ -1033,7 +1057,7 @@ class PredictionMarketExchange {
|
|
|
1033
1057
|
'fetchMarkets', 'fetchEvents', 'fetchOHLCV', 'fetchOrderBook',
|
|
1034
1058
|
'fetchTrades', 'createOrder', 'cancelOrder', 'fetchOrder',
|
|
1035
1059
|
'fetchOpenOrders', 'fetchPositions', 'fetchBalance',
|
|
1036
|
-
'watchAddress', 'unwatchAddress', 'watchOrderBook',
|
|
1060
|
+
'watchAddress', 'unwatchAddress', 'watchOrderBook', 'watchOrderBooks',
|
|
1037
1061
|
'unwatchOrderBook', 'watchTrades', 'fetchMyTrades',
|
|
1038
1062
|
'fetchClosedOrders', 'fetchAllOrders', 'buildOrder', 'submitOrder',
|
|
1039
1063
|
'fetchMarketMatches', 'fetchMatches', 'fetchEventMatches', 'compareMarketPrices',
|
|
@@ -1046,7 +1070,7 @@ class PredictionMarketExchange {
|
|
|
1046
1070
|
fetchOrderBook: true, fetchTrades: true, createOrder: true,
|
|
1047
1071
|
cancelOrder: true, fetchOrder: true, fetchOpenOrders: true,
|
|
1048
1072
|
fetchPositions: true, fetchBalance: true, watchAddress: true,
|
|
1049
|
-
unwatchAddress: true, watchOrderBook: true, unwatchOrderBook: true,
|
|
1073
|
+
unwatchAddress: true, watchOrderBook: true, watchOrderBooks: true, unwatchOrderBook: true,
|
|
1050
1074
|
watchTrades: true, fetchMyTrades: true, fetchClosedOrders: true,
|
|
1051
1075
|
fetchAllOrders: true, buildOrder: true, submitOrder: true,
|
|
1052
1076
|
fetchMarketMatches: true, fetchMatches: true, fetchEventMatches: true, compareMarketPrices: true,
|
|
@@ -1061,6 +1085,7 @@ class PredictionMarketExchange {
|
|
|
1061
1085
|
static _capabilityDelegates = {
|
|
1062
1086
|
fetchMarkets: 'fetchMarketsImpl',
|
|
1063
1087
|
fetchEvents: 'fetchEventsImpl',
|
|
1088
|
+
watchOrderBooks: 'watchOrderBook',
|
|
1064
1089
|
fetchMatches: 'fetchMarketMatches',
|
|
1065
1090
|
fetchHedges: 'fetchRelatedMarkets',
|
|
1066
1091
|
fetchMatchedPrices: 'fetchMatchedMarkets',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/kalshi/Kalshi.yaml
|
|
3
|
-
* Generated at: 2026-
|
|
3
|
+
* Generated at: 2026-05-01T12:04:30.287Z
|
|
4
4
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
5
5
|
*/
|
|
6
6
|
export declare const kalshiApiSpec: {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.kalshiApiSpec = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/kalshi/Kalshi.yaml
|
|
6
|
-
* Generated at: 2026-
|
|
6
|
+
* Generated at: 2026-05-01T12:04:30.287Z
|
|
7
7
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
8
8
|
*/
|
|
9
9
|
exports.kalshiApiSpec = {
|
|
@@ -39,6 +39,7 @@ export declare class KalshiExchange extends PredictionMarketExchange {
|
|
|
39
39
|
fetchOpenOrders(marketId?: string): Promise<Order[]>;
|
|
40
40
|
private ws?;
|
|
41
41
|
watchOrderBook(id: string, limit?: number): Promise<OrderBook>;
|
|
42
|
+
watchOrderBooks(ids: string[], limit?: number): Promise<Record<string, OrderBook>>;
|
|
42
43
|
watchTrades(id: string, address?: string, since?: number, limit?: number): Promise<Trade[]>;
|
|
43
44
|
close(): Promise<void>;
|
|
44
45
|
}
|
|
@@ -263,6 +263,18 @@ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
|
|
|
263
263
|
const marketTicker = id.replace(/-NO$/, "");
|
|
264
264
|
return this.ws.watchOrderBook(marketTicker);
|
|
265
265
|
}
|
|
266
|
+
async watchOrderBooks(ids, limit) {
|
|
267
|
+
const auth = this.ensureAuth();
|
|
268
|
+
if (!this.ws) {
|
|
269
|
+
const wsConfigWithUrl = {
|
|
270
|
+
...this.wsConfig,
|
|
271
|
+
wsUrl: this.wsConfig?.wsUrl || this.config.wsUrl,
|
|
272
|
+
};
|
|
273
|
+
this.ws = new websocket_1.KalshiWebSocket(auth, wsConfigWithUrl);
|
|
274
|
+
}
|
|
275
|
+
const marketTickers = ids.map((id) => id.replace(/-NO$/, ""));
|
|
276
|
+
return this.ws.watchOrderBooks(marketTickers);
|
|
277
|
+
}
|
|
266
278
|
async watchTrades(id, address, since, limit) {
|
|
267
279
|
const auth = this.ensureAuth();
|
|
268
280
|
if (!this.ws) {
|
|
@@ -38,6 +38,7 @@ export declare class KalshiWebSocket {
|
|
|
38
38
|
private handleTrade;
|
|
39
39
|
private resolveOrderBook;
|
|
40
40
|
watchOrderBook(ticker: string): Promise<OrderBook>;
|
|
41
|
+
watchOrderBooks(tickers: string[]): Promise<Record<string, OrderBook>>;
|
|
41
42
|
watchTrades(ticker: string): Promise<Trade[]>;
|
|
42
43
|
close(): Promise<void>;
|
|
43
44
|
}
|
|
@@ -324,7 +324,7 @@ class KalshiWebSocket {
|
|
|
324
324
|
// Subscribe if not already subscribed
|
|
325
325
|
if (!this.subscribedOrderBookTickers.has(ticker)) {
|
|
326
326
|
this.subscribedOrderBookTickers.add(ticker);
|
|
327
|
-
this.subscribeToOrderbook(
|
|
327
|
+
this.subscribeToOrderbook([ticker]);
|
|
328
328
|
}
|
|
329
329
|
// Return a promise that resolves on the next orderbook update
|
|
330
330
|
return new Promise((resolve, reject) => {
|
|
@@ -334,6 +334,40 @@ class KalshiWebSocket {
|
|
|
334
334
|
this.orderBookResolvers.get(ticker).push({ resolve, reject });
|
|
335
335
|
});
|
|
336
336
|
}
|
|
337
|
+
async watchOrderBooks(tickers) {
|
|
338
|
+
// Ensure connection
|
|
339
|
+
if (!this.isConnected) {
|
|
340
|
+
await this.connect();
|
|
341
|
+
}
|
|
342
|
+
// Determine which tickers are actually new (not yet subscribed)
|
|
343
|
+
const newTickers = tickers.filter((t) => !this.subscribedOrderBookTickers.has(t));
|
|
344
|
+
// Add all new tickers to the subscription set
|
|
345
|
+
for (const t of newTickers) {
|
|
346
|
+
this.subscribedOrderBookTickers.add(t);
|
|
347
|
+
}
|
|
348
|
+
// Send ONE subscribe message with only the new tickers
|
|
349
|
+
if (newTickers.length > 0) {
|
|
350
|
+
this.subscribeToOrderbook(newTickers);
|
|
351
|
+
}
|
|
352
|
+
// Wait for all tickers to receive at least one snapshot/update
|
|
353
|
+
const entries = await Promise.all(tickers.map((ticker) => new Promise((resolve, reject) => {
|
|
354
|
+
if (!this.orderBookResolvers.has(ticker)) {
|
|
355
|
+
this.orderBookResolvers.set(ticker, []);
|
|
356
|
+
}
|
|
357
|
+
this.orderBookResolvers.get(ticker).push({
|
|
358
|
+
resolve: (book) => {
|
|
359
|
+
// Unwrap PromiseLike if needed, but in practice it is always OrderBook
|
|
360
|
+
Promise.resolve(book).then((b) => resolve([ticker, b]));
|
|
361
|
+
},
|
|
362
|
+
reject,
|
|
363
|
+
});
|
|
364
|
+
})));
|
|
365
|
+
const result = {};
|
|
366
|
+
for (const [ticker, book] of entries) {
|
|
367
|
+
result[ticker] = book;
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
337
371
|
async watchTrades(ticker) {
|
|
338
372
|
// Ensure connection
|
|
339
373
|
if (!this.isConnected) {
|
|
@@ -342,7 +376,7 @@ class KalshiWebSocket {
|
|
|
342
376
|
// Subscribe if not already subscribed
|
|
343
377
|
if (!this.subscribedTradeTickers.has(ticker)) {
|
|
344
378
|
this.subscribedTradeTickers.add(ticker);
|
|
345
|
-
this.subscribeToTrades(
|
|
379
|
+
this.subscribeToTrades([ticker]);
|
|
346
380
|
}
|
|
347
381
|
// Return a promise that resolves on the next trade
|
|
348
382
|
return new Promise((resolve, reject) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/limitless/Limitless.yaml
|
|
3
|
-
* Generated at: 2026-
|
|
3
|
+
* Generated at: 2026-05-01T12:04:30.324Z
|
|
4
4
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
5
5
|
*/
|
|
6
6
|
export declare const limitlessApiSpec: {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.limitlessApiSpec = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/limitless/Limitless.yaml
|
|
6
|
-
* Generated at: 2026-
|
|
6
|
+
* Generated at: 2026-05-01T12:04:30.324Z
|
|
7
7
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
8
8
|
*/
|
|
9
9
|
exports.limitlessApiSpec = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/myriad/myriad.yaml
|
|
3
|
-
* Generated at: 2026-
|
|
3
|
+
* Generated at: 2026-05-01T12:04:30.336Z
|
|
4
4
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
5
5
|
*/
|
|
6
6
|
export declare const myriadApiSpec: {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.myriadApiSpec = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/myriad/myriad.yaml
|
|
6
|
-
* Generated at: 2026-
|
|
6
|
+
* Generated at: 2026-05-01T12:04:30.336Z
|
|
7
7
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
8
8
|
*/
|
|
9
9
|
exports.myriadApiSpec = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/opinion/opinion-openapi.yaml
|
|
3
|
-
* Generated at: 2026-
|
|
3
|
+
* Generated at: 2026-05-01T12:04:30.340Z
|
|
4
4
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
5
5
|
*/
|
|
6
6
|
export declare const opinionApiSpec: {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.opinionApiSpec = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/opinion/opinion-openapi.yaml
|
|
6
|
-
* Generated at: 2026-
|
|
6
|
+
* Generated at: 2026-05-01T12:04:30.340Z
|
|
7
7
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
8
8
|
*/
|
|
9
9
|
exports.opinionApiSpec = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/PolymarketClobAPI.yaml
|
|
3
|
-
* Generated at: 2026-
|
|
3
|
+
* Generated at: 2026-05-01T12:04:30.294Z
|
|
4
4
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
5
5
|
*/
|
|
6
6
|
export declare const polymarketClobSpec: {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.polymarketClobSpec = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/PolymarketClobAPI.yaml
|
|
6
|
-
* Generated at: 2026-
|
|
6
|
+
* Generated at: 2026-05-01T12:04:30.294Z
|
|
7
7
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
8
8
|
*/
|
|
9
9
|
exports.polymarketClobSpec = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/Polymarket_Data_API.yaml
|
|
3
|
-
* Generated at: 2026-
|
|
3
|
+
* Generated at: 2026-05-01T12:04:30.307Z
|
|
4
4
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
5
5
|
*/
|
|
6
6
|
export declare const polymarketDataSpec: {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.polymarketDataSpec = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/Polymarket_Data_API.yaml
|
|
6
|
-
* Generated at: 2026-
|
|
6
|
+
* Generated at: 2026-05-01T12:04:30.307Z
|
|
7
7
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
8
8
|
*/
|
|
9
9
|
exports.polymarketDataSpec = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/PolymarketGammaAPI.yaml
|
|
3
|
-
* Generated at: 2026-
|
|
3
|
+
* Generated at: 2026-05-01T12:04:30.304Z
|
|
4
4
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
5
5
|
*/
|
|
6
6
|
export declare const polymarketGammaSpec: {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.polymarketGammaSpec = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/PolymarketGammaAPI.yaml
|
|
6
|
-
* Generated at: 2026-
|
|
6
|
+
* Generated at: 2026-05-01T12:04:30.304Z
|
|
7
7
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
8
8
|
*/
|
|
9
9
|
exports.polymarketGammaSpec = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/probable/probable.yaml
|
|
3
|
-
* Generated at: 2026-
|
|
3
|
+
* Generated at: 2026-05-01T12:04:30.329Z
|
|
4
4
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
5
5
|
*/
|
|
6
6
|
export declare const probableApiSpec: {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.probableApiSpec = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/probable/probable.yaml
|
|
6
|
-
* Generated at: 2026-
|
|
6
|
+
* Generated at: 2026-05-01T12:04:30.329Z
|
|
7
7
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
8
8
|
*/
|
|
9
9
|
exports.probableApiSpec = {
|
package/dist/server/app.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Express } from "express";
|
|
2
|
+
import { createWebSocketHandler, CreateWebSocketHandlerOptions } from "./ws-handler";
|
|
2
3
|
/**
|
|
3
4
|
* Options accepted by {@link createApp}.
|
|
4
5
|
*/
|
|
@@ -54,5 +55,10 @@ export declare function createApp(options?: CreateAppOptions): Express;
|
|
|
54
55
|
* Start the PMXT sidecar server on the given port with the built-in
|
|
55
56
|
* access-token auth middleware enabled. Returns the underlying
|
|
56
57
|
* {@link http.Server} once it is listening.
|
|
58
|
+
*
|
|
59
|
+
* Automatically attaches a WebSocket endpoint at `/ws` for streaming
|
|
60
|
+
* methods (watchOrderBook, watchOrderBooks, watchTrades).
|
|
57
61
|
*/
|
|
58
62
|
export declare function startServer(port: number, accessToken: string): Promise<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>>;
|
|
63
|
+
export { createWebSocketHandler };
|
|
64
|
+
export type { CreateWebSocketHandlerOptions };
|
package/dist/server/app.js
CHANGED
|
@@ -3,24 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createWebSocketHandler = void 0;
|
|
6
7
|
exports.createApp = createApp;
|
|
7
8
|
exports.startServer = startServer;
|
|
8
9
|
const express_1 = __importDefault(require("express"));
|
|
9
10
|
const cors_1 = __importDefault(require("cors"));
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const kalshi_demo_1 = require("../exchanges/kalshi-demo");
|
|
16
|
-
const probable_1 = require("../exchanges/probable");
|
|
17
|
-
const baozi_1 = require("../exchanges/baozi");
|
|
18
|
-
const myriad_1 = require("../exchanges/myriad");
|
|
19
|
-
const opinion_1 = require("../exchanges/opinion");
|
|
20
|
-
const metaculus_1 = require("../exchanges/metaculus");
|
|
21
|
-
const smarkets_1 = require("../exchanges/smarkets");
|
|
22
|
-
const polymarket_us_1 = require("../exchanges/polymarket_us");
|
|
23
|
-
const router_1 = require("../router");
|
|
13
|
+
const ws_handler_1 = require("./ws-handler");
|
|
14
|
+
Object.defineProperty(exports, "createWebSocketHandler", { enumerable: true, get: function () { return ws_handler_1.createWebSocketHandler; } });
|
|
15
|
+
const exchange_factory_1 = require("./exchange-factory");
|
|
24
16
|
const errors_1 = require("../errors");
|
|
25
17
|
function loadMethodVerbs() {
|
|
26
18
|
const candidates = [
|
|
@@ -220,17 +212,17 @@ function createApp(options = {}) {
|
|
|
220
212
|
// calls — not a server-side env var. Each request may carry a
|
|
221
213
|
// different key, so Router is never cached as a singleton.
|
|
222
214
|
const bearer = req.headers.authorization?.replace(/^Bearer\s+/i, "") || "";
|
|
223
|
-
exchange = createExchange(exchangeName, undefined, bearer);
|
|
215
|
+
exchange = (0, exchange_factory_1.createExchange)(exchangeName, undefined, bearer);
|
|
224
216
|
}
|
|
225
217
|
else if (credentials &&
|
|
226
218
|
(credentials.privateKey ||
|
|
227
219
|
credentials.apiKey ||
|
|
228
220
|
credentials.apiToken)) {
|
|
229
|
-
exchange = createExchange(exchangeName, credentials);
|
|
221
|
+
exchange = (0, exchange_factory_1.createExchange)(exchangeName, credentials);
|
|
230
222
|
}
|
|
231
223
|
else {
|
|
232
224
|
if (!defaultExchanges[exchangeName]) {
|
|
233
|
-
defaultExchanges[exchangeName] = createExchange(exchangeName);
|
|
225
|
+
defaultExchanges[exchangeName] = (0, exchange_factory_1.createExchange)(exchangeName);
|
|
234
226
|
}
|
|
235
227
|
exchange = defaultExchanges[exchangeName];
|
|
236
228
|
}
|
|
@@ -336,93 +328,15 @@ function createApp(options = {}) {
|
|
|
336
328
|
* Start the PMXT sidecar server on the given port with the built-in
|
|
337
329
|
* access-token auth middleware enabled. Returns the underlying
|
|
338
330
|
* {@link http.Server} once it is listening.
|
|
331
|
+
*
|
|
332
|
+
* Automatically attaches a WebSocket endpoint at `/ws` for streaming
|
|
333
|
+
* methods (watchOrderBook, watchOrderBooks, watchTrades).
|
|
339
334
|
*/
|
|
340
335
|
async function startServer(port, accessToken) {
|
|
341
336
|
const app = createApp({ accessToken });
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return new polymarket_1.PolymarketExchange({
|
|
348
|
-
privateKey: credentials?.privateKey ||
|
|
349
|
-
process.env.POLYMARKET_PK ||
|
|
350
|
-
process.env.POLYMARKET_PRIVATE_KEY,
|
|
351
|
-
apiKey: credentials?.apiKey || process.env.POLYMARKET_API_KEY,
|
|
352
|
-
apiSecret: credentials?.apiSecret || process.env.POLYMARKET_API_SECRET,
|
|
353
|
-
passphrase: credentials?.passphrase || process.env.POLYMARKET_PASSPHRASE,
|
|
354
|
-
funderAddress: credentials?.funderAddress ||
|
|
355
|
-
process.env.POLYMARKET_FUNDER_ADDRESS ||
|
|
356
|
-
process.env.POLYMARKET_PROXY_ADDRESS,
|
|
357
|
-
signatureType: credentials?.signatureType ||
|
|
358
|
-
process.env.POLYMARKET_SIGNATURE_TYPE,
|
|
359
|
-
});
|
|
360
|
-
case "limitless":
|
|
361
|
-
return new limitless_1.LimitlessExchange({
|
|
362
|
-
privateKey: credentials?.privateKey ||
|
|
363
|
-
process.env.LIMITLESS_PK ||
|
|
364
|
-
process.env.LIMITLESS_PRIVATE_KEY,
|
|
365
|
-
apiKey: credentials?.apiKey || process.env.LIMITLESS_API_KEY,
|
|
366
|
-
apiSecret: credentials?.apiSecret || process.env.LIMITLESS_API_SECRET,
|
|
367
|
-
passphrase: credentials?.passphrase || process.env.LIMITLESS_PASSPHRASE,
|
|
368
|
-
});
|
|
369
|
-
case "kalshi":
|
|
370
|
-
return new kalshi_1.KalshiExchange({
|
|
371
|
-
credentials: {
|
|
372
|
-
apiKey: credentials?.apiKey || process.env.KALSHI_API_KEY,
|
|
373
|
-
privateKey: credentials?.privateKey || process.env.KALSHI_PRIVATE_KEY,
|
|
374
|
-
},
|
|
375
|
-
});
|
|
376
|
-
case "kalshi-demo":
|
|
377
|
-
return new kalshi_demo_1.KalshiDemoExchange({
|
|
378
|
-
credentials: {
|
|
379
|
-
apiKey: credentials?.apiKey || process.env.KALSHI_API_KEY,
|
|
380
|
-
privateKey: credentials?.privateKey || process.env.KALSHI_PRIVATE_KEY,
|
|
381
|
-
},
|
|
382
|
-
});
|
|
383
|
-
case "probable":
|
|
384
|
-
return new probable_1.ProbableExchange({
|
|
385
|
-
apiKey: credentials?.apiKey || process.env.PROBABLE_API_KEY,
|
|
386
|
-
apiSecret: credentials?.apiSecret || process.env.PROBABLE_API_SECRET,
|
|
387
|
-
passphrase: credentials?.passphrase || process.env.PROBABLE_PASSPHRASE,
|
|
388
|
-
privateKey: credentials?.privateKey || process.env.PROBABLE_PRIVATE_KEY,
|
|
389
|
-
});
|
|
390
|
-
case "baozi":
|
|
391
|
-
return new baozi_1.BaoziExchange({
|
|
392
|
-
privateKey: credentials?.privateKey || process.env.BAOZI_PRIVATE_KEY,
|
|
393
|
-
});
|
|
394
|
-
case "myriad":
|
|
395
|
-
return new myriad_1.MyriadExchange({
|
|
396
|
-
apiKey: credentials?.apiKey ||
|
|
397
|
-
process.env.MYRIAD_API_KEY ||
|
|
398
|
-
process.env.MYRIAD_PROD,
|
|
399
|
-
privateKey: credentials?.privateKey || process.env.MYRIAD_WALLET_ADDRESS,
|
|
400
|
-
});
|
|
401
|
-
case "opinion":
|
|
402
|
-
return new opinion_1.OpinionExchange({
|
|
403
|
-
apiKey: credentials?.apiKey || process.env.OPINION_API_KEY,
|
|
404
|
-
privateKey: credentials?.privateKey || process.env.OPINION_PRIVATE_KEY,
|
|
405
|
-
funderAddress: credentials?.funderAddress,
|
|
406
|
-
});
|
|
407
|
-
case "metaculus":
|
|
408
|
-
return new metaculus_1.MetaculusExchange({
|
|
409
|
-
apiToken: credentials?.apiToken || process.env.METACULUS_API_TOKEN,
|
|
410
|
-
});
|
|
411
|
-
case "smarkets":
|
|
412
|
-
return new smarkets_1.SmarketsExchange({
|
|
413
|
-
apiKey: credentials?.apiKey || process.env.SMARKETS_EMAIL,
|
|
414
|
-
privateKey: credentials?.privateKey || process.env.SMARKETS_PASSWORD,
|
|
415
|
-
});
|
|
416
|
-
case "polymarket_us":
|
|
417
|
-
return new polymarket_us_1.PolymarketUSExchange({
|
|
418
|
-
apiKey: credentials?.apiKey || process.env.POLYMARKET_US_KEY_ID,
|
|
419
|
-
privateKey: credentials?.privateKey || process.env.POLYMARKET_US_SECRET_KEY,
|
|
420
|
-
});
|
|
421
|
-
case "router":
|
|
422
|
-
return new router_1.Router({
|
|
423
|
-
apiKey: bearerToken,
|
|
424
|
-
});
|
|
425
|
-
default:
|
|
426
|
-
throw new Error(`Unknown exchange: ${name}`);
|
|
427
|
-
}
|
|
337
|
+
const server = app.listen(port, "127.0.0.1");
|
|
338
|
+
// Attach WebSocket handler for streaming subscriptions
|
|
339
|
+
const wsHandler = (0, ws_handler_1.createWebSocketHandler)({ accessToken });
|
|
340
|
+
wsHandler.attach(server);
|
|
341
|
+
return server;
|
|
428
342
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createExchange = createExchange;
|
|
4
|
+
const polymarket_1 = require("../exchanges/polymarket");
|
|
5
|
+
const limitless_1 = require("../exchanges/limitless");
|
|
6
|
+
const kalshi_1 = require("../exchanges/kalshi");
|
|
7
|
+
const kalshi_demo_1 = require("../exchanges/kalshi-demo");
|
|
8
|
+
const probable_1 = require("../exchanges/probable");
|
|
9
|
+
const baozi_1 = require("../exchanges/baozi");
|
|
10
|
+
const myriad_1 = require("../exchanges/myriad");
|
|
11
|
+
const opinion_1 = require("../exchanges/opinion");
|
|
12
|
+
const metaculus_1 = require("../exchanges/metaculus");
|
|
13
|
+
const smarkets_1 = require("../exchanges/smarkets");
|
|
14
|
+
const polymarket_us_1 = require("../exchanges/polymarket_us");
|
|
15
|
+
const router_1 = require("../router");
|
|
16
|
+
function createExchange(name, credentials, bearerToken) {
|
|
17
|
+
switch (name) {
|
|
18
|
+
case "polymarket":
|
|
19
|
+
return new polymarket_1.PolymarketExchange({
|
|
20
|
+
privateKey: credentials?.privateKey ||
|
|
21
|
+
process.env.POLYMARKET_PK ||
|
|
22
|
+
process.env.POLYMARKET_PRIVATE_KEY,
|
|
23
|
+
apiKey: credentials?.apiKey || process.env.POLYMARKET_API_KEY,
|
|
24
|
+
apiSecret: credentials?.apiSecret || process.env.POLYMARKET_API_SECRET,
|
|
25
|
+
passphrase: credentials?.passphrase || process.env.POLYMARKET_PASSPHRASE,
|
|
26
|
+
funderAddress: credentials?.funderAddress ||
|
|
27
|
+
process.env.POLYMARKET_FUNDER_ADDRESS ||
|
|
28
|
+
process.env.POLYMARKET_PROXY_ADDRESS,
|
|
29
|
+
signatureType: credentials?.signatureType ||
|
|
30
|
+
process.env.POLYMARKET_SIGNATURE_TYPE,
|
|
31
|
+
});
|
|
32
|
+
case "limitless":
|
|
33
|
+
return new limitless_1.LimitlessExchange({
|
|
34
|
+
privateKey: credentials?.privateKey ||
|
|
35
|
+
process.env.LIMITLESS_PK ||
|
|
36
|
+
process.env.LIMITLESS_PRIVATE_KEY,
|
|
37
|
+
apiKey: credentials?.apiKey || process.env.LIMITLESS_API_KEY,
|
|
38
|
+
apiSecret: credentials?.apiSecret || process.env.LIMITLESS_API_SECRET,
|
|
39
|
+
passphrase: credentials?.passphrase || process.env.LIMITLESS_PASSPHRASE,
|
|
40
|
+
});
|
|
41
|
+
case "kalshi":
|
|
42
|
+
return new kalshi_1.KalshiExchange({
|
|
43
|
+
credentials: {
|
|
44
|
+
apiKey: credentials?.apiKey || process.env.KALSHI_API_KEY,
|
|
45
|
+
privateKey: credentials?.privateKey || process.env.KALSHI_PRIVATE_KEY,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
case "kalshi-demo":
|
|
49
|
+
return new kalshi_demo_1.KalshiDemoExchange({
|
|
50
|
+
credentials: {
|
|
51
|
+
apiKey: credentials?.apiKey || process.env.KALSHI_API_KEY,
|
|
52
|
+
privateKey: credentials?.privateKey || process.env.KALSHI_PRIVATE_KEY,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
case "probable":
|
|
56
|
+
return new probable_1.ProbableExchange({
|
|
57
|
+
apiKey: credentials?.apiKey || process.env.PROBABLE_API_KEY,
|
|
58
|
+
apiSecret: credentials?.apiSecret || process.env.PROBABLE_API_SECRET,
|
|
59
|
+
passphrase: credentials?.passphrase || process.env.PROBABLE_PASSPHRASE,
|
|
60
|
+
privateKey: credentials?.privateKey || process.env.PROBABLE_PRIVATE_KEY,
|
|
61
|
+
});
|
|
62
|
+
case "baozi":
|
|
63
|
+
return new baozi_1.BaoziExchange({
|
|
64
|
+
privateKey: credentials?.privateKey || process.env.BAOZI_PRIVATE_KEY,
|
|
65
|
+
});
|
|
66
|
+
case "myriad":
|
|
67
|
+
return new myriad_1.MyriadExchange({
|
|
68
|
+
apiKey: credentials?.apiKey ||
|
|
69
|
+
process.env.MYRIAD_API_KEY ||
|
|
70
|
+
process.env.MYRIAD_PROD,
|
|
71
|
+
privateKey: credentials?.privateKey || process.env.MYRIAD_WALLET_ADDRESS,
|
|
72
|
+
});
|
|
73
|
+
case "opinion":
|
|
74
|
+
return new opinion_1.OpinionExchange({
|
|
75
|
+
apiKey: credentials?.apiKey || process.env.OPINION_API_KEY,
|
|
76
|
+
privateKey: credentials?.privateKey || process.env.OPINION_PRIVATE_KEY,
|
|
77
|
+
funderAddress: credentials?.funderAddress,
|
|
78
|
+
});
|
|
79
|
+
case "metaculus":
|
|
80
|
+
return new metaculus_1.MetaculusExchange({
|
|
81
|
+
apiToken: credentials?.apiToken || process.env.METACULUS_API_TOKEN,
|
|
82
|
+
});
|
|
83
|
+
case "smarkets":
|
|
84
|
+
return new smarkets_1.SmarketsExchange({
|
|
85
|
+
apiKey: credentials?.apiKey || process.env.SMARKETS_EMAIL,
|
|
86
|
+
privateKey: credentials?.privateKey || process.env.SMARKETS_PASSWORD,
|
|
87
|
+
});
|
|
88
|
+
case "polymarket_us":
|
|
89
|
+
return new polymarket_us_1.PolymarketUSExchange({
|
|
90
|
+
apiKey: credentials?.apiKey || process.env.POLYMARKET_US_KEY_ID,
|
|
91
|
+
privateKey: credentials?.privateKey || process.env.POLYMARKET_US_SECRET_KEY,
|
|
92
|
+
});
|
|
93
|
+
case "router":
|
|
94
|
+
return new router_1.Router({
|
|
95
|
+
apiKey: bearerToken,
|
|
96
|
+
});
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`Unknown exchange: ${name}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -304,6 +304,21 @@
|
|
|
304
304
|
}
|
|
305
305
|
]
|
|
306
306
|
},
|
|
307
|
+
"watchOrderBooks": {
|
|
308
|
+
"verb": "post",
|
|
309
|
+
"args": [
|
|
310
|
+
{
|
|
311
|
+
"name": "ids",
|
|
312
|
+
"kind": "unknown",
|
|
313
|
+
"optional": false
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "limit",
|
|
317
|
+
"kind": "number",
|
|
318
|
+
"optional": true
|
|
319
|
+
}
|
|
320
|
+
]
|
|
321
|
+
},
|
|
307
322
|
"unwatchOrderBook": {
|
|
308
323
|
"verb": "post",
|
|
309
324
|
"args": [
|
package/dist/server/openapi.yaml
CHANGED
|
@@ -1329,6 +1329,51 @@ paths:
|
|
|
1329
1329
|
description: >-
|
|
1330
1330
|
Watch order book updates in real-time via WebSocket. Returns a promise that resolves with the next order book
|
|
1331
1331
|
update. Call repeatedly in a loop to stream updates (CCXT Pro pattern).
|
|
1332
|
+
'/api/{exchange}/watchOrderBooks':
|
|
1333
|
+
post:
|
|
1334
|
+
summary: Watch Order Books
|
|
1335
|
+
operationId: watchOrderBooks
|
|
1336
|
+
parameters:
|
|
1337
|
+
- $ref: '#/components/parameters/ExchangeParam'
|
|
1338
|
+
requestBody:
|
|
1339
|
+
content:
|
|
1340
|
+
application/json:
|
|
1341
|
+
schema:
|
|
1342
|
+
title: WatchOrderBooksRequest
|
|
1343
|
+
type: object
|
|
1344
|
+
properties:
|
|
1345
|
+
args:
|
|
1346
|
+
type: array
|
|
1347
|
+
minItems: 1
|
|
1348
|
+
maxItems: 2
|
|
1349
|
+
items:
|
|
1350
|
+
oneOf:
|
|
1351
|
+
- type: array
|
|
1352
|
+
items:
|
|
1353
|
+
type: string
|
|
1354
|
+
- type: number
|
|
1355
|
+
credentials:
|
|
1356
|
+
$ref: '#/components/schemas/ExchangeCredentials'
|
|
1357
|
+
required:
|
|
1358
|
+
- args
|
|
1359
|
+
responses:
|
|
1360
|
+
'200':
|
|
1361
|
+
description: Watch Order Books response
|
|
1362
|
+
content:
|
|
1363
|
+
application/json:
|
|
1364
|
+
schema:
|
|
1365
|
+
allOf:
|
|
1366
|
+
- $ref: '#/components/schemas/BaseResponse'
|
|
1367
|
+
- type: object
|
|
1368
|
+
properties:
|
|
1369
|
+
data:
|
|
1370
|
+
type: object
|
|
1371
|
+
additionalProperties:
|
|
1372
|
+
$ref: '#/components/schemas/OrderBook'
|
|
1373
|
+
description: >-
|
|
1374
|
+
Watch multiple order books simultaneously via WebSocket. Returns a promise that resolves with a record of order
|
|
1375
|
+
book snapshots keyed by ID. Exchanges with native batch support (e.g. Kalshi) send a single subscribe message
|
|
1376
|
+
for all tickers; others fall back to individual watchOrderBook calls.
|
|
1332
1377
|
'/api/{exchange}/unwatchOrderBook':
|
|
1333
1378
|
post:
|
|
1334
1379
|
summary: Unwatch Order Book
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import { Server as HttpServer } from "http";
|
|
3
|
+
export interface CreateWebSocketHandlerOptions {
|
|
4
|
+
/** Access token for authentication (same as x-pmxt-access-token). */
|
|
5
|
+
accessToken?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Create a WebSocket handler that can be attached to an HTTP server.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```ts
|
|
12
|
+
* const server = app.listen(port);
|
|
13
|
+
* const wss = createWebSocketHandler({ accessToken });
|
|
14
|
+
* wss.attach(server);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function createWebSocketHandler(options?: CreateWebSocketHandlerOptions): {
|
|
18
|
+
wss: WebSocketServer;
|
|
19
|
+
attach: (server: HttpServer) => void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWebSocketHandler = createWebSocketHandler;
|
|
4
|
+
const ws_1 = require("ws");
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
const exchange_factory_1 = require("./exchange-factory");
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Streaming methods
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/** Set of method names that produce streaming data. */
|
|
11
|
+
const WATCH_METHODS = new Set([
|
|
12
|
+
"watchOrderBook",
|
|
13
|
+
"watchOrderBooks",
|
|
14
|
+
"watchTrades",
|
|
15
|
+
]);
|
|
16
|
+
/** Methods for unsubscribing. */
|
|
17
|
+
const UNWATCH_METHODS = {
|
|
18
|
+
unwatchOrderBook: "watchOrderBook",
|
|
19
|
+
};
|
|
20
|
+
function send(ws, msg) {
|
|
21
|
+
if (ws.readyState === ws_1.WebSocket.OPEN) {
|
|
22
|
+
ws.send(JSON.stringify(msg));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function sendError(ws, id, message, code) {
|
|
26
|
+
send(ws, { event: "error", id, error: { message, code } });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a unique key for a subscription so we can cancel it.
|
|
30
|
+
*/
|
|
31
|
+
function subscriptionKey(msg) {
|
|
32
|
+
return `${msg.exchange}:${msg.method}:${JSON.stringify(msg.args)}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Start a streaming loop for a single-ticker watch method
|
|
36
|
+
* (watchOrderBook, watchTrades).
|
|
37
|
+
*/
|
|
38
|
+
async function streamSingle(exchange, method, args, id, ws, signal) {
|
|
39
|
+
const symbol = typeof args[0] === "string" ? args[0] : String(args[0]);
|
|
40
|
+
// Send subscribed acknowledgement
|
|
41
|
+
send(ws, { event: "subscribed", id });
|
|
42
|
+
while (!signal.aborted && ws.readyState === ws_1.WebSocket.OPEN) {
|
|
43
|
+
try {
|
|
44
|
+
const result = await exchange[method](...args);
|
|
45
|
+
if (signal.aborted)
|
|
46
|
+
break;
|
|
47
|
+
send(ws, { id, event: "data", method, symbol, data: result });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (signal.aborted)
|
|
51
|
+
break;
|
|
52
|
+
const message = err instanceof errors_1.BaseError
|
|
53
|
+
? err.message
|
|
54
|
+
: err instanceof Error
|
|
55
|
+
? err.message
|
|
56
|
+
: "Unknown streaming error";
|
|
57
|
+
const code = err instanceof errors_1.BaseError ? err.code : undefined;
|
|
58
|
+
sendError(ws, id, message, code);
|
|
59
|
+
// Brief pause before retry to avoid tight error loops
|
|
60
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Start a streaming loop for the batch watchOrderBooks method.
|
|
66
|
+
* Each update is sent as an individual data message per symbol.
|
|
67
|
+
*/
|
|
68
|
+
async function streamBatch(exchange, method, args, id, ws, signal) {
|
|
69
|
+
const ids = Array.isArray(args[0]) ? args[0] : [];
|
|
70
|
+
send(ws, { event: "subscribed", id });
|
|
71
|
+
while (!signal.aborted && ws.readyState === ws_1.WebSocket.OPEN) {
|
|
72
|
+
try {
|
|
73
|
+
const result = await exchange[method](...args);
|
|
74
|
+
if (signal.aborted)
|
|
75
|
+
break;
|
|
76
|
+
for (const [symbol, data] of Object.entries(result)) {
|
|
77
|
+
send(ws, { id, event: "data", method, symbol, data });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (signal.aborted)
|
|
82
|
+
break;
|
|
83
|
+
const message = err instanceof errors_1.BaseError
|
|
84
|
+
? err.message
|
|
85
|
+
: err instanceof Error
|
|
86
|
+
? err.message
|
|
87
|
+
: "Unknown streaming error";
|
|
88
|
+
const code = err instanceof errors_1.BaseError ? err.code : undefined;
|
|
89
|
+
sendError(ws, id, message, code);
|
|
90
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Public API
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
/**
|
|
98
|
+
* Create a WebSocket handler that can be attached to an HTTP server.
|
|
99
|
+
*
|
|
100
|
+
* Usage:
|
|
101
|
+
* ```ts
|
|
102
|
+
* const server = app.listen(port);
|
|
103
|
+
* const wss = createWebSocketHandler({ accessToken });
|
|
104
|
+
* wss.attach(server);
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
function createWebSocketHandler(options = {}) {
|
|
108
|
+
const { accessToken } = options;
|
|
109
|
+
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
110
|
+
function attach(server) {
|
|
111
|
+
server.on("upgrade", (request, socket, head) => {
|
|
112
|
+
const url = new URL(request.url || "/", `http://${request.headers.host}`);
|
|
113
|
+
// Only handle upgrades to /ws
|
|
114
|
+
if (url.pathname !== "/ws") {
|
|
115
|
+
socket.destroy();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Validate access token from query parameter if configured
|
|
119
|
+
if (accessToken) {
|
|
120
|
+
const token = url.searchParams.get("token") ||
|
|
121
|
+
request.headers["x-pmxt-access-token"];
|
|
122
|
+
if (!token || token !== accessToken) {
|
|
123
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
124
|
+
socket.destroy();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
129
|
+
wss.emit("connection", ws, request);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
wss.on("connection", (ws) => {
|
|
134
|
+
const state = {
|
|
135
|
+
subscriptions: new Map(),
|
|
136
|
+
exchanges: new Map(),
|
|
137
|
+
authenticated: !accessToken, // pre-authed if no token required
|
|
138
|
+
};
|
|
139
|
+
ws.on("message", (raw) => {
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
sendError(ws, undefined, "Invalid JSON");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const id = parsed.id;
|
|
149
|
+
const action = parsed.action;
|
|
150
|
+
const exchange = parsed.exchange;
|
|
151
|
+
if (!id || !action || !exchange) {
|
|
152
|
+
sendError(ws, id, "Missing required fields: id, action, exchange");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const exchangeName = exchange.toLowerCase();
|
|
156
|
+
if (action === "subscribe") {
|
|
157
|
+
const msg = {
|
|
158
|
+
id,
|
|
159
|
+
action: "subscribe",
|
|
160
|
+
exchange: exchangeName,
|
|
161
|
+
method: parsed.method,
|
|
162
|
+
args: parsed.args || [],
|
|
163
|
+
credentials: parsed.credentials,
|
|
164
|
+
};
|
|
165
|
+
handleSubscribe(ws, state, msg, exchangeName);
|
|
166
|
+
}
|
|
167
|
+
else if (action === "unsubscribe") {
|
|
168
|
+
const msg = {
|
|
169
|
+
id,
|
|
170
|
+
action: "unsubscribe",
|
|
171
|
+
exchange: exchangeName,
|
|
172
|
+
method: parsed.method,
|
|
173
|
+
args: parsed.args || [],
|
|
174
|
+
};
|
|
175
|
+
handleUnsubscribe(ws, state, msg, exchangeName);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
sendError(ws, id, `Unknown action: ${action}`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
ws.on("close", () => {
|
|
182
|
+
// Abort all active subscriptions
|
|
183
|
+
for (const [, sub] of state.subscriptions) {
|
|
184
|
+
sub.abortController.abort();
|
|
185
|
+
}
|
|
186
|
+
state.subscriptions.clear();
|
|
187
|
+
// Close all exchange instances
|
|
188
|
+
for (const [, exchange] of state.exchanges) {
|
|
189
|
+
if (typeof exchange.close === "function") {
|
|
190
|
+
exchange.close().catch(() => { });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
state.exchanges.clear();
|
|
194
|
+
});
|
|
195
|
+
ws.on("error", () => {
|
|
196
|
+
// Abort subscriptions on error too
|
|
197
|
+
for (const [, sub] of state.subscriptions) {
|
|
198
|
+
sub.abortController.abort();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
return { wss, attach };
|
|
203
|
+
}
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Handlers
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
function getOrCreateExchange(state, exchangeName, credentials) {
|
|
208
|
+
// Use credentials as part of the cache key to support multiple auths
|
|
209
|
+
const cacheKey = credentials && (credentials.apiKey || credentials.privateKey)
|
|
210
|
+
? `${exchangeName}:${credentials.apiKey || ""}:${credentials.privateKey || ""}`
|
|
211
|
+
: exchangeName;
|
|
212
|
+
const existing = state.exchanges.get(cacheKey);
|
|
213
|
+
if (existing)
|
|
214
|
+
return existing;
|
|
215
|
+
const exchange = (0, exchange_factory_1.createExchange)(exchangeName, credentials);
|
|
216
|
+
state.exchanges.set(cacheKey, exchange);
|
|
217
|
+
return exchange;
|
|
218
|
+
}
|
|
219
|
+
function handleSubscribe(ws, state, msg, exchangeName) {
|
|
220
|
+
const { id, method, args, credentials } = msg;
|
|
221
|
+
if (!WATCH_METHODS.has(method)) {
|
|
222
|
+
sendError(ws, id, `Method '${method}' is not a streaming method. Use the HTTP API for non-streaming calls.`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const key = subscriptionKey(msg);
|
|
226
|
+
// Already subscribed
|
|
227
|
+
if (state.subscriptions.has(key)) {
|
|
228
|
+
sendError(ws, id, `Already subscribed to ${key}`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
let exchange;
|
|
232
|
+
try {
|
|
233
|
+
exchange = getOrCreateExchange(state, exchangeName, credentials);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
const message = err instanceof Error ? err.message : "Failed to create exchange";
|
|
237
|
+
sendError(ws, id, message);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (typeof exchange[method] !== "function") {
|
|
241
|
+
sendError(ws, id, `Method '${method}' not found on ${exchangeName}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const abortController = new AbortController();
|
|
245
|
+
state.subscriptions.set(key, { abortController });
|
|
246
|
+
const streamFn = method === "watchOrderBooks" ? streamBatch : streamSingle;
|
|
247
|
+
// Fire and forget -- the loop runs until aborted or WS closes
|
|
248
|
+
streamFn(exchange, method, args || [], id, ws, abortController.signal).catch(() => {
|
|
249
|
+
// Stream ended -- clean up
|
|
250
|
+
state.subscriptions.delete(key);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function handleUnsubscribe(ws, state, msg, exchangeName) {
|
|
254
|
+
const { id } = msg;
|
|
255
|
+
// Build the key of the subscription to cancel.
|
|
256
|
+
// For unsubscribe actions, the corresponding subscribe key uses the watch method.
|
|
257
|
+
const watchMethod = UNWATCH_METHODS[msg.method] || msg.method;
|
|
258
|
+
const lookupMsg = { ...msg, method: watchMethod };
|
|
259
|
+
const key = subscriptionKey(lookupMsg);
|
|
260
|
+
const sub = state.subscriptions.get(key);
|
|
261
|
+
if (!sub) {
|
|
262
|
+
sendError(ws, id, `No active subscription for ${key}`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
sub.abortController.abort();
|
|
266
|
+
state.subscriptions.delete(key);
|
|
267
|
+
send(ws, { event: "subscribed", id }); // Acknowledge unsubscribe
|
|
268
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmxt-core",
|
|
3
|
-
"version": "2.35.
|
|
3
|
+
"version": "2.35.32",
|
|
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",
|
|
@@ -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.35.
|
|
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.35.
|
|
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.35.32,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.35.32,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",
|