dexbot 1.1.12 → 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/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 +28 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +386 -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 +34 -3
- 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 +20 -4
- 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 +1 -3
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +9 -27
- 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 +29 -4
- 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,6 +132,8 @@ class DEXBot {
|
|
|
132
132
|
_lastGridActivityAt;
|
|
133
133
|
_currentCycleId;
|
|
134
134
|
_autoCancelOrphanCycleMarker;
|
|
135
|
+
_autoCancelOrphanSubCount;
|
|
136
|
+
_orphanFillsCreditedAt;
|
|
135
137
|
_consecutiveConsumeFailures;
|
|
136
138
|
_consumeFailureFirstAt;
|
|
137
139
|
_reconnectUnregister;
|
|
@@ -221,6 +223,8 @@ class DEXBot {
|
|
|
221
223
|
this._lastGridActivityAt = 0;
|
|
222
224
|
this._currentCycleId = 0;
|
|
223
225
|
this._autoCancelOrphanCycleMarker = null;
|
|
226
|
+
this._autoCancelOrphanSubCount = 0;
|
|
227
|
+
this._orphanFillsCreditedAt = null;
|
|
224
228
|
// Per-session guard: ghost order IDs successfully cancelled
|
|
225
229
|
// (avoids spamming the chain with repeated cancel attempts for the same
|
|
226
230
|
// orphan residual on every fill cycle).
|
|
@@ -543,6 +547,57 @@ class DEXBot {
|
|
|
543
547
|
}
|
|
544
548
|
return Array.from(ids);
|
|
545
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
|
+
}
|
|
546
601
|
/**
|
|
547
602
|
* Attempt to repair size-drift for specific order IDs by reading their
|
|
548
603
|
* current on-chain state and correcting the local grid directly.
|
|
@@ -1449,6 +1504,14 @@ class DEXBot {
|
|
|
1449
1504
|
return;
|
|
1450
1505
|
}
|
|
1451
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;
|
|
1452
1515
|
while (this._incomingFillQueue.length > 0) {
|
|
1453
1516
|
const batchStartTime = Date.now();
|
|
1454
1517
|
// Track max queue depth
|
|
@@ -1512,6 +1575,10 @@ class DEXBot {
|
|
|
1512
1575
|
if (accountingResult.status === 'missing_key') {
|
|
1513
1576
|
requiresOpenOrdersSync = true;
|
|
1514
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();
|
|
1515
1582
|
// Don't add to validFills - we can't do rebalancing without a grid slot
|
|
1516
1583
|
// But the funds are now credited, preventing fund invariant violation
|
|
1517
1584
|
continue;
|
|
@@ -1601,7 +1668,49 @@ class DEXBot {
|
|
|
1601
1668
|
};
|
|
1602
1669
|
this.manager.pauseFundRecalc();
|
|
1603
1670
|
try {
|
|
1604
|
-
|
|
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;
|
|
1605
1714
|
// 4. Handle Price Corrections
|
|
1606
1715
|
if (ordersNeedingCorrection.length > 0) {
|
|
1607
1716
|
const correctionResult = await correctAllPriceMismatches(this.manager, this.account, this.privateKey, chainOrders);
|
|
@@ -2092,7 +2201,8 @@ class DEXBot {
|
|
|
2092
2201
|
const openOrders = await chainOrders.readOpenOrders(accountRef);
|
|
2093
2202
|
const recoveryResult = await this.manager.syncFromOpenOrders(openOrders, {
|
|
2094
2203
|
skipAccounting: false,
|
|
2095
|
-
fillLockAlreadyHeld: true
|
|
2204
|
+
fillLockAlreadyHeld: true,
|
|
2205
|
+
protectCommittedOrders: true
|
|
2096
2206
|
});
|
|
2097
2207
|
this._preserveMissingCreateBlockersAfterRecovery(preRecoveryMissingCreateBlockers, recoveryResult);
|
|
2098
2208
|
// Persist any master grid mutations from the recovery sync. The
|
|
@@ -2480,7 +2590,7 @@ class DEXBot {
|
|
|
2480
2590
|
return { executed: false, hadRotation: false, uncertain: true };
|
|
2481
2591
|
}
|
|
2482
2592
|
const adopted = [];
|
|
2483
|
-
|
|
2593
|
+
let discarded = [];
|
|
2484
2594
|
// 2. For each pending broadcast, look for a chain match.
|
|
2485
2595
|
for (const entry of pending) {
|
|
2486
2596
|
const match = this._findChainOrderForSlot(chainSnapshot, entry.slotId, {
|
|
@@ -2494,45 +2604,82 @@ class DEXBot {
|
|
|
2494
2604
|
this.manager._pendingBroadcasts.delete(entry.fingerprint);
|
|
2495
2605
|
}
|
|
2496
2606
|
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) {
|
|
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;
|
|
2517
2625
|
try {
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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);
|
|
2525
2646
|
}
|
|
2526
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
|
+
}
|
|
2527
2673
|
catch (syncErr) {
|
|
2528
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
|
+
}
|
|
2529
2681
|
}
|
|
2530
2682
|
}
|
|
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
2683
|
// 4. Log structured summary.
|
|
2537
2684
|
const elapsedMs = Date.now() - startedAt;
|
|
2538
2685
|
const heartbeatAgeMs = this._lastBroadcastHeartbeatAt
|
|
@@ -2540,14 +2687,14 @@ class DEXBot {
|
|
|
2540
2687
|
: null;
|
|
2541
2688
|
this.manager.logger.log(`[COW][UNCERTAIN] batchId=${err?.batchId || 'n/a'} ops=${opContexts.length} ` +
|
|
2542
2689
|
`staleSinceMs=${err?.timeoutMs || 'n/a'} heartbeatAgeMs=${heartbeatAgeMs ?? 'n/a'} ` +
|
|
2543
|
-
`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');
|
|
2544
2691
|
if (adopted.length > 0) {
|
|
2545
2692
|
this.manager.logger.log(`[COW][UNCERTAIN] Adopted chain orders: ${adopted
|
|
2546
2693
|
.map(a => `${a.slotId}->${a.chainOrderId}`)
|
|
2547
2694
|
.join(', ')}`, 'info');
|
|
2548
2695
|
}
|
|
2549
2696
|
if (discarded.length > 0) {
|
|
2550
|
-
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
|
|
2551
2698
|
.map(d => d.slotId)
|
|
2552
2699
|
.join(', ')}`, 'warn');
|
|
2553
2700
|
}
|
|
@@ -2596,8 +2743,18 @@ class DEXBot {
|
|
|
2596
2743
|
*/
|
|
2597
2744
|
async _autoCancelOneUnmatchedOrphan() {
|
|
2598
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;
|
|
2599
2749
|
if (this._autoCancelOrphanCycleMarker === cycleId) {
|
|
2600
|
-
|
|
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;
|
|
2601
2758
|
}
|
|
2602
2759
|
const pending = (this.manager && this.manager._pendingBroadcasts instanceof Map)
|
|
2603
2760
|
? this.manager._pendingBroadcasts.size
|
|
@@ -2626,13 +2783,15 @@ class DEXBot {
|
|
|
2626
2783
|
return { cancelled: false, reason: 'cancelOrder-unavailable' };
|
|
2627
2784
|
}
|
|
2628
2785
|
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
2786
|
await chainOrders.cancelOrder(this.account, this.privateKey, orderId);
|
|
2633
2787
|
if (typeof chainOrders.recordOwnCancel === 'function') {
|
|
2634
2788
|
chainOrders.recordOwnCancel(orderId);
|
|
2635
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');
|
|
2636
2795
|
return { cancelled: true, orderId };
|
|
2637
2796
|
}
|
|
2638
2797
|
catch (err) {
|
|
@@ -2686,6 +2845,21 @@ class DEXBot {
|
|
|
2686
2845
|
if (isRetriable) {
|
|
2687
2846
|
this.manager.logger.log(`[COW] Broadcast uncertain (attempt ${attempt}/${MAX_RETRIES + 1}), retrying...`, 'warn');
|
|
2688
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
|
+
}
|
|
2689
2863
|
continue;
|
|
2690
2864
|
}
|
|
2691
2865
|
throw err;
|
|
@@ -3448,58 +3622,111 @@ class DEXBot {
|
|
|
3448
3622
|
? Array.from(this.manager._pendingBroadcasts.values())
|
|
3449
3623
|
: [];
|
|
3450
3624
|
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
|
-
|
|
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
|
+
}
|
|
3469
3670
|
}
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
operations: pendingBroadcasts.map(p => p.order),
|
|
3485
|
-
accountName: this.account,
|
|
3486
|
-
batchId: this._currentBatchId || null,
|
|
3487
|
-
payload: null,
|
|
3488
|
-
timeoutMs: null
|
|
3489
|
-
}), [], { fillLockAlreadyHeld: true });
|
|
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');
|
|
3490
3685
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
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');
|
|
3694
|
+
}
|
|
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 });
|
|
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
|
+
}
|
|
3493
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
|
+
};
|
|
3494
3728
|
}
|
|
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
|
-
};
|
|
3729
|
+
// All unmatched were cancelled — fall through to continue batch
|
|
3503
3730
|
}
|
|
3504
3731
|
const { assetA, assetB } = this.manager.assets;
|
|
3505
3732
|
const operations = [];
|
|
@@ -3540,7 +3767,13 @@ class DEXBot {
|
|
|
3540
3767
|
opContexts.push({ kind: 'cancel', order });
|
|
3541
3768
|
}
|
|
3542
3769
|
catch (err) {
|
|
3543
|
-
|
|
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
|
+
}
|
|
3544
3777
|
}
|
|
3545
3778
|
}
|
|
3546
3779
|
else if (action.type === COW_ACTIONS.CREATE) {
|
|
@@ -3652,8 +3885,60 @@ class DEXBot {
|
|
|
3652
3885
|
type: orderType
|
|
3653
3886
|
};
|
|
3654
3887
|
opContexts.push({ kind: 'size-update', updateInfo: { partialOrder, newSize }, finalInts: op.finalInts });
|
|
3888
|
+
// Catch for both rotation-update and size-update branches.
|
|
3655
3889
|
}
|
|
3656
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
|
+
}
|
|
3657
3942
|
this.manager.logger.log(`Failed to prepare update op for ${action.id}: ${err.message}`, 'error');
|
|
3658
3943
|
}
|
|
3659
3944
|
}
|
|
@@ -4285,6 +4570,16 @@ class DEXBot {
|
|
|
4285
4570
|
return;
|
|
4286
4571
|
this._structuralGridResyncRunning = true;
|
|
4287
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
|
+
}
|
|
4288
4583
|
const suffix = unmatchedCount > 0 ? ` (${unmatchedCount} unmatched chain order(s))` : '';
|
|
4289
4584
|
this._warn(`[RECOVERY] Running structural full grid resync for ${reason}${suffix}`);
|
|
4290
4585
|
const resetResult = await this.requestGridReset('rms_structural_grid_resync', {
|