@catalyst-team/poly-sdk 0.4.3 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/src/clients/bridge-client.d.ts +131 -1
  2. package/dist/src/clients/bridge-client.d.ts.map +1 -1
  3. package/dist/src/clients/bridge-client.js +143 -0
  4. package/dist/src/clients/bridge-client.js.map +1 -1
  5. package/dist/src/core/order-status.d.ts +159 -0
  6. package/dist/src/core/order-status.d.ts.map +1 -0
  7. package/dist/src/core/order-status.js +254 -0
  8. package/dist/src/core/order-status.js.map +1 -0
  9. package/dist/src/core/types.d.ts +124 -0
  10. package/dist/src/core/types.d.ts.map +1 -1
  11. package/dist/src/core/types.js +120 -0
  12. package/dist/src/core/types.js.map +1 -1
  13. package/dist/src/index.d.ts +6 -1
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +6 -0
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/services/ctf-detector.d.ts +215 -0
  18. package/dist/src/services/ctf-detector.d.ts.map +1 -0
  19. package/dist/src/services/ctf-detector.js +420 -0
  20. package/dist/src/services/ctf-detector.js.map +1 -0
  21. package/dist/src/services/ctf-manager.d.ts +202 -0
  22. package/dist/src/services/ctf-manager.d.ts.map +1 -0
  23. package/dist/src/services/ctf-manager.js +542 -0
  24. package/dist/src/services/ctf-manager.js.map +1 -0
  25. package/dist/src/services/order-manager.d.ts +440 -0
  26. package/dist/src/services/order-manager.d.ts.map +1 -0
  27. package/dist/src/services/order-manager.js +853 -0
  28. package/dist/src/services/order-manager.js.map +1 -0
  29. package/dist/src/services/order-manager.test.d.ts +10 -0
  30. package/dist/src/services/order-manager.test.d.ts.map +1 -0
  31. package/dist/src/services/order-manager.test.js +751 -0
  32. package/dist/src/services/order-manager.test.js.map +1 -0
  33. package/dist/src/services/trading-service.d.ts +89 -1
  34. package/dist/src/services/trading-service.d.ts.map +1 -1
  35. package/dist/src/services/trading-service.js +227 -1
  36. package/dist/src/services/trading-service.js.map +1 -1
  37. package/package.json +1 -1
@@ -0,0 +1,853 @@
1
+ /**
2
+ * OrderManager - Unified Order Creation and Monitoring
3
+ *
4
+ * Core Design Philosophy:
5
+ * - Unifies order creation + lifecycle monitoring in one component
6
+ * - Encapsulates all Polymarket-specific details (USDC.e, tick size, tokenId, CTF, Polygon)
7
+ * - Provides complete lifecycle events (CLOB match → tx submit → on-chain confirm)
8
+ * - Auto-validates orders before submission (market state, balance, precision)
9
+ * - Auto-watches orders after creation
10
+ *
11
+ * ============================================================================
12
+ * ORDER TYPES AND LIFECYCLE
13
+ * ============================================================================
14
+ *
15
+ * Polymarket supports 4 order types, each with different lifecycle patterns:
16
+ *
17
+ * ┌─────────────────────────────────────────────────────────────────────────┐
18
+ * │ LIMIT ORDERS (createOrder) │
19
+ * ├─────────────────────────────────────────────────────────────────────────┤
20
+ * │ │
21
+ * │ GTC (Good Till Cancelled) - Default limit order │
22
+ * │ ─────────────────────────────────────────────────────────────────────── │
23
+ * │ Lifecycle: PENDING → OPEN → PARTIALLY_FILLED* → FILLED or CANCELLED │
24
+ * │ Duration: seconds → minutes → hours → days (until filled/cancelled) │
25
+ * │ │
26
+ * │ ┌─────────┐ placed ┌──────┐ partial ┌──────────────────┐ │
27
+ * │ │ PENDING │ ───────────→ │ OPEN │ ──────────→ │ PARTIALLY_FILLED │ │
28
+ * │ └─────────┘ └──────┘ └──────────────────┘ │
29
+ * │ │ │ │ │
30
+ * │ │ reject │ cancel │ fill/cancel│
31
+ * │ ↓ ↓ ↓ │
32
+ * │ ┌──────────┐ ┌───────────┐ ┌────────┐ │
33
+ * │ │ REJECTED │ │ CANCELLED │ │ FILLED │ │
34
+ * │ └──────────┘ └───────────┘ └────────┘ │
35
+ * │ │
36
+ * │ GTD (Good Till Date) - Limit order with expiration │
37
+ * │ ───────────────────────────────────────────────────────────────────── │
38
+ * │ Lifecycle: Same as GTC, but auto-expires at specified time │
39
+ * │ Duration: seconds → specified expiration time │
40
+ * │ Additional terminal state: EXPIRED (when expiration time reached) │
41
+ * │ │
42
+ * ├─────────────────────────────────────────────────────────────────────────┤
43
+ * │ MARKET ORDERS (createMarketOrder) │
44
+ * ├─────────────────────────────────────────────────────────────────────────┤
45
+ * │ │
46
+ * │ FOK (Fill Or Kill) - Must fill completely or cancel immediately │
47
+ * │ ───────────────────────────────────────────────────────────────────────│
48
+ * │ Lifecycle: PENDING → FILLED (success) or PENDING → CANCELLED (fail) │
49
+ * │ Duration: milliseconds (instant execution) │
50
+ * │ Key: NO partial fills - all or nothing │
51
+ * │ │
52
+ * │ ┌─────────┐ full fill ┌────────┐ │
53
+ * │ │ PENDING │ ─────────────→ │ FILLED │ │
54
+ * │ └─────────┘ └────────┘ │
55
+ * │ │ │
56
+ * │ │ cannot fill completely │
57
+ * │ ↓ │
58
+ * │ ┌───────────┐ │
59
+ * │ │ CANCELLED │ (filledSize = 0) │
60
+ * │ └───────────┘ │
61
+ * │ │
62
+ * │ FAK (Fill And Kill) - Fill what's available, cancel the rest │
63
+ * │ ───────────────────────────────────────────────────────────────────────│
64
+ * │ Lifecycle: PENDING → FILLED or PENDING → CANCELLED (with partial fill)│
65
+ * │ Duration: milliseconds (instant execution) │
66
+ * │ Key: May have partial fills, remainder is cancelled immediately │
67
+ * │ │
68
+ * │ ┌─────────┐ full fill ┌────────┐ │
69
+ * │ │ PENDING │ ─────────────→ │ FILLED │ │
70
+ * │ └─────────┘ └────────┘ │
71
+ * │ │ │
72
+ * │ │ partial fill + cancel rest │
73
+ * │ ↓ │
74
+ * │ ┌───────────┐ │
75
+ * │ │ CANCELLED │ (filledSize > 0, cancelledSize > 0) │
76
+ * │ └───────────┘ │
77
+ * │ │
78
+ * └─────────────────────────────────────────────────────────────────────────┘
79
+ *
80
+ * ============================================================================
81
+ * WHY ORDERMANAGER SUPPORTS ALL ORDER TYPES
82
+ * ============================================================================
83
+ *
84
+ * The key insight is that despite different lifecycles, all order types share
85
+ * the same underlying status model and event stream:
86
+ *
87
+ * 1. UNIFIED STATUS MODEL
88
+ * All orders use the same OrderStatus enum: PENDING, OPEN, PARTIALLY_FILLED,
89
+ * FILLED, CANCELLED, EXPIRED. The difference is which transitions are valid
90
+ * and how quickly they occur.
91
+ *
92
+ * 2. SAME WEBSOCKET/POLLING EVENTS
93
+ * RealtimeServiceV2's `clob_user` topic emits the same USER_ORDER and
94
+ * USER_TRADE events for all order types. The eventType field indicates:
95
+ * - PLACEMENT: Order entered the system
96
+ * - UPDATE: Order was modified (fill, partial fill)
97
+ * - CANCELLATION: Order was cancelled
98
+ *
99
+ * 3. STATUS TRANSITION VALIDATION
100
+ * The isValidStatusTransition() function handles all valid paths:
101
+ * - GTC/GTD: PENDING → OPEN → PARTIALLY_FILLED → FILLED/CANCELLED
102
+ * - FOK: PENDING → FILLED or PENDING → CANCELLED (no PARTIALLY_FILLED)
103
+ * - FAK: PENDING → FILLED or PENDING → CANCELLED (filledSize may be > 0)
104
+ *
105
+ * 4. TERMINAL STATE HANDLING
106
+ * All order types eventually reach a terminal state (FILLED, CANCELLED,
107
+ * EXPIRED). OrderManager auto-unwatches orders when they reach terminal
108
+ * states, regardless of order type.
109
+ *
110
+ * 5. FILL EVENT DETECTION
111
+ * Whether fills come rapidly (FOK/FAK) or slowly (GTC/GTD), the same
112
+ * fill detection logic works: compare filledSize changes and emit
113
+ * order_partially_filled or order_filled events.
114
+ *
115
+ * Key differences in behavior:
116
+ * ┌─────────────┬─────────────────────────────────────────────────────────┐
117
+ * │ Order Type │ Behavior │
118
+ * ├─────────────┼─────────────────────────────────────────────────────────┤
119
+ * │ GTC/GTD │ May emit many events over long time (hours/days) │
120
+ * │ │ watchOrder() monitors until filled/cancelled/expired │
121
+ * ├─────────────┼─────────────────────────────────────────────────────────┤
122
+ * │ FOK/FAK │ Emit 1-2 events almost instantly (milliseconds) │
123
+ * │ │ watchOrder() monitors briefly, auto-unwatches quickly │
124
+ * └─────────────┴─────────────────────────────────────────────────────────┘
125
+ *
126
+ * ============================================================================
127
+ * PLATFORM-SPECIFIC IMPLEMENTATION
128
+ * ============================================================================
129
+ *
130
+ * This is a Polymarket-specific implementation. The design allows future extraction
131
+ * to interfaces if supporting multiple prediction markets, but for now we focus on
132
+ * clean encapsulation with Polymarket details in private methods.
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * const orderMgr = new OrderManager({
137
+ * privateKey: '0x...',
138
+ * rateLimiter,
139
+ * cache,
140
+ * });
141
+ *
142
+ * await orderMgr.start();
143
+ *
144
+ * // Listen to complete lifecycle
145
+ * orderMgr.on('order_opened', (order) => {
146
+ * console.log('Order live in orderbook');
147
+ * });
148
+ *
149
+ * orderMgr.on('order_filled', (event) => {
150
+ * console.log(`Filled: ${event.fill.size} @ ${event.fill.price}`);
151
+ * });
152
+ *
153
+ * orderMgr.on('transaction_confirmed', (event) => {
154
+ * console.log(`On-chain settled: ${event.transactionHash}`);
155
+ * });
156
+ *
157
+ * // Create limit order (GTC by default)
158
+ * const limitResult = await orderMgr.createOrder({
159
+ * tokenId: '0x...',
160
+ * side: 'BUY',
161
+ * price: 0.52,
162
+ * size: 100,
163
+ * });
164
+ *
165
+ * // Create market order (FOK - fill completely or cancel)
166
+ * const fokResult = await orderMgr.createMarketOrder({
167
+ * tokenId: '0x...',
168
+ * side: 'BUY',
169
+ * amount: 50, // $50 USDC
170
+ * orderType: 'FOK',
171
+ * });
172
+ *
173
+ * // Create market order (FAK - fill what's available)
174
+ * const fakResult = await orderMgr.createMarketOrder({
175
+ * tokenId: '0x...',
176
+ * side: 'BUY',
177
+ * amount: 50,
178
+ * orderType: 'FAK',
179
+ * });
180
+ * ```
181
+ */
182
+ import { EventEmitter } from 'events';
183
+ import { ethers } from 'ethers';
184
+ import { TradingService } from './trading-service.js';
185
+ import { RateLimiter } from '../core/rate-limiter.js';
186
+ import { createUnifiedCache } from '../core/unified-cache.js';
187
+ import { OrderStatus } from '../core/types.js';
188
+ import { isTerminalStatus, isValidStatusTransition } from '../core/order-status.js';
189
+ import { PolymarketError, ErrorCode } from '../core/errors.js';
190
+ // ============================================================================
191
+ // OrderManager Implementation
192
+ // ============================================================================
193
+ export class OrderManager extends EventEmitter {
194
+ // ========== Polymarket Service Dependencies ==========
195
+ tradingService;
196
+ realtimeService = null;
197
+ polygonProvider = null;
198
+ // ========== Configuration ==========
199
+ config;
200
+ initialized = false;
201
+ // ========== Monitoring State ==========
202
+ watchedOrders = new Map();
203
+ processedEvents = new Set();
204
+ mode;
205
+ // ========== Market Metadata Cache (Polymarket-specific) ==========
206
+ marketCache = new Map();
207
+ constructor(config) {
208
+ super();
209
+ this.config = {
210
+ ...config,
211
+ chainId: config.chainId ?? 137,
212
+ mode: config.mode ?? 'hybrid',
213
+ pollingInterval: config.pollingInterval ?? 5000,
214
+ polygonRpcUrl: config.polygonRpcUrl ?? 'https://polygon-rpc.com',
215
+ };
216
+ this.mode = this.config.mode;
217
+ // Create default RateLimiter and Cache if not provided
218
+ const rateLimiter = config.rateLimiter || new RateLimiter();
219
+ const cache = config.cache || createUnifiedCache();
220
+ // Initialize TradingService (always needed)
221
+ this.tradingService = new TradingService(rateLimiter, cache, {
222
+ privateKey: config.privateKey,
223
+ chainId: this.config.chainId,
224
+ });
225
+ }
226
+ // ============================================================================
227
+ // Lifecycle Management
228
+ // ============================================================================
229
+ /**
230
+ * Initialize OrderManager
231
+ * - Initializes TradingService
232
+ * - Optionally initializes RealtimeService (for WebSocket mode)
233
+ * - Optionally initializes Polygon provider (for settlement tracking)
234
+ */
235
+ async start() {
236
+ if (this.initialized)
237
+ return;
238
+ // Initialize TradingService
239
+ await this.tradingService.initialize();
240
+ // Initialize Polygon provider for settlement tracking
241
+ // (Polymarket-specific: tracks CTF token transfers on Polygon)
242
+ if (this.config.polygonRpcUrl) {
243
+ this.polygonProvider = new ethers.providers.JsonRpcProvider(this.config.polygonRpcUrl);
244
+ }
245
+ this.initialized = true;
246
+ this.emit('initialized');
247
+ }
248
+ /**
249
+ * Stop OrderManager
250
+ * - Unwatch all orders
251
+ * - Disconnect WebSocket (if connected)
252
+ */
253
+ stop() {
254
+ // Stop all polling
255
+ for (const watched of this.watchedOrders.values()) {
256
+ if (watched.pollingIntervalId) {
257
+ clearInterval(watched.pollingIntervalId);
258
+ }
259
+ }
260
+ // Disconnect WebSocket
261
+ if (this.realtimeService) {
262
+ this.realtimeService.disconnect();
263
+ this.realtimeService = null;
264
+ }
265
+ this.watchedOrders.clear();
266
+ this.processedEvents.clear();
267
+ this.initialized = false;
268
+ this.emit('stopped');
269
+ }
270
+ // ============================================================================
271
+ // Public API - Order Operations
272
+ // ============================================================================
273
+ /**
274
+ * Create and submit a limit order
275
+ *
276
+ * Auto-validates (Polymarket-specific):
277
+ * - Market state (active, not closed)
278
+ * - USDC.e balance
279
+ * - Tick size (0.01)
280
+ * - Minimum size (5 shares)
281
+ * - Minimum value ($1)
282
+ *
283
+ * Auto-watches: Starts monitoring immediately after creation
284
+ *
285
+ * @param params Order parameters
286
+ * @param metadata Optional metadata for tracking
287
+ * @returns Order result with orderId or error
288
+ */
289
+ async createOrder(params, metadata) {
290
+ this.ensureInitialized();
291
+ // Validate order (Polymarket-specific checks)
292
+ try {
293
+ await this.validateOrder(params);
294
+ }
295
+ catch (error) {
296
+ const rejectEvent = {
297
+ params,
298
+ reason: error instanceof Error ? error.message : String(error),
299
+ timestamp: Date.now(),
300
+ };
301
+ this.emit('order_rejected', rejectEvent);
302
+ return {
303
+ success: false,
304
+ errorMsg: rejectEvent.reason,
305
+ };
306
+ }
307
+ // Submit order via TradingService
308
+ const result = await this.tradingService.createLimitOrder(params);
309
+ if (result.success && result.orderId) {
310
+ // Auto-watch the order
311
+ this.watchOrder(result.orderId, metadata);
312
+ // Emit order_created event
313
+ this.emit('order_created', {
314
+ id: result.orderId,
315
+ status: OrderStatus.PENDING,
316
+ tokenId: params.tokenId,
317
+ side: params.side,
318
+ price: params.price,
319
+ originalSize: params.size,
320
+ filledSize: 0,
321
+ remainingSize: params.size,
322
+ associateTrades: [],
323
+ createdAt: Date.now(),
324
+ });
325
+ }
326
+ return result;
327
+ }
328
+ /**
329
+ * Create and submit a market order (FOK or FAK)
330
+ *
331
+ * Market order lifecycle:
332
+ * - FOK (Fill Or Kill): Must fill completely or cancels immediately
333
+ * PENDING → FILLED (success) or PENDING → CANCELLED (failed to fill)
334
+ * - FAK (Fill And Kill): Fills what it can, cancels the rest
335
+ * PENDING → PARTIALLY_FILLED + CANCELLED (partial) or PENDING → FILLED (complete)
336
+ *
337
+ * Auto-validates (Polymarket-specific):
338
+ * - Minimum value ($1)
339
+ *
340
+ * Auto-watches: Starts monitoring immediately after creation
341
+ * Note: Market orders typically complete very quickly (within seconds)
342
+ *
343
+ * @param params Market order parameters
344
+ * @param metadata Optional metadata for tracking
345
+ * @returns Order result with orderId or error
346
+ */
347
+ async createMarketOrder(params, metadata) {
348
+ this.ensureInitialized();
349
+ // Validate market order (Polymarket-specific checks)
350
+ try {
351
+ await this.validateMarketOrder(params);
352
+ }
353
+ catch (error) {
354
+ const rejectEvent = {
355
+ params: params, // MarketOrderParams is compatible enough
356
+ reason: error instanceof Error ? error.message : String(error),
357
+ timestamp: Date.now(),
358
+ };
359
+ this.emit('order_rejected', rejectEvent);
360
+ return {
361
+ success: false,
362
+ errorMsg: rejectEvent.reason,
363
+ };
364
+ }
365
+ // Submit market order via TradingService
366
+ const result = await this.tradingService.createMarketOrder(params);
367
+ if (result.success && result.orderId) {
368
+ // Auto-watch the order
369
+ this.watchOrder(result.orderId, {
370
+ ...metadata,
371
+ orderType: params.orderType || 'FOK', // Track order type for lifecycle handling
372
+ });
373
+ // Emit order_created event
374
+ this.emit('order_created', {
375
+ id: result.orderId,
376
+ status: OrderStatus.PENDING,
377
+ tokenId: params.tokenId,
378
+ side: params.side,
379
+ price: params.price || 0, // Market orders may not have a fixed price
380
+ originalSize: params.amount, // For market orders, amount is in USDC
381
+ filledSize: 0,
382
+ remainingSize: params.amount,
383
+ associateTrades: [],
384
+ createdAt: Date.now(),
385
+ orderType: params.orderType || 'FOK',
386
+ });
387
+ }
388
+ return result;
389
+ }
390
+ /**
391
+ * Cancel an order
392
+ * Auto-unwatches after cancellation
393
+ */
394
+ async cancelOrder(orderId) {
395
+ this.ensureInitialized();
396
+ const result = await this.tradingService.cancelOrder(orderId);
397
+ if (result.success) {
398
+ // Unwatch order (will stop polling/listening)
399
+ this.unwatchOrder(orderId);
400
+ }
401
+ return result;
402
+ }
403
+ /**
404
+ * Get order details
405
+ * Fetches from TradingService (CLOB API)
406
+ */
407
+ async getOrder(orderId) {
408
+ this.ensureInitialized();
409
+ return this.tradingService.getOrder(orderId);
410
+ }
411
+ /**
412
+ * Create multiple orders in batch
413
+ * Auto-watches all successfully created orders
414
+ */
415
+ async createBatchOrders(orders, metadata) {
416
+ this.ensureInitialized();
417
+ const result = await this.tradingService.createBatchOrders(orders);
418
+ if (result.success && result.orderIds) {
419
+ // Auto-watch all created orders
420
+ for (const orderId of result.orderIds) {
421
+ this.watchOrder(orderId, metadata);
422
+ }
423
+ }
424
+ return result;
425
+ }
426
+ // ============================================================================
427
+ // Public API - Order Monitoring
428
+ // ============================================================================
429
+ /**
430
+ * Manually watch an order
431
+ * Used for monitoring externally-created orders
432
+ *
433
+ * @param orderId Order ID to watch
434
+ * @param metadata Optional metadata
435
+ */
436
+ watchOrder(orderId, metadata) {
437
+ if (this.watchedOrders.has(orderId)) {
438
+ return; // Already watching
439
+ }
440
+ // Create initial watched state
441
+ const watched = {
442
+ orderId,
443
+ order: {
444
+ id: orderId,
445
+ status: OrderStatus.PENDING,
446
+ tokenId: '',
447
+ side: 'BUY',
448
+ price: 0,
449
+ originalSize: 0,
450
+ filledSize: 0,
451
+ remainingSize: 0,
452
+ associateTrades: [],
453
+ createdAt: Date.now(),
454
+ },
455
+ metadata,
456
+ lastStatus: OrderStatus.PENDING,
457
+ };
458
+ this.watchedOrders.set(orderId, watched);
459
+ // Start monitoring based on mode
460
+ if (this.mode === 'websocket' || this.mode === 'hybrid') {
461
+ // Fire and forget - WebSocket connection is lazy initialized
462
+ this.ensureWebSocketConnected().catch(err => {
463
+ this.emit('error', new Error(`Failed to establish WebSocket connection: ${err.message}`));
464
+ });
465
+ }
466
+ if (this.mode === 'polling' || this.mode === 'hybrid') {
467
+ this.startPolling(orderId);
468
+ }
469
+ this.emit('watch_started', orderId);
470
+ }
471
+ /**
472
+ * Stop watching an order
473
+ */
474
+ unwatchOrder(orderId) {
475
+ const watched = this.watchedOrders.get(orderId);
476
+ if (!watched)
477
+ return;
478
+ // Stop polling if active
479
+ if (watched.pollingIntervalId) {
480
+ clearInterval(watched.pollingIntervalId);
481
+ }
482
+ this.watchedOrders.delete(orderId);
483
+ this.emit('watch_stopped', orderId);
484
+ }
485
+ /**
486
+ * Get all watched orders
487
+ */
488
+ getWatchedOrders() {
489
+ return Array.from(this.watchedOrders.values()).map((w) => w.order);
490
+ }
491
+ /**
492
+ * Get watched order by ID
493
+ */
494
+ getWatchedOrder(orderId) {
495
+ return this.watchedOrders.get(orderId)?.order;
496
+ }
497
+ // ============================================================================
498
+ // Private - Order Validation (Polymarket-specific)
499
+ // ============================================================================
500
+ /**
501
+ * Validate order parameters before submission
502
+ * All checks here are Polymarket-specific
503
+ */
504
+ async validateOrder(params) {
505
+ // 1. Tick size validation (Polymarket: 0.01)
506
+ // Use integer math to avoid floating point precision issues
507
+ const priceInCents = Math.round(params.price * 100);
508
+ const epsilon = 0.001; // Tolerance for floating point errors
509
+ if (Math.abs(priceInCents - params.price * 100) > epsilon) {
510
+ throw new PolymarketError(ErrorCode.ORDER_REJECTED, `Price must be multiple of 0.01 tick size (got ${params.price})`);
511
+ }
512
+ // 2. Minimum size validation (Polymarket: 5 shares)
513
+ if (params.size < 5) {
514
+ throw new PolymarketError(ErrorCode.ORDER_REJECTED, `Size must be at least 5 shares (got ${params.size})`);
515
+ }
516
+ // 3. Minimum value validation (Polymarket: $1)
517
+ const orderValue = params.price * params.size;
518
+ if (orderValue < 1) {
519
+ throw new PolymarketError(ErrorCode.ORDER_REJECTED, `Order value must be at least $1 (got $${orderValue.toFixed(2)})`);
520
+ }
521
+ // Note: Balance and market state checks would require additional service dependencies
522
+ // For now, we rely on TradingService validation
523
+ }
524
+ /**
525
+ * Validate market order parameters before submission
526
+ * Market orders have simpler validation than limit orders
527
+ */
528
+ async validateMarketOrder(params) {
529
+ // 1. Minimum value validation (Polymarket: $1)
530
+ if (params.amount < 1) {
531
+ throw new PolymarketError(ErrorCode.ORDER_REJECTED, `Order amount must be at least $1 (got $${params.amount.toFixed(2)})`);
532
+ }
533
+ // 2. Validate order type if specified
534
+ if (params.orderType && !['FOK', 'FAK'].includes(params.orderType)) {
535
+ throw new PolymarketError(ErrorCode.ORDER_REJECTED, `Invalid market order type: ${params.orderType}. Must be FOK or FAK.`);
536
+ }
537
+ // Note: Price validation is optional for market orders
538
+ // If price is provided, it's used as a limit price (max for BUY, min for SELL)
539
+ }
540
+ // ============================================================================
541
+ // Private - WebSocket Monitoring (Polymarket-specific)
542
+ // ============================================================================
543
+ /**
544
+ * Ensure WebSocket connection is established
545
+ * Lazy initialization of RealtimeService
546
+ */
547
+ async ensureWebSocketConnected() {
548
+ if (this.realtimeService)
549
+ return;
550
+ // Import and initialize RealtimeServiceV2
551
+ // (We use dynamic import to avoid circular dependencies)
552
+ const { RealtimeServiceV2 } = await import('./realtime-service-v2.js');
553
+ this.realtimeService = new RealtimeServiceV2({ autoReconnect: true });
554
+ // Connect to WebSocket
555
+ if (this.realtimeService) {
556
+ this.realtimeService.connect();
557
+ }
558
+ // Subscribe to user events (requires credentials)
559
+ const credentials = this.tradingService.getCredentials();
560
+ if (credentials && this.realtimeService) {
561
+ this.realtimeService.subscribeUserEvents(credentials, {
562
+ onOrder: this.handleUserOrder.bind(this),
563
+ onTrade: this.handleUserTrade.bind(this),
564
+ });
565
+ }
566
+ }
567
+ /**
568
+ * Handle USER_ORDER WebSocket event (Polymarket-specific)
569
+ * Triggered on: PLACEMENT, UPDATE, CANCELLATION
570
+ */
571
+ handleUserOrder(userOrder) {
572
+ const watched = this.watchedOrders.get(userOrder.orderId);
573
+ if (!watched)
574
+ return; // Not watching this order
575
+ // Deduplicate events
576
+ const eventKey = `order_${userOrder.orderId}_${userOrder.timestamp}_${userOrder.eventType}`;
577
+ if (this.processedEvents.has(eventKey))
578
+ return;
579
+ this.processedEvents.add(eventKey);
580
+ // Update order state
581
+ watched.order.price = userOrder.price;
582
+ watched.order.originalSize = userOrder.originalSize;
583
+ watched.order.filledSize = userOrder.matchedSize;
584
+ watched.order.remainingSize = userOrder.originalSize - userOrder.matchedSize;
585
+ // Infer new status
586
+ let newStatus = watched.lastStatus;
587
+ if (userOrder.eventType === 'PLACEMENT') {
588
+ newStatus = OrderStatus.OPEN;
589
+ }
590
+ else if (userOrder.eventType === 'UPDATE') {
591
+ if (userOrder.matchedSize > 0) {
592
+ newStatus =
593
+ userOrder.matchedSize >= userOrder.originalSize
594
+ ? OrderStatus.FILLED
595
+ : OrderStatus.PARTIALLY_FILLED;
596
+ }
597
+ }
598
+ else if (userOrder.eventType === 'CANCELLATION') {
599
+ newStatus = OrderStatus.CANCELLED;
600
+ }
601
+ // Emit status change if changed
602
+ if (newStatus !== watched.lastStatus) {
603
+ this.emitStatusChange(watched, newStatus, 'websocket');
604
+ }
605
+ }
606
+ /**
607
+ * Handle USER_TRADE WebSocket event (Polymarket-specific)
608
+ * Triggered when: MATCHED, MINED, CONFIRMED
609
+ */
610
+ async handleUserTrade(userTrade) {
611
+ const watched = this.watchedOrders.get(userTrade.market);
612
+ if (!watched)
613
+ return;
614
+ // Deduplicate events
615
+ const eventKey = `trade_${userTrade.tradeId}_${userTrade.status}_${userTrade.timestamp}`;
616
+ if (this.processedEvents.has(eventKey))
617
+ return;
618
+ this.processedEvents.add(eventKey);
619
+ // Emit fill event
620
+ const isCompleteFill = watched.order.filledSize + userTrade.size >= watched.order.originalSize;
621
+ const fillEvent = {
622
+ orderId: watched.orderId,
623
+ order: watched.order,
624
+ fill: {
625
+ tradeId: userTrade.tradeId,
626
+ size: userTrade.size,
627
+ price: userTrade.price,
628
+ fee: 0, // Fee info not in UserTrade
629
+ timestamp: userTrade.timestamp,
630
+ transactionHash: userTrade.transactionHash,
631
+ },
632
+ cumulativeFilled: watched.order.filledSize + userTrade.size,
633
+ remainingSize: watched.order.originalSize - (watched.order.filledSize + userTrade.size),
634
+ isCompleteFill,
635
+ };
636
+ if (isCompleteFill) {
637
+ this.emit('order_filled', fillEvent);
638
+ }
639
+ else {
640
+ this.emit('order_partially_filled', fillEvent);
641
+ }
642
+ // If trade has transaction hash, emit transaction event
643
+ if (userTrade.transactionHash) {
644
+ const txEvent = {
645
+ orderId: watched.orderId,
646
+ tradeId: userTrade.tradeId,
647
+ transactionHash: userTrade.transactionHash,
648
+ timestamp: Date.now(),
649
+ };
650
+ this.emit('transaction_submitted', txEvent);
651
+ // Start tracking settlement on Polygon
652
+ if (userTrade.status === 'CONFIRMED') {
653
+ await this.trackSettlement(userTrade.tradeId, userTrade.transactionHash, watched.orderId);
654
+ }
655
+ }
656
+ }
657
+ // ============================================================================
658
+ // Private - Polling Monitoring
659
+ // ============================================================================
660
+ /**
661
+ * Start polling for order status
662
+ * Fallback mechanism when WebSocket is unavailable
663
+ */
664
+ startPolling(orderId) {
665
+ const watched = this.watchedOrders.get(orderId);
666
+ if (!watched || watched.pollingIntervalId)
667
+ return;
668
+ const poll = async () => {
669
+ try {
670
+ const order = await this.tradingService.getOrder(orderId);
671
+ if (order) {
672
+ this.updateWatchedOrder(orderId, order);
673
+ }
674
+ }
675
+ catch (error) {
676
+ this.emit('error', new Error(`Polling error for ${orderId}: ${error}`));
677
+ }
678
+ };
679
+ // Initial poll
680
+ poll();
681
+ // Set up interval
682
+ watched.pollingIntervalId = setInterval(poll, this.config.pollingInterval);
683
+ }
684
+ /**
685
+ * Update watched order from polling result
686
+ * Detects both status changes and fill changes
687
+ */
688
+ updateWatchedOrder(orderId, freshOrder) {
689
+ const watched = this.watchedOrders.get(orderId);
690
+ if (!watched)
691
+ return;
692
+ const oldStatus = watched.lastStatus;
693
+ const oldFilledSize = watched.order.filledSize;
694
+ const newStatus = freshOrder.status;
695
+ const newFilledSize = freshOrder.filledSize;
696
+ // Detect fill changes (size increased)
697
+ // Only emit if this is an actual increase from a previously known state
698
+ if (newFilledSize > oldFilledSize && oldFilledSize >= 0) {
699
+ const fillDelta = newFilledSize - oldFilledSize;
700
+ const isCompleteFill = freshOrder.remainingSize === 0;
701
+ // Deduplicate fill events using fill size as key
702
+ const eventKey = `fill_${orderId}_${newFilledSize}`;
703
+ if (!this.processedEvents.has(eventKey)) {
704
+ this.processedEvents.add(eventKey);
705
+ // Estimate fill price from order price or calculate from avg
706
+ // Note: OpenOrder doesn't have avgFillPrice, so we use order.price as estimate
707
+ const estimatedPrice = freshOrder.price;
708
+ const fillEvent = {
709
+ orderId,
710
+ order: freshOrder,
711
+ fill: {
712
+ tradeId: freshOrder.associateTrades[freshOrder.associateTrades.length - 1] || `polling_${Date.now()}`,
713
+ size: fillDelta,
714
+ price: estimatedPrice,
715
+ fee: 0, // Fee info not available in polling
716
+ timestamp: Date.now(),
717
+ },
718
+ cumulativeFilled: newFilledSize,
719
+ remainingSize: freshOrder.remainingSize,
720
+ isCompleteFill,
721
+ };
722
+ if (isCompleteFill) {
723
+ this.emit('order_filled', fillEvent);
724
+ }
725
+ else {
726
+ this.emit('order_partially_filled', fillEvent);
727
+ }
728
+ }
729
+ }
730
+ // Update order
731
+ watched.order = freshOrder;
732
+ // Emit status change if changed
733
+ // Pass flag to prevent duplicate fill events
734
+ if (newStatus !== oldStatus) {
735
+ this.emitStatusChange(watched, newStatus, 'polling', newFilledSize > oldFilledSize);
736
+ }
737
+ }
738
+ // ============================================================================
739
+ // Private - Status Change Emission
740
+ // ============================================================================
741
+ /**
742
+ * Emit status change event with appropriate specific events
743
+ * @param fillAlreadyEmitted - Set to true if fill event was already emitted (to prevent duplicates)
744
+ */
745
+ emitStatusChange(watched, newStatus, source, fillAlreadyEmitted = false) {
746
+ const oldStatus = watched.lastStatus;
747
+ // Validate transition
748
+ if (!isValidStatusTransition(oldStatus, newStatus)) {
749
+ this.emit('error', new Error(`Invalid status transition: ${oldStatus} → ${newStatus} for ${watched.orderId}`));
750
+ return;
751
+ }
752
+ // Update status
753
+ watched.lastStatus = newStatus;
754
+ watched.order.status = newStatus;
755
+ watched.order.updatedAt = Date.now();
756
+ // Emit generic status_change event
757
+ const changeEvent = {
758
+ orderId: watched.orderId,
759
+ from: oldStatus,
760
+ to: newStatus,
761
+ order: watched.order,
762
+ timestamp: Date.now(),
763
+ reason: source,
764
+ };
765
+ this.emit('status_change', changeEvent);
766
+ // Emit specific events
767
+ if (newStatus === OrderStatus.OPEN) {
768
+ this.emit('order_opened', watched.order);
769
+ }
770
+ else if (newStatus === OrderStatus.FILLED && !fillAlreadyEmitted) {
771
+ // Only emit fill event if not already emitted (from polling fill detection)
772
+ this.emit('order_filled', {
773
+ orderId: watched.orderId,
774
+ order: watched.order,
775
+ fill: {
776
+ tradeId: watched.order.associateTrades[watched.order.associateTrades.length - 1] || '',
777
+ size: watched.order.filledSize,
778
+ price: watched.order.price,
779
+ fee: 0,
780
+ timestamp: Date.now(),
781
+ },
782
+ cumulativeFilled: watched.order.filledSize,
783
+ remainingSize: 0,
784
+ isCompleteFill: true,
785
+ });
786
+ }
787
+ else if (newStatus === OrderStatus.CANCELLED) {
788
+ const cancelEvent = {
789
+ orderId: watched.orderId,
790
+ order: watched.order,
791
+ filledSize: watched.order.filledSize,
792
+ cancelledSize: watched.order.remainingSize,
793
+ reason: 'user',
794
+ timestamp: Date.now(),
795
+ };
796
+ this.emit('order_cancelled', cancelEvent);
797
+ }
798
+ else if (newStatus === OrderStatus.EXPIRED) {
799
+ const expireEvent = {
800
+ orderId: watched.orderId,
801
+ order: watched.order,
802
+ filledSize: watched.order.filledSize,
803
+ expiredSize: watched.order.remainingSize,
804
+ expirationTime: watched.order.expiration || 0,
805
+ timestamp: Date.now(),
806
+ };
807
+ this.emit('order_expired', expireEvent);
808
+ }
809
+ // Auto-unwatch terminal states
810
+ if (isTerminalStatus(newStatus)) {
811
+ this.unwatchOrder(watched.orderId);
812
+ }
813
+ }
814
+ // ============================================================================
815
+ // Private - Chain Settlement Tracking (Polymarket-specific)
816
+ // ============================================================================
817
+ /**
818
+ * Track transaction settlement on Polygon blockchain
819
+ * Waits for on-chain confirmation and emits transaction_confirmed event
820
+ */
821
+ async trackSettlement(tradeId, transactionHash, orderId) {
822
+ if (!this.polygonProvider) {
823
+ return; // No provider configured
824
+ }
825
+ try {
826
+ // Wait for 1 confirmation
827
+ const receipt = await this.polygonProvider.waitForTransaction(transactionHash, 1);
828
+ if (receipt) {
829
+ const settlementEvent = {
830
+ orderId,
831
+ tradeId,
832
+ transactionHash,
833
+ blockNumber: receipt.blockNumber,
834
+ gasUsed: receipt.gasUsed.toString(),
835
+ timestamp: Date.now(),
836
+ };
837
+ this.emit('transaction_confirmed', settlementEvent);
838
+ }
839
+ }
840
+ catch (error) {
841
+ this.emit('error', new Error(`Settlement tracking failed for ${transactionHash}: ${error}`));
842
+ }
843
+ }
844
+ // ============================================================================
845
+ // Private - Utilities
846
+ // ============================================================================
847
+ ensureInitialized() {
848
+ if (!this.initialized) {
849
+ throw new PolymarketError(ErrorCode.ORDER_FAILED, 'OrderManager not initialized. Call start() first.');
850
+ }
851
+ }
852
+ }
853
+ //# sourceMappingURL=order-manager.js.map