@imbingox/acex 0.3.1-beta.0 → 0.4.0-beta.10
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/README.md +11 -10
- package/docs/api.md +502 -1030
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +19 -1
- package/src/adapters/binance/market-catalog.ts +93 -22
- package/src/adapters/binance/private-adapter.ts +302 -59
- package/src/adapters/binance/rate-limit.ts +47 -0
- package/src/adapters/binance/server-time.ts +106 -0
- package/src/adapters/juplend/private-adapter.ts +97 -68
- package/src/adapters/types.ts +25 -1
- package/src/client/context.ts +26 -9
- package/src/client/private-subscription-coordinator.ts +898 -63
- package/src/client/runtime.ts +49 -11
- package/src/client/venue-capabilities.ts +1 -0
- package/src/errors.ts +156 -2
- package/src/index.ts +8 -1
- package/src/internal/decimal.ts +19 -0
- package/src/internal/http-client.ts +608 -0
- package/src/internal/rate-limiter.ts +181 -0
- package/src/internal/watermark.ts +83 -0
- package/src/managers/account-manager.ts +267 -55
- package/src/managers/market-manager.ts +261 -60
- package/src/managers/order-manager.ts +798 -84
- package/src/types/account.ts +27 -28
- package/src/types/client.ts +1 -0
- package/src/types/market.ts +37 -12
- package/src/types/order.ts +7 -7
- package/src/types/shared.ts +66 -0
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
FetchOrderRequest,
|
|
2
3
|
PrivateUserDataAdapter,
|
|
4
|
+
RawOpenOrdersSnapshot,
|
|
3
5
|
StreamHandle,
|
|
4
6
|
} from "../adapters/types.ts";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
+
import {
|
|
8
|
+
AcexError,
|
|
9
|
+
buildAcexErrorDetails,
|
|
10
|
+
formatAcexErrorMessage,
|
|
11
|
+
} from "../errors.ts";
|
|
12
|
+
import { isTransportError } from "../internal/http-client.ts";
|
|
13
|
+
import type {
|
|
14
|
+
AccountRuntimeOptions,
|
|
15
|
+
OrderSnapshot,
|
|
16
|
+
PrivateRuntimeReason,
|
|
17
|
+
Venue,
|
|
18
|
+
} from "../types/index.ts";
|
|
7
19
|
import type {
|
|
8
20
|
ClientContext,
|
|
9
21
|
PrivateAccountDataConsumer,
|
|
@@ -22,6 +34,11 @@ interface PrivateSubscriptionRecord {
|
|
|
22
34
|
accountRefreshTimer?: ReturnType<typeof setTimeout>;
|
|
23
35
|
accountRefreshInFlight?: Promise<void>;
|
|
24
36
|
accountRefreshGeneration: number;
|
|
37
|
+
accountSubscriptionGeneration: number;
|
|
38
|
+
orderSubscriptionGeneration: number;
|
|
39
|
+
privateReconcileTimer?: ReturnType<typeof setTimeout>;
|
|
40
|
+
privateReconcileInFlight?: Promise<void>;
|
|
41
|
+
privateReconcileGeneration: number;
|
|
25
42
|
startPromise?: Promise<void>;
|
|
26
43
|
reconcilePromise?: Promise<void>;
|
|
27
44
|
}
|
|
@@ -31,6 +48,9 @@ const DEFAULT_STREAM_RECONNECT_DELAY_MS = 1_000;
|
|
|
31
48
|
const DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS = 10_000;
|
|
32
49
|
const DEFAULT_LISTEN_KEY_KEEPALIVE_MS = 30 * 60 * 1_000;
|
|
33
50
|
const DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS = 5_000;
|
|
51
|
+
const DEFAULT_BINANCE_PRIVATE_RECONCILE_INTERVAL_MS = 60_000;
|
|
52
|
+
const MAX_ORDER_TERMINAL_BACKFILLS_PER_RECONCILE = 20;
|
|
53
|
+
const MAX_ORDER_TERMINAL_BACKFILL_CONCURRENCY = 4;
|
|
34
54
|
|
|
35
55
|
function normalizePositiveInterval(
|
|
36
56
|
value: number | undefined,
|
|
@@ -41,6 +61,26 @@ function normalizePositiveInterval(
|
|
|
41
61
|
: fallback;
|
|
42
62
|
}
|
|
43
63
|
|
|
64
|
+
function normalizeReconcileInterval(
|
|
65
|
+
value: number | undefined,
|
|
66
|
+
fallback: number,
|
|
67
|
+
): number | undefined {
|
|
68
|
+
if (value === 0) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return normalizePositiveInterval(value, fallback);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function transportReason(
|
|
76
|
+
error: unknown,
|
|
77
|
+
fallback: PrivateRuntimeReason,
|
|
78
|
+
): PrivateRuntimeReason {
|
|
79
|
+
return isTransportError(error) && error.kind === "rate_limited"
|
|
80
|
+
? "rate_limited"
|
|
81
|
+
: fallback;
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
export class PrivateSubscriptionCoordinator {
|
|
45
85
|
private readonly context: ClientContext;
|
|
46
86
|
private readonly adapters: Map<Venue, PrivateUserDataAdapter>;
|
|
@@ -51,7 +91,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
51
91
|
private readonly streamReconnectMaxDelayMs: number;
|
|
52
92
|
private readonly listenKeyKeepAliveMs: number;
|
|
53
93
|
private readonly binanceRiskPollIntervalMs: number;
|
|
54
|
-
private readonly
|
|
94
|
+
private readonly binancePrivateReconcileIntervalMs: number | undefined;
|
|
55
95
|
private readonly records = new Map<string, PrivateSubscriptionRecord>();
|
|
56
96
|
|
|
57
97
|
constructor(
|
|
@@ -80,7 +120,10 @@ export class PrivateSubscriptionCoordinator {
|
|
|
80
120
|
options.binance?.riskPollIntervalMs,
|
|
81
121
|
DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS,
|
|
82
122
|
);
|
|
83
|
-
this.
|
|
123
|
+
this.binancePrivateReconcileIntervalMs = normalizeReconcileInterval(
|
|
124
|
+
options.binance?.privateReconcileIntervalMs,
|
|
125
|
+
DEFAULT_BINANCE_PRIVATE_RECONCILE_INTERVAL_MS,
|
|
126
|
+
);
|
|
84
127
|
}
|
|
85
128
|
|
|
86
129
|
async subscribeAccountFeed(accountId: string): Promise<void> {
|
|
@@ -88,18 +131,69 @@ export class PrivateSubscriptionCoordinator {
|
|
|
88
131
|
const record = this.getOrCreateRecord(account);
|
|
89
132
|
const needsPending = !record.stream && !record.startPromise;
|
|
90
133
|
record.accountSubscribed = true;
|
|
134
|
+
const generation = record.privateReconcileGeneration;
|
|
135
|
+
const accountGeneration = record.accountSubscriptionGeneration;
|
|
91
136
|
if (needsPending) {
|
|
92
137
|
this.accountConsumer.onPrivateAccountPending(accountId, record.venue);
|
|
93
138
|
}
|
|
94
139
|
|
|
95
140
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
141
|
+
const adapter = this.getAdapter(record.venue);
|
|
142
|
+
if (adapter.accountCapabilities.updates === "polling") {
|
|
143
|
+
await this.bootstrapAccount(
|
|
144
|
+
record,
|
|
145
|
+
account,
|
|
146
|
+
generation,
|
|
147
|
+
accountGeneration,
|
|
148
|
+
);
|
|
149
|
+
if (
|
|
150
|
+
!this.shouldContinueAccountBootstrap(
|
|
151
|
+
record,
|
|
152
|
+
generation,
|
|
153
|
+
accountGeneration,
|
|
154
|
+
)
|
|
155
|
+
) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
98
158
|
await this.ensureStream(record, account);
|
|
159
|
+
if (
|
|
160
|
+
!this.shouldContinueAccountBootstrap(
|
|
161
|
+
record,
|
|
162
|
+
generation,
|
|
163
|
+
accountGeneration,
|
|
164
|
+
)
|
|
165
|
+
) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.ensurePrivateReconcilePolling(record);
|
|
99
169
|
} else {
|
|
100
170
|
await this.ensureStream(record, account);
|
|
101
|
-
|
|
171
|
+
if (
|
|
172
|
+
!this.shouldContinueAccountBootstrap(
|
|
173
|
+
record,
|
|
174
|
+
generation,
|
|
175
|
+
accountGeneration,
|
|
176
|
+
)
|
|
177
|
+
) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await this.bootstrapAccount(
|
|
181
|
+
record,
|
|
182
|
+
account,
|
|
183
|
+
generation,
|
|
184
|
+
accountGeneration,
|
|
185
|
+
);
|
|
186
|
+
if (
|
|
187
|
+
!this.shouldContinueAccountBootstrap(
|
|
188
|
+
record,
|
|
189
|
+
generation,
|
|
190
|
+
accountGeneration,
|
|
191
|
+
)
|
|
192
|
+
) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
102
195
|
this.ensureAccountRefreshPolling(record);
|
|
196
|
+
this.ensurePrivateReconcilePolling(record);
|
|
103
197
|
}
|
|
104
198
|
} catch (error) {
|
|
105
199
|
record.accountSubscribed = false;
|
|
@@ -115,7 +209,9 @@ export class PrivateSubscriptionCoordinator {
|
|
|
115
209
|
}
|
|
116
210
|
|
|
117
211
|
record.accountSubscribed = false;
|
|
212
|
+
record.accountSubscriptionGeneration += 1;
|
|
118
213
|
this.stopAccountRefreshPolling(record);
|
|
214
|
+
this.restartPrivateReconcilePolling(record);
|
|
119
215
|
this.closeIfUnused(record);
|
|
120
216
|
}
|
|
121
217
|
|
|
@@ -124,13 +220,26 @@ export class PrivateSubscriptionCoordinator {
|
|
|
124
220
|
const record = this.getOrCreateRecord(account);
|
|
125
221
|
const needsPending = !record.stream && !record.startPromise;
|
|
126
222
|
record.ordersSubscribed = true;
|
|
223
|
+
const generation = record.privateReconcileGeneration;
|
|
224
|
+
const orderGeneration = record.orderSubscriptionGeneration;
|
|
127
225
|
if (needsPending) {
|
|
128
226
|
this.orderConsumer.onPrivateOrderPending(accountId, record.venue);
|
|
129
227
|
}
|
|
130
228
|
|
|
131
229
|
try {
|
|
132
230
|
await this.ensureStream(record, account);
|
|
133
|
-
|
|
231
|
+
if (
|
|
232
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
233
|
+
) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
await this.bootstrapOrders(record, account, generation, orderGeneration);
|
|
237
|
+
if (
|
|
238
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
239
|
+
) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.ensurePrivateReconcilePolling(record);
|
|
134
243
|
} catch (error) {
|
|
135
244
|
record.ordersSubscribed = false;
|
|
136
245
|
this.closeIfUnused(record);
|
|
@@ -145,6 +254,8 @@ export class PrivateSubscriptionCoordinator {
|
|
|
145
254
|
}
|
|
146
255
|
|
|
147
256
|
record.ordersSubscribed = false;
|
|
257
|
+
record.orderSubscriptionGeneration += 1;
|
|
258
|
+
this.restartPrivateReconcilePolling(record);
|
|
148
259
|
this.closeIfUnused(record);
|
|
149
260
|
}
|
|
150
261
|
|
|
@@ -174,6 +285,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
174
285
|
onClientStopping(): void {
|
|
175
286
|
for (const record of this.records.values()) {
|
|
176
287
|
this.stopAccountRefreshPolling(record);
|
|
288
|
+
this.stopPrivateReconcilePolling(record);
|
|
177
289
|
this.closeStream(record);
|
|
178
290
|
}
|
|
179
291
|
}
|
|
@@ -186,6 +298,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
186
298
|
|
|
187
299
|
this.closeStream(record);
|
|
188
300
|
this.stopAccountRefreshPolling(record);
|
|
301
|
+
this.stopPrivateReconcilePolling(record);
|
|
189
302
|
this.records.delete(accountId);
|
|
190
303
|
}
|
|
191
304
|
|
|
@@ -209,20 +322,82 @@ export class PrivateSubscriptionCoordinator {
|
|
|
209
322
|
const account = this.getAccount(record.accountId);
|
|
210
323
|
this.closeStream(record);
|
|
211
324
|
this.stopAccountRefreshPolling(record);
|
|
325
|
+
this.stopPrivateReconcilePolling(record);
|
|
326
|
+
const generation = record.privateReconcileGeneration;
|
|
327
|
+
const accountGeneration = record.accountSubscriptionGeneration;
|
|
328
|
+
const orderGeneration = record.orderSubscriptionGeneration;
|
|
212
329
|
|
|
213
330
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
331
|
+
const adapter = this.getAdapter(record.venue);
|
|
332
|
+
if (
|
|
333
|
+
adapter.accountCapabilities.updates === "polling" &&
|
|
334
|
+
record.accountSubscribed
|
|
335
|
+
) {
|
|
336
|
+
await this.bootstrapAccount(
|
|
337
|
+
record,
|
|
338
|
+
account,
|
|
339
|
+
generation,
|
|
340
|
+
accountGeneration,
|
|
341
|
+
);
|
|
342
|
+
if (
|
|
343
|
+
this.shouldContinueAccountBootstrap(
|
|
344
|
+
record,
|
|
345
|
+
generation,
|
|
346
|
+
accountGeneration,
|
|
347
|
+
)
|
|
348
|
+
) {
|
|
349
|
+
await this.ensureStream(record, account);
|
|
350
|
+
if (
|
|
351
|
+
this.shouldContinueAccountBootstrap(
|
|
352
|
+
record,
|
|
353
|
+
generation,
|
|
354
|
+
accountGeneration,
|
|
355
|
+
)
|
|
356
|
+
) {
|
|
357
|
+
this.ensurePrivateReconcilePolling(record);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
217
360
|
} else {
|
|
218
361
|
await this.ensureStream(record, account);
|
|
219
|
-
if (
|
|
220
|
-
|
|
362
|
+
if (
|
|
363
|
+
this.shouldContinueAccountBootstrap(
|
|
364
|
+
record,
|
|
365
|
+
generation,
|
|
366
|
+
accountGeneration,
|
|
367
|
+
)
|
|
368
|
+
) {
|
|
369
|
+
await this.bootstrapAccount(
|
|
370
|
+
record,
|
|
371
|
+
account,
|
|
372
|
+
generation,
|
|
373
|
+
accountGeneration,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (
|
|
377
|
+
this.shouldContinueAccountBootstrap(
|
|
378
|
+
record,
|
|
379
|
+
generation,
|
|
380
|
+
accountGeneration,
|
|
381
|
+
)
|
|
382
|
+
) {
|
|
221
383
|
this.ensureAccountRefreshPolling(record);
|
|
384
|
+
this.ensurePrivateReconcilePolling(record);
|
|
222
385
|
}
|
|
223
386
|
}
|
|
224
|
-
if (
|
|
225
|
-
|
|
387
|
+
if (
|
|
388
|
+
this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
389
|
+
) {
|
|
390
|
+
await this.bootstrapOrders(
|
|
391
|
+
record,
|
|
392
|
+
account,
|
|
393
|
+
generation,
|
|
394
|
+
orderGeneration,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (
|
|
398
|
+
this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
399
|
+
) {
|
|
400
|
+
this.ensurePrivateReconcilePolling(record);
|
|
226
401
|
}
|
|
227
402
|
} catch {
|
|
228
403
|
// Errors are already published to the runtime error bus.
|
|
@@ -235,6 +410,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
235
410
|
throw new AcexError(
|
|
236
411
|
"VENUE_NOT_SUPPORTED",
|
|
237
412
|
`Venue is not supported yet: ${account.venue}`,
|
|
413
|
+
{ details: buildAcexErrorDetails({ venue: account.venue }) },
|
|
238
414
|
);
|
|
239
415
|
}
|
|
240
416
|
|
|
@@ -247,6 +423,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
247
423
|
throw new AcexError(
|
|
248
424
|
"VENUE_NOT_SUPPORTED",
|
|
249
425
|
`Venue is not supported yet: ${venue}`,
|
|
426
|
+
{ details: buildAcexErrorDetails({ venue }) },
|
|
250
427
|
);
|
|
251
428
|
}
|
|
252
429
|
|
|
@@ -269,6 +446,9 @@ export class PrivateSubscriptionCoordinator {
|
|
|
269
446
|
accountReady: false,
|
|
270
447
|
orderReady: false,
|
|
271
448
|
accountRefreshGeneration: 0,
|
|
449
|
+
accountSubscriptionGeneration: 0,
|
|
450
|
+
orderSubscriptionGeneration: 0,
|
|
451
|
+
privateReconcileGeneration: 0,
|
|
272
452
|
};
|
|
273
453
|
|
|
274
454
|
this.records.set(account.accountId, record);
|
|
@@ -279,12 +459,37 @@ export class PrivateSubscriptionCoordinator {
|
|
|
279
459
|
return record.accountSubscribed || record.ordersSubscribed;
|
|
280
460
|
}
|
|
281
461
|
|
|
462
|
+
private shouldContinueAccountBootstrap(
|
|
463
|
+
record: PrivateSubscriptionRecord,
|
|
464
|
+
generation: number,
|
|
465
|
+
accountGeneration: number,
|
|
466
|
+
): boolean {
|
|
467
|
+
return (
|
|
468
|
+
record.accountSubscribed &&
|
|
469
|
+
generation === record.privateReconcileGeneration &&
|
|
470
|
+
accountGeneration === record.accountSubscriptionGeneration
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private shouldContinueOrderBootstrap(
|
|
475
|
+
record: PrivateSubscriptionRecord,
|
|
476
|
+
generation: number,
|
|
477
|
+
orderGeneration: number,
|
|
478
|
+
): boolean {
|
|
479
|
+
return (
|
|
480
|
+
record.ordersSubscribed &&
|
|
481
|
+
generation === record.privateReconcileGeneration &&
|
|
482
|
+
orderGeneration === record.orderSubscriptionGeneration
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
282
486
|
private closeIfUnused(record: PrivateSubscriptionRecord): void {
|
|
283
487
|
if (this.isActive(record)) {
|
|
284
488
|
return;
|
|
285
489
|
}
|
|
286
490
|
|
|
287
491
|
this.stopAccountRefreshPolling(record);
|
|
492
|
+
this.stopPrivateReconcilePolling(record);
|
|
288
493
|
this.closeStream(record);
|
|
289
494
|
this.records.delete(record.accountId);
|
|
290
495
|
}
|
|
@@ -296,7 +501,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
296
501
|
|
|
297
502
|
private ensureAccountRefreshPolling(record: PrivateSubscriptionRecord): void {
|
|
298
503
|
if (
|
|
299
|
-
record.venue !== "
|
|
504
|
+
typeof this.getAdapter(record.venue).refreshAccount !== "function" ||
|
|
300
505
|
!record.accountSubscribed ||
|
|
301
506
|
record.accountRefreshTimer ||
|
|
302
507
|
record.accountRefreshInFlight
|
|
@@ -317,7 +522,10 @@ export class PrivateSubscriptionCoordinator {
|
|
|
317
522
|
}
|
|
318
523
|
|
|
319
524
|
private scheduleAccountRefreshPoll(record: PrivateSubscriptionRecord): void {
|
|
320
|
-
if (
|
|
525
|
+
if (
|
|
526
|
+
typeof this.getAdapter(record.venue).refreshAccount !== "function" ||
|
|
527
|
+
!record.accountSubscribed
|
|
528
|
+
) {
|
|
321
529
|
return;
|
|
322
530
|
}
|
|
323
531
|
|
|
@@ -326,7 +534,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
326
534
|
record.accountRefreshTimer = undefined;
|
|
327
535
|
if (
|
|
328
536
|
generation !== record.accountRefreshGeneration ||
|
|
329
|
-
record.venue !== "
|
|
537
|
+
typeof this.getAdapter(record.venue).refreshAccount !== "function" ||
|
|
330
538
|
!record.accountSubscribed
|
|
331
539
|
) {
|
|
332
540
|
return;
|
|
@@ -352,13 +560,139 @@ export class PrivateSubscriptionCoordinator {
|
|
|
352
560
|
}
|
|
353
561
|
|
|
354
562
|
record.accountRefreshInFlight = undefined;
|
|
355
|
-
if (
|
|
563
|
+
if (
|
|
564
|
+
record.accountSubscribed &&
|
|
565
|
+
typeof this.getAdapter(record.venue).refreshAccount === "function"
|
|
566
|
+
) {
|
|
356
567
|
this.scheduleAccountRefreshPoll(record);
|
|
357
568
|
}
|
|
358
569
|
});
|
|
359
570
|
}, this.binanceRiskPollIntervalMs);
|
|
360
571
|
}
|
|
361
572
|
|
|
573
|
+
private hasPrivateReconcileCapability(
|
|
574
|
+
record: PrivateSubscriptionRecord,
|
|
575
|
+
): boolean {
|
|
576
|
+
const adapter = this.getAdapter(record.venue);
|
|
577
|
+
return (
|
|
578
|
+
(record.accountSubscribed &&
|
|
579
|
+
(typeof adapter.reconcileAccount === "function" ||
|
|
580
|
+
typeof adapter.bootstrapAccount === "function")) ||
|
|
581
|
+
(record.ordersSubscribed && typeof adapter.fetchOpenOrders === "function")
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private ensurePrivateReconcilePolling(
|
|
586
|
+
record: PrivateSubscriptionRecord,
|
|
587
|
+
): void {
|
|
588
|
+
if (
|
|
589
|
+
this.binancePrivateReconcileIntervalMs === undefined ||
|
|
590
|
+
!this.isActive(record) ||
|
|
591
|
+
!this.hasPrivateReconcileCapability(record) ||
|
|
592
|
+
record.privateReconcileTimer ||
|
|
593
|
+
record.privateReconcileInFlight
|
|
594
|
+
) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
this.schedulePrivateReconcilePoll(record);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private restartPrivateReconcilePolling(
|
|
602
|
+
record: PrivateSubscriptionRecord,
|
|
603
|
+
): void {
|
|
604
|
+
if (record.privateReconcileTimer) {
|
|
605
|
+
clearTimeout(record.privateReconcileTimer);
|
|
606
|
+
record.privateReconcileTimer = undefined;
|
|
607
|
+
}
|
|
608
|
+
this.ensurePrivateReconcilePolling(record);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private stopPrivateReconcilePolling(record: PrivateSubscriptionRecord): void {
|
|
612
|
+
record.privateReconcileGeneration += 1;
|
|
613
|
+
if (record.privateReconcileTimer) {
|
|
614
|
+
clearTimeout(record.privateReconcileTimer);
|
|
615
|
+
record.privateReconcileTimer = undefined;
|
|
616
|
+
}
|
|
617
|
+
record.privateReconcileInFlight = undefined;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private schedulePrivateReconcilePoll(
|
|
621
|
+
record: PrivateSubscriptionRecord,
|
|
622
|
+
): void {
|
|
623
|
+
if (
|
|
624
|
+
this.binancePrivateReconcileIntervalMs === undefined ||
|
|
625
|
+
!this.isActive(record) ||
|
|
626
|
+
!this.hasPrivateReconcileCapability(record)
|
|
627
|
+
) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const generation = record.privateReconcileGeneration;
|
|
632
|
+
record.privateReconcileTimer = setTimeout(() => {
|
|
633
|
+
record.privateReconcileTimer = undefined;
|
|
634
|
+
if (
|
|
635
|
+
generation !== record.privateReconcileGeneration ||
|
|
636
|
+
this.binancePrivateReconcileIntervalMs === undefined ||
|
|
637
|
+
!this.isActive(record) ||
|
|
638
|
+
!this.hasPrivateReconcileCapability(record)
|
|
639
|
+
) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
let latestAccount: RegisteredAccountRecord;
|
|
644
|
+
try {
|
|
645
|
+
latestAccount = this.getAccount(record.accountId);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
this.handlePrivateReconcileLookupError(record, error);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
record.privateReconcileInFlight = this.reconcilePrivateData(
|
|
652
|
+
record,
|
|
653
|
+
latestAccount,
|
|
654
|
+
generation,
|
|
655
|
+
true,
|
|
656
|
+
)
|
|
657
|
+
.catch(() => {})
|
|
658
|
+
.finally(() => {
|
|
659
|
+
if (generation !== record.privateReconcileGeneration) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
record.privateReconcileInFlight = undefined;
|
|
664
|
+
if (
|
|
665
|
+
this.binancePrivateReconcileIntervalMs !== undefined &&
|
|
666
|
+
this.isActive(record) &&
|
|
667
|
+
this.hasPrivateReconcileCapability(record)
|
|
668
|
+
) {
|
|
669
|
+
this.schedulePrivateReconcilePoll(record);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}, this.binancePrivateReconcileIntervalMs);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private handlePrivateReconcileLookupError(
|
|
676
|
+
record: PrivateSubscriptionRecord,
|
|
677
|
+
error: unknown,
|
|
678
|
+
): void {
|
|
679
|
+
this.stopPrivateReconcilePolling(record);
|
|
680
|
+
if (error instanceof AcexError && error.code === "ACCOUNT_NOT_FOUND") {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
this.context.publishRuntimeError(
|
|
685
|
+
"adapter",
|
|
686
|
+
error instanceof Error
|
|
687
|
+
? error
|
|
688
|
+
: new Error(`Failed to load ${record.venue} account for reconcile`),
|
|
689
|
+
{
|
|
690
|
+
accountId: record.accountId,
|
|
691
|
+
venue: record.venue,
|
|
692
|
+
},
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
362
696
|
private handleAccountRefreshLookupError(
|
|
363
697
|
record: PrivateSubscriptionRecord,
|
|
364
698
|
error: unknown,
|
|
@@ -390,6 +724,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
390
724
|
return;
|
|
391
725
|
}
|
|
392
726
|
|
|
727
|
+
const requestStartedAt = this.context.now();
|
|
393
728
|
try {
|
|
394
729
|
const update = await adapter.refreshAccount(account.credentials ?? {}, {
|
|
395
730
|
...account.options,
|
|
@@ -407,7 +742,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
407
742
|
record.accountId,
|
|
408
743
|
record.venue,
|
|
409
744
|
update,
|
|
410
|
-
{ preserveStatus: true },
|
|
745
|
+
{ preserveStatus: true, requestStartedAt },
|
|
411
746
|
);
|
|
412
747
|
} catch (error) {
|
|
413
748
|
if (
|
|
@@ -435,12 +770,337 @@ export class PrivateSubscriptionCoordinator {
|
|
|
435
770
|
{
|
|
436
771
|
runtimeStatus: "degraded",
|
|
437
772
|
ready: record.accountReady,
|
|
438
|
-
reason: "http_failed",
|
|
773
|
+
reason: transportReason(error, "http_failed"),
|
|
774
|
+
},
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
private async reconcilePrivateData(
|
|
780
|
+
record: PrivateSubscriptionRecord,
|
|
781
|
+
account: RegisteredAccountRecord,
|
|
782
|
+
generation: number,
|
|
783
|
+
preserveStatus: boolean,
|
|
784
|
+
): Promise<void> {
|
|
785
|
+
const accountGeneration = record.accountSubscriptionGeneration;
|
|
786
|
+
const orderGeneration = record.orderSubscriptionGeneration;
|
|
787
|
+
|
|
788
|
+
await Promise.all([
|
|
789
|
+
this.reconcileAccount(
|
|
790
|
+
record,
|
|
791
|
+
account,
|
|
792
|
+
generation,
|
|
793
|
+
accountGeneration,
|
|
794
|
+
preserveStatus,
|
|
795
|
+
),
|
|
796
|
+
this.reconcileOrders(
|
|
797
|
+
record,
|
|
798
|
+
account,
|
|
799
|
+
generation,
|
|
800
|
+
orderGeneration,
|
|
801
|
+
preserveStatus,
|
|
802
|
+
),
|
|
803
|
+
]);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private async reconcileAccount(
|
|
807
|
+
record: PrivateSubscriptionRecord,
|
|
808
|
+
account: RegisteredAccountRecord,
|
|
809
|
+
generation: number,
|
|
810
|
+
accountGeneration: number,
|
|
811
|
+
preserveStatus: boolean,
|
|
812
|
+
): Promise<void> {
|
|
813
|
+
const adapter = this.getAdapter(record.venue);
|
|
814
|
+
if (
|
|
815
|
+
!this.shouldContinueAccountBootstrap(
|
|
816
|
+
record,
|
|
817
|
+
generation,
|
|
818
|
+
accountGeneration,
|
|
819
|
+
)
|
|
820
|
+
) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const requestStartedAt = this.context.now();
|
|
825
|
+
try {
|
|
826
|
+
const snapshot = adapter.reconcileAccount
|
|
827
|
+
? await adapter.reconcileAccount(account.credentials ?? {}, {
|
|
828
|
+
...account.options,
|
|
829
|
+
accountId: account.accountId,
|
|
830
|
+
})
|
|
831
|
+
: await adapter.bootstrapAccount(account.credentials ?? {}, {
|
|
832
|
+
...account.options,
|
|
833
|
+
accountId: account.accountId,
|
|
834
|
+
});
|
|
835
|
+
if (
|
|
836
|
+
!this.shouldContinueAccountBootstrap(
|
|
837
|
+
record,
|
|
838
|
+
generation,
|
|
839
|
+
accountGeneration,
|
|
840
|
+
)
|
|
841
|
+
) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
record.accountReady = true;
|
|
846
|
+
this.accountConsumer.onPrivateAccountReconcile(
|
|
847
|
+
record.accountId,
|
|
848
|
+
record.venue,
|
|
849
|
+
snapshot,
|
|
850
|
+
{
|
|
851
|
+
requestStartedAt,
|
|
852
|
+
preserveStatus,
|
|
853
|
+
},
|
|
854
|
+
);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (
|
|
857
|
+
!this.shouldContinueAccountBootstrap(
|
|
858
|
+
record,
|
|
859
|
+
generation,
|
|
860
|
+
accountGeneration,
|
|
861
|
+
)
|
|
862
|
+
) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
this.context.publishRuntimeError(
|
|
867
|
+
"adapter",
|
|
868
|
+
error instanceof Error
|
|
869
|
+
? error
|
|
870
|
+
: new Error(
|
|
871
|
+
`Failed to reconcile ${record.venue} private account state`,
|
|
872
|
+
),
|
|
873
|
+
{
|
|
874
|
+
accountId: record.accountId,
|
|
875
|
+
venue: record.venue,
|
|
876
|
+
},
|
|
877
|
+
);
|
|
878
|
+
this.accountConsumer.onPrivateAccountStreamState(
|
|
879
|
+
record.accountId,
|
|
880
|
+
record.venue,
|
|
881
|
+
{
|
|
882
|
+
runtimeStatus: "degraded",
|
|
883
|
+
ready: record.accountReady,
|
|
884
|
+
reason: transportReason(error, "http_failed"),
|
|
885
|
+
},
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private async reconcileOrders(
|
|
891
|
+
record: PrivateSubscriptionRecord,
|
|
892
|
+
account: RegisteredAccountRecord,
|
|
893
|
+
generation: number,
|
|
894
|
+
orderGeneration: number,
|
|
895
|
+
preserveStatus: boolean,
|
|
896
|
+
): Promise<void> {
|
|
897
|
+
const adapter = this.getAdapter(record.venue);
|
|
898
|
+
if (
|
|
899
|
+
!adapter.fetchOpenOrders ||
|
|
900
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
901
|
+
) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const requestStartedAt = this.context.now();
|
|
906
|
+
try {
|
|
907
|
+
const snapshot = await adapter.fetchOpenOrders(
|
|
908
|
+
account.credentials ?? {},
|
|
909
|
+
{
|
|
910
|
+
...account.options,
|
|
911
|
+
accountId: account.accountId,
|
|
912
|
+
},
|
|
913
|
+
);
|
|
914
|
+
if (
|
|
915
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
916
|
+
) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
record.orderReady = true;
|
|
921
|
+
const disappeared = this.orderConsumer.onPrivateOrderReconcile(
|
|
922
|
+
record.accountId,
|
|
923
|
+
record.venue,
|
|
924
|
+
snapshot,
|
|
925
|
+
{
|
|
926
|
+
requestStartedAt,
|
|
927
|
+
preserveStatus,
|
|
928
|
+
},
|
|
929
|
+
);
|
|
930
|
+
await this.backfillDisappearedOrders(
|
|
931
|
+
record,
|
|
932
|
+
account,
|
|
933
|
+
generation,
|
|
934
|
+
orderGeneration,
|
|
935
|
+
disappeared,
|
|
936
|
+
);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
if (
|
|
939
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
940
|
+
) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
this.handleOrderReconcileError(record, error);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private async backfillDisappearedOrders(
|
|
949
|
+
record: PrivateSubscriptionRecord,
|
|
950
|
+
account: RegisteredAccountRecord,
|
|
951
|
+
generation: number,
|
|
952
|
+
orderGeneration: number,
|
|
953
|
+
disappeared: OrderSnapshot[],
|
|
954
|
+
): Promise<void> {
|
|
955
|
+
const adapter = this.getAdapter(record.venue);
|
|
956
|
+
if (!adapter.fetchOrder || disappeared.length === 0) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const pending = disappeared
|
|
961
|
+
.filter((order) => order.orderId || order.clientOrderId)
|
|
962
|
+
.slice(0, MAX_ORDER_TERMINAL_BACKFILLS_PER_RECONCILE);
|
|
963
|
+
if (pending.length === 0) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
let cursor = 0;
|
|
968
|
+
const workers = Array.from(
|
|
969
|
+
{
|
|
970
|
+
length: Math.min(
|
|
971
|
+
MAX_ORDER_TERMINAL_BACKFILL_CONCURRENCY,
|
|
972
|
+
pending.length,
|
|
973
|
+
),
|
|
974
|
+
},
|
|
975
|
+
async () => {
|
|
976
|
+
while (cursor < pending.length) {
|
|
977
|
+
if (
|
|
978
|
+
!this.shouldContinueOrderBootstrap(
|
|
979
|
+
record,
|
|
980
|
+
generation,
|
|
981
|
+
orderGeneration,
|
|
982
|
+
)
|
|
983
|
+
) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const order = pending[cursor];
|
|
988
|
+
cursor += 1;
|
|
989
|
+
if (!order) {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
await this.backfillDisappearedOrder(
|
|
993
|
+
record,
|
|
994
|
+
account,
|
|
995
|
+
generation,
|
|
996
|
+
orderGeneration,
|
|
997
|
+
order,
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
await Promise.all(workers);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private async backfillDisappearedOrder(
|
|
1007
|
+
record: PrivateSubscriptionRecord,
|
|
1008
|
+
account: RegisteredAccountRecord,
|
|
1009
|
+
generation: number,
|
|
1010
|
+
orderGeneration: number,
|
|
1011
|
+
order: OrderSnapshot,
|
|
1012
|
+
): Promise<void> {
|
|
1013
|
+
const adapter = this.getAdapter(record.venue);
|
|
1014
|
+
if (
|
|
1015
|
+
!adapter.fetchOrder ||
|
|
1016
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1017
|
+
) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const requestStartedAt = this.context.now();
|
|
1022
|
+
try {
|
|
1023
|
+
let request: FetchOrderRequest;
|
|
1024
|
+
if (order.orderId) {
|
|
1025
|
+
request = {
|
|
1026
|
+
symbol: order.symbol,
|
|
1027
|
+
orderId: order.orderId,
|
|
1028
|
+
clientOrderId: order.clientOrderId,
|
|
1029
|
+
};
|
|
1030
|
+
} else if (order.clientOrderId) {
|
|
1031
|
+
request = {
|
|
1032
|
+
symbol: order.symbol,
|
|
1033
|
+
clientOrderId: order.clientOrderId,
|
|
1034
|
+
};
|
|
1035
|
+
} else {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const update = await adapter.fetchOrder(
|
|
1040
|
+
account.credentials ?? {},
|
|
1041
|
+
request,
|
|
1042
|
+
{ ...account.options, accountId: account.accountId },
|
|
1043
|
+
);
|
|
1044
|
+
if (
|
|
1045
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1046
|
+
) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (!update) {
|
|
1051
|
+
this.handleOrderReconcileError(
|
|
1052
|
+
record,
|
|
1053
|
+
new Error(
|
|
1054
|
+
`Failed to backfill disappeared ${record.venue} order terminal state`,
|
|
1055
|
+
),
|
|
1056
|
+
);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
this.orderConsumer.onPrivateOrderUpdate(
|
|
1061
|
+
record.accountId,
|
|
1062
|
+
record.venue,
|
|
1063
|
+
update,
|
|
1064
|
+
{
|
|
1065
|
+
requestStartedAt,
|
|
439
1066
|
},
|
|
440
1067
|
);
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
if (
|
|
1070
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1071
|
+
) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
this.handleOrderReconcileError(record, error);
|
|
441
1076
|
}
|
|
442
1077
|
}
|
|
443
1078
|
|
|
1079
|
+
private handleOrderReconcileError(
|
|
1080
|
+
record: PrivateSubscriptionRecord,
|
|
1081
|
+
error: unknown,
|
|
1082
|
+
): void {
|
|
1083
|
+
this.context.publishRuntimeError(
|
|
1084
|
+
"adapter",
|
|
1085
|
+
error instanceof Error
|
|
1086
|
+
? error
|
|
1087
|
+
: new Error(`Failed to reconcile ${record.venue} private order state`),
|
|
1088
|
+
{
|
|
1089
|
+
accountId: record.accountId,
|
|
1090
|
+
venue: record.venue,
|
|
1091
|
+
},
|
|
1092
|
+
);
|
|
1093
|
+
this.orderConsumer.onPrivateOrderStreamState(
|
|
1094
|
+
record.accountId,
|
|
1095
|
+
record.venue,
|
|
1096
|
+
{
|
|
1097
|
+
runtimeStatus: "degraded",
|
|
1098
|
+
ready: record.orderReady,
|
|
1099
|
+
reason: transportReason(error, "http_failed"),
|
|
1100
|
+
},
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
444
1104
|
private async ensureStream(
|
|
445
1105
|
record: PrivateSubscriptionRecord,
|
|
446
1106
|
account: RegisteredAccountRecord,
|
|
@@ -466,15 +1126,21 @@ export class PrivateSubscriptionCoordinator {
|
|
|
466
1126
|
record: PrivateSubscriptionRecord,
|
|
467
1127
|
account: RegisteredAccountRecord,
|
|
468
1128
|
): Promise<void> {
|
|
1129
|
+
const adapter = this.getAdapter(record.venue);
|
|
469
1130
|
const credentials = account.credentials;
|
|
470
|
-
if (
|
|
1131
|
+
if (adapter.accountCapabilities.credentialsRequired && !credentials) {
|
|
471
1132
|
throw new AcexError(
|
|
472
1133
|
"CREDENTIALS_MISSING",
|
|
473
1134
|
`Account credentials are required for private subscriptions: ${account.accountId}`,
|
|
1135
|
+
{
|
|
1136
|
+
details: buildAcexErrorDetails({
|
|
1137
|
+
accountId: account.accountId,
|
|
1138
|
+
venue: account.venue,
|
|
1139
|
+
}),
|
|
1140
|
+
},
|
|
474
1141
|
);
|
|
475
1142
|
}
|
|
476
1143
|
|
|
477
|
-
const adapter = this.getAdapter(record.venue);
|
|
478
1144
|
const stream = adapter.createPrivateStream(
|
|
479
1145
|
credentials ?? {},
|
|
480
1146
|
{
|
|
@@ -559,7 +1225,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
559
1225
|
{
|
|
560
1226
|
runtimeStatus: "degraded",
|
|
561
1227
|
ready: record.accountReady,
|
|
562
|
-
reason: "http_failed",
|
|
1228
|
+
reason: transportReason(error, "http_failed"),
|
|
563
1229
|
},
|
|
564
1230
|
);
|
|
565
1231
|
}
|
|
@@ -570,10 +1236,9 @@ export class PrivateSubscriptionCoordinator {
|
|
|
570
1236
|
reconnectDelayMs: this.streamReconnectDelayMs,
|
|
571
1237
|
reconnectMaxDelayMs: this.streamReconnectMaxDelayMs,
|
|
572
1238
|
listenKeyKeepAliveMs: this.listenKeyKeepAliveMs,
|
|
573
|
-
juplendPollIntervalMs: this.juplendPollIntervalMs,
|
|
574
1239
|
now: () => this.context.now(),
|
|
575
1240
|
},
|
|
576
|
-
account.options,
|
|
1241
|
+
{ ...account.options, accountId: account.accountId },
|
|
577
1242
|
);
|
|
578
1243
|
|
|
579
1244
|
record.stream = stream;
|
|
@@ -622,31 +1287,65 @@ export class PrivateSubscriptionCoordinator {
|
|
|
622
1287
|
record: PrivateSubscriptionRecord,
|
|
623
1288
|
): Promise<void> {
|
|
624
1289
|
const account = this.getAccount(record.accountId);
|
|
1290
|
+
const generation = record.privateReconcileGeneration;
|
|
1291
|
+
const accountGeneration = record.accountSubscriptionGeneration;
|
|
1292
|
+
const orderGeneration = record.orderSubscriptionGeneration;
|
|
625
1293
|
|
|
626
1294
|
if (record.accountSubscribed) {
|
|
627
1295
|
this.accountConsumer.onPrivateAccountPending(
|
|
628
1296
|
record.accountId,
|
|
629
1297
|
record.venue,
|
|
630
1298
|
);
|
|
631
|
-
await this.
|
|
1299
|
+
await this.reconcileAccount(
|
|
1300
|
+
record,
|
|
1301
|
+
account,
|
|
1302
|
+
generation,
|
|
1303
|
+
accountGeneration,
|
|
1304
|
+
false,
|
|
1305
|
+
);
|
|
632
1306
|
}
|
|
633
1307
|
|
|
634
1308
|
if (record.ordersSubscribed) {
|
|
635
1309
|
this.orderConsumer.onPrivateOrderPending(record.accountId, record.venue);
|
|
636
|
-
await this.
|
|
1310
|
+
await this.reconcileOrders(
|
|
1311
|
+
record,
|
|
1312
|
+
account,
|
|
1313
|
+
generation,
|
|
1314
|
+
orderGeneration,
|
|
1315
|
+
false,
|
|
1316
|
+
);
|
|
637
1317
|
}
|
|
638
1318
|
}
|
|
639
1319
|
|
|
640
1320
|
private async bootstrapAccount(
|
|
641
1321
|
record: PrivateSubscriptionRecord,
|
|
642
1322
|
account: RegisteredAccountRecord,
|
|
1323
|
+
generation: number,
|
|
1324
|
+
accountGeneration: number,
|
|
643
1325
|
): Promise<void> {
|
|
1326
|
+
if (
|
|
1327
|
+
!this.shouldContinueAccountBootstrap(
|
|
1328
|
+
record,
|
|
1329
|
+
generation,
|
|
1330
|
+
accountGeneration,
|
|
1331
|
+
)
|
|
1332
|
+
) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
644
1336
|
try {
|
|
645
|
-
const
|
|
1337
|
+
const adapter = this.getAdapter(record.venue);
|
|
1338
|
+
const bootstrap = await adapter.bootstrapAccount(
|
|
646
1339
|
account.credentials ?? {},
|
|
647
1340
|
{ ...account.options, accountId: account.accountId },
|
|
648
1341
|
);
|
|
649
|
-
if (
|
|
1342
|
+
if (
|
|
1343
|
+
!this.shouldContinueAccountBootstrap(
|
|
1344
|
+
record,
|
|
1345
|
+
generation,
|
|
1346
|
+
accountGeneration,
|
|
1347
|
+
)
|
|
1348
|
+
) {
|
|
650
1349
|
return;
|
|
651
1350
|
}
|
|
652
1351
|
|
|
@@ -657,6 +1356,16 @@ export class PrivateSubscriptionCoordinator {
|
|
|
657
1356
|
bootstrap,
|
|
658
1357
|
);
|
|
659
1358
|
} catch (error) {
|
|
1359
|
+
if (
|
|
1360
|
+
!this.shouldContinueAccountBootstrap(
|
|
1361
|
+
record,
|
|
1362
|
+
generation,
|
|
1363
|
+
accountGeneration,
|
|
1364
|
+
)
|
|
1365
|
+
) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
660
1369
|
record.accountReady = false;
|
|
661
1370
|
this.context.publishRuntimeError(
|
|
662
1371
|
"adapter",
|
|
@@ -676,14 +1385,32 @@ export class PrivateSubscriptionCoordinator {
|
|
|
676
1385
|
{
|
|
677
1386
|
runtimeStatus: "degraded",
|
|
678
1387
|
ready: false,
|
|
679
|
-
reason:
|
|
1388
|
+
reason: transportReason(
|
|
1389
|
+
error,
|
|
1390
|
+
this.getAdapter(record.venue).accountCapabilities
|
|
1391
|
+
.credentialsRequired
|
|
1392
|
+
? "auth_failed"
|
|
1393
|
+
: "http_failed",
|
|
1394
|
+
),
|
|
680
1395
|
},
|
|
681
1396
|
);
|
|
682
|
-
const
|
|
683
|
-
|
|
1397
|
+
const details = buildAcexErrorDetails(
|
|
1398
|
+
{
|
|
1399
|
+
accountId: record.accountId,
|
|
1400
|
+
venue: record.venue,
|
|
1401
|
+
},
|
|
1402
|
+
error,
|
|
1403
|
+
);
|
|
684
1404
|
throw new AcexError(
|
|
685
1405
|
"ACCOUNT_BOOTSTRAP_FAILED",
|
|
686
|
-
|
|
1406
|
+
formatAcexErrorMessage(
|
|
1407
|
+
`Failed to bootstrap account data: ${record.accountId}`,
|
|
1408
|
+
details,
|
|
1409
|
+
),
|
|
1410
|
+
{
|
|
1411
|
+
cause: error,
|
|
1412
|
+
details,
|
|
1413
|
+
},
|
|
687
1414
|
);
|
|
688
1415
|
}
|
|
689
1416
|
}
|
|
@@ -691,49 +1418,157 @@ export class PrivateSubscriptionCoordinator {
|
|
|
691
1418
|
private async bootstrapOrders(
|
|
692
1419
|
record: PrivateSubscriptionRecord,
|
|
693
1420
|
account: RegisteredAccountRecord,
|
|
1421
|
+
generation: number,
|
|
1422
|
+
orderGeneration: number,
|
|
694
1423
|
): Promise<void> {
|
|
1424
|
+
if (
|
|
1425
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1426
|
+
) {
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const adapter = this.getAdapter(record.venue);
|
|
1431
|
+
if (!adapter.fetchOpenOrders) {
|
|
1432
|
+
try {
|
|
1433
|
+
const requestStartedAt = this.context.now();
|
|
1434
|
+
const orders = await adapter.bootstrapOpenOrders(
|
|
1435
|
+
account.credentials ?? {},
|
|
1436
|
+
{ ...account.options, accountId: account.accountId },
|
|
1437
|
+
);
|
|
1438
|
+
const snapshot: RawOpenOrdersSnapshot = {
|
|
1439
|
+
orders,
|
|
1440
|
+
snapshotReceivedAt:
|
|
1441
|
+
orders.reduce(
|
|
1442
|
+
(latest, order) => Math.max(latest, order.receivedAt),
|
|
1443
|
+
0,
|
|
1444
|
+
) || this.context.now(),
|
|
1445
|
+
};
|
|
1446
|
+
if (
|
|
1447
|
+
!this.shouldContinueOrderBootstrap(
|
|
1448
|
+
record,
|
|
1449
|
+
generation,
|
|
1450
|
+
orderGeneration,
|
|
1451
|
+
)
|
|
1452
|
+
) {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
record.orderReady = true;
|
|
1457
|
+
const disappeared = this.orderConsumer.onPrivateOrderBootstrap(
|
|
1458
|
+
record.accountId,
|
|
1459
|
+
record.venue,
|
|
1460
|
+
snapshot,
|
|
1461
|
+
{
|
|
1462
|
+
requestStartedAt,
|
|
1463
|
+
},
|
|
1464
|
+
);
|
|
1465
|
+
await this.backfillDisappearedOrders(
|
|
1466
|
+
record,
|
|
1467
|
+
account,
|
|
1468
|
+
generation,
|
|
1469
|
+
orderGeneration,
|
|
1470
|
+
disappeared,
|
|
1471
|
+
);
|
|
1472
|
+
return;
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
if (
|
|
1475
|
+
!this.shouldContinueOrderBootstrap(
|
|
1476
|
+
record,
|
|
1477
|
+
generation,
|
|
1478
|
+
orderGeneration,
|
|
1479
|
+
)
|
|
1480
|
+
) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
this.handleBootstrapOrdersError(record, error);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const requestStartedAt = this.context.now();
|
|
695
1490
|
try {
|
|
696
|
-
const
|
|
1491
|
+
const snapshot = await adapter.fetchOpenOrders(
|
|
697
1492
|
account.credentials ?? {},
|
|
698
|
-
|
|
1493
|
+
{
|
|
1494
|
+
...account.options,
|
|
1495
|
+
accountId: account.accountId,
|
|
1496
|
+
},
|
|
699
1497
|
);
|
|
700
|
-
if (
|
|
1498
|
+
if (
|
|
1499
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1500
|
+
) {
|
|
701
1501
|
return;
|
|
702
1502
|
}
|
|
703
1503
|
|
|
704
1504
|
record.orderReady = true;
|
|
705
|
-
this.orderConsumer.onPrivateOrderBootstrap(
|
|
1505
|
+
const disappeared = this.orderConsumer.onPrivateOrderBootstrap(
|
|
706
1506
|
record.accountId,
|
|
707
1507
|
record.venue,
|
|
708
|
-
|
|
709
|
-
);
|
|
710
|
-
} catch (error) {
|
|
711
|
-
record.orderReady = false;
|
|
712
|
-
this.context.publishRuntimeError(
|
|
713
|
-
"adapter",
|
|
714
|
-
error instanceof Error
|
|
715
|
-
? error
|
|
716
|
-
: new Error(
|
|
717
|
-
`Failed to bootstrap ${record.venue} private order state`,
|
|
718
|
-
),
|
|
1508
|
+
snapshot,
|
|
719
1509
|
{
|
|
720
|
-
|
|
721
|
-
venue: record.venue,
|
|
1510
|
+
requestStartedAt,
|
|
722
1511
|
},
|
|
723
1512
|
);
|
|
724
|
-
this.
|
|
725
|
-
record
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
reason: "auth_failed",
|
|
731
|
-
},
|
|
732
|
-
);
|
|
733
|
-
throw new AcexError(
|
|
734
|
-
"ORDER_BOOTSTRAP_FAILED",
|
|
735
|
-
`Failed to bootstrap order data: ${record.accountId}`,
|
|
1513
|
+
await this.backfillDisappearedOrders(
|
|
1514
|
+
record,
|
|
1515
|
+
account,
|
|
1516
|
+
generation,
|
|
1517
|
+
orderGeneration,
|
|
1518
|
+
disappeared,
|
|
736
1519
|
);
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
if (
|
|
1522
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1523
|
+
) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
this.handleBootstrapOrdersError(record, error);
|
|
737
1528
|
}
|
|
738
1529
|
}
|
|
1530
|
+
|
|
1531
|
+
private handleBootstrapOrdersError(
|
|
1532
|
+
record: PrivateSubscriptionRecord,
|
|
1533
|
+
error: unknown,
|
|
1534
|
+
): never {
|
|
1535
|
+
record.orderReady = false;
|
|
1536
|
+
this.context.publishRuntimeError(
|
|
1537
|
+
"adapter",
|
|
1538
|
+
error instanceof Error
|
|
1539
|
+
? error
|
|
1540
|
+
: new Error(`Failed to bootstrap ${record.venue} private order state`),
|
|
1541
|
+
{
|
|
1542
|
+
accountId: record.accountId,
|
|
1543
|
+
venue: record.venue,
|
|
1544
|
+
},
|
|
1545
|
+
);
|
|
1546
|
+
this.orderConsumer.onPrivateOrderStreamState(
|
|
1547
|
+
record.accountId,
|
|
1548
|
+
record.venue,
|
|
1549
|
+
{
|
|
1550
|
+
runtimeStatus: "degraded",
|
|
1551
|
+
ready: false,
|
|
1552
|
+
reason: transportReason(error, "auth_failed"),
|
|
1553
|
+
},
|
|
1554
|
+
);
|
|
1555
|
+
const details = buildAcexErrorDetails(
|
|
1556
|
+
{
|
|
1557
|
+
accountId: record.accountId,
|
|
1558
|
+
venue: record.venue,
|
|
1559
|
+
},
|
|
1560
|
+
error,
|
|
1561
|
+
);
|
|
1562
|
+
throw new AcexError(
|
|
1563
|
+
"ORDER_BOOTSTRAP_FAILED",
|
|
1564
|
+
formatAcexErrorMessage(
|
|
1565
|
+
`Failed to bootstrap order data: ${record.accountId}`,
|
|
1566
|
+
details,
|
|
1567
|
+
),
|
|
1568
|
+
{
|
|
1569
|
+
cause: error,
|
|
1570
|
+
details,
|
|
1571
|
+
},
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
739
1574
|
}
|