dexbot 1.1.11 → 1.1.13
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/analysis/analyze_derivatives.js +3 -3
- package/dist/analysis/analyze_derivatives.js.map +1 -1
- package/dist/analysis/analyze_dynamic_weight.js +8 -9
- package/dist/analysis/analyze_dynamic_weight.js.map +1 -1
- package/dist/analysis/analyze_kalman.js +1 -1
- package/dist/analysis/analyze_kalman.js.map +1 -1
- package/dist/analysis/analyze_regime.js +1 -1
- package/dist/analysis/analyze_regime.js.map +1 -1
- package/dist/analysis/analyze_regime_windows.js +1 -1
- package/dist/analysis/analyze_regime_windows.js.map +1 -1
- package/dist/analysis/analyze_volatility.js +1 -1
- package/dist/analysis/analyze_volatility.js.map +1 -1
- package/dist/analysis/bot_key_utils.d.ts +6 -0
- package/dist/analysis/bot_key_utils.d.ts.map +1 -1
- package/dist/analysis/bot_key_utils.js +101 -0
- package/dist/analysis/bot_key_utils.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +3 -5
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +10 -50
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -1
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +3 -2
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -1
- 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 +2 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +8 -0
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +29 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +425 -91
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_credential_client.d.ts +2 -0
- package/dist/modules/dexbot_credential_client.d.ts.map +1 -1
- package/dist/modules/dexbot_credential_client.js +69 -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 +47 -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 +4 -1
- package/dist/modules/key_store.js.map +1 -1
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +21 -5
- package/dist/modules/order/accounting.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 +2 -3
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +18 -21
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts +11 -3
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +41 -5
- package/dist/modules/order/sync_engine.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 +1 -1
|
@@ -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, 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');
|
|
@@ -132,12 +132,15 @@ class DEXBot {
|
|
|
132
132
|
_lastGridActivityAt;
|
|
133
133
|
_currentCycleId;
|
|
134
134
|
_autoCancelOrphanCycleMarker;
|
|
135
|
+
_autoCancelOrphanSubCount;
|
|
136
|
+
_orphanFillsCreditedAt;
|
|
135
137
|
_consecutiveConsumeFailures;
|
|
136
138
|
_consumeFailureFirstAt;
|
|
137
139
|
_reconnectUnregister;
|
|
138
140
|
_credentialRecoveryDeferredTimer;
|
|
139
141
|
_structuralGridResyncTimer;
|
|
140
142
|
_structuralGridResyncRunning;
|
|
143
|
+
_ghostOrderCancelAttempted;
|
|
141
144
|
_dustHealthCheckTimer;
|
|
142
145
|
_lastBroadcastHeartbeatAt;
|
|
143
146
|
_currentBatchId;
|
|
@@ -220,6 +223,12 @@ class DEXBot {
|
|
|
220
223
|
this._lastGridActivityAt = 0;
|
|
221
224
|
this._currentCycleId = 0;
|
|
222
225
|
this._autoCancelOrphanCycleMarker = null;
|
|
226
|
+
this._autoCancelOrphanSubCount = 0;
|
|
227
|
+
this._orphanFillsCreditedAt = null;
|
|
228
|
+
// Per-session guard: ghost order IDs successfully cancelled
|
|
229
|
+
// (avoids spamming the chain with repeated cancel attempts for the same
|
|
230
|
+
// orphan residual on every fill cycle).
|
|
231
|
+
this._ghostOrderCancelAttempted = null;
|
|
223
232
|
// Dust cancellation uses immediate on-chain cancel — no maps or timers needed.
|
|
224
233
|
this._dustHealthCheckTimer = null;
|
|
225
234
|
// Fill consumer watchdog: consecutive failure tracking.
|
|
@@ -364,7 +373,7 @@ class DEXBot {
|
|
|
364
373
|
this.manager.logger.log(`Triggering state recovery sync (${reason})...`, 'info');
|
|
365
374
|
await this.manager.fetchAccountTotals(this.accountId);
|
|
366
375
|
const openOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
367
|
-
await this.manager.syncFromOpenOrders(openOrders, { skipAccounting: true, fillLockAlreadyHeld: true });
|
|
376
|
+
await this.manager.syncFromOpenOrders(openOrders, { skipAccounting: true, fillLockAlreadyHeld: true, protectCommittedOrders: true });
|
|
368
377
|
if (typeof this.manager.persistGrid === 'function') {
|
|
369
378
|
await this.manager.persistGrid();
|
|
370
379
|
}
|
|
@@ -538,6 +547,57 @@ class DEXBot {
|
|
|
538
547
|
}
|
|
539
548
|
return Array.from(ids);
|
|
540
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Reload the entire grid from the persisted on-disk snapshot and reconcile
|
|
552
|
+
* with current chain state. Mirrors the startup recovery path.
|
|
553
|
+
*
|
|
554
|
+
* LOCK SAFETY: Does NOT acquire _fillProcessingLock. Caller must either
|
|
555
|
+
* hold it already or accept concurrent fill-processing risk — matches
|
|
556
|
+
* the startup pattern at line ~1340.
|
|
557
|
+
*
|
|
558
|
+
* @returns {Promise<{success: boolean, reason?: string}>}
|
|
559
|
+
*/
|
|
560
|
+
async _recoverFromPersistedGrid() {
|
|
561
|
+
if (!this.accountOrders || !this.manager) {
|
|
562
|
+
return { success: false, reason: 'accountOrders or manager unavailable' };
|
|
563
|
+
}
|
|
564
|
+
const accountRef = this.accountId || this.account?.id || this.account;
|
|
565
|
+
if (!accountRef) {
|
|
566
|
+
return { success: false, reason: 'no account reference' };
|
|
567
|
+
}
|
|
568
|
+
this.manager.logger.log('[RECOVERY] Attempting full grid reload from persisted snapshot...', 'warn');
|
|
569
|
+
try {
|
|
570
|
+
// 1. Force reload from disk
|
|
571
|
+
const persistedGrid = this.accountOrders.loadGrid(true);
|
|
572
|
+
if (!persistedGrid || persistedGrid.length === 0) {
|
|
573
|
+
return { success: false, reason: 'no persisted grid on disk' };
|
|
574
|
+
}
|
|
575
|
+
const boundaryIdx = this.accountOrders.loadBoundaryIdx(true);
|
|
576
|
+
// 2. Load into manager (same path as startup at line 1340)
|
|
577
|
+
await Grid.loadGrid(this.manager, persistedGrid, boundaryIdx);
|
|
578
|
+
// 3. Read current chain state
|
|
579
|
+
const chainOpenOrders = await chainOrders.readOpenOrders(accountRef);
|
|
580
|
+
// 4. Reconcile
|
|
581
|
+
if (chainOpenOrders.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
582
|
+
await this.manager.syncFromOpenOrders(chainOpenOrders, {
|
|
583
|
+
skipAccounting: true,
|
|
584
|
+
fillLockAlreadyHeld: true,
|
|
585
|
+
protectCommittedOrders: true
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
// 5. Persist the reconciled state
|
|
589
|
+
if (typeof this.manager.persistGrid === 'function') {
|
|
590
|
+
await this.manager.persistGrid();
|
|
591
|
+
}
|
|
592
|
+
this.manager.logger.log(`[RECOVERY] Grid reloaded from persisted snapshot: ${this.manager.orders.size} orders, ` +
|
|
593
|
+
`${chainOpenOrders.length} on-chain orders synced`, 'info');
|
|
594
|
+
return { success: true };
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
this.manager.logger.log(`[RECOVERY] Full grid reload from persisted snapshot failed: ${err.message}`, 'error');
|
|
598
|
+
return { success: false, reason: err.message };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
541
601
|
/**
|
|
542
602
|
* Attempt to repair size-drift for specific order IDs by reading their
|
|
543
603
|
* current on-chain state and correcting the local grid directly.
|
|
@@ -1147,6 +1207,12 @@ class DEXBot {
|
|
|
1147
1207
|
await Grid.loadGrid(this.manager, persistedGrid, persistedBoundaryIdx);
|
|
1148
1208
|
let startupChainOpenOrders = chainOpenOrders;
|
|
1149
1209
|
const syncResult = await this.manager.syncFromOpenOrders(startupChainOpenOrders, { skipAccounting: true, fillLockAlreadyHeld: true });
|
|
1210
|
+
// Process price corrections queued during startup sync.
|
|
1211
|
+
// These are not picked up by _consumeFillQueue until a fill
|
|
1212
|
+
// arrives, which may never come for an idle market.
|
|
1213
|
+
if (syncResult.ordersNeedingCorrection?.length > 0) {
|
|
1214
|
+
await correctAllPriceMismatches(this.manager, this.account, this.privateKey, chainOrders);
|
|
1215
|
+
}
|
|
1150
1216
|
if (syncResult.filledOrders && syncResult.filledOrders.length > 0) {
|
|
1151
1217
|
this._log(`Startup sync: ${syncResult.filledOrders.length} grid order(s) found filled. Processing proceeds.`, 'info');
|
|
1152
1218
|
const batchResult = await this._processFillsWithBatching(syncResult.filledOrders, new Set(), 'startup sync fill rebalance', { skipAccountTotalsUpdate: true });
|
|
@@ -1438,6 +1504,14 @@ class DEXBot {
|
|
|
1438
1504
|
return;
|
|
1439
1505
|
}
|
|
1440
1506
|
await this.manager._fillProcessingLock.acquire(async () => {
|
|
1507
|
+
// Reset orphan-fill credit timestamp at the start of each
|
|
1508
|
+
// fill cycle. It is re-set when orphan fills are credited,
|
|
1509
|
+
// and consumed (set to null) on the next fund-invariant check
|
|
1510
|
+
// in accounting.ts, which widens tolerance by 5x while set.
|
|
1511
|
+
// Also cleared by _performStateRecovery after a fresh chain
|
|
1512
|
+
// fetch. The timestamp value itself is not compared against a
|
|
1513
|
+
// window — it acts as a consume-on-read boolean.
|
|
1514
|
+
this._orphanFillsCreditedAt = null;
|
|
1441
1515
|
while (this._incomingFillQueue.length > 0) {
|
|
1442
1516
|
const batchStartTime = Date.now();
|
|
1443
1517
|
// Track max queue depth
|
|
@@ -1501,6 +1575,10 @@ class DEXBot {
|
|
|
1501
1575
|
if (accountingResult.status === 'missing_key') {
|
|
1502
1576
|
requiresOpenOrdersSync = true;
|
|
1503
1577
|
}
|
|
1578
|
+
// Record orphan fill credit timestamp for fund invariant
|
|
1579
|
+
// tolerance widening. Accounting checks recency via
|
|
1580
|
+
// _checkOrphanFillRecency() instead of a cross-module flag.
|
|
1581
|
+
this._orphanFillsCreditedAt = Date.now();
|
|
1504
1582
|
// Don't add to validFills - we can't do rebalancing without a grid slot
|
|
1505
1583
|
// But the funds are now credited, preventing fund invariant violation
|
|
1506
1584
|
continue;
|
|
@@ -1547,6 +1625,7 @@ class DEXBot {
|
|
|
1547
1625
|
// 3. Sync and Collect Filled Orders
|
|
1548
1626
|
let allFilledOrders = [];
|
|
1549
1627
|
let ordersNeedingCorrection = [];
|
|
1628
|
+
const pendingGhostOrders = new Set();
|
|
1550
1629
|
const fillMode = chainOrders.getFillProcessingMode();
|
|
1551
1630
|
const processValidFills = async (fillsToSync) => {
|
|
1552
1631
|
let resolvedOrders = [];
|
|
@@ -1568,6 +1647,8 @@ class DEXBot {
|
|
|
1568
1647
|
resolvedOrders.push(...resultHistory.filledOrders);
|
|
1569
1648
|
if (resultHistory.requiresOpenOrdersSync)
|
|
1570
1649
|
requiresOpenOrdersSync = true;
|
|
1650
|
+
if (resultHistory.ghostOrderId)
|
|
1651
|
+
pendingGhostOrders.add(resultHistory.ghostOrderId);
|
|
1571
1652
|
}
|
|
1572
1653
|
}
|
|
1573
1654
|
if (fillMode !== 'history' || requiresOpenOrdersSync) {
|
|
@@ -1587,7 +1668,49 @@ class DEXBot {
|
|
|
1587
1668
|
};
|
|
1588
1669
|
this.manager.pauseFundRecalc();
|
|
1589
1670
|
try {
|
|
1590
|
-
|
|
1671
|
+
// FIX 1: Block-level fill batching — group valid fills by block
|
|
1672
|
+
// and process each block group as a unit. This prevents slot
|
|
1673
|
+
// collisions when fills from the same block (original + replacement
|
|
1674
|
+
// orders both filled) arrive in different sync batches. Processing
|
|
1675
|
+
// all fills from a block together lets the sync engine see every
|
|
1676
|
+
// fill for overlapping slots simultaneously.
|
|
1677
|
+
const fillsByBlock = new Map();
|
|
1678
|
+
const fillsWithoutBlock = [];
|
|
1679
|
+
for (const fill of validFills) {
|
|
1680
|
+
if (fill.block_num != null) {
|
|
1681
|
+
const list = fillsByBlock.get(fill.block_num);
|
|
1682
|
+
if (list)
|
|
1683
|
+
list.push(fill);
|
|
1684
|
+
else
|
|
1685
|
+
fillsByBlock.set(fill.block_num, [fill]);
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
fillsWithoutBlock.push(fill);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
// Process block groups in ascending block order so the sync
|
|
1692
|
+
// engine sees a deterministic, chronological fill sequence.
|
|
1693
|
+
const sortedBlocks = [...fillsByBlock.keys()].sort((a, b) => a - b);
|
|
1694
|
+
const accumulatedOrders = [];
|
|
1695
|
+
let anyRequiresSync = false;
|
|
1696
|
+
for (const blockNum of sortedBlocks) {
|
|
1697
|
+
// Reset requiresOpenOrdersSync per block group so one
|
|
1698
|
+
// block's history-id gap doesn't force the next block
|
|
1699
|
+
// into an unnecessary open-orders snapshot re-fetch.
|
|
1700
|
+
requiresOpenOrdersSync = false;
|
|
1701
|
+
this.manager.logger.log(`[FILL-BLOCK] Processing ${fillsByBlock.get(blockNum).length} fill(s) from block ${blockNum}`, 'debug');
|
|
1702
|
+
const blockResult = await processValidFills(fillsByBlock.get(blockNum));
|
|
1703
|
+
accumulatedOrders.push(...blockResult);
|
|
1704
|
+
if (requiresOpenOrdersSync)
|
|
1705
|
+
anyRequiresSync = true;
|
|
1706
|
+
}
|
|
1707
|
+
requiresOpenOrdersSync = anyRequiresSync;
|
|
1708
|
+
if (fillsWithoutBlock.length > 0) {
|
|
1709
|
+
this.manager.logger.log(`[FILL-BLOCK] Processing ${fillsWithoutBlock.length} fill(s) without block info`, 'debug');
|
|
1710
|
+
const noBlockResult = await processValidFills(fillsWithoutBlock);
|
|
1711
|
+
accumulatedOrders.push(...noBlockResult);
|
|
1712
|
+
}
|
|
1713
|
+
allFilledOrders = accumulatedOrders;
|
|
1591
1714
|
// 4. Handle Price Corrections
|
|
1592
1715
|
if (ordersNeedingCorrection.length > 0) {
|
|
1593
1716
|
const correctionResult = await correctAllPriceMismatches(this.manager, this.account, this.privateKey, chainOrders);
|
|
@@ -1596,6 +1719,30 @@ class DEXBot {
|
|
|
1596
1719
|
}
|
|
1597
1720
|
}
|
|
1598
1721
|
finally {
|
|
1722
|
+
// 4b. Cancel orphaned chain orders detected by other-side rounding.
|
|
1723
|
+
// These orders have a tiny residual that the bot treated as fully
|
|
1724
|
+
// filled, but the blockchain did not close the order. We send a
|
|
1725
|
+
// cancel tx to clean up the zombie order on chain.
|
|
1726
|
+
// Runs in the finally so it still fires even if processValidFills
|
|
1727
|
+
// throws after collecting ghost IDs into pendingGhostOrders.
|
|
1728
|
+
// Each cancelOrder is individually try/caught so no per-order
|
|
1729
|
+
// error can prevent resumeFundRecalc from running.
|
|
1730
|
+
if (pendingGhostOrders.size > 0) {
|
|
1731
|
+
if (!this._ghostOrderCancelAttempted)
|
|
1732
|
+
this._ghostOrderCancelAttempted = new Set();
|
|
1733
|
+
for (const ghostOrderId of pendingGhostOrders) {
|
|
1734
|
+
if (this._ghostOrderCancelAttempted.has(ghostOrderId))
|
|
1735
|
+
continue;
|
|
1736
|
+
try {
|
|
1737
|
+
this.manager.logger.log(`[SYNC] Cancelling orphaned chain order ${ghostOrderId} (other-side full-fill residual)`, 'info');
|
|
1738
|
+
await chainOrders.cancelOrder(this.account, this.privateKey, ghostOrderId);
|
|
1739
|
+
this._ghostOrderCancelAttempted.add(ghostOrderId);
|
|
1740
|
+
}
|
|
1741
|
+
catch (err) {
|
|
1742
|
+
this.manager.logger.log(`[SYNC] Failed to cancel orphaned order ${ghostOrderId}: ${err?.message || err}`, 'warn');
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1599
1746
|
await this.manager.resumeFundRecalc();
|
|
1600
1747
|
}
|
|
1601
1748
|
// 5. Fixed-Cap Fill Rebalance
|
|
@@ -2054,7 +2201,8 @@ class DEXBot {
|
|
|
2054
2201
|
const openOrders = await chainOrders.readOpenOrders(accountRef);
|
|
2055
2202
|
const recoveryResult = await this.manager.syncFromOpenOrders(openOrders, {
|
|
2056
2203
|
skipAccounting: false,
|
|
2057
|
-
fillLockAlreadyHeld: true
|
|
2204
|
+
fillLockAlreadyHeld: true,
|
|
2205
|
+
protectCommittedOrders: true
|
|
2058
2206
|
});
|
|
2059
2207
|
this._preserveMissingCreateBlockersAfterRecovery(preRecoveryMissingCreateBlockers, recoveryResult);
|
|
2060
2208
|
// Persist any master grid mutations from the recovery sync. The
|
|
@@ -2442,7 +2590,7 @@ class DEXBot {
|
|
|
2442
2590
|
return { executed: false, hadRotation: false, uncertain: true };
|
|
2443
2591
|
}
|
|
2444
2592
|
const adopted = [];
|
|
2445
|
-
|
|
2593
|
+
let discarded = [];
|
|
2446
2594
|
// 2. For each pending broadcast, look for a chain match.
|
|
2447
2595
|
for (const entry of pending) {
|
|
2448
2596
|
const match = this._findChainOrderForSlot(chainSnapshot, entry.slotId, {
|
|
@@ -2456,44 +2604,82 @@ class DEXBot {
|
|
|
2456
2604
|
this.manager._pendingBroadcasts.delete(entry.fingerprint);
|
|
2457
2605
|
}
|
|
2458
2606
|
else {
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
//
|
|
2467
|
-
//
|
|
2468
|
-
//
|
|
2469
|
-
//
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
const shouldRunHeavySync = !(allCreatesAdopted && discarded.length === 0);
|
|
2478
|
-
if (shouldRunHeavySync) {
|
|
2607
|
+
// Preserve full entry context so the recheck below can use
|
|
2608
|
+
// both the fingerprint match path (against _pendingBroadcasts)
|
|
2609
|
+
// and the near-match path (via finalInts/orderType), not just
|
|
2610
|
+
// slotId alone.
|
|
2611
|
+
discarded.push(entry);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
// ---- RACE MITIGATION: block-delay recheck before discarding creates ----
|
|
2615
|
+
// BROADCAST_DEADLINE fires while the transaction may still be valid
|
|
2616
|
+
// and land in the next block. Wait for up to 3 blocks (~1 BitShares
|
|
2617
|
+
// block interval each) and re-check before permanently discarding.
|
|
2618
|
+
const UNCERTAIN_RECHECK_MAX_ATTEMPTS = PIPELINE_TIMING.RETRY_MAX_ATTEMPTS; // 3
|
|
2619
|
+
const UNCERTAIN_RECHECK_INTERVAL_MS = TIMING.MILLISECONDS_PER_SECOND * 3; // 3s
|
|
2620
|
+
let recheckRound = 0;
|
|
2621
|
+
while (discarded.length > 0 && recheckRound < UNCERTAIN_RECHECK_MAX_ATTEMPTS) {
|
|
2622
|
+
recheckRound++;
|
|
2623
|
+
await new Promise(r => setTimeout(r, UNCERTAIN_RECHECK_INTERVAL_MS));
|
|
2624
|
+
let freshSnapshot;
|
|
2479
2625
|
try {
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2626
|
+
freshSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
2627
|
+
}
|
|
2628
|
+
catch {
|
|
2629
|
+
break;
|
|
2630
|
+
}
|
|
2631
|
+
const stillDiscarded = [];
|
|
2632
|
+
for (const entry of discarded) {
|
|
2633
|
+
const match = this._findChainOrderForSlot(freshSnapshot, entry.slotId, {
|
|
2634
|
+
sell: entry.finalInts?.sell,
|
|
2635
|
+
receive: entry.finalInts?.receive,
|
|
2636
|
+
orderType: entry.orderType || entry.order?.type,
|
|
2637
|
+
fingerprint: entry.fingerprint
|
|
2638
|
+
});
|
|
2639
|
+
if (match) {
|
|
2640
|
+
adopted.push({ slotId: entry.slotId, chainOrderId: match.id });
|
|
2641
|
+
this.manager.logger.log(`[COW][UNCERTAIN] CREATE re-adopted after ${recheckRound} block(s): ` +
|
|
2642
|
+
`${entry.slotId}->${match.id}`, 'info');
|
|
2643
|
+
}
|
|
2644
|
+
else {
|
|
2645
|
+
stillDiscarded.push(entry);
|
|
2486
2646
|
}
|
|
2487
2647
|
}
|
|
2648
|
+
discarded = stillDiscarded;
|
|
2649
|
+
}
|
|
2650
|
+
// 3. Re-read chain with latest state. The initial snapshot may be
|
|
2651
|
+
// stale for non-CREATE ops (updates/cancels) not tracked in
|
|
2652
|
+
// _pendingBroadcasts — the seed cause of the slot-115 cascade.
|
|
2653
|
+
let latestSnapshot;
|
|
2654
|
+
try {
|
|
2655
|
+
latestSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
2656
|
+
}
|
|
2657
|
+
catch {
|
|
2658
|
+
latestSnapshot = chainSnapshot;
|
|
2659
|
+
}
|
|
2660
|
+
// Always sync to link adopted chain orders into grid slots.
|
|
2661
|
+
// Otherwise an adopted slot stays VIRTUAL and the next COW cycle
|
|
2662
|
+
// places a duplicate order at the same price.
|
|
2663
|
+
let hadRotation = false;
|
|
2664
|
+
if (latestSnapshot && latestSnapshot.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
2665
|
+
try {
|
|
2666
|
+
await this.manager.syncFromOpenOrders(latestSnapshot, {
|
|
2667
|
+
skipAccounting: true,
|
|
2668
|
+
fillLockAlreadyHeld: true,
|
|
2669
|
+
protectCommittedOrders: true
|
|
2670
|
+
});
|
|
2671
|
+
hadRotation = true;
|
|
2672
|
+
}
|
|
2488
2673
|
catch (syncErr) {
|
|
2489
2674
|
this.manager.logger.log(`[COW][UNCERTAIN] syncFromOpenOrders failed during recovery: ${syncErr?.message || syncErr}`, 'error');
|
|
2675
|
+
// Fallback: reload from persisted snapshot + re-sync.
|
|
2676
|
+
this.manager.logger.log('[COW][UNCERTAIN] syncFromOpenOrders failed; falling back to full grid reload from persisted snapshot', 'warn');
|
|
2677
|
+
const fallbackResult = await this._recoverFromPersistedGrid();
|
|
2678
|
+
if (fallbackResult.success) {
|
|
2679
|
+
hadRotation = true;
|
|
2680
|
+
}
|
|
2490
2681
|
}
|
|
2491
2682
|
}
|
|
2492
|
-
else {
|
|
2493
|
-
hadRotation = true;
|
|
2494
|
-
this.manager.logger.log(`[COW][UNCERTAIN] All ${adopted.length} fingerprinted CREATE(s) adopted; ` +
|
|
2495
|
-
`skipping heavy re-sync to avoid false-positive unmatched warnings.`, 'debug');
|
|
2496
|
-
}
|
|
2497
2683
|
// 4. Log structured summary.
|
|
2498
2684
|
const elapsedMs = Date.now() - startedAt;
|
|
2499
2685
|
const heartbeatAgeMs = this._lastBroadcastHeartbeatAt
|
|
@@ -2501,14 +2687,14 @@ class DEXBot {
|
|
|
2501
2687
|
: null;
|
|
2502
2688
|
this.manager.logger.log(`[COW][UNCERTAIN] batchId=${err?.batchId || 'n/a'} ops=${opContexts.length} ` +
|
|
2503
2689
|
`staleSinceMs=${err?.timeoutMs || 'n/a'} heartbeatAgeMs=${heartbeatAgeMs ?? 'n/a'} ` +
|
|
2504
|
-
`adopted=${adopted.length} discarded=${discarded.length} elapsedMs=${elapsedMs}`, adopted.length > 0 ? 'info' : 'warn');
|
|
2690
|
+
`adopted=${adopted.length} discarded=${discarded.length} recheckRounds=${recheckRound} elapsedMs=${elapsedMs}`, adopted.length > 0 ? 'info' : 'warn');
|
|
2505
2691
|
if (adopted.length > 0) {
|
|
2506
2692
|
this.manager.logger.log(`[COW][UNCERTAIN] Adopted chain orders: ${adopted
|
|
2507
2693
|
.map(a => `${a.slotId}->${a.chainOrderId}`)
|
|
2508
2694
|
.join(', ')}`, 'info');
|
|
2509
2695
|
}
|
|
2510
2696
|
if (discarded.length > 0) {
|
|
2511
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Discarded planned CREATEs (no chain match): ${discarded
|
|
2697
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Discarded planned CREATEs (no chain match after ${recheckRound} recheck(s)): ${discarded
|
|
2512
2698
|
.map(d => d.slotId)
|
|
2513
2699
|
.join(', ')}`, 'warn');
|
|
2514
2700
|
}
|
|
@@ -2557,8 +2743,18 @@ class DEXBot {
|
|
|
2557
2743
|
*/
|
|
2558
2744
|
async _autoCancelOneUnmatchedOrphan() {
|
|
2559
2745
|
const cycleId = this._currentCycleId || 0;
|
|
2746
|
+
// FIX 4: Higher cap during recovery mode so orphan backlog clears faster.
|
|
2747
|
+
const recoveryActive = this.manager?._recoveryState?.structuralResyncRequested === true;
|
|
2748
|
+
const cycleCap = recoveryActive ? 5 : 1;
|
|
2560
2749
|
if (this._autoCancelOrphanCycleMarker === cycleId) {
|
|
2561
|
-
|
|
2750
|
+
if (this._autoCancelOrphanSubCount >= cycleCap) {
|
|
2751
|
+
return { cancelled: false, reason: 'cap-reached-this-cycle', subCount: this._autoCancelOrphanSubCount };
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
else {
|
|
2755
|
+
// Reset sub-count on first call this cycle
|
|
2756
|
+
this._autoCancelOrphanCycleMarker = cycleId;
|
|
2757
|
+
this._autoCancelOrphanSubCount = 0;
|
|
2562
2758
|
}
|
|
2563
2759
|
const pending = (this.manager && this.manager._pendingBroadcasts instanceof Map)
|
|
2564
2760
|
? this.manager._pendingBroadcasts.size
|
|
@@ -2587,13 +2783,15 @@ class DEXBot {
|
|
|
2587
2783
|
return { cancelled: false, reason: 'cancelOrder-unavailable' };
|
|
2588
2784
|
}
|
|
2589
2785
|
try {
|
|
2590
|
-
this._autoCancelOrphanCycleMarker = cycleId;
|
|
2591
|
-
this.manager.logger.log(`[COW] Auto-cancelling 1/${unmatched.length} unmatched chain order ` +
|
|
2592
|
-
`(${this._formatUnmatchedChainOrderForLog(target)}) — per-cycle cap=1.`, 'warn');
|
|
2593
2786
|
await chainOrders.cancelOrder(this.account, this.privateKey, orderId);
|
|
2594
2787
|
if (typeof chainOrders.recordOwnCancel === 'function') {
|
|
2595
2788
|
chainOrders.recordOwnCancel(orderId);
|
|
2596
2789
|
}
|
|
2790
|
+
// Increment sub-count only after a confirmed successful cancel.
|
|
2791
|
+
// Failed cancels do not consume the per-cycle cap budget.
|
|
2792
|
+
this._autoCancelOrphanSubCount++;
|
|
2793
|
+
this.manager.logger.log(`[COW] Auto-cancelled ${this._autoCancelOrphanSubCount}/${unmatched.length} unmatched chain order ` +
|
|
2794
|
+
`(${this._formatUnmatchedChainOrderForLog(target)}) — per-cycle cap=${cycleCap}.`, 'warn');
|
|
2597
2795
|
return { cancelled: true, orderId };
|
|
2598
2796
|
}
|
|
2599
2797
|
catch (err) {
|
|
@@ -2647,6 +2845,21 @@ class DEXBot {
|
|
|
2647
2845
|
if (isRetriable) {
|
|
2648
2846
|
this.manager.logger.log(`[COW] Broadcast uncertain (attempt ${attempt}/${MAX_RETRIES + 1}), retrying...`, 'warn');
|
|
2649
2847
|
await this._ensureCredentialDaemonWritable('COW batch retry');
|
|
2848
|
+
// Reconcile before retry — stale ops would fail "not found".
|
|
2849
|
+
try {
|
|
2850
|
+
const accountRef = this.accountId || this.account?.id || this.account;
|
|
2851
|
+
const freshChain = await chainOrders.readOpenOrders(accountRef);
|
|
2852
|
+
if (freshChain.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
2853
|
+
await this.manager.syncFromOpenOrders(freshChain, {
|
|
2854
|
+
skipAccounting: true,
|
|
2855
|
+
fillLockAlreadyHeld: true,
|
|
2856
|
+
protectCommittedOrders: true
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
catch (syncErr) {
|
|
2861
|
+
this.manager.logger.log(`[COW] Pre-retry sync failed (non-fatal): ${syncErr?.message || syncErr}`, 'warn');
|
|
2862
|
+
}
|
|
2650
2863
|
continue;
|
|
2651
2864
|
}
|
|
2652
2865
|
throw err;
|
|
@@ -3409,58 +3622,111 @@ class DEXBot {
|
|
|
3409
3622
|
? Array.from(this.manager._pendingBroadcasts.values())
|
|
3410
3623
|
: [];
|
|
3411
3624
|
if (hasCreateActions && (unmatchedChainOrders.length > 0 || pendingBroadcasts.length > 0)) {
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
if
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3625
|
+
// ---- FIX 3: Absorb unmatched orders instead of rejecting ----
|
|
3626
|
+
// When unmatched chain orders exist, try to auto-cancel them
|
|
3627
|
+
// inline to unblock the CREATE batch. Only fall through to
|
|
3628
|
+
// full rejection + structural resync if cancellation fails.
|
|
3629
|
+
let cancelledUnmatched = 0;
|
|
3630
|
+
if (unmatchedChainOrders.length > 0 && pendingBroadcasts.length === 0) {
|
|
3631
|
+
// Bounded-parallel cancellation: process unmatched orders in
|
|
3632
|
+
// concurrent batches (3 at a time) to avoid serialising 19+
|
|
3633
|
+
// individual cancel txs (~0.5s each = 10s batch hold).
|
|
3634
|
+
const CANCEL_CONCURRENCY = 3;
|
|
3635
|
+
const cancelable = unmatchedChainOrders.filter(u => {
|
|
3636
|
+
const oid = u?.id || u?.orderId || u?.chainOrderId;
|
|
3637
|
+
return Boolean(oid) && !u?.fingerprint;
|
|
3638
|
+
});
|
|
3639
|
+
for (let i = 0; i < cancelable.length; i += CANCEL_CONCURRENCY) {
|
|
3640
|
+
const batch = cancelable.slice(i, i + CANCEL_CONCURRENCY);
|
|
3641
|
+
const results = await Promise.allSettled(batch.map(async (unmatched) => {
|
|
3642
|
+
const orderId = unmatched.id || unmatched.orderId || unmatched.chainOrderId;
|
|
3643
|
+
this.manager.logger.log(`[COW] Auto-cancelling unmatched chain order ${orderId} ` +
|
|
3644
|
+
`(${this._formatUnmatchedChainOrderForLog(unmatched)}) to unblock CREATE batch.`, 'warn');
|
|
3645
|
+
await chainOrders.cancelOrder(this.account, this.privateKey, orderId);
|
|
3646
|
+
if (typeof chainOrders.recordOwnCancel === 'function') {
|
|
3647
|
+
chainOrders.recordOwnCancel(orderId);
|
|
3648
|
+
}
|
|
3649
|
+
}));
|
|
3650
|
+
for (const r of results) {
|
|
3651
|
+
if (r.status === 'fulfilled')
|
|
3652
|
+
cancelledUnmatched++;
|
|
3653
|
+
else
|
|
3654
|
+
this.manager.logger.log(`[COW] Failed to cancel unmatched chain order: ${r.reason?.message || r.reason}`, 'warn');
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
if (cancelledUnmatched >= cancelable.length) {
|
|
3658
|
+
this.manager.logger.log(`[COW] Cancelled all ${cancelledUnmatched} unmatched chain order(s); proceeding with CREATE batch.`, 'warn');
|
|
3659
|
+
// Optimistic clear: the cancel txs are sent but not yet
|
|
3660
|
+
// confirmed. If a cancel silently fails on chain, the
|
|
3661
|
+
// next sync cycle will re-detect the unmatched order and
|
|
3662
|
+
// re-enter this guard — that's safe because the cancel
|
|
3663
|
+
// is idempotent and the re-detection is non-blocking.
|
|
3664
|
+
this.manager._lastUnmatchedChainOrders = [];
|
|
3665
|
+
}
|
|
3666
|
+
else {
|
|
3667
|
+
this.manager.logger.log(`[COW] Cancelled ${cancelledUnmatched}/${cancelable.length} unmatched chain order(s); ` +
|
|
3668
|
+
`remaining must be handled by structural reconciliation.`, 'warn');
|
|
3669
|
+
}
|
|
3430
3670
|
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3671
|
+
// Re-check after auto-cancel attempt
|
|
3672
|
+
const remainingUnmatched = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
3673
|
+
? this.manager._lastUnmatchedChainOrders
|
|
3674
|
+
: [];
|
|
3675
|
+
if (remainingUnmatched.length > 0 || pendingBroadcasts.length > 0) {
|
|
3676
|
+
const blockers = [];
|
|
3677
|
+
if (remainingUnmatched.length > 0)
|
|
3678
|
+
blockers.push(`${remainingUnmatched.length} unmatched chain order(s)`);
|
|
3679
|
+
if (pendingBroadcasts.length > 0)
|
|
3680
|
+
blockers.push(`${pendingBroadcasts.length} pending broadcast(s)`);
|
|
3681
|
+
const reasonText = blockers.join(' and ');
|
|
3682
|
+
if (pendingBroadcasts.length > 0) {
|
|
3683
|
+
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${reasonText} from a prior uncertain ` +
|
|
3684
|
+
`broadcast. Running recovery before placing replacement orders.`, 'error');
|
|
3685
|
+
}
|
|
3686
|
+
else {
|
|
3687
|
+
const sample = remainingUnmatched
|
|
3688
|
+
.slice(0, 3)
|
|
3689
|
+
.map(order => this._formatUnmatchedChainOrderForLog(order))
|
|
3690
|
+
.join(' | ');
|
|
3691
|
+
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${reasonText} ` +
|
|
3692
|
+
`are not represented in the grid${sample ? ` (${sample})` : ''}. ` +
|
|
3693
|
+
`Run structural reconciliation before placing replacement orders.`, 'error');
|
|
3451
3694
|
}
|
|
3452
|
-
|
|
3453
|
-
this.manager.
|
|
3695
|
+
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
3696
|
+
if (this.manager._recoveryState)
|
|
3697
|
+
this.manager._recoveryState.structuralResyncRequested = true;
|
|
3698
|
+
await this.manager.requestStructuralGridResync(pendingBroadcasts.length > 0
|
|
3699
|
+
? 'pending broadcasts before COW create'
|
|
3700
|
+
: 'unmatched chain orders before COW create', pendingBroadcasts.length > 0
|
|
3701
|
+
? { pendingBroadcasts: pendingBroadcasts.map(p => p.slotId) }
|
|
3702
|
+
: { unmatchedChainOrders: remainingUnmatched });
|
|
3454
3703
|
}
|
|
3704
|
+
// If we have pending broadcasts, drive the recovery now so the
|
|
3705
|
+
// next planning cycle has a clean state.
|
|
3706
|
+
if (pendingBroadcasts.length > 0) {
|
|
3707
|
+
try {
|
|
3708
|
+
await this._reconcileAfterUncertainBroadcast(new BroadcastUncertainError('rejected CREATE batch had pending broadcasts', {
|
|
3709
|
+
operations: pendingBroadcasts.map(p => p.order),
|
|
3710
|
+
accountName: this.account,
|
|
3711
|
+
batchId: this._currentBatchId || null,
|
|
3712
|
+
payload: null,
|
|
3713
|
+
timeoutMs: null
|
|
3714
|
+
}), [], { fillLockAlreadyHeld: true });
|
|
3715
|
+
}
|
|
3716
|
+
catch (recoverErr) {
|
|
3717
|
+
this.manager.logger.log(`[COW] Recovery from pending broadcasts failed: ${recoverErr?.message || recoverErr}`, 'error');
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
return {
|
|
3721
|
+
executed: false,
|
|
3722
|
+
aborted: true,
|
|
3723
|
+
reason: pendingBroadcasts.length > 0 ? 'PENDING_BROADCASTS' : 'UNMATCHED_CHAIN_ORDERS',
|
|
3724
|
+
unmatchedChainOrders: pendingBroadcasts.length > 0 ? [] : remainingUnmatched,
|
|
3725
|
+
pendingBroadcasts: pendingBroadcasts.map(p => p.slotId),
|
|
3726
|
+
hadRotation: false
|
|
3727
|
+
};
|
|
3455
3728
|
}
|
|
3456
|
-
|
|
3457
|
-
executed: false,
|
|
3458
|
-
aborted: true,
|
|
3459
|
-
reason: pendingBroadcasts.length > 0 ? 'PENDING_BROADCASTS' : 'UNMATCHED_CHAIN_ORDERS',
|
|
3460
|
-
unmatchedChainOrders: pendingBroadcasts.length > 0 ? [] : unmatchedChainOrders,
|
|
3461
|
-
pendingBroadcasts: pendingBroadcasts.map(p => p.slotId),
|
|
3462
|
-
hadRotation: false
|
|
3463
|
-
};
|
|
3729
|
+
// All unmatched were cancelled — fall through to continue batch
|
|
3464
3730
|
}
|
|
3465
3731
|
const { assetA, assetB } = this.manager.assets;
|
|
3466
3732
|
const operations = [];
|
|
@@ -3501,7 +3767,13 @@ class DEXBot {
|
|
|
3501
3767
|
opContexts.push({ kind: 'cancel', order });
|
|
3502
3768
|
}
|
|
3503
3769
|
catch (err) {
|
|
3504
|
-
|
|
3770
|
+
const orderNotFound = /\bnot found\b/i.test(err.message) || /\bdoes not exist\b/i.test(err.message);
|
|
3771
|
+
if (orderNotFound) {
|
|
3772
|
+
this.manager.logger.log(`[COW] Cancel skipped for ${action.id} (${action.orderId}): order already removed from chain`, 'debug');
|
|
3773
|
+
}
|
|
3774
|
+
else {
|
|
3775
|
+
this.manager.logger.log(`Failed to prepare cancel op for ${action.id}: ${err.message}`, 'error');
|
|
3776
|
+
}
|
|
3505
3777
|
}
|
|
3506
3778
|
}
|
|
3507
3779
|
else if (action.type === COW_ACTIONS.CREATE) {
|
|
@@ -3613,8 +3885,60 @@ class DEXBot {
|
|
|
3613
3885
|
type: orderType
|
|
3614
3886
|
};
|
|
3615
3887
|
opContexts.push({ kind: 'size-update', updateInfo: { partialOrder, newSize }, finalInts: op.finalInts });
|
|
3888
|
+
// Catch for both rotation-update and size-update branches.
|
|
3616
3889
|
}
|
|
3617
3890
|
catch (err) {
|
|
3891
|
+
const orderNotFound = /\bnot found\b/i.test(err.message) || /\bdoes not exist\b/i.test(err.message);
|
|
3892
|
+
if (orderNotFound) {
|
|
3893
|
+
try {
|
|
3894
|
+
const fbOrder = action.order || this.manager.orders.get(action.id);
|
|
3895
|
+
const fbType = fbOrder?.type;
|
|
3896
|
+
const fbSize = action.newSize || fbOrder?.size || 0;
|
|
3897
|
+
// Live price from TARGET slot (same pattern as CREATE path).
|
|
3898
|
+
const targetSlotId = action.newGridId || action.id;
|
|
3899
|
+
const plannedPrice = action.newPrice || action.order?.price || 0;
|
|
3900
|
+
const liveSlotForPrice = this.manager.orders.get(targetSlotId);
|
|
3901
|
+
const livePrice = liveSlotForPrice ? Number(liveSlotForPrice.price) : NaN;
|
|
3902
|
+
const priceDrift = Number.isFinite(plannedPrice) && Number.isFinite(livePrice)
|
|
3903
|
+
? Math.abs(livePrice - plannedPrice)
|
|
3904
|
+
: 0;
|
|
3905
|
+
const fbPrice = (priceDrift > 0) ? livePrice : plannedPrice;
|
|
3906
|
+
if (priceDrift > 0) {
|
|
3907
|
+
this.manager.logger.log(`[COW] CREATE fallback price drift for ${action.id} -> ${targetSlotId}: ` +
|
|
3908
|
+
`planned=${plannedPrice} live=${livePrice} (diff=${priceDrift})`, 'debug');
|
|
3909
|
+
}
|
|
3910
|
+
// Same size gate as regular CREATE path.
|
|
3911
|
+
const sizeCheck = this._validateOrderSizeForExecution(fbSize, fbType, fbOrder, fbSize);
|
|
3912
|
+
if (!sizeCheck.isValid) {
|
|
3913
|
+
this.manager.logger.log(`[COW] CREATE fallback for ${action.id} rejected by size validation: ${sizeCheck.reason}`, 'warn');
|
|
3914
|
+
}
|
|
3915
|
+
else if (fbType && fbSize > 0 && fbPrice > 0) {
|
|
3916
|
+
const fbArgs = buildCreateOrderArgs({ type: fbType, size: fbSize, price: fbPrice }, assetA, assetB);
|
|
3917
|
+
const fbResult = await chainOrders.buildCreateOrderOp(this.account, fbArgs.amountToSell, fbArgs.sellAssetId, fbArgs.minToReceive, fbArgs.receiveAssetId, null);
|
|
3918
|
+
if (fbResult) {
|
|
3919
|
+
operations.push(fbResult.op);
|
|
3920
|
+
opContexts.push({
|
|
3921
|
+
kind: 'create',
|
|
3922
|
+
id: targetSlotId,
|
|
3923
|
+
order: { id: targetSlotId, type: fbType, price: fbPrice, size: fbSize },
|
|
3924
|
+
args: { amountToSell: fbArgs.amountToSell, minToReceive: fbArgs.minToReceive },
|
|
3925
|
+
finalInts: fbResult.finalInts
|
|
3926
|
+
});
|
|
3927
|
+
this._recordPendingBroadcast({
|
|
3928
|
+
opIndex: operations.length - 1,
|
|
3929
|
+
ctxIndex: opContexts.length - 1,
|
|
3930
|
+
order: { id: targetSlotId, type: fbType, price: fbPrice, size: fbSize },
|
|
3931
|
+
finalInts: fbResult.finalInts
|
|
3932
|
+
});
|
|
3933
|
+
this.manager.logger.log(`[COW] Recovered "not found" for ${action.id}: converted UPDATE to CREATE for slot ${targetSlotId}`, 'warn');
|
|
3934
|
+
continue;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
catch (fbErr) {
|
|
3939
|
+
this.manager.logger.log(`[COW] CREATE fallback also failed for ${action.id}: ${fbErr.message}`, 'warn');
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3618
3942
|
this.manager.logger.log(`Failed to prepare update op for ${action.id}: ${err.message}`, 'error');
|
|
3619
3943
|
}
|
|
3620
3944
|
}
|
|
@@ -4246,6 +4570,16 @@ class DEXBot {
|
|
|
4246
4570
|
return;
|
|
4247
4571
|
this._structuralGridResyncRunning = true;
|
|
4248
4572
|
try {
|
|
4573
|
+
// Try the lighter persisted-grid reload before full reset.
|
|
4574
|
+
const persistedResult = await this._recoverFromPersistedGrid();
|
|
4575
|
+
if (persistedResult.success) {
|
|
4576
|
+
if (this.manager?._recoveryState) {
|
|
4577
|
+
this.manager._recoveryState.attemptCount = 0;
|
|
4578
|
+
this.manager._recoveryState.lastAttemptAt = 0;
|
|
4579
|
+
this.manager._recoveryState.lastFailureAt = 0;
|
|
4580
|
+
}
|
|
4581
|
+
return;
|
|
4582
|
+
}
|
|
4249
4583
|
const suffix = unmatchedCount > 0 ? ` (${unmatchedCount} unmatched chain order(s))` : '';
|
|
4250
4584
|
this._warn(`[RECOVERY] Running structural full grid resync for ${reason}${suffix}`);
|
|
4251
4585
|
const resetResult = await this.requestGridReset('rms_structural_grid_resync', {
|