dexbot 1.1.14 → 1.2.1

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.
Files changed (125) hide show
  1. package/README.md +1 -0
  2. package/dist/credential-daemon.d.ts.map +1 -1
  3. package/dist/credential-daemon.js +6 -3
  4. package/dist/credential-daemon.js.map +1 -1
  5. package/dist/market_adapter/market_adapter.js +1 -1
  6. package/dist/market_adapter/market_adapter.js.map +1 -1
  7. package/dist/modules/bitshares-native/lru_cache.d.ts +20 -0
  8. package/dist/modules/bitshares-native/lru_cache.d.ts.map +1 -0
  9. package/dist/modules/bitshares-native/lru_cache.js +45 -0
  10. package/dist/modules/bitshares-native/lru_cache.js.map +1 -0
  11. package/dist/modules/bitshares-native/resolvers.d.ts +2 -18
  12. package/dist/modules/bitshares-native/resolvers.d.ts.map +1 -1
  13. package/dist/modules/bitshares-native/resolvers.js +2 -41
  14. package/dist/modules/bitshares-native/resolvers.js.map +1 -1
  15. package/dist/modules/bitshares-native/serial/chain_constants.d.ts +1 -0
  16. package/dist/modules/bitshares-native/serial/chain_constants.d.ts.map +1 -1
  17. package/dist/modules/bitshares-native/serial/chain_constants.js +3 -0
  18. package/dist/modules/bitshares-native/serial/chain_constants.js.map +1 -1
  19. package/dist/modules/bitshares-native/signing_client.d.ts.map +1 -1
  20. package/dist/modules/bitshares-native/signing_client.js +12 -1
  21. package/dist/modules/bitshares-native/signing_client.js.map +1 -1
  22. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  23. package/dist/modules/bitshares-native/subscriptions.js +42 -3
  24. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  25. package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
  26. package/dist/modules/bitshares-native/transport.js +77 -27
  27. package/dist/modules/bitshares-native/transport.js.map +1 -1
  28. package/dist/modules/bitshares-native/tx/builder.d.ts.map +1 -1
  29. package/dist/modules/bitshares-native/tx/builder.js +26 -16
  30. package/dist/modules/bitshares-native/tx/builder.js.map +1 -1
  31. package/dist/modules/bitshares-native/tx/tx_cache.d.ts +17 -0
  32. package/dist/modules/bitshares-native/tx/tx_cache.d.ts.map +1 -0
  33. package/dist/modules/bitshares-native/tx/tx_cache.js +62 -0
  34. package/dist/modules/bitshares-native/tx/tx_cache.js.map +1 -0
  35. package/dist/modules/bot_settings.d.ts.map +1 -1
  36. package/dist/modules/bot_settings.js +2 -1
  37. package/dist/modules/bot_settings.js.map +1 -1
  38. package/dist/modules/chain_orders.d.ts +4 -1
  39. package/dist/modules/chain_orders.d.ts.map +1 -1
  40. package/dist/modules/chain_orders.js +30 -1
  41. package/dist/modules/chain_orders.js.map +1 -1
  42. package/dist/modules/config.d.ts +6 -0
  43. package/dist/modules/config.d.ts.map +1 -1
  44. package/dist/modules/config.js +11 -0
  45. package/dist/modules/config.js.map +1 -1
  46. package/dist/modules/constants.d.ts +14 -1
  47. package/dist/modules/constants.d.ts.map +1 -1
  48. package/dist/modules/constants.js +64 -6
  49. package/dist/modules/constants.js.map +1 -1
  50. package/dist/modules/credential_policy.d.ts.map +1 -1
  51. package/dist/modules/credential_policy.js +7 -5
  52. package/dist/modules/credential_policy.js.map +1 -1
  53. package/dist/modules/credit_runtime.d.ts.map +1 -1
  54. package/dist/modules/credit_runtime.js +40 -29
  55. package/dist/modules/credit_runtime.js.map +1 -1
  56. package/dist/modules/dexbot_class.d.ts +6 -12
  57. package/dist/modules/dexbot_class.d.ts.map +1 -1
  58. package/dist/modules/dexbot_class.js +155 -81
  59. package/dist/modules/dexbot_class.js.map +1 -1
  60. package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -5
  61. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  62. package/dist/modules/dexbot_maintenance_runtime.js +23 -48
  63. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  64. package/dist/modules/launcher/child_env.d.ts.map +1 -1
  65. package/dist/modules/launcher/child_env.js +1 -0
  66. package/dist/modules/launcher/child_env.js.map +1 -1
  67. package/dist/modules/launcher/launch_modes.d.ts +2 -0
  68. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  69. package/dist/modules/launcher/launch_modes.js +7 -6
  70. package/dist/modules/launcher/launch_modes.js.map +1 -1
  71. package/dist/modules/order/accounting.d.ts +8 -2
  72. package/dist/modules/order/accounting.d.ts.map +1 -1
  73. package/dist/modules/order/accounting.js +64 -26
  74. package/dist/modules/order/accounting.js.map +1 -1
  75. package/dist/modules/order/async_lock.d.ts +12 -4
  76. package/dist/modules/order/async_lock.d.ts.map +1 -1
  77. package/dist/modules/order/async_lock.js +27 -5
  78. package/dist/modules/order/async_lock.js.map +1 -1
  79. package/dist/modules/order/grid.d.ts +434 -475
  80. package/dist/modules/order/grid.d.ts.map +1 -1
  81. package/dist/modules/order/grid.js +1612 -1592
  82. package/dist/modules/order/grid.js.map +1 -1
  83. package/dist/modules/order/grid_reconcile.d.ts +3 -73
  84. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  85. package/dist/modules/order/grid_reconcile.js +11 -843
  86. package/dist/modules/order/grid_reconcile.js.map +1 -1
  87. package/dist/modules/order/grid_reconcile_internal.d.ts +8 -0
  88. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -0
  89. package/dist/modules/order/grid_reconcile_internal.js +782 -0
  90. package/dist/modules/order/grid_reconcile_internal.js.map +1 -0
  91. package/dist/modules/order/logger.d.ts +1 -1
  92. package/dist/modules/order/logger.d.ts.map +1 -1
  93. package/dist/modules/order/logger.js +26 -2
  94. package/dist/modules/order/logger.js.map +1 -1
  95. package/dist/modules/order/manager.d.ts +118 -15
  96. package/dist/modules/order/manager.d.ts.map +1 -1
  97. package/dist/modules/order/manager.js +206 -236
  98. package/dist/modules/order/manager.js.map +1 -1
  99. package/dist/modules/order/runner.js +2 -2
  100. package/dist/modules/order/runner.js.map +1 -1
  101. package/dist/modules/order/strategy.js +3 -3
  102. package/dist/modules/order/strategy.js.map +1 -1
  103. package/dist/modules/order/sync_engine.d.ts +3 -4
  104. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  105. package/dist/modules/order/sync_engine.js +85 -32
  106. package/dist/modules/order/sync_engine.js.map +1 -1
  107. package/dist/modules/order/utils/order.d.ts +17 -0
  108. package/dist/modules/order/utils/order.d.ts.map +1 -1
  109. package/dist/modules/order/utils/order.js +81 -24
  110. package/dist/modules/order/utils/order.js.map +1 -1
  111. package/dist/modules/order/utils/system.js +4 -4
  112. package/dist/modules/order/utils/system.js.map +1 -1
  113. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  114. package/dist/modules/order/utils/validate.js +6 -8
  115. package/dist/modules/order/utils/validate.js.map +1 -1
  116. package/dist/modules/types.d.ts +7 -12
  117. package/dist/modules/types.d.ts.map +1 -1
  118. package/dist/scripts/print_grid.js +3 -3
  119. package/dist/scripts/print_grid.js.map +1 -1
  120. package/dist/scripts/test-credit-renewal.js +1 -1
  121. package/dist/scripts/test-credit-renewal.js.map +1 -1
  122. package/dist/unlock.d.ts.map +1 -1
  123. package/dist/unlock.js +26 -12
  124. package/dist/unlock.js.map +1 -1
  125. package/package.json +1 -1
@@ -248,25 +248,28 @@ class DEXBot {
248
248
  */
249
249
  _validateStartupConfig(config) {
250
250
  const errors = [];
251
- // Validate startPrice is numeric or valid string mode
252
- const startPrice = config.startPrice;
253
- const validPriceModes = ['pool', 'book'];
254
- const isPriceNumeric = typeof startPrice === 'number' && Number.isFinite(startPrice) && startPrice > 0;
255
- const isPriceMode = typeof startPrice === 'string' && validPriceModes.includes(startPrice.toLowerCase());
256
- if (!isPriceNumeric && !isPriceMode) {
257
- errors.push(`startPrice must be a positive number or valid mode (${validPriceModes.join('/')}), got: ${startPrice}`);
258
- }
259
- // Validate assetA and assetB are present
260
- if (!config.assetA || typeof config.assetA !== 'string') {
261
- errors.push(`assetA must be a non-empty string, got: ${config.assetA}`);
262
- }
263
- if (!config.assetB || typeof config.assetB !== 'string') {
264
- errors.push(`assetB must be a non-empty string, got: ${config.assetB}`);
265
- }
266
- // Validate incrementPercent
267
- const increment = config.incrementPercent;
268
- if (!Number.isFinite(increment) || increment <= 0 || increment > 100) {
269
- errors.push(`incrementPercent must be between 0 and 100, got: ${increment}`);
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
+ }
270
273
  }
271
274
  // Throw all validation errors at once
272
275
  if (errors.length > 0) {
@@ -346,7 +349,7 @@ class DEXBot {
346
349
  shadowLocks: this.manager?.shadowOrderIds?.size || 0,
347
350
  batchInFlight: this._batchInFlight,
348
351
  recoveryInFlight: this._recoverySyncInFlight,
349
- broadcasting: this.manager?._state?.isBroadcastingActive() || false
352
+ broadcasting: this.manager?.isBroadcastingActive?.() || false
350
353
  };
351
354
  }
352
355
  /**
@@ -375,7 +378,7 @@ class DEXBot {
375
378
  this.manager.logger.log(`Triggering state recovery sync (${reason})...`, 'info');
376
379
  await this.manager.fetchAccountTotals(this.accountId);
377
380
  const openOrders = await chainOrders.readOpenOrders(this.accountId);
378
- await this.manager.syncFromOpenOrders(openOrders, { skipAccounting: true, fillLockAlreadyHeld: true, protectCommittedOrders: true });
381
+ await this.manager.syncFromOpenOrders(openOrders, { skipAccounting: true });
379
382
  if (typeof this.manager.persistGrid === 'function') {
380
383
  await this.manager.persistGrid();
381
384
  }
@@ -587,8 +590,6 @@ class DEXBot {
587
590
  if (chainOpenOrders.length > 0 && this.manager?.syncFromOpenOrders) {
588
591
  await this.manager.syncFromOpenOrders(chainOpenOrders, {
589
592
  skipAccounting: true,
590
- fillLockAlreadyHeld: true,
591
- protectCommittedOrders: true
592
593
  });
593
594
  }
594
595
  // 5. Persist the reconciled state
@@ -819,13 +820,14 @@ class DEXBot {
819
820
  _buildOrphanFillFallbackKey(fill) {
820
821
  return DexbotFillRuntime.buildOrphanFillFallbackKey(this, fill);
821
822
  }
822
- _isNewFillKey(fillKey, processedFillKeys, label = '') {
823
+ _isNewFillKey(fillKey, processedFillKeys, label = '', orderId = '') {
823
824
  const now = Date.now();
824
825
  if (this._recentlyQueuedFills.has(fillKey)) {
825
826
  const lastProcessed = this._recentlyQueuedFills.get(fillKey);
826
827
  if (now - lastProcessed < this._fillDedupeWindowMs) {
827
828
  if (label) {
828
- this.manager?.logger?.log?.(`${label} Skipping duplicate fill (processed ${now - lastProcessed}ms ago)`, 'debug');
829
+ const idSuffix = orderId ? ` for ${orderId}` : '';
830
+ this.manager?.logger?.log?.(`${label} Skipping duplicate fill${idSuffix} (processed ${now - lastProcessed}ms ago)`, 'debug');
829
831
  }
830
832
  return false;
831
833
  }
@@ -953,7 +955,7 @@ class DEXBot {
953
955
  const chainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
954
956
  if (this._shuttingDown)
955
957
  return;
956
- const syncResult = await this.manager.synchronizeWithChain(chainOpenOrders, 'readOpenOrders', { fillLockAlreadyHeld: true });
958
+ const syncResult = await this.manager.synchronizeWithChain(chainOpenOrders, 'readOpenOrders');
957
959
  if (this._shuttingDown)
958
960
  return;
959
961
  if (syncResult?.filledOrders?.length > 0) {
@@ -970,7 +972,6 @@ class DEXBot {
970
972
  await this._cancelDustOrders({
971
973
  buy: reconnectHealth.buyDustOrders,
972
974
  sell: reconnectHealth.sellDustOrders,
973
- fillLockAlreadyHeld: true,
974
975
  });
975
976
  }
976
977
  catch (_dustErr) {
@@ -1050,7 +1051,7 @@ class DEXBot {
1050
1051
  if (!orphanFillKey) {
1051
1052
  orphanFillKey = this._buildOrphanFillFallbackKey(fill);
1052
1053
  }
1053
- if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[POST-RESET]')) {
1054
+ if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[POST-RESET]', fillOp.order_id)) {
1054
1055
  continue;
1055
1056
  }
1056
1057
  this._log(`[POST-RESET] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
@@ -1066,7 +1067,7 @@ class DEXBot {
1066
1067
  }
1067
1068
  this._log(`[POST-RESET] Processing fill for ${gridOrder.type} order ${gridOrder.id} at price ${gridOrder.price}`);
1068
1069
  const trackedFillKey = buildFillKey(fill);
1069
- if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[POST-RESET]')) {
1070
+ if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[POST-RESET]', fillOp.order_id)) {
1070
1071
  continue;
1071
1072
  }
1072
1073
  const accountingResult = await this._applyReplaySafeTrackedFillAccounting(fill, fillOp, {
@@ -1092,7 +1093,7 @@ class DEXBot {
1092
1093
  if (requiresOpenOrdersSync) {
1093
1094
  this._log('[POST-RESET] Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
1094
1095
  const postResetChainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
1095
- const syncResult = await this.manager.syncFromOpenOrders(postResetChainOpenOrders, { fillLockAlreadyHeld: true });
1096
+ const syncResult = await this.manager.syncFromOpenOrders(postResetChainOpenOrders);
1096
1097
  if (syncResult.filledOrders?.length > 0) {
1097
1098
  await this._processFillsWithBatching(syncResult.filledOrders, new Set(), '[POST-RESET] open-orders fallback');
1098
1099
  }
@@ -1123,7 +1124,6 @@ class DEXBot {
1123
1124
  await this._cancelDustOrders({
1124
1125
  buy: postResetHealth.buyDustOrders,
1125
1126
  sell: postResetHealth.sellDustOrders,
1126
- fillLockAlreadyHeld: true,
1127
1127
  });
1128
1128
  }
1129
1129
  catch (_dustErr) {
@@ -1219,7 +1219,7 @@ class DEXBot {
1219
1219
  if (Array.isArray(chainOpenOrders) && chainOpenOrders.length > 0) {
1220
1220
  this._log('Generating new grid and syncing with existing on-chain orders...');
1221
1221
  await Grid.initializeGrid(this.manager);
1222
- await this.manager.syncFromOpenOrders(chainOpenOrders, { skipAccounting: true, fillLockAlreadyHeld: true });
1222
+ await this.manager.syncFromOpenOrders(chainOpenOrders, { skipAccounting: true });
1223
1223
  const rebalanceResult = await reconcileGridOrders({
1224
1224
  manager: this.manager,
1225
1225
  config: this.config,
@@ -1227,7 +1227,6 @@ class DEXBot {
1227
1227
  privateKey: this.privateKey,
1228
1228
  chainOrders,
1229
1229
  chainOpenOrders,
1230
- fillLockAlreadyHeld: true,
1231
1230
  });
1232
1231
  await this._executeBatchIfNeeded(rebalanceResult, 'startup reconcile (regenerated grid)');
1233
1232
  }
@@ -1241,7 +1240,7 @@ class DEXBot {
1241
1240
  this._log('Found active session. Loading and syncing existing grid.');
1242
1241
  await Grid.loadGrid(this.manager, persistedGrid, persistedBoundaryIdx);
1243
1242
  let startupChainOpenOrders = chainOpenOrders;
1244
- const syncResult = await this.manager.syncFromOpenOrders(startupChainOpenOrders, { skipAccounting: true, fillLockAlreadyHeld: true });
1243
+ const syncResult = await this.manager.syncFromOpenOrders(startupChainOpenOrders, { skipAccounting: true });
1245
1244
  // Process price corrections queued during startup sync.
1246
1245
  // These are not picked up by _consumeFillQueue until a fill
1247
1246
  // arrives, which may never come for an idle market.
@@ -1255,7 +1254,7 @@ class DEXBot {
1255
1254
  // Refresh open orders so startup reconcile works with post-batch chain reality
1256
1255
  // and avoids reconciling against a stale pre-batch snapshot.
1257
1256
  startupChainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
1258
- await this.manager.synchronizeWithChain(startupChainOpenOrders, 'readOpenOrders', { fillLockAlreadyHeld: true });
1257
+ await this.manager.synchronizeWithChain(startupChainOpenOrders, 'readOpenOrders');
1259
1258
  }
1260
1259
  }
1261
1260
  const rebalanceResult = await reconcileGridOrders({
@@ -1265,7 +1264,6 @@ class DEXBot {
1265
1264
  privateKey: this.privateKey,
1266
1265
  chainOrders,
1267
1266
  chainOpenOrders: startupChainOpenOrders,
1268
- fillLockAlreadyHeld: true,
1269
1267
  });
1270
1268
  await this._executeBatchIfNeeded(rebalanceResult, 'startup reconcile (loaded grid)');
1271
1269
  // Dust state is no longer persisted — cancelled immediately on detection.
@@ -1281,16 +1279,39 @@ class DEXBot {
1281
1279
  await this._processFillsWithBootstrapMode(chainOrders);
1282
1280
  }
1283
1281
  this.manager.finishBootstrap();
1282
+ // Refresh account totals after bootstrap to eliminate the timing
1283
+ // gap between the initial balance fetch and grid operations (sync,
1284
+ // reconcile, fills). Without this, the first maintenance cycle sees
1285
+ // a fund drift (expected at bootstrap) and triggers an unnecessary
1286
+ // invariant violation + full recovery cycle.
1287
+ // Bound by a timeout to avoid blocking _fillProcessingLock on a
1288
+ // flaky node. If the fetch times out, continue with cached values;
1289
+ // the next periodic maintenance cycle will retry.
1290
+ const FETCH_TIMEOUT_MS = 30000;
1291
+ let _fetchTimeoutHandle;
1292
+ try {
1293
+ await Promise.race([
1294
+ this.manager.fetchAccountTotals(),
1295
+ new Promise((_, reject) => {
1296
+ _fetchTimeoutHandle = setTimeout(() => reject(new Error('timeout')), FETCH_TIMEOUT_MS);
1297
+ })
1298
+ ]);
1299
+ }
1300
+ catch (fetchErr) {
1301
+ this._log(`[STARTUP] [${this.config?.botKey || 'unknown'}] fetchAccountTotals ${fetchErr.message === 'timeout' ? 'timed out' : 'failed'} (${fetchErr.message}). Continuing with cached account totals.`, 'warn');
1302
+ }
1303
+ finally {
1304
+ clearTimeout(_fetchTimeoutHandle);
1305
+ }
1284
1306
  // Perform initial grid maintenance (thresholds, divergence, spread, health)
1285
1307
  // Consolidated into shared logic to ensure consistent behavior at boot and runtime.
1286
1308
  // CRITICAL: Pass lockAlreadyHeld since we're inside _fillProcessingLock.acquire()
1287
- await this._runGridMaintenance('startup', { fillLockAlreadyHeld: true });
1309
+ await this._runGridMaintenance('startup');
1288
1310
  // Cancel any dust from a previous bot lifetime immediately.
1289
1311
  const startupHealth = await this.manager.checkGridHealth(this.updateOrdersOnChainPlan.bind(this));
1290
1312
  await this._cancelDustOrders({
1291
1313
  buy: startupHealth.buyDustOrders,
1292
1314
  sell: startupHealth.sellDustOrders,
1293
- fillLockAlreadyHeld: true,
1294
1315
  });
1295
1316
  this._log('Bootstrap phase complete - fill processing resumed', 'info');
1296
1317
  }
@@ -1302,7 +1323,7 @@ class DEXBot {
1302
1323
  await this._setupTriggerFileDetection();
1303
1324
  await this._setupCreditRuntime();
1304
1325
  await this._refreshAndSyncCreditRuntime();
1305
- await this._runCreditRuntimeMaintenance('startup', { fillLockAlreadyHeld: true });
1326
+ await this._runCreditRuntimeMaintenance('startup');
1306
1327
  this._setupBlockchainFetchInterval();
1307
1328
  this._setupCreditWatchdogInterval();
1308
1329
  this._setupCredentialDaemonWatchdogInterval();
@@ -1346,14 +1367,14 @@ class DEXBot {
1346
1367
  }
1347
1368
  try {
1348
1369
  let openOrders = await chainOrders.readOpenOrders(this.accountId);
1349
- const syncResult = await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders', { fillLockAlreadyHeld: true });
1370
+ const syncResult = await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders');
1350
1371
  let aborted = false;
1351
1372
  if (syncResult?.filledOrders?.length > 0) {
1352
1373
  this._log(`[SYNC-CHAIN] ${syncResult.filledOrders.length} filled order(s) found during ${tag}`, 'info');
1353
1374
  const batchResult = await this._processFillsWithBatching(syncResult.filledOrders, new Set(), `${tag} sync-fill`);
1354
1375
  if (!batchResult?.aborted) {
1355
1376
  openOrders = await chainOrders.readOpenOrders(this.accountId);
1356
- await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders', { fillLockAlreadyHeld: true });
1377
+ await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders');
1357
1378
  }
1358
1379
  else {
1359
1380
  aborted = true;
@@ -1507,11 +1528,11 @@ class DEXBot {
1507
1528
  try {
1508
1529
  // BOOTSTRAP OPTIMIZATION: During bootstrap, prioritize fill processing over grid-wide checks
1509
1530
  // Process fills immediately with side-only rebalancing (no expensive full grid recalculations)
1510
- if (this.manager._state.isBootstrapping()) {
1531
+ if (this.manager.isBootstrapping()) {
1511
1532
  // During bootstrap: skip lock contention checks, process fills directly
1512
1533
  let bootstrapSkipped = false;
1513
1534
  await this.manager._fillProcessingLock.acquire(async () => {
1514
- if (!this.manager._state.isBootstrapping()) {
1535
+ if (!this.manager.isBootstrapping()) {
1515
1536
  // Bootstrap finished while waiting for the lock — no
1516
1537
  // work to do, but the iteration is still healthy.
1517
1538
  bootstrapSkipped = true;
@@ -1601,7 +1622,7 @@ class DEXBot {
1601
1622
  if (!orphanFillKey) {
1602
1623
  orphanFillKey = this._buildOrphanFillFallbackKey(fill);
1603
1624
  }
1604
- if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[ORPHAN-FILL]')) {
1625
+ if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[ORPHAN-FILL]', fillOp.order_id)) {
1605
1626
  continue;
1606
1627
  }
1607
1628
  this.manager.logger.log(`[ORPHAN-FILL] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
@@ -1631,7 +1652,7 @@ class DEXBot {
1631
1652
  requiresOpenOrdersSync = true;
1632
1653
  continue;
1633
1654
  }
1634
- if (!this._isNewFillKey(fillKey, processedFillKeys, '[FILL]')) {
1655
+ if (!this._isNewFillKey(fillKey, processedFillKeys, '[FILL]', fillOp.order_id)) {
1635
1656
  continue;
1636
1657
  }
1637
1658
  validFills.push(fill);
@@ -1694,7 +1715,7 @@ class DEXBot {
1694
1715
  }
1695
1716
  this.manager.logger.log(`Syncing ${fillsToSync.length} fill(s) (open orders mode)`, 'info');
1696
1717
  const chainOpenOrders = await chainOrders.readOpenOrders(this.account);
1697
- const resultOpenOrders = await this.manager.syncFromOpenOrders(chainOpenOrders, { fillLockAlreadyHeld: true });
1718
+ const resultOpenOrders = await this.manager.syncFromOpenOrders(chainOpenOrders);
1698
1719
  // Dust is handled by post-fill detection below.
1699
1720
  if (resultOpenOrders.filledOrders)
1700
1721
  resolvedOrders.push(...resultOpenOrders.filledOrders);
@@ -1827,7 +1848,6 @@ class DEXBot {
1827
1848
  const dustCancelResult = await this._cancelDustOrders({
1828
1849
  buy: healthResult.buyDustOrders,
1829
1850
  sell: healthResult.sellDustOrders,
1830
- fillLockAlreadyHeld: true,
1831
1851
  });
1832
1852
  if (dustCancelResult?.batchResult?.aborted) {
1833
1853
  abortedFillCycle = true;
@@ -1841,7 +1861,7 @@ class DEXBot {
1841
1861
  // After: Grid maintenance waits for isPipelineEmpty() before structural changes
1842
1862
  // Run only when the cycle contains at least one full fill.
1843
1863
  if (shouldRunPostFillChecks && !abortedFillCycle) {
1844
- await this._runGridMaintenance('post-fill', { fillLockAlreadyHeld: true });
1864
+ await this._runGridMaintenance('post-fill');
1845
1865
  }
1846
1866
  }
1847
1867
  else if (pendingFillKeysForCurrentCycle.size > 0) {
@@ -1966,7 +1986,7 @@ class DEXBot {
1966
1986
  if (!orphanFillKey) {
1967
1987
  orphanFillKey = this._buildOrphanFillFallbackKey(fill);
1968
1988
  }
1969
- if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[BOOTSTRAP]')) {
1989
+ if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[BOOTSTRAP]', fillOp.order_id)) {
1970
1990
  continue;
1971
1991
  }
1972
1992
  this.manager.logger.log(`[BOOTSTRAP] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
@@ -1979,7 +1999,7 @@ class DEXBot {
1979
1999
  continue;
1980
2000
  }
1981
2001
  const trackedFillKey = buildFillKey(fill);
1982
- if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[BOOTSTRAP]')) {
2002
+ if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[BOOTSTRAP]', fillOp.order_id)) {
1983
2003
  continue;
1984
2004
  }
1985
2005
  const accountingResult = await this._applyReplaySafeTrackedFillAccounting(fill, fillOp, {
@@ -2000,7 +2020,7 @@ class DEXBot {
2000
2020
  if (requiresOpenOrdersSync) {
2001
2021
  this._log('[BOOTSTRAP] Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
2002
2022
  const bootstrapChainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
2003
- const syncResult = await this.manager.syncFromOpenOrders(bootstrapChainOpenOrders, { fillLockAlreadyHeld: true });
2023
+ const syncResult = await this.manager.syncFromOpenOrders(bootstrapChainOpenOrders);
2004
2024
  if (syncResult.filledOrders?.length > 0) {
2005
2025
  const queuedOrderIds = new Set(validFills.map(fill => fill?.gridOrder?.orderId).filter(Boolean));
2006
2026
  for (const filledOrder of syncResult.filledOrders) {
@@ -2238,8 +2258,6 @@ class DEXBot {
2238
2258
  const openOrders = await chainOrders.readOpenOrders(accountRef);
2239
2259
  const recoveryResult = await this.manager.syncFromOpenOrders(openOrders, {
2240
2260
  skipAccounting: false,
2241
- fillLockAlreadyHeld: true,
2242
- protectCommittedOrders: true
2243
2261
  });
2244
2262
  this._preserveMissingCreateBlockersAfterRecovery(preRecoveryMissingCreateBlockers, recoveryResult);
2245
2263
  // Persist any master grid mutations from the recovery sync. The
@@ -2583,13 +2601,10 @@ class DEXBot {
2583
2601
  * @returns {Promise<Object>} Result object compatible with batch return shape
2584
2602
  */
2585
2603
  async _reconcileAfterUncertainBroadcast(err, opContexts, options = {}) {
2586
- if (options.fillLockAlreadyHeld !== true &&
2587
- this.manager?._fillProcessingLock &&
2588
- typeof this.manager._fillProcessingLock.acquire === 'function') {
2589
- return this.manager._fillProcessingLock.acquire(async () => (this._reconcileAfterUncertainBroadcast(err, opContexts, {
2590
- ...options,
2591
- fillLockAlreadyHeld: true
2592
- })));
2604
+ if (this.manager?._fillProcessingLock &&
2605
+ typeof this.manager._fillProcessingLock.acquire === 'function' &&
2606
+ !this.manager._fillProcessingLock.isReentrant()) {
2607
+ return this.manager._fillProcessingLock.acquire(() => this._reconcileAfterUncertainBroadcast(err, opContexts, options));
2593
2608
  }
2594
2609
  return this._reconcileAfterUncertainBroadcastImpl(err, opContexts, options);
2595
2610
  }
@@ -2637,7 +2652,11 @@ class DEXBot {
2637
2652
  fingerprint: entry.fingerprint
2638
2653
  });
2639
2654
  if (match) {
2640
- adopted.push({ slotId: entry.slotId, chainOrderId: match.id });
2655
+ adopted.push({
2656
+ slotId: entry.slotId,
2657
+ chainOrderId: match.id,
2658
+ orderType: entry.orderType || entry.order?.type,
2659
+ });
2641
2660
  this.manager._pendingBroadcasts.delete(entry.fingerprint);
2642
2661
  }
2643
2662
  else {
@@ -2674,7 +2693,11 @@ class DEXBot {
2674
2693
  fingerprint: entry.fingerprint
2675
2694
  });
2676
2695
  if (match) {
2677
- adopted.push({ slotId: entry.slotId, chainOrderId: match.id });
2696
+ adopted.push({
2697
+ slotId: entry.slotId,
2698
+ chainOrderId: match.id,
2699
+ orderType: entry.orderType || entry.order?.type,
2700
+ });
2678
2701
  this.manager.logger.log(`[COW][UNCERTAIN] CREATE re-adopted after ${recheckRound} block(s): ` +
2679
2702
  `${entry.slotId}->${match.id}`, 'info');
2680
2703
  }
@@ -2702,8 +2725,6 @@ class DEXBot {
2702
2725
  try {
2703
2726
  await this.manager.syncFromOpenOrders(latestSnapshot, {
2704
2727
  skipAccounting: true,
2705
- fillLockAlreadyHeld: true,
2706
- protectCommittedOrders: true
2707
2728
  });
2708
2729
  hadRotation = true;
2709
2730
  }
@@ -2758,9 +2779,52 @@ class DEXBot {
2758
2779
  catch (orphanErr) {
2759
2780
  this.manager.logger.log(`[COW][UNCERTAIN] Auto-cancel pass failed: ${orphanErr?.message || orphanErr}`, 'warn');
2760
2781
  }
2782
+ // Boundary shift recovery: the COW batch that should have committed the
2783
+ // boundary shift from this fill cycle failed before commit. Only COUNT
2784
+ // ADOPTED CREATEs — orders that actually landed on-chain. Discarded
2785
+ // CREATEs represent orders that never existed, so the grid must NOT
2786
+ // shift the boundary for them. The next fill cycle's
2787
+ // calculateTargetGrid → deriveTargetBoundary will recompute the correct
2788
+ // boundary from scratch for any discarded slots.
2789
+ //
2790
+ // NOTE: Direct mutation of manager.boundaryIdx outside a COW commit
2791
+ // violates the invariant stated in grid.ts:1217-1220 (boundary must only
2792
+ // be updated atomically inside _commitWorkingGrid). This is intentional
2793
+ // here because the COW commit already failed — the invariant was already
2794
+ // broken by the broadcast uncertainty, and the adjustment only restores
2795
+ // the state to what the successful commit would have produced for
2796
+ // orders that are confirmed on-chain.
2797
+ if (hadRotation && this.manager && adopted.length > 0) {
2798
+ let boundaryShift = 0;
2799
+ for (const entry of adopted) {
2800
+ if (entry.orderType === ORDER_TYPES.SELL) {
2801
+ boundaryShift--; // SELL CREATE → fill was BUY → boundary LEFT
2802
+ }
2803
+ else if (entry.orderType === ORDER_TYPES.BUY) {
2804
+ boundaryShift++; // BUY CREATE → fill was SELL → boundary RIGHT
2805
+ }
2806
+ }
2807
+ if (boundaryShift !== 0) {
2808
+ const oldIdx = this.manager.boundaryIdx;
2809
+ if (typeof oldIdx === 'number') {
2810
+ const maxIdx = Math.max(0, (this.manager.orders?.size || 1) - 1);
2811
+ const newIdx = Math.max(0, Math.min(maxIdx, oldIdx + boundaryShift));
2812
+ if (newIdx !== oldIdx) {
2813
+ this.manager.boundaryIdx = newIdx;
2814
+ this.manager.logger.log(`[COW][UNCERTAIN] Boundary adjusted by ${boundaryShift} ` +
2815
+ `(${adopted.length} adopted CREATE(s), ${discarded.length} discarded): ` +
2816
+ `${oldIdx} → ${newIdx}. Next COW cycle will generate replacement opposite-side orders.`, 'warn');
2817
+ if (typeof this.manager._markGridDirty === 'function') {
2818
+ this.manager._markGridDirty();
2819
+ }
2820
+ }
2821
+ }
2822
+ }
2823
+ }
2761
2824
  // Persist master grid mutations from the reconciliation sync and any
2762
2825
  // orphan auto-cancellation that ran above. These apply outside the COW
2763
- // broadcast path and would otherwise be in-memory only.
2826
+ // broadcast path and would otherwise be in-memory only. The boundary
2827
+ // adjustment above is also persisted here.
2764
2828
  if (typeof this.manager?.persistGrid === 'function') {
2765
2829
  await this.manager.persistGrid();
2766
2830
  }
@@ -2898,8 +2962,6 @@ class DEXBot {
2898
2962
  if (freshChain.length > 0 && this.manager?.syncFromOpenOrders) {
2899
2963
  await this.manager.syncFromOpenOrders(freshChain, {
2900
2964
  skipAccounting: true,
2901
- fillLockAlreadyHeld: true,
2902
- protectCommittedOrders: true
2903
2965
  });
2904
2966
  }
2905
2967
  }
@@ -3294,7 +3356,7 @@ class DEXBot {
3294
3356
  if (this._credentialRecoveryInFlight || !this._credentialRecoveryNeeded || this._shuttingDown) {
3295
3357
  return;
3296
3358
  }
3297
- if (this.manager?._state?.isBootstrapping?.() || this.manager?._state?.isBroadcastingActive?.()) {
3359
+ if (this.manager?.isBootstrapping?.() || this.manager?.isBroadcastingActive?.()) {
3298
3360
  if (!this._credentialRecoveryDeferredTimer) {
3299
3361
  this.manager?.logger?.log?.('[CREDENTIAL] Deferring credential recovery until startup/broadcast activity is idle.', 'info');
3300
3362
  this._credentialRecoveryDeferredTimer = setTimeout(() => {
@@ -3316,7 +3378,7 @@ class DEXBot {
3316
3378
  this._resumeGridPersistenceAfterCredentialRecovery('credential recovery started');
3317
3379
  const runRecovery = async () => {
3318
3380
  await this._triggerStateRecoverySync('credential daemon restored');
3319
- await this._runGridMaintenance('credential-recovery', { fillLockAlreadyHeld: true });
3381
+ await this._runGridMaintenance('credential-recovery');
3320
3382
  };
3321
3383
  if (this.manager?._fillProcessingLock) {
3322
3384
  await this.manager._fillProcessingLock.acquire(runRecovery);
@@ -3773,7 +3835,7 @@ class DEXBot {
3773
3835
  batchId: this._currentBatchId || null,
3774
3836
  payload: null,
3775
3837
  timeoutMs: null
3776
- }), [], { fillLockAlreadyHeld: true });
3838
+ }), []);
3777
3839
  }
3778
3840
  catch (recoverErr) {
3779
3841
  this.manager.logger.log(`[COW] Recovery from pending broadcasts failed: ${recoverErr?.message || recoverErr}`, 'error');
@@ -4133,7 +4195,7 @@ class DEXBot {
4133
4195
  // throwing would re-enter the catch loop on the next attempt and
4134
4196
  // potentially double-publish.
4135
4197
  if (err instanceof BroadcastUncertainError) {
4136
- return await this._reconcileAfterUncertainBroadcast(err, opContexts, { fillLockAlreadyHeld: true });
4198
+ return await this._reconcileAfterUncertainBroadcast(err, opContexts);
4137
4199
  }
4138
4200
  // Handle hard abort
4139
4201
  const hardAbortResult = await this._handleBatchHardAbort(err, 'COW batch processing', operations.length);
@@ -4439,6 +4501,10 @@ class DEXBot {
4439
4501
  */
4440
4502
  async _runStartupSequence() {
4441
4503
  try {
4504
+ if (this.config.creditOnly) {
4505
+ await this._runCreditOnlyStartup();
4506
+ return;
4507
+ }
4442
4508
  const startupState = await this._initializeStartupState();
4443
4509
  await this._finishStartupSequence(startupState);
4444
4510
  }
@@ -4448,13 +4514,20 @@ class DEXBot {
4448
4514
  throw err;
4449
4515
  }
4450
4516
  }
4517
+ async _runCreditOnlyStartup() {
4518
+ await this._setupCreditRuntime();
4519
+ await this._refreshAndSyncCreditRuntime();
4520
+ await this._runCreditRuntimeMaintenance('startup');
4521
+ this._setupCreditWatchdogInterval();
4522
+ this._setupCredentialDaemonWatchdogInterval();
4523
+ this._log('DEXBot started (credit-only mode)');
4524
+ }
4451
4525
  /**
4452
4526
  * Perform periodic grid checks: fund thresholds, spread condition, grid health.
4453
4527
  * Called by the periodic blockchain fetch interval to check if grid needs updates.
4454
4528
  *
4455
4529
  * IMPORTANT: This method MUST only be called from within _fillProcessingLock.acquire()
4456
- * (specifically from _setupBlockchainFetchInterval). It passes fillLockAlreadyHeld
4457
- * to avoid deadlock with _consumeFillQueue which uses the same lock ordering.
4530
+ * (specifically from _setupBlockchainFetchInterval).
4458
4531
  *
4459
4532
  * @private
4460
4533
  */
@@ -4601,14 +4674,17 @@ class DEXBot {
4601
4674
  return { skipped: true, reason: 'grid resync unavailable' };
4602
4675
  }
4603
4676
  const message = reason ? `[CR-RESET] ${reason}` : '[CR-RESET] grid reset requested';
4604
- // Manual/programmatic resets should advance the persisted center baseline
4605
- // before rebuilding the grid, unless a caller explicitly disables it.
4606
4677
  this._log(`${message}; rebuilding grid from fresh on-chain state`, 'info');
4607
4678
  const resetOptions = {
4608
4679
  ...options,
4609
4680
  refreshCenterPrice: options.refreshCenterPrice !== false,
4610
4681
  };
4611
- if (options.fillLockAlreadyHeld || !this.manager._fillProcessingLock) {
4682
+ // Skip acquire if no lock exists (no fill processing to serialize against)
4683
+ // or if the caller already holds the lock (re-entrant). In the latter case
4684
+ // the lock's isReentrant() check prevents queueing and runs inline.
4685
+ // Otherwise, wait on the queue — this is the intended path for callers
4686
+ // outside the fill-processing context that need exclusive access.
4687
+ if (!this.manager._fillProcessingLock || this.manager._fillProcessingLock.isReentrant()) {
4612
4688
  return this._performGridResync(resetOptions);
4613
4689
  }
4614
4690
  return this.manager._fillProcessingLock.acquire(async () => this._performGridResync(resetOptions));
@@ -4720,12 +4796,12 @@ class DEXBot {
4720
4796
  * Cancel dust partial orders immediately — no maps, no timers, no delay.
4721
4797
  * Each dust order is cancelled on chain and its slot rotated through the
4722
4798
  * synthetic-fill pipeline.
4723
- * @param {{ buy: Array, sell: Array, fillLockAlreadyHeld?: boolean }} options
4799
+ * @param {{ buy: Array, sell: Array }} options
4724
4800
  * @returns {Promise<{cancelledCount: number, batchResult: {aborted: boolean}|null}>}
4725
4801
  * @private
4726
4802
  */
4727
- async _cancelDustOrders({ buy: buyDust = [], sell: sellDust = [], fillLockAlreadyHeld = false } = {}) {
4728
- return DexbotMaintenanceRuntime.cancelDustOrders(this, { buy: buyDust, sell: sellDust, fillLockAlreadyHeld });
4803
+ async _cancelDustOrders({ buy: buyDust = [], sell: sellDust = [] } = {}) {
4804
+ return DexbotMaintenanceRuntime.cancelDustOrders(this, { buy: buyDust, sell: sellDust });
4729
4805
  }
4730
4806
  /**
4731
4807
  * Set up the periodic dust health check interval.
@@ -4750,7 +4826,6 @@ class DEXBot {
4750
4826
  await this._cancelDustOrders({
4751
4827
  buy: health.buyDustOrders,
4752
4828
  sell: health.sellDustOrders,
4753
- fillLockAlreadyHeld: true,
4754
4829
  });
4755
4830
  });
4756
4831
  }
@@ -4776,7 +4851,6 @@ class DEXBot {
4776
4851
  *
4777
4852
  * LOCK ORDERING:
4778
4853
  * - Canonical order: _fillProcessingLock → _divergenceLock
4779
- * - This function handles lock acquisition based on fillLockAlreadyHeld parameter
4780
4854
  * - When called from post-fill context, fill lock is already held
4781
4855
  * - When called from periodic context, both locks must be acquired
4782
4856
  * - Matches the order used in _consumeFillQueue to prevent deadlocks