@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.
- package/dist/src/clients/bridge-client.d.ts +131 -1
- package/dist/src/clients/bridge-client.d.ts.map +1 -1
- package/dist/src/clients/bridge-client.js +143 -0
- package/dist/src/clients/bridge-client.js.map +1 -1
- package/dist/src/core/order-status.d.ts +159 -0
- package/dist/src/core/order-status.d.ts.map +1 -0
- package/dist/src/core/order-status.js +254 -0
- package/dist/src/core/order-status.js.map +1 -0
- package/dist/src/core/types.d.ts +124 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/core/types.js +120 -0
- package/dist/src/core/types.js.map +1 -1
- package/dist/src/index.d.ts +6 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/services/ctf-detector.d.ts +215 -0
- package/dist/src/services/ctf-detector.d.ts.map +1 -0
- package/dist/src/services/ctf-detector.js +420 -0
- package/dist/src/services/ctf-detector.js.map +1 -0
- package/dist/src/services/ctf-manager.d.ts +202 -0
- package/dist/src/services/ctf-manager.d.ts.map +1 -0
- package/dist/src/services/ctf-manager.js +542 -0
- package/dist/src/services/ctf-manager.js.map +1 -0
- package/dist/src/services/order-manager.d.ts +440 -0
- package/dist/src/services/order-manager.d.ts.map +1 -0
- package/dist/src/services/order-manager.js +853 -0
- package/dist/src/services/order-manager.js.map +1 -0
- package/dist/src/services/order-manager.test.d.ts +10 -0
- package/dist/src/services/order-manager.test.d.ts.map +1 -0
- package/dist/src/services/order-manager.test.js +751 -0
- package/dist/src/services/order-manager.test.js.map +1 -0
- package/dist/src/services/trading-service.d.ts +89 -1
- package/dist/src/services/trading-service.d.ts.map +1 -1
- package/dist/src/services/trading-service.js +227 -1
- package/dist/src/services/trading-service.js.map +1 -1
- 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
|