dexbot 1.4.6 → 1.4.8
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 +1 -0
- package/dist/credential-daemon.d.ts.map +1 -1
- package/dist/credential-daemon.js +416 -92
- package/dist/credential-daemon.js.map +1 -1
- package/dist/dexbot.d.ts.map +1 -1
- package/dist/dexbot.js +61 -27
- package/dist/dexbot.js.map +1 -1
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +14 -24
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +21 -0
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/broadcast_failure.d.ts +21 -0
- package/dist/modules/broadcast_failure.d.ts.map +1 -0
- package/dist/modules/broadcast_failure.js +95 -0
- package/dist/modules/broadcast_failure.js.map +1 -0
- package/dist/modules/chain_keys.d.ts +2 -2
- package/dist/modules/chain_keys.d.ts.map +1 -1
- package/dist/modules/chain_keys.js +30 -56
- package/dist/modules/chain_keys.js.map +1 -1
- package/dist/modules/chain_orders.d.ts +79 -1
- package/dist/modules/chain_orders.d.ts.map +1 -1
- package/dist/modules/chain_orders.js +154 -10
- package/dist/modules/chain_orders.js.map +1 -1
- package/dist/modules/constants.d.ts +1 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +21 -7
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/daemon_node_health.d.ts +33 -0
- package/dist/modules/daemon_node_health.d.ts.map +1 -0
- package/dist/modules/daemon_node_health.js +120 -0
- package/dist/modules/daemon_node_health.js.map +1 -0
- package/dist/modules/dexbot_class.d.ts +12 -3
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +19 -5
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +46 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +754 -64
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_credential_client.d.ts +15 -0
- package/dist/modules/dexbot_credential_client.d.ts.map +1 -1
- package/dist/modules/dexbot_credential_client.js +114 -141
- package/dist/modules/dexbot_credential_client.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +45 -15
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +5 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +238 -85
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +95 -33
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +24 -2
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/key_store.d.ts.map +1 -1
- package/dist/modules/key_store.js +5 -4
- package/dist/modules/key_store.js.map +1 -1
- package/dist/modules/launcher/credential_daemon.d.ts.map +1 -1
- package/dist/modules/launcher/credential_daemon.js +15 -3
- package/dist/modules/launcher/credential_daemon.js.map +1 -1
- package/dist/modules/node_failure_ledger.d.ts +39 -0
- package/dist/modules/node_failure_ledger.d.ts.map +1 -0
- package/dist/modules/node_failure_ledger.js +88 -0
- package/dist/modules/node_failure_ledger.js.map +1 -0
- package/dist/modules/node_health_cache.d.ts +2 -1
- package/dist/modules/node_health_cache.d.ts.map +1 -1
- package/dist/modules/node_health_cache.js +25 -0
- package/dist/modules/node_health_cache.js.map +1 -1
- package/dist/modules/node_manager.d.ts +3 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +41 -10
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +26 -20
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +281 -139
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +66 -14
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +136 -56
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +6 -5
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +388 -61
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +98 -4
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +379 -92
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +24 -1
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts +16 -3
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +341 -253
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +79 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +18 -0
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +44 -1
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +91 -5
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +5 -1
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +7 -5
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/withPoolRef.d.ts.map +1 -1
- package/dist/modules/order/utils/withPoolRef.js +39 -4
- package/dist/modules/order/utils/withPoolRef.js.map +1 -1
- package/dist/modules/socket_json_client.d.ts +14 -0
- package/dist/modules/socket_json_client.d.ts.map +1 -0
- package/dist/modules/socket_json_client.js +100 -0
- package/dist/modules/socket_json_client.js.map +1 -0
- package/package.json +2 -1
- package/scripts/clear-all.sh +4 -4
- package/scripts/clear-logs.sh +4 -4
|
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
51
51
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
52
|
};
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.OrderManager = void 0;
|
|
54
|
+
exports.COWRebalanceEngine = exports.OrderManager = void 0;
|
|
55
55
|
// ===============================================================================
|
|
56
56
|
// SECTION 1: EXTERNAL DEPENDENCIES
|
|
57
57
|
// ===============================================================================
|
|
@@ -68,11 +68,77 @@ const Format = __importStar(require("./format"));
|
|
|
68
68
|
const constants_1 = require("../constants");
|
|
69
69
|
const math_1 = require("./utils/math");
|
|
70
70
|
const validate_1 = require("./utils/validate");
|
|
71
|
+
const order_1 = require("./utils/order");
|
|
71
72
|
const errors_1 = require("../utils/errors");
|
|
72
73
|
const { toFiniteNumber } = Format;
|
|
73
74
|
// ===============================================================================
|
|
74
75
|
// SECTION 2: COW REBALANCE ENGINE
|
|
75
76
|
// ===============================================================================
|
|
77
|
+
/**
|
|
78
|
+
* Parse a grid slot id ("slot-123") to its rail index. Slot ids are assigned
|
|
79
|
+
* in ascending price order at grid generation (grid.ts), so the index is
|
|
80
|
+
* strictly price-monotonic and can be compared exactly where float prices
|
|
81
|
+
* would risk rounding ambiguity (adjacent levels can round to the same
|
|
82
|
+
* price). Returns null when the id is not a grid slot id (e.g. orphan fills
|
|
83
|
+
* with chain-derived ids) so callers can fall back to price comparison.
|
|
84
|
+
*/
|
|
85
|
+
function parseSlotIndex(id) {
|
|
86
|
+
if (typeof id !== 'string')
|
|
87
|
+
return null;
|
|
88
|
+
const match = /^slot-(\d+)$/.exec(id);
|
|
89
|
+
if (!match)
|
|
90
|
+
return null;
|
|
91
|
+
const idx = parseInt(match[1], 10);
|
|
92
|
+
return Number.isFinite(idx) ? idx : null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Stale-placement veto: whether a placement action crosses the plan's own
|
|
96
|
+
* boundary and would fill immediately on stale market data.
|
|
97
|
+
* - BUY create/update targeting a slot ABOVE the boundary is dropped; SELL
|
|
98
|
+
* create/update targeting a slot BELOW the boundary is dropped. Strict
|
|
99
|
+
* comparison: a placement AT the boundary is kept (the boundary slot is
|
|
100
|
+
* the spread, which reconcile never targets).
|
|
101
|
+
* - The reference is the PLAN'S OWN targetBoundary — and ONLY that: the
|
|
102
|
+
* plan's slots derive from the same target grid, so a stale plan's
|
|
103
|
+
* placements cross its own boundary while legitimate re-placements never
|
|
104
|
+
* do (the boundary is capped, the fills are not). Fill-based veto lines
|
|
105
|
+
* are deliberately NOT used — after a burst the fills sit beyond the
|
|
106
|
+
* plan's legitimate re-place levels (e.g. SELL fills 5,6,7 with cap=1
|
|
107
|
+
* moves the boundary only to slot 6), so ANY fill comparison can veto
|
|
108
|
+
* valid placements, including the strategy's own rotations (a partial
|
|
109
|
+
* SELL rotated into a slot below the last BUY fill after the boundary
|
|
110
|
+
* crawled left). The guard is therefore "inert" for boundary-consistent
|
|
111
|
+
* engine output by construction.
|
|
112
|
+
* - Slot ids are compared within the plan's own grid generation
|
|
113
|
+
* (targetBoundary and target slots are both from the current plan), so a
|
|
114
|
+
* recenter between the fill capture and the plan cannot skew the
|
|
115
|
+
* comparison. Both CREATE and UPDATE actions are guarded: the
|
|
116
|
+
* cancel/create optimization can convert the same logical placement into
|
|
117
|
+
* an UPDATE op (target slot = newGridId), which the CREATE-only check
|
|
118
|
+
* would have missed.
|
|
119
|
+
* - When no plan boundary is available (hand-built plans), nothing is
|
|
120
|
+
* vetoed — fill evidence is not a reliable veto line. Non-placement
|
|
121
|
+
* actions and non-grid-slot targets are never vetoed.
|
|
122
|
+
* @param {Object} action - COW action
|
|
123
|
+
* @param {number} planBoundary - The plan's target boundary slot index
|
|
124
|
+
* @returns {string|null} Drop reason for the log, or null to keep the action
|
|
125
|
+
*/
|
|
126
|
+
function stalePlacementDropReason(action, planBoundary) {
|
|
127
|
+
const isPlacement = action?.type === constants_1.COW_ACTIONS.CREATE || action?.type === constants_1.COW_ACTIONS.UPDATE;
|
|
128
|
+
if (!isPlacement)
|
|
129
|
+
return null;
|
|
130
|
+
const targetSlot = parseSlotIndex(action?.newGridId ?? action?.id);
|
|
131
|
+
if (targetSlot === null)
|
|
132
|
+
return null;
|
|
133
|
+
const kindWord = action.type === constants_1.COW_ACTIONS.UPDATE ? 'update' : 'create';
|
|
134
|
+
if (action?.order?.type === constants_1.ORDER_TYPES.BUY && targetSlot > planBoundary) {
|
|
135
|
+
return `BUY ${kindWord} for slot ${action.id}: slot ${targetSlot} > plan boundary ${planBoundary}`;
|
|
136
|
+
}
|
|
137
|
+
if (action?.order?.type === constants_1.ORDER_TYPES.SELL && targetSlot < planBoundary) {
|
|
138
|
+
return `SELL ${kindWord} for slot ${action.id}: slot ${targetSlot} < plan boundary ${planBoundary}`;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
76
142
|
//
|
|
77
143
|
// COPY-ON-WRITE (COW) PATTERN FOR SAFE REBALANCING
|
|
78
144
|
//
|
|
@@ -145,6 +211,28 @@ class COWRebalanceEngine {
|
|
|
145
211
|
return (0, validate_1.buildAbortedResult)(reconcileResult.reason);
|
|
146
212
|
}
|
|
147
213
|
const optimizedActions = (0, validate_1.optimizeRebalanceActions)(reconcileResult.actions, masterGrid);
|
|
214
|
+
// Stale-placement guard: drop placements crossing the plan's own
|
|
215
|
+
// boundary (see stalePlacementDropReason) — defer them to the next
|
|
216
|
+
// cycle instead of filling immediately.
|
|
217
|
+
const planBoundary = (targetBoundary !== null && targetBoundary !== undefined && Number.isFinite(Number(targetBoundary)))
|
|
218
|
+
? Number(targetBoundary)
|
|
219
|
+
: null;
|
|
220
|
+
if (planBoundary !== null) {
|
|
221
|
+
const before = optimizedActions.length;
|
|
222
|
+
const guarded = optimizedActions.filter((a) => {
|
|
223
|
+
const dropReason = stalePlacementDropReason(a, planBoundary);
|
|
224
|
+
if (dropReason) {
|
|
225
|
+
this.logger?.log(`[COW] Dropping stale-slot ${dropReason}`, 'warn');
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
if (guarded.length < before) {
|
|
231
|
+
this.logger?.log(`[COW] Stale-placement guard removed ${before - guarded.length} placement(s); ${guarded.length} action(s) remain`, 'warn');
|
|
232
|
+
}
|
|
233
|
+
optimizedActions.length = 0;
|
|
234
|
+
optimizedActions.push(...guarded);
|
|
235
|
+
}
|
|
148
236
|
(0, validate_1.projectTargetToWorkingGrid)(workingGrid, targetGrid, { actions: optimizedActions });
|
|
149
237
|
const precisions = {
|
|
150
238
|
buyPrecision: this.assets?.assetB?.precision,
|
|
@@ -175,6 +263,7 @@ class COWRebalanceEngine {
|
|
|
175
263
|
});
|
|
176
264
|
}
|
|
177
265
|
}
|
|
266
|
+
exports.COWRebalanceEngine = COWRebalanceEngine;
|
|
178
267
|
// ===============================================================================
|
|
179
268
|
// SECTION 3: ORDER MANAGER CLASS
|
|
180
269
|
// ===============================================================================
|
|
@@ -267,6 +356,7 @@ class OrderManager {
|
|
|
267
356
|
_accountTotalsPromise;
|
|
268
357
|
_accountTotalsResolve;
|
|
269
358
|
_isFetchingTotals;
|
|
359
|
+
accountTotalsStale;
|
|
270
360
|
ordersNeedingPriceCorrection;
|
|
271
361
|
shadowOrderIds;
|
|
272
362
|
processedFillTracker;
|
|
@@ -280,6 +370,7 @@ class OrderManager {
|
|
|
280
370
|
_gridSidesUpdated;
|
|
281
371
|
_pauseFundRecalc;
|
|
282
372
|
_pauseFundRecalcWatchdog;
|
|
373
|
+
_fillBatchInFlight;
|
|
283
374
|
_pauseRecalcLogging;
|
|
284
375
|
_pauseRecalcLoggingWatchdog;
|
|
285
376
|
_throwOnIllegalState;
|
|
@@ -291,6 +382,7 @@ class OrderManager {
|
|
|
291
382
|
_pendingBroadcasts;
|
|
292
383
|
_committedOrderIds;
|
|
293
384
|
_committedOrderIdsBuiltAt;
|
|
385
|
+
_orderIdAssignedAt;
|
|
294
386
|
_gapSlots;
|
|
295
387
|
_gridDirtyAt;
|
|
296
388
|
_orphanFillsCreditedAt;
|
|
@@ -338,7 +430,9 @@ class OrderManager {
|
|
|
338
430
|
buy: { armed: true, lastTriggeredAt: 0 },
|
|
339
431
|
sell: { armed: true, lastTriggeredAt: 0 }
|
|
340
432
|
};
|
|
341
|
-
|
|
433
|
+
// Sync reset — _fundLock is not created until later in the constructor,
|
|
434
|
+
// and no concurrent access exists during construction.
|
|
435
|
+
this.accountant.resetFunds();
|
|
342
436
|
this.btsBalance = { free: 0, total: 0, locked: 0 };
|
|
343
437
|
this.targetSpreadCount = 0;
|
|
344
438
|
this.currentSpreadCount = 0;
|
|
@@ -347,6 +441,7 @@ class OrderManager {
|
|
|
347
441
|
this._accountTotalsPromise = null;
|
|
348
442
|
this._accountTotalsResolve = null;
|
|
349
443
|
this._isFetchingTotals = false;
|
|
444
|
+
this.accountTotalsStale = false;
|
|
350
445
|
this.ordersNeedingPriceCorrection = [];
|
|
351
446
|
this.shadowOrderIds = new Map();
|
|
352
447
|
this.processedFillTracker = new Map();
|
|
@@ -372,6 +467,13 @@ class OrderManager {
|
|
|
372
467
|
this._gridSidesUpdated = new Set();
|
|
373
468
|
this._pauseFundRecalc = 0;
|
|
374
469
|
this._pauseFundRecalcWatchdog = null;
|
|
470
|
+
// Depth counter for fill batches currently mid-accounting. While non-zero,
|
|
471
|
+
// _verifyFundInvariants must not run: the batch's balance refresh already
|
|
472
|
+
// reflects the just-filled orders on-chain, but the grid still holds them
|
|
473
|
+
// as committed until the batch's grid mutation lands, so a check in that
|
|
474
|
+
// window would report a spurious Total != Free + Committed by exactly the
|
|
475
|
+
// fills' size. The check runs once the batch settles instead.
|
|
476
|
+
this._fillBatchInFlight = 0;
|
|
375
477
|
this._pauseRecalcLogging = false;
|
|
376
478
|
this._pauseRecalcLoggingWatchdog = null;
|
|
377
479
|
this._throwOnIllegalState = false;
|
|
@@ -383,6 +485,7 @@ class OrderManager {
|
|
|
383
485
|
this._pendingBroadcasts = new Map();
|
|
384
486
|
this._committedOrderIds = new Set();
|
|
385
487
|
this._committedOrderIdsBuiltAt = 0;
|
|
488
|
+
this._orderIdAssignedAt = new Map();
|
|
386
489
|
this._gapSlots = 0;
|
|
387
490
|
this._gridDirtyAt = null;
|
|
388
491
|
this._orphanFillsCreditedAt = null;
|
|
@@ -420,6 +523,54 @@ class OrderManager {
|
|
|
420
523
|
? constants_1.REBALANCE_STATES.REBALANCING
|
|
421
524
|
: constants_1.REBALANCE_STATES.NORMAL;
|
|
422
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Push a working grid onto the rebalance stack (LIFO), set the state to
|
|
528
|
+
* REBALANCING, and — when a result object is provided — mark it as pushed
|
|
529
|
+
* so pop sites can release exactly the entry this push created. This is
|
|
530
|
+
* the only place a grid may be pushed; all releases go through
|
|
531
|
+
* _popWorkingGridRef (marker-guarded, early-return sites) or
|
|
532
|
+
* _releaseWorkingGridRef (identity-checked, commit sites).
|
|
533
|
+
* @param {Object} workingGrid - The working grid to push
|
|
534
|
+
* @param {Object} [result] - Result object to carry the push marker on
|
|
535
|
+
*/
|
|
536
|
+
_pushWorkingGridRef(workingGrid, result = null) {
|
|
537
|
+
this._currentWorkingGridStack.push(workingGrid);
|
|
538
|
+
this._rebalanceState = constants_1.REBALANCE_STATES.REBALANCING;
|
|
539
|
+
if (result) {
|
|
540
|
+
result._workingGridPushed = true;
|
|
541
|
+
}
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Pop the stack entry owned by a result, exactly once (marker-guarded).
|
|
546
|
+
* Results that were never pushed (aborted plans, no-trigger
|
|
547
|
+
* processFilledOrders outputs, updateOrdersOnChainPlan cowResults,
|
|
548
|
+
* reconcileGridOrders null results) leave the stack untouched — an
|
|
549
|
+
* unmatched pop could steal a nested grid's entry. Clears the marker so
|
|
550
|
+
* a later throw in the same frame cannot pop a second time.
|
|
551
|
+
* @param {Object} [result] - Result object carrying the push marker
|
|
552
|
+
*/
|
|
553
|
+
_popWorkingGridRef(result = null) {
|
|
554
|
+
if (result && result._workingGridPushed === true) {
|
|
555
|
+
this._clearWorkingGridRef();
|
|
556
|
+
result._workingGridPushed = false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Release the stack entry for a grid after its commit settles, exactly
|
|
561
|
+
* once per commit invocation. Identity-checked: plan-path grids
|
|
562
|
+
* (updateOrdersOnChainPlan, local-only divergence commits) were never
|
|
563
|
+
* pushed, so popping unconditionally would steal a NESTED grid's stack
|
|
564
|
+
* entry. Pushed grids are always at the top at commit time (LIFO), so
|
|
565
|
+
* the check is a no-op for them.
|
|
566
|
+
* @param {Object} workingGrid - The committed working grid
|
|
567
|
+
*/
|
|
568
|
+
_releaseWorkingGridRef(workingGrid) {
|
|
569
|
+
const stack = this._currentWorkingGridStack;
|
|
570
|
+
if (stack.length > 0 && stack[stack.length - 1] === workingGrid) {
|
|
571
|
+
this._clearWorkingGridRef();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
423
574
|
_setRebalanceState(state) {
|
|
424
575
|
this._rebalanceState = state;
|
|
425
576
|
this.logger?.log(`[COW] Rebalance state: ${state}`, 'debug');
|
|
@@ -536,20 +687,29 @@ class OrderManager {
|
|
|
536
687
|
}
|
|
537
688
|
}
|
|
538
689
|
/**
|
|
539
|
-
*
|
|
690
|
+
* Reset funds structure to zeroed values. Acquires _fundLock so the
|
|
691
|
+
* wholesale funds replacement cannot race with concurrent fund mutations
|
|
692
|
+
* (deductBtsFees, tryDeductFromChainFree, updateOptimisticFreeBalance).
|
|
693
|
+
* @returns {Promise<void>}
|
|
540
694
|
*/
|
|
541
|
-
resetFunds() {
|
|
542
|
-
return this.
|
|
695
|
+
async resetFunds() {
|
|
696
|
+
return await this._fundLock.acquire(async () => {
|
|
697
|
+
return this.accountant.resetFunds();
|
|
698
|
+
});
|
|
543
699
|
}
|
|
544
700
|
async _deductFromChainFree(orderType, size, operation) {
|
|
545
701
|
if (!this.accountant)
|
|
546
702
|
return;
|
|
547
|
-
return await this.
|
|
703
|
+
return await this._fundLock.acquire(async () => {
|
|
704
|
+
return await this.accountant.tryDeductFromChainFree(orderType, size, operation);
|
|
705
|
+
});
|
|
548
706
|
}
|
|
549
707
|
async _addToChainFree(orderType, size, operation) {
|
|
550
708
|
if (!this.accountant)
|
|
551
709
|
return;
|
|
552
|
-
return await this.
|
|
710
|
+
return await this._fundLock.acquire(async () => {
|
|
711
|
+
return await this.accountant.addToChainFree(orderType, size, operation);
|
|
712
|
+
});
|
|
553
713
|
}
|
|
554
714
|
_getGridTotal(side) {
|
|
555
715
|
return (this.funds?.committed?.grid?.[side] || 0) + (this.funds?.virtual?.[side] || 0);
|
|
@@ -573,7 +733,7 @@ class OrderManager {
|
|
|
573
733
|
const size = toFiniteNumber(order.size);
|
|
574
734
|
if (size <= 0)
|
|
575
735
|
continue;
|
|
576
|
-
const isBuy = order.type === constants_1.ORDER_TYPES.BUY || (order.type === constants_1.ORDER_TYPES.SPREAD && order.price
|
|
736
|
+
const isBuy = order.type === constants_1.ORDER_TYPES.BUY || (order.type === constants_1.ORDER_TYPES.SPREAD && (0, order_1.resolveSpreadOrderSide)(order.price, this.config.startPrice) === constants_1.ORDER_TYPES.BUY);
|
|
577
737
|
if (isBuy)
|
|
578
738
|
committedBuy += size;
|
|
579
739
|
else
|
|
@@ -622,6 +782,66 @@ class OrderManager {
|
|
|
622
782
|
this.accountId = accountId;
|
|
623
783
|
await this._fetchAccountBalancesAndSetTotals();
|
|
624
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* Refresh accountTotals from chain when the cached snapshot is stale.
|
|
787
|
+
*
|
|
788
|
+
* Optimistic deductions (tryDeductFromChainFree) refuse to operate on
|
|
789
|
+
* snapshots older than MAX_ACCOUNT_TOTALS_AGE_MS and schedule a recovery
|
|
790
|
+
* instead, leaving the order placed but unaccounted. By refreshing once
|
|
791
|
+
* up front, the caller's accounting batch runs against a fresh snapshot
|
|
792
|
+
* so deductions apply normally and no skip/recovery cycle is triggered.
|
|
793
|
+
*
|
|
794
|
+
* @returns {Promise<{ok: boolean, reason?: string}>} {ok: true} when the
|
|
795
|
+
* snapshot is (now) fresh; {ok: false, reason} when the refresh failed and
|
|
796
|
+
* the snapshot is still stale — the caller must not proceed with accounting.
|
|
797
|
+
*/
|
|
798
|
+
async refreshAccountTotalsIfStale(options = {}) {
|
|
799
|
+
const force = options?.force === true;
|
|
800
|
+
const lastFetched = this.accountTotals?._lastFetchedAt || 0;
|
|
801
|
+
if (!force && Date.now() - lastFetched <= constants_1.TIMING.MAX_ACCOUNT_TOTALS_AGE_MS) {
|
|
802
|
+
return { ok: true };
|
|
803
|
+
}
|
|
804
|
+
const fetchedBefore = lastFetched;
|
|
805
|
+
// Apply the standard blockchain-op policy (30s timeout, 3 attempts, then
|
|
806
|
+
// node failover) so a transiently bad node does not force the caller to
|
|
807
|
+
// defer the whole fill batch to the next cycle. Only a genuinely
|
|
808
|
+
// unreachable chain leaves the snapshot stale.
|
|
809
|
+
try {
|
|
810
|
+
await (0, system_1.withBlockchainRetry)(() => this.fetchAccountTotals(this.accountId), 'refreshAccountTotalsIfStale', { logger: this.logger });
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
this.logger?.log?.(`[SYNC] refreshAccountTotalsIfStale fetch failed: ${(0, errors_1.getErrorMessage)(err)}`, 'warn');
|
|
814
|
+
}
|
|
815
|
+
const fetchedAfter = this.accountTotals?._lastFetchedAt || 0;
|
|
816
|
+
if (fetchedAfter <= fetchedBefore) {
|
|
817
|
+
this.logger?.log?.(`[SYNC] refreshAccountTotalsIfStale: accountTotals still stale after refresh (age=${Math.max(0, Date.now() - fetchedAfter)}ms).`, 'warn');
|
|
818
|
+
return { ok: false, reason: 'refresh-failed' };
|
|
819
|
+
}
|
|
820
|
+
return { ok: true };
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Re-anchor accountTotals to authoritative on-chain values after a fill
|
|
824
|
+
* batch has applied its optimistic accounting and grid mutation.
|
|
825
|
+
*
|
|
826
|
+
* A fill batch's up-front stale-totals refresh returns POST-fill balances
|
|
827
|
+
* (the fills already settled on-chain), then processFillAccounting applies
|
|
828
|
+
* the same pays/receives again — a double-count on the totals. Force-fetching
|
|
829
|
+
* here overwrites the optimistic values with the authoritative post-fill
|
|
830
|
+
* state so the batch ends exactly consistent with chain and the invariant
|
|
831
|
+
* holds at the final consolidated recalculation.
|
|
832
|
+
*
|
|
833
|
+
* @param {string} [label] - Context label for log messages
|
|
834
|
+
* @returns {Promise<boolean>} true when the re-anchor refreshed successfully
|
|
835
|
+
*/
|
|
836
|
+
async reanchorAccountTotals(label = 'fill-batch') {
|
|
837
|
+
const result = await this.refreshAccountTotalsIfStale({ force: true });
|
|
838
|
+
if (!result.ok) {
|
|
839
|
+
this.logger?.log?.(`[ACCOUNTING] ${label}: accountTotals re-anchor failed (${result.reason}); marking totals stale.`, 'warn');
|
|
840
|
+
this.accountTotalsStale = true;
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
625
845
|
async _fetchAccountBalancesAndSetTotals() {
|
|
626
846
|
return await this.sync.fetchAccountBalancesAndSetTotals();
|
|
627
847
|
}
|
|
@@ -637,7 +857,7 @@ class OrderManager {
|
|
|
637
857
|
async _setAccountTotals(totals) {
|
|
638
858
|
this.accountTotals = { ...(this.accountTotals || {}), ...totals, _lastFetchedAt: Date.now() };
|
|
639
859
|
if (!this.funds)
|
|
640
|
-
this.resetFunds();
|
|
860
|
+
await this.resetFunds();
|
|
641
861
|
await this._recalculateFunds();
|
|
642
862
|
if ((0, math_1.hasValidAccountTotals)(this.accountTotals, true) && typeof this._accountTotalsResolve === 'function') {
|
|
643
863
|
try {
|
|
@@ -733,7 +953,7 @@ class OrderManager {
|
|
|
733
953
|
this.logger?.log?.(`[MANAGER] pauseFundRecalc safety watchdog: resetting depth from ${this._pauseFundRecalc} to 0`, 'warn');
|
|
734
954
|
this._pauseFundRecalc = 0;
|
|
735
955
|
this._pauseFundRecalcWatchdog = null;
|
|
736
|
-
this.
|
|
956
|
+
this.recalculateFunds().catch((err) => {
|
|
737
957
|
this.logger?.log?.(`[MANAGER] Watchdog recalc failed: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
|
|
738
958
|
});
|
|
739
959
|
}
|
|
@@ -944,6 +1164,24 @@ class OrderManager {
|
|
|
944
1164
|
}
|
|
945
1165
|
const updatedOrder = (0, system_1.deepFreeze)({ ...nextOrder });
|
|
946
1166
|
const id = order.id;
|
|
1167
|
+
// Track when an orderId is first assigned to a slot. A freshly assigned
|
|
1168
|
+
// orderId (create/adopt broadcast still in flight, or not yet visible to
|
|
1169
|
+
// a lagging/truncated chain read) must not be treated as "absent from
|
|
1170
|
+
// chain" by phantom cleanup — that would virtualize a real live order
|
|
1171
|
+
// and re-create a duplicate. Absence decisions defer while the stamp is
|
|
1172
|
+
// younger than SYNC_LOCK_TIMEOUT_MS.
|
|
1173
|
+
if (updatedOrder.orderId && (!oldOrder?.orderId || oldOrder.orderId !== updatedOrder.orderId)) {
|
|
1174
|
+
// Prune stamps older than the guard window — they can never be
|
|
1175
|
+
// consulted again (the consumer defers only while the stamp is
|
|
1176
|
+
// younger than SYNC_LOCK_TIMEOUT_MS), so keeping them would leak one
|
|
1177
|
+
// entry per orderId on a perpetual-turnover bot.
|
|
1178
|
+
for (const [orderId, assignedAt] of this._orderIdAssignedAt) {
|
|
1179
|
+
if (Date.now() - assignedAt >= constants_1.TIMING.SYNC_LOCK_TIMEOUT_MS) {
|
|
1180
|
+
this._orderIdAssignedAt.delete(orderId);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
this._orderIdAssignedAt.set(updatedOrder.orderId, Date.now());
|
|
1184
|
+
}
|
|
947
1185
|
const newMap = (0, system_1.cloneMap)(this.orders);
|
|
948
1186
|
newMap.set(id, updatedOrder);
|
|
949
1187
|
this.orders = Object.freeze(newMap);
|
|
@@ -1088,6 +1326,11 @@ class OrderManager {
|
|
|
1088
1326
|
const shouldRebalance = triggerFills.length > 0;
|
|
1089
1327
|
if (shouldRebalance) {
|
|
1090
1328
|
const rebalanceResult = await this.performSafeRebalance(orders, excl);
|
|
1329
|
+
// Carry the fill set + exclusions on the result so the COW batch
|
|
1330
|
+
// executor can re-plan once from fresh master if the plan goes
|
|
1331
|
+
// stale between planning and broadcast.
|
|
1332
|
+
rebalanceResult.fills = orders;
|
|
1333
|
+
rebalanceResult.excludeIds = excl || new Set();
|
|
1091
1334
|
return rebalanceResult;
|
|
1092
1335
|
}
|
|
1093
1336
|
const workingGrid = new working_grid_1.WorkingGrid(this.orders, { baseVersion: this._gridVersion });
|
|
@@ -1349,10 +1592,13 @@ class OrderManager {
|
|
|
1349
1592
|
excludeIds
|
|
1350
1593
|
});
|
|
1351
1594
|
if (result.aborted) {
|
|
1352
|
-
|
|
1595
|
+
// Nothing was pushed for an aborted result — the push below is
|
|
1596
|
+
// skipped — so there is no working-grid ref to clear here. Popping
|
|
1597
|
+
// would steal an unrelated stack entry (double-pop) when combined
|
|
1598
|
+
// with the caller-side no-action handling in _executeBatchIfNeeded.
|
|
1353
1599
|
return result;
|
|
1354
1600
|
}
|
|
1355
|
-
this.
|
|
1601
|
+
this._pushWorkingGridRef(result.workingGrid, result);
|
|
1356
1602
|
return result;
|
|
1357
1603
|
}
|
|
1358
1604
|
_reconcileGridCOW(targetGrid, targetBoundary, workingGrid) {
|
|
@@ -1410,100 +1656,128 @@ class OrderManager {
|
|
|
1410
1656
|
return (0, validate_1.buildAbortedResult)(reason);
|
|
1411
1657
|
}
|
|
1412
1658
|
async _commitWorkingGrid(workingGrid, _workingIndexes, workingBoundary, options = {}) {
|
|
1413
|
-
const { skipRecalc } = this._normalizeCommitOptions(options);
|
|
1414
1659
|
const startTime = Date.now();
|
|
1415
|
-
const stats = workingGrid.getMemoryStats();
|
|
1416
1660
|
let committed = false;
|
|
1417
1661
|
let comparePrecisions;
|
|
1662
|
+
let skipRecalc = false;
|
|
1663
|
+
// Exactly-once stack-release contract: every settle path (return or
|
|
1664
|
+
// throw) releases the working-grid stack entry exactly one time via
|
|
1665
|
+
// _releaseWorkingGridRef (identity-checked, so plan-path grids that
|
|
1666
|
+
// were never pushed leave the stack untouched). When the caller passes
|
|
1667
|
+
// its result object (options.result), the push marker is cleared too —
|
|
1668
|
+
// the batch executor's catch block then cannot pop a second time for
|
|
1669
|
+
// the same grid.
|
|
1670
|
+
let popped = false;
|
|
1671
|
+
const releaseStackEntry = () => {
|
|
1672
|
+
if (popped)
|
|
1673
|
+
return;
|
|
1674
|
+
popped = true;
|
|
1675
|
+
this._releaseWorkingGridRef(workingGrid);
|
|
1676
|
+
if (options?.result) {
|
|
1677
|
+
options.result._workingGridPushed = false;
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1418
1680
|
try {
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
if (!preCommitGuard.canCommit) {
|
|
1433
|
-
this.logger.log(`[COW] ${preCommitGuard.reason}`, preCommitGuard.level || 'warn');
|
|
1434
|
-
this._clearWorkingGridRef();
|
|
1435
|
-
return false;
|
|
1436
|
-
}
|
|
1437
|
-
await this._gridLock.acquire(async () => {
|
|
1438
|
-
const lockCommitGuard = (0, validate_1.evaluateCommit)(workingGrid, {
|
|
1439
|
-
hasLock: true,
|
|
1681
|
+
const { skipRecalc: skipRecalcOpt } = this._normalizeCommitOptions(options);
|
|
1682
|
+
skipRecalc = skipRecalcOpt;
|
|
1683
|
+
const stats = workingGrid.getMemoryStats();
|
|
1684
|
+
try {
|
|
1685
|
+
comparePrecisions = this._getCowComparePrecisions();
|
|
1686
|
+
}
|
|
1687
|
+
catch (precisionErr) {
|
|
1688
|
+
this.logger.log(`[COW] ${(0, errors_1.getErrorMessage)(precisionErr)}`, 'error');
|
|
1689
|
+
releaseStackEntry();
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
const preCommitGuard = (0, validate_1.evaluateCommit)(workingGrid, {
|
|
1693
|
+
hasLock: false,
|
|
1440
1694
|
currentVersion: this._gridVersion,
|
|
1441
1695
|
masterGrid: this.orders,
|
|
1442
1696
|
comparePrecisions
|
|
1443
1697
|
});
|
|
1444
|
-
if (!
|
|
1445
|
-
this.logger.log(`[COW] ${
|
|
1446
|
-
|
|
1447
|
-
return;
|
|
1698
|
+
if (!preCommitGuard.canCommit) {
|
|
1699
|
+
this.logger.log(`[COW] ${preCommitGuard.reason}`, preCommitGuard.level || 'warn');
|
|
1700
|
+
releaseStackEntry();
|
|
1701
|
+
return false;
|
|
1448
1702
|
}
|
|
1449
|
-
this.
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1703
|
+
await this._gridLock.acquire(async () => {
|
|
1704
|
+
const lockCommitGuard = (0, validate_1.evaluateCommit)(workingGrid, {
|
|
1705
|
+
hasLock: true,
|
|
1706
|
+
currentVersion: this._gridVersion,
|
|
1707
|
+
masterGrid: this.orders,
|
|
1708
|
+
comparePrecisions
|
|
1709
|
+
});
|
|
1710
|
+
if (!lockCommitGuard.canCommit) {
|
|
1711
|
+
this.logger.log(`[COW] ${lockCommitGuard.reason}`, lockCommitGuard.level || 'warn');
|
|
1712
|
+
// Working grid ref is cleared exactly once below via the
|
|
1713
|
+
// !committed path (the callback returns without committing).
|
|
1714
|
+
return;
|
|
1456
1715
|
}
|
|
1716
|
+
this.logger.log(`[COW] Committing working grid: ${stats.size} orders, ${stats.modified} modified`, 'debug');
|
|
1717
|
+
const finalMap = workingGrid.toMap();
|
|
1718
|
+
// RC-4: Deep-freeze all modified orders before committing to master state
|
|
1719
|
+
// Ensures COW immutability invariants are maintained for all grid entries.
|
|
1720
|
+
for (const [, order] of finalMap.entries()) {
|
|
1721
|
+
if (order && !Object.isFrozen(order)) {
|
|
1722
|
+
(0, system_1.deepFreeze)(order);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
this.orders = Object.freeze(finalMap);
|
|
1726
|
+
this._setBoundary(workingBoundary);
|
|
1727
|
+
this._gridVersion++;
|
|
1728
|
+
committed = true;
|
|
1729
|
+
// Track orderIds from successful COW commits for recovery sync protection.
|
|
1730
|
+
// Build a new set from the committed finalMap and atomically swap to
|
|
1731
|
+
// avoid the intermediate empty state that clear() creates — a crash
|
|
1732
|
+
// during clear()+repopulate would lose all committed IDs.
|
|
1733
|
+
const newCommittedIds = new Set();
|
|
1734
|
+
for (const [, order] of finalMap.entries()) {
|
|
1735
|
+
if (order.orderId)
|
|
1736
|
+
newCommittedIds.add(order.orderId);
|
|
1737
|
+
}
|
|
1738
|
+
this._committedOrderIds = newCommittedIds;
|
|
1739
|
+
this._committedOrderIdsBuiltAt = Date.now();
|
|
1740
|
+
// Index caches invalidated by _gridVersion bump above — lazy getters recompute from this.orders
|
|
1741
|
+
});
|
|
1742
|
+
if (!committed) {
|
|
1743
|
+
releaseStackEntry();
|
|
1744
|
+
return false;
|
|
1457
1745
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
// Index caches invalidated by _gridVersion bump above — lazy getters recompute from this.orders
|
|
1474
|
-
});
|
|
1475
|
-
if (!committed) {
|
|
1476
|
-
this._clearWorkingGridRef();
|
|
1477
|
-
return false;
|
|
1478
|
-
}
|
|
1479
|
-
try {
|
|
1480
|
-
if (!skipRecalc) {
|
|
1481
|
-
// If the last accounting failure was due to stale accountTotals,
|
|
1482
|
-
// the commit proceeded without locking funds. Refresh accountTotals
|
|
1483
|
-
// before recalculateFunds() to avoid a false-positive drift recovery.
|
|
1484
|
-
if (this._lastAccountingFailure?.reason === 'stale') {
|
|
1485
|
-
this.logger.log('[COW] Stale accountTotals during commit; refreshing before recalculateFunds to avoid false recovery', 'warn');
|
|
1486
|
-
await this.fetchAccountTotals(this.accountId);
|
|
1746
|
+
try {
|
|
1747
|
+
if (!skipRecalc) {
|
|
1748
|
+
// If the last accounting failure was due to stale accountTotals,
|
|
1749
|
+
// the commit proceeded without locking funds. Refresh accountTotals
|
|
1750
|
+
// before recalculateFunds() to avoid a false-positive drift recovery.
|
|
1751
|
+
if (this._lastAccountingFailure?.reason === 'stale') {
|
|
1752
|
+
this.logger.log('[COW] Stale accountTotals during commit; refreshing before recalculateFunds to avoid false recovery', 'warn');
|
|
1753
|
+
await this.fetchAccountTotals(this.accountId);
|
|
1754
|
+
}
|
|
1755
|
+
await this.recalculateFunds();
|
|
1756
|
+
}
|
|
1757
|
+
const duration = Date.now() - startTime;
|
|
1758
|
+
this.logger.log(`[COW] Grid committed in ${duration}ms`, 'debug');
|
|
1759
|
+
if (stats.size > constants_1.COW_PERFORMANCE.GRID_MEMORY_WARNING) {
|
|
1760
|
+
this.logger.log(`[COW] Warning: Large grid size (${stats.size} orders). Peak memory: ~${Math.round(stats.estimatedBytes / 1024)}KB`, 'warn');
|
|
1487
1761
|
}
|
|
1488
|
-
await this.recalculateFunds();
|
|
1489
1762
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1763
|
+
catch (recalcErr) {
|
|
1764
|
+
this.logger.log(`[COW] Fund recalculation failed post-commit: ${(0, errors_1.getErrorMessage)(recalcErr)}`, 'error');
|
|
1765
|
+
this._recoveryState = { ...this._recoveryState, lastFailureAt: Date.now() };
|
|
1766
|
+
// Re-throw to signal callers that the commit is incomplete
|
|
1767
|
+
// (grid state committed, but fund state is stale).
|
|
1768
|
+
releaseStackEntry();
|
|
1769
|
+
throw recalcErr;
|
|
1494
1770
|
}
|
|
1771
|
+
releaseStackEntry();
|
|
1772
|
+
return true;
|
|
1495
1773
|
}
|
|
1496
|
-
catch (
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
//
|
|
1500
|
-
|
|
1501
|
-
throw
|
|
1774
|
+
catch (err) {
|
|
1775
|
+
// Unhandled commit failure (e.g. _gridLock acquisition timeout or
|
|
1776
|
+
// an in-lock commit error): nothing was committed, but the stack
|
|
1777
|
+
// entry must still be released exactly once.
|
|
1778
|
+
releaseStackEntry();
|
|
1779
|
+
throw err;
|
|
1502
1780
|
}
|
|
1503
|
-
finally {
|
|
1504
|
-
this._clearWorkingGridRef();
|
|
1505
|
-
}
|
|
1506
|
-
return true;
|
|
1507
1781
|
}
|
|
1508
1782
|
/**
|
|
1509
1783
|
* @param {Object} [options]
|
|
@@ -1544,7 +1818,7 @@ class OrderManager {
|
|
|
1544
1818
|
* swapping the live map and exposing it to concurrent readers.
|
|
1545
1819
|
* @returns {Promise<import('./types').PersistenceValidationResult>}
|
|
1546
1820
|
*/
|
|
1547
|
-
async persistGrid(snapshotOrders, recentFillKeys) {
|
|
1821
|
+
async persistGrid(snapshotOrders, recentFillKeys, fundSnapshot) {
|
|
1548
1822
|
if (this._gridPersistenceSuspendedReason) {
|
|
1549
1823
|
this.logger.log(`[PERSISTENCE-GATE] Skipping grid persistence while suspended: ${this._gridPersistenceSuspendedReason}`, 'warn');
|
|
1550
1824
|
return { isValid: true, skipped: true, suspended: true, reason: this._gridPersistenceSuspendedReason };
|
|
@@ -1554,7 +1828,20 @@ class OrderManager {
|
|
|
1554
1828
|
this.logger.log(`[PERSISTENCE-GATE] Skipping persistence of corrupted state: ${validation.reason}`, 'warn');
|
|
1555
1829
|
return validation;
|
|
1556
1830
|
}
|
|
1557
|
-
|
|
1831
|
+
// Capture fund state under _fundLock so the persisted snapshot is
|
|
1832
|
+
// internally consistent even when fills/fee settlement mutate
|
|
1833
|
+
// accountTotals concurrently (same TOCTOU fixed for recalculateGrid
|
|
1834
|
+
// at grid.ts:~1047). An explicit fundSnapshot (recalculateGrid) is
|
|
1835
|
+
// kept as an override — the caller captured it under the same lock
|
|
1836
|
+
// right after resetFunds.
|
|
1837
|
+
let effectiveFundSnapshot = fundSnapshot;
|
|
1838
|
+
if (!effectiveFundSnapshot) {
|
|
1839
|
+
effectiveFundSnapshot = await this._fundLock.acquire(async () => ({
|
|
1840
|
+
btsFeesOwed: this.funds.btsFeesOwed,
|
|
1841
|
+
accountTotals: this.accountTotals,
|
|
1842
|
+
}));
|
|
1843
|
+
}
|
|
1844
|
+
const persisted = await (0, system_1.persistGridSnapshot)(this, this.accountOrders, snapshotOrders, recentFillKeys, effectiveFundSnapshot);
|
|
1558
1845
|
if (persisted === false) {
|
|
1559
1846
|
this.logger.log(`[PERSIST] Grid persistence FAILED (disk full / permissions / write error)`, 'error');
|
|
1560
1847
|
// Keep the dirty flag set so the next flush retries.
|