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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Exports a single Accountant class that manages all fund accounting operations.
|
|
8
8
|
*
|
|
9
9
|
* ===============================================================================
|
|
10
|
-
* TABLE OF CONTENTS - Accountant Class (
|
|
10
|
+
* TABLE OF CONTENTS - Accountant Class (19 methods)
|
|
11
11
|
* ===============================================================================
|
|
12
12
|
*
|
|
13
13
|
* CORE INITIALIZATION & RECALCULATION (2 methods)
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Called after any state change. Aggregates committed/available funds and triggers allocation.
|
|
20
20
|
*
|
|
21
21
|
* VERIFICATION & RECOVERY (3 methods - async, internal)
|
|
22
|
-
* 4. _verifyFundInvariants(mgr, chainFreeBuy, chainFreeSell, chainBuy, chainSell) - Verify fund tracking invariants
|
|
22
|
+
* 4. _verifyFundInvariants(mgr, chainFreeBuy, chainFreeSell, chainBuy, chainSell, actualBuy, actualSell) - Verify fund tracking invariants
|
|
23
23
|
* 5. _performStateRecovery(mgr) - Centralized state recovery (fetch + sync + validate) (async, internal)
|
|
24
24
|
* 6. _attemptFundRecovery(mgr, violationType) - Attempt immediate recovery from invariant violations (async, internal)
|
|
25
25
|
*
|
|
@@ -27,25 +27,26 @@
|
|
|
27
27
|
* 7. tryDeductFromChainFree(orderType, size, operation) - Atomically deduct from FREE portion
|
|
28
28
|
* 8. addToChainFree(orderType, size, operation) - Add amount back to optimistic chainFree balance
|
|
29
29
|
*
|
|
30
|
-
* BALANCE ADJUSTMENTS (
|
|
31
|
-
* 9. adjustTotalBalance(orderType, delta, operation) -
|
|
32
|
-
* 10.
|
|
33
|
-
* 11.
|
|
34
|
-
* 12.
|
|
30
|
+
* BALANCE ADJUSTMENTS (5 methods)
|
|
31
|
+
* 9. adjustTotalBalance(orderType, delta, operation) - Acquires _fundLock, adjusts total and free balances
|
|
32
|
+
* 10. _adjustTotalBalanceLocked(orderType, delta, operation) - PRIVATE: body of adjustTotalBalance, caller must hold _fundLock
|
|
33
|
+
* 11. _normalizeSideHint(sideHint) - Normalize side hint to standard key (internal)
|
|
34
|
+
* 12. _resolveOrderSide(order, fallbackOrder, explicitSideHint) - Resolve order side (internal)
|
|
35
|
+
* 13. updateOptimisticFreeBalance(oldOrder, newOrder, context, fee, skipAssetAccounting) - Update optimistic balance during transitions
|
|
35
36
|
*
|
|
36
37
|
* FEE MANAGEMENT (2 methods)
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* 14. deductBtsFees(requestedSide) - Deduct BTS fees using adjustTotalBalance with deferral strategy (async)
|
|
39
|
+
* 15. _deductFeesFromProceeds(assetSymbol, rawAmount, isMaker) - Deduct fees from fill proceeds (internal)
|
|
39
40
|
*
|
|
40
41
|
* FILL BALANCE TRACKING (1 method)
|
|
41
|
-
*
|
|
42
|
+
* 16. recordFillBalances(paysAsset, paysAmount, receivesAsset, receivesAmount, context) - Record fill proceeds (async)
|
|
42
43
|
*
|
|
43
44
|
* FILL PROCESSING (1 method)
|
|
44
|
-
*
|
|
45
|
+
* 17. processFillAccounting(fillOp, fillKey, persistenceMode) - Process fund impact of order fill (atomically updates accountTotals)
|
|
45
46
|
*
|
|
46
47
|
* RECOVERY & VALIDATION (2 methods)
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* 18. resetRecoveryState() - Reset recovery backoff and state
|
|
49
|
+
* 19. validateTargetGrid(targetGrid, mgr) - Validate target grid fund requirements
|
|
49
50
|
*
|
|
50
51
|
* ===============================================================================
|
|
51
52
|
* FUND STRUCTURE (managed by Accountant)
|
|
@@ -110,9 +111,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
110
111
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
111
112
|
const constants_1 = require("../constants");
|
|
112
113
|
const system_1 = require("./utils/system");
|
|
114
|
+
const order_1 = require("./utils/order");
|
|
113
115
|
const Format = __importStar(require("./format"));
|
|
114
116
|
const fundRegistry = __importStar(require("../fund_registry"));
|
|
115
117
|
const chainOrders = __importStar(require("../chain_orders"));
|
|
118
|
+
const chain_orders_1 = require("../chain_orders");
|
|
116
119
|
const math_1 = require("./utils/math");
|
|
117
120
|
const processed_fill_store_1 = require("./processed_fill_store");
|
|
118
121
|
const errors_1 = require("../utils/errors");
|
|
@@ -160,9 +163,9 @@ class Accountant {
|
|
|
160
163
|
* @param {Array<{orderType: string, delta: number, operation: string}>} balanceAdjustments
|
|
161
164
|
* @returns {void}
|
|
162
165
|
*/
|
|
163
|
-
_applyBalanceAdjustments(balanceAdjustments) {
|
|
166
|
+
async _applyBalanceAdjustments(balanceAdjustments) {
|
|
164
167
|
for (const adjustment of balanceAdjustments) {
|
|
165
|
-
this.adjustTotalBalance(adjustment.orderType, adjustment.delta, adjustment.operation);
|
|
168
|
+
await this.adjustTotalBalance(adjustment.orderType, adjustment.delta, adjustment.operation);
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
171
|
_getBtsOrderType() {
|
|
@@ -339,8 +342,20 @@ class Accountant {
|
|
|
339
342
|
const mgr = this.manager;
|
|
340
343
|
if (mgr._pauseFundRecalc > 0)
|
|
341
344
|
return;
|
|
342
|
-
if (!mgr.funds)
|
|
343
|
-
|
|
345
|
+
if (!mgr.funds) {
|
|
346
|
+
// Lazy funds init mutates mgr.funds wholesale, so run it under
|
|
347
|
+
// _fundLock when available (reentrant when the caller already
|
|
348
|
+
// holds it, e.g. manager.recalculateFunds). Guards the direct
|
|
349
|
+
// accountant.recalculateFunds() path against unlocked mutation.
|
|
350
|
+
if (mgr._fundLock) {
|
|
351
|
+
await mgr._fundLock.acquire(async () => {
|
|
352
|
+
this.resetFunds();
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
this.resetFunds();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
344
359
|
// Sync btsBalance from manager into funds for non-BTS pairs
|
|
345
360
|
if (mgr.btsBalance) {
|
|
346
361
|
mgr.funds.btsBalance = { ...mgr.funds.btsBalance, ...mgr.btsBalance };
|
|
@@ -364,8 +379,8 @@ class Accountant {
|
|
|
364
379
|
// - SPREAD type: derive from price relation to startPrice (market midpoint)
|
|
365
380
|
// * price < startPrice → BUY side (lower prices are bids)
|
|
366
381
|
// * price >= startPrice → SELL side (higher prices are asks)
|
|
367
|
-
const isBuy = order.type === constants_1.ORDER_TYPES.BUY || (order.type === constants_1.ORDER_TYPES.SPREAD && order.price
|
|
368
|
-
const isSell = order.type === constants_1.ORDER_TYPES.SELL || (order.type === constants_1.ORDER_TYPES.SPREAD && order.price
|
|
382
|
+
const isBuy = order.type === constants_1.ORDER_TYPES.BUY || (order.type === constants_1.ORDER_TYPES.SPREAD && (0, order_1.resolveSpreadOrderSide)(order.price, mgr.config.startPrice) === constants_1.ORDER_TYPES.BUY);
|
|
383
|
+
const isSell = order.type === constants_1.ORDER_TYPES.SELL || (order.type === constants_1.ORDER_TYPES.SPREAD && (0, order_1.resolveSpreadOrderSide)(order.price, mgr.config.startPrice) === constants_1.ORDER_TYPES.SELL);
|
|
369
384
|
if (isBuy) {
|
|
370
385
|
if (isActive) {
|
|
371
386
|
gridBuy += size;
|
|
@@ -413,10 +428,10 @@ class Accountant {
|
|
|
413
428
|
}
|
|
414
429
|
}
|
|
415
430
|
if (mgr._pauseFundRecalc === 0 && !mgr.isBootstrapping() && !mgr.isBroadcastingActive()) {
|
|
416
|
-
const snapshot = { chainFreeBuy, chainFreeSell, chainBuy, chainSell };
|
|
431
|
+
const snapshot = { chainFreeBuy, chainFreeSell, chainBuy, chainSell, actualBuy: mgr.accountTotals?.buy, actualSell: mgr.accountTotals?.sell };
|
|
417
432
|
const runVerification = (nextSnapshot) => {
|
|
418
433
|
this._isVerifyingInvariants = true;
|
|
419
|
-
this._verifyFundInvariants(mgr, nextSnapshot.chainFreeBuy, nextSnapshot.chainFreeSell, nextSnapshot.chainBuy, nextSnapshot.chainSell)
|
|
434
|
+
this._verifyFundInvariants(mgr, nextSnapshot.chainFreeBuy, nextSnapshot.chainFreeSell, nextSnapshot.chainBuy, nextSnapshot.chainSell, nextSnapshot.actualBuy, nextSnapshot.actualSell)
|
|
420
435
|
.catch((err) => {
|
|
421
436
|
mgr.logger?.log?.(`[RECOVERY] Verification error: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
|
|
422
437
|
})
|
|
@@ -455,7 +470,20 @@ class Accountant {
|
|
|
455
470
|
* @returns {void}
|
|
456
471
|
* @private
|
|
457
472
|
*/
|
|
458
|
-
async _verifyFundInvariants(mgr, chainFreeBuy, chainFreeSell, chainBuy, chainSell) {
|
|
473
|
+
async _verifyFundInvariants(mgr, chainFreeBuy, chainFreeSell, chainBuy, chainSell, actualBuy, actualSell) {
|
|
474
|
+
// Half-baked guard: while a fill batch is mid-accounting, the balance
|
|
475
|
+
// snapshot may already reflect the just-filled orders on-chain while
|
|
476
|
+
// the grid still counts them as committed (or the reverse during
|
|
477
|
+
// optimistic fill adjustments). Running Total = Free + Committed in
|
|
478
|
+
// that window produces a spurious CRITICAL equal to the batch size.
|
|
479
|
+
// The check is deferred until the batch settles (grid mutation +
|
|
480
|
+
// authoritative re-anchor) and is re-run by the resume recalculation.
|
|
481
|
+
if ((mgr._fillBatchInFlight ?? 0) > 0) {
|
|
482
|
+
if (mgr.logger?.level === 'debug') {
|
|
483
|
+
mgr.logger.log('[INVARIANT] Fill batch in flight; deferring fund-invariant check until batch settles.', 'debug');
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
459
487
|
const buyPrecision = mgr.assets?.assetB?.precision;
|
|
460
488
|
const sellPrecision = mgr.assets?.assetA?.precision;
|
|
461
489
|
if (!Number.isFinite(buyPrecision) || !Number.isFinite(sellPrecision)) {
|
|
@@ -484,7 +512,6 @@ class Accountant {
|
|
|
484
512
|
// actualBuy = mgr.accountTotals.buy (what blockchain says)
|
|
485
513
|
// If |actualBuy - expectedBuy| > tolerance → fund tracking corruption detected
|
|
486
514
|
const expectedBuy = chainFreeBuy + chainBuy;
|
|
487
|
-
const actualBuy = mgr.accountTotals?.buy;
|
|
488
515
|
const diffBuy = Math.abs((actualBuy ?? expectedBuy) - expectedBuy);
|
|
489
516
|
const allowedBuyTolerance = Math.max(precisionSlackBuy, (actualBuy || expectedBuy) * effectivePercentTolerance);
|
|
490
517
|
if (actualBuy !== null && actualBuy !== undefined && diffBuy > allowedBuyTolerance) {
|
|
@@ -495,7 +522,6 @@ class Accountant {
|
|
|
495
522
|
this._logThrottled('fund-invariant-buy', `CRITICAL: Fund invariant violation (BUY): blockchainTotal (${Format.formatAmountByPrecision(actualBuy, buyPrecision)}) != trackedTotal (${Format.formatAmountByPrecision(expectedBuy, buyPrecision)}) (diff: ${Format.formatAmountByPrecision(diffBuy, buyPrecision)}, allowed: ${Format.formatAmountByPrecision(allowedBuyTolerance, buyPrecision)}${orphanFillsAt != null ? ', orphan-fill buffer active' : ''})`, orphanFillsAt != null ? 'warn' : 'error');
|
|
496
523
|
}
|
|
497
524
|
const expectedSell = chainFreeSell + chainSell;
|
|
498
|
-
const actualSell = mgr.accountTotals?.sell;
|
|
499
525
|
const diffSell = Math.abs((actualSell ?? expectedSell) - expectedSell);
|
|
500
526
|
const allowedSellTolerance = Math.max(precisionSlackSell, (actualSell || expectedSell) * effectivePercentTolerance);
|
|
501
527
|
if (actualSell !== null && actualSell !== undefined && diffSell > allowedSellTolerance) {
|
|
@@ -589,7 +615,25 @@ class Accountant {
|
|
|
589
615
|
// proceeds, so the temporary invariant tolerance is no longer needed.
|
|
590
616
|
mgr._orphanFillsCreditedAt = null;
|
|
591
617
|
// 2. Sync from open orders
|
|
592
|
-
|
|
618
|
+
// An empty/truncated read is ambiguous — the account is either genuinely
|
|
619
|
+
// empty or the node is lagging/capped (fresh orders omitted). Running
|
|
620
|
+
// syncFromOpenOrders on it would let pass-1 phantom cleanup virtualize
|
|
621
|
+
// ACTIVE/PARTIAL slots that are live on chain, after which the next
|
|
622
|
+
// cycle re-creates them as duplicates. Skip the sync and defer to the
|
|
623
|
+
// next reconcile cycle, mirroring the other recovery-read guards.
|
|
624
|
+
const openOrders = await (0, chain_orders_1.readOpenOrdersGuarded)(chainOrders, accountRef, {
|
|
625
|
+
log: (message, level) => mgr.logger?.log?.(message, level),
|
|
626
|
+
label: 'RECOVERY',
|
|
627
|
+
detail: 'during state recovery',
|
|
628
|
+
deferEmpty: true,
|
|
629
|
+
});
|
|
630
|
+
if (openOrders === null) {
|
|
631
|
+
return {
|
|
632
|
+
isValid: false,
|
|
633
|
+
deferred: true,
|
|
634
|
+
reason: 'Recovery sync skipped: ambiguous chain read (truncated or empty) - node may be lagging; deferring to next reconcile cycle',
|
|
635
|
+
};
|
|
636
|
+
}
|
|
593
637
|
// Recovery runs after fetchAccountTotals() has refreshed authoritative balances
|
|
594
638
|
// from chain. During this pass we only want to reconcile grid structure/order
|
|
595
639
|
// mapping against open orders; re-applying optimistic accounting deltas here
|
|
@@ -693,6 +737,19 @@ class Accountant {
|
|
|
693
737
|
mgr.logger?.log?.(`[RECOVERY] ${violationType} - attempting state recovery (attempt ${state.attemptCount}${hasAttemptLimit ? `/${maxAttemptsRaw}` : ''})...`, 'warn');
|
|
694
738
|
try {
|
|
695
739
|
const validation = await this._performStateRecovery(mgr);
|
|
740
|
+
if (validation?.deferred) {
|
|
741
|
+
// Ambiguous (empty/truncated) chain read: the recovery was
|
|
742
|
+
// DEFERRED, not failed. Roll back the attempt increment so an
|
|
743
|
+
// ambiguous snapshot can never burn the attempt budget toward
|
|
744
|
+
// MAX_RECOVERY_ATTEMPTS (which blocks all CREATEs until the
|
|
745
|
+
// next fill/sync cycle), and skip the structural resync — a
|
|
746
|
+
// full grid reset driven by an ambiguous read would
|
|
747
|
+
// virtualize live slots via pass-1 phantom cleanup.
|
|
748
|
+
state.attemptCount = Math.max(0, state.attemptCount - 1);
|
|
749
|
+
state.lastFailureAt = 0;
|
|
750
|
+
mgr.logger?.log?.(`[RECOVERY] State recovery deferred (${validation.reason}); attempt not counted`, 'warn');
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
696
753
|
if (validation.isValid) {
|
|
697
754
|
mgr.logger?.log?.('[RECOVERY] State recovery succeeded', 'info');
|
|
698
755
|
state.structuralResyncRequested = false;
|
|
@@ -840,23 +897,22 @@ class Accountant {
|
|
|
840
897
|
// Determine orientation
|
|
841
898
|
if (paysAsset === assetA) {
|
|
842
899
|
// Bot paid assetA (Selling assetA, buying assetB)
|
|
843
|
-
this.
|
|
844
|
-
this.
|
|
900
|
+
this._adjustTotalBalanceLocked(constants_1.ORDER_TYPES.SELL, -paysAmount, `${context}-pays`);
|
|
901
|
+
this._adjustTotalBalanceLocked(constants_1.ORDER_TYPES.BUY, receivesAmount, `${context}-receives`);
|
|
845
902
|
}
|
|
846
903
|
else {
|
|
847
904
|
// Bot paid assetB (Buying assetA, selling assetB)
|
|
848
|
-
this.
|
|
849
|
-
this.
|
|
905
|
+
this._adjustTotalBalanceLocked(constants_1.ORDER_TYPES.BUY, -paysAmount, `${context}-pays`);
|
|
906
|
+
this._adjustTotalBalanceLocked(constants_1.ORDER_TYPES.SELL, receivesAmount, `${context}-receives`);
|
|
850
907
|
}
|
|
851
908
|
});
|
|
852
909
|
}
|
|
853
910
|
/**
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
* @param {string} operation - Context for logging
|
|
911
|
+
* PRIVATE: Must be called while holding _fundLock.
|
|
912
|
+
* Body of adjustTotalBalance; takes the lock externally for clean
|
|
913
|
+
* nesting in callers that already hold the lock (e.g. recordFillBalances).
|
|
858
914
|
*/
|
|
859
|
-
|
|
915
|
+
_adjustTotalBalanceLocked(orderType, delta, operation) {
|
|
860
916
|
const mgr = this.manager;
|
|
861
917
|
const isBuy = (orderType === constants_1.ORDER_TYPES.BUY);
|
|
862
918
|
const freeKey = isBuy ? 'buyFree' : 'sellFree';
|
|
@@ -876,6 +932,16 @@ class Accountant {
|
|
|
876
932
|
mgr.logger.log(`[ACCOUNTING] ${totalKey} ${delta >= 0 ? '+' : ''}${Format.formatAmount8(delta)} (${operation}) -> Total: ${Format.formatAmount8(mgr.accountTotals[totalKey])}, Free: ${Format.formatAmount8(mgr.accountTotals[freeKey])}`, 'debug');
|
|
877
933
|
}
|
|
878
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* PUBLIC API: Acquires _fundLock.
|
|
937
|
+
* Adjust both total and free balances (for fills, fees, deposits).
|
|
938
|
+
* @returns {Promise<void>}
|
|
939
|
+
*/
|
|
940
|
+
async adjustTotalBalance(orderType, delta, operation) {
|
|
941
|
+
await this.manager._fundLock.acquire(async () => {
|
|
942
|
+
this._adjustTotalBalanceLocked(orderType, delta, operation);
|
|
943
|
+
});
|
|
944
|
+
}
|
|
879
945
|
/**
|
|
880
946
|
* Normalize side hint to standard ORDER_TYPES constant.
|
|
881
947
|
* @param {string|null} sideHint - Side hint ('buy', 'sell', ORDER_TYPES.BUY, ORDER_TYPES.SELL, or null)
|
|
@@ -933,92 +999,142 @@ class Accountant {
|
|
|
933
999
|
const mgr = this.manager;
|
|
934
1000
|
if (!oldOrder || !newOrder)
|
|
935
1001
|
return;
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1002
|
+
// If this transition will LOCK capital, ensure a fresh accountTotals snapshot
|
|
1003
|
+
// BEFORE acquiring _fundLock. The refresh is a live chain RPC (30s timeout,
|
|
1004
|
+
// 3 retries, then node failover) — running it while holding _fundLock would
|
|
1005
|
+
// stall every other fund-critical waiter (deductBtsFees, setAccountTotals,
|
|
1006
|
+
// persistGrid) and, past the 30s acquire timeout, force them to throw instead
|
|
1007
|
+
// of deferring. Only the plain mutation runs under the lock.
|
|
1008
|
+
let preLockRefreshFailed = false;
|
|
941
1009
|
if (!skipAssetAccounting) {
|
|
942
1010
|
const oldIsActive = (oldOrder.state === constants_1.ORDER_STATES.ACTIVE || oldOrder.state === constants_1.ORDER_STATES.PARTIAL);
|
|
943
1011
|
const newIsActive = (newOrder.state === constants_1.ORDER_STATES.ACTIVE || newOrder.state === constants_1.ORDER_STATES.PARTIAL);
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
if (mgr.logger && mgr.logger.level === 'debug') {
|
|
955
|
-
mgr.logger.log(`[ACCOUNTING] updateOptimisticFreeBalance: id=${newOrder.id}, type=${newOrder.type}, ` +
|
|
956
|
-
`state=${oldOrder.state}->${newOrder.state}, ` +
|
|
957
|
-
`size=${Format.formatSizeByOrderType(oldSize, sideForPrecision ?? '', mgr.assets)}->${Format.formatSizeByOrderType(newSize, sideForPrecision ?? '', mgr.assets)}, ` +
|
|
958
|
-
`delta=${Format.formatSizeByOrderType(commitmentDelta, sideForPrecision ?? '', mgr.assets)}, context=${context}`, 'debug');
|
|
1012
|
+
const willLock = (newIsActive ? toFiniteNumber(newOrder.size) : 0) - (oldIsActive ? toFiniteNumber(oldOrder.size) : 0) > 0;
|
|
1013
|
+
if (willLock) {
|
|
1014
|
+
try {
|
|
1015
|
+
const pre = await mgr.refreshAccountTotalsIfStale();
|
|
1016
|
+
preLockRefreshFailed = !pre.ok;
|
|
1017
|
+
}
|
|
1018
|
+
catch (err) {
|
|
1019
|
+
preLockRefreshFailed = true;
|
|
1020
|
+
mgr.logger?.log?.(`[ACCOUNTING] pre-lock accountTotals refresh error: ${(0, errors_1.getErrorMessage)(err)}`, 'warn');
|
|
1021
|
+
}
|
|
959
1022
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1023
|
+
}
|
|
1024
|
+
return await mgr._fundLock.acquire(async () => {
|
|
1025
|
+
// Ensure a copy: _resolveBtsFeeLifecycle mutates btsFeeState on
|
|
1026
|
+
// newOrder (line 230), but the caller may pass a frozen master-grid order.
|
|
1027
|
+
if (Object.isFrozen(newOrder)) {
|
|
1028
|
+
newOrder = { ...newOrder };
|
|
1029
|
+
}
|
|
1030
|
+
if (!skipAssetAccounting) {
|
|
1031
|
+
const oldIsActive = (oldOrder.state === constants_1.ORDER_STATES.ACTIVE || oldOrder.state === constants_1.ORDER_STATES.PARTIAL);
|
|
1032
|
+
const newIsActive = (newOrder.state === constants_1.ORDER_STATES.ACTIVE || newOrder.state === constants_1.ORDER_STATES.PARTIAL);
|
|
1033
|
+
const oldSize = toFiniteNumber(oldOrder.size);
|
|
1034
|
+
const newSize = toFiniteNumber(newOrder.size);
|
|
1035
|
+
// 1. Handle Capital Commitment (Moves between FREE and LOCKED)
|
|
1036
|
+
// For COMMITMENT: Use GRID state (isActive), not blockchain ID
|
|
1037
|
+
const oldGridCommitted = oldIsActive ? oldSize : 0;
|
|
1038
|
+
const newGridCommitted = newIsActive ? newSize : 0;
|
|
1039
|
+
const commitmentDelta = newGridCommitted - oldGridCommitted;
|
|
1040
|
+
const newSideType = this._resolveOrderSide(newOrder, oldOrder);
|
|
1041
|
+
const oldSideType = this._resolveOrderSide(oldOrder, newOrder);
|
|
1042
|
+
const sideForPrecision = newSideType || oldSideType;
|
|
1043
|
+
if (mgr.logger && mgr.logger.level === 'debug') {
|
|
1044
|
+
mgr.logger.log(`[ACCOUNTING] updateOptimisticFreeBalance: id=${newOrder.id}, type=${newOrder.type}, ` +
|
|
1045
|
+
`state=${oldOrder.state}->${newOrder.state}, ` +
|
|
1046
|
+
`size=${Format.formatSizeByOrderType(oldSize, sideForPrecision ?? '', mgr.assets)}->${Format.formatSizeByOrderType(newSize, sideForPrecision ?? '', mgr.assets)}, ` +
|
|
1047
|
+
`delta=${Format.formatSizeByOrderType(commitmentDelta, sideForPrecision ?? '', mgr.assets)}, context=${context}`, 'debug');
|
|
1048
|
+
}
|
|
1049
|
+
if (commitmentDelta > 0) {
|
|
1050
|
+
// Lock capital: move from Free to Committed
|
|
1051
|
+
const commitmentSide = newSideType || newOrder.type;
|
|
1052
|
+
let result = await this.tryDeductFromChainFree(commitmentSide, commitmentDelta, `${context}`);
|
|
1053
|
+
// Fix staleness in the first place: a stale snapshot refused
|
|
1054
|
+
// the lock. When the pre-lock refresh was skipped (not needed)
|
|
1055
|
+
// or is in flight and this deduction still races a fresh window,
|
|
1056
|
+
// refresh accountTotals from chain and retry once. But if the
|
|
1057
|
+
// pre-lock refresh already FAILED, the chain is unhealthy — do
|
|
1058
|
+
// NOT re-run the 30s/3-retry/node-failover RPC while holding
|
|
1059
|
+
// _fundLock (that would stall every other fund-critical waiter
|
|
1060
|
+
// exactly like the regression this rework eliminated). Fall
|
|
1061
|
+
// through to the failure/recovery path below instead.
|
|
1062
|
+
if (!result.ok && result.reason === 'stale' && !preLockRefreshFailed) {
|
|
1063
|
+
mgr.logger?.log?.(`[ACCOUNTING] Stale accountTotals during ${context}; refreshing from chain before retrying optimistic lock.`, 'warn');
|
|
1064
|
+
const refresh = await mgr.refreshAccountTotalsIfStale();
|
|
1065
|
+
if (refresh.ok) {
|
|
1066
|
+
result = await this.tryDeductFromChainFree(commitmentSide, commitmentDelta, `${context} (post-refresh retry)`);
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
mgr.logger?.log?.(`[ACCOUNTING] accountTotals refresh failed (${refresh.reason}); cannot retry optimistic lock of ${Format.formatAmount8(commitmentDelta)} for ${commitmentSide}.`, 'warn');
|
|
986
1070
|
}
|
|
987
1071
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
mgr.logger?.log?.(`[RECOVERY] Immediate recovery scheduling failed: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
|
|
995
|
-
mgr._recoveryState = { ...mgr._recoveryState, lastFailureAt: Date.now() };
|
|
996
|
-
})
|
|
997
|
-
.finally(() => {
|
|
998
|
-
mgr._pendingRecovery = null;
|
|
999
|
-
});
|
|
1072
|
+
else if (!result.ok && result.reason === 'stale' && preLockRefreshFailed) {
|
|
1073
|
+
// The pre-lock refresh already failed (chain unhealthy) so we
|
|
1074
|
+
// refuse to re-run the blocking RPC under _fundLock. Fall
|
|
1075
|
+
// through to the stale failure/recovery path below; the batch
|
|
1076
|
+
// stays accounted for and recovery is scheduled.
|
|
1077
|
+
mgr.logger?.log?.(`[ACCOUNTING] Stale accountTotals during ${context}; pre-lock refresh failed, skipping in-lock refresh to avoid holding _fundLock across a blocking chain RPC.`, 'warn');
|
|
1000
1078
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1079
|
+
if (!result.ok) {
|
|
1080
|
+
const failure = {
|
|
1081
|
+
code: result.reason === 'stale' ? 'ACCOUNTING_STALE_ACCOUNT_TOTALS' : 'ACCOUNTING_COMMITMENT_FAILED',
|
|
1082
|
+
side: commitmentSide,
|
|
1083
|
+
amount: commitmentDelta,
|
|
1084
|
+
context,
|
|
1085
|
+
reason: result.reason,
|
|
1086
|
+
at: Date.now()
|
|
1087
|
+
};
|
|
1088
|
+
mgr._lastAccountingFailure = failure;
|
|
1089
|
+
if (result.reason === 'stale') {
|
|
1090
|
+
mgr.logger?.log?.(`[ACCOUNTING] Stale accountTotals: skipping optimistic lock of ${Format.formatAmount8(commitmentDelta)} for ${commitmentSide} during ${context}. Recovery scheduled.`, 'warn');
|
|
1091
|
+
// Schedule recovery but DON'T throw — staleness is transient.
|
|
1092
|
+
// The batch already confirmed on-chain; aborting the commit
|
|
1093
|
+
// would orphan a successful broadcast from the grid state.
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
mgr.logger?.log?.(`[ACCOUNTING] CRITICAL: Failed to lock ${Format.formatAmount8(commitmentDelta)} for ${commitmentSide} during ${context}. Scheduling recovery.`, 'error');
|
|
1097
|
+
if (mgr._throwOnIllegalState) {
|
|
1098
|
+
const err = new Error(`CRITICAL ACCOUNTING STATE: failed to lock ${Format.formatAmount8(commitmentDelta)} ${commitmentSide} during ${context}`);
|
|
1099
|
+
err.code = 'ACCOUNTING_COMMITMENT_FAILED';
|
|
1100
|
+
throw err;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
// Promote recovery from fire-and-forget to a stored promise.
|
|
1104
|
+
// Subsequent callers can await the in-flight recovery before
|
|
1105
|
+
// attempting new deductions, reducing redundant recovery cycles.
|
|
1106
|
+
if (!mgr._pendingRecovery) {
|
|
1107
|
+
mgr._pendingRecovery = this._attemptFundRecovery(mgr, 'Optimistic commitment deduction failure')
|
|
1108
|
+
.catch((err) => {
|
|
1109
|
+
mgr.logger?.log?.(`[RECOVERY] Immediate recovery scheduling failed: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
|
|
1110
|
+
mgr._recoveryState = { ...mgr._recoveryState, lastFailureAt: Date.now() };
|
|
1111
|
+
})
|
|
1112
|
+
.finally(() => {
|
|
1113
|
+
mgr._pendingRecovery = null;
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
mgr.logger?.log?.(`[ACCOUNTING] Recovery already in-flight; waiting for existing recovery to complete before retry deduction of ${Format.formatAmount8(commitmentDelta)} for ${commitmentSide}`, 'warn');
|
|
1118
|
+
}
|
|
1003
1119
|
}
|
|
1004
1120
|
}
|
|
1121
|
+
else if (commitmentDelta < 0) {
|
|
1122
|
+
// Release capital: move from Committed back to Free
|
|
1123
|
+
const releaseSide = oldSideType || oldOrder.type;
|
|
1124
|
+
await this.addToChainFree(releaseSide, Math.abs(commitmentDelta), `${context}`);
|
|
1125
|
+
}
|
|
1005
1126
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
const btsOrderType = this._getBtsOrderType();
|
|
1016
|
-
if (btsOrderType) {
|
|
1017
|
-
const { balanceDelta } = this._resolveBtsFeeLifecycle(oldOrder, newOrder, context, fee);
|
|
1018
|
-
if (Math.abs(balanceDelta) > 0) {
|
|
1019
|
-
this.adjustTotalBalance(btsOrderType, balanceDelta, `${context}-fee`);
|
|
1127
|
+
// 2. Handle BTS blockchain fee lifecycle.
|
|
1128
|
+
// BitShares stores create/update fees as order.deferred_fee and later
|
|
1129
|
+
// refunds or charges that deferred fee on fill, update, or cancel.
|
|
1130
|
+
const btsOrderType = this._getBtsOrderType();
|
|
1131
|
+
if (btsOrderType) {
|
|
1132
|
+
const { balanceDelta } = this._resolveBtsFeeLifecycle(oldOrder, newOrder, context, fee);
|
|
1133
|
+
if (Math.abs(balanceDelta) > 0) {
|
|
1134
|
+
await this.adjustTotalBalance(btsOrderType, balanceDelta, `${context}-fee`);
|
|
1135
|
+
}
|
|
1020
1136
|
}
|
|
1021
|
-
}
|
|
1137
|
+
});
|
|
1022
1138
|
}
|
|
1023
1139
|
/**
|
|
1024
1140
|
* Deduct BTS fees using adjustTotalBalance.
|
|
@@ -1032,10 +1148,9 @@ class Accountant {
|
|
|
1032
1148
|
*/
|
|
1033
1149
|
async deductBtsFees(requestedSide = null) {
|
|
1034
1150
|
const mgr = this.manager;
|
|
1035
|
-
// Early returns for no work needed
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (!mgr.accountTotals)
|
|
1151
|
+
// Early returns for no work needed (existence checks only — value reads
|
|
1152
|
+
// happen inside _fundLock below so overlapping calls cannot double-deduct)
|
|
1153
|
+
if (!mgr.funds || !mgr.accountTotals)
|
|
1039
1154
|
return;
|
|
1040
1155
|
const btsSide = (0, math_1.getBtsSide)(mgr.config?.assetA, mgr.config?.assetB);
|
|
1041
1156
|
const normalizedRequestedSide = (requestedSide === 'buy' || requestedSide === 'sell') ? requestedSide : null;
|
|
@@ -1046,26 +1161,37 @@ class Accountant {
|
|
|
1046
1161
|
}
|
|
1047
1162
|
if (!side)
|
|
1048
1163
|
return;
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1164
|
+
let settled = false;
|
|
1165
|
+
// ATOMIC: read btsFeesOwed + sufficiency check + deduction + reset in one
|
|
1166
|
+
// lock. Two overlapping calls then serialize: the second sees fees=0 and
|
|
1167
|
+
// no-ops instead of double-deducting a stale amount.
|
|
1168
|
+
await mgr._fundLock.acquire(async () => {
|
|
1169
|
+
const fees = mgr.funds.btsFeesOwed;
|
|
1170
|
+
if (!fees || fees <= 0)
|
|
1171
|
+
return;
|
|
1172
|
+
const orderType = (side === 'buy') ? constants_1.ORDER_TYPES.BUY : constants_1.ORDER_TYPES.SELL;
|
|
1173
|
+
const freeKey = (side === 'buy') ? 'buyFree' : 'sellFree';
|
|
1174
|
+
const chainFree = mgr.accountTotals[freeKey] || 0;
|
|
1175
|
+
// SUFFICIENCY CHECK: Defer if insufficient funds
|
|
1176
|
+
if (chainFree < fees) {
|
|
1177
|
+
if (mgr.logger && mgr.logger.level === 'debug') {
|
|
1178
|
+
mgr.logger.log(`[BTS-FEE] Deferring settlement: need ${Format.formatAmount8(fees)}, have ${Format.formatAmount8(chainFree)}`, 'debug');
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
this._adjustTotalBalanceLocked(orderType, -fees, 'bts-fee-settlement');
|
|
1183
|
+
mgr.funds.btsFeesOwed = 0;
|
|
1184
|
+
settled = true;
|
|
1055
1185
|
if (mgr.logger && mgr.logger.level === 'debug') {
|
|
1056
|
-
mgr.logger.log(`[BTS-FEE]
|
|
1186
|
+
mgr.logger.log(`[BTS-FEE] Settled: ${Format.formatAmount8(fees)} BTS`, 'debug');
|
|
1057
1187
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
if (
|
|
1063
|
-
mgr.
|
|
1188
|
+
});
|
|
1189
|
+
// Recalculate funds only when something was actually settled — the
|
|
1190
|
+
// no-op calls (no fees owed, or deferred for insufficient funds) skip
|
|
1191
|
+
// the full grid iteration.
|
|
1192
|
+
if (settled) {
|
|
1193
|
+
await mgr.recalculateFunds();
|
|
1064
1194
|
}
|
|
1065
|
-
// Reset fees after successful settlement
|
|
1066
|
-
mgr.funds.btsFeesOwed = 0;
|
|
1067
|
-
// Recalculate funds to update all tracking metrics
|
|
1068
|
-
await mgr.recalculateFunds();
|
|
1069
1195
|
}
|
|
1070
1196
|
/**
|
|
1071
1197
|
* Validate that a proposed Target Grid fits within the account's total available funds.
|
|
@@ -1233,23 +1359,39 @@ class Accountant {
|
|
|
1233
1359
|
}
|
|
1234
1360
|
}
|
|
1235
1361
|
const processedFillStore = this.manager.processedFillStore;
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1238
|
-
//
|
|
1362
|
+
// Queue the dedup key before applying balance adjustments. This ensures
|
|
1363
|
+
// persist failures are caught before any balance mutation. The queue entry
|
|
1364
|
+
// is flushed only after balance adjustments succeed below.
|
|
1239
1365
|
if (fillKey && processedFillStore) {
|
|
1240
1366
|
try {
|
|
1241
|
-
|
|
1367
|
+
// Queue without flush — flush follows after balance adjustments succeed.
|
|
1368
|
+
await processedFillStore.persist(fillKey, processedAt || Date.now(), { mode: processed_fill_store_1.PROCESSED_FILL_PERSISTENCE_MODES.MANUAL });
|
|
1242
1369
|
}
|
|
1243
1370
|
catch (err) {
|
|
1371
|
+
mgr.logger?.log?.(`[FILL-DEDUP] Failed to queue fill ${fillKey}: ${(0, errors_1.getErrorMessage)(err)}`, 'warn');
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
await this._applyBalanceAdjustments(balanceAdjustments);
|
|
1376
|
+
// Flush the queued dedup key now that balance adjustments succeeded.
|
|
1377
|
+
// For IMMEDIATE mode, this persists durably. For BATCHED mode, the
|
|
1378
|
+
// key remains queued for the caller's batch flush.
|
|
1379
|
+
if (fillKey && processedFillStore) {
|
|
1380
|
+
try {
|
|
1244
1381
|
if (persistenceMode === processed_fill_store_1.PROCESSED_FILL_PERSISTENCE_MODES.IMMEDIATE) {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1382
|
+
await processedFillStore.flush('fill-persist', { throwOnError: true });
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
catch (err) {
|
|
1386
|
+
// Roll back balance adjustments since the dedup key was not persisted.
|
|
1387
|
+
// This keeps the invariant: dedup key persisted ⇔ balance adjustments applied.
|
|
1388
|
+
for (const adj of balanceAdjustments) {
|
|
1389
|
+
await this.adjustTotalBalance(adj.orderType, -adj.delta, 'rollback-' + adj.operation);
|
|
1248
1390
|
}
|
|
1249
|
-
|
|
1391
|
+
processedFillStore.discard(fillKey, processedAt || Date.now());
|
|
1392
|
+
throw err;
|
|
1250
1393
|
}
|
|
1251
1394
|
}
|
|
1252
|
-
this._applyBalanceAdjustments(balanceAdjustments);
|
|
1253
1395
|
if (fillKey) {
|
|
1254
1396
|
tracker.set(fillKey, processedAt || Date.now());
|
|
1255
1397
|
// Prune entries beyond the retention horizon to prevent unbounded growth.
|