@catalyst-team/poly-sdk 0.4.5 → 0.4.7

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 (63) hide show
  1. package/README.md +18 -9
  2. package/README.zh-CN.md +18 -9
  3. package/dist/src/clients/bridge-client.d.ts +131 -1
  4. package/dist/src/clients/bridge-client.d.ts.map +1 -1
  5. package/dist/src/clients/bridge-client.js +143 -0
  6. package/dist/src/clients/bridge-client.js.map +1 -1
  7. package/dist/src/clients/data-api.d.ts +25 -0
  8. package/dist/src/clients/data-api.d.ts.map +1 -1
  9. package/dist/src/clients/data-api.js +57 -0
  10. package/dist/src/clients/data-api.js.map +1 -1
  11. package/dist/src/core/types.d.ts +55 -0
  12. package/dist/src/core/types.d.ts.map +1 -1
  13. package/dist/src/index.d.ts +7 -6
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +4 -2
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/realtime/index.d.ts +18 -0
  18. package/dist/src/realtime/index.d.ts.map +1 -0
  19. package/dist/src/realtime/index.js +14 -0
  20. package/dist/src/realtime/index.js.map +1 -0
  21. package/dist/src/realtime/realtime-data-client.d.ts +274 -0
  22. package/dist/src/realtime/realtime-data-client.d.ts.map +1 -0
  23. package/dist/src/realtime/realtime-data-client.js +771 -0
  24. package/dist/src/realtime/realtime-data-client.js.map +1 -0
  25. package/dist/src/realtime/types.d.ts +485 -0
  26. package/dist/src/realtime/types.d.ts.map +1 -0
  27. package/dist/src/realtime/types.js +36 -0
  28. package/dist/src/realtime/types.js.map +1 -0
  29. package/dist/src/services/arbitrage-service.d.ts.map +1 -1
  30. package/dist/src/services/arbitrage-service.js +2 -1
  31. package/dist/src/services/arbitrage-service.js.map +1 -1
  32. package/dist/src/services/dip-arb-service.d.ts.map +1 -1
  33. package/dist/src/services/dip-arb-service.js +3 -19
  34. package/dist/src/services/dip-arb-service.js.map +1 -1
  35. package/dist/src/services/market-service.d.ts +93 -11
  36. package/dist/src/services/market-service.d.ts.map +1 -1
  37. package/dist/src/services/market-service.js +189 -22
  38. package/dist/src/services/market-service.js.map +1 -1
  39. package/dist/src/services/order-handle.test.d.ts +15 -0
  40. package/dist/src/services/order-handle.test.d.ts.map +1 -0
  41. package/dist/src/services/order-handle.test.js +333 -0
  42. package/dist/src/services/order-handle.test.js.map +1 -0
  43. package/dist/src/services/order-manager.d.ts +325 -10
  44. package/dist/src/services/order-manager.d.ts.map +1 -1
  45. package/dist/src/services/order-manager.js +633 -32
  46. package/dist/src/services/order-manager.js.map +1 -1
  47. package/dist/src/services/order-manager.test.d.ts +2 -0
  48. package/dist/src/services/order-manager.test.d.ts.map +1 -1
  49. package/dist/src/services/order-manager.test.js +274 -0
  50. package/dist/src/services/order-manager.test.js.map +1 -1
  51. package/dist/src/services/realtime-service-v2.d.ts +122 -6
  52. package/dist/src/services/realtime-service-v2.d.ts.map +1 -1
  53. package/dist/src/services/realtime-service-v2.js +475 -70
  54. package/dist/src/services/realtime-service-v2.js.map +1 -1
  55. package/dist/src/services/trading-service.d.ts +129 -1
  56. package/dist/src/services/trading-service.d.ts.map +1 -1
  57. package/dist/src/services/trading-service.js +198 -5
  58. package/dist/src/services/trading-service.js.map +1 -1
  59. package/package.json +1 -2
  60. package/dist/src/services/ctf-detector.d.ts +0 -215
  61. package/dist/src/services/ctf-detector.d.ts.map +0 -1
  62. package/dist/src/services/ctf-detector.js +0 -420
  63. package/dist/src/services/ctf-detector.js.map +0 -1
@@ -8,7 +8,125 @@
8
8
  * - Auto-validates orders before submission (market state, balance, precision)
9
9
  * - Auto-watches orders after creation
10
10
  *
11
- * Platform-Specific Implementation:
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
+ *
12
130
  * This is a Polymarket-specific implementation. The design allows future extraction
13
131
  * to interfaces if supporting multiple prediction markets, but for now we focus on
14
132
  * clean encapsulation with Polymarket details in private methods.
@@ -36,13 +154,29 @@
36
154
  * console.log(`On-chain settled: ${event.transactionHash}`);
37
155
  * });
38
156
  *
39
- * // Create order (auto-validates + auto-watches)
40
- * const result = await orderMgr.createOrder({
157
+ * // Create limit order (GTC by default)
158
+ * const limitResult = await orderMgr.createOrder({
41
159
  * tokenId: '0x...',
42
160
  * side: 'BUY',
43
161
  * price: 0.52,
44
162
  * size: 100,
45
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
+ * });
46
180
  * ```
47
181
  */
48
182
  import { EventEmitter } from 'events';
@@ -53,6 +187,219 @@ import { createUnifiedCache } from '../core/unified-cache.js';
53
187
  import { OrderStatus } from '../core/types.js';
54
188
  import { isTerminalStatus, isValidStatusTransition } from '../core/order-status.js';
55
189
  import { PolymarketError, ErrorCode } from '../core/errors.js';
190
+ /**
191
+ * Internal implementation of OrderHandle.
192
+ *
193
+ * Lifecycle:
194
+ * 1. Constructor receives orderManager ref and an executor function
195
+ * 2. Status starts at 'created'
196
+ * 3. Executor is invoked immediately (fire-and-forget)
197
+ * 4. On success: stores orderId, subscribes to OrderManager events
198
+ * 5. On rejection: transitions to 'rejected', resolves promise
199
+ * 6. On terminal events: resolves promise, removes listeners
200
+ */
201
+ export class OrderHandleImpl {
202
+ orderManager;
203
+ executor;
204
+ _orderId;
205
+ _status = 'created';
206
+ _fills = [];
207
+ _order = null;
208
+ _reason;
209
+ _resolve;
210
+ _promise;
211
+ _handlers = new Map();
212
+ _eventCleanup = null;
213
+ constructor(orderManager, executor) {
214
+ this.orderManager = orderManager;
215
+ this.executor = executor;
216
+ this._promise = new Promise((resolve) => {
217
+ this._resolve = resolve;
218
+ });
219
+ // Fire-and-forget execution
220
+ this.execute();
221
+ }
222
+ // ===== PromiseLike implementation =====
223
+ then(onfulfilled, onrejected) {
224
+ return this._promise.then(onfulfilled, onrejected);
225
+ }
226
+ // ===== Chainable lifecycle callbacks =====
227
+ onAccepted(handler) {
228
+ this.addHandler('accepted', handler);
229
+ return this;
230
+ }
231
+ onPartialFill(handler) {
232
+ this.addHandler('partial_fill', handler);
233
+ return this;
234
+ }
235
+ onFilled(handler) {
236
+ this.addHandler('filled', handler);
237
+ return this;
238
+ }
239
+ onRejected(handler) {
240
+ this.addHandler('rejected', handler);
241
+ return this;
242
+ }
243
+ onCancelled(handler) {
244
+ this.addHandler('cancelled', handler);
245
+ return this;
246
+ }
247
+ onExpired(handler) {
248
+ this.addHandler('expired', handler);
249
+ return this;
250
+ }
251
+ // ===== Operations =====
252
+ async cancel() {
253
+ if (!this._orderId)
254
+ return false;
255
+ if (this.isTerminal())
256
+ return false;
257
+ const result = await this.orderManager.cancelOrder(this._orderId);
258
+ return result.success;
259
+ }
260
+ // ===== State query =====
261
+ get orderId() {
262
+ return this._orderId;
263
+ }
264
+ get status() {
265
+ return this._status;
266
+ }
267
+ // ===== Private implementation =====
268
+ async execute() {
269
+ try {
270
+ const result = await this.executor();
271
+ if (!result.success || !result.orderId) {
272
+ // Order rejected by API or validation
273
+ this._status = 'rejected';
274
+ this._reason = result.errorMsg || 'Order rejected';
275
+ this.invokeHandlers('rejected', this._reason);
276
+ this._resolve({
277
+ status: 'rejected',
278
+ order: null,
279
+ fills: [],
280
+ reason: this._reason,
281
+ });
282
+ return;
283
+ }
284
+ // Store orderId and subscribe to events
285
+ this._orderId = result.orderId;
286
+ this.subscribeToEvents();
287
+ }
288
+ catch (error) {
289
+ // Unexpected error during execution
290
+ this._status = 'rejected';
291
+ this._reason = error instanceof Error ? error.message : String(error);
292
+ this.invokeHandlers('rejected', this._reason);
293
+ this._resolve({
294
+ status: 'rejected',
295
+ order: null,
296
+ fills: [],
297
+ reason: this._reason,
298
+ });
299
+ }
300
+ }
301
+ subscribeToEvents() {
302
+ const onOpened = (order) => {
303
+ if (order.id !== this._orderId)
304
+ return;
305
+ this._status = 'open';
306
+ this._order = order;
307
+ this.invokeHandlers('accepted', order);
308
+ };
309
+ const onPartialFill = (fill) => {
310
+ if (fill.orderId !== this._orderId)
311
+ return;
312
+ this._status = 'partially_filled';
313
+ this._order = fill.order;
314
+ this._fills.push(fill);
315
+ this.invokeHandlers('partial_fill', fill);
316
+ };
317
+ const onFilled = (fill) => {
318
+ if (fill.orderId !== this._orderId)
319
+ return;
320
+ this._status = 'filled';
321
+ this._order = fill.order;
322
+ this._fills.push(fill);
323
+ this.invokeHandlers('filled', fill);
324
+ this.resolveTerminal('filled');
325
+ };
326
+ const onCancelled = (event) => {
327
+ if (event.orderId !== this._orderId)
328
+ return;
329
+ this._status = 'cancelled';
330
+ this._order = event.order;
331
+ this.invokeHandlers('cancelled', event.order);
332
+ this.resolveTerminal('cancelled', 'Order cancelled');
333
+ };
334
+ const onExpired = (event) => {
335
+ if (event.orderId !== this._orderId)
336
+ return;
337
+ this._status = 'expired';
338
+ this._order = event.order;
339
+ this.invokeHandlers('expired', event.order);
340
+ this.resolveTerminal('expired', 'Order expired');
341
+ };
342
+ const onRejected = (event) => {
343
+ if (event.orderId !== this._orderId)
344
+ return;
345
+ this._status = 'rejected';
346
+ this._reason = event.reason;
347
+ this.invokeHandlers('rejected', event.reason);
348
+ this.resolveTerminal('rejected', event.reason);
349
+ };
350
+ this.orderManager.on('order_opened', onOpened);
351
+ this.orderManager.on('order_partially_filled', onPartialFill);
352
+ this.orderManager.on('order_filled', onFilled);
353
+ this.orderManager.on('order_cancelled', onCancelled);
354
+ this.orderManager.on('order_expired', onExpired);
355
+ this.orderManager.on('order_rejected', onRejected);
356
+ this._eventCleanup = () => {
357
+ this.orderManager.removeListener('order_opened', onOpened);
358
+ this.orderManager.removeListener('order_partially_filled', onPartialFill);
359
+ this.orderManager.removeListener('order_filled', onFilled);
360
+ this.orderManager.removeListener('order_cancelled', onCancelled);
361
+ this.orderManager.removeListener('order_expired', onExpired);
362
+ this.orderManager.removeListener('order_rejected', onRejected);
363
+ };
364
+ }
365
+ resolveTerminal(status, reason) {
366
+ this.cleanup();
367
+ this._resolve({
368
+ status,
369
+ order: this._order,
370
+ fills: this._fills,
371
+ reason,
372
+ });
373
+ }
374
+ cleanup() {
375
+ if (this._eventCleanup) {
376
+ this._eventCleanup();
377
+ this._eventCleanup = null;
378
+ }
379
+ }
380
+ isTerminal() {
381
+ return this._status === 'filled' || this._status === 'cancelled' || this._status === 'rejected' || this._status === 'expired';
382
+ }
383
+ addHandler(event, handler) {
384
+ if (!this._handlers.has(event)) {
385
+ this._handlers.set(event, []);
386
+ }
387
+ this._handlers.get(event).push(handler);
388
+ }
389
+ invokeHandlers(event, ...args) {
390
+ const handlers = this._handlers.get(event);
391
+ if (!handlers)
392
+ return;
393
+ for (const handler of handlers) {
394
+ try {
395
+ handler(...args);
396
+ }
397
+ catch {
398
+ // Swallow handler errors to avoid breaking the lifecycle
399
+ }
400
+ }
401
+ }
402
+ }
56
403
  // ============================================================================
57
404
  // OrderManager Implementation
58
405
  // ============================================================================
@@ -75,7 +422,10 @@ export class OrderManager extends EventEmitter {
75
422
  this.config = {
76
423
  ...config,
77
424
  chainId: config.chainId ?? 137,
78
- mode: config.mode ?? 'hybrid',
425
+ // Bug 24 fix: Default to 'websocket' mode instead of 'hybrid'
426
+ // Polling mechanism can update watched orders with incorrect tokenId,
427
+ // causing fill events to be processed with wrong token type
428
+ mode: config.mode ?? 'websocket',
79
429
  pollingInterval: config.pollingInterval ?? 5000,
80
430
  polygonRpcUrl: config.polygonRpcUrl ?? 'https://polygon-rpc.com',
81
431
  };
@@ -173,8 +523,13 @@ export class OrderManager extends EventEmitter {
173
523
  // Submit order via TradingService
174
524
  const result = await this.tradingService.createLimitOrder(params);
175
525
  if (result.success && result.orderId) {
176
- // Auto-watch the order
177
- this.watchOrder(result.orderId, metadata);
526
+ // Auto-watch the order with initial order info (Bug 24 fix)
527
+ this.watchOrder(result.orderId, metadata, {
528
+ tokenId: params.tokenId,
529
+ side: params.side,
530
+ price: params.price,
531
+ size: params.size,
532
+ });
178
533
  // Emit order_created event
179
534
  this.emit('order_created', {
180
535
  id: result.orderId,
@@ -191,6 +546,73 @@ export class OrderManager extends EventEmitter {
191
546
  }
192
547
  return result;
193
548
  }
549
+ /**
550
+ * Create and submit a market order (FOK or FAK)
551
+ *
552
+ * Market order lifecycle:
553
+ * - FOK (Fill Or Kill): Must fill completely or cancels immediately
554
+ * PENDING → FILLED (success) or PENDING → CANCELLED (failed to fill)
555
+ * - FAK (Fill And Kill): Fills what it can, cancels the rest
556
+ * PENDING → PARTIALLY_FILLED + CANCELLED (partial) or PENDING → FILLED (complete)
557
+ *
558
+ * Auto-validates (Polymarket-specific):
559
+ * - Minimum value ($1)
560
+ *
561
+ * Auto-watches: Starts monitoring immediately after creation
562
+ * Note: Market orders typically complete very quickly (within seconds)
563
+ *
564
+ * @param params Market order parameters
565
+ * @param metadata Optional metadata for tracking
566
+ * @returns Order result with orderId or error
567
+ */
568
+ async createMarketOrder(params, metadata) {
569
+ this.ensureInitialized();
570
+ // Validate market order (Polymarket-specific checks)
571
+ try {
572
+ await this.validateMarketOrder(params);
573
+ }
574
+ catch (error) {
575
+ const rejectEvent = {
576
+ params: params, // MarketOrderParams is compatible enough
577
+ reason: error instanceof Error ? error.message : String(error),
578
+ timestamp: Date.now(),
579
+ };
580
+ this.emit('order_rejected', rejectEvent);
581
+ return {
582
+ success: false,
583
+ errorMsg: rejectEvent.reason,
584
+ };
585
+ }
586
+ // Submit market order via TradingService
587
+ const result = await this.tradingService.createMarketOrder(params);
588
+ if (result.success && result.orderId) {
589
+ // Auto-watch the order with initial order info (Bug 24 fix)
590
+ this.watchOrder(result.orderId, {
591
+ ...metadata,
592
+ orderType: params.orderType || 'FOK', // Track order type for lifecycle handling
593
+ }, {
594
+ tokenId: params.tokenId,
595
+ side: params.side,
596
+ price: params.price,
597
+ size: params.amount,
598
+ });
599
+ // Emit order_created event
600
+ this.emit('order_created', {
601
+ id: result.orderId,
602
+ status: OrderStatus.PENDING,
603
+ tokenId: params.tokenId,
604
+ side: params.side,
605
+ price: params.price || 0, // Market orders may not have a fixed price
606
+ originalSize: params.amount, // For market orders, amount is in USDC
607
+ filledSize: 0,
608
+ remainingSize: params.amount,
609
+ associateTrades: [],
610
+ createdAt: Date.now(),
611
+ orderType: params.orderType || 'FOK',
612
+ });
613
+ }
614
+ return result;
615
+ }
194
616
  /**
195
617
  * Cancel an order
196
618
  * Auto-unwatches after cancellation
@@ -204,6 +626,21 @@ export class OrderManager extends EventEmitter {
204
626
  }
205
627
  return result;
206
628
  }
629
+ /**
630
+ * Cancel all open orders for this wallet.
631
+ * Useful for cleanup on strategy shutdown.
632
+ */
633
+ async cancelAllOrders() {
634
+ this.ensureInitialized();
635
+ const result = await this.tradingService.cancelAllOrders();
636
+ if (result.success) {
637
+ // Unwatch all orders
638
+ for (const orderId of this.watchedOrders.keys()) {
639
+ this.unwatchOrder(orderId);
640
+ }
641
+ }
642
+ return result;
643
+ }
207
644
  /**
208
645
  * Get order details
209
646
  * Fetches from TradingService (CLOB API)
@@ -228,36 +665,93 @@ export class OrderManager extends EventEmitter {
228
665
  return result;
229
666
  }
230
667
  // ============================================================================
668
+ // Public API - OrderHandle (Fluent Lifecycle)
669
+ // ============================================================================
670
+ /**
671
+ * Place a limit order and return a fluent OrderHandle.
672
+ *
673
+ * The handle is PromiseLike - await it for the terminal result.
674
+ * Chain lifecycle callbacks for real-time updates.
675
+ *
676
+ * @param params Limit order parameters
677
+ * @param metadata Optional metadata for tracking
678
+ * @returns OrderHandle - chainable, awaitable order lifecycle
679
+ *
680
+ * @example
681
+ * ```typescript
682
+ * const handle = orderManager.placeOrder({
683
+ * tokenId: '0x...',
684
+ * side: 'BUY',
685
+ * price: 0.52,
686
+ * size: 100,
687
+ * });
688
+ *
689
+ * handle
690
+ * .onAccepted((order) => console.log('Live:', order.id))
691
+ * .onFilled((fill) => console.log('Done!'));
692
+ *
693
+ * const result = await handle;
694
+ * ```
695
+ */
696
+ placeOrder(params, metadata) {
697
+ return new OrderHandleImpl(this, () => this.createOrder(params, metadata));
698
+ }
699
+ /**
700
+ * Place a market order (FOK/FAK) and return a fluent OrderHandle.
701
+ *
702
+ * @param params Market order parameters
703
+ * @param metadata Optional metadata for tracking
704
+ * @returns OrderHandle - chainable, awaitable order lifecycle
705
+ *
706
+ * @example
707
+ * ```typescript
708
+ * const result = await orderManager.placeMarketOrder({
709
+ * tokenId: '0x...',
710
+ * side: 'BUY',
711
+ * amount: 50,
712
+ * orderType: 'FOK',
713
+ * });
714
+ * console.log(result.status); // 'filled' or 'cancelled'
715
+ * ```
716
+ */
717
+ placeMarketOrder(params, metadata) {
718
+ return new OrderHandleImpl(this, () => this.createMarketOrder(params, metadata));
719
+ }
720
+ // ============================================================================
231
721
  // Public API - Order Monitoring
232
722
  // ============================================================================
233
723
  /**
234
- * Manually watch an order
235
- * Used for monitoring externally-created orders
724
+ * Watch an order by ID
236
725
  *
237
- * @param orderId Order ID to watch
238
- * @param metadata Optional metadata
726
+ * @param orderId - Order ID to watch
727
+ * @param metadata - Optional metadata for strategy context
728
+ * @param initialOrderInfo - Optional initial order info (tokenId, side, price, size)
729
+ * to avoid relying on polling for accurate token type
239
730
  */
240
- watchOrder(orderId, metadata) {
731
+ watchOrder(orderId, metadata, initialOrderInfo) {
241
732
  if (this.watchedOrders.has(orderId)) {
242
733
  return; // Already watching
243
734
  }
244
735
  // Create initial watched state
736
+ // Bug 24 fix: Use initialOrderInfo if provided to ensure correct tokenId
245
737
  const watched = {
246
738
  orderId,
247
739
  order: {
248
740
  id: orderId,
249
741
  status: OrderStatus.PENDING,
250
- tokenId: '',
251
- side: 'BUY',
252
- price: 0,
253
- originalSize: 0,
742
+ tokenId: initialOrderInfo?.tokenId || '',
743
+ side: initialOrderInfo?.side || 'BUY',
744
+ price: initialOrderInfo?.price || 0,
745
+ originalSize: initialOrderInfo?.size || 0,
254
746
  filledSize: 0,
255
- remainingSize: 0,
747
+ remainingSize: initialOrderInfo?.size || 0,
256
748
  associateTrades: [],
257
749
  createdAt: Date.now(),
258
750
  },
259
751
  metadata,
260
752
  lastStatus: OrderStatus.PENDING,
753
+ // Bug 24 fix: Store initialTokenId to protect from polling overwrites
754
+ initialTokenId: initialOrderInfo?.tokenId,
261
755
  };
262
756
  this.watchedOrders.set(orderId, watched);
263
757
  // Start monitoring based on mode
@@ -325,6 +819,22 @@ export class OrderManager extends EventEmitter {
325
819
  // Note: Balance and market state checks would require additional service dependencies
326
820
  // For now, we rely on TradingService validation
327
821
  }
822
+ /**
823
+ * Validate market order parameters before submission
824
+ * Market orders have simpler validation than limit orders
825
+ */
826
+ async validateMarketOrder(params) {
827
+ // 1. Minimum value validation (Polymarket: $1)
828
+ if (params.amount < 1) {
829
+ throw new PolymarketError(ErrorCode.ORDER_REJECTED, `Order amount must be at least $1 (got $${params.amount.toFixed(2)})`);
830
+ }
831
+ // 2. Validate order type if specified
832
+ if (params.orderType && !['FOK', 'FAK'].includes(params.orderType)) {
833
+ throw new PolymarketError(ErrorCode.ORDER_REJECTED, `Invalid market order type: ${params.orderType}. Must be FOK or FAK.`);
834
+ }
835
+ // Note: Price validation is optional for market orders
836
+ // If price is provided, it's used as a limit price (max for BUY, min for SELL)
837
+ }
328
838
  // ============================================================================
329
839
  // Private - WebSocket Monitoring (Polymarket-specific)
330
840
  // ============================================================================
@@ -339,17 +849,29 @@ export class OrderManager extends EventEmitter {
339
849
  // (We use dynamic import to avoid circular dependencies)
340
850
  const { RealtimeServiceV2 } = await import('./realtime-service-v2.js');
341
851
  this.realtimeService = new RealtimeServiceV2({ autoReconnect: true });
342
- // Connect to WebSocket
852
+ // Connect to WebSocket and wait for connection to be established
853
+ // connect() is async and returns a Promise that resolves when connected
343
854
  if (this.realtimeService) {
344
- this.realtimeService.connect();
855
+ console.log(`[OrderManager] Connecting to WebSocket...`);
856
+ await this.realtimeService.connect();
857
+ console.log(`[OrderManager] WebSocket connected successfully`);
345
858
  }
346
859
  // Subscribe to user events (requires credentials)
860
+ // Now safe to subscribe since connection is established
347
861
  const credentials = this.tradingService.getCredentials();
348
862
  if (credentials && this.realtimeService) {
349
- this.realtimeService.subscribeUserEvents(credentials, {
863
+ console.log(`[OrderManager] Subscribing to user events...`);
864
+ // Map ApiCredentials (key) to ClobApiKeyCreds (apiKey) format
865
+ const clobAuth = {
866
+ apiKey: credentials.key,
867
+ secret: credentials.secret,
868
+ passphrase: credentials.passphrase,
869
+ };
870
+ this.realtimeService.subscribeUserEvents(clobAuth, {
350
871
  onOrder: this.handleUserOrder.bind(this),
351
872
  onTrade: this.handleUserTrade.bind(this),
352
873
  });
874
+ console.log(`[OrderManager] User events subscription complete`);
353
875
  }
354
876
  }
355
877
  /**
@@ -368,17 +890,17 @@ export class OrderManager extends EventEmitter {
368
890
  // Update order state
369
891
  watched.order.price = userOrder.price;
370
892
  watched.order.originalSize = userOrder.originalSize;
371
- watched.order.filledSize = userOrder.matchedSize;
372
- watched.order.remainingSize = userOrder.originalSize - userOrder.matchedSize;
893
+ watched.order.filledSize = userOrder.sizeMatched;
894
+ watched.order.remainingSize = userOrder.originalSize - userOrder.sizeMatched;
373
895
  // Infer new status
374
896
  let newStatus = watched.lastStatus;
375
897
  if (userOrder.eventType === 'PLACEMENT') {
376
898
  newStatus = OrderStatus.OPEN;
377
899
  }
378
900
  else if (userOrder.eventType === 'UPDATE') {
379
- if (userOrder.matchedSize > 0) {
901
+ if (userOrder.sizeMatched > 0) {
380
902
  newStatus =
381
- userOrder.matchedSize >= userOrder.originalSize
903
+ userOrder.sizeMatched >= userOrder.originalSize
382
904
  ? OrderStatus.FILLED
383
905
  : OrderStatus.PARTIALLY_FILLED;
384
906
  }
@@ -394,31 +916,105 @@ export class OrderManager extends EventEmitter {
394
916
  /**
395
917
  * Handle USER_TRADE WebSocket event (Polymarket-specific)
396
918
  * Triggered when: MATCHED, MINED, CONFIRMED
919
+ *
920
+ * Order lookup priority:
921
+ * 1. takerOrderId - if we placed a market order (FOK/FAK) or taker limit order
922
+ * 2. makerOrders[].orderId - if our limit order was matched as maker
397
923
  */
398
924
  async handleUserTrade(userTrade) {
399
- const watched = this.watchedOrders.get(userTrade.market);
400
- if (!watched)
925
+ // Debug: Log all incoming USER_TRADE events
926
+ console.log(`[OrderManager] WebSocket USER_TRADE received:`, {
927
+ tradeId: userTrade.tradeId,
928
+ takerOrderId: userTrade.takerOrderId,
929
+ makerOrderIds: userTrade.makerOrders?.map(m => m.orderId),
930
+ size: userTrade.size,
931
+ price: userTrade.price,
932
+ status: userTrade.status,
933
+ watchedOrderCount: this.watchedOrders.size,
934
+ });
935
+ // Find the watched order using takerOrderId or makerOrders
936
+ let watched;
937
+ let makerInfo;
938
+ // Priority 1: Check if we're the taker
939
+ if (userTrade.takerOrderId) {
940
+ watched = this.watchedOrders.get(userTrade.takerOrderId);
941
+ }
942
+ // Priority 2: Check if we're a maker
943
+ if (!watched && userTrade.makerOrders) {
944
+ for (const maker of userTrade.makerOrders) {
945
+ watched = this.watchedOrders.get(maker.orderId);
946
+ if (watched) {
947
+ makerInfo = maker;
948
+ break;
949
+ }
950
+ }
951
+ }
952
+ if (!watched) {
953
+ console.log(`[OrderManager] USER_TRADE not for any watched order, ignoring`);
401
954
  return;
955
+ }
956
+ console.log(`[OrderManager] USER_TRADE matched watched order: ${watched.orderId}`);
957
+ // Bug 16 Fix: When we're the maker, use maker-specific matchedAmount and price
958
+ // userTrade.size is the TOTAL trade size (sum of all makers), not our individual fill
959
+ // Bug 20 Fix: Previously used wrong field name (matched_size instead of matched_amount)
960
+ // Now correctly reading matched_amount from Polymarket API
961
+ const rawMatchedAmount = makerInfo?.matchedAmount;
962
+ let fillSize = rawMatchedAmount ?? userTrade.size;
963
+ // Debug: Log raw values for verification
964
+ if (makerInfo) {
965
+ console.log(`[OrderManager] Maker fill debug:`, {
966
+ rawMatchedAmount,
967
+ makerOrdersLength: userTrade.makerOrders?.length,
968
+ willApplyFallback: fillSize === 0 || fillSize === undefined,
969
+ makerOrdersDetail: userTrade.makerOrders?.map(m => ({
970
+ orderId: m.orderId?.slice(0, 12),
971
+ matchedAmount: m.matchedAmount,
972
+ price: m.price,
973
+ })),
974
+ });
975
+ }
976
+ // Fallback: if matchedAmount is 0/undefined and we're a maker, use trade size
977
+ // This shouldn't happen now that we're reading the correct field, but kept as safety
978
+ if (makerInfo && (fillSize === 0 || fillSize === undefined)) {
979
+ console.log(`[OrderManager] Fallback applied: using userTrade.size ${userTrade.size} instead of ${fillSize}`);
980
+ fillSize = userTrade.size;
981
+ }
982
+ const fillPrice = makerInfo?.price ?? userTrade.price;
983
+ console.log(`[OrderManager] USER_TRADE fill details:`, {
984
+ isMaker: !!makerInfo,
985
+ fillSize,
986
+ fillPrice,
987
+ tradeTotalSize: userTrade.size,
988
+ });
402
989
  // Deduplicate events
403
- const eventKey = `trade_${userTrade.tradeId}_${userTrade.status}_${userTrade.timestamp}`;
990
+ // When we're maker, include our orderId in the key to handle multiple makers in same trade
991
+ const eventKey = makerInfo
992
+ ? `trade_${userTrade.tradeId}_${userTrade.status}_${watched.orderId}`
993
+ : `trade_${userTrade.tradeId}_${userTrade.status}_${userTrade.timestamp}`;
404
994
  if (this.processedEvents.has(eventKey))
405
995
  return;
406
996
  this.processedEvents.add(eventKey);
407
997
  // Emit fill event
408
- const isCompleteFill = watched.order.filledSize + userTrade.size >= watched.order.originalSize;
998
+ // Calculate remaining size after this fill
999
+ const remainingAfterFill = watched.order.originalSize - (watched.order.filledSize + fillSize);
1000
+ // Complete fill if: sum >= originalSize OR remaining is zero/negative
1001
+ // Note: For market orders (FOK/FAK), originalSize is in USDC but filledSize is in shares,
1002
+ // causing remainingAfterFill to be negative. This is still a complete fill.
1003
+ const isCompleteFill = watched.order.filledSize + fillSize >= watched.order.originalSize ||
1004
+ remainingAfterFill <= 0;
409
1005
  const fillEvent = {
410
1006
  orderId: watched.orderId,
411
1007
  order: watched.order,
412
1008
  fill: {
413
1009
  tradeId: userTrade.tradeId,
414
- size: userTrade.size,
415
- price: userTrade.price,
1010
+ size: fillSize,
1011
+ price: fillPrice,
416
1012
  fee: 0, // Fee info not in UserTrade
417
1013
  timestamp: userTrade.timestamp,
418
1014
  transactionHash: userTrade.transactionHash,
419
1015
  },
420
- cumulativeFilled: watched.order.filledSize + userTrade.size,
421
- remainingSize: watched.order.originalSize - (watched.order.filledSize + userTrade.size),
1016
+ cumulativeFilled: watched.order.filledSize + fillSize,
1017
+ remainingSize: watched.order.originalSize - (watched.order.filledSize + fillSize),
422
1018
  isCompleteFill,
423
1019
  };
424
1020
  if (isCompleteFill) {
@@ -485,7 +1081,12 @@ export class OrderManager extends EventEmitter {
485
1081
  // Only emit if this is an actual increase from a previously known state
486
1082
  if (newFilledSize > oldFilledSize && oldFilledSize >= 0) {
487
1083
  const fillDelta = newFilledSize - oldFilledSize;
488
- const isCompleteFill = freshOrder.remainingSize === 0;
1084
+ // Check for complete fill using status (preferred) or remainingSize
1085
+ // Note: For market orders (FOK/FAK), remainingSize may be negative due to unit mismatch
1086
+ // (originalSize is in USDC, filledSize is in shares), so we rely on status
1087
+ const isCompleteFill = freshOrder.status === OrderStatus.FILLED ||
1088
+ freshOrder.remainingSize === 0 ||
1089
+ freshOrder.remainingSize <= 0;
489
1090
  // Deduplicate fill events using fill size as key
490
1091
  const eventKey = `fill_${orderId}_${newFilledSize}`;
491
1092
  if (!this.processedEvents.has(eventKey)) {