dexbot 1.2.1 → 1.2.3
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/modules/bitshares-native/lru_cache.d.ts +9 -0
- package/dist/modules/bitshares-native/lru_cache.d.ts.map +1 -1
- package/dist/modules/bitshares-native/lru_cache.js +12 -0
- package/dist/modules/bitshares-native/lru_cache.js.map +1 -1
- package/dist/modules/bitshares-native/tx/builder.d.ts +4 -0
- package/dist/modules/bitshares-native/tx/builder.d.ts.map +1 -1
- package/dist/modules/bitshares-native/tx/builder.js +19 -0
- package/dist/modules/bitshares-native/tx/builder.js.map +1 -1
- package/dist/modules/bitshares-native/tx/tx_cache.d.ts +7 -0
- package/dist/modules/bitshares-native/tx/tx_cache.d.ts.map +1 -1
- package/dist/modules/bitshares-native/tx/tx_cache.js +11 -0
- package/dist/modules/bitshares-native/tx/tx_cache.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +5 -0
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/constants.d.ts +1 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +1 -0
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +22 -8
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +266 -146
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +3 -0
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +9 -0
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/node_manager.d.ts +1 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +11 -1
- 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 +4 -1
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/grid.d.ts +1 -0
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +124 -5
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/index.d.ts +3 -8
- package/dist/modules/order/index.d.ts.map +1 -1
- package/dist/modules/order/index.js +3 -10
- package/dist/modules/order/index.js.map +1 -1
- package/dist/modules/order/manager.d.ts +2 -0
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +5 -0
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.js +1 -1
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +40 -17
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +28 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +40 -2
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +6 -3
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/utils/math_utils.d.ts +3 -6
- package/dist/modules/utils/math_utils.d.ts.map +1 -1
- package/dist/modules/utils/math_utils.js +1 -14
- package/dist/modules/utils/math_utils.js.map +1 -1
- package/dist/{modules/order → scripts}/runner.d.ts +15 -9
- package/dist/scripts/runner.d.ts.map +1 -0
- package/dist/{modules/order → scripts}/runner.js +24 -45
- package/dist/scripts/runner.js.map +1 -0
- package/package.json +1 -2
- package/scripts/README.md +13 -0
- package/dist/modules/cli_whitelist_args.d.ts +0 -6
- package/dist/modules/cli_whitelist_args.d.ts.map +0 -1
- package/dist/modules/cli_whitelist_args.js +0 -13
- package/dist/modules/cli_whitelist_args.js.map +0 -1
- package/dist/modules/order/runner.d.ts.map +0 -1
- package/dist/modules/order/runner.js.map +0 -1
|
@@ -134,7 +134,6 @@ class DEXBot {
|
|
|
134
134
|
_currentCycleId;
|
|
135
135
|
_autoCancelOrphanCycleMarker;
|
|
136
136
|
_autoCancelOrphanSubCount;
|
|
137
|
-
_orphanFillsCreditedAt;
|
|
138
137
|
_consecutiveConsumeFailures;
|
|
139
138
|
_consumeFailureFirstAt;
|
|
140
139
|
_reconnectUnregister;
|
|
@@ -226,7 +225,6 @@ class DEXBot {
|
|
|
226
225
|
this._currentCycleId = 0;
|
|
227
226
|
this._autoCancelOrphanCycleMarker = null;
|
|
228
227
|
this._autoCancelOrphanSubCount = 0;
|
|
229
|
-
this._orphanFillsCreditedAt = null;
|
|
230
228
|
// Per-session guard: ghost order IDs successfully cancelled
|
|
231
229
|
// (avoids spamming the chain with repeated cancel attempts for the same
|
|
232
230
|
// orphan residual on every fill cycle).
|
|
@@ -598,6 +596,42 @@ class DEXBot {
|
|
|
598
596
|
}
|
|
599
597
|
this.manager.logger.log(`[RECOVERY] Grid reloaded from persisted snapshot: ${this.manager.orders.size} orders, ` +
|
|
600
598
|
`${chainOpenOrders.length} on-chain orders synced`, 'info');
|
|
599
|
+
// Gap 2: Check for unmatched chain orders after reload + sync.
|
|
600
|
+
// If the sync resolved all unmatched entries, _lastUnmatchedChainOrders
|
|
601
|
+
// was cleared by the sync engine. If any remain, the persisted snapshot
|
|
602
|
+
// produced an inconsistent grid — reject so the structural resync
|
|
603
|
+
// falls through to requestGridReset (full rebuild from chain).
|
|
604
|
+
const remainingUnmatched = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
605
|
+
? this.manager._lastUnmatchedChainOrders
|
|
606
|
+
: [];
|
|
607
|
+
if (remainingUnmatched.length > 0) {
|
|
608
|
+
const sample = remainingUnmatched.slice(0, 3)
|
|
609
|
+
.map(o => this._formatUnmatchedChainOrderForLog(o))
|
|
610
|
+
.join(' | ');
|
|
611
|
+
this.manager.logger.log(`[RECOVERY] Persisted grid reloaded but ${remainingUnmatched.length} unmatched chain order(s) ` +
|
|
612
|
+
`remain${sample ? ` (${sample})` : ''}. Rejecting — full grid reset required.`, 'warn');
|
|
613
|
+
return { success: false, reason: `grid inconsistent after reload: ${remainingUnmatched.length} unmatched remain` };
|
|
614
|
+
}
|
|
615
|
+
// If the reloaded grid is still bloated, reject recovery so the
|
|
616
|
+
// caller falls through to a full grid reset (requestGridReset).
|
|
617
|
+
// Without this check, a bloated snapshot gets accepted as "success"
|
|
618
|
+
// and the structural-resync loop loads the same broken state forever.
|
|
619
|
+
//
|
|
620
|
+
// NOTE: loadGrid() already fires requestStructuralGridResync when it
|
|
621
|
+
// detects bloat internally, so the inner async resync may be in flight
|
|
622
|
+
// by the time this outer check runs. That's fine — the structural-resync
|
|
623
|
+
// gate (_structuralGridResyncRunning / _structuralGridResyncTimer) dedup's
|
|
624
|
+
// concurrent requests. This outer check exists so the synchronous return
|
|
625
|
+
// value is honest about the state; the inner resync is a safety net.
|
|
626
|
+
const { isGridBloated } = require('./order/grid');
|
|
627
|
+
const ordersArr = Array.from(this.manager.orders.values());
|
|
628
|
+
const bloatPostRecovery = isGridBloated(this.manager, ordersArr);
|
|
629
|
+
if (bloatPostRecovery.bloated) {
|
|
630
|
+
const d = bloatPostRecovery.details;
|
|
631
|
+
this.manager.logger.log(`[RECOVERY] Persisted grid reloaded but still bloated ` +
|
|
632
|
+
`(${d.gridSize} slots, max ${d.maxAllowed}). Rejecting — full grid reset required.`, 'warn');
|
|
633
|
+
return { success: false, reason: 'grid still bloated after reload' };
|
|
634
|
+
}
|
|
601
635
|
return { success: true };
|
|
602
636
|
}
|
|
603
637
|
catch (err) {
|
|
@@ -1070,24 +1104,30 @@ class DEXBot {
|
|
|
1070
1104
|
if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[POST-RESET]', fillOp.order_id)) {
|
|
1071
1105
|
continue;
|
|
1072
1106
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1107
|
+
this.manager.lockOrders([gridOrder.id]);
|
|
1108
|
+
try {
|
|
1109
|
+
const accountingResult = await this._applyReplaySafeTrackedFillAccounting(fill, fillOp, {
|
|
1110
|
+
context: 'POST-RESET',
|
|
1111
|
+
logger: { log: this._log.bind(this) },
|
|
1112
|
+
replayMessage: (op) => `[POST-RESET] Replay detected for ${op.order_id}; skipping duplicate rebalance`
|
|
1113
|
+
});
|
|
1114
|
+
if (accountingResult.status === 'missing_key') {
|
|
1115
|
+
requiresOpenOrdersSync = true;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (accountingResult.status !== 'applied') {
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
// Process this fill through the full rebalance pipeline
|
|
1122
|
+
// This will shift the boundary and place a new order on the filled slot
|
|
1123
|
+
const result = await this._processFillsWithBatching([gridOrder], new Set(), `[POST-RESET] fill ${gridOrder.id}`);
|
|
1124
|
+
if (result.aborted) {
|
|
1125
|
+
this._warn('[POST-RESET] Aborted batch due to illegal state; skipping grid persistence this cycle');
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1084
1128
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const result = await this._processFillsWithBatching([gridOrder], new Set(), `[POST-RESET] fill ${gridOrder.id}`);
|
|
1088
|
-
if (result.aborted) {
|
|
1089
|
-
this._warn('[POST-RESET] Aborted batch due to illegal state; skipping grid persistence this cycle');
|
|
1090
|
-
continue;
|
|
1129
|
+
finally {
|
|
1130
|
+
this.manager.unlockOrders([gridOrder.id]);
|
|
1091
1131
|
}
|
|
1092
1132
|
}
|
|
1093
1133
|
if (requiresOpenOrdersSync) {
|
|
@@ -1566,10 +1606,12 @@ class DEXBot {
|
|
|
1566
1606
|
// fill cycle. It is re-set when orphan fills are credited,
|
|
1567
1607
|
// and consumed (set to null) on the next fund-invariant check
|
|
1568
1608
|
// in accounting.ts, which widens tolerance by 5x while set.
|
|
1609
|
+
// Written to this.manager (not this) because accounting.ts
|
|
1610
|
+
// reads from the OrderManager reference (mgr).
|
|
1569
1611
|
// Also cleared by _performStateRecovery after a fresh chain
|
|
1570
1612
|
// fetch. The timestamp value itself is not compared against a
|
|
1571
1613
|
// window — it acts as a consume-on-read boolean.
|
|
1572
|
-
this._orphanFillsCreditedAt = null;
|
|
1614
|
+
this.manager._orphanFillsCreditedAt = null;
|
|
1573
1615
|
while (this._incomingFillQueue.length > 0) {
|
|
1574
1616
|
const batchStartTime = Date.now();
|
|
1575
1617
|
// Track max queue depth
|
|
@@ -1634,9 +1676,9 @@ class DEXBot {
|
|
|
1634
1676
|
requiresOpenOrdersSync = true;
|
|
1635
1677
|
}
|
|
1636
1678
|
// Record orphan fill credit timestamp for fund invariant
|
|
1637
|
-
// tolerance widening.
|
|
1638
|
-
//
|
|
1639
|
-
this._orphanFillsCreditedAt = Date.now();
|
|
1679
|
+
// tolerance widening. Written to this.manager because
|
|
1680
|
+
// accounting.ts reads from the OrderManager (mgr).
|
|
1681
|
+
this.manager._orphanFillsCreditedAt = Date.now();
|
|
1640
1682
|
// Don't add to validFills - we can't do rebalancing without a grid slot
|
|
1641
1683
|
// But the funds are now credited, preventing fund invariant violation
|
|
1642
1684
|
continue;
|
|
@@ -1644,7 +1686,7 @@ class DEXBot {
|
|
|
1644
1686
|
// Process both maker and taker fills for our grid orders
|
|
1645
1687
|
// Grid validation ensures we only process fills belonging to our account
|
|
1646
1688
|
// Taker fills are included because the bot may execute market orders or act as taker
|
|
1647
|
-
const roleStr = fillOp.is_maker ? 'maker' : 'taker';
|
|
1689
|
+
const roleStr = fillOp.is_maker !== false ? 'maker' : 'taker';
|
|
1648
1690
|
this.manager.logger.log(`Processing ${roleStr} fill for order ${fillOp.order_id}`, 'debug');
|
|
1649
1691
|
const fillKey = buildFillKey(fill);
|
|
1650
1692
|
if (!fillKey) {
|
|
@@ -1751,6 +1793,12 @@ class DEXBot {
|
|
|
1751
1793
|
const sortedBlocks = [...fillsByBlock.keys()].sort((a, b) => a - b);
|
|
1752
1794
|
const accumulatedOrders = [];
|
|
1753
1795
|
let anyRequiresSync = false;
|
|
1796
|
+
// Capture whether any fill was skipped during validation
|
|
1797
|
+
// (e.g. missing history ID) and set the sync flag. The
|
|
1798
|
+
// per-block reset below would lose this state since those
|
|
1799
|
+
// fills were filtered out of validFills and never enter
|
|
1800
|
+
// any block group.
|
|
1801
|
+
const initialRequiresSync = requiresOpenOrdersSync;
|
|
1754
1802
|
for (const blockNum of sortedBlocks) {
|
|
1755
1803
|
// Reset requiresOpenOrdersSync per block group so one
|
|
1756
1804
|
// block's history-id gap doesn't force the next block
|
|
@@ -1762,12 +1810,24 @@ class DEXBot {
|
|
|
1762
1810
|
if (requiresOpenOrdersSync)
|
|
1763
1811
|
anyRequiresSync = true;
|
|
1764
1812
|
}
|
|
1765
|
-
|
|
1813
|
+
// Preserve both per-block sync flags and the initial flag
|
|
1814
|
+
// from filtered-out fills (missing history ID).
|
|
1815
|
+
requiresOpenOrdersSync = anyRequiresSync || initialRequiresSync;
|
|
1766
1816
|
if (fillsWithoutBlock.length > 0) {
|
|
1767
1817
|
this.manager.logger.log(`[FILL-BLOCK] Processing ${fillsWithoutBlock.length} fill(s) without block info`, 'debug');
|
|
1768
1818
|
const noBlockResult = await processValidFills(fillsWithoutBlock);
|
|
1769
1819
|
accumulatedOrders.push(...noBlockResult);
|
|
1770
1820
|
}
|
|
1821
|
+
// If a fill was filtered out during validation (e.g. missing
|
|
1822
|
+
// history ID) and no block group triggered the open-orders
|
|
1823
|
+
// sync, run it now so the grid state is reconciled.
|
|
1824
|
+
if (requiresOpenOrdersSync && !anyRequiresSync) {
|
|
1825
|
+
this.manager.logger.log('[FILL-BLOCK] Running open-orders sync for fills with missing history identifiers', 'warn');
|
|
1826
|
+
const fallbackOrders = await processValidFills([]);
|
|
1827
|
+
accumulatedOrders.push(...fallbackOrders);
|
|
1828
|
+
// Flag consumed — variable goes out of scope on
|
|
1829
|
+
// next while iteration.
|
|
1830
|
+
}
|
|
1771
1831
|
allFilledOrders = accumulatedOrders;
|
|
1772
1832
|
// 4. Handle Price Corrections
|
|
1773
1833
|
if (ordersNeedingCorrection.length > 0) {
|
|
@@ -2002,20 +2062,26 @@ class DEXBot {
|
|
|
2002
2062
|
if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[BOOTSTRAP]', fillOp.order_id)) {
|
|
2003
2063
|
continue;
|
|
2004
2064
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2065
|
+
this.manager.lockOrders([gridOrder.id]);
|
|
2066
|
+
try {
|
|
2067
|
+
const accountingResult = await this._applyReplaySafeTrackedFillAccounting(fill, fillOp, {
|
|
2068
|
+
context: 'BOOTSTRAP',
|
|
2069
|
+
replayMessage: (op) => `[BOOTSTRAP] Replay detected for ${op.order_id}; skipping duplicate bootstrap rebalance`
|
|
2070
|
+
});
|
|
2071
|
+
if (accountingResult.status === 'missing_key') {
|
|
2072
|
+
requiresOpenOrdersSync = true;
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
if (accountingResult.status !== 'applied') {
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
2078
|
+
validFills.push({ ...fill, gridOrder });
|
|
2079
|
+
const fillType = gridOrder.type === ORDER_TYPES.BUY ? 'BUY' : 'SELL';
|
|
2080
|
+
this._log(`[BOOTSTRAP] Fill detected: ${fillType} order (${fillOp.is_maker !== false ? 'maker' : 'taker'})`);
|
|
2012
2081
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2082
|
+
finally {
|
|
2083
|
+
this.manager.unlockOrders([gridOrder.id]);
|
|
2015
2084
|
}
|
|
2016
|
-
validFills.push({ ...fill, gridOrder });
|
|
2017
|
-
const fillType = gridOrder.type === ORDER_TYPES.BUY ? 'BUY' : 'SELL';
|
|
2018
|
-
this._log(`[BOOTSTRAP] Fill detected: ${fillType} order (${fillOp.is_maker ? 'maker' : 'taker'})`);
|
|
2019
2085
|
}
|
|
2020
2086
|
if (requiresOpenOrdersSync) {
|
|
2021
2087
|
this._log('[BOOTSTRAP] Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
|
|
@@ -2764,6 +2830,45 @@ class DEXBot {
|
|
|
2764
2830
|
this.manager.logger.log(`[COW][UNCERTAIN] Discarded planned CREATEs (no chain match after ${recheckRound} recheck(s)): ${discarded
|
|
2765
2831
|
.map(d => d.slotId)
|
|
2766
2832
|
.join(', ')}`, 'warn');
|
|
2833
|
+
// Restore target grid sizes for discarded CREATEs so the slots are
|
|
2834
|
+
// not left as virtual/0 after recovery. Without this, the slot stays
|
|
2835
|
+
// at size 0 (set by the fill handler) and is never reactivated until
|
|
2836
|
+
// a fresh fill cycle triggers another rebalance that happens to succeed.
|
|
2837
|
+
// entry.order is the pending-broadcast target order (captured at broadcast
|
|
2838
|
+
// time); its .id matches entry.slotId — the lookup below keys on slotId.
|
|
2839
|
+
for (const entry of discarded) {
|
|
2840
|
+
if (entry.order && entry.slotId) {
|
|
2841
|
+
const current = this.manager.orders.get(entry.slotId);
|
|
2842
|
+
if (current && current.state === ORDER_STATES.VIRTUAL && !current.orderId) {
|
|
2843
|
+
try {
|
|
2844
|
+
await this.manager._applyOrderUpdate({ ...entry.order, state: ORDER_STATES.VIRTUAL, orderId: null }, 'uncertain-recovery-restore-size', { skipAccounting: true, fee: 0 });
|
|
2845
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Restored target size for discarded CREATE slot ${entry.slotId} (size: ${entry.order.size})`, 'info');
|
|
2846
|
+
}
|
|
2847
|
+
catch (restoreErr) {
|
|
2848
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Failed to restore size for slot ${entry.slotId}: ${restoreErr?.message || restoreErr}`, 'warn');
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
// ---- Structural resync safeguard after skipAccounting restore ----
|
|
2855
|
+
// The discarded CREATE restore above used skipAccounting: true to avoid
|
|
2856
|
+
// double-counting when the structural resync recalculates accounting
|
|
2857
|
+
// from scratch. Ensure one is scheduled — if already in-flight (timer
|
|
2858
|
+
// or running), the existing resync will handle the accounting fix.
|
|
2859
|
+
if (discarded.length > 0 && typeof this.manager?.requestStructuralGridResync === 'function') {
|
|
2860
|
+
const alreadyScheduled = this._structuralGridResyncRunning || this._structuralGridResyncTimer;
|
|
2861
|
+
if (!alreadyScheduled) {
|
|
2862
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Scheduling structural resync after discarded CREATE restore ` +
|
|
2863
|
+
`(skipAccounting used — resync needed for fund recalculation)`, 'info');
|
|
2864
|
+
this.manager.requestStructuralGridResync('cow-uncertain-accounting-repair', { discardedCount: discarded.length }).catch((err) => {
|
|
2865
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Failed to schedule accounting repair resync: ${err.message}`, 'error');
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
else {
|
|
2869
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Structural resync already ${this._structuralGridResyncRunning ? 'running' : 'scheduled'}; ` +
|
|
2870
|
+
`it will repair accounting after discarded CREATE restore`, 'debug');
|
|
2871
|
+
}
|
|
2767
2872
|
}
|
|
2768
2873
|
this._clearPendingBroadcasts();
|
|
2769
2874
|
// Post-recovery safety net: if there are still unmatched chain
|
|
@@ -2831,18 +2936,22 @@ class DEXBot {
|
|
|
2831
2936
|
return { executed: false, hadRotation, uncertain: true, adopted, discarded };
|
|
2832
2937
|
}
|
|
2833
2938
|
/**
|
|
2834
|
-
* Auto-cancel a
|
|
2939
|
+
* Auto-cancel a price-drift orphan from the unmatched-order snapshot.
|
|
2940
|
+
*
|
|
2941
|
+
* Only cancels entries with reason === 'price-drift-orphan' — these are
|
|
2942
|
+
* surplus orders that drifted away from their slot price and have no
|
|
2943
|
+
* adoptable grid slot. All other unmatched orders (duplicate-price-level,
|
|
2944
|
+
* already-matched-slot, etc.) are adoptable positions that the structural
|
|
2945
|
+
* resync will integrate into the grid; cancelling them destroys capital.
|
|
2835
2946
|
*
|
|
2836
2947
|
* This is the post-recovery safety net: if, after
|
|
2837
|
-
* _reconcileAfterUncertainBroadcast runs, there are still
|
|
2838
|
-
*
|
|
2839
|
-
*
|
|
2840
|
-
* them per call. Per-cycle cap = 1 — the next cycle will pick up the
|
|
2841
|
-
* next unmatched order if more remain.
|
|
2948
|
+
* _reconcileAfterUncertainBroadcast runs, there are still price-drift
|
|
2949
|
+
* orphans, cancel ONE per cycle. Per-cycle cap = 1 (or 5 in recovery mode)
|
|
2950
|
+
* — the next cycle will pick up the next orphan if more remain.
|
|
2842
2951
|
*
|
|
2843
2952
|
* Safety conditions (ALL must hold):
|
|
2844
2953
|
* 1. _pendingBroadcasts is empty (no in-flight recovery)
|
|
2845
|
-
* 2. _lastUnmatchedChainOrders
|
|
2954
|
+
* 2. _lastUnmatchedChainOrders contains at least one price-drift-orphan
|
|
2846
2955
|
* 3. The current cycle has not already auto-cancelled an orphan
|
|
2847
2956
|
* (tracked via this._autoCancelOrphanCycleMarker)
|
|
2848
2957
|
*
|
|
@@ -2878,17 +2987,27 @@ class DEXBot {
|
|
|
2878
2987
|
if (unmatched.length === 0) {
|
|
2879
2988
|
return { cancelled: false, reason: 'no-unmatched' };
|
|
2880
2989
|
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2990
|
+
// Check for fingerprinted entries first — these came from a pending
|
|
2991
|
+
// broadcast (missing-create-result) and must be handled by the recovery
|
|
2992
|
+
// path, not by auto-cancel. The first fingerprinted entry is checked
|
|
2993
|
+
// regardless of its reason field.
|
|
2994
|
+
const fingerprinted = unmatched.find(u => u && u.fingerprint);
|
|
2995
|
+
if (fingerprinted) {
|
|
2996
|
+
return { cancelled: false, reason: 'fingerprinted-handle-via-recovery' };
|
|
2997
|
+
}
|
|
2998
|
+
// Only cancel price-drift orphans — these are surplus orders that
|
|
2999
|
+
// drifted away from their assigned slot price and have no adoptable
|
|
3000
|
+
// grid slot. All other unmatched orders (duplicate-price-level,
|
|
3001
|
+
// already-matched-slot, etc.) are adoptable positions that the structural
|
|
3002
|
+
// resync will integrate into the grid — cancelling them destroys capital.
|
|
3003
|
+
const target = unmatched.find(u => u && u.reason === 'price-drift-orphan');
|
|
3004
|
+
if (!target) {
|
|
3005
|
+
return { cancelled: false, reason: 'no-price-drift-orphan', message: 'no price-drift orphan to cancel; other unmatched orders are adoptable' };
|
|
3006
|
+
}
|
|
3007
|
+
const orderId = target.id || target.orderId || target.chainOrderId;
|
|
2884
3008
|
if (!orderId) {
|
|
2885
3009
|
return { cancelled: false, reason: 'no-orderId' };
|
|
2886
3010
|
}
|
|
2887
|
-
if (target?.fingerprint) {
|
|
2888
|
-
// Fingerprinted unmatched orders came from a pending broadcast.
|
|
2889
|
-
// The recovery path is the right place to handle them, not here.
|
|
2890
|
-
return { cancelled: false, reason: 'fingerprinted-handle-via-recovery' };
|
|
2891
|
-
}
|
|
2892
3011
|
if (!chainOrders?.cancelOrder) {
|
|
2893
3012
|
return { cancelled: false, reason: 'cancelOrder-unavailable' };
|
|
2894
3013
|
}
|
|
@@ -3746,111 +3865,112 @@ class DEXBot {
|
|
|
3746
3865
|
? Array.from(this.manager._pendingBroadcasts.values())
|
|
3747
3866
|
: [];
|
|
3748
3867
|
if (hasCreateActions && (unmatchedChainOrders.length > 0 || pendingBroadcasts.length > 0)) {
|
|
3749
|
-
// ----
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
let cancelledUnmatched = 0;
|
|
3754
|
-
if (unmatchedChainOrders.length > 0 && pendingBroadcasts.length === 0) {
|
|
3755
|
-
// Bounded-parallel cancellation: process unmatched orders in
|
|
3756
|
-
// concurrent batches (3 at a time) to avoid serialising 19+
|
|
3757
|
-
// individual cancel txs (~0.5s each = 10s batch hold).
|
|
3758
|
-
const CANCEL_CONCURRENCY = 3;
|
|
3759
|
-
const cancelable = unmatchedChainOrders.filter(u => {
|
|
3760
|
-
const oid = u?.id || u?.orderId || u?.chainOrderId;
|
|
3761
|
-
return Boolean(oid) && !u?.fingerprint;
|
|
3762
|
-
});
|
|
3763
|
-
for (let i = 0; i < cancelable.length; i += CANCEL_CONCURRENCY) {
|
|
3764
|
-
const batch = cancelable.slice(i, i + CANCEL_CONCURRENCY);
|
|
3765
|
-
const results = await Promise.allSettled(batch.map(async (unmatched) => {
|
|
3766
|
-
const orderId = unmatched.id || unmatched.orderId || unmatched.chainOrderId;
|
|
3767
|
-
this.manager.logger.log(`[COW] Auto-cancelling unmatched chain order ${orderId} ` +
|
|
3768
|
-
`(${this._formatUnmatchedChainOrderForLog(unmatched)}) to unblock CREATE batch.`, 'warn');
|
|
3769
|
-
await chainOrders.cancelOrder(this.account, this.privateKey, orderId);
|
|
3770
|
-
if (typeof chainOrders.recordOwnCancel === 'function') {
|
|
3771
|
-
chainOrders.recordOwnCancel(orderId);
|
|
3772
|
-
}
|
|
3773
|
-
}));
|
|
3774
|
-
for (const r of results) {
|
|
3775
|
-
if (r.status === 'fulfilled')
|
|
3776
|
-
cancelledUnmatched++;
|
|
3777
|
-
else
|
|
3778
|
-
this.manager.logger.log(`[COW] Failed to cancel unmatched chain order: ${r.reason?.message || r.reason}`, 'warn');
|
|
3779
|
-
}
|
|
3780
|
-
}
|
|
3781
|
-
if (cancelable.length > 0 && cancelledUnmatched >= cancelable.length) {
|
|
3782
|
-
this.manager.logger.log(`[COW] Cancelled all ${cancelledUnmatched} unmatched chain order(s); proceeding with CREATE batch.`, 'warn');
|
|
3783
|
-
// Optimistic clear: the cancel txs are sent but not yet
|
|
3784
|
-
// confirmed. If a cancel silently fails on chain, the
|
|
3785
|
-
// next sync cycle will re-detect the unmatched order and
|
|
3786
|
-
// re-enter this guard — that's safe because the cancel
|
|
3787
|
-
// is idempotent and the re-detection is non-blocking.
|
|
3788
|
-
this.manager._lastUnmatchedChainOrders = [];
|
|
3789
|
-
}
|
|
3790
|
-
else if (cancelable.length > 0) {
|
|
3791
|
-
this.manager.logger.log(`[COW] Cancelled ${cancelledUnmatched}/${cancelable.length} unmatched chain order(s); ` +
|
|
3792
|
-
`remaining must be handled by structural reconciliation.`, 'warn');
|
|
3793
|
-
}
|
|
3794
|
-
}
|
|
3795
|
-
// Re-check after auto-cancel attempt
|
|
3796
|
-
const remainingUnmatched = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
3797
|
-
? this.manager._lastUnmatchedChainOrders
|
|
3798
|
-
: [];
|
|
3799
|
-
if (remainingUnmatched.length > 0 || pendingBroadcasts.length > 0) {
|
|
3800
|
-
const blockers = [];
|
|
3801
|
-
if (remainingUnmatched.length > 0)
|
|
3802
|
-
blockers.push(`${remainingUnmatched.length} unmatched chain order(s)`);
|
|
3803
|
-
if (pendingBroadcasts.length > 0)
|
|
3804
|
-
blockers.push(`${pendingBroadcasts.length} pending broadcast(s)`);
|
|
3805
|
-
const reasonText = blockers.join(' and ');
|
|
3806
|
-
if (pendingBroadcasts.length > 0) {
|
|
3807
|
-
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${reasonText} from a prior uncertain ` +
|
|
3808
|
-
`broadcast. Running recovery before placing replacement orders.`, 'error');
|
|
3809
|
-
}
|
|
3810
|
-
else {
|
|
3811
|
-
const sample = remainingUnmatched
|
|
3812
|
-
.slice(0, 3)
|
|
3813
|
-
.map(order => this._formatUnmatchedChainOrderForLog(order))
|
|
3814
|
-
.join(' | ');
|
|
3815
|
-
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${reasonText} ` +
|
|
3816
|
-
`are not represented in the grid${sample ? ` (${sample})` : ''}. ` +
|
|
3817
|
-
`Run structural reconciliation before placing replacement orders.`, 'error');
|
|
3818
|
-
}
|
|
3868
|
+
// ---- Handle pending broadcasts first ----
|
|
3869
|
+
if (pendingBroadcasts.length > 0) {
|
|
3870
|
+
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${pendingBroadcasts.length} pending broadcast(s) from a prior uncertain ` +
|
|
3871
|
+
`broadcast. Running recovery before placing replacement orders.`, 'error');
|
|
3819
3872
|
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
3820
3873
|
if (this.manager._recoveryState)
|
|
3821
3874
|
this.manager._recoveryState.structuralResyncRequested = true;
|
|
3822
|
-
await this.manager.requestStructuralGridResync(pendingBroadcasts.
|
|
3823
|
-
? 'pending broadcasts before COW create'
|
|
3824
|
-
: 'unmatched chain orders before COW create', pendingBroadcasts.length > 0
|
|
3825
|
-
? { pendingBroadcasts: pendingBroadcasts.map(p => p.slotId) }
|
|
3826
|
-
: { unmatchedChainOrders: remainingUnmatched });
|
|
3875
|
+
await this.manager.requestStructuralGridResync('pending broadcasts before COW create', { pendingBroadcasts: pendingBroadcasts.map(p => p.slotId) });
|
|
3827
3876
|
}
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
}
|
|
3840
|
-
catch (recoverErr) {
|
|
3841
|
-
this.manager.logger.log(`[COW] Recovery from pending broadcasts failed: ${recoverErr?.message || recoverErr}`, 'error');
|
|
3842
|
-
}
|
|
3877
|
+
try {
|
|
3878
|
+
await this._reconcileAfterUncertainBroadcast(new BroadcastUncertainError('rejected CREATE batch had pending broadcasts', {
|
|
3879
|
+
operations: pendingBroadcasts.map(p => p.order),
|
|
3880
|
+
accountName: this.account,
|
|
3881
|
+
batchId: this._currentBatchId || null,
|
|
3882
|
+
payload: null,
|
|
3883
|
+
timeoutMs: null
|
|
3884
|
+
}), []);
|
|
3885
|
+
}
|
|
3886
|
+
catch (recoverErr) {
|
|
3887
|
+
this.manager.logger.log(`[COW] Recovery from pending broadcasts failed: ${recoverErr?.message || recoverErr}`, 'error');
|
|
3843
3888
|
}
|
|
3844
3889
|
return {
|
|
3845
3890
|
executed: false,
|
|
3846
3891
|
aborted: true,
|
|
3847
|
-
reason:
|
|
3848
|
-
unmatchedChainOrders: pendingBroadcasts.length > 0 ? [] : remainingUnmatched,
|
|
3849
|
-
pendingBroadcasts: pendingBroadcasts.map(p => p.slotId),
|
|
3892
|
+
reason: 'PENDING_BROADCASTS',
|
|
3850
3893
|
hadRotation: false
|
|
3851
3894
|
};
|
|
3852
3895
|
}
|
|
3853
|
-
//
|
|
3896
|
+
// ---- Unmatched orders: adopt instead of cancel ----
|
|
3897
|
+
// Unmatched orders are legitimate on-chain positions the grid
|
|
3898
|
+
// hasn't adopted yet. Cancelling them destroys capital and
|
|
3899
|
+
// creates gaps in the order book. Instead, re-sync to adopt
|
|
3900
|
+
// them into the grid, then reject (the sync invalidated the
|
|
3901
|
+
// working grid, so the existing COW plan is stale).
|
|
3902
|
+
// NOTE: syncFromOpenOrders may be a partial no-op under certain
|
|
3903
|
+
// lock conditions (e.g. _fillProcessingLock + !isReentrant).
|
|
3904
|
+
// Structural resync scheduled below handles adoption regardless.
|
|
3905
|
+
const unmatchedSample = unmatchedChainOrders
|
|
3906
|
+
.slice(0, 3)
|
|
3907
|
+
.map(o => this._formatUnmatchedChainOrderForLog(o))
|
|
3908
|
+
.join(' | ');
|
|
3909
|
+
this.manager.logger.log(`[COW] ${unmatchedChainOrders.length} unmatched chain order(s) blocking CREATES ` +
|
|
3910
|
+
(unmatchedSample ? `(${unmatchedSample})` : '') +
|
|
3911
|
+
` — adopting via sync instead of cancelling`, 'info');
|
|
3912
|
+
try {
|
|
3913
|
+
const accountRef = this.account;
|
|
3914
|
+
const freshSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
3915
|
+
if (freshSnapshot && freshSnapshot.length > 0) {
|
|
3916
|
+
const syncResult = await this.manager.syncFromOpenOrders(freshSnapshot, {
|
|
3917
|
+
skipAccounting: true,
|
|
3918
|
+
});
|
|
3919
|
+
// Only overwrite _lastUnmatchedChainOrders when sync actually
|
|
3920
|
+
// processed orders (filled + updated + corrected > 0).
|
|
3921
|
+
// Force-release and lock-contention early-exit paths return
|
|
3922
|
+
// empty arrays without touching _lastUnmatchedChainOrders —
|
|
3923
|
+
// overwriting with [] would drop stale unmatched entries.
|
|
3924
|
+
if (syncResult && Array.isArray(syncResult.unmatchedChainOrders)) {
|
|
3925
|
+
const processed = (syncResult.filledOrders?.length || 0) +
|
|
3926
|
+
(syncResult.updatedOrders?.length || 0) +
|
|
3927
|
+
(syncResult.ordersNeedingCorrection?.length || 0);
|
|
3928
|
+
if (processed > 0) {
|
|
3929
|
+
this.manager._lastUnmatchedChainOrders = syncResult.unmatchedChainOrders;
|
|
3930
|
+
this.manager.logger.log(`[COW] Adopted chain order(s) via sync: ${processed} processed, ` +
|
|
3931
|
+
`${syncResult.unmatchedChainOrders.length} still unmatched`, 'info');
|
|
3932
|
+
}
|
|
3933
|
+
else {
|
|
3934
|
+
// processed === 0 with unmatched still present means the
|
|
3935
|
+
// sync could not adopt the unmatched chain orders. This is
|
|
3936
|
+
// normal when called re-entrantly (inside _fillProcessingLock):
|
|
3937
|
+
// the sync engine runs inline and either timed out (unlikely
|
|
3938
|
+
// for a re-entrant call) or all chain orders were already
|
|
3939
|
+
// matched — leaving only stale unmatched entries.
|
|
3940
|
+
const syncUnmatchedCount = syncResult.unmatchedChainOrders.length;
|
|
3941
|
+
this.manager.logger.log(`[COW] Sync returned without processing (processed=0, ` +
|
|
3942
|
+
`unmatched=${syncUnmatchedCount} in result, ` +
|
|
3943
|
+
`_lastUnmatchedChainOrders=${unmatchedChainOrders.length}). ` +
|
|
3944
|
+
`Structural resync will handle adoption.`, syncUnmatchedCount > 0 ? 'warn' : 'debug');
|
|
3945
|
+
// If the sync result itself has unmatched entries that
|
|
3946
|
+
// differ from _lastUnmatchedChainOrders, adopt them now
|
|
3947
|
+
// so the stale tracker is more accurate for the resync.
|
|
3948
|
+
if (syncUnmatchedCount > 0 && syncUnmatchedCount !== unmatchedChainOrders.length) {
|
|
3949
|
+
this.manager._lastUnmatchedChainOrders = syncResult.unmatchedChainOrders.map((o) => ({ ...o }));
|
|
3950
|
+
this.manager.logger.log(`[COW] Updated _lastUnmatchedChainOrders from sync result: ` +
|
|
3951
|
+
`${unmatchedChainOrders.length} → ${syncUnmatchedCount}`, 'debug');
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
catch (syncErr) {
|
|
3958
|
+
this.manager.logger.log(`[COW] Failed to sync/unmatched orders: ${syncErr?.message || syncErr}`, 'warn');
|
|
3959
|
+
}
|
|
3960
|
+
// Working grid is stale after master grid mutation from sync.
|
|
3961
|
+
// Request structural resync to rebuild the grid on the next cycle.
|
|
3962
|
+
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
3963
|
+
if (this.manager._recoveryState)
|
|
3964
|
+
this.manager._recoveryState.structuralResyncRequested = true;
|
|
3965
|
+
await this.manager.requestStructuralGridResync('unmatched chain orders before COW create', { unmatchedChainOrders: unmatchedChainOrders });
|
|
3966
|
+
}
|
|
3967
|
+
this.manager.logger.log(`[COW] Rejecting CREATE batch after sync: working grid invalidated by master mutation`, 'info');
|
|
3968
|
+
return {
|
|
3969
|
+
executed: false,
|
|
3970
|
+
aborted: true,
|
|
3971
|
+
reason: 'UNMATCHED_CHAIN_ORDERS',
|
|
3972
|
+
hadRotation: false
|
|
3973
|
+
};
|
|
3854
3974
|
}
|
|
3855
3975
|
const { assetA, assetB } = this.manager.assets;
|
|
3856
3976
|
const operations = [];
|