@catalyst-team/poly-sdk 0.4.6 → 0.5.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/README.md +18 -9
- package/README.zh-CN.md +18 -9
- package/dist/src/clients/data-api.d.ts +26 -0
- package/dist/src/clients/data-api.d.ts.map +1 -1
- package/dist/src/clients/data-api.js +58 -0
- package/dist/src/clients/data-api.js.map +1 -1
- package/dist/src/core/types.d.ts +56 -1
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/realtime/index.d.ts +18 -0
- package/dist/src/realtime/index.d.ts.map +1 -0
- package/dist/src/realtime/index.js +14 -0
- package/dist/src/realtime/index.js.map +1 -0
- package/dist/src/realtime/realtime-data-client.d.ts +274 -0
- package/dist/src/realtime/realtime-data-client.d.ts.map +1 -0
- package/dist/src/realtime/realtime-data-client.js +771 -0
- package/dist/src/realtime/realtime-data-client.js.map +1 -0
- package/dist/src/realtime/types.d.ts +485 -0
- package/dist/src/realtime/types.d.ts.map +1 -0
- package/dist/src/realtime/types.js +36 -0
- package/dist/src/realtime/types.js.map +1 -0
- package/dist/src/services/arbitrage-service.d.ts.map +1 -1
- package/dist/src/services/arbitrage-service.js +2 -1
- package/dist/src/services/arbitrage-service.js.map +1 -1
- package/dist/src/services/dip-arb-service.d.ts.map +1 -1
- package/dist/src/services/dip-arb-service.js +3 -19
- package/dist/src/services/dip-arb-service.js.map +1 -1
- package/dist/src/services/market-service.d.ts +93 -11
- package/dist/src/services/market-service.d.ts.map +1 -1
- package/dist/src/services/market-service.js +189 -22
- package/dist/src/services/market-service.js.map +1 -1
- package/dist/src/services/order-handle.test.d.ts +15 -0
- package/dist/src/services/order-handle.test.d.ts.map +1 -0
- package/dist/src/services/order-handle.test.js +333 -0
- package/dist/src/services/order-handle.test.js.map +1 -0
- package/dist/src/services/order-manager.d.ts +162 -6
- package/dist/src/services/order-manager.d.ts.map +1 -1
- package/dist/src/services/order-manager.js +419 -30
- package/dist/src/services/order-manager.js.map +1 -1
- package/dist/src/services/realtime-service-v2.d.ts +122 -68
- package/dist/src/services/realtime-service-v2.d.ts.map +1 -1
- package/dist/src/services/realtime-service-v2.js +475 -144
- package/dist/src/services/realtime-service-v2.js.map +1 -1
- package/dist/src/services/smart-money-service.d.ts +33 -10
- package/dist/src/services/smart-money-service.d.ts.map +1 -1
- package/dist/src/services/smart-money-service.js +180 -73
- package/dist/src/services/smart-money-service.js.map +1 -1
- package/dist/src/services/trading-service.d.ts +129 -1
- package/dist/src/services/trading-service.d.ts.map +1 -1
- package/dist/src/services/trading-service.js +198 -5
- package/dist/src/services/trading-service.js.map +1 -1
- package/package.json +1 -2
- package/dist/src/services/ctf-detector.d.ts +0 -215
- package/dist/src/services/ctf-detector.d.ts.map +0 -1
- package/dist/src/services/ctf-detector.js +0 -420
- package/dist/src/services/ctf-detector.js.map +0 -1
|
@@ -13,18 +13,43 @@
|
|
|
13
13
|
* - rfq: request_*, quote_*
|
|
14
14
|
*/
|
|
15
15
|
import { EventEmitter } from 'events';
|
|
16
|
-
import { RealTimeDataClient, ConnectionStatus, } from '
|
|
16
|
+
import { RealTimeDataClient, ConnectionStatus, WS_ENDPOINTS, } from '../realtime/index.js';
|
|
17
17
|
// ============================================================================
|
|
18
18
|
// RealtimeServiceV2 Implementation
|
|
19
19
|
// ============================================================================
|
|
20
20
|
export class RealtimeServiceV2 extends EventEmitter {
|
|
21
21
|
client = null;
|
|
22
|
+
/** Separate client for user channel (uses USER endpoint) */
|
|
23
|
+
userClient = null;
|
|
24
|
+
/** Separate client for crypto prices (uses LIVE_DATA endpoint) */
|
|
25
|
+
cryptoClient = null;
|
|
22
26
|
config;
|
|
23
27
|
subscriptions = new Map();
|
|
24
28
|
subscriptionIdCounter = 0;
|
|
25
29
|
connected = false;
|
|
30
|
+
userConnected = false;
|
|
31
|
+
cryptoConnected = false;
|
|
32
|
+
connectResolve;
|
|
33
|
+
// Subscription refresh timer: re-sends subscriptions shortly after they're added
|
|
34
|
+
// This fixes a bug where initial subscriptions on a fresh connection only receive
|
|
35
|
+
// the snapshot but no updates. Re-sending them "wakes up" the server.
|
|
36
|
+
subscriptionRefreshTimer = null;
|
|
37
|
+
// Track subscriptions that need to be refreshed (newly added on this connection)
|
|
38
|
+
pendingRefreshSubIds = new Set();
|
|
39
|
+
// Connection generation counter: incremented on each new connection.
|
|
40
|
+
// Used to avoid sending unsubscribe for stale subscriptions after reconnection.
|
|
41
|
+
connectionGeneration = 0;
|
|
42
|
+
// Tracks which generation each subscription was last (re-)subscribed on
|
|
43
|
+
subscriptionGenerations = new Map();
|
|
26
44
|
// Store subscription messages for reconnection
|
|
27
45
|
subscriptionMessages = new Map();
|
|
46
|
+
// Store user credentials for reconnection
|
|
47
|
+
userCredentials = null;
|
|
48
|
+
// Accumulated market token IDs - we merge all markets into a single subscription
|
|
49
|
+
// to avoid server overwriting previous subscriptions with same topic+type
|
|
50
|
+
accumulatedMarketTokenIds = new Set();
|
|
51
|
+
// Timer to batch market subscription updates
|
|
52
|
+
marketSubscriptionBatchTimer = null;
|
|
28
53
|
// Caches
|
|
29
54
|
priceCache = new Map();
|
|
30
55
|
bookCache = new Map();
|
|
@@ -43,31 +68,124 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
43
68
|
/**
|
|
44
69
|
* Connect to WebSocket server
|
|
45
70
|
*/
|
|
46
|
-
connect() {
|
|
71
|
+
async connect() {
|
|
47
72
|
if (this.client) {
|
|
48
73
|
this.log('Already connected or connecting');
|
|
49
|
-
return
|
|
74
|
+
return;
|
|
50
75
|
}
|
|
76
|
+
// Promises to track when all clients connect
|
|
77
|
+
const mainConnectPromise = new Promise((resolve) => {
|
|
78
|
+
this.connectResolve = resolve;
|
|
79
|
+
});
|
|
80
|
+
let userConnectResolve;
|
|
81
|
+
const userConnectPromise = new Promise((resolve) => {
|
|
82
|
+
userConnectResolve = resolve;
|
|
83
|
+
});
|
|
84
|
+
let cryptoConnectResolve;
|
|
85
|
+
const cryptoConnectPromise = new Promise((resolve) => {
|
|
86
|
+
cryptoConnectResolve = resolve;
|
|
87
|
+
});
|
|
88
|
+
// Main client for MARKET/USER channels
|
|
51
89
|
this.client = new RealTimeDataClient({
|
|
52
90
|
onConnect: this.handleConnect.bind(this),
|
|
53
91
|
onMessage: this.handleMessage.bind(this),
|
|
54
92
|
onStatusChange: this.handleStatusChange.bind(this),
|
|
55
93
|
autoReconnect: this.config.autoReconnect,
|
|
56
94
|
pingInterval: this.config.pingInterval,
|
|
95
|
+
debug: this.config.debug,
|
|
96
|
+
});
|
|
97
|
+
// User client for USER channel (clob_user events)
|
|
98
|
+
this.userClient = new RealTimeDataClient({
|
|
99
|
+
url: WS_ENDPOINTS.USER,
|
|
100
|
+
onConnect: (client) => {
|
|
101
|
+
this.handleUserConnect(client);
|
|
102
|
+
userConnectResolve();
|
|
103
|
+
},
|
|
104
|
+
onMessage: this.handleUserChannelMessage.bind(this),
|
|
105
|
+
onStatusChange: (status) => {
|
|
106
|
+
this.log(`User client status: ${status}`);
|
|
107
|
+
this.userConnected = status === ConnectionStatus.CONNECTED;
|
|
108
|
+
},
|
|
109
|
+
autoReconnect: this.config.autoReconnect,
|
|
110
|
+
pingInterval: this.config.pingInterval,
|
|
111
|
+
debug: this.config.debug,
|
|
112
|
+
});
|
|
113
|
+
// Crypto client for LIVE_DATA channel (crypto_prices)
|
|
114
|
+
this.cryptoClient = new RealTimeDataClient({
|
|
115
|
+
url: WS_ENDPOINTS.LIVE_DATA,
|
|
116
|
+
onConnect: (client) => {
|
|
117
|
+
this.handleCryptoConnect(client);
|
|
118
|
+
cryptoConnectResolve();
|
|
119
|
+
},
|
|
120
|
+
onMessage: this.handleCryptoMessage.bind(this),
|
|
121
|
+
onStatusChange: (status) => {
|
|
122
|
+
this.log(`Crypto client status: ${status}`);
|
|
123
|
+
this.cryptoConnected = status === ConnectionStatus.CONNECTED;
|
|
124
|
+
},
|
|
125
|
+
autoReconnect: this.config.autoReconnect,
|
|
126
|
+
pingInterval: this.config.pingInterval,
|
|
127
|
+
debug: this.config.debug,
|
|
57
128
|
});
|
|
58
129
|
this.client.connect();
|
|
59
|
-
|
|
130
|
+
this.userClient.connect();
|
|
131
|
+
this.cryptoClient.connect();
|
|
132
|
+
// Wait for all clients to connect (with timeout)
|
|
133
|
+
const timeout = 10_000;
|
|
134
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
135
|
+
setTimeout(() => reject(new Error('WebSocket connection timeout (10s)')), timeout);
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
await Promise.race([
|
|
139
|
+
Promise.all([mainConnectPromise, userConnectPromise, cryptoConnectPromise]),
|
|
140
|
+
timeoutPromise,
|
|
141
|
+
]);
|
|
142
|
+
this.log('All WebSocket clients connected, ready to subscribe');
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// If timeout, check which clients connected
|
|
146
|
+
const status = {
|
|
147
|
+
main: this.connected,
|
|
148
|
+
user: this.userConnected,
|
|
149
|
+
crypto: this.cryptoConnected,
|
|
150
|
+
};
|
|
151
|
+
this.log(`WebSocket connection warning: ${error}. Status: ${JSON.stringify(status)}`);
|
|
152
|
+
// Continue anyway if main client is connected (minimum required)
|
|
153
|
+
if (!this.connected) {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
60
157
|
}
|
|
61
158
|
/**
|
|
62
159
|
* Disconnect from WebSocket server
|
|
63
160
|
*/
|
|
64
161
|
disconnect() {
|
|
162
|
+
this.cancelSubscriptionRefresh();
|
|
163
|
+
this.cancelMarketSubscriptionBatch();
|
|
65
164
|
if (this.client) {
|
|
66
165
|
this.client.disconnect();
|
|
67
166
|
this.client = null;
|
|
68
167
|
this.connected = false;
|
|
69
|
-
|
|
70
|
-
|
|
168
|
+
}
|
|
169
|
+
if (this.userClient) {
|
|
170
|
+
this.userClient.disconnect();
|
|
171
|
+
this.userClient = null;
|
|
172
|
+
this.userConnected = false;
|
|
173
|
+
}
|
|
174
|
+
if (this.cryptoClient) {
|
|
175
|
+
this.cryptoClient.disconnect();
|
|
176
|
+
this.cryptoClient = null;
|
|
177
|
+
this.cryptoConnected = false;
|
|
178
|
+
}
|
|
179
|
+
this.subscriptions.clear();
|
|
180
|
+
this.subscriptionMessages.clear();
|
|
181
|
+
this.subscriptionGenerations.clear();
|
|
182
|
+
this.accumulatedMarketTokenIds.clear();
|
|
183
|
+
this.userCredentials = null;
|
|
184
|
+
}
|
|
185
|
+
cancelMarketSubscriptionBatch() {
|
|
186
|
+
if (this.marketSubscriptionBatchTimer) {
|
|
187
|
+
clearTimeout(this.marketSubscriptionBatchTimer);
|
|
188
|
+
this.marketSubscriptionBatchTimer = null;
|
|
71
189
|
}
|
|
72
190
|
}
|
|
73
191
|
/**
|
|
@@ -83,21 +201,22 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
83
201
|
* Subscribe to market data (orderbook, prices, trades)
|
|
84
202
|
* @param tokenIds - Array of token IDs to subscribe to
|
|
85
203
|
* @param handlers - Event handlers
|
|
204
|
+
*
|
|
205
|
+
* IMPORTANT: This method uses an accumulation strategy. Instead of sending
|
|
206
|
+
* separate subscription messages for each market, we accumulate all token IDs
|
|
207
|
+
* and send a single merged subscription. This prevents the server from
|
|
208
|
+
* overwriting previous subscriptions (which happens when multiple messages
|
|
209
|
+
* have the same topic+type but different filters).
|
|
86
210
|
*/
|
|
87
211
|
subscribeMarkets(tokenIds, handlers = {}) {
|
|
88
212
|
const subId = `market_${++this.subscriptionIdCounter}`;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
];
|
|
97
|
-
const subMsg = { subscriptions };
|
|
98
|
-
this.sendSubscription(subMsg);
|
|
99
|
-
this.subscriptionMessages.set(subId, subMsg); // Store for reconnection
|
|
100
|
-
// Register handlers
|
|
213
|
+
// Add new token IDs to accumulated set
|
|
214
|
+
for (const tokenId of tokenIds) {
|
|
215
|
+
this.accumulatedMarketTokenIds.add(tokenId);
|
|
216
|
+
}
|
|
217
|
+
// Schedule a batched subscription update (debounced)
|
|
218
|
+
this.scheduleMergedMarketSubscription();
|
|
219
|
+
// Register handlers (filtered by this subscription's tokenIds)
|
|
101
220
|
const orderbookHandler = (book) => {
|
|
102
221
|
if (tokenIds.includes(book.assetId)) {
|
|
103
222
|
handlers.onOrderbook?.(book);
|
|
@@ -118,10 +237,16 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
118
237
|
handlers.onTickSizeChange?.(change);
|
|
119
238
|
}
|
|
120
239
|
};
|
|
240
|
+
const bestBidAskHandler = (bba) => {
|
|
241
|
+
if (tokenIds.includes(bba.assetId)) {
|
|
242
|
+
handlers.onBestBidAsk?.(bba);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
121
245
|
this.on('orderbook', orderbookHandler);
|
|
122
246
|
this.on('priceChange', priceChangeHandler);
|
|
123
247
|
this.on('lastTrade', lastTradeHandler);
|
|
124
248
|
this.on('tickSizeChange', tickSizeHandler);
|
|
249
|
+
this.on('bestBidAsk', bestBidAskHandler);
|
|
125
250
|
const subscription = {
|
|
126
251
|
id: subId,
|
|
127
252
|
topic: 'clob_market',
|
|
@@ -132,14 +257,64 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
132
257
|
this.off('priceChange', priceChangeHandler);
|
|
133
258
|
this.off('lastTrade', lastTradeHandler);
|
|
134
259
|
this.off('tickSizeChange', tickSizeHandler);
|
|
135
|
-
this.
|
|
260
|
+
this.off('bestBidAsk', bestBidAskHandler);
|
|
261
|
+
// Remove these token IDs from accumulated set
|
|
262
|
+
for (const tokenId of tokenIds) {
|
|
263
|
+
this.accumulatedMarketTokenIds.delete(tokenId);
|
|
264
|
+
}
|
|
265
|
+
// Re-subscribe with remaining tokens (or send empty to clear)
|
|
266
|
+
this.scheduleMergedMarketSubscription();
|
|
136
267
|
this.subscriptions.delete(subId);
|
|
137
|
-
this.subscriptionMessages.delete(subId); // Remove from reconnection list
|
|
138
268
|
},
|
|
139
269
|
};
|
|
140
270
|
this.subscriptions.set(subId, subscription);
|
|
141
271
|
return subscription;
|
|
142
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Schedule a merged market subscription update.
|
|
275
|
+
* Debounces multiple rapid subscription changes into a single WebSocket message.
|
|
276
|
+
*/
|
|
277
|
+
scheduleMergedMarketSubscription() {
|
|
278
|
+
// Clear existing timer
|
|
279
|
+
if (this.marketSubscriptionBatchTimer) {
|
|
280
|
+
clearTimeout(this.marketSubscriptionBatchTimer);
|
|
281
|
+
}
|
|
282
|
+
// Schedule subscription send after a short delay (100ms) to batch rapid changes
|
|
283
|
+
this.marketSubscriptionBatchTimer = setTimeout(() => {
|
|
284
|
+
this.marketSubscriptionBatchTimer = null;
|
|
285
|
+
this.sendMergedMarketSubscription();
|
|
286
|
+
}, 100);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Send a single merged subscription containing all accumulated market token IDs.
|
|
290
|
+
*/
|
|
291
|
+
sendMergedMarketSubscription() {
|
|
292
|
+
if (!this.client || !this.connected) {
|
|
293
|
+
this.log('Cannot send merged subscription: not connected');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const allTokenIds = Array.from(this.accumulatedMarketTokenIds);
|
|
297
|
+
if (allTokenIds.length === 0) {
|
|
298
|
+
this.log('No market tokens to subscribe to');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const filterStr = JSON.stringify(allTokenIds);
|
|
302
|
+
const subscriptions = [
|
|
303
|
+
{ topic: 'clob_market', type: 'agg_orderbook', filters: filterStr },
|
|
304
|
+
{ topic: 'clob_market', type: 'price_change', filters: filterStr },
|
|
305
|
+
{ topic: 'clob_market', type: 'last_trade_price', filters: filterStr },
|
|
306
|
+
{ topic: 'clob_market', type: 'tick_size_change', filters: filterStr },
|
|
307
|
+
{ topic: 'clob_market', type: 'best_bid_ask', filters: filterStr },
|
|
308
|
+
];
|
|
309
|
+
const subMsg = { subscriptions };
|
|
310
|
+
this.log(`Sending merged market subscription with ${allTokenIds.length} tokens`);
|
|
311
|
+
this.client.subscribe(subMsg);
|
|
312
|
+
// Store for reconnection (use a fixed key for the merged subscription)
|
|
313
|
+
this.subscriptionMessages.set('__merged_market__', subMsg);
|
|
314
|
+
this.subscriptionGenerations.set('__merged_market__', this.connectionGeneration);
|
|
315
|
+
// Schedule refresh to ensure we receive updates (not just snapshot)
|
|
316
|
+
this.scheduleSubscriptionRefresh('__merged_market__');
|
|
317
|
+
}
|
|
143
318
|
/**
|
|
144
319
|
* Subscribe to a single market (YES + NO tokens)
|
|
145
320
|
* Also emits derived price updates compatible with old API
|
|
@@ -240,15 +415,25 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
240
415
|
// ============================================================================
|
|
241
416
|
/**
|
|
242
417
|
* Subscribe to user order and trade events
|
|
418
|
+
*
|
|
419
|
+
* User channel requires a separate WebSocket endpoint (USER endpoint).
|
|
420
|
+
* Subscription format: { type: 'USER', auth: { apiKey, secret, passphrase } }
|
|
421
|
+
*
|
|
243
422
|
* @param credentials - CLOB API credentials
|
|
244
423
|
* @param handlers - Event handlers
|
|
245
424
|
*/
|
|
246
425
|
subscribeUserEvents(credentials, handlers = {}) {
|
|
247
426
|
const subId = `user_${++this.subscriptionIdCounter}`;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.
|
|
427
|
+
// Store credentials for reconnection
|
|
428
|
+
this.userCredentials = credentials;
|
|
429
|
+
// Send subscription using the user client
|
|
430
|
+
if (this.userClient && this.userConnected) {
|
|
431
|
+
this.log('Sending user subscription via user client');
|
|
432
|
+
this.userClient.subscribeUser(credentials);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
this.log('User client not connected, will subscribe on connect');
|
|
436
|
+
}
|
|
252
437
|
const orderHandler = (order) => handlers.onOrder?.(order);
|
|
253
438
|
const tradeHandler = (trade) => handlers.onTrade?.(trade);
|
|
254
439
|
this.on('userOrder', orderHandler);
|
|
@@ -260,82 +445,30 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
260
445
|
unsubscribe: () => {
|
|
261
446
|
this.off('userOrder', orderHandler);
|
|
262
447
|
this.off('userTrade', tradeHandler);
|
|
263
|
-
this.
|
|
264
|
-
this.subscriptions.delete(subId);
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
this.subscriptions.set(subId, subscription);
|
|
268
|
-
return subscription;
|
|
269
|
-
}
|
|
270
|
-
// ============================================================================
|
|
271
|
-
// Activity Subscriptions (trades, orders_matched)
|
|
272
|
-
// ============================================================================
|
|
273
|
-
/**
|
|
274
|
-
* Subscribe to trading activity for a market or event
|
|
275
|
-
* @param filter - Event or market slug (optional - if empty, subscribes to all activity)
|
|
276
|
-
* @param handlers - Event handlers
|
|
277
|
-
*/
|
|
278
|
-
subscribeActivity(filter = {}, handlers = {}) {
|
|
279
|
-
const subId = `activity_${++this.subscriptionIdCounter}`;
|
|
280
|
-
// Build filter object with snake_case keys (as expected by the server)
|
|
281
|
-
// Only include filters if we have actual filter values
|
|
282
|
-
const hasFilter = filter.eventSlug || filter.marketSlug;
|
|
283
|
-
const filterObj = {};
|
|
284
|
-
if (filter.eventSlug)
|
|
285
|
-
filterObj.event_slug = filter.eventSlug;
|
|
286
|
-
if (filter.marketSlug)
|
|
287
|
-
filterObj.market_slug = filter.marketSlug;
|
|
288
|
-
// Create subscription objects - only include filters field if we have filters
|
|
289
|
-
const subscriptions = hasFilter
|
|
290
|
-
? [
|
|
291
|
-
{ topic: 'activity', type: 'trades', filters: JSON.stringify(filterObj) },
|
|
292
|
-
{ topic: 'activity', type: 'orders_matched', filters: JSON.stringify(filterObj) },
|
|
293
|
-
]
|
|
294
|
-
: [
|
|
295
|
-
{ topic: 'activity', type: 'trades' },
|
|
296
|
-
{ topic: 'activity', type: 'orders_matched' },
|
|
297
|
-
];
|
|
298
|
-
this.sendSubscription({ subscriptions });
|
|
299
|
-
const handler = (trade) => handlers.onTrade?.(trade);
|
|
300
|
-
this.on('activityTrade', handler);
|
|
301
|
-
const subscription = {
|
|
302
|
-
id: subId,
|
|
303
|
-
topic: 'activity',
|
|
304
|
-
type: '*',
|
|
305
|
-
unsubscribe: () => {
|
|
306
|
-
this.off('activityTrade', handler);
|
|
307
|
-
this.sendUnsubscription({ subscriptions });
|
|
448
|
+
this.userCredentials = null;
|
|
308
449
|
this.subscriptions.delete(subId);
|
|
309
450
|
},
|
|
310
451
|
};
|
|
311
452
|
this.subscriptions.set(subId, subscription);
|
|
312
453
|
return subscription;
|
|
313
454
|
}
|
|
314
|
-
/**
|
|
315
|
-
* Subscribe to ALL trading activity across all markets (no filtering)
|
|
316
|
-
* This is useful for Copy Trading - monitoring Smart Money across the platform
|
|
317
|
-
* @param handlers - Event handlers
|
|
318
|
-
*/
|
|
319
|
-
subscribeAllActivity(handlers = {}) {
|
|
320
|
-
return this.subscribeActivity({}, handlers);
|
|
321
|
-
}
|
|
322
455
|
// ============================================================================
|
|
323
456
|
// Crypto Price Subscriptions
|
|
324
457
|
// ============================================================================
|
|
325
458
|
/**
|
|
326
|
-
* Subscribe to crypto price updates
|
|
327
|
-
*
|
|
459
|
+
* Subscribe to crypto price updates (Binance)
|
|
460
|
+
*
|
|
461
|
+
* Uses lowercase symbols: 'btcusdt', 'ethusdt', 'solusdt', 'xrpusdt'
|
|
462
|
+
*
|
|
463
|
+
* @param symbols - Array of lowercase Binance symbols (e.g., ['btcusdt', 'ethusdt'])
|
|
328
464
|
* @param handlers - Event handlers
|
|
329
465
|
*/
|
|
330
466
|
subscribeCryptoPrices(symbols, handlers = {}) {
|
|
331
467
|
const subId = `crypto_${++this.subscriptionIdCounter}`;
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
filters: JSON.stringify({ symbol }),
|
|
337
|
-
}));
|
|
338
|
-
this.sendSubscription({ subscriptions });
|
|
468
|
+
// Use custom RealTimeDataClient method
|
|
469
|
+
if (this.cryptoClient) {
|
|
470
|
+
this.cryptoClient.subscribeCryptoPrices(symbols);
|
|
471
|
+
}
|
|
339
472
|
const handler = (price) => {
|
|
340
473
|
if (symbols.includes(price.symbol)) {
|
|
341
474
|
handlers.onPrice?.(price);
|
|
@@ -348,7 +481,9 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
348
481
|
type: 'update',
|
|
349
482
|
unsubscribe: () => {
|
|
350
483
|
this.off('cryptoPrice', handler);
|
|
351
|
-
this.
|
|
484
|
+
if (this.cryptoClient) {
|
|
485
|
+
this.cryptoClient.unsubscribeCryptoPrices(symbols);
|
|
486
|
+
}
|
|
352
487
|
this.subscriptions.delete(subId);
|
|
353
488
|
},
|
|
354
489
|
};
|
|
@@ -357,18 +492,18 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
357
492
|
}
|
|
358
493
|
/**
|
|
359
494
|
* Subscribe to Chainlink crypto prices
|
|
360
|
-
*
|
|
495
|
+
*
|
|
496
|
+
* Uses lowercase slash-separated symbols: 'btc/usd', 'eth/usd', 'sol/usd', 'xrp/usd'
|
|
497
|
+
*
|
|
498
|
+
* @param symbols - Array of lowercase Chainlink symbols (e.g., ['btc/usd', 'eth/usd'])
|
|
499
|
+
* @param handlers - Event handlers
|
|
361
500
|
*/
|
|
362
501
|
subscribeCryptoChainlinkPrices(symbols, handlers = {}) {
|
|
363
502
|
const subId = `crypto_chainlink_${++this.subscriptionIdCounter}`;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}));
|
|
369
|
-
const subMsg = { subscriptions };
|
|
370
|
-
this.sendSubscription(subMsg);
|
|
371
|
-
this.subscriptionMessages.set(subId, subMsg); // Store for reconnection
|
|
503
|
+
// Use custom RealTimeDataClient method
|
|
504
|
+
if (this.cryptoClient) {
|
|
505
|
+
this.cryptoClient.subscribeCryptoChainlinkPrices(symbols);
|
|
506
|
+
}
|
|
372
507
|
const handler = (price) => {
|
|
373
508
|
if (symbols.includes(price.symbol)) {
|
|
374
509
|
handlers.onPrice?.(price);
|
|
@@ -381,9 +516,10 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
381
516
|
type: 'update',
|
|
382
517
|
unsubscribe: () => {
|
|
383
518
|
this.off('cryptoChainlinkPrice', handler);
|
|
384
|
-
this.
|
|
519
|
+
if (this.cryptoClient) {
|
|
520
|
+
this.cryptoClient.unsubscribeCryptoChainlinkPrices(symbols);
|
|
521
|
+
}
|
|
385
522
|
this.subscriptions.delete(subId);
|
|
386
|
-
this.subscriptionMessages.delete(subId); // Remove from reconnection list
|
|
387
523
|
},
|
|
388
524
|
};
|
|
389
525
|
this.subscriptions.set(subId, subscription);
|
|
@@ -542,21 +678,85 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
542
678
|
sub.unsubscribe();
|
|
543
679
|
}
|
|
544
680
|
this.subscriptions.clear();
|
|
545
|
-
this.subscriptionMessages.clear();
|
|
681
|
+
this.subscriptionMessages.clear();
|
|
682
|
+
this.subscriptionGenerations.clear();
|
|
546
683
|
}
|
|
547
684
|
// ============================================================================
|
|
548
685
|
// Private Methods
|
|
549
686
|
// ============================================================================
|
|
550
|
-
|
|
687
|
+
/**
|
|
688
|
+
* Schedule a subscription refresh after a short delay.
|
|
689
|
+
*
|
|
690
|
+
* Problem: When subscriptions are sent right after connection, the server sometimes
|
|
691
|
+
* only sends the initial snapshot but no subsequent updates. This appears to be a
|
|
692
|
+
* server-side timing issue where the subscription "window" closes before updates flow.
|
|
693
|
+
*
|
|
694
|
+
* Solution: Re-send the subscription after 3 seconds. Polymarket's server apparently
|
|
695
|
+
* accepts duplicate subscriptions and refreshes the stream. Unsubscribe doesn't work
|
|
696
|
+
* (returns "Invalid request body"), so we just re-subscribe.
|
|
697
|
+
*
|
|
698
|
+
* Important: We do NOT cancel existing timers. If multiple subscriptions are added
|
|
699
|
+
* within the 3-second window, they all get added to pendingRefreshSubIds and will
|
|
700
|
+
* be refreshed together when the first timer fires. This ensures all markets get
|
|
701
|
+
* refreshed, not just the last one.
|
|
702
|
+
*/
|
|
703
|
+
scheduleSubscriptionRefresh(subId) {
|
|
704
|
+
this.pendingRefreshSubIds.add(subId);
|
|
705
|
+
// Only create a new timer if one doesn't exist
|
|
706
|
+
// Don't cancel existing timer - it will refresh all pending subscriptions
|
|
707
|
+
if (this.subscriptionRefreshTimer) {
|
|
708
|
+
this.log(`Subscription ${subId} added to pending refresh (timer already scheduled)`);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// Schedule refresh after 3 seconds (enough time for initial snapshot to arrive)
|
|
712
|
+
this.subscriptionRefreshTimer = setTimeout(() => {
|
|
713
|
+
this.subscriptionRefreshTimer = null;
|
|
714
|
+
if (!this.client || !this.connected || this.pendingRefreshSubIds.size === 0) {
|
|
715
|
+
this.pendingRefreshSubIds.clear();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
this.log(`Refreshing ${this.pendingRefreshSubIds.size} subscriptions (re-send)...`);
|
|
719
|
+
for (const pendingSubId of this.pendingRefreshSubIds) {
|
|
720
|
+
const msg = this.subscriptionMessages.get(pendingSubId);
|
|
721
|
+
if (msg) {
|
|
722
|
+
this.log(`Refresh: ${pendingSubId} - re-subscribe`);
|
|
723
|
+
this.client.subscribe(msg);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
this.pendingRefreshSubIds.clear();
|
|
727
|
+
}, 3000);
|
|
728
|
+
}
|
|
729
|
+
cancelSubscriptionRefresh() {
|
|
730
|
+
if (this.subscriptionRefreshTimer) {
|
|
731
|
+
clearTimeout(this.subscriptionRefreshTimer);
|
|
732
|
+
this.subscriptionRefreshTimer = null;
|
|
733
|
+
}
|
|
734
|
+
this.pendingRefreshSubIds.clear();
|
|
735
|
+
}
|
|
736
|
+
handleConnect(_client) {
|
|
551
737
|
this.connected = true;
|
|
552
|
-
this.
|
|
738
|
+
this.connectionGeneration++;
|
|
739
|
+
this.log(`Connected to WebSocket server (generation ${this.connectionGeneration})`);
|
|
740
|
+
// Resolve the connect() promise if waiting
|
|
741
|
+
if (this.connectResolve) {
|
|
742
|
+
this.connectResolve();
|
|
743
|
+
this.connectResolve = undefined;
|
|
744
|
+
}
|
|
553
745
|
// Re-subscribe to all active subscriptions on reconnect
|
|
746
|
+
// Delay subscriptions by 1 second to let the connection stabilize.
|
|
747
|
+
// This helps avoid the "snapshot only, no updates" bug.
|
|
554
748
|
if (this.subscriptionMessages.size > 0) {
|
|
555
|
-
this.log(`Re-subscribing to ${this.subscriptionMessages.size} subscriptions...`);
|
|
556
|
-
|
|
557
|
-
this.
|
|
558
|
-
|
|
559
|
-
|
|
749
|
+
this.log(`Re-subscribing to ${this.subscriptionMessages.size} subscriptions (delayed 1s)...`);
|
|
750
|
+
setTimeout(() => {
|
|
751
|
+
if (!this.client || !this.connected)
|
|
752
|
+
return;
|
|
753
|
+
for (const [subId, msg] of this.subscriptionMessages) {
|
|
754
|
+
this.log(`Re-subscribing: ${subId}`);
|
|
755
|
+
this.client?.subscribe(msg);
|
|
756
|
+
// Update generation so unsubscribe knows it's valid on this connection
|
|
757
|
+
this.subscriptionGenerations.set(subId, this.connectionGeneration);
|
|
758
|
+
}
|
|
759
|
+
}, 1000);
|
|
560
760
|
}
|
|
561
761
|
this.emit('connected');
|
|
562
762
|
}
|
|
@@ -564,6 +764,8 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
564
764
|
this.log(`Connection status: ${status}`);
|
|
565
765
|
if (status === ConnectionStatus.DISCONNECTED) {
|
|
566
766
|
this.connected = false;
|
|
767
|
+
this.cancelSubscriptionRefresh();
|
|
768
|
+
this.cancelMarketSubscriptionBatch();
|
|
567
769
|
this.emit('disconnected');
|
|
568
770
|
}
|
|
569
771
|
else if (status === ConnectionStatus.CONNECTED) {
|
|
@@ -571,7 +773,50 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
571
773
|
}
|
|
572
774
|
this.emit('statusChange', status);
|
|
573
775
|
}
|
|
574
|
-
|
|
776
|
+
handleUserConnect(_client) {
|
|
777
|
+
this.userConnected = true;
|
|
778
|
+
this.log('Connected to user channel WebSocket');
|
|
779
|
+
// Re-subscribe with stored credentials if available
|
|
780
|
+
if (this.userCredentials) {
|
|
781
|
+
this.log('Re-subscribing to user events with stored credentials');
|
|
782
|
+
setTimeout(() => {
|
|
783
|
+
if (this.userClient && this.userConnected && this.userCredentials) {
|
|
784
|
+
this.userClient.subscribeUser(this.userCredentials);
|
|
785
|
+
}
|
|
786
|
+
}, 1000);
|
|
787
|
+
}
|
|
788
|
+
this.emit('userConnected');
|
|
789
|
+
}
|
|
790
|
+
handleUserChannelMessage(_client, message) {
|
|
791
|
+
this.log(`User channel received: ${message.topic}:${message.type}`);
|
|
792
|
+
const payload = message.payload;
|
|
793
|
+
if (message.topic === 'clob_user') {
|
|
794
|
+
this.handleUserMessage(message.type, payload, message.timestamp);
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
this.log(`Unexpected topic on user channel: ${message.topic}`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
handleCryptoConnect(_client) {
|
|
801
|
+
this.cryptoConnected = true;
|
|
802
|
+
this.log('Connected to crypto prices WebSocket');
|
|
803
|
+
this.emit('cryptoConnected');
|
|
804
|
+
}
|
|
805
|
+
handleCryptoMessage(_client, message) {
|
|
806
|
+
this.log(`Crypto received: ${message.topic}:${message.type}`);
|
|
807
|
+
const payload = message.payload;
|
|
808
|
+
switch (message.topic) {
|
|
809
|
+
case 'crypto_prices':
|
|
810
|
+
this.handleCryptoPriceMessage(payload, message.timestamp);
|
|
811
|
+
break;
|
|
812
|
+
case 'crypto_prices_chainlink':
|
|
813
|
+
this.handleCryptoChainlinkPriceMessage(payload, message.timestamp);
|
|
814
|
+
break;
|
|
815
|
+
default:
|
|
816
|
+
this.log(`Unknown crypto topic: ${message.topic}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
handleMessage(_client, message) {
|
|
575
820
|
this.log(`Received: ${message.topic}:${message.type}`);
|
|
576
821
|
const payload = message.payload;
|
|
577
822
|
switch (message.topic) {
|
|
@@ -581,9 +826,6 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
581
826
|
case 'clob_user':
|
|
582
827
|
this.handleUserMessage(message.type, payload, message.timestamp);
|
|
583
828
|
break;
|
|
584
|
-
case 'activity':
|
|
585
|
-
this.handleActivityMessage(message.type, payload, message.timestamp);
|
|
586
|
-
break;
|
|
587
829
|
case 'crypto_prices':
|
|
588
830
|
this.handleCryptoPriceMessage(payload, message.timestamp);
|
|
589
831
|
break;
|
|
@@ -603,35 +845,95 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
603
845
|
this.log(`Unknown topic: ${message.topic}`);
|
|
604
846
|
}
|
|
605
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Handle market channel messages
|
|
850
|
+
* @see https://docs.polymarket.com/developers/CLOB/websocket/market-channel
|
|
851
|
+
*
|
|
852
|
+
* Market channel events:
|
|
853
|
+
* - book: Orderbook snapshot - triggered on subscribe or when trades affect orderbook
|
|
854
|
+
* - price_change: Price level change - triggered when order placed or cancelled
|
|
855
|
+
* - last_trade_price: Trade execution - triggered when maker/taker orders match
|
|
856
|
+
* - tick_size_change: Tick size adjustment - triggered when price > 0.96 or < 0.04
|
|
857
|
+
* - best_bid_ask: Best prices update (feature-flagged) - triggered on best price change
|
|
858
|
+
* - new_market: Market created (feature-flagged) - triggered on market creation
|
|
859
|
+
* - market_resolved: Market resolved (feature-flagged) - triggered on market resolution
|
|
860
|
+
*/
|
|
606
861
|
handleMarketMessage(type, payload, timestamp) {
|
|
607
862
|
switch (type) {
|
|
863
|
+
case 'book': // New format from custom RealTimeDataClient
|
|
608
864
|
case 'agg_orderbook': {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
865
|
+
// book event: Orderbook snapshot with bids/asks
|
|
866
|
+
const items = Array.isArray(payload) ? payload : [payload];
|
|
867
|
+
for (const item of items) {
|
|
868
|
+
const book = this.parseOrderbook(item, timestamp);
|
|
869
|
+
if (book.assetId) {
|
|
870
|
+
this.bookCache.set(book.assetId, book);
|
|
871
|
+
this.emit('orderbook', book);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
612
874
|
break;
|
|
613
875
|
}
|
|
614
876
|
case 'price_change': {
|
|
615
|
-
const
|
|
616
|
-
|
|
877
|
+
const items = Array.isArray(payload) ? payload : [payload];
|
|
878
|
+
for (const item of items) {
|
|
879
|
+
const change = this.parsePriceChange(item, timestamp);
|
|
880
|
+
if (change.assetId) {
|
|
881
|
+
this.emit('priceChange', change);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
617
884
|
break;
|
|
618
885
|
}
|
|
619
886
|
case 'last_trade_price': {
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
887
|
+
const items = Array.isArray(payload) ? payload : [payload];
|
|
888
|
+
for (const item of items) {
|
|
889
|
+
const trade = this.parseLastTrade(item, timestamp);
|
|
890
|
+
if (trade.assetId) {
|
|
891
|
+
this.lastTradeCache.set(trade.assetId, trade);
|
|
892
|
+
this.emit('lastTrade', trade);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
623
895
|
break;
|
|
624
896
|
}
|
|
625
897
|
case 'tick_size_change': {
|
|
898
|
+
// tick_size_change event: Tick size adjustment (price > 0.96 or < 0.04)
|
|
899
|
+
// @see https://docs.polymarket.com/developers/CLOB/websocket/market-channel
|
|
626
900
|
const change = this.parseTickSizeChange(payload, timestamp);
|
|
627
901
|
this.emit('tickSizeChange', change);
|
|
628
902
|
break;
|
|
629
903
|
}
|
|
630
|
-
case '
|
|
904
|
+
case 'best_bid_ask': {
|
|
905
|
+
// best_bid_ask event: Best prices changed (feature-flagged)
|
|
906
|
+
// @see https://docs.polymarket.com/developers/CLOB/websocket/market-channel
|
|
907
|
+
const bestPrices = {
|
|
908
|
+
assetId: payload.asset_id || '',
|
|
909
|
+
market: payload.market || '',
|
|
910
|
+
bestBid: Number(payload.best_bid) || 0,
|
|
911
|
+
bestAsk: Number(payload.best_ask) || 0,
|
|
912
|
+
spread: Number(payload.spread) || 0,
|
|
913
|
+
timestamp,
|
|
914
|
+
};
|
|
915
|
+
this.emit('bestBidAsk', bestPrices);
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
case 'new_market':
|
|
919
|
+
case 'market_created': {
|
|
920
|
+
// new_market event: Market creation (feature-flagged)
|
|
921
|
+
// @see https://docs.polymarket.com/developers/CLOB/websocket/market-channel
|
|
922
|
+
const event = {
|
|
923
|
+
conditionId: payload.market || payload.condition_id || '',
|
|
924
|
+
type: 'created',
|
|
925
|
+
data: payload,
|
|
926
|
+
timestamp,
|
|
927
|
+
};
|
|
928
|
+
this.emit('marketEvent', event);
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
631
931
|
case 'market_resolved': {
|
|
932
|
+
// market_resolved event: Market resolution (feature-flagged)
|
|
933
|
+
// @see https://docs.polymarket.com/developers/CLOB/websocket/market-channel
|
|
632
934
|
const event = {
|
|
633
|
-
conditionId: payload.condition_id || '',
|
|
634
|
-
type:
|
|
935
|
+
conditionId: payload.market || payload.condition_id || '',
|
|
936
|
+
type: 'resolved',
|
|
635
937
|
data: payload,
|
|
636
938
|
timestamp,
|
|
637
939
|
};
|
|
@@ -640,8 +942,18 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
640
942
|
}
|
|
641
943
|
}
|
|
642
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* Handle user channel messages
|
|
947
|
+
* @see https://docs.polymarket.com/developers/CLOB/websocket/user-channel
|
|
948
|
+
*
|
|
949
|
+
* User channel events:
|
|
950
|
+
* - order: Emitted when order placed (PLACEMENT), partially matched (UPDATE), or cancelled (CANCELLATION)
|
|
951
|
+
* - trade: Emitted when market order matches, limit order included in trade, or status changes
|
|
952
|
+
* Status values: MATCHED, MINED, CONFIRMED, RETRYING, FAILED
|
|
953
|
+
*/
|
|
643
954
|
handleUserMessage(type, payload, timestamp) {
|
|
644
955
|
if (type === 'order') {
|
|
956
|
+
// order event: Order placed (PLACEMENT), updated (UPDATE), or cancelled (CANCELLATION)
|
|
645
957
|
const order = {
|
|
646
958
|
orderId: payload.order_id || '',
|
|
647
959
|
market: payload.market || '',
|
|
@@ -649,13 +961,26 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
649
961
|
side: payload.side,
|
|
650
962
|
price: Number(payload.price) || 0,
|
|
651
963
|
originalSize: Number(payload.original_size) || 0,
|
|
652
|
-
|
|
964
|
+
sizeMatched: Number(payload.size_matched) || 0, // API field: size_matched
|
|
653
965
|
eventType: payload.event_type,
|
|
654
966
|
timestamp,
|
|
655
967
|
};
|
|
656
968
|
this.emit('userOrder', order);
|
|
657
969
|
}
|
|
658
970
|
else if (type === 'trade') {
|
|
971
|
+
// trade event: Trade status updates (MATCHED, MINED, CONFIRMED, RETRYING, FAILED)
|
|
972
|
+
// Parse maker_orders array if present
|
|
973
|
+
let makerOrders;
|
|
974
|
+
if (Array.isArray(payload.maker_orders)) {
|
|
975
|
+
makerOrders = payload.maker_orders.map(m => ({
|
|
976
|
+
orderId: m.order_id || '',
|
|
977
|
+
matchedAmount: Number(m.matched_amount) || 0,
|
|
978
|
+
price: Number(m.price) || 0,
|
|
979
|
+
assetId: m.asset_id,
|
|
980
|
+
outcome: m.outcome,
|
|
981
|
+
owner: m.owner,
|
|
982
|
+
}));
|
|
983
|
+
}
|
|
659
984
|
const trade = {
|
|
660
985
|
tradeId: payload.trade_id || '',
|
|
661
986
|
market: payload.market || '',
|
|
@@ -666,29 +991,13 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
666
991
|
status: payload.status,
|
|
667
992
|
timestamp,
|
|
668
993
|
transactionHash: payload.transaction_hash,
|
|
994
|
+
// New fields for order-trade linking
|
|
995
|
+
takerOrderId: payload.taker_order_id,
|
|
996
|
+
makerOrders,
|
|
669
997
|
};
|
|
670
998
|
this.emit('userTrade', trade);
|
|
671
999
|
}
|
|
672
1000
|
}
|
|
673
|
-
handleActivityMessage(type, payload, timestamp) {
|
|
674
|
-
const trade = {
|
|
675
|
-
asset: payload.asset || '',
|
|
676
|
-
conditionId: payload.conditionId || '',
|
|
677
|
-
eventSlug: payload.eventSlug || '',
|
|
678
|
-
marketSlug: payload.slug || '',
|
|
679
|
-
outcome: payload.outcome || '',
|
|
680
|
-
price: Number(payload.price) || 0,
|
|
681
|
-
side: payload.side,
|
|
682
|
-
size: Number(payload.size) || 0,
|
|
683
|
-
timestamp: this.normalizeTimestamp(payload.timestamp) || timestamp,
|
|
684
|
-
transactionHash: payload.transactionHash || '',
|
|
685
|
-
trader: {
|
|
686
|
-
name: payload.name,
|
|
687
|
-
address: payload.proxyWallet,
|
|
688
|
-
},
|
|
689
|
-
};
|
|
690
|
-
this.emit('activityTrade', trade);
|
|
691
|
-
}
|
|
692
1001
|
handleCryptoPriceMessage(payload, timestamp) {
|
|
693
1002
|
const price = {
|
|
694
1003
|
symbol: payload.symbol || '',
|
|
@@ -795,12 +1104,14 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
795
1104
|
};
|
|
796
1105
|
}
|
|
797
1106
|
parseLastTrade(payload, timestamp) {
|
|
1107
|
+
const feeRateBps = payload.fee_rate_bps !== undefined ? Number(payload.fee_rate_bps) : undefined;
|
|
798
1108
|
return {
|
|
799
1109
|
assetId: payload.asset_id || '',
|
|
800
1110
|
price: parseFloat(payload.price) || 0,
|
|
801
1111
|
side: payload.side || 'BUY',
|
|
802
1112
|
size: parseFloat(payload.size) || 0,
|
|
803
1113
|
timestamp: this.normalizeTimestamp(payload.timestamp) || timestamp,
|
|
1114
|
+
feeRateBps: feeRateBps !== undefined && !isNaN(feeRateBps) ? feeRateBps : undefined,
|
|
804
1115
|
};
|
|
805
1116
|
}
|
|
806
1117
|
parseTickSizeChange(payload, timestamp) {
|
|
@@ -838,16 +1149,36 @@ export class RealtimeServiceV2 extends EventEmitter {
|
|
|
838
1149
|
}
|
|
839
1150
|
sendSubscription(msg) {
|
|
840
1151
|
if (this.client && this.connected) {
|
|
1152
|
+
// Log subscription details (redact credentials)
|
|
1153
|
+
const loggableSubs = msg.subscriptions.map(s => ({
|
|
1154
|
+
topic: s.topic,
|
|
1155
|
+
type: s.type,
|
|
1156
|
+
filters: s.filters,
|
|
1157
|
+
hasAuth: !!s.clob_auth,
|
|
1158
|
+
}));
|
|
1159
|
+
this.log(`Sending subscription: ${JSON.stringify(loggableSubs)}`);
|
|
841
1160
|
this.client.subscribe(msg);
|
|
842
1161
|
}
|
|
843
1162
|
else {
|
|
844
1163
|
this.log('Cannot subscribe: not connected');
|
|
845
1164
|
}
|
|
846
1165
|
}
|
|
847
|
-
sendUnsubscription(msg) {
|
|
848
|
-
if (this.client
|
|
849
|
-
|
|
1166
|
+
sendUnsubscription(msg, subId) {
|
|
1167
|
+
if (!this.client || !this.connected)
|
|
1168
|
+
return;
|
|
1169
|
+
// If subId is provided, only send unsubscribe if subscription is on current connection.
|
|
1170
|
+
// After reconnect, stale subscriptions may not exist on the server (expired markets),
|
|
1171
|
+
// so sending unsubscribe would trigger "Invalid request body" errors.
|
|
1172
|
+
if (subId) {
|
|
1173
|
+
const subGeneration = this.subscriptionGenerations.get(subId);
|
|
1174
|
+
if (subGeneration !== undefined && subGeneration !== this.connectionGeneration) {
|
|
1175
|
+
this.log(`Skipping unsubscribe for ${subId}: stale (gen ${subGeneration} vs current ${this.connectionGeneration})`);
|
|
1176
|
+
this.subscriptionGenerations.delete(subId);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
this.subscriptionGenerations.delete(subId);
|
|
850
1180
|
}
|
|
1181
|
+
this.client.unsubscribe(msg);
|
|
851
1182
|
}
|
|
852
1183
|
log(message) {
|
|
853
1184
|
if (this.config.debug) {
|