dexbot 1.4.7 → 1.4.9
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 +4 -5
- 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 +3 -0
- package/dist/dexbot.d.ts.map +1 -1
- package/dist/dexbot.js +81 -28
- 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/credit_runtime.d.ts.map +1 -1
- package/dist/modules/credit_runtime.js +26 -0
- package/dist/modules/credit_runtime.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/dist/unlock.d.ts +4 -4
- package/dist/unlock.js +6 -6
- package/dist/unlock.js.map +1 -1
- package/package.json +2 -1
- package/scripts/clear-all.sh +4 -4
- package/scripts/clear-logs.sh +4 -4
|
@@ -111,9 +111,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
111
111
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
112
112
|
const constants_1 = require("../constants");
|
|
113
113
|
const system_1 = require("./utils/system");
|
|
114
|
+
const order_1 = require("./utils/order");
|
|
114
115
|
const Format = __importStar(require("./format"));
|
|
115
116
|
const fundRegistry = __importStar(require("../fund_registry"));
|
|
116
117
|
const chainOrders = __importStar(require("../chain_orders"));
|
|
118
|
+
const chain_orders_1 = require("../chain_orders");
|
|
117
119
|
const math_1 = require("./utils/math");
|
|
118
120
|
const processed_fill_store_1 = require("./processed_fill_store");
|
|
119
121
|
const errors_1 = require("../utils/errors");
|
|
@@ -340,8 +342,20 @@ class Accountant {
|
|
|
340
342
|
const mgr = this.manager;
|
|
341
343
|
if (mgr._pauseFundRecalc > 0)
|
|
342
344
|
return;
|
|
343
|
-
if (!mgr.funds)
|
|
344
|
-
|
|
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
|
+
}
|
|
345
359
|
// Sync btsBalance from manager into funds for non-BTS pairs
|
|
346
360
|
if (mgr.btsBalance) {
|
|
347
361
|
mgr.funds.btsBalance = { ...mgr.funds.btsBalance, ...mgr.btsBalance };
|
|
@@ -365,8 +379,8 @@ class Accountant {
|
|
|
365
379
|
// - SPREAD type: derive from price relation to startPrice (market midpoint)
|
|
366
380
|
// * price < startPrice → BUY side (lower prices are bids)
|
|
367
381
|
// * price >= startPrice → SELL side (higher prices are asks)
|
|
368
|
-
const isBuy = order.type === constants_1.ORDER_TYPES.BUY || (order.type === constants_1.ORDER_TYPES.SPREAD && order.price
|
|
369
|
-
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);
|
|
370
384
|
if (isBuy) {
|
|
371
385
|
if (isActive) {
|
|
372
386
|
gridBuy += size;
|
|
@@ -457,6 +471,19 @@ class Accountant {
|
|
|
457
471
|
* @private
|
|
458
472
|
*/
|
|
459
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
|
+
}
|
|
460
487
|
const buyPrecision = mgr.assets?.assetB?.precision;
|
|
461
488
|
const sellPrecision = mgr.assets?.assetA?.precision;
|
|
462
489
|
if (!Number.isFinite(buyPrecision) || !Number.isFinite(sellPrecision)) {
|
|
@@ -588,7 +615,25 @@ class Accountant {
|
|
|
588
615
|
// proceeds, so the temporary invariant tolerance is no longer needed.
|
|
589
616
|
mgr._orphanFillsCreditedAt = null;
|
|
590
617
|
// 2. Sync from open orders
|
|
591
|
-
|
|
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
|
+
}
|
|
592
637
|
// Recovery runs after fetchAccountTotals() has refreshed authoritative balances
|
|
593
638
|
// from chain. During this pass we only want to reconcile grid structure/order
|
|
594
639
|
// mapping against open orders; re-applying optimistic accounting deltas here
|
|
@@ -692,6 +737,19 @@ class Accountant {
|
|
|
692
737
|
mgr.logger?.log?.(`[RECOVERY] ${violationType} - attempting state recovery (attempt ${state.attemptCount}${hasAttemptLimit ? `/${maxAttemptsRaw}` : ''})...`, 'warn');
|
|
693
738
|
try {
|
|
694
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
|
+
}
|
|
695
753
|
if (validation.isValid) {
|
|
696
754
|
mgr.logger?.log?.('[RECOVERY] State recovery succeeded', 'info');
|
|
697
755
|
state.structuralResyncRequested = false;
|
|
@@ -941,92 +999,142 @@ class Accountant {
|
|
|
941
999
|
const mgr = this.manager;
|
|
942
1000
|
if (!oldOrder || !newOrder)
|
|
943
1001
|
return;
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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;
|
|
949
1009
|
if (!skipAssetAccounting) {
|
|
950
1010
|
const oldIsActive = (oldOrder.state === constants_1.ORDER_STATES.ACTIVE || oldOrder.state === constants_1.ORDER_STATES.PARTIAL);
|
|
951
1011
|
const newIsActive = (newOrder.state === constants_1.ORDER_STATES.ACTIVE || newOrder.state === constants_1.ORDER_STATES.PARTIAL);
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
if (mgr.logger && mgr.logger.level === 'debug') {
|
|
963
|
-
mgr.logger.log(`[ACCOUNTING] updateOptimisticFreeBalance: id=${newOrder.id}, type=${newOrder.type}, ` +
|
|
964
|
-
`state=${oldOrder.state}->${newOrder.state}, ` +
|
|
965
|
-
`size=${Format.formatSizeByOrderType(oldSize, sideForPrecision ?? '', mgr.assets)}->${Format.formatSizeByOrderType(newSize, sideForPrecision ?? '', mgr.assets)}, ` +
|
|
966
|
-
`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
|
+
}
|
|
967
1022
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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');
|
|
994
1070
|
}
|
|
995
1071
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
mgr.logger?.log?.(`[RECOVERY] Immediate recovery scheduling failed: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
|
|
1003
|
-
mgr._recoveryState = { ...mgr._recoveryState, lastFailureAt: Date.now() };
|
|
1004
|
-
})
|
|
1005
|
-
.finally(() => {
|
|
1006
|
-
mgr._pendingRecovery = null;
|
|
1007
|
-
});
|
|
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');
|
|
1008
1078
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
+
}
|
|
1011
1119
|
}
|
|
1012
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
|
+
}
|
|
1013
1126
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const btsOrderType = this._getBtsOrderType();
|
|
1024
|
-
if (btsOrderType) {
|
|
1025
|
-
const { balanceDelta } = this._resolveBtsFeeLifecycle(oldOrder, newOrder, context, fee);
|
|
1026
|
-
if (Math.abs(balanceDelta) > 0) {
|
|
1027
|
-
await 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
|
+
}
|
|
1028
1136
|
}
|
|
1029
|
-
}
|
|
1137
|
+
});
|
|
1030
1138
|
}
|
|
1031
1139
|
/**
|
|
1032
1140
|
* Deduct BTS fees using adjustTotalBalance.
|
|
@@ -1040,10 +1148,9 @@ class Accountant {
|
|
|
1040
1148
|
*/
|
|
1041
1149
|
async deductBtsFees(requestedSide = null) {
|
|
1042
1150
|
const mgr = this.manager;
|
|
1043
|
-
// Early returns for no work needed
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
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)
|
|
1047
1154
|
return;
|
|
1048
1155
|
const btsSide = (0, math_1.getBtsSide)(mgr.config?.assetA, mgr.config?.assetB);
|
|
1049
1156
|
const normalizedRequestedSide = (requestedSide === 'buy' || requestedSide === 'sell') ? requestedSide : null;
|
|
@@ -1054,26 +1161,37 @@ class Accountant {
|
|
|
1054
1161
|
}
|
|
1055
1162
|
if (!side)
|
|
1056
1163
|
return;
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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;
|
|
1063
1185
|
if (mgr.logger && mgr.logger.level === 'debug') {
|
|
1064
|
-
mgr.logger.log(`[BTS-FEE]
|
|
1186
|
+
mgr.logger.log(`[BTS-FEE] Settled: ${Format.formatAmount8(fees)} BTS`, 'debug');
|
|
1065
1187
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
//
|
|
1069
|
-
|
|
1070
|
-
if (
|
|
1071
|
-
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();
|
|
1072
1194
|
}
|
|
1073
|
-
// Reset fees after successful settlement
|
|
1074
|
-
mgr.funds.btsFeesOwed = 0;
|
|
1075
|
-
// Recalculate funds to update all tracking metrics
|
|
1076
|
-
await mgr.recalculateFunds();
|
|
1077
1195
|
}
|
|
1078
1196
|
/**
|
|
1079
1197
|
* Validate that a proposed Target Grid fits within the account's total available funds.
|