dexbot 1.4.23 → 1.4.25

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 (137) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +1 -0
  3. package/analysis/README.md +49 -0
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/grid_correction_check.ts +896 -0
  6. package/analysis/trend_detection/package.json +1 -1
  7. package/claw/package.json +1 -1
  8. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  9. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  10. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  11. package/dist/analysis/grid_correction_check.d.ts +90 -0
  12. package/dist/analysis/grid_correction_check.d.ts.map +1 -0
  13. package/dist/analysis/grid_correction_check.js +861 -0
  14. package/dist/analysis/grid_correction_check.js.map +1 -0
  15. package/dist/dexbot.d.ts.map +1 -1
  16. package/dist/dexbot.js +62 -6
  17. package/dist/dexbot.js.map +1 -1
  18. package/dist/modules/account_bots.d.ts.map +1 -1
  19. package/dist/modules/account_bots.js +53 -5
  20. package/dist/modules/account_bots.js.map +1 -1
  21. package/dist/modules/account_orders.d.ts +7 -10
  22. package/dist/modules/account_orders.d.ts.map +1 -1
  23. package/dist/modules/account_orders.js +26 -2
  24. package/dist/modules/account_orders.js.map +1 -1
  25. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  26. package/dist/modules/bitshares-native/subscriptions.js +176 -21
  27. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  28. package/dist/modules/chain_orders.d.ts.map +1 -1
  29. package/dist/modules/chain_orders.js +21 -2
  30. package/dist/modules/chain_orders.js.map +1 -1
  31. package/dist/modules/config.d.ts.map +1 -1
  32. package/dist/modules/config.js +27 -0
  33. package/dist/modules/config.js.map +1 -1
  34. package/dist/modules/constants.d.ts +15 -1
  35. package/dist/modules/constants.d.ts.map +1 -1
  36. package/dist/modules/constants.js +70 -6
  37. package/dist/modules/constants.js.map +1 -1
  38. package/dist/modules/credit_runtime.d.ts.map +1 -1
  39. package/dist/modules/credit_runtime.js +31 -32
  40. package/dist/modules/credit_runtime.js.map +1 -1
  41. package/dist/modules/dexbot_class.d.ts +24 -0
  42. package/dist/modules/dexbot_class.d.ts.map +1 -1
  43. package/dist/modules/dexbot_class.js +66 -12
  44. package/dist/modules/dexbot_class.js.map +1 -1
  45. package/dist/modules/dexbot_cow_runtime.d.ts +22 -17
  46. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  47. package/dist/modules/dexbot_cow_runtime.js +646 -128
  48. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  49. package/dist/modules/dexbot_fill_runtime.d.ts +11 -1
  50. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  51. package/dist/modules/dexbot_fill_runtime.js +108 -1
  52. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  53. package/dist/modules/dexbot_maintenance_runtime.d.ts +17 -1
  54. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  55. package/dist/modules/dexbot_maintenance_runtime.js +215 -26
  56. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  57. package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
  58. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  59. package/dist/modules/dexbot_startup_runtime.js +58 -7
  60. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  61. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  62. package/dist/modules/dexbot_state_recovery.js +55 -3
  63. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  64. package/dist/modules/graceful_shutdown.d.ts +12 -1
  65. package/dist/modules/graceful_shutdown.d.ts.map +1 -1
  66. package/dist/modules/graceful_shutdown.js +39 -1
  67. package/dist/modules/graceful_shutdown.js.map +1 -1
  68. package/dist/modules/key_store.d.ts.map +1 -1
  69. package/dist/modules/key_store.js +38 -1
  70. package/dist/modules/key_store.js.map +1 -1
  71. package/dist/modules/order/accounting.d.ts.map +1 -1
  72. package/dist/modules/order/accounting.js +20 -3
  73. package/dist/modules/order/accounting.js.map +1 -1
  74. package/dist/modules/order/grid.d.ts +1 -1
  75. package/dist/modules/order/grid.d.ts.map +1 -1
  76. package/dist/modules/order/grid.js +288 -31
  77. package/dist/modules/order/grid.js.map +1 -1
  78. package/dist/modules/order/grid_reconcile.d.ts +4 -2
  79. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  80. package/dist/modules/order/grid_reconcile.js +242 -222
  81. package/dist/modules/order/grid_reconcile.js.map +1 -1
  82. package/dist/modules/order/grid_reconcile_internal.d.ts +11 -1
  83. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  84. package/dist/modules/order/grid_reconcile_internal.js +132 -6
  85. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  86. package/dist/modules/order/manager.d.ts +68 -14
  87. package/dist/modules/order/manager.d.ts.map +1 -1
  88. package/dist/modules/order/manager.js +241 -52
  89. package/dist/modules/order/manager.js.map +1 -1
  90. package/dist/modules/order/strategy.d.ts.map +1 -1
  91. package/dist/modules/order/strategy.js +25 -8
  92. package/dist/modules/order/strategy.js.map +1 -1
  93. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  94. package/dist/modules/order/sync_engine.js +261 -166
  95. package/dist/modules/order/sync_engine.js.map +1 -1
  96. package/dist/modules/order/utils/math.d.ts +47 -5
  97. package/dist/modules/order/utils/math.d.ts.map +1 -1
  98. package/dist/modules/order/utils/math.js +175 -11
  99. package/dist/modules/order/utils/math.js.map +1 -1
  100. package/dist/modules/order/utils/order.d.ts +16 -15
  101. package/dist/modules/order/utils/order.d.ts.map +1 -1
  102. package/dist/modules/order/utils/order.js +199 -32
  103. package/dist/modules/order/utils/order.js.map +1 -1
  104. package/dist/modules/order/utils/slot.d.ts +8 -0
  105. package/dist/modules/order/utils/slot.d.ts.map +1 -0
  106. package/dist/modules/order/utils/slot.js +16 -0
  107. package/dist/modules/order/utils/slot.js.map +1 -0
  108. package/dist/modules/order/utils/system.d.ts.map +1 -1
  109. package/dist/modules/order/utils/system.js +3 -2
  110. package/dist/modules/order/utils/system.js.map +1 -1
  111. package/dist/modules/order/utils/validate.d.ts +5 -8
  112. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  113. package/dist/modules/order/utils/validate.js +64 -82
  114. package/dist/modules/order/utils/validate.js.map +1 -1
  115. package/dist/modules/order/working_grid.d.ts +5 -0
  116. package/dist/modules/order/working_grid.d.ts.map +1 -1
  117. package/dist/modules/order/working_grid.js +21 -0
  118. package/dist/modules/order/working_grid.js.map +1 -1
  119. package/dist/modules/paths.d.ts +4 -2
  120. package/dist/modules/paths.d.ts.map +1 -1
  121. package/dist/modules/paths.js +79 -24
  122. package/dist/modules/paths.js.map +1 -1
  123. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  124. package/dist/scripts/analyze-orders.js +23 -2
  125. package/dist/scripts/analyze-orders.js.map +1 -1
  126. package/dist/scripts/generate_market_adapter_whitelist.d.ts +2 -5
  127. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
  128. package/dist/scripts/generate_market_adapter_whitelist.js +83 -1
  129. package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
  130. package/docs/DEXBOT_COMPARISON.md +15 -15
  131. package/docs/EVOLUTION.md +10 -8
  132. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  133. package/docs/GRID_RECALCULATION.md +6 -0
  134. package/docs/README.md +2 -2
  135. package/market_adapter/README.md +10 -0
  136. package/package.json +2 -1
  137. package/scripts/README.md +4 -0
@@ -3,11 +3,10 @@ const require = createRequire(import.meta.url);
3
3
  import { _countActiveOnGrid, _cancelChainOrder, _recoverStartupSyncFailure, _refreshStartupUpdatePlans, _executeStartupUpdateBatch, _executeStartupSequentialUpdateFallback, _executePlannedStartupCreates, _reconcileStartupSide, } from './grid_reconcile_internal.js';
4
4
  import { ORDER_TYPES, ORDER_STATES, TIMING } from '../constants.js';
5
5
  import { readOpenOrdersGuarded } from '../chain_orders.js';
6
- import { calculatePriceTolerance, getAssetFeesSafe } from './utils/math.js';
6
+ import { getAssetFeesSafe, priceSlotEqual } from './utils/math.js';
7
7
  import { isOrderPlaced, parseChainOrder, isOrderOnChain, chainOrderMatchesSlot, duplicateOrphanLogInfo, } from './utils/order.js';
8
8
  import * as Format from './format.js';
9
9
  import { getErrorMessage } from '../utils/errors.js';
10
- const SUSPECTED_DUPLICATE_TOLERANCE_MULTIPLIER = 5;
11
10
  /**
12
11
  * Returns { resumed: boolean, matchedCount: number }.
13
12
  * @param {Object} params - Destructured parameters
@@ -18,7 +17,7 @@ const SUSPECTED_DUPLICATE_TOLERANCE_MULTIPLIER = 5;
18
17
  * @param {Function} params.storeGrid - Grid persistence callback
19
18
  * @returns {Promise<{resumed: boolean, matchedCount: number}>}
20
19
  */
21
- export async function attemptResumePersistedGridByPriceMatch({ manager, persistedGrid, chainOpenOrders, logger, storeGrid, boundaryIdx = null, }) {
20
+ export async function attemptResumePersistedGridByPriceMatch({ manager, persistedGrid, chainOpenOrders, logger, storeGrid, boundaryIdx = null, genesis = null, }) {
22
21
  if (!Array.isArray(persistedGrid) || persistedGrid.length === 0)
23
22
  return { resumed: false, matchedCount: 0 };
24
23
  if (!Array.isArray(chainOpenOrders) || chainOpenOrders.length === 0)
@@ -28,7 +27,9 @@ export async function attemptResumePersistedGridByPriceMatch({ manager, persiste
28
27
  try {
29
28
  logger && logger.log && logger.log('No matching active order IDs found. Attempting to match by price...', 'info');
30
29
  const { loadGrid } = require('./grid');
31
- await loadGrid(manager, persistedGrid, boundaryIdx);
30
+ // Prefer explicit genesis if caller wired it, else fall back to manager._genesis
31
+ const genesisArg = genesis ?? manager?._genesis ?? null;
32
+ await loadGrid(manager, persistedGrid, boundaryIdx, genesisArg);
32
33
  await manager.synchronizeWithChain(chainOpenOrders, 'readOpenOrders');
33
34
  const matchedOrderIds = new Set(Array.from(manager.orders.values())
34
35
  .filter((o) => o && isOrderOnChain(o))
@@ -66,7 +67,7 @@ export async function attemptResumePersistedGridByPriceMatch({ manager, persiste
66
67
  * @param {Function} [params.attemptResumeFn=attemptResumePersistedGridByPriceMatch] - Resume function
67
68
  * @returns {Promise<Object>} { shouldRegenerate, hasActiveMatch, resumedByPrice, matchedCount }
68
69
  */
69
- export async function decideStartupGridAction({ persistedGrid, chainOpenOrders, manager, logger, storeGrid, boundaryIdx = null, attemptResumeFn = attemptResumePersistedGridByPriceMatch, }) {
70
+ export async function decideStartupGridAction({ persistedGrid, chainOpenOrders, manager, logger, storeGrid, boundaryIdx = null, genesis = null, attemptResumeFn = attemptResumePersistedGridByPriceMatch, }) {
70
71
  const persisted = Array.isArray(persistedGrid) ? persistedGrid : [];
71
72
  const chain = Array.isArray(chainOpenOrders) ? chainOpenOrders : [];
72
73
  if (persisted.length === 0) {
@@ -78,7 +79,7 @@ export async function decideStartupGridAction({ persistedGrid, chainOpenOrders,
78
79
  return { shouldRegenerate: false, hasActiveMatch: true, resumedByPrice: false, matchedCount: 0 };
79
80
  }
80
81
  if (chain.length > 0) {
81
- const resume = await attemptResumeFn({ manager, persistedGrid: persisted, chainOpenOrders: chain, logger, storeGrid, boundaryIdx });
82
+ const resume = await attemptResumeFn({ manager, persistedGrid: persisted, chainOpenOrders: chain, logger, storeGrid, boundaryIdx, genesis });
82
83
  return { shouldRegenerate: !resume.resumed, hasActiveMatch: false, resumedByPrice: !!resume.resumed, matchedCount: resume.matchedCount || 0 };
83
84
  }
84
85
  return { shouldRegenerate: true, hasActiveMatch: false, resumedByPrice: false, matchedCount: 0 };
@@ -185,6 +186,12 @@ export async function reconcileGridOrders({ manager, config, account, privateKey
185
186
  const plannedCancels = [];
186
187
  const cancelledDuplicateIds = new Set();
187
188
  const activeGridOrders = Array.from(manager.orders.values()).filter((o) => o && o.orderId && isOrderPlaced(o));
189
+ // Update-first policy: unmatched chain orders are NEVER cancelled here.
190
+ // They flow into _reconcileStartupSide below, which price-updates them onto
191
+ // rail slots in a single batch (plannedUpdates), creates missing orders, and
192
+ // cancels only true surplus (chainCount > targetCount). Pre-emptive
193
+ // cancellation would turn one batched update into per-order cancel+create
194
+ // churn and strip the bot's own live orders during a reset.
188
195
  for (const u of unmatchedParsed) {
189
196
  const p = u.parsed;
190
197
  const desc = `Unmatched chain order: ${p.orderId} (${p.type === ORDER_TYPES.BUY ? 'BUY' : 'SELL'}), price=${Format.formatPrice6(p.price)}, size=${Format.formatSizeByOrderType(p.size ?? 0, p.type, manager.assets)}`;
@@ -192,18 +199,14 @@ export async function reconcileGridOrders({ manager, config, account, privateKey
192
199
  for (const gridOrder of activeGridOrders) {
193
200
  if (gridOrder.type !== p.type)
194
201
  continue;
202
+ const precision = p.type === ORDER_TYPES.SELL ? manager.assets.assetA.precision : manager.assets.assetB.precision;
203
+ const isEqual = priceSlotEqual(p.price, gridOrder.price, precision);
195
204
  const priceDiff = Math.abs(p.price - gridOrder.price);
196
- const tolerance = calculatePriceTolerance(gridOrder.price, gridOrder.size, gridOrder.type, manager.assets) || 0;
197
- const candidate = {
198
- gridOrder,
199
- priceDiff,
200
- tolerance,
201
- looseTolerance: tolerance * SUSPECTED_DUPLICATE_TOLERANCE_MULTIPLIER,
202
- };
205
+ const candidate = { gridOrder, priceDiff, isEqual };
203
206
  if (!nearest || priceDiff < nearest.priceDiff)
204
207
  nearest = candidate;
205
208
  }
206
- if (nearest && nearest.priceDiff <= nearest.looseTolerance) {
209
+ if (nearest && nearest.isEqual) {
207
210
  // The sync layer already owns this duplicate: PASS 2 queues a
208
211
  // cancel-only correction, and startup runs it (correctAllPriceMismatches)
209
212
  // before reconcile — leaving a stale snapshot where the order is already
@@ -226,8 +229,7 @@ export async function reconcileGridOrders({ manager, config, account, privateKey
226
229
  const { level, suffix } = duplicateOrphanLogInfo(p.orderId);
227
230
  logger?.log?.(`SUSPECTED DUPLICATE: ${desc} - nearest active grid ${nearest.gridOrder.id} ` +
228
231
  `(orderId=${nearest.gridOrder.orderId}, price=${Format.formatPrice6(nearest.gridOrder.price)}, ` +
229
- `diff=${Format.formatPrice6(nearest.priceDiff)}, tolerance=${Format.formatPrice6(nearest.tolerance)}, ` +
230
- `looseTolerance=${Format.formatPrice6(nearest.looseTolerance)})${suffix}`, level);
232
+ `diff=${Format.formatPrice6(nearest.priceDiff)}, slotEqual=${nearest.isEqual})${suffix}`, level);
231
233
  // Queue duplicate for Phase 2 cancellation instead of executing under lock
232
234
  cancelledDuplicateIds.add(p.orderId);
233
235
  plannedCancels.push({
@@ -239,8 +241,7 @@ export async function reconcileGridOrders({ manager, config, account, privateKey
239
241
  else if (nearest) {
240
242
  logger?.log?.(`${desc}; nearest active same-side grid ${nearest.gridOrder.id} ` +
241
243
  `(orderId=${nearest.gridOrder.orderId}, price=${Format.formatPrice6(nearest.gridOrder.price)}, ` +
242
- `diff=${Format.formatPrice6(nearest.priceDiff)}, tolerance=${Format.formatPrice6(nearest.tolerance)}, ` +
243
- `looseTolerance=${Format.formatPrice6(nearest.looseTolerance)})`, 'warn');
244
+ `diff=${Format.formatPrice6(nearest.priceDiff)}, slotEqual=${nearest.isEqual})`, 'warn');
244
245
  }
245
246
  else {
246
247
  logger?.log?.(`${desc}; no active same-side grid order exists`, 'warn');
@@ -290,240 +291,259 @@ export async function reconcileGridOrders({ manager, config, account, privateKey
290
291
  // These are the heavy operations that would block all other grid operations
291
292
  // if held under _gridLock. All lock acquisitions here follow the canonical
292
293
  // hierarchy (fillProcessingLock → syncLock → gridLock → fundLock).
293
- const { plannedCreates, plannedUpdates, plannedCancels, chainSellCount, chainBuyCount } = phase1Result;
294
- // Execute planned cancellations (duplicates, edge, excess) outside lock.
295
- // Each _cancelChainOrder call acquires _gridLock internally via synchronizeWithChain.
296
- if (!dryRun && plannedCancels.length > 0) {
297
- logger?.log?.(`Startup: Executing ${plannedCancels.length} queued cancellations (Phase 2)`, 'info');
298
- for (const cancelPlan of plannedCancels) {
299
- try {
300
- await _cancelChainOrder({
301
- chainOrders,
302
- account,
303
- privateKey,
304
- manager,
305
- chainOrderId: cancelPlan.chainOrderId,
306
- dryRun,
307
- chainOrderObj: cancelPlan.chainOrderObj,
308
- releaseUntrackedFunds: cancelPlan.releaseUntrackedFunds,
309
- });
310
- logger?.log?.(`Startup: Cancelled queued order ${cancelPlan.chainOrderId} (Phase 2)`, 'info');
311
- }
312
- catch (cancelErr) {
313
- logger?.log?.(`Startup: Failed to cancel queued order ${cancelPlan.chainOrderId}: ${getErrorMessage(cancelErr)}`, 'error');
314
- }
315
- }
316
- }
317
- if (!dryRun && plannedUpdates.length > 0) {
318
- let updatePlans = plannedUpdates;
319
- const maxBatchAttempts = 3;
320
- let batchCompleted = false;
321
- if (supportsBatchUpdate) {
322
- for (let attempt = 1; attempt <= maxBatchAttempts; attempt++) {
294
+ // P0 placement mutex: the whole of Phase 2 is broadcast I/O plus master-grid
295
+ // mutations, and it is NOT under _gridLock. Without the broadcasting gate a
296
+ // fill-driven COW rebalance can plan and broadcast the SAME slots in parallel
297
+ // with this loop: the commit is then refused ("master mutation during
298
+ // broadcasting"), both order generations land on chain, and the loser becomes
299
+ // a duplicate-price orphan backlog that blocks placements until drained.
300
+ // startBroadcasting() defers that rebalance —
301
+ // manager.performSafeRebalance waits on the flag and the fill consumer gates
302
+ // on it. The flag is refcounted and has a 120s stale-clear watchdog.
303
+ if (typeof manager.startBroadcasting === 'function')
304
+ manager.startBroadcasting();
305
+ try {
306
+ const { plannedCreates, plannedUpdates, plannedCancels, chainSellCount, chainBuyCount } = phase1Result;
307
+ // Execute planned cancellations (duplicates, edge, excess) outside lock.
308
+ // Each _cancelChainOrder call acquires _gridLock internally via synchronizeWithChain.
309
+ if (!dryRun && plannedCancels.length > 0) {
310
+ logger?.log?.(`Startup: Executing ${plannedCancels.length} queued cancellations (Phase 2)`, 'info');
311
+ for (const cancelPlan of plannedCancels) {
323
312
  try {
324
- const batchResult = await _executeStartupUpdateBatch({
325
- updatePlans,
313
+ await _cancelChainOrder({
326
314
  chainOrders,
327
315
  account,
328
316
  privateKey,
329
317
  manager,
318
+ chainOrderId: cancelPlan.chainOrderId,
330
319
  dryRun,
320
+ chainOrderObj: cancelPlan.chainOrderObj,
321
+ releaseUntrackedFunds: cancelPlan.releaseUntrackedFunds,
331
322
  });
332
- if (batchResult.executed) {
333
- logger?.log?.(`Startup: Update batch complete (${batchResult.prepared} updated)`, 'info');
323
+ logger?.log?.(`Startup: Cancelled queued order ${cancelPlan.chainOrderId} (Phase 2)`, 'info');
324
+ }
325
+ catch (cancelErr) {
326
+ logger?.log?.(`Startup: Failed to cancel queued order ${cancelPlan.chainOrderId}: ${getErrorMessage(cancelErr)}. ` +
327
+ `The order stays live: the relocation/create phases will skip any placement ` +
328
+ `that would cross it (STARTUP-CROSS-GUARD) and re-align on the next cycle.`, 'error');
329
+ }
330
+ }
331
+ }
332
+ if (!dryRun && plannedUpdates.length > 0) {
333
+ let updatePlans = plannedUpdates;
334
+ const maxBatchAttempts = 3;
335
+ let batchCompleted = false;
336
+ if (supportsBatchUpdate) {
337
+ for (let attempt = 1; attempt <= maxBatchAttempts; attempt++) {
338
+ try {
339
+ const batchResult = await _executeStartupUpdateBatch({
340
+ updatePlans,
341
+ chainOrders,
342
+ account,
343
+ privateKey,
344
+ manager,
345
+ dryRun,
346
+ });
347
+ if (batchResult.executed) {
348
+ logger?.log?.(`Startup: Update batch complete (${batchResult.prepared} updated)`, 'info');
349
+ }
350
+ batchCompleted = true;
351
+ break;
352
+ }
353
+ catch (err) {
354
+ logger?.log?.(`Startup: Update batch attempt ${attempt}/${maxBatchAttempts} failed: ${getErrorMessage(err)}`, 'error');
355
+ const refreshedChainOrders = await _recoverStartupSyncFailure({
356
+ chainOrders,
357
+ manager,
358
+ account,
359
+ logger,
360
+ triggerMessage: `Startup: Triggering recovery sync after update batch failure (attempt ${attempt}/${maxBatchAttempts})`,
361
+ source: 'startupReconcileUpdateBatchFailure',
362
+ });
363
+ updatePlans = _refreshStartupUpdatePlans(updatePlans, refreshedChainOrders);
364
+ if (updatePlans.length === 0) {
365
+ logger?.log?.('Startup: No update plans remain after recovery sync; skipping further update attempts', 'warn');
366
+ batchCompleted = true;
367
+ break;
368
+ }
369
+ if (attempt >= maxBatchAttempts) {
370
+ logger?.log?.('Startup: Update batch retries exhausted; switching to sequential fallback', 'warn');
371
+ break;
372
+ }
334
373
  }
335
- batchCompleted = true;
336
- break;
337
374
  }
338
- catch (err) {
339
- logger?.log?.(`Startup: Update batch attempt ${attempt}/${maxBatchAttempts} failed: ${getErrorMessage(err)}`, 'error');
340
- const refreshedChainOrders = await _recoverStartupSyncFailure({
375
+ }
376
+ else {
377
+ logger?.log?.('Startup: Batch update helpers unavailable; using sequential fallback updates', 'warn');
378
+ }
379
+ if (!batchCompleted && updatePlans.length > 0) {
380
+ try {
381
+ const fallbackResult = await _executeStartupSequentialUpdateFallback({
382
+ updatePlans,
341
383
  chainOrders,
342
- manager,
343
384
  account,
344
- logger,
345
- triggerMessage: `Startup: Triggering recovery sync after update batch failure (attempt ${attempt}/${maxBatchAttempts})`,
346
- source: 'startupReconcileUpdateBatchFailure',
385
+ privateKey,
386
+ manager,
387
+ dryRun,
347
388
  });
348
- updatePlans = _refreshStartupUpdatePlans(updatePlans, refreshedChainOrders);
349
- if (updatePlans.length === 0) {
350
- logger?.log?.('Startup: No update plans remain after recovery sync; skipping further update attempts', 'warn');
351
- batchCompleted = true;
352
- break;
353
- }
354
- if (attempt >= maxBatchAttempts) {
355
- logger?.log?.('Startup: Update batch retries exhausted; switching to sequential fallback', 'warn');
356
- break;
389
+ if (fallbackResult.executed > 0 || fallbackResult.skipped > 0 || fallbackResult.failed > 0) {
390
+ logger?.log?.(`Startup: Sequential fallback complete (updated=${fallbackResult.executed}, skipped=${fallbackResult.skipped}, failed=${fallbackResult.failed})`, fallbackResult.failed > 0 ? 'warn' : 'info');
357
391
  }
358
392
  }
393
+ catch (err) {
394
+ logger?.log?.(`Startup: Sequential fallback failed unexpectedly: ${getErrorMessage(err)}`, 'error');
395
+ }
359
396
  }
360
397
  }
361
- else {
362
- logger?.log?.('Startup: Batch update helpers unavailable; using sequential fallback updates', 'warn');
398
+ const phase2CreatedOrderIds = new Set();
399
+ if (!dryRun && plannedCreates.length > 0) {
400
+ const createdIds = await _executePlannedStartupCreates({
401
+ createPlans: plannedCreates,
402
+ chainOrders,
403
+ account,
404
+ privateKey,
405
+ manager,
406
+ dryRun,
407
+ });
408
+ for (const id of createdIds) {
409
+ phase2CreatedOrderIds.add(id);
410
+ }
363
411
  }
364
- if (!batchCompleted && updatePlans.length > 0) {
412
+ let finalChainSellCount = chainSellCount;
413
+ let finalChainBuyCount = chainBuyCount;
414
+ if (!dryRun) {
365
415
  try {
366
- const fallbackResult = await _executeStartupSequentialUpdateFallback({
367
- updatePlans,
368
- chainOrders,
369
- account,
370
- privateKey,
371
- manager,
372
- dryRun,
416
+ // Truncated-read guard: get_full_accounts caps the limit_orders
417
+ // window and fresh orders (exactly the Phase-2 creates) sort last
418
+ // and are the first entries omitted. The adoption loop and the
419
+ // surplus-cancel counts would silently operate on a partial
420
+ // snapshot — defer to the sync loop's targeted drift detection and
421
+ // keep the pre-Phase-2 counts for the summary log.
422
+ const freshOpenOrders = await readOpenOrdersGuarded(chainOrders, account, {
423
+ log: (message, level) => logger?.log?.(message, level),
424
+ label: 'STARTUP',
425
+ detail: 'Post-Phase-2 chain read',
373
426
  });
374
- if (fallbackResult.executed > 0 || fallbackResult.skipped > 0 || fallbackResult.failed > 0) {
375
- logger?.log?.(`Startup: Sequential fallback complete (updated=${fallbackResult.executed}, skipped=${fallbackResult.skipped}, failed=${fallbackResult.failed})`, fallbackResult.failed > 0 ? 'warn' : 'info');
376
- }
377
- }
378
- catch (err) {
379
- logger?.log?.(`Startup: Sequential fallback failed unexpectedly: ${getErrorMessage(err)}`, 'error');
380
- }
381
- }
382
- }
383
- const phase2CreatedOrderIds = new Set();
384
- if (!dryRun && plannedCreates.length > 0) {
385
- const createdIds = await _executePlannedStartupCreates({
386
- createPlans: plannedCreates,
387
- chainOrders,
388
- account,
389
- privateKey,
390
- manager,
391
- dryRun,
392
- });
393
- for (const id of createdIds) {
394
- phase2CreatedOrderIds.add(id);
395
- }
396
- }
397
- let finalChainSellCount = chainSellCount;
398
- let finalChainBuyCount = chainBuyCount;
399
- if (!dryRun) {
400
- try {
401
- // Truncated-read guard: get_full_accounts caps the limit_orders
402
- // window and fresh orders (exactly the Phase-2 creates) sort last
403
- // and are the first entries omitted. The adoption loop and the
404
- // surplus-cancel counts would silently operate on a partial
405
- // snapshot — defer to the sync loop's targeted drift detection and
406
- // keep the pre-Phase-2 counts for the summary log.
407
- const freshOpenOrders = await readOpenOrdersGuarded(chainOrders, account, {
408
- log: (message, level) => logger?.log?.(message, level),
409
- label: 'STARTUP',
410
- detail: 'Post-Phase-2 chain read',
411
- });
412
- if (freshOpenOrders !== null) {
413
- const freshParsed = (Array.isArray(freshOpenOrders) ? freshOpenOrders : [])
414
- .map((co) => ({ chain: co, parsed: parseChainOrder(co, manager.assets) }))
415
- .filter((x) => x.parsed);
416
- const gridOrderIds = new Set();
417
- for (const order of manager.orders.values()) {
418
- if (order && order.orderId)
419
- gridOrderIds.add(order.orderId);
420
- }
421
- // Merge any chain order IDs Phase 2 successfully created on-chain
422
- // (even if _applySync failed to register them in manager.orders).
423
- // This prevents Phase 3 from cancelling legitimately created orders.
424
- for (const id of phase2CreatedOrderIds) {
425
- gridOrderIds.add(id);
426
- }
427
- // Adopt any Phase-2 uncertain-landed chain orders that never made
428
- // it into grid slots (the group adoption sync only runs when the
429
- // post-uncertain read returned orders). Targeted slot adoption
430
- // only: match VIRTUAL/SPREAD slots without an orderId by
431
- // type+price+size (within tolerance). Full syncFromOpenOrders is
432
- // deliberately NOT used here — its pass-1 virtualizes ACTIVE slots
433
- // missing from the snapshot, and a lagging read right after the
434
- // Phase-2 broadcast would destroy the confirmed grid.
435
- const btsFeeData = getAssetFeesSafe('BTS');
436
- for (const entry of freshParsed) {
437
- const co = entry.chain;
438
- const parsed = entry.parsed;
439
- if (!co?.id || !parsed)
440
- continue;
441
- if (gridOrderIds.has(co.id))
442
- continue;
443
- const candidate = Array.from(manager.orders.values()).find((o) => {
444
- if (!o || o.orderId || o.state !== ORDER_STATES.VIRTUAL)
445
- return false;
446
- return chainOrderMatchesSlot(parsed, o, manager.assets);
447
- });
448
- if (!candidate)
449
- continue;
450
- try {
451
- await manager._applySync({
452
- gridOrderId: candidate.id,
453
- chainOrderId: co.id,
454
- isPartialPlacement: false,
455
- expectedType: parsed.type,
456
- fee: btsFeeData?.createFee || 0,
457
- }, 'createOrder');
458
- gridOrderIds.add(co.id);
459
- phase2CreatedOrderIds.add(co.id);
460
- logger?.log?.(`Startup: Adopted uncertain-landed chain order ${co.id} into slot ${candidate.id} (${parsed.type}, price=${parsed.price})`, 'warn');
427
+ if (freshOpenOrders !== null) {
428
+ const freshParsed = (Array.isArray(freshOpenOrders) ? freshOpenOrders : [])
429
+ .map((co) => ({ chain: co, parsed: parseChainOrder(co, manager.assets) }))
430
+ .filter((x) => x.parsed);
431
+ const gridOrderIds = new Set();
432
+ for (const order of manager.orders.values()) {
433
+ if (order && order.orderId)
434
+ gridOrderIds.add(order.orderId);
461
435
  }
462
- catch (adoptErr) {
463
- logger?.log?.(`Startup: Failed to adopt landed order ${co.id} into slot ${candidate?.id}: ${getErrorMessage(adoptErr)}`, 'warn');
464
- // The order IS on chain (Phase-2 uncertain-landed). Even
465
- // though the slot registration failed, the ID must still
466
- // be protected from the same pass's surplus-cancel —
467
- // mirror the capturedId protection of _createOrderFromGrid.
468
- // The next sync loop's orphan adoption registers it.
469
- gridOrderIds.add(co.id);
436
+ // Merge any chain order IDs Phase 2 successfully created on-chain
437
+ // (even if _applySync failed to register them in manager.orders).
438
+ // This prevents Phase 3 from cancelling legitimately created orders.
439
+ for (const id of phase2CreatedOrderIds) {
440
+ gridOrderIds.add(id);
470
441
  }
471
- }
472
- const staleSurplusCancels = [];
473
- for (const side of [ORDER_TYPES.SELL, ORDER_TYPES.BUY]) {
474
- const targetCount = side === ORDER_TYPES.SELL ? targetSell : targetBuy;
475
- let sideOrders = freshParsed.filter((x) => x.parsed.type === side);
476
- const sideLabel = side === ORDER_TYPES.SELL ? 'SELL' : 'BUY';
477
- if (sideOrders.length > targetCount) {
478
- // Sort by chain ID for deterministic cancellation order.
479
- sideOrders = sideOrders.sort((a, b) => (a.chain.id || '').localeCompare(b.chain.id || ''));
480
- let cancelLimit = sideOrders.length - targetCount;
481
- for (const so of sideOrders) {
482
- if (cancelLimit <= 0)
483
- break;
484
- if (!gridOrderIds.has(so.chain.id)) {
485
- staleSurplusCancels.push({ chainOrderObj: so.chain, sideLabel });
486
- cancelLimit--;
487
- }
442
+ // Adopt any Phase-2 uncertain-landed chain orders that never made
443
+ // it into grid slots (the group adoption sync only runs when the
444
+ // post-uncertain read returned orders). Targeted slot adoption
445
+ // only: match VIRTUAL/SPREAD slots without an orderId by
446
+ // type+price+size (within tolerance). Full syncFromOpenOrders is
447
+ // deliberately NOT used here — its pass-1 virtualizes ACTIVE slots
448
+ // missing from the snapshot, and a lagging read right after the
449
+ // Phase-2 broadcast would destroy the confirmed grid.
450
+ const btsFeeData = getAssetFeesSafe('BTS');
451
+ for (const entry of freshParsed) {
452
+ const co = entry.chain;
453
+ const parsed = entry.parsed;
454
+ if (!co?.id || !parsed)
455
+ continue;
456
+ if (gridOrderIds.has(co.id))
457
+ continue;
458
+ const candidate = Array.from(manager.orders.values()).find((o) => {
459
+ if (!o || o.orderId || o.state !== ORDER_STATES.VIRTUAL)
460
+ return false;
461
+ return chainOrderMatchesSlot(parsed, o, manager.assets);
462
+ });
463
+ if (!candidate)
464
+ continue;
465
+ try {
466
+ await manager._applySync({
467
+ gridOrderId: candidate.id,
468
+ chainOrderId: co.id,
469
+ isPartialPlacement: false,
470
+ expectedType: parsed.type,
471
+ fee: btsFeeData?.createFee || 0,
472
+ }, 'createOrder');
473
+ gridOrderIds.add(co.id);
474
+ phase2CreatedOrderIds.add(co.id);
475
+ logger?.log?.(`Startup: Adopted uncertain-landed chain order ${co.id} into slot ${candidate.id} (${parsed.type}, price=${parsed.price})`, 'warn');
476
+ }
477
+ catch (adoptErr) {
478
+ logger?.log?.(`Startup: Failed to adopt landed order ${co.id} into slot ${candidate?.id}: ${getErrorMessage(adoptErr)}`, 'warn');
479
+ // The order IS on chain (Phase-2 uncertain-landed). Even
480
+ // though the slot registration failed, the ID must still
481
+ // be protected from the same pass's surplus-cancel —
482
+ // mirror the capturedId protection of _createOrderFromGrid.
483
+ // The next sync loop's orphan adoption registers it.
484
+ gridOrderIds.add(co.id);
488
485
  }
489
486
  }
490
- }
491
- let cancelledSellCount = 0;
492
- let cancelledBuyCount = 0;
493
- if (staleSurplusCancels.length > 0) {
494
- for (const sc of staleSurplusCancels) {
495
- try {
496
- await _cancelChainOrder({
497
- chainOrders,
498
- account,
499
- privateKey,
500
- manager,
501
- chainOrderId: sc.chainOrderObj.id,
502
- dryRun,
503
- chainOrderObj: sc.chainOrderObj,
504
- releaseUntrackedFunds: true,
505
- });
506
- if (sc.sideLabel === 'SELL')
507
- cancelledSellCount++;
508
- else
509
- cancelledBuyCount++;
510
- logger?.log?.(`Startup: Cancelled stale surplus ${sc.sideLabel} order ${sc.chainOrderObj.id}`, 'info');
487
+ const staleSurplusCancels = [];
488
+ for (const side of [ORDER_TYPES.SELL, ORDER_TYPES.BUY]) {
489
+ const targetCount = side === ORDER_TYPES.SELL ? targetSell : targetBuy;
490
+ let sideOrders = freshParsed.filter((x) => x.parsed.type === side);
491
+ const sideLabel = side === ORDER_TYPES.SELL ? 'SELL' : 'BUY';
492
+ if (sideOrders.length > targetCount) {
493
+ // Sort by chain ID for deterministic cancellation order.
494
+ sideOrders = sideOrders.sort((a, b) => (a.chain.id || '').localeCompare(b.chain.id || ''));
495
+ let cancelLimit = sideOrders.length - targetCount;
496
+ for (const so of sideOrders) {
497
+ if (cancelLimit <= 0)
498
+ break;
499
+ if (!gridOrderIds.has(so.chain.id)) {
500
+ staleSurplusCancels.push({ chainOrderObj: so.chain, sideLabel });
501
+ cancelLimit--;
502
+ }
503
+ }
511
504
  }
512
- catch (e) {
513
- logger?.log?.(`Startup: Failed to cancel surplus ${sc.sideLabel} ${sc.chainOrderObj.id}: ${getErrorMessage(e)}`, 'warn');
505
+ }
506
+ let cancelledSellCount = 0;
507
+ let cancelledBuyCount = 0;
508
+ if (staleSurplusCancels.length > 0) {
509
+ for (const sc of staleSurplusCancels) {
510
+ try {
511
+ await _cancelChainOrder({
512
+ chainOrders,
513
+ account,
514
+ privateKey,
515
+ manager,
516
+ chainOrderId: sc.chainOrderObj.id,
517
+ dryRun,
518
+ chainOrderObj: sc.chainOrderObj,
519
+ releaseUntrackedFunds: true,
520
+ });
521
+ if (sc.sideLabel === 'SELL')
522
+ cancelledSellCount++;
523
+ else
524
+ cancelledBuyCount++;
525
+ logger?.log?.(`Startup: Cancelled stale surplus ${sc.sideLabel} order ${sc.chainOrderObj.id}`, 'info');
526
+ }
527
+ catch (e) {
528
+ logger?.log?.(`Startup: Failed to cancel surplus ${sc.sideLabel} ${sc.chainOrderObj.id}: ${getErrorMessage(e)}`, 'warn');
529
+ }
514
530
  }
515
531
  }
532
+ finalChainSellCount = freshParsed.filter((x) => x.parsed.type === ORDER_TYPES.SELL).length - cancelledSellCount;
533
+ finalChainBuyCount = freshParsed.filter((x) => x.parsed.type === ORDER_TYPES.BUY).length - cancelledBuyCount;
516
534
  }
517
- finalChainSellCount = freshParsed.filter((x) => x.parsed.type === ORDER_TYPES.SELL).length - cancelledSellCount;
518
- finalChainBuyCount = freshParsed.filter((x) => x.parsed.type === ORDER_TYPES.BUY).length - cancelledBuyCount;
535
+ }
536
+ catch (err) {
537
+ logger?.log?.(`Startup: Failed to refresh final chain counts: ${getErrorMessage(err)}`, 'warn');
519
538
  }
520
539
  }
521
- catch (err) {
522
- logger?.log?.(`Startup: Failed to refresh final chain counts: ${getErrorMessage(err)}`, 'warn');
523
- }
540
+ logger && logger.log && logger.log(`Startup reconcile complete: target(sell=${targetSell}, buy=${targetBuy}), chain(sell=${finalChainSellCount}, buy=${finalChainBuyCount}), ` +
541
+ `gridActive(sell=${_countActiveOnGrid(manager, ORDER_TYPES.SELL)}, buy=${_countActiveOnGrid(manager, ORDER_TYPES.BUY)})`, 'info');
542
+ }
543
+ finally {
544
+ if (typeof manager.stopBroadcasting === 'function')
545
+ manager.stopBroadcasting();
524
546
  }
525
- logger && logger.log && logger.log(`Startup reconcile complete: target(sell=${targetSell}, buy=${targetBuy}), chain(sell=${finalChainSellCount}, buy=${finalChainBuyCount}), ` +
526
- `gridActive(sell=${_countActiveOnGrid(manager, ORDER_TYPES.SELL)}, buy=${_countActiveOnGrid(manager, ORDER_TYPES.BUY)})`, 'info');
527
547
  return null;
528
548
  }
529
549
  //# sourceMappingURL=grid_reconcile.js.map