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.
- package/dist/credential-daemon.d.ts.map +1 -1
- package/dist/credential-daemon.js +53 -13
- package/dist/credential-daemon.js.map +1 -1
- package/dist/modules/bitshares-native/serial/types.js +1 -1
- package/dist/modules/bitshares-native/serial/types.js.map +1 -1
- package/dist/modules/chain_keys.js +1 -1
- package/dist/modules/chain_keys.js.map +1 -1
- package/dist/modules/constants.d.ts +2 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +10 -0
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/credential_policy.js +1 -1
- package/dist/modules/credential_policy.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +45 -175
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +84 -3496
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +319 -0
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -0
- package/dist/modules/dexbot_cow_runtime.js +1764 -0
- package/dist/modules/dexbot_cow_runtime.js.map +1 -0
- package/dist/modules/dexbot_fill_runtime.d.ts +47 -0
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +607 -3
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +69 -0
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +244 -15
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts +30 -0
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -0
- package/dist/modules/dexbot_startup_runtime.js +539 -0
- package/dist/modules/dexbot_startup_runtime.js.map +1 -0
- package/dist/modules/dexbot_state_recovery.d.ts +128 -0
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -0
- package/dist/modules/dexbot_state_recovery.js +397 -0
- package/dist/modules/dexbot_state_recovery.js.map +1 -0
- package/dist/modules/fund_registry.d.ts.map +1 -1
- package/dist/modules/fund_registry.js +0 -1
- package/dist/modules/fund_registry.js.map +1 -1
- package/dist/modules/general_settings.d.ts.map +1 -1
- package/dist/modules/general_settings.js +0 -1
- package/dist/modules/general_settings.js.map +1 -1
- package/dist/modules/key_store.d.ts.map +1 -1
- package/dist/modules/key_store.js +1 -2
- package/dist/modules/key_store.js.map +1 -1
- package/dist/modules/launcher/market_adapter_runtime.d.ts.map +1 -1
- package/dist/modules/launcher/market_adapter_runtime.js +0 -1
- package/dist/modules/launcher/market_adapter_runtime.js.map +1 -1
- package/dist/modules/launcher/market_adapter_watchdog.d.ts.map +1 -1
- package/dist/modules/launcher/market_adapter_watchdog.js +1 -2
- package/dist/modules/launcher/market_adapter_watchdog.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +0 -1
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +2 -2
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/utils/math.js +1 -1
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/system.js +1 -1
- 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 +0 -1
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/modules/paths.d.ts +2 -0
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +13 -2
- package/dist/modules/paths.js.map +1 -1
- package/dist/scripts/verify-browser-bundle.js +25 -1
- package/dist/scripts/verify-browser-bundle.js.map +1 -1
- 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
|
-
|
|
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
|