pmxt-core 2.44.5 → 2.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.js +13 -2
- 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/polymarket/websocket.d.ts +12 -0
- package/dist/exchanges/polymarket/websocket.js +120 -14
- package/dist/exchanges/probable/api.d.ts +1 -1
- package/dist/exchanges/probable/api.js +1 -1
- package/dist/exchanges/suibets/api.d.ts +15 -0
- package/dist/exchanges/suibets/api.js +17 -0
- package/dist/exchanges/suibets/config.d.ts +16 -0
- package/dist/exchanges/suibets/config.js +34 -0
- package/dist/exchanges/suibets/errors.d.ts +16 -0
- package/dist/exchanges/suibets/errors.js +71 -0
- package/dist/exchanges/suibets/fetcher.d.ts +64 -0
- package/dist/exchanges/suibets/fetcher.js +128 -0
- package/dist/exchanges/suibets/index.d.ts +54 -0
- package/dist/exchanges/suibets/index.js +114 -0
- package/dist/exchanges/suibets/normalizer.d.ts +8 -0
- package/dist/exchanges/suibets/normalizer.js +102 -0
- package/dist/exchanges/suibets/utils.d.ts +63 -0
- package/dist/exchanges/suibets/utils.js +124 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -1
- package/dist/router/Router.js +12 -3
- package/dist/server/app.js +76 -1
- package/dist/server/exchange-factory.js +6 -0
- package/dist/server/openapi.yaml +7 -0
- package/dist/server/ws-handler.js +196 -23
- package/package.json +6 -6
|
@@ -46,6 +46,7 @@ const goldsky_1 = require("../../subscriber/external/goldsky");
|
|
|
46
46
|
const watcher_1 = require("../../subscriber/watcher");
|
|
47
47
|
const watch_timeout_1 = require("../../utils/watch-timeout");
|
|
48
48
|
const DEFAULT_CONNECTION_TIMEOUT_MS = 30_000;
|
|
49
|
+
const DEFAULT_SNAPSHOT_FALLBACK_MS = 3_000;
|
|
49
50
|
const MAX_PENDING_TRADES_PER_ASSET = 1000;
|
|
50
51
|
const MAX_USER_CALLBACKS = 100;
|
|
51
52
|
const POLYMARKET_MARKET_WS_URL = 'wss://ws-subscriptions-clob.polymarket.com/ws/market';
|
|
@@ -63,7 +64,10 @@ class PolymarketWebSocket {
|
|
|
63
64
|
orderBooks = new Map();
|
|
64
65
|
config;
|
|
65
66
|
initializationPromise;
|
|
67
|
+
marketPingInterval = null;
|
|
68
|
+
callApi;
|
|
66
69
|
constructor(callApi, config = {}) {
|
|
70
|
+
this.callApi = callApi;
|
|
67
71
|
this.config = config;
|
|
68
72
|
const watcherConfig = this.config.watcherConfig;
|
|
69
73
|
const subscriber = new goldsky_1.GoldSkySubscriber({
|
|
@@ -78,12 +82,20 @@ class PolymarketWebSocket {
|
|
|
78
82
|
async watchOrderBook(outcomeId) {
|
|
79
83
|
await this.ensureInitialized();
|
|
80
84
|
await this.subscribe([outcomeId]);
|
|
81
|
-
// Return a promise that resolves on the next orderbook update
|
|
85
|
+
// Return a promise that resolves on the next orderbook update.
|
|
86
|
+
// If the upstream market channel accepts the subscription but stays
|
|
87
|
+
// quiet, return a real REST snapshot instead of hanging indefinitely.
|
|
88
|
+
const resolverEntry = {
|
|
89
|
+
resolve: () => { },
|
|
90
|
+
reject: () => { },
|
|
91
|
+
};
|
|
82
92
|
const dataPromise = new Promise((resolve, reject) => {
|
|
93
|
+
resolverEntry.resolve = resolve;
|
|
94
|
+
resolverEntry.reject = reject;
|
|
83
95
|
const existing = this.orderBookResolvers.get(outcomeId) ?? [];
|
|
84
|
-
this.orderBookResolvers.set(outcomeId, [...existing,
|
|
96
|
+
this.orderBookResolvers.set(outcomeId, [...existing, resolverEntry]);
|
|
85
97
|
});
|
|
86
|
-
return (0, watch_timeout_1.withWatchTimeout)(dataPromise, this.config.watchTimeoutMs ?? watch_timeout_1.DEFAULT_WATCH_TIMEOUT_MS, `watchOrderBook('${outcomeId}')`);
|
|
98
|
+
return (0, watch_timeout_1.withWatchTimeout)(this.withSnapshotFallback(outcomeId, dataPromise, resolverEntry), this.config.watchTimeoutMs ?? watch_timeout_1.DEFAULT_WATCH_TIMEOUT_MS, `watchOrderBook('${outcomeId}')`);
|
|
87
99
|
}
|
|
88
100
|
async unwatchOrderBook(outcomeId) {
|
|
89
101
|
this.subscribedAssets.delete(outcomeId);
|
|
@@ -269,6 +281,7 @@ class PolymarketWebSocket {
|
|
|
269
281
|
this.ws.close();
|
|
270
282
|
this.ws = null;
|
|
271
283
|
}
|
|
284
|
+
this.stopMarketHeartbeat();
|
|
272
285
|
this.subscribedAssets.clear();
|
|
273
286
|
this.closeUserChannel();
|
|
274
287
|
this.watcher.close();
|
|
@@ -302,11 +315,15 @@ class PolymarketWebSocket {
|
|
|
302
315
|
}, timeoutMs);
|
|
303
316
|
this.ws.on('open', () => {
|
|
304
317
|
clearTimeout(timeout);
|
|
318
|
+
this.startMarketHeartbeat();
|
|
305
319
|
resolve();
|
|
306
320
|
});
|
|
307
321
|
this.ws.on('message', (raw) => {
|
|
308
322
|
try {
|
|
309
|
-
const
|
|
323
|
+
const text = raw.toString();
|
|
324
|
+
if (text === 'PONG' || text === 'PING')
|
|
325
|
+
return;
|
|
326
|
+
const msgs = JSON.parse(text);
|
|
310
327
|
const arr = Array.isArray(msgs) ? msgs : [msgs];
|
|
311
328
|
for (const msg of arr) {
|
|
312
329
|
const type = msg.event_type;
|
|
@@ -327,9 +344,12 @@ class PolymarketWebSocket {
|
|
|
327
344
|
this.ws.on('error', (err) => {
|
|
328
345
|
clearTimeout(timeout);
|
|
329
346
|
logger_1.logger.error('[polymarket-ws] WebSocket error', { error: err.message });
|
|
347
|
+
this.rejectPendingMarketResolvers(err);
|
|
330
348
|
reject(err);
|
|
331
349
|
});
|
|
332
350
|
this.ws.on('close', () => {
|
|
351
|
+
this.stopMarketHeartbeat();
|
|
352
|
+
this.rejectPendingMarketResolvers(new Error('Polymarket market channel closed'));
|
|
333
353
|
this.initializationPromise = undefined;
|
|
334
354
|
this.ws = null;
|
|
335
355
|
});
|
|
@@ -338,21 +358,107 @@ class PolymarketWebSocket {
|
|
|
338
358
|
}
|
|
339
359
|
handleBookSnapshot(event) {
|
|
340
360
|
const id = event.asset_id;
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
361
|
+
const orderBook = this.normalizeRawOrderBook(event);
|
|
362
|
+
this.orderBooks.set(id, orderBook);
|
|
363
|
+
this.resolveOrderBook(id, orderBook);
|
|
364
|
+
}
|
|
365
|
+
withSnapshotFallback(outcomeId, dataPromise, resolverEntry) {
|
|
366
|
+
const fallbackMs = this.config.snapshotFallbackMs ?? DEFAULT_SNAPSHOT_FALLBACK_MS;
|
|
367
|
+
if (fallbackMs <= 0)
|
|
368
|
+
return dataPromise;
|
|
369
|
+
let timer;
|
|
370
|
+
const fallbackPromise = new Promise((resolve, reject) => {
|
|
371
|
+
timer = setTimeout(async () => {
|
|
372
|
+
this.removeOrderBookResolver(outcomeId, resolverEntry);
|
|
373
|
+
try {
|
|
374
|
+
resolve(await this.fetchOrderBookSnapshot(outcomeId));
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
reject(error);
|
|
378
|
+
}
|
|
379
|
+
}, fallbackMs);
|
|
380
|
+
});
|
|
381
|
+
return Promise.race([dataPromise, fallbackPromise]).finally(() => {
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
removeOrderBookResolver(outcomeId, resolverEntry) {
|
|
386
|
+
const resolvers = this.orderBookResolvers.get(outcomeId);
|
|
387
|
+
if (!resolvers)
|
|
388
|
+
return;
|
|
389
|
+
const filtered = resolvers.filter((entry) => entry !== resolverEntry);
|
|
390
|
+
if (filtered.length > 0) {
|
|
391
|
+
this.orderBookResolvers.set(outcomeId, filtered);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
this.orderBookResolvers.delete(outcomeId);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async fetchOrderBookSnapshot(outcomeId) {
|
|
398
|
+
const raw = await this.callApi('getBook', { token_id: outcomeId });
|
|
399
|
+
const orderBook = this.normalizeRawOrderBook({
|
|
400
|
+
...raw,
|
|
401
|
+
asset_id: raw?.asset_id ?? outcomeId,
|
|
402
|
+
});
|
|
403
|
+
this.orderBooks.set(outcomeId, orderBook);
|
|
404
|
+
return orderBook;
|
|
405
|
+
}
|
|
406
|
+
normalizeRawOrderBook(raw) {
|
|
407
|
+
const bids = (raw?.bids || []).map((level) => ({
|
|
408
|
+
price: parseFloat(level.price),
|
|
409
|
+
size: parseFloat(level.size),
|
|
344
410
|
})).sort((a, b) => b.price - a.price);
|
|
345
|
-
const asks =
|
|
346
|
-
price: parseFloat(
|
|
347
|
-
size: parseFloat(
|
|
411
|
+
const asks = (raw?.asks || []).map((level) => ({
|
|
412
|
+
price: parseFloat(level.price),
|
|
413
|
+
size: parseFloat(level.size),
|
|
348
414
|
})).sort((a, b) => a.price - b.price);
|
|
349
|
-
|
|
415
|
+
return {
|
|
350
416
|
bids,
|
|
351
417
|
asks,
|
|
352
|
-
timestamp:
|
|
418
|
+
timestamp: this.parseTimestamp(raw?.timestamp),
|
|
353
419
|
};
|
|
354
|
-
|
|
355
|
-
|
|
420
|
+
}
|
|
421
|
+
parseTimestamp(value) {
|
|
422
|
+
if (typeof value === 'number')
|
|
423
|
+
return value;
|
|
424
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
425
|
+
const numeric = Number(value);
|
|
426
|
+
return Number.isNaN(numeric) ? new Date(value).getTime() : numeric;
|
|
427
|
+
}
|
|
428
|
+
return Date.now();
|
|
429
|
+
}
|
|
430
|
+
startMarketHeartbeat() {
|
|
431
|
+
this.stopMarketHeartbeat();
|
|
432
|
+
this.marketPingInterval = setInterval(() => {
|
|
433
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
434
|
+
try {
|
|
435
|
+
this.ws.send('PING');
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
logger_1.logger.warn('[polymarket-ws] market heartbeat failed', {
|
|
439
|
+
error: error instanceof Error ? error.message : String(error),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}, 10_000);
|
|
444
|
+
}
|
|
445
|
+
stopMarketHeartbeat() {
|
|
446
|
+
if (this.marketPingInterval) {
|
|
447
|
+
clearInterval(this.marketPingInterval);
|
|
448
|
+
this.marketPingInterval = null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
rejectPendingMarketResolvers(error) {
|
|
452
|
+
for (const [, resolvers] of this.orderBookResolvers) {
|
|
453
|
+
for (const resolver of resolvers)
|
|
454
|
+
resolver.reject(error);
|
|
455
|
+
}
|
|
456
|
+
this.orderBookResolvers.clear();
|
|
457
|
+
for (const [, resolvers] of this.tradeResolvers) {
|
|
458
|
+
for (const resolver of resolvers)
|
|
459
|
+
resolver.reject(error);
|
|
460
|
+
}
|
|
461
|
+
this.tradeResolvers.clear();
|
|
356
462
|
}
|
|
357
463
|
handlePriceChange(event) {
|
|
358
464
|
const id = event.asset_id;
|
|
@@ -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-05-
|
|
3
|
+
* Generated at: 2026-05-25T15:44:11.722Z
|
|
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-05-
|
|
6
|
+
* Generated at: 2026-05-25T15:44:11.722Z
|
|
7
7
|
* Do not edit manually -- run "npm run fetch:openapi" to regenerate.
|
|
8
8
|
*/
|
|
9
9
|
exports.probableApiSpec = {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SuiBets P2P Sports Betting API Reference
|
|
3
|
+
*
|
|
4
|
+
* This file documents the SuiBets REST API endpoints used by the fetcher.
|
|
5
|
+
* It is NOT wired into defineImplicitApi — the fetcher calls these endpoints
|
|
6
|
+
* directly via FetcherContext.http (the rate-limited HTTP client).
|
|
7
|
+
*
|
|
8
|
+
* Base URL: https://suibets.replit.app
|
|
9
|
+
*
|
|
10
|
+
* Endpoints:
|
|
11
|
+
* GET /api/p2p/offers - List open P2P offers (status, matchId, sport, limit, offset)
|
|
12
|
+
* GET /api/p2p/offers/:id - Get a single P2P offer by ID
|
|
13
|
+
* GET /api/p2p/my?wallet=... - Get user activity (created offers, matched bets, parlays)
|
|
14
|
+
* GET /api/events/upcoming - List upcoming sports events (sport, limit)
|
|
15
|
+
*/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SuiBets P2P Sports Betting API Reference
|
|
4
|
+
*
|
|
5
|
+
* This file documents the SuiBets REST API endpoints used by the fetcher.
|
|
6
|
+
* It is NOT wired into defineImplicitApi — the fetcher calls these endpoints
|
|
7
|
+
* directly via FetcherContext.http (the rate-limited HTTP client).
|
|
8
|
+
*
|
|
9
|
+
* Base URL: https://suibets.replit.app
|
|
10
|
+
*
|
|
11
|
+
* Endpoints:
|
|
12
|
+
* GET /api/p2p/offers - List open P2P offers (status, matchId, sport, limit, offset)
|
|
13
|
+
* GET /api/p2p/offers/:id - Get a single P2P offer by ID
|
|
14
|
+
* GET /api/p2p/my?wallet=... - Get user activity (created offers, matched bets, parlays)
|
|
15
|
+
* GET /api/events/upcoming - List upcoming sports events (sport, limit)
|
|
16
|
+
*/
|
|
17
|
+
// No runtime exports — this file serves as API documentation only.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const SUIBETS_BASE_URL = "https://suibets.replit.app";
|
|
2
|
+
export declare const SUIBETS_PLATFORM_FEE = 0.02;
|
|
3
|
+
export declare const MIST_PER_SUI = 1000000000;
|
|
4
|
+
export declare const MIN_PRICE = 0.01;
|
|
5
|
+
export declare const MAX_PRICE = 0.99;
|
|
6
|
+
export declare const RATE_LIMIT_MS = 300;
|
|
7
|
+
export declare const ALLOWED_HOSTS: readonly string[];
|
|
8
|
+
/**
|
|
9
|
+
* Validates that the given URL's hostname is in the ALLOWED_HOSTS allowlist.
|
|
10
|
+
* Throws if the hostname is not permitted, to prevent SSRF.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateBaseUrl(url: string): void;
|
|
13
|
+
export interface SuibetsApiConfig {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function getSuibetsConfig(baseUrlOverride?: string): SuibetsApiConfig;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ALLOWED_HOSTS = exports.RATE_LIMIT_MS = exports.MAX_PRICE = exports.MIN_PRICE = exports.MIST_PER_SUI = exports.SUIBETS_PLATFORM_FEE = exports.SUIBETS_BASE_URL = void 0;
|
|
4
|
+
exports.validateBaseUrl = validateBaseUrl;
|
|
5
|
+
exports.getSuibetsConfig = getSuibetsConfig;
|
|
6
|
+
exports.SUIBETS_BASE_URL = 'https://suibets.replit.app';
|
|
7
|
+
// SuiBets is a P2P sports betting platform on Sui blockchain.
|
|
8
|
+
// Platform takes a 2% fee on settled markets.
|
|
9
|
+
exports.SUIBETS_PLATFORM_FEE = 0.02;
|
|
10
|
+
// Sui uses MIST as its base unit; 1 SUI = 1,000,000,000 MIST
|
|
11
|
+
exports.MIST_PER_SUI = 1e9;
|
|
12
|
+
// Prices represent probabilities in the range [0.01, 0.99]
|
|
13
|
+
exports.MIN_PRICE = 0.01;
|
|
14
|
+
exports.MAX_PRICE = 0.99;
|
|
15
|
+
// Minimum delay between outbound requests (milliseconds)
|
|
16
|
+
exports.RATE_LIMIT_MS = 300;
|
|
17
|
+
// Allowlist of permitted hostnames for SSRF protection
|
|
18
|
+
exports.ALLOWED_HOSTS = ['suibets.replit.app'];
|
|
19
|
+
/**
|
|
20
|
+
* Validates that the given URL's hostname is in the ALLOWED_HOSTS allowlist.
|
|
21
|
+
* Throws if the hostname is not permitted, to prevent SSRF.
|
|
22
|
+
*/
|
|
23
|
+
function validateBaseUrl(url) {
|
|
24
|
+
const parsed = new URL(url);
|
|
25
|
+
if (!exports.ALLOWED_HOSTS.includes(parsed.hostname)) {
|
|
26
|
+
throw new Error(`Base URL hostname "${parsed.hostname}" is not in the SSRF allowlist. ` +
|
|
27
|
+
`Permitted hosts: ${exports.ALLOWED_HOSTS.join(', ')}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function getSuibetsConfig(baseUrlOverride) {
|
|
31
|
+
const baseUrl = baseUrlOverride ?? exports.SUIBETS_BASE_URL;
|
|
32
|
+
validateBaseUrl(baseUrl);
|
|
33
|
+
return { baseUrl };
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ErrorMapper } from '../../utils/error-mapper';
|
|
2
|
+
/**
|
|
3
|
+
* Maps SuiBets API errors to PMXT unified error classes.
|
|
4
|
+
*
|
|
5
|
+
* SuiBets is a read-only public API, so error mapping focuses on
|
|
6
|
+
* network errors and rate limits. Error responses are expected in the form:
|
|
7
|
+
* { error: string }
|
|
8
|
+
* or:
|
|
9
|
+
* { message: string }
|
|
10
|
+
*/
|
|
11
|
+
export declare class SuibetsErrorMapper extends ErrorMapper {
|
|
12
|
+
constructor();
|
|
13
|
+
protected extractErrorMessage(error: unknown): string;
|
|
14
|
+
mapError(error: unknown): ReturnType<ErrorMapper['mapError']>;
|
|
15
|
+
}
|
|
16
|
+
export declare const suibetsErrorMapper: SuibetsErrorMapper;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.suibetsErrorMapper = exports.SuibetsErrorMapper = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const error_mapper_1 = require("../../utils/error-mapper");
|
|
9
|
+
const errors_1 = require("../../errors");
|
|
10
|
+
/**
|
|
11
|
+
* Maps SuiBets API errors to PMXT unified error classes.
|
|
12
|
+
*
|
|
13
|
+
* SuiBets is a read-only public API, so error mapping focuses on
|
|
14
|
+
* network errors and rate limits. Error responses are expected in the form:
|
|
15
|
+
* { error: string }
|
|
16
|
+
* or:
|
|
17
|
+
* { message: string }
|
|
18
|
+
*/
|
|
19
|
+
class SuibetsErrorMapper extends error_mapper_1.ErrorMapper {
|
|
20
|
+
constructor() {
|
|
21
|
+
super('SuiBets');
|
|
22
|
+
}
|
|
23
|
+
extractErrorMessage(error) {
|
|
24
|
+
if (axios_1.default.isAxiosError(error) && error.response?.data) {
|
|
25
|
+
const data = error.response.data;
|
|
26
|
+
if (typeof data === 'string') {
|
|
27
|
+
return `[${error.response.status}] ${data}`;
|
|
28
|
+
}
|
|
29
|
+
if (typeof data === 'object' && data !== null) {
|
|
30
|
+
const obj = data;
|
|
31
|
+
if (typeof obj.error === 'string') {
|
|
32
|
+
return `[${error.response.status}] ${obj.error}`;
|
|
33
|
+
}
|
|
34
|
+
if (typeof obj.message === 'string') {
|
|
35
|
+
return `[${error.response.status}] ${obj.message}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return super.extractErrorMessage(error);
|
|
40
|
+
}
|
|
41
|
+
mapError(error) {
|
|
42
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
43
|
+
const status = error.response?.status;
|
|
44
|
+
if (status === 429) {
|
|
45
|
+
const retryAfter = error.response?.headers?.['retry-after'];
|
|
46
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : undefined;
|
|
47
|
+
return new errors_1.RateLimitExceeded(this.extractErrorMessage(error), retryAfterSeconds, this.exchangeName);
|
|
48
|
+
}
|
|
49
|
+
if (status === 401 || status === 403) {
|
|
50
|
+
return new errors_1.AuthenticationError(this.extractErrorMessage(error), this.exchangeName);
|
|
51
|
+
}
|
|
52
|
+
if (status !== undefined && status >= 500) {
|
|
53
|
+
return new errors_1.ExchangeNotAvailable(`Exchange error (${status}): ${this.extractErrorMessage(error)}`, this.exchangeName);
|
|
54
|
+
}
|
|
55
|
+
if (!status) {
|
|
56
|
+
return new errors_1.NetworkError(`Network error: ${this.extractErrorMessage(error)}`, this.exchangeName);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (error instanceof Error && !axios_1.default.isAxiosError(error)) {
|
|
60
|
+
const nodeErr = error;
|
|
61
|
+
if (nodeErr.code === 'ECONNREFUSED' ||
|
|
62
|
+
nodeErr.code === 'ENOTFOUND' ||
|
|
63
|
+
nodeErr.code === 'ETIMEDOUT') {
|
|
64
|
+
return new errors_1.NetworkError(`Network error: ${error.message}`, this.exchangeName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return super.mapError(error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.SuibetsErrorMapper = SuibetsErrorMapper;
|
|
71
|
+
exports.suibetsErrorMapper = new SuibetsErrorMapper();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { MarketFilterParams, EventFetchParams } from '../../BaseExchange';
|
|
2
|
+
import { IExchangeFetcher, FetcherContext } from '../interfaces';
|
|
3
|
+
export interface SuibetsRawOffer {
|
|
4
|
+
id: string;
|
|
5
|
+
matchId: string;
|
|
6
|
+
matchName: string;
|
|
7
|
+
sport: string;
|
|
8
|
+
homeTeam: string;
|
|
9
|
+
awayTeam: string;
|
|
10
|
+
creatorWallet: string;
|
|
11
|
+
creatorTeam: string;
|
|
12
|
+
creatorOdds: number;
|
|
13
|
+
creatorStake: number;
|
|
14
|
+
takerStake: number;
|
|
15
|
+
remainingStake?: number;
|
|
16
|
+
matchDate: string;
|
|
17
|
+
expiresAt: string;
|
|
18
|
+
status: string;
|
|
19
|
+
totalMatched?: number;
|
|
20
|
+
currency?: string;
|
|
21
|
+
isOnchain?: boolean;
|
|
22
|
+
onchainOfferId?: string;
|
|
23
|
+
leagueName?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface SuibetsRawEvent {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
homeTeam: string;
|
|
29
|
+
awayTeam: string;
|
|
30
|
+
sport: string;
|
|
31
|
+
leagueName?: string;
|
|
32
|
+
matchDate: string;
|
|
33
|
+
status: string;
|
|
34
|
+
offers?: SuibetsRawOffer[];
|
|
35
|
+
}
|
|
36
|
+
export declare class SuibetsFetcher implements IExchangeFetcher<SuibetsRawOffer, SuibetsRawEvent> {
|
|
37
|
+
private readonly ctx;
|
|
38
|
+
private readonly baseUrl;
|
|
39
|
+
constructor(ctx: FetcherContext, baseUrl: string);
|
|
40
|
+
/**
|
|
41
|
+
* Performs a GET request via the rate-limited HTTP client provided by the
|
|
42
|
+
* base class. All errors are mapped to pmxt unified error types.
|
|
43
|
+
*/
|
|
44
|
+
private get;
|
|
45
|
+
/**
|
|
46
|
+
* Fetches raw P2P bet offers from the SuiBets API.
|
|
47
|
+
*
|
|
48
|
+
* When `params.query` is set, filtering is applied client-side after
|
|
49
|
+
* fetching because the API does not support full-text search.
|
|
50
|
+
*/
|
|
51
|
+
fetchRawMarkets(params?: MarketFilterParams): Promise<SuibetsRawOffer[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Fetches raw events by grouping active P2P offers by their matchId.
|
|
54
|
+
*
|
|
55
|
+
* SuiBets has no dedicated events endpoint; events are synthesised from
|
|
56
|
+
* the offers list so each unique match becomes one event.
|
|
57
|
+
*/
|
|
58
|
+
fetchRawEvents(params: EventFetchParams): Promise<SuibetsRawEvent[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Fetches raw positions (created offers, matched bets, parlays) for a
|
|
61
|
+
* given Sui wallet address.
|
|
62
|
+
*/
|
|
63
|
+
fetchRawPositions(walletAddress: string): Promise<unknown[]>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SuibetsFetcher = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
class SuibetsFetcher {
|
|
6
|
+
ctx;
|
|
7
|
+
baseUrl;
|
|
8
|
+
constructor(ctx, baseUrl) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Performs a GET request via the rate-limited HTTP client provided by the
|
|
14
|
+
* base class. All errors are mapped to pmxt unified error types.
|
|
15
|
+
*/
|
|
16
|
+
async get(path, params) {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(path, this.baseUrl);
|
|
19
|
+
if (params) {
|
|
20
|
+
for (const [k, v] of Object.entries(params)) {
|
|
21
|
+
url.searchParams.set(k, v);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const response = await this.ctx.http.get(url.toString(), {
|
|
25
|
+
maxContentLength: 5 * 1024 * 1024,
|
|
26
|
+
});
|
|
27
|
+
return response.data;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw errors_1.suibetsErrorMapper.mapError(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fetches raw P2P bet offers from the SuiBets API.
|
|
35
|
+
*
|
|
36
|
+
* When `params.query` is set, filtering is applied client-side after
|
|
37
|
+
* fetching because the API does not support full-text search.
|
|
38
|
+
*/
|
|
39
|
+
async fetchRawMarkets(params) {
|
|
40
|
+
if (params?.marketId) {
|
|
41
|
+
const id = params.marketId.replace(/^suibets:/, '');
|
|
42
|
+
const data = await this.get(`/api/p2p/offers/${id}`);
|
|
43
|
+
const offer = data.offer ?? data;
|
|
44
|
+
return offer ? [offer] : [];
|
|
45
|
+
}
|
|
46
|
+
const baseParams = {
|
|
47
|
+
status: params?.status === 'all' ? 'all' : 'OPEN',
|
|
48
|
+
limit: String(params?.limit ?? 50),
|
|
49
|
+
offset: String(params?.offset ?? 0),
|
|
50
|
+
};
|
|
51
|
+
const queryParams = params?.eventId
|
|
52
|
+
? { ...baseParams, matchId: params.eventId.replace(/^suibets:/, '') }
|
|
53
|
+
: { ...baseParams };
|
|
54
|
+
const data = await this.get('/api/p2p/offers', queryParams);
|
|
55
|
+
const offers = data.offers ??
|
|
56
|
+
(Array.isArray(data) ? data : []);
|
|
57
|
+
if (!params?.query) {
|
|
58
|
+
return offers;
|
|
59
|
+
}
|
|
60
|
+
// Client-side text filter: the API has no search endpoint.
|
|
61
|
+
const q = params.query.toLowerCase();
|
|
62
|
+
return offers.filter(o => o.matchName?.toLowerCase().includes(q) ||
|
|
63
|
+
o.homeTeam?.toLowerCase().includes(q) ||
|
|
64
|
+
o.awayTeam?.toLowerCase().includes(q) ||
|
|
65
|
+
o.sport?.toLowerCase().includes(q));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Fetches raw events by grouping active P2P offers by their matchId.
|
|
69
|
+
*
|
|
70
|
+
* SuiBets has no dedicated events endpoint; events are synthesised from
|
|
71
|
+
* the offers list so each unique match becomes one event.
|
|
72
|
+
*/
|
|
73
|
+
async fetchRawEvents(params) {
|
|
74
|
+
const queryParams = {
|
|
75
|
+
status: 'OPEN',
|
|
76
|
+
limit: String(params.limit ?? 100),
|
|
77
|
+
};
|
|
78
|
+
const data = await this.get('/api/p2p/offers', queryParams);
|
|
79
|
+
const offers = data.offers ??
|
|
80
|
+
(Array.isArray(data) ? data : []);
|
|
81
|
+
// Group offers by matchId using a Map; each entry is built immutably.
|
|
82
|
+
const byMatch = new Map();
|
|
83
|
+
for (const offer of offers) {
|
|
84
|
+
if (!offer.matchId)
|
|
85
|
+
continue;
|
|
86
|
+
const existing = byMatch.get(offer.matchId) ?? [];
|
|
87
|
+
byMatch.set(offer.matchId, [...existing, offer]);
|
|
88
|
+
}
|
|
89
|
+
const q = params.query?.toLowerCase();
|
|
90
|
+
const events = [];
|
|
91
|
+
for (const [matchId, matchOffers] of byMatch) {
|
|
92
|
+
const first = matchOffers[0];
|
|
93
|
+
if (q) {
|
|
94
|
+
const matches = first.matchName?.toLowerCase().includes(q) ||
|
|
95
|
+
first.homeTeam?.toLowerCase().includes(q) ||
|
|
96
|
+
first.awayTeam?.toLowerCase().includes(q) ||
|
|
97
|
+
first.sport?.toLowerCase().includes(q);
|
|
98
|
+
if (!matches)
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
events.push({
|
|
102
|
+
id: matchId,
|
|
103
|
+
name: first.matchName || `${first.homeTeam} vs ${first.awayTeam}`,
|
|
104
|
+
homeTeam: first.homeTeam,
|
|
105
|
+
awayTeam: first.awayTeam,
|
|
106
|
+
sport: first.sport,
|
|
107
|
+
leagueName: first.leagueName,
|
|
108
|
+
matchDate: first.matchDate,
|
|
109
|
+
status: 'active',
|
|
110
|
+
offers: matchOffers,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return events;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Fetches raw positions (created offers, matched bets, parlays) for a
|
|
117
|
+
* given Sui wallet address.
|
|
118
|
+
*/
|
|
119
|
+
async fetchRawPositions(walletAddress) {
|
|
120
|
+
const data = await this.get('/api/p2p/my', { wallet: walletAddress });
|
|
121
|
+
return [
|
|
122
|
+
...(data.createdOffers ?? []),
|
|
123
|
+
...(data.matchedBets ?? []),
|
|
124
|
+
...(data.parlays ?? []),
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports.SuibetsFetcher = SuibetsFetcher;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PredictionMarketExchange, MarketFilterParams, EventFetchParams, ExchangeCredentials } from '../../BaseExchange';
|
|
2
|
+
import { UnifiedMarket, UnifiedEvent, OrderBook, Position } from '../../types';
|
|
3
|
+
export interface SuibetsCredentials extends ExchangeCredentials {
|
|
4
|
+
/** Sui wallet address for fetching personal positions */
|
|
5
|
+
walletAddress?: string;
|
|
6
|
+
/** Override API base URL (default: https://suibets.replit.app) */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* SuiBets — Decentralised P2P sports betting on Sui blockchain.
|
|
11
|
+
*
|
|
12
|
+
* Maps P2P bet offers to the pmxt unified market model:
|
|
13
|
+
* - Market = one P2P offer (creator side vs taker side)
|
|
14
|
+
* - Event = a sports match (groups all offers for that match)
|
|
15
|
+
* - Outcome = creator's pick (YES) or opposite (NO)
|
|
16
|
+
* - Price = implied probability derived from the offer odds
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* ```ts
|
|
20
|
+
* import pmxt from 'pmxtjs';
|
|
21
|
+
* const exchange = new pmxt.SuiBets();
|
|
22
|
+
* const markets = await exchange.fetchMarkets({ limit: 20 });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class SuiBetsExchange extends PredictionMarketExchange {
|
|
26
|
+
protected readonly capabilityOverrides: {
|
|
27
|
+
fetchOrderBook: "emulated";
|
|
28
|
+
createOrder: false;
|
|
29
|
+
cancelOrder: false;
|
|
30
|
+
fetchOrder: false;
|
|
31
|
+
fetchOpenOrders: false;
|
|
32
|
+
fetchBalance: false;
|
|
33
|
+
fetchPositions: true;
|
|
34
|
+
watchOrderBook: false;
|
|
35
|
+
watchTrades: false;
|
|
36
|
+
};
|
|
37
|
+
private readonly config;
|
|
38
|
+
private readonly fetcher;
|
|
39
|
+
private readonly normalizer;
|
|
40
|
+
private readonly walletAddress?;
|
|
41
|
+
constructor(credentials?: SuibetsCredentials);
|
|
42
|
+
get name(): string;
|
|
43
|
+
protected sign(): Record<string, string>;
|
|
44
|
+
protected fetchMarketsImpl(params?: MarketFilterParams): Promise<UnifiedMarket[]>;
|
|
45
|
+
protected fetchEventsImpl(params: EventFetchParams): Promise<UnifiedEvent[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Emulated order book derived from offer odds.
|
|
48
|
+
*
|
|
49
|
+
* Bid side: what buyers pay to back the creator's pick (YES price).
|
|
50
|
+
* Ask side: what sellers want to take the opposite side (NO price).
|
|
51
|
+
*/
|
|
52
|
+
fetchOrderBook(outcomeId: string): Promise<OrderBook>;
|
|
53
|
+
fetchPositions(): Promise<Position[]>;
|
|
54
|
+
}
|