@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,751 @@
1
+ /**
2
+ * OrderManager Unit Tests
3
+ *
4
+ * Focus: Fill event generation in polling mode
5
+ * Issue #7: Polling mode doesn't emit fill events, only status changes
6
+ *
7
+ * Extended: Market order (FOK/FAK) support
8
+ */
9
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
+ import { OrderManager } from './order-manager.js';
11
+ import { OrderStatus } from '../core/types.js';
12
+ import { MockRateLimiter, MockCache, waitFor } from '../__tests__/test-utils.js';
13
+ // ============================================================================
14
+ // Mock Dependencies
15
+ // ============================================================================
16
+ class MockTradingService {
17
+ orders = new Map();
18
+ lastMarketOrderParams = null;
19
+ async initialize() { }
20
+ async getOrder(orderId) {
21
+ return this.orders.get(orderId) || null;
22
+ }
23
+ // Mock createMarketOrder for testing
24
+ async createMarketOrder(params) {
25
+ this.lastMarketOrderParams = params;
26
+ const orderId = `market-order-${Date.now()}`;
27
+ // Simulate order creation
28
+ this.updateOrder(orderId, {
29
+ id: orderId,
30
+ status: OrderStatus.PENDING,
31
+ tokenId: params.tokenId,
32
+ side: params.side,
33
+ price: params.price || 0,
34
+ originalSize: params.amount,
35
+ filledSize: 0,
36
+ remainingSize: params.amount,
37
+ });
38
+ return { success: true, orderId };
39
+ }
40
+ // Test helper: update order state
41
+ updateOrder(orderId, updates) {
42
+ const existing = this.orders.get(orderId);
43
+ if (existing) {
44
+ this.orders.set(orderId, { ...existing, ...updates });
45
+ }
46
+ else {
47
+ this.orders.set(orderId, {
48
+ id: orderId,
49
+ status: OrderStatus.OPEN,
50
+ tokenId: 'token123',
51
+ side: 'BUY',
52
+ price: 0.52,
53
+ originalSize: 100,
54
+ filledSize: 0,
55
+ remainingSize: 100,
56
+ associateTrades: [],
57
+ createdAt: Date.now(),
58
+ ...updates,
59
+ });
60
+ }
61
+ }
62
+ getCredentials() {
63
+ return null;
64
+ }
65
+ }
66
+ // ============================================================================
67
+ // Test Fixtures
68
+ // ============================================================================
69
+ const createTestOrder = (overrides) => ({
70
+ id: 'test-order-1',
71
+ status: OrderStatus.OPEN,
72
+ tokenId: 'token123',
73
+ side: 'BUY',
74
+ price: 0.52,
75
+ originalSize: 100,
76
+ filledSize: 0,
77
+ remainingSize: 100,
78
+ associateTrades: [],
79
+ createdAt: Date.now(),
80
+ ...overrides,
81
+ });
82
+ // ============================================================================
83
+ // Tests
84
+ // ============================================================================
85
+ describe('OrderManager - Polling Fill Detection', () => {
86
+ let orderManager;
87
+ let mockTradingService;
88
+ let rateLimiter;
89
+ let cache;
90
+ beforeEach(async () => {
91
+ rateLimiter = new MockRateLimiter();
92
+ cache = new MockCache();
93
+ mockTradingService = new MockTradingService();
94
+ orderManager = new OrderManager({
95
+ privateKey: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
96
+ rateLimiter: rateLimiter,
97
+ cache: cache,
98
+ mode: 'polling',
99
+ pollingInterval: 100, // Fast polling for tests
100
+ });
101
+ // Inject mock trading service
102
+ orderManager.tradingService = mockTradingService;
103
+ await orderManager.start();
104
+ });
105
+ afterEach(() => {
106
+ orderManager.stop();
107
+ });
108
+ describe('Fill Detection', () => {
109
+ it('should emit order_partially_filled when filledSize increases', async () => {
110
+ const orderId = 'order-1';
111
+ const initialOrder = createTestOrder({
112
+ id: orderId,
113
+ filledSize: 0,
114
+ remainingSize: 100,
115
+ });
116
+ // Set initial state
117
+ mockTradingService.updateOrder(orderId, initialOrder);
118
+ // Start watching
119
+ orderManager.watchOrder(orderId);
120
+ // Setup event listener
121
+ const fillEvents = [];
122
+ orderManager.on('order_partially_filled', (event) => {
123
+ fillEvents.push(event);
124
+ });
125
+ // Wait for initial poll
126
+ await waitFor(150);
127
+ // Simulate partial fill (50 shares filled)
128
+ mockTradingService.updateOrder(orderId, {
129
+ filledSize: 50,
130
+ remainingSize: 50,
131
+ status: OrderStatus.PARTIALLY_FILLED,
132
+ associateTrades: ['trade-1'],
133
+ });
134
+ // Wait for next poll
135
+ await waitFor(150);
136
+ // Verify fill event emitted
137
+ expect(fillEvents).toHaveLength(1);
138
+ expect(fillEvents[0]).toMatchObject({
139
+ orderId,
140
+ fill: {
141
+ size: 50, // Delta from 0 to 50
142
+ price: 0.52, // Order price
143
+ },
144
+ cumulativeFilled: 50,
145
+ remainingSize: 50,
146
+ isCompleteFill: false,
147
+ });
148
+ });
149
+ it('should emit order_filled when order completely filled', async () => {
150
+ const orderId = 'order-2';
151
+ // Set initial state with partial fill
152
+ mockTradingService.updateOrder(orderId, {
153
+ id: orderId,
154
+ filledSize: 50,
155
+ remainingSize: 50,
156
+ status: OrderStatus.PARTIALLY_FILLED,
157
+ associateTrades: ['trade-1'],
158
+ });
159
+ // Start watching (this will poll and set initial state)
160
+ orderManager.watchOrder(orderId);
161
+ // Setup event listener AFTER first poll
162
+ await waitFor(150);
163
+ const fillEvents = [];
164
+ orderManager.on('order_filled', (event) => {
165
+ fillEvents.push(event);
166
+ });
167
+ // Simulate complete fill
168
+ mockTradingService.updateOrder(orderId, {
169
+ filledSize: 100,
170
+ remainingSize: 0,
171
+ status: OrderStatus.FILLED,
172
+ associateTrades: ['trade-1', 'trade-2'],
173
+ });
174
+ // Wait for next poll
175
+ await waitFor(150);
176
+ // Verify fill event emitted
177
+ expect(fillEvents).toHaveLength(1);
178
+ expect(fillEvents[0]).toMatchObject({
179
+ orderId,
180
+ fill: {
181
+ size: 50, // Delta from 50 to 100
182
+ price: 0.52,
183
+ },
184
+ cumulativeFilled: 100,
185
+ remainingSize: 0,
186
+ isCompleteFill: true,
187
+ });
188
+ });
189
+ it('should emit multiple partial fill events correctly', async () => {
190
+ const orderId = 'order-3';
191
+ // Set initial state
192
+ mockTradingService.updateOrder(orderId, {
193
+ id: orderId,
194
+ filledSize: 0,
195
+ remainingSize: 100,
196
+ });
197
+ // Start watching and wait for initial state
198
+ orderManager.watchOrder(orderId);
199
+ await waitFor(150);
200
+ // Setup event listener AFTER initial poll
201
+ const partialFillEvents = [];
202
+ const completeFillEvents = [];
203
+ orderManager.on('order_partially_filled', (event) => {
204
+ partialFillEvents.push(event);
205
+ });
206
+ orderManager.on('order_filled', (event) => {
207
+ completeFillEvents.push(event);
208
+ });
209
+ // First fill: 30 shares
210
+ mockTradingService.updateOrder(orderId, {
211
+ filledSize: 30,
212
+ remainingSize: 70,
213
+ status: OrderStatus.PARTIALLY_FILLED,
214
+ associateTrades: ['trade-1'],
215
+ });
216
+ await waitFor(150);
217
+ // Second fill: 40 more shares (total 70)
218
+ mockTradingService.updateOrder(orderId, {
219
+ filledSize: 70,
220
+ remainingSize: 30,
221
+ status: OrderStatus.PARTIALLY_FILLED,
222
+ associateTrades: ['trade-1', 'trade-2'],
223
+ });
224
+ await waitFor(150);
225
+ // Final fill: 30 more shares (total 100, complete)
226
+ mockTradingService.updateOrder(orderId, {
227
+ filledSize: 100,
228
+ remainingSize: 0,
229
+ status: OrderStatus.FILLED,
230
+ associateTrades: ['trade-1', 'trade-2', 'trade-3'],
231
+ });
232
+ await waitFor(150);
233
+ // Verify partial fills
234
+ expect(partialFillEvents).toHaveLength(2);
235
+ expect(partialFillEvents[0].fill.size).toBe(30);
236
+ expect(partialFillEvents[1].fill.size).toBe(40);
237
+ // Verify complete fill
238
+ expect(completeFillEvents).toHaveLength(1);
239
+ expect(completeFillEvents[0].fill.size).toBe(30);
240
+ expect(completeFillEvents[0].isCompleteFill).toBe(true);
241
+ });
242
+ it('should not emit duplicate fill events', async () => {
243
+ const orderId = 'order-4';
244
+ // Set initial state
245
+ mockTradingService.updateOrder(orderId, {
246
+ id: orderId,
247
+ filledSize: 0,
248
+ remainingSize: 100,
249
+ });
250
+ // Start watching
251
+ orderManager.watchOrder(orderId);
252
+ // Setup event listener
253
+ const fillEvents = [];
254
+ orderManager.on('order_partially_filled', (event) => {
255
+ fillEvents.push(event);
256
+ });
257
+ // Wait for initial poll
258
+ await waitFor(150);
259
+ // Simulate fill
260
+ mockTradingService.updateOrder(orderId, {
261
+ filledSize: 50,
262
+ remainingSize: 50,
263
+ status: OrderStatus.PARTIALLY_FILLED,
264
+ });
265
+ // Wait for multiple polls
266
+ await waitFor(150);
267
+ await waitFor(150);
268
+ await waitFor(150);
269
+ // Should only emit once (deduplication works)
270
+ expect(fillEvents).toHaveLength(1);
271
+ });
272
+ it('should include correct fill details', async () => {
273
+ const orderId = 'order-5';
274
+ // Set initial state
275
+ mockTradingService.updateOrder(orderId, {
276
+ id: orderId,
277
+ price: 0.65,
278
+ originalSize: 200,
279
+ filledSize: 0,
280
+ remainingSize: 200,
281
+ associateTrades: [],
282
+ });
283
+ // Start watching
284
+ orderManager.watchOrder(orderId);
285
+ // Setup event listener
286
+ let capturedEvent = null;
287
+ orderManager.on('order_partially_filled', (event) => {
288
+ capturedEvent = event;
289
+ });
290
+ // Wait for initial poll
291
+ await waitFor(150);
292
+ // Simulate fill
293
+ mockTradingService.updateOrder(orderId, {
294
+ filledSize: 100,
295
+ remainingSize: 100,
296
+ status: OrderStatus.PARTIALLY_FILLED,
297
+ associateTrades: ['trade-abc'],
298
+ });
299
+ // Wait for poll
300
+ await waitFor(150);
301
+ // Verify fill details
302
+ expect(capturedEvent).not.toBeNull();
303
+ expect(capturedEvent).toMatchObject({
304
+ orderId,
305
+ fill: {
306
+ tradeId: 'trade-abc',
307
+ size: 100,
308
+ price: 0.65,
309
+ fee: 0, // Polling doesn't have fee info
310
+ },
311
+ cumulativeFilled: 100,
312
+ remainingSize: 100,
313
+ isCompleteFill: false,
314
+ });
315
+ // Verify order reference
316
+ expect(capturedEvent.order.id).toBe(orderId);
317
+ expect(capturedEvent.order.filledSize).toBe(100);
318
+ });
319
+ });
320
+ describe('Status Change Events', () => {
321
+ it('should emit both fill event and status change event', async () => {
322
+ const orderId = 'order-6';
323
+ // Set initial state
324
+ mockTradingService.updateOrder(orderId, {
325
+ id: orderId,
326
+ filledSize: 0,
327
+ remainingSize: 100,
328
+ status: OrderStatus.OPEN,
329
+ });
330
+ // Start watching and wait for initial state
331
+ orderManager.watchOrder(orderId);
332
+ await waitFor(150);
333
+ // Setup event listeners AFTER initial poll
334
+ const fillEvents = [];
335
+ const statusChanges = [];
336
+ orderManager.on('order_partially_filled', (event) => {
337
+ fillEvents.push(event);
338
+ });
339
+ orderManager.on('status_change', (event) => {
340
+ statusChanges.push(event);
341
+ });
342
+ // Simulate fill with status change
343
+ mockTradingService.updateOrder(orderId, {
344
+ filledSize: 50,
345
+ remainingSize: 50,
346
+ status: OrderStatus.PARTIALLY_FILLED,
347
+ });
348
+ // Wait for poll
349
+ await waitFor(150);
350
+ // Both events should be emitted
351
+ expect(fillEvents).toHaveLength(1);
352
+ expect(statusChanges).toHaveLength(1);
353
+ expect(statusChanges[0].to).toBe(OrderStatus.PARTIALLY_FILLED);
354
+ });
355
+ });
356
+ describe('Edge Cases', () => {
357
+ it('should handle filledSize staying the same (no fill)', async () => {
358
+ const orderId = 'order-7';
359
+ // Set initial state with existing fill
360
+ mockTradingService.updateOrder(orderId, {
361
+ id: orderId,
362
+ filledSize: 50,
363
+ remainingSize: 50,
364
+ status: OrderStatus.PARTIALLY_FILLED,
365
+ });
366
+ // Start watching and wait for initial state
367
+ orderManager.watchOrder(orderId);
368
+ await waitFor(150);
369
+ // Setup event listener AFTER initial poll
370
+ const fillEvents = [];
371
+ orderManager.on('order_partially_filled', (event) => {
372
+ fillEvents.push(event);
373
+ });
374
+ // Wait for multiple polls (no fill change)
375
+ await waitFor(150);
376
+ await waitFor(150);
377
+ // No fill events should be emitted
378
+ expect(fillEvents).toHaveLength(0);
379
+ });
380
+ it('should handle order with no associateTrades', async () => {
381
+ const orderId = 'order-8';
382
+ // Set initial state
383
+ mockTradingService.updateOrder(orderId, {
384
+ id: orderId,
385
+ filledSize: 0,
386
+ remainingSize: 100,
387
+ associateTrades: [],
388
+ });
389
+ // Start watching
390
+ orderManager.watchOrder(orderId);
391
+ // Setup event listener
392
+ let capturedEvent = null;
393
+ orderManager.on('order_partially_filled', (event) => {
394
+ capturedEvent = event;
395
+ });
396
+ // Wait for initial poll
397
+ await waitFor(150);
398
+ // Simulate fill with no trade ID
399
+ mockTradingService.updateOrder(orderId, {
400
+ filledSize: 50,
401
+ remainingSize: 50,
402
+ status: OrderStatus.PARTIALLY_FILLED,
403
+ associateTrades: [], // Empty array
404
+ });
405
+ // Wait for poll
406
+ await waitFor(150);
407
+ // Should generate synthetic tradeId
408
+ expect(capturedEvent).not.toBeNull();
409
+ expect(capturedEvent.fill.tradeId).toMatch(/^polling_\d+$/);
410
+ });
411
+ it('should handle order going directly from OPEN to FILLED', async () => {
412
+ const orderId = 'order-9';
413
+ // Set initial state
414
+ mockTradingService.updateOrder(orderId, {
415
+ id: orderId,
416
+ filledSize: 0,
417
+ remainingSize: 100,
418
+ status: OrderStatus.OPEN,
419
+ });
420
+ // Start watching and wait for initial state
421
+ orderManager.watchOrder(orderId);
422
+ await waitFor(150);
423
+ // Setup event listeners AFTER initial poll
424
+ const partialFillEvents = [];
425
+ const completeFillEvents = [];
426
+ orderManager.on('order_partially_filled', (event) => {
427
+ partialFillEvents.push(event);
428
+ });
429
+ orderManager.on('order_filled', (event) => {
430
+ completeFillEvents.push(event);
431
+ });
432
+ // Simulate instant complete fill (market order scenario)
433
+ mockTradingService.updateOrder(orderId, {
434
+ filledSize: 100,
435
+ remainingSize: 0,
436
+ status: OrderStatus.FILLED,
437
+ associateTrades: ['trade-1'],
438
+ });
439
+ // Wait for poll
440
+ await waitFor(150);
441
+ // Should emit complete fill only
442
+ expect(partialFillEvents).toHaveLength(0);
443
+ expect(completeFillEvents).toHaveLength(1);
444
+ expect(completeFillEvents[0].fill.size).toBe(100);
445
+ });
446
+ });
447
+ });
448
+ describe('OrderManager - WebSocket + Polling Deduplication', () => {
449
+ let orderManager;
450
+ let mockTradingService;
451
+ beforeEach(async () => {
452
+ mockTradingService = new MockTradingService();
453
+ orderManager = new OrderManager({
454
+ privateKey: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
455
+ rateLimiter: new MockRateLimiter(),
456
+ cache: new MockCache(),
457
+ mode: 'hybrid', // Both WebSocket and Polling
458
+ pollingInterval: 100,
459
+ });
460
+ // Inject mock trading service
461
+ orderManager.tradingService = mockTradingService;
462
+ await orderManager.start();
463
+ });
464
+ afterEach(() => {
465
+ orderManager.stop();
466
+ });
467
+ it('should not emit duplicate fills when both WebSocket and Polling detect same fill', async () => {
468
+ const orderId = 'order-10';
469
+ // Set initial state
470
+ mockTradingService.updateOrder(orderId, {
471
+ id: orderId,
472
+ filledSize: 0,
473
+ remainingSize: 100,
474
+ });
475
+ // Start watching
476
+ orderManager.watchOrder(orderId);
477
+ // Setup event listener
478
+ const fillEvents = [];
479
+ orderManager.on('order_partially_filled', (event) => {
480
+ fillEvents.push(event);
481
+ });
482
+ // Wait for initial poll
483
+ await waitFor(150);
484
+ // Update order state
485
+ mockTradingService.updateOrder(orderId, {
486
+ filledSize: 50,
487
+ remainingSize: 50,
488
+ status: OrderStatus.PARTIALLY_FILLED,
489
+ });
490
+ // Wait for multiple polls
491
+ await waitFor(150);
492
+ await waitFor(150);
493
+ // Deduplication should prevent multiple emissions
494
+ expect(fillEvents).toHaveLength(1);
495
+ });
496
+ });
497
+ // ============================================================================
498
+ // Market Order Tests (FOK/FAK)
499
+ // ============================================================================
500
+ describe('OrderManager - Market Orders (FOK/FAK)', () => {
501
+ let orderManager;
502
+ let mockTradingService;
503
+ let rateLimiter;
504
+ let cache;
505
+ beforeEach(async () => {
506
+ rateLimiter = new MockRateLimiter();
507
+ cache = new MockCache();
508
+ mockTradingService = new MockTradingService();
509
+ orderManager = new OrderManager({
510
+ privateKey: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
511
+ rateLimiter: rateLimiter,
512
+ cache: cache,
513
+ mode: 'polling',
514
+ pollingInterval: 100,
515
+ });
516
+ // Inject mock trading service
517
+ orderManager.tradingService = mockTradingService;
518
+ await orderManager.start();
519
+ });
520
+ afterEach(() => {
521
+ orderManager.stop();
522
+ });
523
+ describe('Market Order Creation', () => {
524
+ it('should create FOK market order and auto-watch', async () => {
525
+ const orderCreatedEvents = [];
526
+ orderManager.on('order_created', (order) => {
527
+ orderCreatedEvents.push(order);
528
+ });
529
+ const result = await orderManager.createMarketOrder({
530
+ tokenId: 'token123',
531
+ side: 'BUY',
532
+ amount: 10,
533
+ orderType: 'FOK',
534
+ });
535
+ expect(result.success).toBe(true);
536
+ expect(result.orderId).toBeDefined();
537
+ expect(orderCreatedEvents).toHaveLength(1);
538
+ expect(orderCreatedEvents[0].orderType).toBe('FOK');
539
+ // Should be auto-watched
540
+ const watched = orderManager.getWatchedOrders();
541
+ expect(watched.some(o => o.id === result.orderId)).toBe(true);
542
+ });
543
+ it('should create FAK market order and auto-watch', async () => {
544
+ const result = await orderManager.createMarketOrder({
545
+ tokenId: 'token123',
546
+ side: 'SELL',
547
+ amount: 20,
548
+ orderType: 'FAK',
549
+ });
550
+ expect(result.success).toBe(true);
551
+ expect(mockTradingService.lastMarketOrderParams?.orderType).toBe('FAK');
552
+ });
553
+ it('should reject market order below minimum amount ($1)', async () => {
554
+ const rejectEvents = [];
555
+ orderManager.on('order_rejected', (event) => {
556
+ rejectEvents.push(event);
557
+ });
558
+ const result = await orderManager.createMarketOrder({
559
+ tokenId: 'token123',
560
+ side: 'BUY',
561
+ amount: 0.5, // Below $1 minimum
562
+ });
563
+ expect(result.success).toBe(false);
564
+ expect(result.errorMsg).toContain('at least $1');
565
+ expect(rejectEvents).toHaveLength(1);
566
+ });
567
+ it('should default to FOK when orderType not specified', async () => {
568
+ const orderCreatedEvents = [];
569
+ orderManager.on('order_created', (order) => {
570
+ orderCreatedEvents.push(order);
571
+ });
572
+ await orderManager.createMarketOrder({
573
+ tokenId: 'token123',
574
+ side: 'BUY',
575
+ amount: 10,
576
+ // No orderType specified
577
+ });
578
+ expect(orderCreatedEvents[0].orderType).toBe('FOK');
579
+ });
580
+ });
581
+ describe('FOK Order Lifecycle', () => {
582
+ it('should handle FOK order that fills completely', async () => {
583
+ const fillEvents = [];
584
+ orderManager.on('order_filled', (event) => {
585
+ fillEvents.push(event);
586
+ });
587
+ // Create FOK order
588
+ const result = await orderManager.createMarketOrder({
589
+ tokenId: 'token123',
590
+ side: 'BUY',
591
+ amount: 10,
592
+ orderType: 'FOK',
593
+ });
594
+ // Wait for initial poll
595
+ await waitFor(150);
596
+ // Simulate instant complete fill (FOK success)
597
+ mockTradingService.updateOrder(result.orderId, {
598
+ filledSize: 10,
599
+ remainingSize: 0,
600
+ status: OrderStatus.FILLED,
601
+ associateTrades: ['trade-fok-1'],
602
+ });
603
+ // Wait for poll to detect
604
+ await waitFor(150);
605
+ expect(fillEvents).toHaveLength(1);
606
+ expect(fillEvents[0].isCompleteFill).toBe(true);
607
+ expect(fillEvents[0].cumulativeFilled).toBe(10);
608
+ });
609
+ it('should handle FOK order that fails to fill (cancelled)', async () => {
610
+ const cancelEvents = [];
611
+ orderManager.on('order_cancelled', (event) => {
612
+ cancelEvents.push(event);
613
+ });
614
+ // Create FOK order
615
+ const result = await orderManager.createMarketOrder({
616
+ tokenId: 'token123',
617
+ side: 'BUY',
618
+ amount: 100, // Large order that may fail to fill
619
+ orderType: 'FOK',
620
+ });
621
+ // Wait for initial poll
622
+ await waitFor(150);
623
+ // Simulate FOK failure - order cancelled without any fill
624
+ mockTradingService.updateOrder(result.orderId, {
625
+ filledSize: 0,
626
+ remainingSize: 100,
627
+ status: OrderStatus.CANCELLED,
628
+ });
629
+ // Wait for poll to detect
630
+ await waitFor(150);
631
+ expect(cancelEvents).toHaveLength(1);
632
+ expect(cancelEvents[0].filledSize).toBe(0);
633
+ expect(cancelEvents[0].cancelledSize).toBe(100);
634
+ });
635
+ });
636
+ describe('FAK Order Lifecycle', () => {
637
+ it('should handle FAK order with partial fill', async () => {
638
+ const fillEvents = [];
639
+ const cancelEvents = [];
640
+ orderManager.on('order_partially_filled', (event) => {
641
+ fillEvents.push(event);
642
+ });
643
+ orderManager.on('order_cancelled', (event) => {
644
+ cancelEvents.push(event);
645
+ });
646
+ // Create FAK order
647
+ const result = await orderManager.createMarketOrder({
648
+ tokenId: 'token123',
649
+ side: 'BUY',
650
+ amount: 100,
651
+ orderType: 'FAK',
652
+ });
653
+ // Wait for initial poll
654
+ await waitFor(150);
655
+ // Simulate FAK partial fill - fills 60, cancels 40
656
+ // First the partial fill is detected
657
+ mockTradingService.updateOrder(result.orderId, {
658
+ filledSize: 60,
659
+ remainingSize: 40,
660
+ status: OrderStatus.PARTIALLY_FILLED,
661
+ associateTrades: ['trade-fak-1'],
662
+ });
663
+ await waitFor(150);
664
+ // Then the cancellation of remaining
665
+ mockTradingService.updateOrder(result.orderId, {
666
+ filledSize: 60,
667
+ remainingSize: 40,
668
+ status: OrderStatus.CANCELLED,
669
+ });
670
+ await waitFor(150);
671
+ expect(fillEvents).toHaveLength(1);
672
+ expect(fillEvents[0].cumulativeFilled).toBe(60);
673
+ expect(cancelEvents).toHaveLength(1);
674
+ expect(cancelEvents[0].filledSize).toBe(60);
675
+ expect(cancelEvents[0].cancelledSize).toBe(40);
676
+ });
677
+ it('should handle FAK order that fills completely', async () => {
678
+ const fillEvents = [];
679
+ orderManager.on('order_filled', (event) => {
680
+ fillEvents.push(event);
681
+ });
682
+ // Create FAK order
683
+ const result = await orderManager.createMarketOrder({
684
+ tokenId: 'token123',
685
+ side: 'BUY',
686
+ amount: 10,
687
+ orderType: 'FAK',
688
+ });
689
+ // Wait for initial poll
690
+ await waitFor(150);
691
+ // Simulate FAK complete fill (all liquidity available)
692
+ mockTradingService.updateOrder(result.orderId, {
693
+ filledSize: 10,
694
+ remainingSize: 0,
695
+ status: OrderStatus.FILLED,
696
+ associateTrades: ['trade-fak-complete'],
697
+ });
698
+ await waitFor(150);
699
+ expect(fillEvents).toHaveLength(1);
700
+ expect(fillEvents[0].isCompleteFill).toBe(true);
701
+ });
702
+ });
703
+ describe('Status Transitions', () => {
704
+ it('should allow PENDING → FILLED transition (instant FOK fill)', async () => {
705
+ const statusChanges = [];
706
+ orderManager.on('status_change', (event) => {
707
+ statusChanges.push(event);
708
+ });
709
+ const result = await orderManager.createMarketOrder({
710
+ tokenId: 'token123',
711
+ side: 'BUY',
712
+ amount: 10,
713
+ orderType: 'FOK',
714
+ });
715
+ await waitFor(150);
716
+ // Instant fill - PENDING → FILLED directly
717
+ mockTradingService.updateOrder(result.orderId, {
718
+ filledSize: 10,
719
+ remainingSize: 0,
720
+ status: OrderStatus.FILLED,
721
+ });
722
+ await waitFor(150);
723
+ // Should have valid status change
724
+ const filledChange = statusChanges.find(c => c.to === OrderStatus.FILLED);
725
+ expect(filledChange).toBeDefined();
726
+ });
727
+ it('should allow PENDING → CANCELLED transition (FOK failure)', async () => {
728
+ const statusChanges = [];
729
+ orderManager.on('status_change', (event) => {
730
+ statusChanges.push(event);
731
+ });
732
+ const result = await orderManager.createMarketOrder({
733
+ tokenId: 'token123',
734
+ side: 'BUY',
735
+ amount: 1000,
736
+ orderType: 'FOK',
737
+ });
738
+ await waitFor(150);
739
+ // FOK fails - PENDING → CANCELLED directly
740
+ mockTradingService.updateOrder(result.orderId, {
741
+ filledSize: 0,
742
+ remainingSize: 1000,
743
+ status: OrderStatus.CANCELLED,
744
+ });
745
+ await waitFor(150);
746
+ const cancelledChange = statusChanges.find(c => c.to === OrderStatus.CANCELLED);
747
+ expect(cancelledChange).toBeDefined();
748
+ });
749
+ });
750
+ });
751
+ //# sourceMappingURL=order-manager.test.js.map