dexbot 1.1.13 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/market_adapter/market_adapter.js +1 -1
- package/dist/market_adapter/market_adapter.js.map +1 -1
- package/dist/modules/bitshares-native/lru_cache.d.ts +20 -0
- package/dist/modules/bitshares-native/lru_cache.d.ts.map +1 -0
- package/dist/modules/bitshares-native/lru_cache.js +45 -0
- package/dist/modules/bitshares-native/lru_cache.js.map +1 -0
- package/dist/modules/bitshares-native/resolvers.d.ts +2 -18
- package/dist/modules/bitshares-native/resolvers.d.ts.map +1 -1
- package/dist/modules/bitshares-native/resolvers.js +2 -41
- package/dist/modules/bitshares-native/resolvers.js.map +1 -1
- package/dist/modules/bitshares-native/serial/chain_constants.d.ts +1 -0
- package/dist/modules/bitshares-native/serial/chain_constants.d.ts.map +1 -1
- package/dist/modules/bitshares-native/serial/chain_constants.js +3 -0
- package/dist/modules/bitshares-native/serial/chain_constants.js.map +1 -1
- package/dist/modules/bitshares-native/signing_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/signing_client.js +12 -1
- package/dist/modules/bitshares-native/signing_client.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +42 -3
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +77 -27
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/bitshares-native/tx/builder.d.ts.map +1 -1
- package/dist/modules/bitshares-native/tx/builder.js +26 -16
- package/dist/modules/bitshares-native/tx/builder.js.map +1 -1
- package/dist/modules/bitshares-native/tx/tx_cache.d.ts +17 -0
- package/dist/modules/bitshares-native/tx/tx_cache.d.ts.map +1 -0
- package/dist/modules/bitshares-native/tx/tx_cache.js +62 -0
- package/dist/modules/bitshares-native/tx/tx_cache.js.map +1 -0
- package/dist/modules/bot_settings.d.ts.map +1 -1
- package/dist/modules/bot_settings.js +2 -1
- package/dist/modules/bot_settings.js.map +1 -1
- package/dist/modules/chain_orders.d.ts +4 -1
- package/dist/modules/chain_orders.d.ts.map +1 -1
- package/dist/modules/chain_orders.js +30 -1
- package/dist/modules/chain_orders.js.map +1 -1
- package/dist/modules/config.d.ts +6 -0
- package/dist/modules/config.d.ts.map +1 -1
- package/dist/modules/config.js +11 -0
- package/dist/modules/config.js.map +1 -1
- package/dist/modules/constants.d.ts +17 -1
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +84 -18
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/credit_runtime.d.ts.map +1 -1
- package/dist/modules/credit_runtime.js +40 -29
- package/dist/modules/credit_runtime.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +16 -12
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +183 -78
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_credential_client.d.ts +1 -0
- package/dist/modules/dexbot_credential_client.d.ts.map +1 -1
- package/dist/modules/dexbot_credential_client.js +15 -6
- package/dist/modules/dexbot_credential_client.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -5
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +78 -42
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/key_store.d.ts.map +1 -1
- package/dist/modules/key_store.js +2 -0
- package/dist/modules/key_store.js.map +1 -1
- package/dist/modules/launcher/child_env.d.ts.map +1 -1
- package/dist/modules/launcher/child_env.js +1 -0
- package/dist/modules/launcher/child_env.js.map +1 -1
- package/dist/modules/launcher/launch_modes.d.ts +2 -0
- package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
- package/dist/modules/launcher/launch_modes.js +7 -6
- package/dist/modules/launcher/launch_modes.js.map +1 -1
- package/dist/modules/node_manager.d.ts +9 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +35 -17
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +56 -13
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/async_lock.d.ts +33 -4
- package/dist/modules/order/async_lock.d.ts.map +1 -1
- package/dist/modules/order/async_lock.js +69 -7
- package/dist/modules/order/async_lock.js.map +1 -1
- package/dist/modules/order/grid.d.ts +435 -450
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +1621 -1548
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts +3 -73
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +11 -843
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +8 -0
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -0
- package/dist/modules/order/grid_reconcile_internal.js +771 -0
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -0
- package/dist/modules/order/logger.d.ts +1 -1
- package/dist/modules/order/logger.d.ts.map +1 -1
- package/dist/modules/order/logger.js +26 -2
- package/dist/modules/order/logger.js.map +1 -1
- package/dist/modules/order/manager.d.ts +40 -3
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +97 -15
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/runner.js +2 -2
- package/dist/modules/order/runner.js.map +1 -1
- package/dist/modules/order/strategy.js +3 -3
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +121 -29
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +17 -0
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +56 -19
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.js +4 -4
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +6 -8
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/scripts/print_grid.js +3 -3
- package/dist/scripts/print_grid.js.map +1 -1
- package/dist/scripts/test-credit-renewal.js +1 -1
- package/dist/scripts/test-credit-renewal.js.map +1 -1
- package/dist/unlock.d.ts.map +1 -1
- package/dist/unlock.js +26 -12
- package/dist/unlock.js.map +1 -1
- package/package.json +14 -4
|
@@ -71,7 +71,7 @@ const { ProcessedFillStore, PROCESSED_FILL_PERSISTENCE_MODES } = require('./orde
|
|
|
71
71
|
const DexbotFillRuntime = require('./dexbot_fill_runtime');
|
|
72
72
|
const DexbotMaintenanceRuntime = require('./dexbot_maintenance_runtime');
|
|
73
73
|
const CreditRuntime = require('./credit_runtime');
|
|
74
|
-
const { ORDER_STATES, ORDER_TYPES, REBALANCE_STATES, COW_ACTIONS, TIMING, PIPELINE_TIMING, MAINTENANCE, FILL_PROCESSING, DAEMON_CODES, } = require('./constants');
|
|
74
|
+
const { ORDER_STATES, ORDER_TYPES, REBALANCE_STATES, COW_ACTIONS, TIMING, PIPELINE_TIMING, GRID_LIMITS, MAINTENANCE, FILL_PROCESSING, DAEMON_CODES, } = require('./constants');
|
|
75
75
|
const { PATHS, getRecalculateTriggerFile } = require('./paths');
|
|
76
76
|
const { attemptResumePersistedGridByPriceMatch, decideStartupGridAction, reconcileGridOrders } = require('./order/grid_reconcile');
|
|
77
77
|
const { AccountOrders } = require('./account_orders');
|
|
@@ -127,6 +127,7 @@ class DEXBot {
|
|
|
127
127
|
_batchInFlight;
|
|
128
128
|
_recoverySyncInFlight;
|
|
129
129
|
_lastTargetedDriftSyncAt;
|
|
130
|
+
_lightweightSyncCheckAt;
|
|
130
131
|
_targetedDriftSyncCooldownMs;
|
|
131
132
|
_maintenanceCooldownCycles;
|
|
132
133
|
_lastGridActivityAt;
|
|
@@ -218,6 +219,7 @@ class DEXBot {
|
|
|
218
219
|
this._batchInFlight = false;
|
|
219
220
|
this._recoverySyncInFlight = false;
|
|
220
221
|
this._lastTargetedDriftSyncAt = 0;
|
|
222
|
+
this._lightweightSyncCheckAt = 0;
|
|
221
223
|
this._targetedDriftSyncCooldownMs = this.config.timing.TARGETED_DRIFT_SYNC_COOLDOWN_MS;
|
|
222
224
|
this._maintenanceCooldownCycles = 0;
|
|
223
225
|
this._lastGridActivityAt = 0;
|
|
@@ -246,25 +248,28 @@ class DEXBot {
|
|
|
246
248
|
*/
|
|
247
249
|
_validateStartupConfig(config) {
|
|
248
250
|
const errors = [];
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
251
|
+
// Skip trading field validation in credit-only mode
|
|
252
|
+
if (!config.creditOnly) {
|
|
253
|
+
// Validate startPrice is numeric or valid string mode
|
|
254
|
+
const startPrice = config.startPrice;
|
|
255
|
+
const validPriceModes = ['pool', 'book'];
|
|
256
|
+
const isPriceNumeric = typeof startPrice === 'number' && Number.isFinite(startPrice) && startPrice > 0;
|
|
257
|
+
const isPriceMode = typeof startPrice === 'string' && validPriceModes.includes(startPrice.toLowerCase());
|
|
258
|
+
if (!isPriceNumeric && !isPriceMode) {
|
|
259
|
+
errors.push(`startPrice must be a positive number or valid mode (${validPriceModes.join('/')}), got: ${startPrice}`);
|
|
260
|
+
}
|
|
261
|
+
// Validate assetA and assetB are present
|
|
262
|
+
if (!config.assetA || typeof config.assetA !== 'string') {
|
|
263
|
+
errors.push(`assetA must be a non-empty string, got: ${config.assetA}`);
|
|
264
|
+
}
|
|
265
|
+
if (!config.assetB || typeof config.assetB !== 'string') {
|
|
266
|
+
errors.push(`assetB must be a non-empty string, got: ${config.assetB}`);
|
|
267
|
+
}
|
|
268
|
+
// Validate incrementPercent
|
|
269
|
+
const increment = config.incrementPercent;
|
|
270
|
+
if (!Number.isFinite(increment) || increment <= 0 || increment > 100) {
|
|
271
|
+
errors.push(`incrementPercent must be between 0 and 100, got: ${increment}`);
|
|
272
|
+
}
|
|
268
273
|
}
|
|
269
274
|
// Throw all validation errors at once
|
|
270
275
|
if (errors.length > 0) {
|
|
@@ -373,7 +378,7 @@ class DEXBot {
|
|
|
373
378
|
this.manager.logger.log(`Triggering state recovery sync (${reason})...`, 'info');
|
|
374
379
|
await this.manager.fetchAccountTotals(this.accountId);
|
|
375
380
|
const openOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
376
|
-
await this.manager.syncFromOpenOrders(openOrders, { skipAccounting: true
|
|
381
|
+
await this.manager.syncFromOpenOrders(openOrders, { skipAccounting: true });
|
|
377
382
|
if (typeof this.manager.persistGrid === 'function') {
|
|
378
383
|
await this.manager.persistGrid();
|
|
379
384
|
}
|
|
@@ -575,14 +580,16 @@ class DEXBot {
|
|
|
575
580
|
const boundaryIdx = this.accountOrders.loadBoundaryIdx(true);
|
|
576
581
|
// 2. Load into manager (same path as startup at line 1340)
|
|
577
582
|
await Grid.loadGrid(this.manager, persistedGrid, boundaryIdx);
|
|
583
|
+
// Gap 1: Grid snapshot sanity check — shared logic with startup path.
|
|
584
|
+
if (await this._rejectCorruptedGridSnapshot('recovery')) {
|
|
585
|
+
return { success: false, reason: 'corrupted grid snapshot rejected (fund drift)' };
|
|
586
|
+
}
|
|
578
587
|
// 3. Read current chain state
|
|
579
588
|
const chainOpenOrders = await chainOrders.readOpenOrders(accountRef);
|
|
580
589
|
// 4. Reconcile
|
|
581
590
|
if (chainOpenOrders.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
582
591
|
await this.manager.syncFromOpenOrders(chainOpenOrders, {
|
|
583
592
|
skipAccounting: true,
|
|
584
|
-
fillLockAlreadyHeld: true,
|
|
585
|
-
protectCommittedOrders: true
|
|
586
593
|
});
|
|
587
594
|
}
|
|
588
595
|
// 5. Persist the reconciled state
|
|
@@ -598,6 +605,35 @@ class DEXBot {
|
|
|
598
605
|
return { success: false, reason: err.message };
|
|
599
606
|
}
|
|
600
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Reject a corrupted grid snapshot when catastrophic fund drift is detected.
|
|
610
|
+
* Shared between startup and recovery paths to avoid duplicating the
|
|
611
|
+
* drift-ratio math and snapshot-clearing logic.
|
|
612
|
+
*
|
|
613
|
+
* @param {'startup'|'recovery'} context - Controls log prefix.
|
|
614
|
+
* @returns {Promise<boolean>} True if the snapshot was rejected (cleared).
|
|
615
|
+
*/
|
|
616
|
+
async _rejectCorruptedGridSnapshot(context) {
|
|
617
|
+
if (!this.manager?.checkFundDriftAfterFills)
|
|
618
|
+
return false;
|
|
619
|
+
const driftCheck = this.manager.checkFundDriftAfterFills();
|
|
620
|
+
if (driftCheck.isValid)
|
|
621
|
+
return false;
|
|
622
|
+
const tag = context === 'recovery' ? '[RECOVERY][SNAPSHOT-REJECT]' : '[SNAPSHOT-REJECT]';
|
|
623
|
+
this._warn(`${tag} Corrupted grid snapshot detected: ` +
|
|
624
|
+
`drift sell=${driftCheck.driftSell.toFixed(2)} buy=${driftCheck.driftBuy.toFixed(2)}. ` +
|
|
625
|
+
`Deleting corrupted snapshot.`);
|
|
626
|
+
if (this.accountOrders && typeof this.accountOrders.clearGrid === 'function') {
|
|
627
|
+
try {
|
|
628
|
+
await this.accountOrders.clearGrid();
|
|
629
|
+
this._warn(`${tag} Corrupted grid snapshot deleted.`);
|
|
630
|
+
}
|
|
631
|
+
catch (clearErr) {
|
|
632
|
+
this._warn(`${tag} Failed to delete corrupted snapshot: ${clearErr.message}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
601
637
|
/**
|
|
602
638
|
* Attempt to repair size-drift for specific order IDs by reading their
|
|
603
639
|
* current on-chain state and correcting the local grid directly.
|
|
@@ -784,13 +820,14 @@ class DEXBot {
|
|
|
784
820
|
_buildOrphanFillFallbackKey(fill) {
|
|
785
821
|
return DexbotFillRuntime.buildOrphanFillFallbackKey(this, fill);
|
|
786
822
|
}
|
|
787
|
-
_isNewFillKey(fillKey, processedFillKeys, label = '') {
|
|
823
|
+
_isNewFillKey(fillKey, processedFillKeys, label = '', orderId = '') {
|
|
788
824
|
const now = Date.now();
|
|
789
825
|
if (this._recentlyQueuedFills.has(fillKey)) {
|
|
790
826
|
const lastProcessed = this._recentlyQueuedFills.get(fillKey);
|
|
791
827
|
if (now - lastProcessed < this._fillDedupeWindowMs) {
|
|
792
828
|
if (label) {
|
|
793
|
-
|
|
829
|
+
const idSuffix = orderId ? ` for ${orderId}` : '';
|
|
830
|
+
this.manager?.logger?.log?.(`${label} Skipping duplicate fill${idSuffix} (processed ${now - lastProcessed}ms ago)`, 'debug');
|
|
794
831
|
}
|
|
795
832
|
return false;
|
|
796
833
|
}
|
|
@@ -918,7 +955,7 @@ class DEXBot {
|
|
|
918
955
|
const chainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
919
956
|
if (this._shuttingDown)
|
|
920
957
|
return;
|
|
921
|
-
const syncResult = await this.manager.synchronizeWithChain(chainOpenOrders, 'readOpenOrders'
|
|
958
|
+
const syncResult = await this.manager.synchronizeWithChain(chainOpenOrders, 'readOpenOrders');
|
|
922
959
|
if (this._shuttingDown)
|
|
923
960
|
return;
|
|
924
961
|
if (syncResult?.filledOrders?.length > 0) {
|
|
@@ -935,7 +972,6 @@ class DEXBot {
|
|
|
935
972
|
await this._cancelDustOrders({
|
|
936
973
|
buy: reconnectHealth.buyDustOrders,
|
|
937
974
|
sell: reconnectHealth.sellDustOrders,
|
|
938
|
-
fillLockAlreadyHeld: true,
|
|
939
975
|
});
|
|
940
976
|
}
|
|
941
977
|
catch (_dustErr) {
|
|
@@ -1015,7 +1051,7 @@ class DEXBot {
|
|
|
1015
1051
|
if (!orphanFillKey) {
|
|
1016
1052
|
orphanFillKey = this._buildOrphanFillFallbackKey(fill);
|
|
1017
1053
|
}
|
|
1018
|
-
if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[POST-RESET]')) {
|
|
1054
|
+
if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[POST-RESET]', fillOp.order_id)) {
|
|
1019
1055
|
continue;
|
|
1020
1056
|
}
|
|
1021
1057
|
this._log(`[POST-RESET] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
|
|
@@ -1031,7 +1067,7 @@ class DEXBot {
|
|
|
1031
1067
|
}
|
|
1032
1068
|
this._log(`[POST-RESET] Processing fill for ${gridOrder.type} order ${gridOrder.id} at price ${gridOrder.price}`);
|
|
1033
1069
|
const trackedFillKey = buildFillKey(fill);
|
|
1034
|
-
if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[POST-RESET]')) {
|
|
1070
|
+
if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[POST-RESET]', fillOp.order_id)) {
|
|
1035
1071
|
continue;
|
|
1036
1072
|
}
|
|
1037
1073
|
const accountingResult = await this._applyReplaySafeTrackedFillAccounting(fill, fillOp, {
|
|
@@ -1057,7 +1093,7 @@ class DEXBot {
|
|
|
1057
1093
|
if (requiresOpenOrdersSync) {
|
|
1058
1094
|
this._log('[POST-RESET] Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
|
|
1059
1095
|
const postResetChainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1060
|
-
const syncResult = await this.manager.syncFromOpenOrders(postResetChainOpenOrders
|
|
1096
|
+
const syncResult = await this.manager.syncFromOpenOrders(postResetChainOpenOrders);
|
|
1061
1097
|
if (syncResult.filledOrders?.length > 0) {
|
|
1062
1098
|
await this._processFillsWithBatching(syncResult.filledOrders, new Set(), '[POST-RESET] open-orders fallback');
|
|
1063
1099
|
}
|
|
@@ -1088,7 +1124,6 @@ class DEXBot {
|
|
|
1088
1124
|
await this._cancelDustOrders({
|
|
1089
1125
|
buy: postResetHealth.buyDustOrders,
|
|
1090
1126
|
sell: postResetHealth.sellDustOrders,
|
|
1091
|
-
fillLockAlreadyHeld: true,
|
|
1092
1127
|
});
|
|
1093
1128
|
}
|
|
1094
1129
|
catch (_dustErr) {
|
|
@@ -1184,7 +1219,7 @@ class DEXBot {
|
|
|
1184
1219
|
if (Array.isArray(chainOpenOrders) && chainOpenOrders.length > 0) {
|
|
1185
1220
|
this._log('Generating new grid and syncing with existing on-chain orders...');
|
|
1186
1221
|
await Grid.initializeGrid(this.manager);
|
|
1187
|
-
await this.manager.syncFromOpenOrders(chainOpenOrders, { skipAccounting: true
|
|
1222
|
+
await this.manager.syncFromOpenOrders(chainOpenOrders, { skipAccounting: true });
|
|
1188
1223
|
const rebalanceResult = await reconcileGridOrders({
|
|
1189
1224
|
manager: this.manager,
|
|
1190
1225
|
config: this.config,
|
|
@@ -1192,7 +1227,6 @@ class DEXBot {
|
|
|
1192
1227
|
privateKey: this.privateKey,
|
|
1193
1228
|
chainOrders,
|
|
1194
1229
|
chainOpenOrders,
|
|
1195
|
-
fillLockAlreadyHeld: true,
|
|
1196
1230
|
});
|
|
1197
1231
|
await this._executeBatchIfNeeded(rebalanceResult, 'startup reconcile (regenerated grid)');
|
|
1198
1232
|
}
|
|
@@ -1206,7 +1240,7 @@ class DEXBot {
|
|
|
1206
1240
|
this._log('Found active session. Loading and syncing existing grid.');
|
|
1207
1241
|
await Grid.loadGrid(this.manager, persistedGrid, persistedBoundaryIdx);
|
|
1208
1242
|
let startupChainOpenOrders = chainOpenOrders;
|
|
1209
|
-
const syncResult = await this.manager.syncFromOpenOrders(startupChainOpenOrders, { skipAccounting: true
|
|
1243
|
+
const syncResult = await this.manager.syncFromOpenOrders(startupChainOpenOrders, { skipAccounting: true });
|
|
1210
1244
|
// Process price corrections queued during startup sync.
|
|
1211
1245
|
// These are not picked up by _consumeFillQueue until a fill
|
|
1212
1246
|
// arrives, which may never come for an idle market.
|
|
@@ -1220,7 +1254,7 @@ class DEXBot {
|
|
|
1220
1254
|
// Refresh open orders so startup reconcile works with post-batch chain reality
|
|
1221
1255
|
// and avoids reconciling against a stale pre-batch snapshot.
|
|
1222
1256
|
startupChainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1223
|
-
await this.manager.synchronizeWithChain(startupChainOpenOrders, 'readOpenOrders'
|
|
1257
|
+
await this.manager.synchronizeWithChain(startupChainOpenOrders, 'readOpenOrders');
|
|
1224
1258
|
}
|
|
1225
1259
|
}
|
|
1226
1260
|
const rebalanceResult = await reconcileGridOrders({
|
|
@@ -1230,11 +1264,12 @@ class DEXBot {
|
|
|
1230
1264
|
privateKey: this.privateKey,
|
|
1231
1265
|
chainOrders,
|
|
1232
1266
|
chainOpenOrders: startupChainOpenOrders,
|
|
1233
|
-
fillLockAlreadyHeld: true,
|
|
1234
1267
|
});
|
|
1235
1268
|
await this._executeBatchIfNeeded(rebalanceResult, 'startup reconcile (loaded grid)');
|
|
1236
1269
|
// Dust state is no longer persisted — cancelled immediately on detection.
|
|
1237
1270
|
await this._persistAndRecoverIfNeeded();
|
|
1271
|
+
// Gap 1: Grid snapshot sanity check — shared logic with recovery path.
|
|
1272
|
+
await this._rejectCorruptedGridSnapshot('startup');
|
|
1238
1273
|
}
|
|
1239
1274
|
// Drain any fills that arrived during startup while still in bootstrap
|
|
1240
1275
|
// mode. Safe to call directly since we already hold _fillProcessingLock
|
|
@@ -1247,13 +1282,12 @@ class DEXBot {
|
|
|
1247
1282
|
// Perform initial grid maintenance (thresholds, divergence, spread, health)
|
|
1248
1283
|
// Consolidated into shared logic to ensure consistent behavior at boot and runtime.
|
|
1249
1284
|
// CRITICAL: Pass lockAlreadyHeld since we're inside _fillProcessingLock.acquire()
|
|
1250
|
-
await this._runGridMaintenance('startup'
|
|
1285
|
+
await this._runGridMaintenance('startup');
|
|
1251
1286
|
// Cancel any dust from a previous bot lifetime immediately.
|
|
1252
1287
|
const startupHealth = await this.manager.checkGridHealth(this.updateOrdersOnChainPlan.bind(this));
|
|
1253
1288
|
await this._cancelDustOrders({
|
|
1254
1289
|
buy: startupHealth.buyDustOrders,
|
|
1255
1290
|
sell: startupHealth.sellDustOrders,
|
|
1256
|
-
fillLockAlreadyHeld: true,
|
|
1257
1291
|
});
|
|
1258
1292
|
this._log('Bootstrap phase complete - fill processing resumed', 'info');
|
|
1259
1293
|
}
|
|
@@ -1265,7 +1299,7 @@ class DEXBot {
|
|
|
1265
1299
|
await this._setupTriggerFileDetection();
|
|
1266
1300
|
await this._setupCreditRuntime();
|
|
1267
1301
|
await this._refreshAndSyncCreditRuntime();
|
|
1268
|
-
await this._runCreditRuntimeMaintenance('startup'
|
|
1302
|
+
await this._runCreditRuntimeMaintenance('startup');
|
|
1269
1303
|
this._setupBlockchainFetchInterval();
|
|
1270
1304
|
this._setupCreditWatchdogInterval();
|
|
1271
1305
|
this._setupCredentialDaemonWatchdogInterval();
|
|
@@ -1309,14 +1343,14 @@ class DEXBot {
|
|
|
1309
1343
|
}
|
|
1310
1344
|
try {
|
|
1311
1345
|
let openOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1312
|
-
const syncResult = await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders'
|
|
1346
|
+
const syncResult = await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders');
|
|
1313
1347
|
let aborted = false;
|
|
1314
1348
|
if (syncResult?.filledOrders?.length > 0) {
|
|
1315
1349
|
this._log(`[SYNC-CHAIN] ${syncResult.filledOrders.length} filled order(s) found during ${tag}`, 'info');
|
|
1316
1350
|
const batchResult = await this._processFillsWithBatching(syncResult.filledOrders, new Set(), `${tag} sync-fill`);
|
|
1317
1351
|
if (!batchResult?.aborted) {
|
|
1318
1352
|
openOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1319
|
-
await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders'
|
|
1353
|
+
await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders');
|
|
1320
1354
|
}
|
|
1321
1355
|
else {
|
|
1322
1356
|
aborted = true;
|
|
@@ -1564,7 +1598,7 @@ class DEXBot {
|
|
|
1564
1598
|
if (!orphanFillKey) {
|
|
1565
1599
|
orphanFillKey = this._buildOrphanFillFallbackKey(fill);
|
|
1566
1600
|
}
|
|
1567
|
-
if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[ORPHAN-FILL]')) {
|
|
1601
|
+
if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[ORPHAN-FILL]', fillOp.order_id)) {
|
|
1568
1602
|
continue;
|
|
1569
1603
|
}
|
|
1570
1604
|
this.manager.logger.log(`[ORPHAN-FILL] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
|
|
@@ -1594,7 +1628,7 @@ class DEXBot {
|
|
|
1594
1628
|
requiresOpenOrdersSync = true;
|
|
1595
1629
|
continue;
|
|
1596
1630
|
}
|
|
1597
|
-
if (!this._isNewFillKey(fillKey, processedFillKeys, '[FILL]')) {
|
|
1631
|
+
if (!this._isNewFillKey(fillKey, processedFillKeys, '[FILL]', fillOp.order_id)) {
|
|
1598
1632
|
continue;
|
|
1599
1633
|
}
|
|
1600
1634
|
validFills.push(fill);
|
|
@@ -1657,7 +1691,7 @@ class DEXBot {
|
|
|
1657
1691
|
}
|
|
1658
1692
|
this.manager.logger.log(`Syncing ${fillsToSync.length} fill(s) (open orders mode)`, 'info');
|
|
1659
1693
|
const chainOpenOrders = await chainOrders.readOpenOrders(this.account);
|
|
1660
|
-
const resultOpenOrders = await this.manager.syncFromOpenOrders(chainOpenOrders
|
|
1694
|
+
const resultOpenOrders = await this.manager.syncFromOpenOrders(chainOpenOrders);
|
|
1661
1695
|
// Dust is handled by post-fill detection below.
|
|
1662
1696
|
if (resultOpenOrders.filledOrders)
|
|
1663
1697
|
resolvedOrders.push(...resultOpenOrders.filledOrders);
|
|
@@ -1790,7 +1824,6 @@ class DEXBot {
|
|
|
1790
1824
|
const dustCancelResult = await this._cancelDustOrders({
|
|
1791
1825
|
buy: healthResult.buyDustOrders,
|
|
1792
1826
|
sell: healthResult.sellDustOrders,
|
|
1793
|
-
fillLockAlreadyHeld: true,
|
|
1794
1827
|
});
|
|
1795
1828
|
if (dustCancelResult?.batchResult?.aborted) {
|
|
1796
1829
|
abortedFillCycle = true;
|
|
@@ -1804,7 +1837,7 @@ class DEXBot {
|
|
|
1804
1837
|
// After: Grid maintenance waits for isPipelineEmpty() before structural changes
|
|
1805
1838
|
// Run only when the cycle contains at least one full fill.
|
|
1806
1839
|
if (shouldRunPostFillChecks && !abortedFillCycle) {
|
|
1807
|
-
await this._runGridMaintenance('post-fill'
|
|
1840
|
+
await this._runGridMaintenance('post-fill');
|
|
1808
1841
|
}
|
|
1809
1842
|
}
|
|
1810
1843
|
else if (pendingFillKeysForCurrentCycle.size > 0) {
|
|
@@ -1929,7 +1962,7 @@ class DEXBot {
|
|
|
1929
1962
|
if (!orphanFillKey) {
|
|
1930
1963
|
orphanFillKey = this._buildOrphanFillFallbackKey(fill);
|
|
1931
1964
|
}
|
|
1932
|
-
if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[BOOTSTRAP]')) {
|
|
1965
|
+
if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[BOOTSTRAP]', fillOp.order_id)) {
|
|
1933
1966
|
continue;
|
|
1934
1967
|
}
|
|
1935
1968
|
this.manager.logger.log(`[BOOTSTRAP] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
|
|
@@ -1942,7 +1975,7 @@ class DEXBot {
|
|
|
1942
1975
|
continue;
|
|
1943
1976
|
}
|
|
1944
1977
|
const trackedFillKey = buildFillKey(fill);
|
|
1945
|
-
if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[BOOTSTRAP]')) {
|
|
1978
|
+
if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[BOOTSTRAP]', fillOp.order_id)) {
|
|
1946
1979
|
continue;
|
|
1947
1980
|
}
|
|
1948
1981
|
const accountingResult = await this._applyReplaySafeTrackedFillAccounting(fill, fillOp, {
|
|
@@ -1963,7 +1996,7 @@ class DEXBot {
|
|
|
1963
1996
|
if (requiresOpenOrdersSync) {
|
|
1964
1997
|
this._log('[BOOTSTRAP] Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
|
|
1965
1998
|
const bootstrapChainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1966
|
-
const syncResult = await this.manager.syncFromOpenOrders(bootstrapChainOpenOrders
|
|
1999
|
+
const syncResult = await this.manager.syncFromOpenOrders(bootstrapChainOpenOrders);
|
|
1967
2000
|
if (syncResult.filledOrders?.length > 0) {
|
|
1968
2001
|
const queuedOrderIds = new Set(validFills.map(fill => fill?.gridOrder?.orderId).filter(Boolean));
|
|
1969
2002
|
for (const filledOrder of syncResult.filledOrders) {
|
|
@@ -2201,8 +2234,6 @@ class DEXBot {
|
|
|
2201
2234
|
const openOrders = await chainOrders.readOpenOrders(accountRef);
|
|
2202
2235
|
const recoveryResult = await this.manager.syncFromOpenOrders(openOrders, {
|
|
2203
2236
|
skipAccounting: false,
|
|
2204
|
-
fillLockAlreadyHeld: true,
|
|
2205
|
-
protectCommittedOrders: true
|
|
2206
2237
|
});
|
|
2207
2238
|
this._preserveMissingCreateBlockersAfterRecovery(preRecoveryMissingCreateBlockers, recoveryResult);
|
|
2208
2239
|
// Persist any master grid mutations from the recovery sync. The
|
|
@@ -2546,13 +2577,10 @@ class DEXBot {
|
|
|
2546
2577
|
* @returns {Promise<Object>} Result object compatible with batch return shape
|
|
2547
2578
|
*/
|
|
2548
2579
|
async _reconcileAfterUncertainBroadcast(err, opContexts, options = {}) {
|
|
2549
|
-
if (
|
|
2550
|
-
this.manager
|
|
2551
|
-
|
|
2552
|
-
return this.manager._fillProcessingLock.acquire(
|
|
2553
|
-
...options,
|
|
2554
|
-
fillLockAlreadyHeld: true
|
|
2555
|
-
})));
|
|
2580
|
+
if (this.manager?._fillProcessingLock &&
|
|
2581
|
+
typeof this.manager._fillProcessingLock.acquire === 'function' &&
|
|
2582
|
+
!this.manager._fillProcessingLock.isReentrant()) {
|
|
2583
|
+
return this.manager._fillProcessingLock.acquire(() => this._reconcileAfterUncertainBroadcast(err, opContexts, options));
|
|
2556
2584
|
}
|
|
2557
2585
|
return this._reconcileAfterUncertainBroadcastImpl(err, opContexts, options);
|
|
2558
2586
|
}
|
|
@@ -2665,8 +2693,6 @@ class DEXBot {
|
|
|
2665
2693
|
try {
|
|
2666
2694
|
await this.manager.syncFromOpenOrders(latestSnapshot, {
|
|
2667
2695
|
skipAccounting: true,
|
|
2668
|
-
fillLockAlreadyHeld: true,
|
|
2669
|
-
protectCommittedOrders: true
|
|
2670
2696
|
});
|
|
2671
2697
|
hadRotation = true;
|
|
2672
2698
|
}
|
|
@@ -2678,6 +2704,15 @@ class DEXBot {
|
|
|
2678
2704
|
if (fallbackResult.success) {
|
|
2679
2705
|
hadRotation = true;
|
|
2680
2706
|
}
|
|
2707
|
+
else {
|
|
2708
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Persisted grid reload failed: ${fallbackResult.reason}. ` +
|
|
2709
|
+
`Escalating to structural resync.`, 'warn');
|
|
2710
|
+
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
2711
|
+
this.manager.requestStructuralGridResync('cow-uncertain-recovery-failed', { reason: fallbackResult.reason }).catch((escalateErr) => {
|
|
2712
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Escalation to structural resync failed: ${escalateErr.message}`, 'error');
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2681
2716
|
}
|
|
2682
2717
|
}
|
|
2683
2718
|
// 4. Log structured summary.
|
|
@@ -2712,9 +2747,52 @@ class DEXBot {
|
|
|
2712
2747
|
catch (orphanErr) {
|
|
2713
2748
|
this.manager.logger.log(`[COW][UNCERTAIN] Auto-cancel pass failed: ${orphanErr?.message || orphanErr}`, 'warn');
|
|
2714
2749
|
}
|
|
2750
|
+
// Boundary shift recovery: the COW batch that should have committed the
|
|
2751
|
+
// boundary shift from this fill cycle failed before commit. Every planned
|
|
2752
|
+
// CREATE in the batch represents a fill whose opposite-side boundary step
|
|
2753
|
+
// was lost — regardless of whether the individual CREATE was later adopted
|
|
2754
|
+
// on chain (adopted) or not (discarded). Manually adjust manager.boundaryIdx
|
|
2755
|
+
// so the next COW cycle (triggered by the next fill or periodic maintenance)
|
|
2756
|
+
// generates replacement opposite-side orders using the correct boundary.
|
|
2757
|
+
//
|
|
2758
|
+
// NOTE: Direct mutation of manager.boundaryIdx outside a COW commit
|
|
2759
|
+
// violates the invariant stated in grid.ts:1217-1220 (boundary must only
|
|
2760
|
+
// be updated atomically inside _commitWorkingGrid). This is intentional
|
|
2761
|
+
// here because the COW commit already failed — the invariant was already
|
|
2762
|
+
// broken by the broadcast uncertainty, and the adjustment only restores
|
|
2763
|
+
// the state to what the successful commit would have produced.
|
|
2764
|
+
if (hadRotation && this.manager && pending.length > 0) {
|
|
2765
|
+
let boundaryShift = 0;
|
|
2766
|
+
for (const entry of pending) {
|
|
2767
|
+
const orderType = entry.orderType || entry.order?.type;
|
|
2768
|
+
if (orderType === ORDER_TYPES.SELL) {
|
|
2769
|
+
boundaryShift--; // SELL CREATE → fill was BUY → boundary LEFT
|
|
2770
|
+
}
|
|
2771
|
+
else if (orderType === ORDER_TYPES.BUY) {
|
|
2772
|
+
boundaryShift++; // BUY CREATE → fill was SELL → boundary RIGHT
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
if (boundaryShift !== 0) {
|
|
2776
|
+
const oldIdx = this.manager.boundaryIdx;
|
|
2777
|
+
if (typeof oldIdx === 'number') {
|
|
2778
|
+
const maxIdx = Math.max(0, (this.manager.orders?.size || 1) - 1);
|
|
2779
|
+
const newIdx = Math.max(0, Math.min(maxIdx, oldIdx + boundaryShift));
|
|
2780
|
+
if (newIdx !== oldIdx) {
|
|
2781
|
+
this.manager.boundaryIdx = newIdx;
|
|
2782
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Boundary adjusted by ${boundaryShift} ` +
|
|
2783
|
+
`(${adopted.length} adopted / ${discarded.length} discarded CREATE(s)): ` +
|
|
2784
|
+
`${oldIdx} → ${newIdx}. Next COW cycle will generate replacement opposite-side orders.`, 'warn');
|
|
2785
|
+
if (typeof this.manager._markGridDirty === 'function') {
|
|
2786
|
+
this.manager._markGridDirty(`boundary adjustment after ${pending.length} planned CREATE(s)`);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2715
2792
|
// Persist master grid mutations from the reconciliation sync and any
|
|
2716
2793
|
// orphan auto-cancellation that ran above. These apply outside the COW
|
|
2717
|
-
// broadcast path and would otherwise be in-memory only.
|
|
2794
|
+
// broadcast path and would otherwise be in-memory only. The boundary
|
|
2795
|
+
// adjustment above is also persisted here.
|
|
2718
2796
|
if (typeof this.manager?.persistGrid === 'function') {
|
|
2719
2797
|
await this.manager.persistGrid();
|
|
2720
2798
|
}
|
|
@@ -2852,8 +2930,6 @@ class DEXBot {
|
|
|
2852
2930
|
if (freshChain.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
2853
2931
|
await this.manager.syncFromOpenOrders(freshChain, {
|
|
2854
2932
|
skipAccounting: true,
|
|
2855
|
-
fillLockAlreadyHeld: true,
|
|
2856
|
-
protectCommittedOrders: true
|
|
2857
2933
|
});
|
|
2858
2934
|
}
|
|
2859
2935
|
}
|
|
@@ -3270,7 +3346,7 @@ class DEXBot {
|
|
|
3270
3346
|
this._resumeGridPersistenceAfterCredentialRecovery('credential recovery started');
|
|
3271
3347
|
const runRecovery = async () => {
|
|
3272
3348
|
await this._triggerStateRecoverySync('credential daemon restored');
|
|
3273
|
-
await this._runGridMaintenance('credential-recovery'
|
|
3349
|
+
await this._runGridMaintenance('credential-recovery');
|
|
3274
3350
|
};
|
|
3275
3351
|
if (this.manager?._fillProcessingLock) {
|
|
3276
3352
|
await this.manager._fillProcessingLock.acquire(runRecovery);
|
|
@@ -3609,6 +3685,22 @@ class DEXBot {
|
|
|
3609
3685
|
};
|
|
3610
3686
|
}
|
|
3611
3687
|
const hasCreateActions = actions.some(action => action.type === COW_ACTIONS.CREATE);
|
|
3688
|
+
// Gap 5: Recovery exhausted — block all CREATES. The bot has exhausted
|
|
3689
|
+
// its recovery attempt budget and needs a fill or sync cycle to reset.
|
|
3690
|
+
// Existing orders remain active and are monitored, but no new orders
|
|
3691
|
+
// are placed until recovery resets.
|
|
3692
|
+
if (hasCreateActions && this.manager?._recoveryExhaustedAt) {
|
|
3693
|
+
const exhaustedAge = Date.now() - this.manager._recoveryExhaustedAt;
|
|
3694
|
+
this.manager.logger.log?.(`[RECOVERY-EXHAUSTED] Blocking ${actions.filter(a => a.type === COW_ACTIONS.CREATE).length} CREATE(s) ` +
|
|
3695
|
+
`(exhausted ${(exhaustedAge / 1000).toFixed(0)}s ago). ` +
|
|
3696
|
+
`Waiting for next fill or sync cycle to reset recovery state.`, 'warn');
|
|
3697
|
+
return {
|
|
3698
|
+
executed: false,
|
|
3699
|
+
aborted: true,
|
|
3700
|
+
reason: 'RECOVERY_EXHAUSTED',
|
|
3701
|
+
hadRotation: false
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3612
3704
|
const unmatchedChainOrders = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
3613
3705
|
? this.manager._lastUnmatchedChainOrders
|
|
3614
3706
|
: [];
|
|
@@ -3654,7 +3746,7 @@ class DEXBot {
|
|
|
3654
3746
|
this.manager.logger.log(`[COW] Failed to cancel unmatched chain order: ${r.reason?.message || r.reason}`, 'warn');
|
|
3655
3747
|
}
|
|
3656
3748
|
}
|
|
3657
|
-
if (cancelledUnmatched >= cancelable.length) {
|
|
3749
|
+
if (cancelable.length > 0 && cancelledUnmatched >= cancelable.length) {
|
|
3658
3750
|
this.manager.logger.log(`[COW] Cancelled all ${cancelledUnmatched} unmatched chain order(s); proceeding with CREATE batch.`, 'warn');
|
|
3659
3751
|
// Optimistic clear: the cancel txs are sent but not yet
|
|
3660
3752
|
// confirmed. If a cancel silently fails on chain, the
|
|
@@ -3663,7 +3755,7 @@ class DEXBot {
|
|
|
3663
3755
|
// is idempotent and the re-detection is non-blocking.
|
|
3664
3756
|
this.manager._lastUnmatchedChainOrders = [];
|
|
3665
3757
|
}
|
|
3666
|
-
else {
|
|
3758
|
+
else if (cancelable.length > 0) {
|
|
3667
3759
|
this.manager.logger.log(`[COW] Cancelled ${cancelledUnmatched}/${cancelable.length} unmatched chain order(s); ` +
|
|
3668
3760
|
`remaining must be handled by structural reconciliation.`, 'warn');
|
|
3669
3761
|
}
|
|
@@ -3711,7 +3803,7 @@ class DEXBot {
|
|
|
3711
3803
|
batchId: this._currentBatchId || null,
|
|
3712
3804
|
payload: null,
|
|
3713
3805
|
timeoutMs: null
|
|
3714
|
-
}), []
|
|
3806
|
+
}), []);
|
|
3715
3807
|
}
|
|
3716
3808
|
catch (recoverErr) {
|
|
3717
3809
|
this.manager.logger.log(`[COW] Recovery from pending broadcasts failed: ${recoverErr?.message || recoverErr}`, 'error');
|
|
@@ -4071,7 +4163,7 @@ class DEXBot {
|
|
|
4071
4163
|
// throwing would re-enter the catch loop on the next attempt and
|
|
4072
4164
|
// potentially double-publish.
|
|
4073
4165
|
if (err instanceof BroadcastUncertainError) {
|
|
4074
|
-
return await this._reconcileAfterUncertainBroadcast(err, opContexts
|
|
4166
|
+
return await this._reconcileAfterUncertainBroadcast(err, opContexts);
|
|
4075
4167
|
}
|
|
4076
4168
|
// Handle hard abort
|
|
4077
4169
|
const hardAbortResult = await this._handleBatchHardAbort(err, 'COW batch processing', operations.length);
|
|
@@ -4377,6 +4469,10 @@ class DEXBot {
|
|
|
4377
4469
|
*/
|
|
4378
4470
|
async _runStartupSequence() {
|
|
4379
4471
|
try {
|
|
4472
|
+
if (this.config.creditOnly) {
|
|
4473
|
+
await this._runCreditOnlyStartup();
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4380
4476
|
const startupState = await this._initializeStartupState();
|
|
4381
4477
|
await this._finishStartupSequence(startupState);
|
|
4382
4478
|
}
|
|
@@ -4386,13 +4482,20 @@ class DEXBot {
|
|
|
4386
4482
|
throw err;
|
|
4387
4483
|
}
|
|
4388
4484
|
}
|
|
4485
|
+
async _runCreditOnlyStartup() {
|
|
4486
|
+
await this._setupCreditRuntime();
|
|
4487
|
+
await this._refreshAndSyncCreditRuntime();
|
|
4488
|
+
await this._runCreditRuntimeMaintenance('startup');
|
|
4489
|
+
this._setupCreditWatchdogInterval();
|
|
4490
|
+
this._setupCredentialDaemonWatchdogInterval();
|
|
4491
|
+
this._log('DEXBot started (credit-only mode)');
|
|
4492
|
+
}
|
|
4389
4493
|
/**
|
|
4390
4494
|
* Perform periodic grid checks: fund thresholds, spread condition, grid health.
|
|
4391
4495
|
* Called by the periodic blockchain fetch interval to check if grid needs updates.
|
|
4392
4496
|
*
|
|
4393
4497
|
* IMPORTANT: This method MUST only be called from within _fillProcessingLock.acquire()
|
|
4394
|
-
* (specifically from _setupBlockchainFetchInterval).
|
|
4395
|
-
* to avoid deadlock with _consumeFillQueue which uses the same lock ordering.
|
|
4498
|
+
* (specifically from _setupBlockchainFetchInterval).
|
|
4396
4499
|
*
|
|
4397
4500
|
* @private
|
|
4398
4501
|
*/
|
|
@@ -4539,14 +4642,17 @@ class DEXBot {
|
|
|
4539
4642
|
return { skipped: true, reason: 'grid resync unavailable' };
|
|
4540
4643
|
}
|
|
4541
4644
|
const message = reason ? `[CR-RESET] ${reason}` : '[CR-RESET] grid reset requested';
|
|
4542
|
-
// Manual/programmatic resets should advance the persisted center baseline
|
|
4543
|
-
// before rebuilding the grid, unless a caller explicitly disables it.
|
|
4544
4645
|
this._log(`${message}; rebuilding grid from fresh on-chain state`, 'info');
|
|
4545
4646
|
const resetOptions = {
|
|
4546
4647
|
...options,
|
|
4547
4648
|
refreshCenterPrice: options.refreshCenterPrice !== false,
|
|
4548
4649
|
};
|
|
4549
|
-
if (
|
|
4650
|
+
// Skip acquire if no lock exists (no fill processing to serialize against)
|
|
4651
|
+
// or if the caller already holds the lock (re-entrant). In the latter case
|
|
4652
|
+
// the lock's isReentrant() check prevents queueing and runs inline.
|
|
4653
|
+
// Otherwise, wait on the queue — this is the intended path for callers
|
|
4654
|
+
// outside the fill-processing context that need exclusive access.
|
|
4655
|
+
if (!this.manager._fillProcessingLock || this.manager._fillProcessingLock.isReentrant()) {
|
|
4550
4656
|
return this._performGridResync(resetOptions);
|
|
4551
4657
|
}
|
|
4552
4658
|
return this.manager._fillProcessingLock.acquire(async () => this._performGridResync(resetOptions));
|
|
@@ -4616,6 +4722,7 @@ class DEXBot {
|
|
|
4616
4722
|
fillProcessingLockActive: this.manager?._fillProcessingLock?.isLocked() || false,
|
|
4617
4723
|
divergenceLockActive: this.manager?._divergenceLock?.isLocked() || false,
|
|
4618
4724
|
shadowLocksActive: this.manager?.shadowOrderIds?.size || 0,
|
|
4725
|
+
recoveryExhaustedAt: this.manager?._recoveryExhaustedAt || null,
|
|
4619
4726
|
recentFillsTracked: this._recentlyProcessedFills.size
|
|
4620
4727
|
};
|
|
4621
4728
|
}
|
|
@@ -4657,12 +4764,12 @@ class DEXBot {
|
|
|
4657
4764
|
* Cancel dust partial orders immediately — no maps, no timers, no delay.
|
|
4658
4765
|
* Each dust order is cancelled on chain and its slot rotated through the
|
|
4659
4766
|
* synthetic-fill pipeline.
|
|
4660
|
-
* @param {{ buy: Array, sell: Array
|
|
4767
|
+
* @param {{ buy: Array, sell: Array }} options
|
|
4661
4768
|
* @returns {Promise<{cancelledCount: number, batchResult: {aborted: boolean}|null}>}
|
|
4662
4769
|
* @private
|
|
4663
4770
|
*/
|
|
4664
|
-
async _cancelDustOrders({ buy: buyDust = [], sell: sellDust = []
|
|
4665
|
-
return DexbotMaintenanceRuntime.cancelDustOrders(this, { buy: buyDust, sell: sellDust
|
|
4771
|
+
async _cancelDustOrders({ buy: buyDust = [], sell: sellDust = [] } = {}) {
|
|
4772
|
+
return DexbotMaintenanceRuntime.cancelDustOrders(this, { buy: buyDust, sell: sellDust });
|
|
4666
4773
|
}
|
|
4667
4774
|
/**
|
|
4668
4775
|
* Set up the periodic dust health check interval.
|
|
@@ -4687,7 +4794,6 @@ class DEXBot {
|
|
|
4687
4794
|
await this._cancelDustOrders({
|
|
4688
4795
|
buy: health.buyDustOrders,
|
|
4689
4796
|
sell: health.sellDustOrders,
|
|
4690
|
-
fillLockAlreadyHeld: true,
|
|
4691
4797
|
});
|
|
4692
4798
|
});
|
|
4693
4799
|
}
|
|
@@ -4713,7 +4819,6 @@ class DEXBot {
|
|
|
4713
4819
|
*
|
|
4714
4820
|
* LOCK ORDERING:
|
|
4715
4821
|
* - Canonical order: _fillProcessingLock → _divergenceLock
|
|
4716
|
-
* - This function handles lock acquisition based on fillLockAlreadyHeld parameter
|
|
4717
4822
|
* - When called from post-fill context, fill lock is already held
|
|
4718
4823
|
* - When called from periodic context, both locks must be acquired
|
|
4719
4824
|
* - Matches the order used in _consumeFillQueue to prevent deadlocks
|