dexbot 1.1.12 → 1.1.14
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/dist/credential-daemon.d.ts.map +1 -1
- package/dist/credential-daemon.js +13 -4
- package/dist/credential-daemon.js.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.js +6 -0
- package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
- package/dist/market_adapter/market_adapter.d.ts.map +1 -1
- package/dist/market_adapter/market_adapter.js +6 -3
- package/dist/market_adapter/market_adapter.js.map +1 -1
- package/dist/modules/chain_orders.d.ts +5 -1
- package/dist/modules/chain_orders.d.ts.map +1 -1
- package/dist/modules/chain_orders.js +8 -4
- package/dist/modules/chain_orders.js.map +1 -1
- package/dist/modules/constants.d.ts +5 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +28 -12
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +38 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +449 -91
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_credential_client.d.ts +3 -0
- package/dist/modules/dexbot_credential_client.d.ts.map +1 -1
- package/dist/modules/dexbot_credential_client.js +78 -43
- package/dist/modules/dexbot_credential_client.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +100 -4
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/key_store.d.ts +2 -2
- package/dist/modules/key_store.d.ts.map +1 -1
- package/dist/modules/key_store.js +6 -1
- package/dist/modules/key_store.js.map +1 -1
- package/dist/modules/node_manager.d.ts +9 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +35 -17
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +36 -5
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/async_lock.d.ts +21 -0
- package/dist/modules/order/async_lock.d.ts.map +1 -1
- package/dist/modules/order/async_lock.js +43 -3
- package/dist/modules/order/async_lock.js.map +1 -1
- package/dist/modules/order/grid.d.ts +26 -0
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +53 -0
- 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 +6 -3
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/manager.d.ts +1 -3
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +15 -30
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts +9 -2
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +79 -5
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +17 -1
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +1 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +90 -38
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/paths.d.ts +1 -0
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +1 -0
- package/dist/modules/paths.js.map +1 -1
- package/package.json +14 -4
|
@@ -71,7 +71,7 @@ const { ProcessedFillStore, PROCESSED_FILL_PERSISTENCE_MODES } = require('./orde
|
|
|
71
71
|
const DexbotFillRuntime = require('./dexbot_fill_runtime');
|
|
72
72
|
const DexbotMaintenanceRuntime = require('./dexbot_maintenance_runtime');
|
|
73
73
|
const CreditRuntime = require('./credit_runtime');
|
|
74
|
-
const { ORDER_STATES, ORDER_TYPES, REBALANCE_STATES, COW_ACTIONS, TIMING, MAINTENANCE, FILL_PROCESSING, DAEMON_CODES, } = require('./constants');
|
|
74
|
+
const { ORDER_STATES, ORDER_TYPES, REBALANCE_STATES, COW_ACTIONS, TIMING, PIPELINE_TIMING, GRID_LIMITS, MAINTENANCE, FILL_PROCESSING, DAEMON_CODES, } = require('./constants');
|
|
75
75
|
const { PATHS, getRecalculateTriggerFile } = require('./paths');
|
|
76
76
|
const { attemptResumePersistedGridByPriceMatch, decideStartupGridAction, reconcileGridOrders } = require('./order/grid_reconcile');
|
|
77
77
|
const { AccountOrders } = require('./account_orders');
|
|
@@ -127,11 +127,14 @@ class DEXBot {
|
|
|
127
127
|
_batchInFlight;
|
|
128
128
|
_recoverySyncInFlight;
|
|
129
129
|
_lastTargetedDriftSyncAt;
|
|
130
|
+
_lightweightSyncCheckAt;
|
|
130
131
|
_targetedDriftSyncCooldownMs;
|
|
131
132
|
_maintenanceCooldownCycles;
|
|
132
133
|
_lastGridActivityAt;
|
|
133
134
|
_currentCycleId;
|
|
134
135
|
_autoCancelOrphanCycleMarker;
|
|
136
|
+
_autoCancelOrphanSubCount;
|
|
137
|
+
_orphanFillsCreditedAt;
|
|
135
138
|
_consecutiveConsumeFailures;
|
|
136
139
|
_consumeFailureFirstAt;
|
|
137
140
|
_reconnectUnregister;
|
|
@@ -216,11 +219,14 @@ class DEXBot {
|
|
|
216
219
|
this._batchInFlight = false;
|
|
217
220
|
this._recoverySyncInFlight = false;
|
|
218
221
|
this._lastTargetedDriftSyncAt = 0;
|
|
222
|
+
this._lightweightSyncCheckAt = 0;
|
|
219
223
|
this._targetedDriftSyncCooldownMs = this.config.timing.TARGETED_DRIFT_SYNC_COOLDOWN_MS;
|
|
220
224
|
this._maintenanceCooldownCycles = 0;
|
|
221
225
|
this._lastGridActivityAt = 0;
|
|
222
226
|
this._currentCycleId = 0;
|
|
223
227
|
this._autoCancelOrphanCycleMarker = null;
|
|
228
|
+
this._autoCancelOrphanSubCount = 0;
|
|
229
|
+
this._orphanFillsCreditedAt = null;
|
|
224
230
|
// Per-session guard: ghost order IDs successfully cancelled
|
|
225
231
|
// (avoids spamming the chain with repeated cancel attempts for the same
|
|
226
232
|
// orphan residual on every fill cycle).
|
|
@@ -543,6 +549,90 @@ class DEXBot {
|
|
|
543
549
|
}
|
|
544
550
|
return Array.from(ids);
|
|
545
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* Reload the entire grid from the persisted on-disk snapshot and reconcile
|
|
554
|
+
* with current chain state. Mirrors the startup recovery path.
|
|
555
|
+
*
|
|
556
|
+
* LOCK SAFETY: Does NOT acquire _fillProcessingLock. Caller must either
|
|
557
|
+
* hold it already or accept concurrent fill-processing risk — matches
|
|
558
|
+
* the startup pattern at line ~1340.
|
|
559
|
+
*
|
|
560
|
+
* @returns {Promise<{success: boolean, reason?: string}>}
|
|
561
|
+
*/
|
|
562
|
+
async _recoverFromPersistedGrid() {
|
|
563
|
+
if (!this.accountOrders || !this.manager) {
|
|
564
|
+
return { success: false, reason: 'accountOrders or manager unavailable' };
|
|
565
|
+
}
|
|
566
|
+
const accountRef = this.accountId || this.account?.id || this.account;
|
|
567
|
+
if (!accountRef) {
|
|
568
|
+
return { success: false, reason: 'no account reference' };
|
|
569
|
+
}
|
|
570
|
+
this.manager.logger.log('[RECOVERY] Attempting full grid reload from persisted snapshot...', 'warn');
|
|
571
|
+
try {
|
|
572
|
+
// 1. Force reload from disk
|
|
573
|
+
const persistedGrid = this.accountOrders.loadGrid(true);
|
|
574
|
+
if (!persistedGrid || persistedGrid.length === 0) {
|
|
575
|
+
return { success: false, reason: 'no persisted grid on disk' };
|
|
576
|
+
}
|
|
577
|
+
const boundaryIdx = this.accountOrders.loadBoundaryIdx(true);
|
|
578
|
+
// 2. Load into manager (same path as startup at line 1340)
|
|
579
|
+
await Grid.loadGrid(this.manager, persistedGrid, boundaryIdx);
|
|
580
|
+
// Gap 1: Grid snapshot sanity check — shared logic with startup path.
|
|
581
|
+
if (await this._rejectCorruptedGridSnapshot('recovery')) {
|
|
582
|
+
return { success: false, reason: 'corrupted grid snapshot rejected (fund drift)' };
|
|
583
|
+
}
|
|
584
|
+
// 3. Read current chain state
|
|
585
|
+
const chainOpenOrders = await chainOrders.readOpenOrders(accountRef);
|
|
586
|
+
// 4. Reconcile
|
|
587
|
+
if (chainOpenOrders.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
588
|
+
await this.manager.syncFromOpenOrders(chainOpenOrders, {
|
|
589
|
+
skipAccounting: true,
|
|
590
|
+
fillLockAlreadyHeld: true,
|
|
591
|
+
protectCommittedOrders: true
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
// 5. Persist the reconciled state
|
|
595
|
+
if (typeof this.manager.persistGrid === 'function') {
|
|
596
|
+
await this.manager.persistGrid();
|
|
597
|
+
}
|
|
598
|
+
this.manager.logger.log(`[RECOVERY] Grid reloaded from persisted snapshot: ${this.manager.orders.size} orders, ` +
|
|
599
|
+
`${chainOpenOrders.length} on-chain orders synced`, 'info');
|
|
600
|
+
return { success: true };
|
|
601
|
+
}
|
|
602
|
+
catch (err) {
|
|
603
|
+
this.manager.logger.log(`[RECOVERY] Full grid reload from persisted snapshot failed: ${err.message}`, 'error');
|
|
604
|
+
return { success: false, reason: err.message };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Reject a corrupted grid snapshot when catastrophic fund drift is detected.
|
|
609
|
+
* Shared between startup and recovery paths to avoid duplicating the
|
|
610
|
+
* drift-ratio math and snapshot-clearing logic.
|
|
611
|
+
*
|
|
612
|
+
* @param {'startup'|'recovery'} context - Controls log prefix.
|
|
613
|
+
* @returns {Promise<boolean>} True if the snapshot was rejected (cleared).
|
|
614
|
+
*/
|
|
615
|
+
async _rejectCorruptedGridSnapshot(context) {
|
|
616
|
+
if (!this.manager?.checkFundDriftAfterFills)
|
|
617
|
+
return false;
|
|
618
|
+
const driftCheck = this.manager.checkFundDriftAfterFills();
|
|
619
|
+
if (driftCheck.isValid)
|
|
620
|
+
return false;
|
|
621
|
+
const tag = context === 'recovery' ? '[RECOVERY][SNAPSHOT-REJECT]' : '[SNAPSHOT-REJECT]';
|
|
622
|
+
this._warn(`${tag} Corrupted grid snapshot detected: ` +
|
|
623
|
+
`drift sell=${driftCheck.driftSell.toFixed(2)} buy=${driftCheck.driftBuy.toFixed(2)}. ` +
|
|
624
|
+
`Deleting corrupted snapshot.`);
|
|
625
|
+
if (this.accountOrders && typeof this.accountOrders.clearGrid === 'function') {
|
|
626
|
+
try {
|
|
627
|
+
await this.accountOrders.clearGrid();
|
|
628
|
+
this._warn(`${tag} Corrupted grid snapshot deleted.`);
|
|
629
|
+
}
|
|
630
|
+
catch (clearErr) {
|
|
631
|
+
this._warn(`${tag} Failed to delete corrupted snapshot: ${clearErr.message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
546
636
|
/**
|
|
547
637
|
* Attempt to repair size-drift for specific order IDs by reading their
|
|
548
638
|
* current on-chain state and correcting the local grid directly.
|
|
@@ -1180,6 +1270,8 @@ class DEXBot {
|
|
|
1180
1270
|
await this._executeBatchIfNeeded(rebalanceResult, 'startup reconcile (loaded grid)');
|
|
1181
1271
|
// Dust state is no longer persisted — cancelled immediately on detection.
|
|
1182
1272
|
await this._persistAndRecoverIfNeeded();
|
|
1273
|
+
// Gap 1: Grid snapshot sanity check — shared logic with recovery path.
|
|
1274
|
+
await this._rejectCorruptedGridSnapshot('startup');
|
|
1183
1275
|
}
|
|
1184
1276
|
// Drain any fills that arrived during startup while still in bootstrap
|
|
1185
1277
|
// mode. Safe to call directly since we already hold _fillProcessingLock
|
|
@@ -1449,6 +1541,14 @@ class DEXBot {
|
|
|
1449
1541
|
return;
|
|
1450
1542
|
}
|
|
1451
1543
|
await this.manager._fillProcessingLock.acquire(async () => {
|
|
1544
|
+
// Reset orphan-fill credit timestamp at the start of each
|
|
1545
|
+
// fill cycle. It is re-set when orphan fills are credited,
|
|
1546
|
+
// and consumed (set to null) on the next fund-invariant check
|
|
1547
|
+
// in accounting.ts, which widens tolerance by 5x while set.
|
|
1548
|
+
// Also cleared by _performStateRecovery after a fresh chain
|
|
1549
|
+
// fetch. The timestamp value itself is not compared against a
|
|
1550
|
+
// window — it acts as a consume-on-read boolean.
|
|
1551
|
+
this._orphanFillsCreditedAt = null;
|
|
1452
1552
|
while (this._incomingFillQueue.length > 0) {
|
|
1453
1553
|
const batchStartTime = Date.now();
|
|
1454
1554
|
// Track max queue depth
|
|
@@ -1512,6 +1612,10 @@ class DEXBot {
|
|
|
1512
1612
|
if (accountingResult.status === 'missing_key') {
|
|
1513
1613
|
requiresOpenOrdersSync = true;
|
|
1514
1614
|
}
|
|
1615
|
+
// Record orphan fill credit timestamp for fund invariant
|
|
1616
|
+
// tolerance widening. Accounting checks recency via
|
|
1617
|
+
// _checkOrphanFillRecency() instead of a cross-module flag.
|
|
1618
|
+
this._orphanFillsCreditedAt = Date.now();
|
|
1515
1619
|
// Don't add to validFills - we can't do rebalancing without a grid slot
|
|
1516
1620
|
// But the funds are now credited, preventing fund invariant violation
|
|
1517
1621
|
continue;
|
|
@@ -1601,7 +1705,49 @@ class DEXBot {
|
|
|
1601
1705
|
};
|
|
1602
1706
|
this.manager.pauseFundRecalc();
|
|
1603
1707
|
try {
|
|
1604
|
-
|
|
1708
|
+
// FIX 1: Block-level fill batching — group valid fills by block
|
|
1709
|
+
// and process each block group as a unit. This prevents slot
|
|
1710
|
+
// collisions when fills from the same block (original + replacement
|
|
1711
|
+
// orders both filled) arrive in different sync batches. Processing
|
|
1712
|
+
// all fills from a block together lets the sync engine see every
|
|
1713
|
+
// fill for overlapping slots simultaneously.
|
|
1714
|
+
const fillsByBlock = new Map();
|
|
1715
|
+
const fillsWithoutBlock = [];
|
|
1716
|
+
for (const fill of validFills) {
|
|
1717
|
+
if (fill.block_num != null) {
|
|
1718
|
+
const list = fillsByBlock.get(fill.block_num);
|
|
1719
|
+
if (list)
|
|
1720
|
+
list.push(fill);
|
|
1721
|
+
else
|
|
1722
|
+
fillsByBlock.set(fill.block_num, [fill]);
|
|
1723
|
+
}
|
|
1724
|
+
else {
|
|
1725
|
+
fillsWithoutBlock.push(fill);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
// Process block groups in ascending block order so the sync
|
|
1729
|
+
// engine sees a deterministic, chronological fill sequence.
|
|
1730
|
+
const sortedBlocks = [...fillsByBlock.keys()].sort((a, b) => a - b);
|
|
1731
|
+
const accumulatedOrders = [];
|
|
1732
|
+
let anyRequiresSync = false;
|
|
1733
|
+
for (const blockNum of sortedBlocks) {
|
|
1734
|
+
// Reset requiresOpenOrdersSync per block group so one
|
|
1735
|
+
// block's history-id gap doesn't force the next block
|
|
1736
|
+
// into an unnecessary open-orders snapshot re-fetch.
|
|
1737
|
+
requiresOpenOrdersSync = false;
|
|
1738
|
+
this.manager.logger.log(`[FILL-BLOCK] Processing ${fillsByBlock.get(blockNum).length} fill(s) from block ${blockNum}`, 'debug');
|
|
1739
|
+
const blockResult = await processValidFills(fillsByBlock.get(blockNum));
|
|
1740
|
+
accumulatedOrders.push(...blockResult);
|
|
1741
|
+
if (requiresOpenOrdersSync)
|
|
1742
|
+
anyRequiresSync = true;
|
|
1743
|
+
}
|
|
1744
|
+
requiresOpenOrdersSync = anyRequiresSync;
|
|
1745
|
+
if (fillsWithoutBlock.length > 0) {
|
|
1746
|
+
this.manager.logger.log(`[FILL-BLOCK] Processing ${fillsWithoutBlock.length} fill(s) without block info`, 'debug');
|
|
1747
|
+
const noBlockResult = await processValidFills(fillsWithoutBlock);
|
|
1748
|
+
accumulatedOrders.push(...noBlockResult);
|
|
1749
|
+
}
|
|
1750
|
+
allFilledOrders = accumulatedOrders;
|
|
1605
1751
|
// 4. Handle Price Corrections
|
|
1606
1752
|
if (ordersNeedingCorrection.length > 0) {
|
|
1607
1753
|
const correctionResult = await correctAllPriceMismatches(this.manager, this.account, this.privateKey, chainOrders);
|
|
@@ -2092,7 +2238,8 @@ class DEXBot {
|
|
|
2092
2238
|
const openOrders = await chainOrders.readOpenOrders(accountRef);
|
|
2093
2239
|
const recoveryResult = await this.manager.syncFromOpenOrders(openOrders, {
|
|
2094
2240
|
skipAccounting: false,
|
|
2095
|
-
fillLockAlreadyHeld: true
|
|
2241
|
+
fillLockAlreadyHeld: true,
|
|
2242
|
+
protectCommittedOrders: true
|
|
2096
2243
|
});
|
|
2097
2244
|
this._preserveMissingCreateBlockersAfterRecovery(preRecoveryMissingCreateBlockers, recoveryResult);
|
|
2098
2245
|
// Persist any master grid mutations from the recovery sync. The
|
|
@@ -2480,7 +2627,7 @@ class DEXBot {
|
|
|
2480
2627
|
return { executed: false, hadRotation: false, uncertain: true };
|
|
2481
2628
|
}
|
|
2482
2629
|
const adopted = [];
|
|
2483
|
-
|
|
2630
|
+
let discarded = [];
|
|
2484
2631
|
// 2. For each pending broadcast, look for a chain match.
|
|
2485
2632
|
for (const entry of pending) {
|
|
2486
2633
|
const match = this._findChainOrderForSlot(chainSnapshot, entry.slotId, {
|
|
@@ -2494,45 +2641,91 @@ class DEXBot {
|
|
|
2494
2641
|
this.manager._pendingBroadcasts.delete(entry.fingerprint);
|
|
2495
2642
|
}
|
|
2496
2643
|
else {
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
//
|
|
2505
|
-
//
|
|
2506
|
-
//
|
|
2507
|
-
//
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
const shouldRunHeavySync = !(allCreatesAdopted && discarded.length === 0);
|
|
2516
|
-
if (shouldRunHeavySync) {
|
|
2644
|
+
// Preserve full entry context so the recheck below can use
|
|
2645
|
+
// both the fingerprint match path (against _pendingBroadcasts)
|
|
2646
|
+
// and the near-match path (via finalInts/orderType), not just
|
|
2647
|
+
// slotId alone.
|
|
2648
|
+
discarded.push(entry);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
// ---- RACE MITIGATION: block-delay recheck before discarding creates ----
|
|
2652
|
+
// BROADCAST_DEADLINE fires while the transaction may still be valid
|
|
2653
|
+
// and land in the next block. Wait for up to 3 blocks (~1 BitShares
|
|
2654
|
+
// block interval each) and re-check before permanently discarding.
|
|
2655
|
+
const UNCERTAIN_RECHECK_MAX_ATTEMPTS = PIPELINE_TIMING.RETRY_MAX_ATTEMPTS; // 3
|
|
2656
|
+
const UNCERTAIN_RECHECK_INTERVAL_MS = TIMING.MILLISECONDS_PER_SECOND * 3; // 3s
|
|
2657
|
+
let recheckRound = 0;
|
|
2658
|
+
while (discarded.length > 0 && recheckRound < UNCERTAIN_RECHECK_MAX_ATTEMPTS) {
|
|
2659
|
+
recheckRound++;
|
|
2660
|
+
await new Promise(r => setTimeout(r, UNCERTAIN_RECHECK_INTERVAL_MS));
|
|
2661
|
+
let freshSnapshot;
|
|
2517
2662
|
try {
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2663
|
+
freshSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
2664
|
+
}
|
|
2665
|
+
catch {
|
|
2666
|
+
break;
|
|
2667
|
+
}
|
|
2668
|
+
const stillDiscarded = [];
|
|
2669
|
+
for (const entry of discarded) {
|
|
2670
|
+
const match = this._findChainOrderForSlot(freshSnapshot, entry.slotId, {
|
|
2671
|
+
sell: entry.finalInts?.sell,
|
|
2672
|
+
receive: entry.finalInts?.receive,
|
|
2673
|
+
orderType: entry.orderType || entry.order?.type,
|
|
2674
|
+
fingerprint: entry.fingerprint
|
|
2675
|
+
});
|
|
2676
|
+
if (match) {
|
|
2677
|
+
adopted.push({ slotId: entry.slotId, chainOrderId: match.id });
|
|
2678
|
+
this.manager.logger.log(`[COW][UNCERTAIN] CREATE re-adopted after ${recheckRound} block(s): ` +
|
|
2679
|
+
`${entry.slotId}->${match.id}`, 'info');
|
|
2525
2680
|
}
|
|
2681
|
+
else {
|
|
2682
|
+
stillDiscarded.push(entry);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
discarded = stillDiscarded;
|
|
2686
|
+
}
|
|
2687
|
+
// 3. Re-read chain with latest state. The initial snapshot may be
|
|
2688
|
+
// stale for non-CREATE ops (updates/cancels) not tracked in
|
|
2689
|
+
// _pendingBroadcasts — the seed cause of the slot-115 cascade.
|
|
2690
|
+
let latestSnapshot;
|
|
2691
|
+
try {
|
|
2692
|
+
latestSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
2693
|
+
}
|
|
2694
|
+
catch {
|
|
2695
|
+
latestSnapshot = chainSnapshot;
|
|
2696
|
+
}
|
|
2697
|
+
// Always sync to link adopted chain orders into grid slots.
|
|
2698
|
+
// Otherwise an adopted slot stays VIRTUAL and the next COW cycle
|
|
2699
|
+
// places a duplicate order at the same price.
|
|
2700
|
+
let hadRotation = false;
|
|
2701
|
+
if (latestSnapshot && latestSnapshot.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
2702
|
+
try {
|
|
2703
|
+
await this.manager.syncFromOpenOrders(latestSnapshot, {
|
|
2704
|
+
skipAccounting: true,
|
|
2705
|
+
fillLockAlreadyHeld: true,
|
|
2706
|
+
protectCommittedOrders: true
|
|
2707
|
+
});
|
|
2708
|
+
hadRotation = true;
|
|
2526
2709
|
}
|
|
2527
2710
|
catch (syncErr) {
|
|
2528
2711
|
this.manager.logger.log(`[COW][UNCERTAIN] syncFromOpenOrders failed during recovery: ${syncErr?.message || syncErr}`, 'error');
|
|
2712
|
+
// Fallback: reload from persisted snapshot + re-sync.
|
|
2713
|
+
this.manager.logger.log('[COW][UNCERTAIN] syncFromOpenOrders failed; falling back to full grid reload from persisted snapshot', 'warn');
|
|
2714
|
+
const fallbackResult = await this._recoverFromPersistedGrid();
|
|
2715
|
+
if (fallbackResult.success) {
|
|
2716
|
+
hadRotation = true;
|
|
2717
|
+
}
|
|
2718
|
+
else {
|
|
2719
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Persisted grid reload failed: ${fallbackResult.reason}. ` +
|
|
2720
|
+
`Escalating to structural resync.`, 'warn');
|
|
2721
|
+
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
2722
|
+
this.manager.requestStructuralGridResync('cow-uncertain-recovery-failed', { reason: fallbackResult.reason }).catch((escalateErr) => {
|
|
2723
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Escalation to structural resync failed: ${escalateErr.message}`, 'error');
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2529
2727
|
}
|
|
2530
2728
|
}
|
|
2531
|
-
else {
|
|
2532
|
-
hadRotation = true;
|
|
2533
|
-
this.manager.logger.log(`[COW][UNCERTAIN] All ${adopted.length} fingerprinted CREATE(s) adopted; ` +
|
|
2534
|
-
`skipping heavy re-sync to avoid false-positive unmatched warnings.`, 'debug');
|
|
2535
|
-
}
|
|
2536
2729
|
// 4. Log structured summary.
|
|
2537
2730
|
const elapsedMs = Date.now() - startedAt;
|
|
2538
2731
|
const heartbeatAgeMs = this._lastBroadcastHeartbeatAt
|
|
@@ -2540,14 +2733,14 @@ class DEXBot {
|
|
|
2540
2733
|
: null;
|
|
2541
2734
|
this.manager.logger.log(`[COW][UNCERTAIN] batchId=${err?.batchId || 'n/a'} ops=${opContexts.length} ` +
|
|
2542
2735
|
`staleSinceMs=${err?.timeoutMs || 'n/a'} heartbeatAgeMs=${heartbeatAgeMs ?? 'n/a'} ` +
|
|
2543
|
-
`adopted=${adopted.length} discarded=${discarded.length} elapsedMs=${elapsedMs}`, adopted.length > 0 ? 'info' : 'warn');
|
|
2736
|
+
`adopted=${adopted.length} discarded=${discarded.length} recheckRounds=${recheckRound} elapsedMs=${elapsedMs}`, adopted.length > 0 ? 'info' : 'warn');
|
|
2544
2737
|
if (adopted.length > 0) {
|
|
2545
2738
|
this.manager.logger.log(`[COW][UNCERTAIN] Adopted chain orders: ${adopted
|
|
2546
2739
|
.map(a => `${a.slotId}->${a.chainOrderId}`)
|
|
2547
2740
|
.join(', ')}`, 'info');
|
|
2548
2741
|
}
|
|
2549
2742
|
if (discarded.length > 0) {
|
|
2550
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Discarded planned CREATEs (no chain match): ${discarded
|
|
2743
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Discarded planned CREATEs (no chain match after ${recheckRound} recheck(s)): ${discarded
|
|
2551
2744
|
.map(d => d.slotId)
|
|
2552
2745
|
.join(', ')}`, 'warn');
|
|
2553
2746
|
}
|
|
@@ -2596,8 +2789,18 @@ class DEXBot {
|
|
|
2596
2789
|
*/
|
|
2597
2790
|
async _autoCancelOneUnmatchedOrphan() {
|
|
2598
2791
|
const cycleId = this._currentCycleId || 0;
|
|
2792
|
+
// FIX 4: Higher cap during recovery mode so orphan backlog clears faster.
|
|
2793
|
+
const recoveryActive = this.manager?._recoveryState?.structuralResyncRequested === true;
|
|
2794
|
+
const cycleCap = recoveryActive ? 5 : 1;
|
|
2599
2795
|
if (this._autoCancelOrphanCycleMarker === cycleId) {
|
|
2600
|
-
|
|
2796
|
+
if (this._autoCancelOrphanSubCount >= cycleCap) {
|
|
2797
|
+
return { cancelled: false, reason: 'cap-reached-this-cycle', subCount: this._autoCancelOrphanSubCount };
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
else {
|
|
2801
|
+
// Reset sub-count on first call this cycle
|
|
2802
|
+
this._autoCancelOrphanCycleMarker = cycleId;
|
|
2803
|
+
this._autoCancelOrphanSubCount = 0;
|
|
2601
2804
|
}
|
|
2602
2805
|
const pending = (this.manager && this.manager._pendingBroadcasts instanceof Map)
|
|
2603
2806
|
? this.manager._pendingBroadcasts.size
|
|
@@ -2626,13 +2829,15 @@ class DEXBot {
|
|
|
2626
2829
|
return { cancelled: false, reason: 'cancelOrder-unavailable' };
|
|
2627
2830
|
}
|
|
2628
2831
|
try {
|
|
2629
|
-
this._autoCancelOrphanCycleMarker = cycleId;
|
|
2630
|
-
this.manager.logger.log(`[COW] Auto-cancelling 1/${unmatched.length} unmatched chain order ` +
|
|
2631
|
-
`(${this._formatUnmatchedChainOrderForLog(target)}) — per-cycle cap=1.`, 'warn');
|
|
2632
2832
|
await chainOrders.cancelOrder(this.account, this.privateKey, orderId);
|
|
2633
2833
|
if (typeof chainOrders.recordOwnCancel === 'function') {
|
|
2634
2834
|
chainOrders.recordOwnCancel(orderId);
|
|
2635
2835
|
}
|
|
2836
|
+
// Increment sub-count only after a confirmed successful cancel.
|
|
2837
|
+
// Failed cancels do not consume the per-cycle cap budget.
|
|
2838
|
+
this._autoCancelOrphanSubCount++;
|
|
2839
|
+
this.manager.logger.log(`[COW] Auto-cancelled ${this._autoCancelOrphanSubCount}/${unmatched.length} unmatched chain order ` +
|
|
2840
|
+
`(${this._formatUnmatchedChainOrderForLog(target)}) — per-cycle cap=${cycleCap}.`, 'warn');
|
|
2636
2841
|
return { cancelled: true, orderId };
|
|
2637
2842
|
}
|
|
2638
2843
|
catch (err) {
|
|
@@ -2686,6 +2891,21 @@ class DEXBot {
|
|
|
2686
2891
|
if (isRetriable) {
|
|
2687
2892
|
this.manager.logger.log(`[COW] Broadcast uncertain (attempt ${attempt}/${MAX_RETRIES + 1}), retrying...`, 'warn');
|
|
2688
2893
|
await this._ensureCredentialDaemonWritable('COW batch retry');
|
|
2894
|
+
// Reconcile before retry — stale ops would fail "not found".
|
|
2895
|
+
try {
|
|
2896
|
+
const accountRef = this.accountId || this.account?.id || this.account;
|
|
2897
|
+
const freshChain = await chainOrders.readOpenOrders(accountRef);
|
|
2898
|
+
if (freshChain.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
2899
|
+
await this.manager.syncFromOpenOrders(freshChain, {
|
|
2900
|
+
skipAccounting: true,
|
|
2901
|
+
fillLockAlreadyHeld: true,
|
|
2902
|
+
protectCommittedOrders: true
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
catch (syncErr) {
|
|
2907
|
+
this.manager.logger.log(`[COW] Pre-retry sync failed (non-fatal): ${syncErr?.message || syncErr}`, 'warn');
|
|
2908
|
+
}
|
|
2689
2909
|
continue;
|
|
2690
2910
|
}
|
|
2691
2911
|
throw err;
|
|
@@ -3435,6 +3655,22 @@ class DEXBot {
|
|
|
3435
3655
|
};
|
|
3436
3656
|
}
|
|
3437
3657
|
const hasCreateActions = actions.some(action => action.type === COW_ACTIONS.CREATE);
|
|
3658
|
+
// Gap 5: Recovery exhausted — block all CREATES. The bot has exhausted
|
|
3659
|
+
// its recovery attempt budget and needs a fill or sync cycle to reset.
|
|
3660
|
+
// Existing orders remain active and are monitored, but no new orders
|
|
3661
|
+
// are placed until recovery resets.
|
|
3662
|
+
if (hasCreateActions && this.manager?._recoveryExhaustedAt) {
|
|
3663
|
+
const exhaustedAge = Date.now() - this.manager._recoveryExhaustedAt;
|
|
3664
|
+
this.manager.logger.log?.(`[RECOVERY-EXHAUSTED] Blocking ${actions.filter(a => a.type === COW_ACTIONS.CREATE).length} CREATE(s) ` +
|
|
3665
|
+
`(exhausted ${(exhaustedAge / 1000).toFixed(0)}s ago). ` +
|
|
3666
|
+
`Waiting for next fill or sync cycle to reset recovery state.`, 'warn');
|
|
3667
|
+
return {
|
|
3668
|
+
executed: false,
|
|
3669
|
+
aborted: true,
|
|
3670
|
+
reason: 'RECOVERY_EXHAUSTED',
|
|
3671
|
+
hadRotation: false
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3438
3674
|
const unmatchedChainOrders = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
3439
3675
|
? this.manager._lastUnmatchedChainOrders
|
|
3440
3676
|
: [];
|
|
@@ -3448,58 +3684,111 @@ class DEXBot {
|
|
|
3448
3684
|
? Array.from(this.manager._pendingBroadcasts.values())
|
|
3449
3685
|
: [];
|
|
3450
3686
|
if (hasCreateActions && (unmatchedChainOrders.length > 0 || pendingBroadcasts.length > 0)) {
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
if
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3687
|
+
// ---- FIX 3: Absorb unmatched orders instead of rejecting ----
|
|
3688
|
+
// When unmatched chain orders exist, try to auto-cancel them
|
|
3689
|
+
// inline to unblock the CREATE batch. Only fall through to
|
|
3690
|
+
// full rejection + structural resync if cancellation fails.
|
|
3691
|
+
let cancelledUnmatched = 0;
|
|
3692
|
+
if (unmatchedChainOrders.length > 0 && pendingBroadcasts.length === 0) {
|
|
3693
|
+
// Bounded-parallel cancellation: process unmatched orders in
|
|
3694
|
+
// concurrent batches (3 at a time) to avoid serialising 19+
|
|
3695
|
+
// individual cancel txs (~0.5s each = 10s batch hold).
|
|
3696
|
+
const CANCEL_CONCURRENCY = 3;
|
|
3697
|
+
const cancelable = unmatchedChainOrders.filter(u => {
|
|
3698
|
+
const oid = u?.id || u?.orderId || u?.chainOrderId;
|
|
3699
|
+
return Boolean(oid) && !u?.fingerprint;
|
|
3700
|
+
});
|
|
3701
|
+
for (let i = 0; i < cancelable.length; i += CANCEL_CONCURRENCY) {
|
|
3702
|
+
const batch = cancelable.slice(i, i + CANCEL_CONCURRENCY);
|
|
3703
|
+
const results = await Promise.allSettled(batch.map(async (unmatched) => {
|
|
3704
|
+
const orderId = unmatched.id || unmatched.orderId || unmatched.chainOrderId;
|
|
3705
|
+
this.manager.logger.log(`[COW] Auto-cancelling unmatched chain order ${orderId} ` +
|
|
3706
|
+
`(${this._formatUnmatchedChainOrderForLog(unmatched)}) to unblock CREATE batch.`, 'warn');
|
|
3707
|
+
await chainOrders.cancelOrder(this.account, this.privateKey, orderId);
|
|
3708
|
+
if (typeof chainOrders.recordOwnCancel === 'function') {
|
|
3709
|
+
chainOrders.recordOwnCancel(orderId);
|
|
3710
|
+
}
|
|
3711
|
+
}));
|
|
3712
|
+
for (const r of results) {
|
|
3713
|
+
if (r.status === 'fulfilled')
|
|
3714
|
+
cancelledUnmatched++;
|
|
3715
|
+
else
|
|
3716
|
+
this.manager.logger.log(`[COW] Failed to cancel unmatched chain order: ${r.reason?.message || r.reason}`, 'warn');
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
if (cancelable.length > 0 && cancelledUnmatched >= cancelable.length) {
|
|
3720
|
+
this.manager.logger.log(`[COW] Cancelled all ${cancelledUnmatched} unmatched chain order(s); proceeding with CREATE batch.`, 'warn');
|
|
3721
|
+
// Optimistic clear: the cancel txs are sent but not yet
|
|
3722
|
+
// confirmed. If a cancel silently fails on chain, the
|
|
3723
|
+
// next sync cycle will re-detect the unmatched order and
|
|
3724
|
+
// re-enter this guard — that's safe because the cancel
|
|
3725
|
+
// is idempotent and the re-detection is non-blocking.
|
|
3726
|
+
this.manager._lastUnmatchedChainOrders = [];
|
|
3727
|
+
}
|
|
3728
|
+
else if (cancelable.length > 0) {
|
|
3729
|
+
this.manager.logger.log(`[COW] Cancelled ${cancelledUnmatched}/${cancelable.length} unmatched chain order(s); ` +
|
|
3730
|
+
`remaining must be handled by structural reconciliation.`, 'warn');
|
|
3731
|
+
}
|
|
3469
3732
|
}
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3733
|
+
// Re-check after auto-cancel attempt
|
|
3734
|
+
const remainingUnmatched = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
3735
|
+
? this.manager._lastUnmatchedChainOrders
|
|
3736
|
+
: [];
|
|
3737
|
+
if (remainingUnmatched.length > 0 || pendingBroadcasts.length > 0) {
|
|
3738
|
+
const blockers = [];
|
|
3739
|
+
if (remainingUnmatched.length > 0)
|
|
3740
|
+
blockers.push(`${remainingUnmatched.length} unmatched chain order(s)`);
|
|
3741
|
+
if (pendingBroadcasts.length > 0)
|
|
3742
|
+
blockers.push(`${pendingBroadcasts.length} pending broadcast(s)`);
|
|
3743
|
+
const reasonText = blockers.join(' and ');
|
|
3744
|
+
if (pendingBroadcasts.length > 0) {
|
|
3745
|
+
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${reasonText} from a prior uncertain ` +
|
|
3746
|
+
`broadcast. Running recovery before placing replacement orders.`, 'error');
|
|
3747
|
+
}
|
|
3748
|
+
else {
|
|
3749
|
+
const sample = remainingUnmatched
|
|
3750
|
+
.slice(0, 3)
|
|
3751
|
+
.map(order => this._formatUnmatchedChainOrderForLog(order))
|
|
3752
|
+
.join(' | ');
|
|
3753
|
+
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${reasonText} ` +
|
|
3754
|
+
`are not represented in the grid${sample ? ` (${sample})` : ''}. ` +
|
|
3755
|
+
`Run structural reconciliation before placing replacement orders.`, 'error');
|
|
3756
|
+
}
|
|
3757
|
+
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
3758
|
+
if (this.manager._recoveryState)
|
|
3759
|
+
this.manager._recoveryState.structuralResyncRequested = true;
|
|
3760
|
+
await this.manager.requestStructuralGridResync(pendingBroadcasts.length > 0
|
|
3761
|
+
? 'pending broadcasts before COW create'
|
|
3762
|
+
: 'unmatched chain orders before COW create', pendingBroadcasts.length > 0
|
|
3763
|
+
? { pendingBroadcasts: pendingBroadcasts.map(p => p.slotId) }
|
|
3764
|
+
: { unmatchedChainOrders: remainingUnmatched });
|
|
3490
3765
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
3766
|
+
// If we have pending broadcasts, drive the recovery now so the
|
|
3767
|
+
// next planning cycle has a clean state.
|
|
3768
|
+
if (pendingBroadcasts.length > 0) {
|
|
3769
|
+
try {
|
|
3770
|
+
await this._reconcileAfterUncertainBroadcast(new BroadcastUncertainError('rejected CREATE batch had pending broadcasts', {
|
|
3771
|
+
operations: pendingBroadcasts.map(p => p.order),
|
|
3772
|
+
accountName: this.account,
|
|
3773
|
+
batchId: this._currentBatchId || null,
|
|
3774
|
+
payload: null,
|
|
3775
|
+
timeoutMs: null
|
|
3776
|
+
}), [], { fillLockAlreadyHeld: true });
|
|
3777
|
+
}
|
|
3778
|
+
catch (recoverErr) {
|
|
3779
|
+
this.manager.logger.log(`[COW] Recovery from pending broadcasts failed: ${recoverErr?.message || recoverErr}`, 'error');
|
|
3780
|
+
}
|
|
3493
3781
|
}
|
|
3782
|
+
return {
|
|
3783
|
+
executed: false,
|
|
3784
|
+
aborted: true,
|
|
3785
|
+
reason: pendingBroadcasts.length > 0 ? 'PENDING_BROADCASTS' : 'UNMATCHED_CHAIN_ORDERS',
|
|
3786
|
+
unmatchedChainOrders: pendingBroadcasts.length > 0 ? [] : remainingUnmatched,
|
|
3787
|
+
pendingBroadcasts: pendingBroadcasts.map(p => p.slotId),
|
|
3788
|
+
hadRotation: false
|
|
3789
|
+
};
|
|
3494
3790
|
}
|
|
3495
|
-
|
|
3496
|
-
executed: false,
|
|
3497
|
-
aborted: true,
|
|
3498
|
-
reason: pendingBroadcasts.length > 0 ? 'PENDING_BROADCASTS' : 'UNMATCHED_CHAIN_ORDERS',
|
|
3499
|
-
unmatchedChainOrders: pendingBroadcasts.length > 0 ? [] : unmatchedChainOrders,
|
|
3500
|
-
pendingBroadcasts: pendingBroadcasts.map(p => p.slotId),
|
|
3501
|
-
hadRotation: false
|
|
3502
|
-
};
|
|
3791
|
+
// All unmatched were cancelled — fall through to continue batch
|
|
3503
3792
|
}
|
|
3504
3793
|
const { assetA, assetB } = this.manager.assets;
|
|
3505
3794
|
const operations = [];
|
|
@@ -3540,7 +3829,13 @@ class DEXBot {
|
|
|
3540
3829
|
opContexts.push({ kind: 'cancel', order });
|
|
3541
3830
|
}
|
|
3542
3831
|
catch (err) {
|
|
3543
|
-
|
|
3832
|
+
const orderNotFound = /\bnot found\b/i.test(err.message) || /\bdoes not exist\b/i.test(err.message);
|
|
3833
|
+
if (orderNotFound) {
|
|
3834
|
+
this.manager.logger.log(`[COW] Cancel skipped for ${action.id} (${action.orderId}): order already removed from chain`, 'debug');
|
|
3835
|
+
}
|
|
3836
|
+
else {
|
|
3837
|
+
this.manager.logger.log(`Failed to prepare cancel op for ${action.id}: ${err.message}`, 'error');
|
|
3838
|
+
}
|
|
3544
3839
|
}
|
|
3545
3840
|
}
|
|
3546
3841
|
else if (action.type === COW_ACTIONS.CREATE) {
|
|
@@ -3652,8 +3947,60 @@ class DEXBot {
|
|
|
3652
3947
|
type: orderType
|
|
3653
3948
|
};
|
|
3654
3949
|
opContexts.push({ kind: 'size-update', updateInfo: { partialOrder, newSize }, finalInts: op.finalInts });
|
|
3950
|
+
// Catch for both rotation-update and size-update branches.
|
|
3655
3951
|
}
|
|
3656
3952
|
catch (err) {
|
|
3953
|
+
const orderNotFound = /\bnot found\b/i.test(err.message) || /\bdoes not exist\b/i.test(err.message);
|
|
3954
|
+
if (orderNotFound) {
|
|
3955
|
+
try {
|
|
3956
|
+
const fbOrder = action.order || this.manager.orders.get(action.id);
|
|
3957
|
+
const fbType = fbOrder?.type;
|
|
3958
|
+
const fbSize = action.newSize || fbOrder?.size || 0;
|
|
3959
|
+
// Live price from TARGET slot (same pattern as CREATE path).
|
|
3960
|
+
const targetSlotId = action.newGridId || action.id;
|
|
3961
|
+
const plannedPrice = action.newPrice || action.order?.price || 0;
|
|
3962
|
+
const liveSlotForPrice = this.manager.orders.get(targetSlotId);
|
|
3963
|
+
const livePrice = liveSlotForPrice ? Number(liveSlotForPrice.price) : NaN;
|
|
3964
|
+
const priceDrift = Number.isFinite(plannedPrice) && Number.isFinite(livePrice)
|
|
3965
|
+
? Math.abs(livePrice - plannedPrice)
|
|
3966
|
+
: 0;
|
|
3967
|
+
const fbPrice = (priceDrift > 0) ? livePrice : plannedPrice;
|
|
3968
|
+
if (priceDrift > 0) {
|
|
3969
|
+
this.manager.logger.log(`[COW] CREATE fallback price drift for ${action.id} -> ${targetSlotId}: ` +
|
|
3970
|
+
`planned=${plannedPrice} live=${livePrice} (diff=${priceDrift})`, 'debug');
|
|
3971
|
+
}
|
|
3972
|
+
// Same size gate as regular CREATE path.
|
|
3973
|
+
const sizeCheck = this._validateOrderSizeForExecution(fbSize, fbType, fbOrder, fbSize);
|
|
3974
|
+
if (!sizeCheck.isValid) {
|
|
3975
|
+
this.manager.logger.log(`[COW] CREATE fallback for ${action.id} rejected by size validation: ${sizeCheck.reason}`, 'warn');
|
|
3976
|
+
}
|
|
3977
|
+
else if (fbType && fbSize > 0 && fbPrice > 0) {
|
|
3978
|
+
const fbArgs = buildCreateOrderArgs({ type: fbType, size: fbSize, price: fbPrice }, assetA, assetB);
|
|
3979
|
+
const fbResult = await chainOrders.buildCreateOrderOp(this.account, fbArgs.amountToSell, fbArgs.sellAssetId, fbArgs.minToReceive, fbArgs.receiveAssetId, null);
|
|
3980
|
+
if (fbResult) {
|
|
3981
|
+
operations.push(fbResult.op);
|
|
3982
|
+
opContexts.push({
|
|
3983
|
+
kind: 'create',
|
|
3984
|
+
id: targetSlotId,
|
|
3985
|
+
order: { id: targetSlotId, type: fbType, price: fbPrice, size: fbSize },
|
|
3986
|
+
args: { amountToSell: fbArgs.amountToSell, minToReceive: fbArgs.minToReceive },
|
|
3987
|
+
finalInts: fbResult.finalInts
|
|
3988
|
+
});
|
|
3989
|
+
this._recordPendingBroadcast({
|
|
3990
|
+
opIndex: operations.length - 1,
|
|
3991
|
+
ctxIndex: opContexts.length - 1,
|
|
3992
|
+
order: { id: targetSlotId, type: fbType, price: fbPrice, size: fbSize },
|
|
3993
|
+
finalInts: fbResult.finalInts
|
|
3994
|
+
});
|
|
3995
|
+
this.manager.logger.log(`[COW] Recovered "not found" for ${action.id}: converted UPDATE to CREATE for slot ${targetSlotId}`, 'warn');
|
|
3996
|
+
continue;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
catch (fbErr) {
|
|
4001
|
+
this.manager.logger.log(`[COW] CREATE fallback also failed for ${action.id}: ${fbErr.message}`, 'warn');
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
3657
4004
|
this.manager.logger.log(`Failed to prepare update op for ${action.id}: ${err.message}`, 'error');
|
|
3658
4005
|
}
|
|
3659
4006
|
}
|
|
@@ -4285,6 +4632,16 @@ class DEXBot {
|
|
|
4285
4632
|
return;
|
|
4286
4633
|
this._structuralGridResyncRunning = true;
|
|
4287
4634
|
try {
|
|
4635
|
+
// Try the lighter persisted-grid reload before full reset.
|
|
4636
|
+
const persistedResult = await this._recoverFromPersistedGrid();
|
|
4637
|
+
if (persistedResult.success) {
|
|
4638
|
+
if (this.manager?._recoveryState) {
|
|
4639
|
+
this.manager._recoveryState.attemptCount = 0;
|
|
4640
|
+
this.manager._recoveryState.lastAttemptAt = 0;
|
|
4641
|
+
this.manager._recoveryState.lastFailureAt = 0;
|
|
4642
|
+
}
|
|
4643
|
+
return;
|
|
4644
|
+
}
|
|
4288
4645
|
const suffix = unmatchedCount > 0 ? ` (${unmatchedCount} unmatched chain order(s))` : '';
|
|
4289
4646
|
this._warn(`[RECOVERY] Running structural full grid resync for ${reason}${suffix}`);
|
|
4290
4647
|
const resetResult = await this.requestGridReset('rms_structural_grid_resync', {
|
|
@@ -4321,6 +4678,7 @@ class DEXBot {
|
|
|
4321
4678
|
fillProcessingLockActive: this.manager?._fillProcessingLock?.isLocked() || false,
|
|
4322
4679
|
divergenceLockActive: this.manager?._divergenceLock?.isLocked() || false,
|
|
4323
4680
|
shadowLocksActive: this.manager?.shadowOrderIds?.size || 0,
|
|
4681
|
+
recoveryExhaustedAt: this.manager?._recoveryExhaustedAt || null,
|
|
4324
4682
|
recentFillsTracked: this._recentlyProcessedFills.size
|
|
4325
4683
|
};
|
|
4326
4684
|
}
|