dexbot 1.4.7 → 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 +237 -84
- 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 +1 -1
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +219 -101
- 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 +130 -56
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +2 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +333 -54
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +97 -4
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +377 -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 +329 -247
- 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
|
// ===============================================================================
|
|
@@ -281,6 +370,7 @@ class OrderManager {
|
|
|
281
370
|
_gridSidesUpdated;
|
|
282
371
|
_pauseFundRecalc;
|
|
283
372
|
_pauseFundRecalcWatchdog;
|
|
373
|
+
_fillBatchInFlight;
|
|
284
374
|
_pauseRecalcLogging;
|
|
285
375
|
_pauseRecalcLoggingWatchdog;
|
|
286
376
|
_throwOnIllegalState;
|
|
@@ -292,6 +382,7 @@ class OrderManager {
|
|
|
292
382
|
_pendingBroadcasts;
|
|
293
383
|
_committedOrderIds;
|
|
294
384
|
_committedOrderIdsBuiltAt;
|
|
385
|
+
_orderIdAssignedAt;
|
|
295
386
|
_gapSlots;
|
|
296
387
|
_gridDirtyAt;
|
|
297
388
|
_orphanFillsCreditedAt;
|
|
@@ -339,7 +430,9 @@ class OrderManager {
|
|
|
339
430
|
buy: { armed: true, lastTriggeredAt: 0 },
|
|
340
431
|
sell: { armed: true, lastTriggeredAt: 0 }
|
|
341
432
|
};
|
|
342
|
-
|
|
433
|
+
// Sync reset — _fundLock is not created until later in the constructor,
|
|
434
|
+
// and no concurrent access exists during construction.
|
|
435
|
+
this.accountant.resetFunds();
|
|
343
436
|
this.btsBalance = { free: 0, total: 0, locked: 0 };
|
|
344
437
|
this.targetSpreadCount = 0;
|
|
345
438
|
this.currentSpreadCount = 0;
|
|
@@ -374,6 +467,13 @@ class OrderManager {
|
|
|
374
467
|
this._gridSidesUpdated = new Set();
|
|
375
468
|
this._pauseFundRecalc = 0;
|
|
376
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;
|
|
377
477
|
this._pauseRecalcLogging = false;
|
|
378
478
|
this._pauseRecalcLoggingWatchdog = null;
|
|
379
479
|
this._throwOnIllegalState = false;
|
|
@@ -385,6 +485,7 @@ class OrderManager {
|
|
|
385
485
|
this._pendingBroadcasts = new Map();
|
|
386
486
|
this._committedOrderIds = new Set();
|
|
387
487
|
this._committedOrderIdsBuiltAt = 0;
|
|
488
|
+
this._orderIdAssignedAt = new Map();
|
|
388
489
|
this._gapSlots = 0;
|
|
389
490
|
this._gridDirtyAt = null;
|
|
390
491
|
this._orphanFillsCreditedAt = null;
|
|
@@ -422,6 +523,54 @@ class OrderManager {
|
|
|
422
523
|
? constants_1.REBALANCE_STATES.REBALANCING
|
|
423
524
|
: constants_1.REBALANCE_STATES.NORMAL;
|
|
424
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
|
+
}
|
|
425
574
|
_setRebalanceState(state) {
|
|
426
575
|
this._rebalanceState = state;
|
|
427
576
|
this.logger?.log(`[COW] Rebalance state: ${state}`, 'debug');
|
|
@@ -538,20 +687,29 @@ class OrderManager {
|
|
|
538
687
|
}
|
|
539
688
|
}
|
|
540
689
|
/**
|
|
541
|
-
*
|
|
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>}
|
|
542
694
|
*/
|
|
543
|
-
resetFunds() {
|
|
544
|
-
return this.
|
|
695
|
+
async resetFunds() {
|
|
696
|
+
return await this._fundLock.acquire(async () => {
|
|
697
|
+
return this.accountant.resetFunds();
|
|
698
|
+
});
|
|
545
699
|
}
|
|
546
700
|
async _deductFromChainFree(orderType, size, operation) {
|
|
547
701
|
if (!this.accountant)
|
|
548
702
|
return;
|
|
549
|
-
return await this.
|
|
703
|
+
return await this._fundLock.acquire(async () => {
|
|
704
|
+
return await this.accountant.tryDeductFromChainFree(orderType, size, operation);
|
|
705
|
+
});
|
|
550
706
|
}
|
|
551
707
|
async _addToChainFree(orderType, size, operation) {
|
|
552
708
|
if (!this.accountant)
|
|
553
709
|
return;
|
|
554
|
-
return await this.
|
|
710
|
+
return await this._fundLock.acquire(async () => {
|
|
711
|
+
return await this.accountant.addToChainFree(orderType, size, operation);
|
|
712
|
+
});
|
|
555
713
|
}
|
|
556
714
|
_getGridTotal(side) {
|
|
557
715
|
return (this.funds?.committed?.grid?.[side] || 0) + (this.funds?.virtual?.[side] || 0);
|
|
@@ -575,7 +733,7 @@ class OrderManager {
|
|
|
575
733
|
const size = toFiniteNumber(order.size);
|
|
576
734
|
if (size <= 0)
|
|
577
735
|
continue;
|
|
578
|
-
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);
|
|
579
737
|
if (isBuy)
|
|
580
738
|
committedBuy += size;
|
|
581
739
|
else
|
|
@@ -624,6 +782,66 @@ class OrderManager {
|
|
|
624
782
|
this.accountId = accountId;
|
|
625
783
|
await this._fetchAccountBalancesAndSetTotals();
|
|
626
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
|
+
}
|
|
627
845
|
async _fetchAccountBalancesAndSetTotals() {
|
|
628
846
|
return await this.sync.fetchAccountBalancesAndSetTotals();
|
|
629
847
|
}
|
|
@@ -639,7 +857,7 @@ class OrderManager {
|
|
|
639
857
|
async _setAccountTotals(totals) {
|
|
640
858
|
this.accountTotals = { ...(this.accountTotals || {}), ...totals, _lastFetchedAt: Date.now() };
|
|
641
859
|
if (!this.funds)
|
|
642
|
-
this.resetFunds();
|
|
860
|
+
await this.resetFunds();
|
|
643
861
|
await this._recalculateFunds();
|
|
644
862
|
if ((0, math_1.hasValidAccountTotals)(this.accountTotals, true) && typeof this._accountTotalsResolve === 'function') {
|
|
645
863
|
try {
|
|
@@ -735,7 +953,7 @@ class OrderManager {
|
|
|
735
953
|
this.logger?.log?.(`[MANAGER] pauseFundRecalc safety watchdog: resetting depth from ${this._pauseFundRecalc} to 0`, 'warn');
|
|
736
954
|
this._pauseFundRecalc = 0;
|
|
737
955
|
this._pauseFundRecalcWatchdog = null;
|
|
738
|
-
this.
|
|
956
|
+
this.recalculateFunds().catch((err) => {
|
|
739
957
|
this.logger?.log?.(`[MANAGER] Watchdog recalc failed: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
|
|
740
958
|
});
|
|
741
959
|
}
|
|
@@ -946,6 +1164,24 @@ class OrderManager {
|
|
|
946
1164
|
}
|
|
947
1165
|
const updatedOrder = (0, system_1.deepFreeze)({ ...nextOrder });
|
|
948
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
|
+
}
|
|
949
1185
|
const newMap = (0, system_1.cloneMap)(this.orders);
|
|
950
1186
|
newMap.set(id, updatedOrder);
|
|
951
1187
|
this.orders = Object.freeze(newMap);
|
|
@@ -1090,6 +1326,11 @@ class OrderManager {
|
|
|
1090
1326
|
const shouldRebalance = triggerFills.length > 0;
|
|
1091
1327
|
if (shouldRebalance) {
|
|
1092
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();
|
|
1093
1334
|
return rebalanceResult;
|
|
1094
1335
|
}
|
|
1095
1336
|
const workingGrid = new working_grid_1.WorkingGrid(this.orders, { baseVersion: this._gridVersion });
|
|
@@ -1351,10 +1592,13 @@ class OrderManager {
|
|
|
1351
1592
|
excludeIds
|
|
1352
1593
|
});
|
|
1353
1594
|
if (result.aborted) {
|
|
1354
|
-
|
|
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.
|
|
1355
1599
|
return result;
|
|
1356
1600
|
}
|
|
1357
|
-
this.
|
|
1601
|
+
this._pushWorkingGridRef(result.workingGrid, result);
|
|
1358
1602
|
return result;
|
|
1359
1603
|
}
|
|
1360
1604
|
_reconcileGridCOW(targetGrid, targetBoundary, workingGrid) {
|
|
@@ -1412,100 +1656,128 @@ class OrderManager {
|
|
|
1412
1656
|
return (0, validate_1.buildAbortedResult)(reason);
|
|
1413
1657
|
}
|
|
1414
1658
|
async _commitWorkingGrid(workingGrid, _workingIndexes, workingBoundary, options = {}) {
|
|
1415
|
-
const { skipRecalc } = this._normalizeCommitOptions(options);
|
|
1416
1659
|
const startTime = Date.now();
|
|
1417
|
-
const stats = workingGrid.getMemoryStats();
|
|
1418
1660
|
let committed = false;
|
|
1419
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
|
+
};
|
|
1420
1680
|
try {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
if (!preCommitGuard.canCommit) {
|
|
1435
|
-
this.logger.log(`[COW] ${preCommitGuard.reason}`, preCommitGuard.level || 'warn');
|
|
1436
|
-
this._clearWorkingGridRef();
|
|
1437
|
-
return false;
|
|
1438
|
-
}
|
|
1439
|
-
await this._gridLock.acquire(async () => {
|
|
1440
|
-
const lockCommitGuard = (0, validate_1.evaluateCommit)(workingGrid, {
|
|
1441
|
-
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,
|
|
1442
1694
|
currentVersion: this._gridVersion,
|
|
1443
1695
|
masterGrid: this.orders,
|
|
1444
1696
|
comparePrecisions
|
|
1445
1697
|
});
|
|
1446
|
-
if (!
|
|
1447
|
-
this.logger.log(`[COW] ${
|
|
1448
|
-
|
|
1449
|
-
return;
|
|
1698
|
+
if (!preCommitGuard.canCommit) {
|
|
1699
|
+
this.logger.log(`[COW] ${preCommitGuard.reason}`, preCommitGuard.level || 'warn');
|
|
1700
|
+
releaseStackEntry();
|
|
1701
|
+
return false;
|
|
1450
1702
|
}
|
|
1451
|
-
this.
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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;
|
|
1458
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;
|
|
1459
1745
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
// Index caches invalidated by _gridVersion bump above — lazy getters recompute from this.orders
|
|
1476
|
-
});
|
|
1477
|
-
if (!committed) {
|
|
1478
|
-
this._clearWorkingGridRef();
|
|
1479
|
-
return false;
|
|
1480
|
-
}
|
|
1481
|
-
try {
|
|
1482
|
-
if (!skipRecalc) {
|
|
1483
|
-
// If the last accounting failure was due to stale accountTotals,
|
|
1484
|
-
// the commit proceeded without locking funds. Refresh accountTotals
|
|
1485
|
-
// before recalculateFunds() to avoid a false-positive drift recovery.
|
|
1486
|
-
if (this._lastAccountingFailure?.reason === 'stale') {
|
|
1487
|
-
this.logger.log('[COW] Stale accountTotals during commit; refreshing before recalculateFunds to avoid false recovery', 'warn');
|
|
1488
|
-
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');
|
|
1489
1761
|
}
|
|
1490
|
-
await this.recalculateFunds();
|
|
1491
1762
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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;
|
|
1496
1770
|
}
|
|
1771
|
+
releaseStackEntry();
|
|
1772
|
+
return true;
|
|
1497
1773
|
}
|
|
1498
|
-
catch (
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
//
|
|
1502
|
-
|
|
1503
|
-
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;
|
|
1504
1780
|
}
|
|
1505
|
-
finally {
|
|
1506
|
-
this._clearWorkingGridRef();
|
|
1507
|
-
}
|
|
1508
|
-
return true;
|
|
1509
1781
|
}
|
|
1510
1782
|
/**
|
|
1511
1783
|
* @param {Object} [options]
|
|
@@ -1546,7 +1818,7 @@ class OrderManager {
|
|
|
1546
1818
|
* swapping the live map and exposing it to concurrent readers.
|
|
1547
1819
|
* @returns {Promise<import('./types').PersistenceValidationResult>}
|
|
1548
1820
|
*/
|
|
1549
|
-
async persistGrid(snapshotOrders, recentFillKeys) {
|
|
1821
|
+
async persistGrid(snapshotOrders, recentFillKeys, fundSnapshot) {
|
|
1550
1822
|
if (this._gridPersistenceSuspendedReason) {
|
|
1551
1823
|
this.logger.log(`[PERSISTENCE-GATE] Skipping grid persistence while suspended: ${this._gridPersistenceSuspendedReason}`, 'warn');
|
|
1552
1824
|
return { isValid: true, skipped: true, suspended: true, reason: this._gridPersistenceSuspendedReason };
|
|
@@ -1556,7 +1828,20 @@ class OrderManager {
|
|
|
1556
1828
|
this.logger.log(`[PERSISTENCE-GATE] Skipping persistence of corrupted state: ${validation.reason}`, 'warn');
|
|
1557
1829
|
return validation;
|
|
1558
1830
|
}
|
|
1559
|
-
|
|
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);
|
|
1560
1845
|
if (persisted === false) {
|
|
1561
1846
|
this.logger.log(`[PERSIST] Grid persistence FAILED (disk full / permissions / write error)`, 'error');
|
|
1562
1847
|
// Keep the dirty flag set so the next flush retries.
|