pmxt-core 2.44.4 → 2.44.6
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/kalshi/fetcher.d.ts +11 -1
- package/dist/exchanges/kalshi/fetcher.js +49 -17
- package/dist/exchanges/kalshi/normalizer.d.ts +12 -0
- package/dist/exchanges/kalshi/normalizer.js +125 -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
|
@@ -6,12 +6,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.BinanceFeed = void 0;
|
|
7
7
|
const ws_1 = __importDefault(require("ws"));
|
|
8
8
|
const logger_1 = require("../../utils/logger");
|
|
9
|
+
const errors_1 = require("../../errors");
|
|
9
10
|
const base_feed_1 = require("../base-feed");
|
|
10
11
|
const types_1 = require("./types");
|
|
11
12
|
const normalizer_1 = require("./normalizer");
|
|
12
13
|
class BinanceFeed extends base_feed_1.BaseDataFeed {
|
|
13
14
|
name = 'binance';
|
|
14
15
|
description = 'Binance spot trade firehose via obdata relay';
|
|
16
|
+
has = {
|
|
17
|
+
loadMarkets: true,
|
|
18
|
+
fetchTicker: true,
|
|
19
|
+
fetchTickers: true,
|
|
20
|
+
watchTicker: true,
|
|
21
|
+
fetchOHLCV: false,
|
|
22
|
+
fetchOrderBook: false,
|
|
23
|
+
};
|
|
15
24
|
wsUrl;
|
|
16
25
|
apiKey;
|
|
17
26
|
reconnectIntervalMs;
|
|
@@ -23,7 +32,7 @@ class BinanceFeed extends base_feed_1.BaseDataFeed {
|
|
|
23
32
|
connectionPromise = null;
|
|
24
33
|
constructor(config = {}, options) {
|
|
25
34
|
super(options);
|
|
26
|
-
this.wsUrl = config.wsUrl ?? types_1.BINANCE_RELAY_DEFAULTS.wsUrl;
|
|
35
|
+
this.wsUrl = config.wsUrl ?? process.env.BINANCE_RELAY_WS_URL ?? types_1.BINANCE_RELAY_DEFAULTS.wsUrl;
|
|
27
36
|
this.apiKey = config.apiKey ?? process.env.OBDATA_API_KEY ?? '';
|
|
28
37
|
this.reconnectIntervalMs = config.reconnectIntervalMs ?? types_1.BINANCE_RELAY_DEFAULTS.reconnectIntervalMs;
|
|
29
38
|
}
|
|
@@ -117,10 +126,10 @@ class BinanceFeed extends base_feed_1.BaseDataFeed {
|
|
|
117
126
|
};
|
|
118
127
|
}
|
|
119
128
|
async fetchOHLCVImpl(_symbol, _timeframe, _since, _limit) {
|
|
120
|
-
throw new
|
|
129
|
+
throw new errors_1.NotSupported('BinanceFeed does not support fetchOHLCV via the configured trade relay.', this.name);
|
|
121
130
|
}
|
|
122
131
|
async fetchOrderBookImpl(_symbol, _limit) {
|
|
123
|
-
throw new
|
|
132
|
+
throw new errors_1.NotSupported('BinanceFeed does not support fetchOrderBook via the configured trade relay.', this.name);
|
|
124
133
|
}
|
|
125
134
|
// -- Internal --
|
|
126
135
|
async ensureConnected() {
|
|
@@ -130,10 +139,11 @@ class BinanceFeed extends base_feed_1.BaseDataFeed {
|
|
|
130
139
|
}
|
|
131
140
|
establishConnection() {
|
|
132
141
|
return new Promise((resolve, reject) => {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
const relayUrl = this.validateRelayWsUrl();
|
|
143
|
+
if (this.apiKey) {
|
|
144
|
+
relayUrl.searchParams.set('key', this.apiKey);
|
|
145
|
+
}
|
|
146
|
+
const ws = new ws_1.default(relayUrl.toString());
|
|
137
147
|
const connectionTimeout = setTimeout(() => {
|
|
138
148
|
ws.close();
|
|
139
149
|
this.ws = null;
|
|
@@ -203,5 +213,22 @@ class BinanceFeed extends base_feed_1.BaseDataFeed {
|
|
|
203
213
|
}
|
|
204
214
|
}, this.reconnectIntervalMs);
|
|
205
215
|
}
|
|
216
|
+
validateRelayWsUrl() {
|
|
217
|
+
const rawUrl = this.wsUrl.trim();
|
|
218
|
+
if (!rawUrl) {
|
|
219
|
+
throw new errors_1.ExchangeNotAvailable('BinanceFeed requires BINANCE_RELAY_WS_URL to fetch live ticker data.', this.name);
|
|
220
|
+
}
|
|
221
|
+
let url;
|
|
222
|
+
try {
|
|
223
|
+
url = new URL(rawUrl);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
throw new errors_1.ExchangeNotAvailable('BinanceFeed requires BINANCE_RELAY_WS_URL to be a valid WebSocket URL.', this.name);
|
|
227
|
+
}
|
|
228
|
+
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
|
|
229
|
+
throw new errors_1.ExchangeNotAvailable('BinanceFeed requires BINANCE_RELAY_WS_URL to use ws:// or wss://.', this.name);
|
|
230
|
+
}
|
|
231
|
+
return url;
|
|
232
|
+
}
|
|
206
233
|
}
|
|
207
234
|
exports.BinanceFeed = BinanceFeed;
|
|
@@ -4,6 +4,18 @@ import { ChainlinkFeedConfig } from './types';
|
|
|
4
4
|
export declare class ChainlinkFeed extends BaseDataFeed {
|
|
5
5
|
readonly name = "chainlink";
|
|
6
6
|
readonly description = "Chainlink price feeds (ETH, BTC, XRP, SOL) on Polygon via pmxt-ohlc";
|
|
7
|
+
readonly has: {
|
|
8
|
+
readonly loadMarkets: true;
|
|
9
|
+
readonly fetchTicker: true;
|
|
10
|
+
readonly fetchTickers: true;
|
|
11
|
+
readonly watchTicker: true;
|
|
12
|
+
readonly fetchOHLCV: false;
|
|
13
|
+
readonly fetchOrderBook: false;
|
|
14
|
+
readonly fetchOracleRound: true;
|
|
15
|
+
readonly fetchOracleHistory: true;
|
|
16
|
+
readonly fetchHistoricalPrices: true;
|
|
17
|
+
};
|
|
18
|
+
private readonly baseUrl;
|
|
7
19
|
private readonly client;
|
|
8
20
|
private readonly wsUrl;
|
|
9
21
|
private readonly wsApiKey;
|
|
@@ -32,7 +44,9 @@ export declare class ChainlinkFeed extends BaseDataFeed {
|
|
|
32
44
|
order?: 'asc' | 'desc';
|
|
33
45
|
}): Promise<Ticker[]>;
|
|
34
46
|
private ensureConnected;
|
|
47
|
+
private ensureRestConfigured;
|
|
35
48
|
private establishConnection;
|
|
49
|
+
private validateWsUrl;
|
|
36
50
|
private handleMessage;
|
|
37
51
|
private scheduleReconnect;
|
|
38
52
|
}
|
|
@@ -8,11 +8,24 @@ const ws_1 = __importDefault(require("ws"));
|
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
const base_feed_1 = require("../base-feed");
|
|
10
10
|
const logger_1 = require("../../utils/logger");
|
|
11
|
+
const errors_1 = require("../../errors");
|
|
11
12
|
const types_1 = require("./types");
|
|
12
13
|
const normalizer_1 = require("./normalizer");
|
|
13
14
|
class ChainlinkFeed extends base_feed_1.BaseDataFeed {
|
|
14
15
|
name = 'chainlink';
|
|
15
16
|
description = 'Chainlink price feeds (ETH, BTC, XRP, SOL) on Polygon via pmxt-ohlc';
|
|
17
|
+
has = {
|
|
18
|
+
loadMarkets: true,
|
|
19
|
+
fetchTicker: true,
|
|
20
|
+
fetchTickers: true,
|
|
21
|
+
watchTicker: true,
|
|
22
|
+
fetchOHLCV: false,
|
|
23
|
+
fetchOrderBook: false,
|
|
24
|
+
fetchOracleRound: true,
|
|
25
|
+
fetchOracleHistory: true,
|
|
26
|
+
fetchHistoricalPrices: true,
|
|
27
|
+
};
|
|
28
|
+
baseUrl;
|
|
16
29
|
client;
|
|
17
30
|
wsUrl;
|
|
18
31
|
wsApiKey;
|
|
@@ -25,13 +38,14 @@ class ChainlinkFeed extends base_feed_1.BaseDataFeed {
|
|
|
25
38
|
connectionPromise = null;
|
|
26
39
|
constructor(config, options) {
|
|
27
40
|
super(options);
|
|
28
|
-
const baseURL = config.baseUrl ?? types_1.CHAINLINK_DEFAULTS.baseUrl;
|
|
41
|
+
const baseURL = config.baseUrl ?? process.env.CHAINLINK_API_URL ?? types_1.CHAINLINK_DEFAULTS.baseUrl;
|
|
42
|
+
this.baseUrl = baseURL;
|
|
29
43
|
this.client = axios_1.default.create({
|
|
30
44
|
baseURL,
|
|
31
45
|
headers: { 'X-API-Key': config.apiKey },
|
|
32
46
|
timeout: 10_000,
|
|
33
47
|
});
|
|
34
|
-
this.wsUrl = config.wsUrl ?? types_1.CHAINLINK_DEFAULTS.wsUrl;
|
|
48
|
+
this.wsUrl = config.wsUrl ?? process.env.CHAINLINK_WS_URL ?? types_1.CHAINLINK_DEFAULTS.wsUrl;
|
|
35
49
|
this.wsApiKey = config.wsApiKey ?? config.apiKey;
|
|
36
50
|
this.reconnectIntervalMs = config.reconnectIntervalMs ?? types_1.CHAINLINK_DEFAULTS.reconnectIntervalMs;
|
|
37
51
|
}
|
|
@@ -87,6 +101,7 @@ class ChainlinkFeed extends base_feed_1.BaseDataFeed {
|
|
|
87
101
|
const cached = this.latestTickers.get(symbol.toUpperCase());
|
|
88
102
|
if (cached)
|
|
89
103
|
return cached;
|
|
104
|
+
this.ensureRestConfigured();
|
|
90
105
|
const token = types_1.TOKEN_BY_PAIR.get(symbol.toUpperCase());
|
|
91
106
|
if (!token) {
|
|
92
107
|
throw new Error(`Unsupported Chainlink symbol: ${symbol}. Supported: ${types_1.SUPPORTED_TOKENS.map((t) => t.pair).join(', ')}`);
|
|
@@ -101,6 +116,7 @@ class ChainlinkFeed extends base_feed_1.BaseDataFeed {
|
|
|
101
116
|
}
|
|
102
117
|
// -- CCXT: fetchTickers --
|
|
103
118
|
async fetchTickersImpl(symbols) {
|
|
119
|
+
this.ensureRestConfigured();
|
|
104
120
|
const { data } = await this.client.get('/v1/chainlink/latest-prices');
|
|
105
121
|
const now = Date.now();
|
|
106
122
|
const requested = symbols
|
|
@@ -133,15 +149,16 @@ class ChainlinkFeed extends base_feed_1.BaseDataFeed {
|
|
|
133
149
|
}
|
|
134
150
|
// -- CCXT: fetchOHLCV (not supported) --
|
|
135
151
|
async fetchOHLCVImpl(_symbol, _timeframe, _since, _limit) {
|
|
136
|
-
throw new
|
|
137
|
-
'Use fetchOracleHistory() for raw AnswerUpdated records.');
|
|
152
|
+
throw new errors_1.NotSupported('Chainlink feed does not provide OHLCV candles. ' +
|
|
153
|
+
'Use fetchOracleHistory() for raw AnswerUpdated records.', this.name);
|
|
138
154
|
}
|
|
139
155
|
// -- CCXT: fetchOrderBook (not applicable) --
|
|
140
156
|
async fetchOrderBookImpl(_symbol, _limit) {
|
|
141
|
-
throw new
|
|
157
|
+
throw new errors_1.NotSupported('Chainlink oracle feeds do not have order books.', this.name);
|
|
142
158
|
}
|
|
143
159
|
// -- pmxt extensions: Oracle --
|
|
144
160
|
async fetchOracleRound(params) {
|
|
161
|
+
this.ensureRestConfigured();
|
|
145
162
|
const token = types_1.TOKEN_BY_PAIR.get(params.feed.toUpperCase());
|
|
146
163
|
if (!token) {
|
|
147
164
|
throw new Error(`Unsupported Chainlink feed: ${params.feed}. Supported: ${types_1.SUPPORTED_TOKENS.map((t) => t.pair).join(', ')}`);
|
|
@@ -153,6 +170,7 @@ class ChainlinkFeed extends base_feed_1.BaseDataFeed {
|
|
|
153
170
|
return (0, normalizer_1.normalizePriceRecordToOracleRound)(data.data[0]);
|
|
154
171
|
}
|
|
155
172
|
async fetchOracleHistory(params) {
|
|
173
|
+
this.ensureRestConfigured();
|
|
156
174
|
const token = types_1.TOKEN_BY_PAIR.get(params.feed.toUpperCase());
|
|
157
175
|
if (!token) {
|
|
158
176
|
throw new Error(`Unsupported Chainlink feed: ${params.feed}. Supported: ${types_1.SUPPORTED_TOKENS.map((t) => t.pair).join(', ')}`);
|
|
@@ -161,6 +179,7 @@ class ChainlinkFeed extends base_feed_1.BaseDataFeed {
|
|
|
161
179
|
return data.data.map(normalizer_1.normalizePriceRecordToOracleRound);
|
|
162
180
|
}
|
|
163
181
|
async fetchHistoricalPrices(symbol, opts) {
|
|
182
|
+
this.ensureRestConfigured();
|
|
164
183
|
const token = types_1.TOKEN_BY_PAIR.get(symbol.toUpperCase());
|
|
165
184
|
if (!token) {
|
|
166
185
|
throw new Error(`Unsupported Chainlink symbol: ${symbol}. Supported: ${types_1.SUPPORTED_TOKENS.map((t) => t.pair).join(', ')}`);
|
|
@@ -183,10 +202,29 @@ class ChainlinkFeed extends base_feed_1.BaseDataFeed {
|
|
|
183
202
|
return;
|
|
184
203
|
await this.connect();
|
|
185
204
|
}
|
|
205
|
+
ensureRestConfigured() {
|
|
206
|
+
const rawUrl = this.baseUrl.trim();
|
|
207
|
+
if (!rawUrl) {
|
|
208
|
+
throw new errors_1.ExchangeNotAvailable('ChainlinkFeed requires CHAINLINK_API_URL to fetch oracle data.', this.name);
|
|
209
|
+
}
|
|
210
|
+
let url;
|
|
211
|
+
try {
|
|
212
|
+
url = new URL(rawUrl);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
throw new errors_1.ExchangeNotAvailable('ChainlinkFeed requires CHAINLINK_API_URL to be a valid HTTP URL.', this.name);
|
|
216
|
+
}
|
|
217
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
218
|
+
throw new errors_1.ExchangeNotAvailable('ChainlinkFeed requires CHAINLINK_API_URL to use http:// or https://.', this.name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
186
221
|
establishConnection() {
|
|
187
222
|
return new Promise((resolve, reject) => {
|
|
188
|
-
const
|
|
189
|
-
|
|
223
|
+
const wsUrl = this.validateWsUrl();
|
|
224
|
+
if (this.wsApiKey) {
|
|
225
|
+
wsUrl.searchParams.set('key', this.wsApiKey);
|
|
226
|
+
}
|
|
227
|
+
const ws = new ws_1.default(wsUrl.toString());
|
|
190
228
|
const connectionTimeout = setTimeout(() => {
|
|
191
229
|
ws.close();
|
|
192
230
|
this.ws = null;
|
|
@@ -220,6 +258,23 @@ class ChainlinkFeed extends base_feed_1.BaseDataFeed {
|
|
|
220
258
|
});
|
|
221
259
|
});
|
|
222
260
|
}
|
|
261
|
+
validateWsUrl() {
|
|
262
|
+
const rawUrl = this.wsUrl.trim();
|
|
263
|
+
if (!rawUrl) {
|
|
264
|
+
throw new errors_1.ExchangeNotAvailable('ChainlinkFeed requires CHAINLINK_WS_URL to stream live oracle data.', this.name);
|
|
265
|
+
}
|
|
266
|
+
let url;
|
|
267
|
+
try {
|
|
268
|
+
url = new URL(rawUrl);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
throw new errors_1.ExchangeNotAvailable('ChainlinkFeed requires CHAINLINK_WS_URL to be a valid WebSocket URL.', this.name);
|
|
272
|
+
}
|
|
273
|
+
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
|
|
274
|
+
throw new errors_1.ExchangeNotAvailable('ChainlinkFeed requires CHAINLINK_WS_URL to use ws:// or wss://.', this.name);
|
|
275
|
+
}
|
|
276
|
+
return url;
|
|
277
|
+
}
|
|
223
278
|
handleMessage(data) {
|
|
224
279
|
const text = typeof data === 'string' ? data : data.toString();
|
|
225
280
|
let msg;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Ticker, Tickers, OHLCV, OrderBook, Market, FundingRate, FundingRates, OracleRound, OracleParams, Dictionary } from './types';
|
|
2
|
+
export type DataFeedCapability = 'loadMarkets' | 'fetchTicker' | 'fetchTickers' | 'watchTicker' | 'fetchOHLCV' | 'fetchOrderBook' | 'watchOrderBook' | 'fetchFundingRate' | 'fetchFundingRates' | 'fetchOracleRound' | 'fetchOracleHistory' | 'fetchHistoricalPrices';
|
|
3
|
+
export type DataFeedCapabilityValue = true | false | 'emulated';
|
|
4
|
+
export type DataFeedCapabilities = Readonly<Partial<Record<DataFeedCapability, DataFeedCapabilityValue>>>;
|
|
2
5
|
export interface IDataFeed {
|
|
3
6
|
readonly name: string;
|
|
4
7
|
readonly description: string;
|
|
8
|
+
readonly has?: DataFeedCapabilities;
|
|
5
9
|
loadMarkets(reload?: boolean): Promise<Dictionary<Market>>;
|
|
6
10
|
fetchTicker(symbol: string): Promise<Ticker>;
|
|
7
11
|
fetchTickers(symbols?: string[]): Promise<Tickers>;
|
|
@@ -13,6 +17,12 @@ export interface IDataFeed {
|
|
|
13
17
|
fetchFundingRates?(symbols?: string[]): Promise<FundingRates>;
|
|
14
18
|
fetchOracleRound?(params: OracleParams): Promise<OracleRound>;
|
|
15
19
|
fetchOracleHistory?(params: OracleParams): Promise<OracleRound[]>;
|
|
20
|
+
fetchHistoricalPrices?(symbol: string, opts?: {
|
|
21
|
+
fromTimestamp?: number;
|
|
22
|
+
untilTimestamp?: number;
|
|
23
|
+
maxSize?: number;
|
|
24
|
+
order?: 'asc' | 'desc';
|
|
25
|
+
}): Promise<Ticker[]>;
|
|
16
26
|
connect?(): Promise<void>;
|
|
17
27
|
close?(): Promise<void>;
|
|
18
28
|
}
|
package/dist/router/Router.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { RouterOptions, MatchResult, EventMatchResult, PriceComparison, Arb
|
|
|
4
4
|
export declare class Router extends PredictionMarketExchange {
|
|
5
5
|
private readonly client;
|
|
6
6
|
private readonly exchanges;
|
|
7
|
+
private readonly localExchanges;
|
|
7
8
|
constructor(options: RouterOptions);
|
|
8
9
|
get name(): string;
|
|
9
10
|
protected fetchMarketsImpl(params?: MarketFetchParams): Promise<UnifiedMarket[]>;
|
|
@@ -28,6 +29,14 @@ export declare class Router extends PredictionMarketExchange {
|
|
|
28
29
|
fetchMatchedPrices(params?: FetchMatchedPricesParams): Promise<MatchedPricePair[]>;
|
|
29
30
|
/** @deprecated Use {@link fetchMatchedMarkets} instead. */
|
|
30
31
|
fetchArbitrage(params?: FetchArbitrageParams): Promise<ArbitrageOpportunity[]>;
|
|
32
|
+
private getLocalExchange;
|
|
33
|
+
/**
|
|
34
|
+
* Local mock IDs are sidecar-only fixtures. Hosted match endpoints do not
|
|
35
|
+
* know them, so resolve them locally when possible and avoid opaque hosted
|
|
36
|
+
* "not found" responses.
|
|
37
|
+
*/
|
|
38
|
+
private resolveLocalMockMarketLookup;
|
|
39
|
+
private resolveLocalMockEventLookup;
|
|
31
40
|
private fetchArbitrageInternal;
|
|
32
41
|
/**
|
|
33
42
|
* Bulk arbitrage via `GET /v0/arbitrage`. One round-trip.
|
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 };
|