dexbot 1.3.2 → 1.3.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.
Files changed (71) hide show
  1. package/dist/credential-daemon.d.ts.map +1 -1
  2. package/dist/credential-daemon.js +53 -13
  3. package/dist/credential-daemon.js.map +1 -1
  4. package/dist/modules/bitshares-native/serial/types.js +1 -1
  5. package/dist/modules/bitshares-native/serial/types.js.map +1 -1
  6. package/dist/modules/chain_keys.js +1 -1
  7. package/dist/modules/chain_keys.js.map +1 -1
  8. package/dist/modules/constants.d.ts +2 -0
  9. package/dist/modules/constants.d.ts.map +1 -1
  10. package/dist/modules/constants.js +10 -0
  11. package/dist/modules/constants.js.map +1 -1
  12. package/dist/modules/credential_policy.js +1 -1
  13. package/dist/modules/credential_policy.js.map +1 -1
  14. package/dist/modules/dexbot_class.d.ts +45 -175
  15. package/dist/modules/dexbot_class.d.ts.map +1 -1
  16. package/dist/modules/dexbot_class.js +84 -3496
  17. package/dist/modules/dexbot_class.js.map +1 -1
  18. package/dist/modules/dexbot_cow_runtime.d.ts +319 -0
  19. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -0
  20. package/dist/modules/dexbot_cow_runtime.js +1764 -0
  21. package/dist/modules/dexbot_cow_runtime.js.map +1 -0
  22. package/dist/modules/dexbot_fill_runtime.d.ts +47 -0
  23. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  24. package/dist/modules/dexbot_fill_runtime.js +607 -3
  25. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  26. package/dist/modules/dexbot_maintenance_runtime.d.ts +69 -0
  27. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  28. package/dist/modules/dexbot_maintenance_runtime.js +244 -15
  29. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  30. package/dist/modules/dexbot_startup_runtime.d.ts +30 -0
  31. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -0
  32. package/dist/modules/dexbot_startup_runtime.js +539 -0
  33. package/dist/modules/dexbot_startup_runtime.js.map +1 -0
  34. package/dist/modules/dexbot_state_recovery.d.ts +128 -0
  35. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -0
  36. package/dist/modules/dexbot_state_recovery.js +397 -0
  37. package/dist/modules/dexbot_state_recovery.js.map +1 -0
  38. package/dist/modules/fund_registry.d.ts.map +1 -1
  39. package/dist/modules/fund_registry.js +0 -1
  40. package/dist/modules/fund_registry.js.map +1 -1
  41. package/dist/modules/general_settings.d.ts.map +1 -1
  42. package/dist/modules/general_settings.js +0 -1
  43. package/dist/modules/general_settings.js.map +1 -1
  44. package/dist/modules/key_store.d.ts.map +1 -1
  45. package/dist/modules/key_store.js +1 -2
  46. package/dist/modules/key_store.js.map +1 -1
  47. package/dist/modules/launcher/market_adapter_runtime.d.ts.map +1 -1
  48. package/dist/modules/launcher/market_adapter_runtime.js +0 -1
  49. package/dist/modules/launcher/market_adapter_runtime.js.map +1 -1
  50. package/dist/modules/launcher/market_adapter_watchdog.d.ts.map +1 -1
  51. package/dist/modules/launcher/market_adapter_watchdog.js +1 -2
  52. package/dist/modules/launcher/market_adapter_watchdog.js.map +1 -1
  53. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  54. package/dist/modules/order/grid_reconcile.js +0 -1
  55. package/dist/modules/order/grid_reconcile.js.map +1 -1
  56. package/dist/modules/order/grid_reconcile_internal.js +2 -2
  57. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  58. package/dist/modules/order/utils/math.js +1 -1
  59. package/dist/modules/order/utils/math.js.map +1 -1
  60. package/dist/modules/order/utils/system.js +1 -1
  61. package/dist/modules/order/utils/system.js.map +1 -1
  62. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  63. package/dist/modules/order/utils/validate.js +0 -1
  64. package/dist/modules/order/utils/validate.js.map +1 -1
  65. package/dist/modules/paths.d.ts +2 -0
  66. package/dist/modules/paths.d.ts.map +1 -1
  67. package/dist/modules/paths.js +13 -2
  68. package/dist/modules/paths.js.map +1 -1
  69. package/dist/scripts/verify-browser-bundle.js +25 -1
  70. package/dist/scripts/verify-browser-bundle.js.map +1 -1
  71. package/package.json +7 -2
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  /** Fill processing runtime - handles order fill events and replay-safe accounting */
3
- const { buildFillKey } = require('./order/utils/order');
4
3
  const { PROCESSED_FILL_PERSISTENCE_MODES } = require('./order/processed_fill_store');
5
- const { NATIVE_CLIENT } = require('./constants');
4
+ function buildFillKey(...args) { return require('./order/utils/order').buildFillKey(...args); }
5
+ function correctAllPriceMismatches(...args) { return require('./order/utils/order').correctAllPriceMismatches(...args); }
6
+ function retryPersistenceIfNeeded(...args) { return require('./order/utils/system').retryPersistenceIfNeeded(...args); }
7
+ const { NATIVE_CLIENT, FILL_PROCESSING, TIMING, MAINTENANCE, ORDER_TYPES } = require('./constants');
6
8
  /**
7
9
  * Wire processed fill tracking into the manager and processed fill store.
8
10
  * Establishes the bidirectional link between the runtime's processed fill store
@@ -206,6 +208,603 @@ function createFillCallback(bot, chainOrders) {
206
208
  }
207
209
  };
208
210
  }
211
+ /**
212
+ * Returns the maximum consecutive fill consumer failures allowed before backoff.
213
+ * @param {import('./dexbot_class').DEXBot} bot
214
+ * @returns {number}
215
+ */
216
+ function maxConsecutiveFillConsumerFailures(bot) {
217
+ return bot.config.fillProcessing?.MAX_CONSECUTIVE_CONSUMER_FAILURES ?? FILL_PROCESSING.MAX_CONSECUTIVE_CONSUMER_FAILURES;
218
+ }
219
+ /**
220
+ * Compute the backoff delay for fill-consumer retries after the failure
221
+ * budget (MAX_CONSECUTIVE_CONSUMER_FAILURES) is exhausted. Each retry
222
+ * doubles the previous delay, capped at CONSUMER_BACKOFF_MAX_MS. The
223
+ * consumer NEVER permanently stops re-scheduling — it just slows down.
224
+ * @param {import('./dexbot_class').DEXBot} bot
225
+ * @param {number} failures The current consecutive-failure count.
226
+ * @returns {number} Delay in milliseconds before the next retry.
227
+ */
228
+ function computeFillConsumerBackoffMs(bot, failures) {
229
+ const fp = bot.config.fillProcessing || FILL_PROCESSING;
230
+ const initial = fp.CONSUMER_BACKOFF_INITIAL_MS;
231
+ const max = fp.CONSUMER_BACKOFF_MAX_MS;
232
+ const stepAfterMax = Math.max(0, failures - maxConsecutiveFillConsumerFailures(bot));
233
+ return Math.min(max, initial * Math.pow(2, stepAfterMax));
234
+ }
235
+ /**
236
+ * Schedule a fill consumer restart with exponential backoff when the
237
+ * failure budget is exhausted, or immediate retry via setImmediate when
238
+ * within the budget. The consumer NEVER permanently stops re-scheduling.
239
+ * @param {import('./dexbot_class').DEXBot} bot
240
+ * @param {Object} chainOrders - Chain orders module
241
+ */
242
+ function scheduleFillConsumerRestart(bot, chainOrders) {
243
+ const failures = bot._consecutiveConsumeFailures;
244
+ if (failures >= maxConsecutiveFillConsumerFailures(bot)) {
245
+ const backoffMs = computeFillConsumerBackoffMs(bot, failures);
246
+ const elapsedSec = bot._consumeFailureFirstAt
247
+ ? Math.round((Date.now() - bot._consumeFailureFirstAt) / TIMING.MILLISECONDS_PER_SECOND)
248
+ : null;
249
+ const elapsed = elapsedSec !== null ? `${elapsedSec}s` : 'unknown';
250
+ const sustainedLevel = (failures >= 20 || (elapsedSec !== null && elapsedSec >= 900))
251
+ ? 'critical'
252
+ : (failures >= 10 || (elapsedSec !== null && elapsedSec >= 300))
253
+ ? 'error'
254
+ : 'warn';
255
+ bot._log(`[FILL-QUEUE] Fill consumer has failed ${failures} consecutive times over ${elapsed}; ` +
256
+ `backing off ${Math.round(backoffMs / TIMING.MILLISECONDS_PER_SECOND)}s before retry. ` +
257
+ `Queue: ${bot._incomingFillQueue.length} fills.`, sustainedLevel);
258
+ setTimeout(() => {
259
+ if (bot._shuttingDown)
260
+ return;
261
+ bot._consumeFillQueue(chainOrders).catch(err => {
262
+ if (!bot._consumeFailureFirstAt) {
263
+ bot._consumeFailureFirstAt = Date.now();
264
+ }
265
+ bot._consecutiveConsumeFailures++;
266
+ const newFailures = bot._consecutiveConsumeFailures;
267
+ const newElapsedSec = bot._consumeFailureFirstAt
268
+ ? Math.round((Date.now() - bot._consumeFailureFirstAt) / TIMING.MILLISECONDS_PER_SECOND)
269
+ : null;
270
+ const resumeLevel = (newFailures >= 20 || (newElapsedSec !== null && newElapsedSec >= 900))
271
+ ? 'critical'
272
+ : (newFailures >= 10 || (newElapsedSec !== null && newElapsedSec >= 300))
273
+ ? 'error'
274
+ : 'warn';
275
+ bot._log(`Fill consumer resume after backoff failed ` +
276
+ `(${newFailures} total, ` +
277
+ `next backoff ${Math.round(computeFillConsumerBackoffMs(bot, newFailures) / TIMING.MILLISECONDS_PER_SECOND)}s): ` +
278
+ `${err.message}`, resumeLevel);
279
+ bot._scheduleFillConsumerRestart(chainOrders);
280
+ });
281
+ }, backoffMs);
282
+ return;
283
+ }
284
+ setImmediate(() => bot._consumeFillQueue(chainOrders).catch(err => {
285
+ if (!bot._consumeFailureFirstAt) {
286
+ bot._consumeFailureFirstAt = Date.now();
287
+ }
288
+ bot._consecutiveConsumeFailures++;
289
+ const remaining = maxConsecutiveFillConsumerFailures(bot) - bot._consecutiveConsumeFailures;
290
+ bot._log(`Fill consumer failed (${bot._consecutiveConsumeFailures}/${maxConsecutiveFillConsumerFailures(bot)}, ` +
291
+ `${remaining} attempts remaining): ${err.message}`, bot._consecutiveConsumeFailures >= 3 ? 'warn' : 'error');
292
+ }));
293
+ }
294
+ /**
295
+ * Process fills during bootstrap phase using the standard fill pipeline.
296
+ * Delegates to the same fill pipeline as the post-reset path.
297
+ * @param {import('./dexbot_class').DEXBot} bot
298
+ * @param {Object} chainOrders - Chain orders module for blockchain operations
299
+ * @returns {Promise<void>}
300
+ */
301
+ async function processFillsWithBootstrapMode(bot, chainOrders) {
302
+ if (bot._incomingFillQueue.length === 0)
303
+ return;
304
+ const startTime = Date.now();
305
+ const fills = bot._incomingFillQueue.splice(0);
306
+ const validFills = [];
307
+ const processedFillKeys = new Set();
308
+ let requiresOpenOrdersSync = false;
309
+ for (const fill of fills) {
310
+ if (!fill || fill.op?.[0] !== 4)
311
+ continue;
312
+ const fillOp = fill.op[1];
313
+ const gridOrder = bot.manager.orders.get(fillOp.order_id) ||
314
+ Array.from(bot.manager.orders.values()).find((o) => o.orderId === fillOp.order_id);
315
+ if (!gridOrder) {
316
+ let orphanFillKey = buildFillKey(fill);
317
+ if (!orphanFillKey) {
318
+ orphanFillKey = bot._buildOrphanFillFallbackKey(fill);
319
+ }
320
+ if (orphanFillKey && !bot._isNewFillKey(orphanFillKey, processedFillKeys, '[BOOTSTRAP]', fillOp.order_id)) {
321
+ continue;
322
+ }
323
+ bot.manager.logger.log(`[BOOTSTRAP] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
324
+ const accountingResult = await bot._applyReplaySafeOrphanFillAccounting(fill, fillOp, {
325
+ context: 'BOOTSTRAP'
326
+ });
327
+ if (accountingResult.status === 'missing_key') {
328
+ requiresOpenOrdersSync = true;
329
+ }
330
+ continue;
331
+ }
332
+ const trackedFillKey = buildFillKey(fill);
333
+ if (trackedFillKey && !bot._isNewFillKey(trackedFillKey, processedFillKeys, '[BOOTSTRAP]', fillOp.order_id)) {
334
+ continue;
335
+ }
336
+ bot.manager.lockOrders([gridOrder.id]);
337
+ try {
338
+ const accountingResult = await bot._applyReplaySafeTrackedFillAccounting(fill, fillOp, {
339
+ context: 'BOOTSTRAP',
340
+ replayMessage: (op) => `[BOOTSTRAP] Replay detected for ${op.order_id}; skipping duplicate bootstrap rebalance`
341
+ });
342
+ if (accountingResult.status === 'missing_key') {
343
+ requiresOpenOrdersSync = true;
344
+ continue;
345
+ }
346
+ if (accountingResult.status !== 'applied') {
347
+ continue;
348
+ }
349
+ validFills.push({ ...fill, gridOrder });
350
+ const fillType = gridOrder.type === ORDER_TYPES.BUY ? 'BUY' : 'SELL';
351
+ bot._log(`[BOOTSTRAP] Fill detected: ${fillType} order (${fillOp.is_maker !== false ? 'maker' : 'taker'})`);
352
+ }
353
+ finally {
354
+ bot.manager.unlockOrders([gridOrder.id]);
355
+ }
356
+ }
357
+ if (requiresOpenOrdersSync) {
358
+ bot._log('[BOOTSTRAP] Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
359
+ const bootstrapChainOpenOrders = await chainOrders.readOpenOrders(bot.accountId);
360
+ const syncResult = await bot.manager.syncFromOpenOrders(bootstrapChainOpenOrders);
361
+ if (syncResult.filledOrders?.length > 0) {
362
+ const queuedOrderIds = new Set(validFills.map(fill => fill?.gridOrder?.orderId).filter(Boolean));
363
+ for (const filledOrder of syncResult.filledOrders) {
364
+ if (!filledOrder?.orderId || queuedOrderIds.has(filledOrder.orderId))
365
+ continue;
366
+ validFills.push({ gridOrder: filledOrder });
367
+ queuedOrderIds.add(filledOrder.orderId);
368
+ }
369
+ }
370
+ }
371
+ await bot._flushProcessedFillPersistence('bootstrap-batch');
372
+ if (validFills.length === 0)
373
+ return;
374
+ try {
375
+ bot._log(`[BOOTSTRAP] Processing ${validFills.length} fill(s) through standard pipeline`, 'info');
376
+ const filledOrders = validFills.map(f => f.gridOrder);
377
+ const result = await bot._processFillsWithBatching(filledOrders, new Set(), '[BOOTSTRAP] fill processing');
378
+ if (result.aborted) {
379
+ bot._warn('[BOOTSTRAP] Aborted batch due to illegal state; skipping grid persistence this cycle');
380
+ }
381
+ bot._metrics.fillsProcessed += validFills.length;
382
+ bot._metrics.fillProcessingTimeMs += Date.now() - startTime;
383
+ }
384
+ catch (err) {
385
+ bot._warn(`[BOOTSTRAP] Error processing fills: ${err.message}`);
386
+ bot.manager.logger.log(`[BOOTSTRAP] Fill error: ${err.message}`, 'error');
387
+ }
388
+ }
389
+ /**
390
+ * Consume queued fills from incomingFillQueue and rebalance.
391
+ * Deduplicates fills against already-processed set (replay-safe), syncs filled
392
+ * orders from history or open orders mode, handles price mismatches, processes
393
+ * fills sequentially with interruptible rebalancing, and periodically cleans old
394
+ * fill records.
395
+ * @param {import('./dexbot_class').DEXBot} bot
396
+ * @param {Object} chainOrders - Chain orders module for blockchain operations
397
+ */
398
+ async function consumeFillQueue(bot, chainOrders) {
399
+ const resetFailureWatchdogIfSet = () => {
400
+ if (bot._consecutiveConsumeFailures > 0 || bot._consumeFailureFirstAt > 0) {
401
+ bot._consecutiveConsumeFailures = 0;
402
+ bot._consumeFailureFirstAt = 0;
403
+ }
404
+ };
405
+ if (bot._incomingFillQueue.length === 0) {
406
+ resetFailureWatchdogIfSet();
407
+ return;
408
+ }
409
+ if (bot._shuttingDown) {
410
+ bot._warn('Fill processing skipped: shutdown in progress');
411
+ resetFailureWatchdogIfSet();
412
+ return;
413
+ }
414
+ if (bot._batchInFlight || bot._recoverySyncInFlight) {
415
+ bot.manager?.logger?.log?.(`Fill processing deferred: order pipeline active (${bot._incomingFillQueue.length} queued)`, 'debug');
416
+ resetFailureWatchdogIfSet();
417
+ return;
418
+ }
419
+ let pendingFillKeysForCurrentCycle = new Set();
420
+ try {
421
+ if (bot.manager.isBootstrapping()) {
422
+ let bootstrapSkipped = false;
423
+ await bot.manager._fillProcessingLock.acquire(async () => {
424
+ if (!bot.manager.isBootstrapping()) {
425
+ bootstrapSkipped = true;
426
+ return;
427
+ }
428
+ await bot._processFillsWithBootstrapMode(chainOrders);
429
+ });
430
+ if (bootstrapSkipped) {
431
+ resetFailureWatchdogIfSet();
432
+ }
433
+ return;
434
+ }
435
+ if (bot.manager._fillProcessingLock.getQueueLength() > 0) {
436
+ bot._metrics.lockContentionEvents++;
437
+ resetFailureWatchdogIfSet();
438
+ return;
439
+ }
440
+ await bot.manager._fillProcessingLock.acquire(async () => {
441
+ bot.manager._orphanFillsCreditedAt = null;
442
+ while (bot._incomingFillQueue.length > 0) {
443
+ const batchStartTime = Date.now();
444
+ bot._metrics.maxQueueDepth = Math.max(bot._metrics.maxQueueDepth, bot._incomingFillQueue.length);
445
+ const allFills = bot._incomingFillQueue.splice(0);
446
+ const validFills = [];
447
+ const processedFillKeys = new Set();
448
+ pendingFillKeysForCurrentCycle = new Set();
449
+ let requiresOpenOrdersSync = false;
450
+ for (const fill of allFills) {
451
+ if (fill && fill.op && fill.op[0] === FILL_PROCESSING.OPERATION_TYPE) {
452
+ const fillOp = fill.op[1];
453
+ const hasFillEconomics = fillOp?.pays?.asset_id && fillOp?.pays?.amount != null
454
+ && fillOp?.receives?.asset_id && fillOp?.receives?.amount != null;
455
+ if (chainOrders && typeof chainOrders.wasRecentlyOwnCancelled === 'function'
456
+ && chainOrders.wasRecentlyOwnCancelled(fillOp.order_id)
457
+ && !hasFillEconomics) {
458
+ bot.manager.logger.log(`[SELF-CANCEL] Skipping non-economic fill artifact for order ${fillOp.order_id} (just cancelled by this bot)`, 'debug');
459
+ continue;
460
+ }
461
+ const gridOrder = bot.manager.orders.get(fillOp.order_id) ||
462
+ Array.from(bot.manager.orders.values()).find((o) => o.orderId === fillOp.order_id);
463
+ if (!gridOrder) {
464
+ const staleMarkedAt = bot._staleCleanedOrderIds.get(fillOp.order_id);
465
+ if (staleMarkedAt != null) {
466
+ const staleAgeMs = Date.now() - staleMarkedAt;
467
+ if (staleAgeMs <= bot._staleCleanupRetentionMs) {
468
+ bot.manager.logger.log(`[ORPHAN-FILL] Skipping double-credit for stale-cleaned order ${fillOp.order_id} ` +
469
+ `(funds already freed by batch cleanup, age=${staleAgeMs}ms)`, 'warn');
470
+ continue;
471
+ }
472
+ bot._staleCleanedOrderIds.delete(fillOp.order_id);
473
+ }
474
+ let orphanFillKey = buildFillKey(fill);
475
+ if (!orphanFillKey) {
476
+ orphanFillKey = bot._buildOrphanFillFallbackKey(fill);
477
+ }
478
+ if (orphanFillKey && !bot._isNewFillKey(orphanFillKey, processedFillKeys, '[ORPHAN-FILL]', fillOp.order_id)) {
479
+ continue;
480
+ }
481
+ bot.manager.logger.log(`[ORPHAN-FILL] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
482
+ const accountingResult = await bot._applyReplaySafeOrphanFillAccounting(fill, fillOp, {
483
+ context: 'ORPHAN-FILL',
484
+ replayMessage: (op) => `[ORPHAN-FILL] Replay detected for ${op.order_id}; skipping duplicate credit`
485
+ });
486
+ if (accountingResult.status === 'missing_key') {
487
+ requiresOpenOrdersSync = true;
488
+ }
489
+ bot.manager._orphanFillsCreditedAt = Date.now();
490
+ continue;
491
+ }
492
+ const roleStr = fillOp.is_maker !== false ? 'maker' : 'taker';
493
+ bot.manager.logger.log(`Processing ${roleStr} fill for order ${fillOp.order_id}`, 'debug');
494
+ const fillKey = buildFillKey(fill);
495
+ if (!fillKey) {
496
+ bot.manager.logger.log(`[FILL] Missing history id for order ${fillOp.order_id} block ${fill.block_num}; deferring to open-orders sync`, 'warn');
497
+ requiresOpenOrdersSync = true;
498
+ continue;
499
+ }
500
+ if (!bot._isNewFillKey(fillKey, processedFillKeys, '[FILL]', fillOp.order_id)) {
501
+ continue;
502
+ }
503
+ validFills.push(fill);
504
+ const paysAmount = fillOp.pays ? fillOp.pays.amount : '?';
505
+ const receivesAmount = fillOp.receives ? fillOp.receives.amount : '?';
506
+ bot._log(`\n===== FILL DETECTED =====`);
507
+ bot._log(`Order ID: ${fillOp.order_id}`);
508
+ bot._log(`Pays: ${paysAmount}, Receives: ${receivesAmount}`);
509
+ bot._log(`Block: ${fill.block_num} (History ID: ${fill.id || 'N/A'})`);
510
+ bot._log(`=========================\n`);
511
+ }
512
+ }
513
+ const cleanupTimestamp = Date.now();
514
+ let cleanedCount = 0;
515
+ for (const [key, timestamp] of bot._recentlyQueuedFills) {
516
+ if (cleanupTimestamp - timestamp > bot._fillDedupeWindowMs) {
517
+ bot._recentlyQueuedFills.delete(key);
518
+ cleanedCount++;
519
+ }
520
+ }
521
+ if (cleanedCount > 0) {
522
+ bot.manager.logger.log(`Cleaned ${cleanedCount} old queued fill records. Remaining: ${bot._recentlyQueuedFills.size}`, 'debug');
523
+ }
524
+ if (validFills.length === 0 && !requiresOpenOrdersSync)
525
+ continue;
526
+ let allFilledOrders = [];
527
+ let ordersNeedingCorrection = [];
528
+ const pendingGhostOrders = new Set();
529
+ const fillMode = chainOrders.getFillProcessingMode();
530
+ const processValidFills = async (fillsToSync) => {
531
+ let resolvedOrders = [];
532
+ if (fillMode === 'history') {
533
+ bot.manager.logger.log(`Syncing ${fillsToSync.length} fill(s) (history mode)`, 'info');
534
+ if (fillsToSync.length >= 2) {
535
+ const batchResult = await bot.manager.syncFromFillHistoryBatch(fillsToSync, {
536
+ persistenceMode: PROCESSED_FILL_PERSISTENCE_MODES.BATCHED
537
+ });
538
+ for (const fill of fillsToSync) {
539
+ const fillKey = buildFillKey({
540
+ orderId: fill?.op?.[1]?.order_id,
541
+ blockNum: fill?.block_num,
542
+ historyId: fill?.id
543
+ });
544
+ if (fillKey)
545
+ pendingFillKeysForCurrentCycle.add(fillKey);
546
+ }
547
+ if (batchResult.filledOrders)
548
+ resolvedOrders.push(...batchResult.filledOrders);
549
+ if (batchResult.requiresOpenOrdersSync)
550
+ requiresOpenOrdersSync = true;
551
+ if (batchResult.ghostOrderIds?.length > 0) {
552
+ for (const id of batchResult.ghostOrderIds) {
553
+ pendingGhostOrders.add(id);
554
+ }
555
+ }
556
+ }
557
+ else {
558
+ for (const fill of fillsToSync) {
559
+ const resultHistory = await bot.manager.syncFromFillHistory(fill, {
560
+ persistenceMode: PROCESSED_FILL_PERSISTENCE_MODES.BATCHED
561
+ });
562
+ const fillKey = buildFillKey({
563
+ orderId: fill?.op?.[1]?.order_id,
564
+ blockNum: fill?.block_num,
565
+ historyId: fill?.id
566
+ });
567
+ if (fillKey)
568
+ pendingFillKeysForCurrentCycle.add(fillKey);
569
+ if (resultHistory.filledOrders)
570
+ resolvedOrders.push(...resultHistory.filledOrders);
571
+ if (resultHistory.requiresOpenOrdersSync)
572
+ requiresOpenOrdersSync = true;
573
+ if (resultHistory.ghostOrderId)
574
+ pendingGhostOrders.add(resultHistory.ghostOrderId);
575
+ }
576
+ }
577
+ }
578
+ if (fillMode !== 'history' || requiresOpenOrdersSync) {
579
+ if (fillMode === 'history' && requiresOpenOrdersSync) {
580
+ bot.manager.logger.log('Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
581
+ }
582
+ bot.manager.logger.log(`Syncing ${fillsToSync.length} fill(s) (open orders mode)`, 'info');
583
+ const chainOpenOrders = await chainOrders.readOpenOrders(bot.account);
584
+ const resultOpenOrders = await bot.manager.syncFromOpenOrders(chainOpenOrders);
585
+ if (resultOpenOrders.filledOrders)
586
+ resolvedOrders.push(...resultOpenOrders.filledOrders);
587
+ if (resultOpenOrders.ordersNeedingCorrection)
588
+ ordersNeedingCorrection.push(...resultOpenOrders.ordersNeedingCorrection);
589
+ }
590
+ return resolvedOrders;
591
+ };
592
+ bot.manager.pauseFundRecalc();
593
+ try {
594
+ const fillsByBlock = new Map();
595
+ const fillsWithoutBlock = [];
596
+ for (const fill of validFills) {
597
+ if (fill.block_num != null) {
598
+ const list = fillsByBlock.get(fill.block_num);
599
+ if (list)
600
+ list.push(fill);
601
+ else
602
+ fillsByBlock.set(fill.block_num, [fill]);
603
+ }
604
+ else {
605
+ fillsWithoutBlock.push(fill);
606
+ }
607
+ }
608
+ const sortedBlocks = [...fillsByBlock.keys()].sort((a, b) => a - b);
609
+ const accumulatedOrders = [];
610
+ let anyRequiresSync = false;
611
+ const initialRequiresSync = requiresOpenOrdersSync;
612
+ for (const blockNum of sortedBlocks) {
613
+ requiresOpenOrdersSync = false;
614
+ bot.manager.logger.log(`[FILL-BLOCK] Processing ${fillsByBlock.get(blockNum).length} fill(s) from block ${blockNum}`, 'debug');
615
+ const blockResult = await processValidFills(fillsByBlock.get(blockNum));
616
+ accumulatedOrders.push(...blockResult);
617
+ if (requiresOpenOrdersSync)
618
+ anyRequiresSync = true;
619
+ }
620
+ requiresOpenOrdersSync = anyRequiresSync || initialRequiresSync;
621
+ if (fillsWithoutBlock.length > 0) {
622
+ bot.manager.logger.log(`[FILL-BLOCK] Processing ${fillsWithoutBlock.length} fill(s) without block info`, 'debug');
623
+ const noBlockResult = await processValidFills(fillsWithoutBlock);
624
+ accumulatedOrders.push(...noBlockResult);
625
+ if (requiresOpenOrdersSync)
626
+ anyRequiresSync = true;
627
+ }
628
+ if (requiresOpenOrdersSync && !anyRequiresSync) {
629
+ bot.manager.logger.log('[FILL-BLOCK] Running open-orders sync for fills with missing history identifiers', 'warn');
630
+ const fallbackOrders = await processValidFills([]);
631
+ accumulatedOrders.push(...fallbackOrders);
632
+ }
633
+ allFilledOrders = accumulatedOrders;
634
+ if (ordersNeedingCorrection.length > 0) {
635
+ const correctionResult = await correctAllPriceMismatches(bot.manager, bot.account, bot.privateKey, chainOrders);
636
+ if (correctionResult.failed > 0)
637
+ bot.manager.logger.log(`${correctionResult.failed} corrections failed`, 'error');
638
+ }
639
+ }
640
+ finally {
641
+ if (pendingGhostOrders.size > 0) {
642
+ if (!bot._ghostOrderCancelAttempted)
643
+ bot._ghostOrderCancelAttempted = new Set();
644
+ const newGhostIds = [...pendingGhostOrders].filter(id => !bot._ghostOrderCancelAttempted.has(id));
645
+ if (newGhostIds.length > 0) {
646
+ const MAX_OPS_PER_TX = 200;
647
+ let batchFailed = false;
648
+ const buildResults = await Promise.allSettled(newGhostIds.map(id => chainOrders.buildCancelOrderOp(bot.account, id)));
649
+ const cancelOps = [];
650
+ for (let i = 0; i < buildResults.length; i++) {
651
+ const result = buildResults[i];
652
+ const id = newGhostIds[i];
653
+ if (result.status === 'fulfilled') {
654
+ cancelOps.push(result.value);
655
+ }
656
+ else {
657
+ bot.manager.logger.log(`[SYNC] Failed to build cancel op for ghost order ${id}: ${result.reason?.message || result.reason}`, 'warn');
658
+ }
659
+ }
660
+ for (let i = 0; i < cancelOps.length; i += MAX_OPS_PER_TX) {
661
+ const chunk = cancelOps.slice(i, i + MAX_OPS_PER_TX);
662
+ const batchIds = newGhostIds.slice(i, i + MAX_OPS_PER_TX);
663
+ try {
664
+ bot.manager.logger.log(`[SYNC] Batch-cancelling ${chunk.length} orphaned chain order(s) ` +
665
+ `(batch ${Math.floor(i / MAX_OPS_PER_TX) + 1}/${Math.ceil(cancelOps.length / MAX_OPS_PER_TX)})`, 'info');
666
+ await chainOrders.executeBatch(bot.account, bot.privateKey, chunk);
667
+ for (const id of batchIds) {
668
+ bot._ghostOrderCancelAttempted.add(id);
669
+ }
670
+ }
671
+ catch (batchErr) {
672
+ batchFailed = true;
673
+ bot.manager.logger.log(`[SYNC] Batch ghost cancel failed for chunk ${Math.floor(i / MAX_OPS_PER_TX) + 1} ` +
674
+ `(${chunk.length} orders): ${batchErr?.message || batchErr}`, 'warn');
675
+ }
676
+ }
677
+ if (!batchFailed && cancelOps.length === newGhostIds.length) {
678
+ bot.manager.logger.log(`[SYNC] Successfully batch-cancelled ${newGhostIds.length} orphaned chain order(s)`, 'info');
679
+ }
680
+ for (const ghostOrderId of newGhostIds) {
681
+ if (bot._ghostOrderCancelAttempted.has(ghostOrderId))
682
+ continue;
683
+ try {
684
+ bot.manager.logger.log(`[SYNC] Cancelling orphaned chain order ${ghostOrderId} (other-side full-fill residual)`, 'info');
685
+ await chainOrders.cancelOrder(bot.account, bot.privateKey, ghostOrderId);
686
+ bot._ghostOrderCancelAttempted.add(ghostOrderId);
687
+ }
688
+ catch (err) {
689
+ bot.manager.logger.log(`[SYNC] Failed to cancel orphaned order ${ghostOrderId}: ${err?.message || err}`, 'warn');
690
+ }
691
+ }
692
+ }
693
+ }
694
+ await bot.manager.resumeFundRecalc();
695
+ }
696
+ bot._refreshDynamicWeightDistribution('fill queue');
697
+ if (allFilledOrders.length > 0) {
698
+ const result = await bot._processFillsWithBatching(allFilledOrders, null, 'fill set');
699
+ let abortedFillCycle = result.aborted;
700
+ if (!abortedFillCycle) {
701
+ const batchFillKeys = new Set(allFilledOrders.map(filledOrder => buildFillKey({
702
+ orderId: filledOrder?.orderId,
703
+ blockNum: filledOrder?.blockNum,
704
+ historyId: filledOrder?.historyId
705
+ })).filter(Boolean));
706
+ await bot._flushProcessedFillPersistenceForKeys(batchFillKeys, 'fill-batch-committed');
707
+ }
708
+ else {
709
+ bot.manager.logger.log('[FILL-DEDUP] Fill cycle aborted; fill key persistence guarded under abort path.', 'warn');
710
+ }
711
+ const fullFillCount = allFilledOrders.filter(o => o && o.isPartial !== true).length;
712
+ const hasAnyFills = allFilledOrders.some(o => o);
713
+ const shouldRunPostFillChecks = !abortedFillCycle && fullFillCount > 0;
714
+ const shouldRunDustDetection = !abortedFillCycle && hasAnyFills;
715
+ if (shouldRunDustDetection) {
716
+ const healthResult = await bot.manager.checkGridHealth(bot.updateOrdersOnChainPlan.bind(bot));
717
+ const allDust = [
718
+ ...(healthResult.buyDustOrders || []),
719
+ ...(healthResult.sellDustOrders || []),
720
+ ];
721
+ if (allDust.length > 0) {
722
+ const dustCancelResult = await bot._cancelDustOrders({
723
+ buy: healthResult.buyDustOrders,
724
+ sell: healthResult.sellDustOrders,
725
+ });
726
+ if (dustCancelResult?.batchResult?.aborted) {
727
+ abortedFillCycle = true;
728
+ }
729
+ }
730
+ }
731
+ if (shouldRunPostFillChecks && !abortedFillCycle) {
732
+ await bot._runGridMaintenance('post-fill');
733
+ }
734
+ }
735
+ else if (pendingFillKeysForCurrentCycle.size > 0) {
736
+ await bot._flushProcessedFillPersistenceForKeys(pendingFillKeysForCurrentCycle, 'fill-batch-no-rotations');
737
+ }
738
+ bot.manager._recentFillKeysSnapshot = bot._getRecentFillKeysSnapshot();
739
+ await retryPersistenceIfNeeded(bot.manager);
740
+ bot._fillCleanupCounter += validFills.length;
741
+ const cleanupThreshold = MAINTENANCE.CLEANUP_PROBABILITY > 0 && MAINTENANCE.CLEANUP_PROBABILITY < 1
742
+ ? Math.floor(1 / MAINTENANCE.CLEANUP_PROBABILITY)
743
+ : 100;
744
+ if (bot._fillCleanupCounter >= cleanupThreshold) {
745
+ try {
746
+ await bot.accountOrders.cleanOldProcessedFills(TIMING.FILL_RECORD_RETENTION_MS);
747
+ bot._fillCleanupCounter = 0;
748
+ }
749
+ catch (err) {
750
+ bot.manager?.logger?.log(`Warning: Fill cleanup failed (will retry): ${err.message}`, 'warn');
751
+ }
752
+ }
753
+ bot._metrics.fillsProcessed += validFills.length;
754
+ bot._metrics.fillProcessingTimeMs += Date.now() - batchStartTime;
755
+ if (bot._staleCleanedOrderIds.size > 0) {
756
+ const now = Date.now();
757
+ let prunedCount = 0;
758
+ for (const [orderId, markedAt] of bot._staleCleanedOrderIds) {
759
+ if (now - markedAt > bot._staleCleanupRetentionMs) {
760
+ bot._staleCleanedOrderIds.delete(orderId);
761
+ prunedCount++;
762
+ }
763
+ }
764
+ if (prunedCount > 0) {
765
+ bot.manager.logger.log(`[STALE-CLEANUP] Pruned ${prunedCount} expired stale-cleaned order IDs ` +
766
+ `(retention=${bot._staleCleanupRetentionMs}ms, remaining=${bot._staleCleanedOrderIds.size})`, 'debug');
767
+ }
768
+ }
769
+ }
770
+ bot._markGridActivity('fill processing end');
771
+ bot._consecutiveConsumeFailures = 0;
772
+ bot._consumeFailureFirstAt = 0;
773
+ });
774
+ }
775
+ catch (err) {
776
+ const isCredentialOutage = bot._isCredentialDaemonError(err);
777
+ if (pendingFillKeysForCurrentCycle.size > 0) {
778
+ const flushReason = isCredentialOutage
779
+ ? 'credential-outage-verified-fills'
780
+ : 'fill-cycle-error-verified-fills';
781
+ if (isCredentialOutage) {
782
+ bot._credentialRecoveryNeeded = true;
783
+ bot._suspendGridPersistenceForCredentialOutage(`credential outage during fill processing: ${err.message}`);
784
+ }
785
+ try {
786
+ await bot._flushProcessedFillPersistenceForKeys(pendingFillKeysForCurrentCycle, flushReason, { throwOnError: true });
787
+ const credentialSuffix = isCredentialOutage
788
+ ? '; grid persistence is suspended until recovery'
789
+ : '';
790
+ bot.manager?.logger?.log?.(`[FILL-DEDUP] Persisted ${pendingFillKeysForCurrentCycle.size} verified processed-fill write(s) after fill cycle error${credentialSuffix}.`, isCredentialOutage ? 'warn' : 'info');
791
+ }
792
+ catch (flushErr) {
793
+ bot.manager?.logger?.log?.(`[FILL-DEDUP] Failed to persist verified fill keys during fill error handling: ${flushErr.message}`, 'warn');
794
+ }
795
+ }
796
+ if (isCredentialOutage && pendingFillKeysForCurrentCycle.size === 0) {
797
+ bot._credentialRecoveryNeeded = true;
798
+ bot._suspendGridPersistenceForCredentialOutage(`credential outage during fill processing: ${err.message}`);
799
+ }
800
+ bot._log(`Error processing fills: ${err.message}`, 'error');
801
+ if (err.stack)
802
+ bot._log(err.stack, 'error');
803
+ }
804
+ if (!bot._shuttingDown && bot._incomingFillQueue.length > 0) {
805
+ scheduleFillConsumerRestart(bot, chainOrders);
806
+ }
807
+ }
209
808
  module.exports = {
210
809
  wireProcessedFillTracking,
211
810
  flushProcessedFillPersistence,
@@ -215,6 +814,11 @@ module.exports = {
215
814
  applyReplaySafeFillAccounting,
216
815
  applyReplaySafeTrackedFillAccounting,
217
816
  applyReplaySafeOrphanFillAccounting,
218
- createFillCallback
817
+ createFillCallback,
818
+ maxConsecutiveFillConsumerFailures,
819
+ computeFillConsumerBackoffMs,
820
+ scheduleFillConsumerRestart,
821
+ consumeFillQueue,
822
+ processFillsWithBootstrapMode
219
823
  };
220
824
  //# sourceMappingURL=dexbot_fill_runtime.js.map