pmxt-core 2.44.4 → 2.44.5
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/errors.d.ts +6 -0
- package/dist/errors.js +10 -1
- package/dist/exchanges/kalshi/api.d.ts +1 -1
- package/dist/exchanges/kalshi/api.js +1 -1
- package/dist/exchanges/limitless/api.d.ts +1 -1
- package/dist/exchanges/limitless/api.js +1 -1
- package/dist/exchanges/mock/index.d.ts +3 -2
- package/dist/exchanges/mock/index.js +14 -5
- 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/feeds/binance/binance-feed.d.ts +9 -0
- package/dist/feeds/binance/binance-feed.js +34 -7
- package/dist/feeds/chainlink/chainlink-feed.d.ts +14 -0
- package/dist/feeds/chainlink/chainlink-feed.js +62 -7
- package/dist/feeds/interfaces.d.ts +10 -0
- package/dist/router/Router.d.ts +9 -0
- package/dist/router/Router.js +153 -2
- package/dist/router/types.d.ts +5 -0
- package/dist/server/app.d.ts +26 -2
- package/dist/server/app.js +50 -9
- package/dist/server/feed-routes.js +34 -12
- package/dist/server/sql-route.d.ts +2 -0
- package/dist/server/sql-route.js +277 -0
- package/package.json +3 -3
package/dist/router/Router.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Router = void 0;
|
|
4
4
|
const BaseExchange_1 = require("../BaseExchange");
|
|
5
|
+
const errors_1 = require("../errors");
|
|
5
6
|
const logger_1 = require("../utils/logger");
|
|
6
7
|
const client_1 = require("./client");
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -28,13 +29,82 @@ function mergeOrderBooks(books) {
|
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
32
|
+
const MOCK_MARKET_OR_OUTCOME_ID_RE = /^(mock-m\d+)(?:-(?:yes|no|\d+))?$/;
|
|
33
|
+
const MOCK_EVENT_ID_RE = /^mock-event-\d+$/;
|
|
34
|
+
class LocalRouterMatchLookupUnsupported extends errors_1.BaseError {
|
|
35
|
+
constructor(kind, identifier) {
|
|
36
|
+
super(`LOCAL_MATCH_LOOKUP_UNSUPPORTED: Router match lookup for local mock ${kind} ` +
|
|
37
|
+
`"${identifier}" cannot be served by the hosted match catalog. ` +
|
|
38
|
+
`Configure RouterOptions.localExchanges.mock when using Router in-process; ` +
|
|
39
|
+
`the /api/router sidecar endpoint resolves bundled mock IDs locally and returns [] because mock fixtures have no hosted cross-venue matches.`, 501, 'LOCAL_MATCH_LOOKUP_UNSUPPORTED', false, 'Router');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function sourceExchangeIsMock(sourceExchange) {
|
|
43
|
+
return typeof sourceExchange === 'string' && sourceExchange.toLowerCase() === 'mock';
|
|
44
|
+
}
|
|
45
|
+
function lookupString(value) {
|
|
46
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
function mockMarketIdFromLocalId(id) {
|
|
49
|
+
const value = lookupString(id);
|
|
50
|
+
if (!value)
|
|
51
|
+
return undefined;
|
|
52
|
+
return value.match(MOCK_MARKET_OR_OUTCOME_ID_RE)?.[1];
|
|
53
|
+
}
|
|
54
|
+
function isMockEventId(id) {
|
|
55
|
+
const value = lookupString(id);
|
|
56
|
+
return value !== undefined && MOCK_EVENT_ID_RE.test(value);
|
|
57
|
+
}
|
|
58
|
+
function isMockUrl(url, resource) {
|
|
59
|
+
const value = lookupString(url);
|
|
60
|
+
if (!value)
|
|
61
|
+
return false;
|
|
62
|
+
try {
|
|
63
|
+
const parsed = new URL(value);
|
|
64
|
+
return parsed.hostname === 'mock.pmxt.dev' && parsed.pathname.startsWith(`/${resource}/`);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return value.includes(`mock.pmxt.dev/${resource}/`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function describeMarketLookup(params) {
|
|
71
|
+
return params.market?.marketId
|
|
72
|
+
?? lookupString(params.marketId)
|
|
73
|
+
?? lookupString(params.slug)
|
|
74
|
+
?? lookupString(params.url)
|
|
75
|
+
?? 'unknown';
|
|
76
|
+
}
|
|
77
|
+
function describeEventLookup(params) {
|
|
78
|
+
return params.event?.id
|
|
79
|
+
?? lookupString(params.eventId)
|
|
80
|
+
?? lookupString(params.slug)
|
|
81
|
+
?? 'unknown';
|
|
82
|
+
}
|
|
83
|
+
function isLocalMockMarketLookup(params) {
|
|
84
|
+
return sourceExchangeIsMock(params.market?.sourceExchange)
|
|
85
|
+
|| mockMarketIdFromLocalId(params.market?.marketId) !== undefined
|
|
86
|
+
|| mockMarketIdFromLocalId(params.marketId) !== undefined
|
|
87
|
+
|| mockMarketIdFromLocalId(params.slug) !== undefined
|
|
88
|
+
|| isMockUrl(params.url, 'market');
|
|
89
|
+
}
|
|
90
|
+
function isLocalMockEventLookup(params) {
|
|
91
|
+
return sourceExchangeIsMock(params.event?.sourceExchange)
|
|
92
|
+
|| isMockEventId(params.event?.id)
|
|
93
|
+
|| isMockEventId(params.eventId)
|
|
94
|
+
|| isMockEventId(params.slug);
|
|
95
|
+
}
|
|
96
|
+
function findByUrl(items, url) {
|
|
97
|
+
return items.find((item) => item.url === url);
|
|
98
|
+
}
|
|
31
99
|
class Router extends BaseExchange_1.PredictionMarketExchange {
|
|
32
100
|
client;
|
|
33
101
|
exchanges;
|
|
102
|
+
localExchanges;
|
|
34
103
|
constructor(options) {
|
|
35
104
|
super({ apiKey: options.apiKey });
|
|
36
105
|
this.client = new client_1.PmxtApiClient(options.apiKey, options.baseUrl);
|
|
37
106
|
this.exchanges = options.exchanges ?? {};
|
|
107
|
+
this.localExchanges = options.localExchanges ?? options.exchanges ?? {};
|
|
38
108
|
this.rateLimit = 100;
|
|
39
109
|
}
|
|
40
110
|
get name() {
|
|
@@ -126,7 +196,10 @@ class Router extends BaseExchange_1.PredictionMarketExchange {
|
|
|
126
196
|
// -----------------------------------------------------------------------
|
|
127
197
|
async fetchMarketMatches(params = {}) {
|
|
128
198
|
if (params.market && !params.marketId) {
|
|
129
|
-
if (params.market.
|
|
199
|
+
if (sourceExchangeIsMock(params.market.sourceExchange)) {
|
|
200
|
+
params = { ...params, marketId: params.market.marketId };
|
|
201
|
+
}
|
|
202
|
+
else if (params.market.slug && !params.slug) {
|
|
130
203
|
params = { ...params, slug: params.market.slug };
|
|
131
204
|
}
|
|
132
205
|
else {
|
|
@@ -139,6 +212,9 @@ class Router extends BaseExchange_1.PredictionMarketExchange {
|
|
|
139
212
|
if (!hasIdentifier) {
|
|
140
213
|
return this.fetchMarketMatchesBrowse(params);
|
|
141
214
|
}
|
|
215
|
+
if (await this.resolveLocalMockMarketLookup(params)) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
142
218
|
// Lookup mode: find matches for a specific market.
|
|
143
219
|
const response = await this.client.getMarketMatches(params);
|
|
144
220
|
const matches = response.matches ?? [];
|
|
@@ -172,7 +248,10 @@ class Router extends BaseExchange_1.PredictionMarketExchange {
|
|
|
172
248
|
// -----------------------------------------------------------------------
|
|
173
249
|
async fetchEventMatches(params = {}) {
|
|
174
250
|
if (params.event && !params.eventId) {
|
|
175
|
-
if (params.event.
|
|
251
|
+
if (sourceExchangeIsMock(params.event.sourceExchange)) {
|
|
252
|
+
params = { ...params, eventId: params.event.id };
|
|
253
|
+
}
|
|
254
|
+
else if (params.event.slug && !params.slug) {
|
|
176
255
|
params = { ...params, slug: params.event.slug };
|
|
177
256
|
}
|
|
178
257
|
else {
|
|
@@ -185,6 +264,9 @@ class Router extends BaseExchange_1.PredictionMarketExchange {
|
|
|
185
264
|
const results = await this.client.browseEventMatches(params);
|
|
186
265
|
return Array.isArray(results) ? results : [];
|
|
187
266
|
}
|
|
267
|
+
if (await this.resolveLocalMockEventLookup(params)) {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
188
270
|
// Lookup mode: find matches for a specific event.
|
|
189
271
|
const response = await this.client.getEventMatches(params);
|
|
190
272
|
return response.matches ?? [];
|
|
@@ -278,6 +360,75 @@ class Router extends BaseExchange_1.PredictionMarketExchange {
|
|
|
278
360
|
logger_1.logger.warn('fetchArbitrage is deprecated, use fetchMatchedPrices instead');
|
|
279
361
|
return this.fetchArbitrageInternal(params);
|
|
280
362
|
}
|
|
363
|
+
getLocalExchange(name) {
|
|
364
|
+
const target = name.toLowerCase();
|
|
365
|
+
for (const [key, exchange] of Object.entries(this.localExchanges)) {
|
|
366
|
+
if (key.toLowerCase() === target || exchange.name.toLowerCase() === target) {
|
|
367
|
+
return exchange;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Local mock IDs are sidecar-only fixtures. Hosted match endpoints do not
|
|
374
|
+
* know them, so resolve them locally when possible and avoid opaque hosted
|
|
375
|
+
* "not found" responses.
|
|
376
|
+
*/
|
|
377
|
+
async resolveLocalMockMarketLookup(params) {
|
|
378
|
+
if (!isLocalMockMarketLookup(params))
|
|
379
|
+
return false;
|
|
380
|
+
const identifier = describeMarketLookup(params);
|
|
381
|
+
const mock = this.getLocalExchange('mock');
|
|
382
|
+
if (!mock) {
|
|
383
|
+
throw new LocalRouterMatchLookupUnsupported('market', identifier);
|
|
384
|
+
}
|
|
385
|
+
if (params.market && sourceExchangeIsMock(params.market.sourceExchange)) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
const localMarketId = mockMarketIdFromLocalId(params.marketId)
|
|
389
|
+
?? mockMarketIdFromLocalId(params.slug);
|
|
390
|
+
if (localMarketId) {
|
|
391
|
+
await mock.fetchMarket({ marketId: localMarketId });
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
const url = lookupString(params.url);
|
|
395
|
+
if (url && isMockUrl(url, 'market')) {
|
|
396
|
+
const markets = await mock.fetchMarkets();
|
|
397
|
+
if (!findByUrl(markets, url)) {
|
|
398
|
+
throw new errors_1.MarketNotFound(url, mock.name);
|
|
399
|
+
}
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
async resolveLocalMockEventLookup(params) {
|
|
405
|
+
if (!isLocalMockEventLookup(params))
|
|
406
|
+
return false;
|
|
407
|
+
const identifier = describeEventLookup(params);
|
|
408
|
+
const mock = this.getLocalExchange('mock');
|
|
409
|
+
if (!mock) {
|
|
410
|
+
throw new LocalRouterMatchLookupUnsupported('event', identifier);
|
|
411
|
+
}
|
|
412
|
+
if (params.event && sourceExchangeIsMock(params.event.sourceExchange)) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
const localEventId = isMockEventId(params.eventId) ? params.eventId
|
|
416
|
+
: isMockEventId(params.slug) ? params.slug
|
|
417
|
+
: undefined;
|
|
418
|
+
if (localEventId) {
|
|
419
|
+
await mock.fetchEvent({ eventId: localEventId });
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
const url = lookupString(params.url);
|
|
423
|
+
if (url && isMockUrl(url, 'event')) {
|
|
424
|
+
const events = await mock.fetchEvents();
|
|
425
|
+
if (!findByUrl(events, url)) {
|
|
426
|
+
throw new errors_1.EventNotFound(url, mock.name);
|
|
427
|
+
}
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
281
432
|
async fetchArbitrageInternal(params) {
|
|
282
433
|
// Try the dedicated bulk endpoint first (single DB query).
|
|
283
434
|
try {
|
package/dist/router/types.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export interface RouterOptions {
|
|
|
6
6
|
baseUrl?: string;
|
|
7
7
|
/** Exchange instances for cross-venue orderbook aggregation. Keyed by exchange name (e.g. 'polymarket', 'kalshi'). */
|
|
8
8
|
exchanges?: Record<string, PredictionMarketExchange>;
|
|
9
|
+
/**
|
|
10
|
+
* Local exchange instances used only to resolve sidecar-only fixture IDs
|
|
11
|
+
* before hosted catalog match lookups. Does not affect orderbook routing.
|
|
12
|
+
*/
|
|
13
|
+
localExchanges?: Record<string, PredictionMarketExchange>;
|
|
9
14
|
}
|
|
10
15
|
export interface MatchResult {
|
|
11
16
|
market: UnifiedMarket;
|
package/dist/server/app.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Express } from "express";
|
|
2
|
+
import { Server as HttpServer } from "http";
|
|
2
3
|
import { createWebSocketHandler, CreateWebSocketHandlerOptions } from "./ws-handler";
|
|
3
4
|
/**
|
|
4
5
|
* Options accepted by {@link createApp}.
|
|
@@ -33,12 +34,25 @@ export interface CreateAppOptions {
|
|
|
33
34
|
* wrap the sidecar in their own auth / quota / usage middleware and serve
|
|
34
35
|
* it as part of a larger Express application.
|
|
35
36
|
*
|
|
36
|
-
* The returned app registers:
|
|
37
|
+
* The returned app registers HTTP routes only:
|
|
37
38
|
* - `GET /health`
|
|
38
39
|
* - (optional) the built-in `x-pmxt-access-token` auth check
|
|
39
40
|
* - `POST /api/:exchange/:method`
|
|
40
41
|
* - the error handler
|
|
41
42
|
*
|
|
43
|
+
* WebSocket upgrades do not pass through Express routing. Local servers
|
|
44
|
+
* created from this app can expose `/ws` by attaching the WebSocket endpoint
|
|
45
|
+
* to the underlying HTTP server:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { createApp, attachWebSocketEndpoint } from 'pmxt-core';
|
|
49
|
+
*
|
|
50
|
+
* const accessToken = process.env.PMXT_ACCESS_TOKEN;
|
|
51
|
+
* const app = createApp({ accessToken });
|
|
52
|
+
* const server = app.listen(4000, "127.0.0.1");
|
|
53
|
+
* attachWebSocketEndpoint(server, { accessToken });
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
42
56
|
* Usage:
|
|
43
57
|
* ```ts
|
|
44
58
|
* import express from 'express';
|
|
@@ -51,6 +65,16 @@ export interface CreateAppOptions {
|
|
|
51
65
|
* ```
|
|
52
66
|
*/
|
|
53
67
|
export declare function createApp(options?: CreateAppOptions): Express;
|
|
68
|
+
export type WebSocketEndpoint = ReturnType<typeof createWebSocketHandler>;
|
|
69
|
+
/**
|
|
70
|
+
* Attach the PMXT streaming WebSocket endpoint to an HTTP server.
|
|
71
|
+
*
|
|
72
|
+
* Use this with servers built from `createApp()` when you need `/ws` support
|
|
73
|
+
* for watchOrderBook, watchOrderBooks, or watchTrades. The access token should
|
|
74
|
+
* match the one passed to `createApp()` so HTTP and WebSocket requests share
|
|
75
|
+
* the same local auth policy.
|
|
76
|
+
*/
|
|
77
|
+
export declare function attachWebSocketEndpoint(server: HttpServer, options?: CreateWebSocketHandlerOptions): WebSocketEndpoint;
|
|
54
78
|
/**
|
|
55
79
|
* Start the PMXT sidecar server on the given port with the built-in
|
|
56
80
|
* access-token auth middleware enabled. Returns the underlying
|
|
@@ -59,6 +83,6 @@ export declare function createApp(options?: CreateAppOptions): Express;
|
|
|
59
83
|
* Automatically attaches a WebSocket endpoint at `/ws` for streaming
|
|
60
84
|
* methods (watchOrderBook, watchOrderBooks, watchTrades).
|
|
61
85
|
*/
|
|
62
|
-
export declare function startServer(port: number, accessToken: string): Promise<
|
|
86
|
+
export declare function startServer(port: number, accessToken: string): Promise<HttpServer<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>>;
|
|
63
87
|
export { createWebSocketHandler };
|
|
64
88
|
export type { CreateWebSocketHandlerOptions };
|
package/dist/server/app.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createWebSocketHandler = void 0;
|
|
7
7
|
exports.createApp = createApp;
|
|
8
|
+
exports.attachWebSocketEndpoint = attachWebSocketEndpoint;
|
|
8
9
|
exports.startServer = startServer;
|
|
9
10
|
const express_1 = __importDefault(require("express"));
|
|
10
11
|
const cors_1 = __importDefault(require("cors"));
|
|
@@ -13,6 +14,9 @@ const path_1 = __importDefault(require("path"));
|
|
|
13
14
|
const ws_handler_1 = require("./ws-handler");
|
|
14
15
|
Object.defineProperty(exports, "createWebSocketHandler", { enumerable: true, get: function () { return ws_handler_1.createWebSocketHandler; } });
|
|
15
16
|
const exchange_factory_1 = require("./exchange-factory");
|
|
17
|
+
const feed_routes_1 = require("./feed-routes");
|
|
18
|
+
const sql_route_1 = require("./sql-route");
|
|
19
|
+
const router_1 = require("../router");
|
|
16
20
|
const errors_1 = require("../errors");
|
|
17
21
|
const logger_1 = require("../utils/logger");
|
|
18
22
|
function loadMethodVerbs() {
|
|
@@ -142,7 +146,14 @@ const defaultExchanges = {
|
|
|
142
146
|
opinion: null,
|
|
143
147
|
metaculus: null,
|
|
144
148
|
smarkets: null,
|
|
149
|
+
mock: null,
|
|
145
150
|
};
|
|
151
|
+
function getDefaultExchange(exchangeName) {
|
|
152
|
+
if (!defaultExchanges[exchangeName]) {
|
|
153
|
+
defaultExchanges[exchangeName] = (0, exchange_factory_1.createExchange)(exchangeName);
|
|
154
|
+
}
|
|
155
|
+
return defaultExchanges[exchangeName];
|
|
156
|
+
}
|
|
146
157
|
/**
|
|
147
158
|
* Build an Express app that serves the PMXT sidecar API surface without
|
|
148
159
|
* binding to a port.
|
|
@@ -151,12 +162,25 @@ const defaultExchanges = {
|
|
|
151
162
|
* wrap the sidecar in their own auth / quota / usage middleware and serve
|
|
152
163
|
* it as part of a larger Express application.
|
|
153
164
|
*
|
|
154
|
-
* The returned app registers:
|
|
165
|
+
* The returned app registers HTTP routes only:
|
|
155
166
|
* - `GET /health`
|
|
156
167
|
* - (optional) the built-in `x-pmxt-access-token` auth check
|
|
157
168
|
* - `POST /api/:exchange/:method`
|
|
158
169
|
* - the error handler
|
|
159
170
|
*
|
|
171
|
+
* WebSocket upgrades do not pass through Express routing. Local servers
|
|
172
|
+
* created from this app can expose `/ws` by attaching the WebSocket endpoint
|
|
173
|
+
* to the underlying HTTP server:
|
|
174
|
+
*
|
|
175
|
+
* ```ts
|
|
176
|
+
* import { createApp, attachWebSocketEndpoint } from 'pmxt-core';
|
|
177
|
+
*
|
|
178
|
+
* const accessToken = process.env.PMXT_ACCESS_TOKEN;
|
|
179
|
+
* const app = createApp({ accessToken });
|
|
180
|
+
* const server = app.listen(4000, "127.0.0.1");
|
|
181
|
+
* attachWebSocketEndpoint(server, { accessToken });
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
160
184
|
* Usage:
|
|
161
185
|
* ```ts
|
|
162
186
|
* import express from 'express';
|
|
@@ -195,6 +219,10 @@ function createApp(options = {}) {
|
|
|
195
219
|
next();
|
|
196
220
|
});
|
|
197
221
|
}
|
|
222
|
+
app.use("/v0/sql", (0, sql_route_1.createSqlRouter)());
|
|
223
|
+
// Mount before /api/:exchange/:method so "feeds" is not interpreted as
|
|
224
|
+
// an exchange name by the generic dispatcher.
|
|
225
|
+
app.use("/api/feeds", (0, feed_routes_1.createFeedRouter)());
|
|
198
226
|
// Shared dispatch used by both GET and POST handlers below. Given the
|
|
199
227
|
// method name, the positional args, and optional credentials, it
|
|
200
228
|
// resolves the exchange instance (singleton or per-request) and
|
|
@@ -208,7 +236,12 @@ function createApp(options = {}) {
|
|
|
208
236
|
// calls — not a server-side env var. Each request may carry a
|
|
209
237
|
// different key, so Router is never cached as a singleton.
|
|
210
238
|
const bearer = req.headers.authorization?.replace(/^Bearer\s+/i, "") || "";
|
|
211
|
-
exchange =
|
|
239
|
+
exchange = new router_1.Router({
|
|
240
|
+
apiKey: bearer,
|
|
241
|
+
localExchanges: {
|
|
242
|
+
mock: getDefaultExchange("mock"),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
212
245
|
}
|
|
213
246
|
else if (credentials &&
|
|
214
247
|
(credentials.privateKey ||
|
|
@@ -217,10 +250,7 @@ function createApp(options = {}) {
|
|
|
217
250
|
exchange = (0, exchange_factory_1.createExchange)(exchangeName, credentials);
|
|
218
251
|
}
|
|
219
252
|
else {
|
|
220
|
-
|
|
221
|
-
defaultExchanges[exchangeName] = (0, exchange_factory_1.createExchange)(exchangeName);
|
|
222
|
-
}
|
|
223
|
-
exchange = defaultExchanges[exchangeName];
|
|
253
|
+
exchange = getDefaultExchange(exchangeName);
|
|
224
254
|
}
|
|
225
255
|
if (req.headers["x-pmxt-verbose"] === "true") {
|
|
226
256
|
exchange.verbose = true;
|
|
@@ -327,6 +357,19 @@ function createApp(options = {}) {
|
|
|
327
357
|
});
|
|
328
358
|
return app;
|
|
329
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Attach the PMXT streaming WebSocket endpoint to an HTTP server.
|
|
362
|
+
*
|
|
363
|
+
* Use this with servers built from `createApp()` when you need `/ws` support
|
|
364
|
+
* for watchOrderBook, watchOrderBooks, or watchTrades. The access token should
|
|
365
|
+
* match the one passed to `createApp()` so HTTP and WebSocket requests share
|
|
366
|
+
* the same local auth policy.
|
|
367
|
+
*/
|
|
368
|
+
function attachWebSocketEndpoint(server, options = {}) {
|
|
369
|
+
const wsHandler = (0, ws_handler_1.createWebSocketHandler)(options);
|
|
370
|
+
wsHandler.attach(server);
|
|
371
|
+
return wsHandler;
|
|
372
|
+
}
|
|
330
373
|
/**
|
|
331
374
|
* Start the PMXT sidecar server on the given port with the built-in
|
|
332
375
|
* access-token auth middleware enabled. Returns the underlying
|
|
@@ -338,8 +381,6 @@ function createApp(options = {}) {
|
|
|
338
381
|
async function startServer(port, accessToken) {
|
|
339
382
|
const app = createApp({ accessToken });
|
|
340
383
|
const server = app.listen(port, "127.0.0.1");
|
|
341
|
-
|
|
342
|
-
const wsHandler = (0, ws_handler_1.createWebSocketHandler)({ accessToken });
|
|
343
|
-
wsHandler.attach(server);
|
|
384
|
+
attachWebSocketEndpoint(server, { accessToken });
|
|
344
385
|
return server;
|
|
345
386
|
}
|
|
@@ -66,6 +66,8 @@ function createFeedRouter() {
|
|
|
66
66
|
// GET /api/feeds/:feed/fetchOHLCV?symbol=BTC/USDT&timeframe=1h&since=...&limit=...
|
|
67
67
|
router.get('/:feed/fetchOHLCV', async (req, res, next) => {
|
|
68
68
|
try {
|
|
69
|
+
if (sendUnsupportedIfNeeded(req, res, 'fetchOHLCV'))
|
|
70
|
+
return;
|
|
69
71
|
const symbol = req.query.symbol;
|
|
70
72
|
if (typeof symbol !== 'string') {
|
|
71
73
|
res.status(400).json({ success: false, error: 'Missing required query parameter: symbol' });
|
|
@@ -87,10 +89,10 @@ function createFeedRouter() {
|
|
|
87
89
|
router.get('/:feed/fetchOrderBook', async (req, res, next) => {
|
|
88
90
|
try {
|
|
89
91
|
const feed = req._feed;
|
|
90
|
-
if (
|
|
91
|
-
res.status(501).json({ success: false, error: `Feed '${req.params.feed}' does not support fetchOrderBook` });
|
|
92
|
+
if (sendUnsupportedIfNeeded(req, res, 'fetchOrderBook'))
|
|
92
93
|
return;
|
|
93
|
-
|
|
94
|
+
if (typeof feed.fetchOrderBook !== 'function')
|
|
95
|
+
return sendUnsupported(res, getFeedParam(req), 'fetchOrderBook');
|
|
94
96
|
const symbol = req.query.symbol;
|
|
95
97
|
if (typeof symbol !== 'string') {
|
|
96
98
|
res.status(400).json({ success: false, error: 'Missing required query parameter: symbol' });
|
|
@@ -107,10 +109,10 @@ function createFeedRouter() {
|
|
|
107
109
|
router.get('/:feed/fetchOracleRound', async (req, res, next) => {
|
|
108
110
|
try {
|
|
109
111
|
const feed = req._feed;
|
|
110
|
-
if (
|
|
111
|
-
res.status(501).json({ success: false, error: `Feed '${req.params.feed}' does not support fetchOracleRound` });
|
|
112
|
+
if (sendUnsupportedIfNeeded(req, res, 'fetchOracleRound'))
|
|
112
113
|
return;
|
|
113
|
-
|
|
114
|
+
if (typeof feed.fetchOracleRound !== 'function')
|
|
115
|
+
return sendUnsupported(res, getFeedParam(req), 'fetchOracleRound');
|
|
114
116
|
const feedName = req.query.feed;
|
|
115
117
|
if (typeof feedName !== 'string') {
|
|
116
118
|
res.status(400).json({ success: false, error: 'Missing required query parameter: feed' });
|
|
@@ -127,10 +129,10 @@ function createFeedRouter() {
|
|
|
127
129
|
router.get('/:feed/fetchOracleHistory', async (req, res, next) => {
|
|
128
130
|
try {
|
|
129
131
|
const feed = req._feed;
|
|
130
|
-
if (
|
|
131
|
-
res.status(501).json({ success: false, error: `Feed '${req.params.feed}' does not support fetchOracleHistory` });
|
|
132
|
+
if (sendUnsupportedIfNeeded(req, res, 'fetchOracleHistory'))
|
|
132
133
|
return;
|
|
133
|
-
|
|
134
|
+
if (typeof feed.fetchOracleHistory !== 'function')
|
|
135
|
+
return sendUnsupported(res, getFeedParam(req), 'fetchOracleHistory');
|
|
134
136
|
const feedName = req.query.feed;
|
|
135
137
|
if (typeof feedName !== 'string') {
|
|
136
138
|
res.status(400).json({ success: false, error: 'Missing required query parameter: feed' });
|
|
@@ -150,10 +152,10 @@ function createFeedRouter() {
|
|
|
150
152
|
router.get('/:feed/fetchHistoricalPrices', async (req, res, next) => {
|
|
151
153
|
try {
|
|
152
154
|
const feed = req._feed;
|
|
153
|
-
if (
|
|
154
|
-
res.status(501).json({ success: false, error: `Feed '${req.params.feed}' does not support fetchHistoricalPrices` });
|
|
155
|
+
if (sendUnsupportedIfNeeded(req, res, 'fetchHistoricalPrices'))
|
|
155
156
|
return;
|
|
156
|
-
|
|
157
|
+
if (typeof feed.fetchHistoricalPrices !== 'function')
|
|
158
|
+
return sendUnsupported(res, getFeedParam(req), 'fetchHistoricalPrices');
|
|
157
159
|
const symbol = req.query.symbol;
|
|
158
160
|
if (typeof symbol !== 'string') {
|
|
159
161
|
res.status(400).json({ success: false, error: 'Missing required query parameter: symbol' });
|
|
@@ -178,3 +180,23 @@ function createFeedRouter() {
|
|
|
178
180
|
});
|
|
179
181
|
return router;
|
|
180
182
|
}
|
|
183
|
+
function getRequestFeed(req) {
|
|
184
|
+
return req._feed;
|
|
185
|
+
}
|
|
186
|
+
function sendUnsupportedIfNeeded(req, res, method) {
|
|
187
|
+
const feed = getRequestFeed(req);
|
|
188
|
+
if (feed.has?.[method] !== false)
|
|
189
|
+
return false;
|
|
190
|
+
sendUnsupported(res, feed.name || getFeedParam(req), method);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
function sendUnsupported(res, feedName, method) {
|
|
194
|
+
res.status(501).json({
|
|
195
|
+
success: false,
|
|
196
|
+
error: `Feed '${feedName}' does not support ${method}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function getFeedParam(req) {
|
|
200
|
+
const value = req.params.feed;
|
|
201
|
+
return Array.isArray(value) ? value[0] ?? 'unknown' : value;
|
|
202
|
+
}
|