@catalyst-team/poly-sdk 0.2.1 → 0.3.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.
Files changed (97) hide show
  1. package/README.md +545 -812
  2. package/README.zh-CN.md +645 -342
  3. package/dist/__tests__/integration/arbitrage-service.integration.test.d.ts +12 -0
  4. package/dist/__tests__/integration/arbitrage-service.integration.test.d.ts.map +1 -0
  5. package/dist/__tests__/integration/arbitrage-service.integration.test.js +267 -0
  6. package/dist/__tests__/integration/arbitrage-service.integration.test.js.map +1 -0
  7. package/dist/__tests__/integration/data-api.integration.test.js +6 -3
  8. package/dist/__tests__/integration/data-api.integration.test.js.map +1 -1
  9. package/dist/__tests__/integration/market-service.integration.test.d.ts +10 -0
  10. package/dist/__tests__/integration/market-service.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/integration/market-service.integration.test.js +173 -0
  12. package/dist/__tests__/integration/market-service.integration.test.js.map +1 -0
  13. package/dist/__tests__/integration/realtime-service-v2.integration.test.d.ts +10 -0
  14. package/dist/__tests__/integration/realtime-service-v2.integration.test.d.ts.map +1 -0
  15. package/dist/__tests__/integration/realtime-service-v2.integration.test.js +307 -0
  16. package/dist/__tests__/integration/realtime-service-v2.integration.test.js.map +1 -0
  17. package/dist/__tests__/integration/trading-service.integration.test.d.ts +10 -0
  18. package/dist/__tests__/integration/trading-service.integration.test.d.ts.map +1 -0
  19. package/dist/__tests__/integration/trading-service.integration.test.js +58 -0
  20. package/dist/__tests__/integration/trading-service.integration.test.js.map +1 -0
  21. package/dist/clients/clob-api.d.ts +73 -0
  22. package/dist/clients/clob-api.d.ts.map +1 -1
  23. package/dist/clients/clob-api.js +60 -0
  24. package/dist/clients/clob-api.js.map +1 -1
  25. package/dist/clients/data-api.d.ts +319 -14
  26. package/dist/clients/data-api.d.ts.map +1 -1
  27. package/dist/clients/data-api.js +342 -15
  28. package/dist/clients/data-api.js.map +1 -1
  29. package/dist/clients/subgraph.d.ts +196 -0
  30. package/dist/clients/subgraph.d.ts.map +1 -0
  31. package/dist/clients/subgraph.js +332 -0
  32. package/dist/clients/subgraph.js.map +1 -0
  33. package/dist/clients/websocket-manager.d.ts +3 -0
  34. package/dist/clients/websocket-manager.d.ts.map +1 -1
  35. package/dist/clients/websocket-manager.js +10 -3
  36. package/dist/clients/websocket-manager.js.map +1 -1
  37. package/dist/core/cache.d.ts +1 -0
  38. package/dist/core/cache.d.ts.map +1 -1
  39. package/dist/core/cache.js +1 -0
  40. package/dist/core/cache.js.map +1 -1
  41. package/dist/core/errors.d.ts +2 -1
  42. package/dist/core/errors.d.ts.map +1 -1
  43. package/dist/core/errors.js +2 -0
  44. package/dist/core/errors.js.map +1 -1
  45. package/dist/core/rate-limiter.d.ts +2 -1
  46. package/dist/core/rate-limiter.d.ts.map +1 -1
  47. package/dist/core/rate-limiter.js +5 -0
  48. package/dist/core/rate-limiter.js.map +1 -1
  49. package/dist/core/types.d.ts +100 -12
  50. package/dist/core/types.d.ts.map +1 -1
  51. package/dist/core/types.js.map +1 -1
  52. package/dist/core/types.test.d.ts +7 -0
  53. package/dist/core/types.test.d.ts.map +1 -0
  54. package/dist/core/types.test.js +122 -0
  55. package/dist/core/types.test.js.map +1 -0
  56. package/dist/index.d.ts +76 -18
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +125 -132
  59. package/dist/index.js.map +1 -1
  60. package/dist/services/arbitrage-service.d.ts +3 -2
  61. package/dist/services/arbitrage-service.d.ts.map +1 -1
  62. package/dist/services/arbitrage-service.js +58 -40
  63. package/dist/services/arbitrage-service.js.map +1 -1
  64. package/dist/services/market-service.d.ts +108 -8
  65. package/dist/services/market-service.d.ts.map +1 -1
  66. package/dist/services/market-service.js +352 -36
  67. package/dist/services/market-service.js.map +1 -1
  68. package/dist/services/onchain-service.d.ts +309 -0
  69. package/dist/services/onchain-service.d.ts.map +1 -0
  70. package/dist/services/onchain-service.js +417 -0
  71. package/dist/services/onchain-service.js.map +1 -0
  72. package/dist/services/realtime-service-v2.d.ts +361 -0
  73. package/dist/services/realtime-service-v2.d.ts.map +1 -0
  74. package/dist/services/realtime-service-v2.js +840 -0
  75. package/dist/services/realtime-service-v2.js.map +1 -0
  76. package/dist/services/realtime-service.d.ts +17 -17
  77. package/dist/services/realtime-service.d.ts.map +1 -1
  78. package/dist/services/realtime-service.js +91 -59
  79. package/dist/services/realtime-service.js.map +1 -1
  80. package/dist/services/smart-money-service.d.ts +196 -0
  81. package/dist/services/smart-money-service.d.ts.map +1 -0
  82. package/dist/services/smart-money-service.js +358 -0
  83. package/dist/services/smart-money-service.js.map +1 -0
  84. package/dist/services/trading-service.d.ts +156 -0
  85. package/dist/services/trading-service.d.ts.map +1 -0
  86. package/dist/services/trading-service.js +356 -0
  87. package/dist/services/trading-service.js.map +1 -0
  88. package/dist/services/wallet-service.d.ts +183 -2
  89. package/dist/services/wallet-service.d.ts.map +1 -1
  90. package/dist/services/wallet-service.js +458 -1
  91. package/dist/services/wallet-service.js.map +1 -1
  92. package/dist/utils/price-utils.test.d.ts +5 -0
  93. package/dist/utils/price-utils.test.d.ts.map +1 -0
  94. package/dist/utils/price-utils.test.js +192 -0
  95. package/dist/utils/price-utils.test.js.map +1 -0
  96. package/package.json +4 -3
  97. package/README.en.md +0 -502
@@ -0,0 +1,840 @@
1
+ /**
2
+ * RealtimeService V2
3
+ *
4
+ * Comprehensive real-time data service using official @polymarket/real-time-data-client.
5
+ *
6
+ * Supports ALL available topics:
7
+ * - clob_market: price_change, agg_orderbook, last_trade_price, tick_size_change, market_created, market_resolved
8
+ * - clob_user: order, trade (requires authentication)
9
+ * - activity: trades, orders_matched
10
+ * - crypto_prices: update (BTC, ETH, etc.)
11
+ * - equity_prices: update (AAPL, etc.)
12
+ * - comments: comment_created, comment_removed, reaction_created, reaction_removed
13
+ * - rfq: request_*, quote_*
14
+ */
15
+ import { EventEmitter } from 'events';
16
+ import { RealTimeDataClient, ConnectionStatus, } from '@polymarket/real-time-data-client';
17
+ // ============================================================================
18
+ // RealtimeServiceV2 Implementation
19
+ // ============================================================================
20
+ export class RealtimeServiceV2 extends EventEmitter {
21
+ client = null;
22
+ config;
23
+ subscriptions = new Map();
24
+ subscriptionIdCounter = 0;
25
+ connected = false;
26
+ // Caches
27
+ priceCache = new Map();
28
+ bookCache = new Map();
29
+ lastTradeCache = new Map();
30
+ constructor(config = {}) {
31
+ super();
32
+ this.config = {
33
+ autoReconnect: config.autoReconnect ?? true,
34
+ pingInterval: config.pingInterval ?? 5000,
35
+ debug: config.debug ?? false,
36
+ };
37
+ }
38
+ // ============================================================================
39
+ // Connection Management
40
+ // ============================================================================
41
+ /**
42
+ * Connect to WebSocket server
43
+ */
44
+ connect() {
45
+ if (this.client) {
46
+ this.log('Already connected or connecting');
47
+ return this;
48
+ }
49
+ this.client = new RealTimeDataClient({
50
+ onConnect: this.handleConnect.bind(this),
51
+ onMessage: this.handleMessage.bind(this),
52
+ onStatusChange: this.handleStatusChange.bind(this),
53
+ autoReconnect: this.config.autoReconnect,
54
+ pingInterval: this.config.pingInterval,
55
+ });
56
+ this.client.connect();
57
+ return this;
58
+ }
59
+ /**
60
+ * Disconnect from WebSocket server
61
+ */
62
+ disconnect() {
63
+ if (this.client) {
64
+ this.client.disconnect();
65
+ this.client = null;
66
+ this.connected = false;
67
+ this.subscriptions.clear();
68
+ }
69
+ }
70
+ /**
71
+ * Check if connected
72
+ */
73
+ isConnected() {
74
+ return this.connected;
75
+ }
76
+ // ============================================================================
77
+ // Market Data Subscriptions (clob_market)
78
+ // ============================================================================
79
+ /**
80
+ * Subscribe to market data (orderbook, prices, trades)
81
+ * @param tokenIds - Array of token IDs to subscribe to
82
+ * @param handlers - Event handlers
83
+ */
84
+ subscribeMarkets(tokenIds, handlers = {}) {
85
+ const subId = `market_${++this.subscriptionIdCounter}`;
86
+ const filterStr = JSON.stringify(tokenIds);
87
+ // Subscribe to all market data types
88
+ const subscriptions = [
89
+ { topic: 'clob_market', type: 'agg_orderbook', filters: filterStr },
90
+ { topic: 'clob_market', type: 'price_change', filters: filterStr },
91
+ { topic: 'clob_market', type: 'last_trade_price', filters: filterStr },
92
+ { topic: 'clob_market', type: 'tick_size_change', filters: filterStr },
93
+ ];
94
+ this.sendSubscription({ subscriptions });
95
+ // Register handlers
96
+ const orderbookHandler = (book) => {
97
+ if (tokenIds.includes(book.assetId)) {
98
+ handlers.onOrderbook?.(book);
99
+ }
100
+ };
101
+ const priceChangeHandler = (change) => {
102
+ if (tokenIds.includes(change.assetId)) {
103
+ handlers.onPriceChange?.(change);
104
+ }
105
+ };
106
+ const lastTradeHandler = (trade) => {
107
+ if (tokenIds.includes(trade.assetId)) {
108
+ handlers.onLastTrade?.(trade);
109
+ }
110
+ };
111
+ const tickSizeHandler = (change) => {
112
+ if (tokenIds.includes(change.assetId)) {
113
+ handlers.onTickSizeChange?.(change);
114
+ }
115
+ };
116
+ this.on('orderbook', orderbookHandler);
117
+ this.on('priceChange', priceChangeHandler);
118
+ this.on('lastTrade', lastTradeHandler);
119
+ this.on('tickSizeChange', tickSizeHandler);
120
+ const subscription = {
121
+ id: subId,
122
+ topic: 'clob_market',
123
+ type: '*',
124
+ tokenIds,
125
+ unsubscribe: () => {
126
+ this.off('orderbook', orderbookHandler);
127
+ this.off('priceChange', priceChangeHandler);
128
+ this.off('lastTrade', lastTradeHandler);
129
+ this.off('tickSizeChange', tickSizeHandler);
130
+ this.sendUnsubscription({ subscriptions });
131
+ this.subscriptions.delete(subId);
132
+ },
133
+ };
134
+ this.subscriptions.set(subId, subscription);
135
+ return subscription;
136
+ }
137
+ /**
138
+ * Subscribe to a single market (YES + NO tokens)
139
+ * Also emits derived price updates compatible with old API
140
+ */
141
+ subscribeMarket(yesTokenId, noTokenId, handlers = {}) {
142
+ let lastYesUpdate;
143
+ let lastNoUpdate;
144
+ const checkPairUpdate = () => {
145
+ if (lastYesUpdate && lastNoUpdate && handlers.onPairUpdate) {
146
+ handlers.onPairUpdate({
147
+ yes: lastYesUpdate,
148
+ no: lastNoUpdate,
149
+ spread: lastYesUpdate.price + lastNoUpdate.price,
150
+ });
151
+ }
152
+ };
153
+ return this.subscribeMarkets([yesTokenId, noTokenId], {
154
+ onOrderbook: (book) => {
155
+ handlers.onOrderbook?.(book);
156
+ // Convert to BookUpdate for backward compatibility
157
+ if (handlers.onBookUpdate) {
158
+ const bookUpdate = {
159
+ assetId: book.assetId,
160
+ bids: book.bids,
161
+ asks: book.asks,
162
+ timestamp: book.timestamp,
163
+ };
164
+ handlers.onBookUpdate(bookUpdate);
165
+ }
166
+ // Calculate derived price (Polymarket display logic)
167
+ const priceUpdate = this.calculateDerivedPrice(book.assetId, book);
168
+ if (priceUpdate) {
169
+ this.priceCache.set(book.assetId, priceUpdate);
170
+ if (book.assetId === yesTokenId) {
171
+ lastYesUpdate = priceUpdate;
172
+ }
173
+ else if (book.assetId === noTokenId) {
174
+ lastNoUpdate = priceUpdate;
175
+ }
176
+ handlers.onPriceUpdate?.(priceUpdate);
177
+ this.emit('priceUpdate', priceUpdate);
178
+ checkPairUpdate();
179
+ }
180
+ },
181
+ onLastTrade: (trade) => {
182
+ handlers.onLastTrade?.(trade);
183
+ this.lastTradeCache.set(trade.assetId, trade);
184
+ // Recalculate derived price with new last trade
185
+ const book = this.bookCache.get(trade.assetId);
186
+ if (book) {
187
+ const priceUpdate = this.calculateDerivedPrice(trade.assetId, book);
188
+ if (priceUpdate) {
189
+ this.priceCache.set(trade.assetId, priceUpdate);
190
+ if (trade.assetId === yesTokenId) {
191
+ lastYesUpdate = priceUpdate;
192
+ }
193
+ else if (trade.assetId === noTokenId) {
194
+ lastNoUpdate = priceUpdate;
195
+ }
196
+ handlers.onPriceUpdate?.(priceUpdate);
197
+ this.emit('priceUpdate', priceUpdate);
198
+ checkPairUpdate();
199
+ }
200
+ }
201
+ },
202
+ onPriceChange: handlers.onPriceChange,
203
+ onTickSizeChange: handlers.onTickSizeChange,
204
+ onError: handlers.onError,
205
+ });
206
+ }
207
+ /**
208
+ * Subscribe to market lifecycle events (creation, resolution)
209
+ */
210
+ subscribeMarketEvents(handlers) {
211
+ const subId = `market_event_${++this.subscriptionIdCounter}`;
212
+ const subscriptions = [
213
+ { topic: 'clob_market', type: 'market_created' },
214
+ { topic: 'clob_market', type: 'market_resolved' },
215
+ ];
216
+ this.sendSubscription({ subscriptions });
217
+ const handler = (event) => handlers.onMarketEvent?.(event);
218
+ this.on('marketEvent', handler);
219
+ const subscription = {
220
+ id: subId,
221
+ topic: 'clob_market',
222
+ type: 'lifecycle',
223
+ unsubscribe: () => {
224
+ this.off('marketEvent', handler);
225
+ this.sendUnsubscription({ subscriptions });
226
+ this.subscriptions.delete(subId);
227
+ },
228
+ };
229
+ this.subscriptions.set(subId, subscription);
230
+ return subscription;
231
+ }
232
+ // ============================================================================
233
+ // User Data Subscriptions (clob_user) - Requires Authentication
234
+ // ============================================================================
235
+ /**
236
+ * Subscribe to user order and trade events
237
+ * @param credentials - CLOB API credentials
238
+ * @param handlers - Event handlers
239
+ */
240
+ subscribeUserEvents(credentials, handlers = {}) {
241
+ const subId = `user_${++this.subscriptionIdCounter}`;
242
+ const subscriptions = [
243
+ { topic: 'clob_user', type: '*', clob_auth: credentials },
244
+ ];
245
+ this.sendSubscription({ subscriptions });
246
+ const orderHandler = (order) => handlers.onOrder?.(order);
247
+ const tradeHandler = (trade) => handlers.onTrade?.(trade);
248
+ this.on('userOrder', orderHandler);
249
+ this.on('userTrade', tradeHandler);
250
+ const subscription = {
251
+ id: subId,
252
+ topic: 'clob_user',
253
+ type: '*',
254
+ unsubscribe: () => {
255
+ this.off('userOrder', orderHandler);
256
+ this.off('userTrade', tradeHandler);
257
+ this.sendUnsubscription({ subscriptions });
258
+ this.subscriptions.delete(subId);
259
+ },
260
+ };
261
+ this.subscriptions.set(subId, subscription);
262
+ return subscription;
263
+ }
264
+ // ============================================================================
265
+ // Activity Subscriptions (trades, orders_matched)
266
+ // ============================================================================
267
+ /**
268
+ * Subscribe to trading activity for a market or event
269
+ * @param filter - Event or market slug (optional - if empty, subscribes to all activity)
270
+ * @param handlers - Event handlers
271
+ */
272
+ subscribeActivity(filter = {}, handlers = {}) {
273
+ const subId = `activity_${++this.subscriptionIdCounter}`;
274
+ // Build filter object with snake_case keys (as expected by the server)
275
+ // Only include filters if we have actual filter values
276
+ const hasFilter = filter.eventSlug || filter.marketSlug;
277
+ const filterObj = {};
278
+ if (filter.eventSlug)
279
+ filterObj.event_slug = filter.eventSlug;
280
+ if (filter.marketSlug)
281
+ filterObj.market_slug = filter.marketSlug;
282
+ // Create subscription objects - only include filters field if we have filters
283
+ const subscriptions = hasFilter
284
+ ? [
285
+ { topic: 'activity', type: 'trades', filters: JSON.stringify(filterObj) },
286
+ { topic: 'activity', type: 'orders_matched', filters: JSON.stringify(filterObj) },
287
+ ]
288
+ : [
289
+ { topic: 'activity', type: 'trades' },
290
+ { topic: 'activity', type: 'orders_matched' },
291
+ ];
292
+ this.sendSubscription({ subscriptions });
293
+ const handler = (trade) => handlers.onTrade?.(trade);
294
+ this.on('activityTrade', handler);
295
+ const subscription = {
296
+ id: subId,
297
+ topic: 'activity',
298
+ type: '*',
299
+ unsubscribe: () => {
300
+ this.off('activityTrade', handler);
301
+ this.sendUnsubscription({ subscriptions });
302
+ this.subscriptions.delete(subId);
303
+ },
304
+ };
305
+ this.subscriptions.set(subId, subscription);
306
+ return subscription;
307
+ }
308
+ /**
309
+ * Subscribe to ALL trading activity across all markets (no filtering)
310
+ * This is useful for Copy Trading - monitoring Smart Money across the platform
311
+ * @param handlers - Event handlers
312
+ */
313
+ subscribeAllActivity(handlers = {}) {
314
+ return this.subscribeActivity({}, handlers);
315
+ }
316
+ // ============================================================================
317
+ // Crypto Price Subscriptions
318
+ // ============================================================================
319
+ /**
320
+ * Subscribe to crypto price updates
321
+ * @param symbols - Array of symbols (e.g., ['BTCUSDT', 'ETHUSDT'])
322
+ * @param handlers - Event handlers
323
+ */
324
+ subscribeCryptoPrices(symbols, handlers = {}) {
325
+ const subId = `crypto_${++this.subscriptionIdCounter}`;
326
+ // Subscribe to each symbol
327
+ const subscriptions = symbols.map(symbol => ({
328
+ topic: 'crypto_prices',
329
+ type: 'update',
330
+ filters: JSON.stringify({ symbol }),
331
+ }));
332
+ this.sendSubscription({ subscriptions });
333
+ const handler = (price) => {
334
+ if (symbols.includes(price.symbol)) {
335
+ handlers.onPrice?.(price);
336
+ }
337
+ };
338
+ this.on('cryptoPrice', handler);
339
+ const subscription = {
340
+ id: subId,
341
+ topic: 'crypto_prices',
342
+ type: 'update',
343
+ unsubscribe: () => {
344
+ this.off('cryptoPrice', handler);
345
+ this.sendUnsubscription({ subscriptions });
346
+ this.subscriptions.delete(subId);
347
+ },
348
+ };
349
+ this.subscriptions.set(subId, subscription);
350
+ return subscription;
351
+ }
352
+ /**
353
+ * Subscribe to Chainlink crypto prices
354
+ * @param symbols - Array of symbols (e.g., ['ETH/USD', 'BTC/USD'])
355
+ */
356
+ subscribeCryptoChainlinkPrices(symbols, handlers = {}) {
357
+ const subId = `crypto_chainlink_${++this.subscriptionIdCounter}`;
358
+ const subscriptions = symbols.map(symbol => ({
359
+ topic: 'crypto_prices_chainlink',
360
+ type: 'update',
361
+ filters: JSON.stringify({ symbol }),
362
+ }));
363
+ this.sendSubscription({ subscriptions });
364
+ const handler = (price) => {
365
+ if (symbols.includes(price.symbol)) {
366
+ handlers.onPrice?.(price);
367
+ }
368
+ };
369
+ this.on('cryptoChainlinkPrice', handler);
370
+ const subscription = {
371
+ id: subId,
372
+ topic: 'crypto_prices_chainlink',
373
+ type: 'update',
374
+ unsubscribe: () => {
375
+ this.off('cryptoChainlinkPrice', handler);
376
+ this.sendUnsubscription({ subscriptions });
377
+ this.subscriptions.delete(subId);
378
+ },
379
+ };
380
+ this.subscriptions.set(subId, subscription);
381
+ return subscription;
382
+ }
383
+ // ============================================================================
384
+ // Equity Price Subscriptions
385
+ // ============================================================================
386
+ /**
387
+ * Subscribe to equity price updates
388
+ * @param symbols - Array of symbols (e.g., ['AAPL', 'GOOGL'])
389
+ * @param handlers - Event handlers
390
+ */
391
+ subscribeEquityPrices(symbols, handlers = {}) {
392
+ const subId = `equity_${++this.subscriptionIdCounter}`;
393
+ const subscriptions = symbols.map(symbol => ({
394
+ topic: 'equity_prices',
395
+ type: 'update',
396
+ filters: JSON.stringify({ symbol }),
397
+ }));
398
+ this.sendSubscription({ subscriptions });
399
+ const handler = (price) => {
400
+ if (symbols.includes(price.symbol)) {
401
+ handlers.onPrice?.(price);
402
+ }
403
+ };
404
+ this.on('equityPrice', handler);
405
+ const subscription = {
406
+ id: subId,
407
+ topic: 'equity_prices',
408
+ type: 'update',
409
+ unsubscribe: () => {
410
+ this.off('equityPrice', handler);
411
+ this.sendUnsubscription({ subscriptions });
412
+ this.subscriptions.delete(subId);
413
+ },
414
+ };
415
+ this.subscriptions.set(subId, subscription);
416
+ return subscription;
417
+ }
418
+ // ============================================================================
419
+ // Comments Subscriptions
420
+ // ============================================================================
421
+ /**
422
+ * Subscribe to comment and reaction events
423
+ */
424
+ subscribeComments(filter, handlers = {}) {
425
+ const subId = `comments_${++this.subscriptionIdCounter}`;
426
+ const filterStr = JSON.stringify({
427
+ parentEntityID: filter.parentEntityId,
428
+ parentEntityType: filter.parentEntityType,
429
+ });
430
+ const subscriptions = [
431
+ { topic: 'comments', type: 'comment_created', filters: filterStr },
432
+ { topic: 'comments', type: 'comment_removed', filters: filterStr },
433
+ { topic: 'comments', type: 'reaction_created', filters: filterStr },
434
+ { topic: 'comments', type: 'reaction_removed', filters: filterStr },
435
+ ];
436
+ this.sendSubscription({ subscriptions });
437
+ const commentHandler = (comment) => handlers.onComment?.(comment);
438
+ const reactionHandler = (reaction) => handlers.onReaction?.(reaction);
439
+ this.on('comment', commentHandler);
440
+ this.on('reaction', reactionHandler);
441
+ const subscription = {
442
+ id: subId,
443
+ topic: 'comments',
444
+ type: '*',
445
+ unsubscribe: () => {
446
+ this.off('comment', commentHandler);
447
+ this.off('reaction', reactionHandler);
448
+ this.sendUnsubscription({ subscriptions });
449
+ this.subscriptions.delete(subId);
450
+ },
451
+ };
452
+ this.subscriptions.set(subId, subscription);
453
+ return subscription;
454
+ }
455
+ // ============================================================================
456
+ // RFQ Subscriptions
457
+ // ============================================================================
458
+ /**
459
+ * Subscribe to RFQ (Request for Quote) events
460
+ */
461
+ subscribeRFQ(handlers = {}) {
462
+ const subId = `rfq_${++this.subscriptionIdCounter}`;
463
+ const subscriptions = [
464
+ { topic: 'rfq', type: 'request_created' },
465
+ { topic: 'rfq', type: 'request_edited' },
466
+ { topic: 'rfq', type: 'request_canceled' },
467
+ { topic: 'rfq', type: 'request_expired' },
468
+ { topic: 'rfq', type: 'quote_created' },
469
+ { topic: 'rfq', type: 'quote_edited' },
470
+ { topic: 'rfq', type: 'quote_canceled' },
471
+ { topic: 'rfq', type: 'quote_expired' },
472
+ ];
473
+ this.sendSubscription({ subscriptions });
474
+ const requestHandler = (request) => handlers.onRequest?.(request);
475
+ const quoteHandler = (quote) => handlers.onQuote?.(quote);
476
+ this.on('rfqRequest', requestHandler);
477
+ this.on('rfqQuote', quoteHandler);
478
+ const subscription = {
479
+ id: subId,
480
+ topic: 'rfq',
481
+ type: '*',
482
+ unsubscribe: () => {
483
+ this.off('rfqRequest', requestHandler);
484
+ this.off('rfqQuote', quoteHandler);
485
+ this.sendUnsubscription({ subscriptions });
486
+ this.subscriptions.delete(subId);
487
+ },
488
+ };
489
+ this.subscriptions.set(subId, subscription);
490
+ return subscription;
491
+ }
492
+ // ============================================================================
493
+ // Cache Access
494
+ // ============================================================================
495
+ /**
496
+ * Get cached derived price for an asset
497
+ */
498
+ getPrice(assetId) {
499
+ return this.priceCache.get(assetId);
500
+ }
501
+ /**
502
+ * Get all cached prices
503
+ */
504
+ getAllPrices() {
505
+ return new Map(this.priceCache);
506
+ }
507
+ /**
508
+ * Get cached orderbook for an asset
509
+ */
510
+ getBook(assetId) {
511
+ return this.bookCache.get(assetId);
512
+ }
513
+ /**
514
+ * Get cached last trade for an asset
515
+ */
516
+ getLastTrade(assetId) {
517
+ return this.lastTradeCache.get(assetId);
518
+ }
519
+ // ============================================================================
520
+ // Subscription Management
521
+ // ============================================================================
522
+ /**
523
+ * Get all active subscriptions
524
+ */
525
+ getActiveSubscriptions() {
526
+ return Array.from(this.subscriptions.values());
527
+ }
528
+ /**
529
+ * Unsubscribe from all
530
+ */
531
+ unsubscribeAll() {
532
+ for (const sub of this.subscriptions.values()) {
533
+ sub.unsubscribe();
534
+ }
535
+ this.subscriptions.clear();
536
+ }
537
+ // ============================================================================
538
+ // Private Methods
539
+ // ============================================================================
540
+ handleConnect(client) {
541
+ this.connected = true;
542
+ this.log('Connected to WebSocket server');
543
+ this.emit('connected');
544
+ }
545
+ handleStatusChange(status) {
546
+ this.log(`Connection status: ${status}`);
547
+ if (status === ConnectionStatus.DISCONNECTED) {
548
+ this.connected = false;
549
+ this.emit('disconnected');
550
+ }
551
+ else if (status === ConnectionStatus.CONNECTED) {
552
+ this.connected = true;
553
+ }
554
+ this.emit('statusChange', status);
555
+ }
556
+ handleMessage(client, message) {
557
+ this.log(`Received: ${message.topic}:${message.type}`);
558
+ const payload = message.payload;
559
+ switch (message.topic) {
560
+ case 'clob_market':
561
+ this.handleMarketMessage(message.type, payload, message.timestamp);
562
+ break;
563
+ case 'clob_user':
564
+ this.handleUserMessage(message.type, payload, message.timestamp);
565
+ break;
566
+ case 'activity':
567
+ this.handleActivityMessage(message.type, payload, message.timestamp);
568
+ break;
569
+ case 'crypto_prices':
570
+ this.handleCryptoPriceMessage(payload, message.timestamp);
571
+ break;
572
+ case 'crypto_prices_chainlink':
573
+ this.handleCryptoChainlinkPriceMessage(payload, message.timestamp);
574
+ break;
575
+ case 'equity_prices':
576
+ this.handleEquityPriceMessage(payload, message.timestamp);
577
+ break;
578
+ case 'comments':
579
+ this.handleCommentMessage(message.type, payload, message.timestamp);
580
+ break;
581
+ case 'rfq':
582
+ this.handleRFQMessage(message.type, payload, message.timestamp);
583
+ break;
584
+ default:
585
+ this.log(`Unknown topic: ${message.topic}`);
586
+ }
587
+ }
588
+ handleMarketMessage(type, payload, timestamp) {
589
+ switch (type) {
590
+ case 'agg_orderbook': {
591
+ const book = this.parseOrderbook(payload, timestamp);
592
+ this.bookCache.set(book.assetId, book);
593
+ this.emit('orderbook', book);
594
+ break;
595
+ }
596
+ case 'price_change': {
597
+ const change = this.parsePriceChange(payload, timestamp);
598
+ this.emit('priceChange', change);
599
+ break;
600
+ }
601
+ case 'last_trade_price': {
602
+ const trade = this.parseLastTrade(payload, timestamp);
603
+ this.lastTradeCache.set(trade.assetId, trade);
604
+ this.emit('lastTrade', trade);
605
+ break;
606
+ }
607
+ case 'tick_size_change': {
608
+ const change = this.parseTickSizeChange(payload, timestamp);
609
+ this.emit('tickSizeChange', change);
610
+ break;
611
+ }
612
+ case 'market_created':
613
+ case 'market_resolved': {
614
+ const event = {
615
+ conditionId: payload.condition_id || '',
616
+ type: type === 'market_created' ? 'created' : 'resolved',
617
+ data: payload,
618
+ timestamp,
619
+ };
620
+ this.emit('marketEvent', event);
621
+ break;
622
+ }
623
+ }
624
+ }
625
+ handleUserMessage(type, payload, timestamp) {
626
+ if (type === 'order') {
627
+ const order = {
628
+ orderId: payload.order_id || '',
629
+ market: payload.market || '',
630
+ asset: payload.asset || '',
631
+ side: payload.side,
632
+ price: Number(payload.price) || 0,
633
+ originalSize: Number(payload.original_size) || 0,
634
+ matchedSize: Number(payload.matched_size) || 0,
635
+ eventType: payload.event_type,
636
+ timestamp,
637
+ };
638
+ this.emit('userOrder', order);
639
+ }
640
+ else if (type === 'trade') {
641
+ const trade = {
642
+ tradeId: payload.trade_id || '',
643
+ market: payload.market || '',
644
+ outcome: payload.outcome || '',
645
+ price: Number(payload.price) || 0,
646
+ size: Number(payload.size) || 0,
647
+ side: payload.side,
648
+ status: payload.status,
649
+ timestamp,
650
+ transactionHash: payload.transaction_hash,
651
+ };
652
+ this.emit('userTrade', trade);
653
+ }
654
+ }
655
+ handleActivityMessage(type, payload, timestamp) {
656
+ const trade = {
657
+ asset: payload.asset || '',
658
+ conditionId: payload.conditionId || '',
659
+ eventSlug: payload.eventSlug || '',
660
+ marketSlug: payload.slug || '',
661
+ outcome: payload.outcome || '',
662
+ price: Number(payload.price) || 0,
663
+ side: payload.side,
664
+ size: Number(payload.size) || 0,
665
+ timestamp: Number(payload.timestamp) || timestamp,
666
+ transactionHash: payload.transactionHash || '',
667
+ trader: {
668
+ name: payload.name,
669
+ address: payload.proxyWallet,
670
+ },
671
+ };
672
+ this.emit('activityTrade', trade);
673
+ }
674
+ handleCryptoPriceMessage(payload, timestamp) {
675
+ const price = {
676
+ symbol: payload.symbol || '',
677
+ price: Number(payload.value) || 0,
678
+ timestamp: Number(payload.timestamp) || timestamp,
679
+ };
680
+ this.emit('cryptoPrice', price);
681
+ }
682
+ handleCryptoChainlinkPriceMessage(payload, timestamp) {
683
+ const price = {
684
+ symbol: payload.symbol || '',
685
+ price: Number(payload.value) || 0,
686
+ timestamp: Number(payload.timestamp) || timestamp,
687
+ };
688
+ this.emit('cryptoChainlinkPrice', price);
689
+ }
690
+ handleEquityPriceMessage(payload, timestamp) {
691
+ const price = {
692
+ symbol: payload.symbol || '',
693
+ price: Number(payload.value) || 0,
694
+ timestamp: Number(payload.timestamp) || timestamp,
695
+ };
696
+ this.emit('equityPrice', price);
697
+ }
698
+ handleCommentMessage(type, payload, timestamp) {
699
+ if (type.includes('comment')) {
700
+ const comment = {
701
+ id: payload.id || '',
702
+ parentEntityId: payload.parentEntityID || 0,
703
+ parentEntityType: payload.parentEntityType,
704
+ content: payload.content,
705
+ author: payload.author,
706
+ timestamp,
707
+ };
708
+ this.emit('comment', comment);
709
+ }
710
+ else if (type.includes('reaction')) {
711
+ const reaction = {
712
+ id: payload.id || '',
713
+ commentId: payload.commentId || '',
714
+ type: payload.type || '',
715
+ author: payload.author,
716
+ timestamp,
717
+ };
718
+ this.emit('reaction', reaction);
719
+ }
720
+ }
721
+ handleRFQMessage(type, payload, timestamp) {
722
+ if (type.startsWith('request_')) {
723
+ const status = type.replace('request_', '');
724
+ const request = {
725
+ id: payload.id || '',
726
+ market: payload.market || '',
727
+ side: payload.side,
728
+ size: Number(payload.size) || 0,
729
+ status,
730
+ timestamp,
731
+ };
732
+ this.emit('rfqRequest', request);
733
+ }
734
+ else if (type.startsWith('quote_')) {
735
+ const status = type.replace('quote_', '');
736
+ const quote = {
737
+ id: payload.id || '',
738
+ requestId: payload.request_id || '',
739
+ price: Number(payload.price) || 0,
740
+ size: Number(payload.size) || 0,
741
+ status,
742
+ timestamp,
743
+ };
744
+ this.emit('rfqQuote', quote);
745
+ }
746
+ }
747
+ // Parsers
748
+ parseOrderbook(payload, timestamp) {
749
+ const bidsRaw = payload.bids || [];
750
+ const asksRaw = payload.asks || [];
751
+ // Sort bids descending, asks ascending
752
+ const bids = bidsRaw
753
+ .map(l => ({ price: parseFloat(l.price), size: parseFloat(l.size) }))
754
+ .sort((a, b) => b.price - a.price);
755
+ const asks = asksRaw
756
+ .map(l => ({ price: parseFloat(l.price), size: parseFloat(l.size) }))
757
+ .sort((a, b) => a.price - b.price);
758
+ const tokenId = payload.asset_id || '';
759
+ return {
760
+ tokenId,
761
+ assetId: tokenId, // Backward compatibility
762
+ market: payload.market || '',
763
+ bids,
764
+ asks,
765
+ timestamp: parseInt(payload.timestamp, 10) || timestamp,
766
+ tickSize: payload.tick_size || '0.01',
767
+ minOrderSize: payload.min_order_size || '1',
768
+ hash: payload.hash || '',
769
+ };
770
+ }
771
+ parsePriceChange(payload, timestamp) {
772
+ const changes = payload.price_changes || [];
773
+ return {
774
+ assetId: payload.asset_id || '',
775
+ changes,
776
+ timestamp,
777
+ };
778
+ }
779
+ parseLastTrade(payload, timestamp) {
780
+ return {
781
+ assetId: payload.asset_id || '',
782
+ price: parseFloat(payload.price) || 0,
783
+ side: payload.side || 'BUY',
784
+ size: parseFloat(payload.size) || 0,
785
+ timestamp: parseInt(payload.timestamp, 10) || timestamp,
786
+ };
787
+ }
788
+ parseTickSizeChange(payload, timestamp) {
789
+ return {
790
+ assetId: payload.asset_id || '',
791
+ oldTickSize: payload.old_tick_size || '',
792
+ newTickSize: payload.new_tick_size || '',
793
+ timestamp,
794
+ };
795
+ }
796
+ /**
797
+ * Calculate derived price using Polymarket's display logic:
798
+ * - If spread <= 0.10: use midpoint
799
+ * - If spread > 0.10: use last trade price
800
+ */
801
+ calculateDerivedPrice(assetId, book) {
802
+ if (book.bids.length === 0 || book.asks.length === 0) {
803
+ return null;
804
+ }
805
+ const bestBid = book.bids[0].price;
806
+ const bestAsk = book.asks[0].price;
807
+ const spread = bestAsk - bestBid;
808
+ const midpoint = (bestBid + bestAsk) / 2;
809
+ const lastTrade = this.lastTradeCache.get(assetId);
810
+ const lastTradePrice = lastTrade?.price ?? midpoint;
811
+ // Polymarket display logic
812
+ const displayPrice = spread <= 0.10 ? midpoint : lastTradePrice;
813
+ return {
814
+ assetId,
815
+ price: displayPrice,
816
+ midpoint,
817
+ spread,
818
+ timestamp: book.timestamp,
819
+ };
820
+ }
821
+ sendSubscription(msg) {
822
+ if (this.client && this.connected) {
823
+ this.client.subscribe(msg);
824
+ }
825
+ else {
826
+ this.log('Cannot subscribe: not connected');
827
+ }
828
+ }
829
+ sendUnsubscription(msg) {
830
+ if (this.client && this.connected) {
831
+ this.client.unsubscribe(msg);
832
+ }
833
+ }
834
+ log(message) {
835
+ if (this.config.debug) {
836
+ console.log(`[RealtimeService] ${message}`);
837
+ }
838
+ }
839
+ }
840
+ //# sourceMappingURL=realtime-service-v2.js.map