dexbot 1.1.14 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +1 -0
  2. package/dist/credential-daemon.d.ts.map +1 -1
  3. package/dist/credential-daemon.js +6 -3
  4. package/dist/credential-daemon.js.map +1 -1
  5. package/dist/market_adapter/market_adapter.js +1 -1
  6. package/dist/market_adapter/market_adapter.js.map +1 -1
  7. package/dist/modules/bitshares-native/lru_cache.d.ts +20 -0
  8. package/dist/modules/bitshares-native/lru_cache.d.ts.map +1 -0
  9. package/dist/modules/bitshares-native/lru_cache.js +45 -0
  10. package/dist/modules/bitshares-native/lru_cache.js.map +1 -0
  11. package/dist/modules/bitshares-native/resolvers.d.ts +2 -18
  12. package/dist/modules/bitshares-native/resolvers.d.ts.map +1 -1
  13. package/dist/modules/bitshares-native/resolvers.js +2 -41
  14. package/dist/modules/bitshares-native/resolvers.js.map +1 -1
  15. package/dist/modules/bitshares-native/serial/chain_constants.d.ts +1 -0
  16. package/dist/modules/bitshares-native/serial/chain_constants.d.ts.map +1 -1
  17. package/dist/modules/bitshares-native/serial/chain_constants.js +3 -0
  18. package/dist/modules/bitshares-native/serial/chain_constants.js.map +1 -1
  19. package/dist/modules/bitshares-native/signing_client.d.ts.map +1 -1
  20. package/dist/modules/bitshares-native/signing_client.js +12 -1
  21. package/dist/modules/bitshares-native/signing_client.js.map +1 -1
  22. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  23. package/dist/modules/bitshares-native/subscriptions.js +42 -3
  24. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  25. package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
  26. package/dist/modules/bitshares-native/transport.js +77 -27
  27. package/dist/modules/bitshares-native/transport.js.map +1 -1
  28. package/dist/modules/bitshares-native/tx/builder.d.ts.map +1 -1
  29. package/dist/modules/bitshares-native/tx/builder.js +26 -16
  30. package/dist/modules/bitshares-native/tx/builder.js.map +1 -1
  31. package/dist/modules/bitshares-native/tx/tx_cache.d.ts +17 -0
  32. package/dist/modules/bitshares-native/tx/tx_cache.d.ts.map +1 -0
  33. package/dist/modules/bitshares-native/tx/tx_cache.js +62 -0
  34. package/dist/modules/bitshares-native/tx/tx_cache.js.map +1 -0
  35. package/dist/modules/bot_settings.d.ts.map +1 -1
  36. package/dist/modules/bot_settings.js +2 -1
  37. package/dist/modules/bot_settings.js.map +1 -1
  38. package/dist/modules/chain_orders.d.ts +4 -1
  39. package/dist/modules/chain_orders.d.ts.map +1 -1
  40. package/dist/modules/chain_orders.js +30 -1
  41. package/dist/modules/chain_orders.js.map +1 -1
  42. package/dist/modules/config.d.ts +6 -0
  43. package/dist/modules/config.d.ts.map +1 -1
  44. package/dist/modules/config.js +11 -0
  45. package/dist/modules/config.js.map +1 -1
  46. package/dist/modules/constants.d.ts +14 -1
  47. package/dist/modules/constants.d.ts.map +1 -1
  48. package/dist/modules/constants.js +64 -6
  49. package/dist/modules/constants.js.map +1 -1
  50. package/dist/modules/credential_policy.d.ts.map +1 -1
  51. package/dist/modules/credential_policy.js +7 -5
  52. package/dist/modules/credential_policy.js.map +1 -1
  53. package/dist/modules/credit_runtime.d.ts.map +1 -1
  54. package/dist/modules/credit_runtime.js +40 -29
  55. package/dist/modules/credit_runtime.js.map +1 -1
  56. package/dist/modules/dexbot_class.d.ts +6 -12
  57. package/dist/modules/dexbot_class.d.ts.map +1 -1
  58. package/dist/modules/dexbot_class.js +155 -81
  59. package/dist/modules/dexbot_class.js.map +1 -1
  60. package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -5
  61. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  62. package/dist/modules/dexbot_maintenance_runtime.js +23 -48
  63. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  64. package/dist/modules/launcher/child_env.d.ts.map +1 -1
  65. package/dist/modules/launcher/child_env.js +1 -0
  66. package/dist/modules/launcher/child_env.js.map +1 -1
  67. package/dist/modules/launcher/launch_modes.d.ts +2 -0
  68. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  69. package/dist/modules/launcher/launch_modes.js +7 -6
  70. package/dist/modules/launcher/launch_modes.js.map +1 -1
  71. package/dist/modules/order/accounting.d.ts +8 -2
  72. package/dist/modules/order/accounting.d.ts.map +1 -1
  73. package/dist/modules/order/accounting.js +64 -26
  74. package/dist/modules/order/accounting.js.map +1 -1
  75. package/dist/modules/order/async_lock.d.ts +12 -4
  76. package/dist/modules/order/async_lock.d.ts.map +1 -1
  77. package/dist/modules/order/async_lock.js +27 -5
  78. package/dist/modules/order/async_lock.js.map +1 -1
  79. package/dist/modules/order/grid.d.ts +434 -475
  80. package/dist/modules/order/grid.d.ts.map +1 -1
  81. package/dist/modules/order/grid.js +1612 -1592
  82. package/dist/modules/order/grid.js.map +1 -1
  83. package/dist/modules/order/grid_reconcile.d.ts +3 -73
  84. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  85. package/dist/modules/order/grid_reconcile.js +11 -843
  86. package/dist/modules/order/grid_reconcile.js.map +1 -1
  87. package/dist/modules/order/grid_reconcile_internal.d.ts +8 -0
  88. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -0
  89. package/dist/modules/order/grid_reconcile_internal.js +782 -0
  90. package/dist/modules/order/grid_reconcile_internal.js.map +1 -0
  91. package/dist/modules/order/logger.d.ts +1 -1
  92. package/dist/modules/order/logger.d.ts.map +1 -1
  93. package/dist/modules/order/logger.js +26 -2
  94. package/dist/modules/order/logger.js.map +1 -1
  95. package/dist/modules/order/manager.d.ts +118 -15
  96. package/dist/modules/order/manager.d.ts.map +1 -1
  97. package/dist/modules/order/manager.js +206 -236
  98. package/dist/modules/order/manager.js.map +1 -1
  99. package/dist/modules/order/runner.js +2 -2
  100. package/dist/modules/order/runner.js.map +1 -1
  101. package/dist/modules/order/strategy.js +3 -3
  102. package/dist/modules/order/strategy.js.map +1 -1
  103. package/dist/modules/order/sync_engine.d.ts +3 -4
  104. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  105. package/dist/modules/order/sync_engine.js +85 -32
  106. package/dist/modules/order/sync_engine.js.map +1 -1
  107. package/dist/modules/order/utils/order.d.ts +17 -0
  108. package/dist/modules/order/utils/order.d.ts.map +1 -1
  109. package/dist/modules/order/utils/order.js +81 -24
  110. package/dist/modules/order/utils/order.js.map +1 -1
  111. package/dist/modules/order/utils/system.js +4 -4
  112. package/dist/modules/order/utils/system.js.map +1 -1
  113. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  114. package/dist/modules/order/utils/validate.js +6 -8
  115. package/dist/modules/order/utils/validate.js.map +1 -1
  116. package/dist/modules/types.d.ts +7 -12
  117. package/dist/modules/types.d.ts.map +1 -1
  118. package/dist/scripts/print_grid.js +3 -3
  119. package/dist/scripts/print_grid.js.map +1 -1
  120. package/dist/scripts/test-credit-renewal.js +1 -1
  121. package/dist/scripts/test-credit-renewal.js.map +1 -1
  122. package/dist/unlock.d.ts.map +1 -1
  123. package/dist/unlock.js +26 -12
  124. package/dist/unlock.js.map +1 -1
  125. package/package.json +1 -1
@@ -11,8 +11,7 @@
11
11
  *
12
12
  * SECTION 1: EXTERNAL DEPENDENCIES
13
13
  * SECTION 2: COW REBALANCE ENGINE
14
- * SECTION 3: STATE MANAGER
15
- * SECTION 4: ORDER MANAGER CLASS
14
+ * SECTION 3: ORDER MANAGER CLASS
16
15
  * ===============================================================================
17
16
  */
18
17
  // ===============================================================================
@@ -29,7 +28,7 @@ const AsyncLock = require('./async_lock');
29
28
  const Accountant = require('./accounting');
30
29
  const StrategyEngine = require('./strategy');
31
30
  const SyncEngine = require('./sync_engine');
32
- const Grid = require('./grid');
31
+ const { calculateCurrentSpread, checkSpreadCondition, checkGridHealth } = require('./grid');
33
32
  const Format = require('./format');
34
33
  const { toFiniteNumber, isValidNumber } = Format;
35
34
  // ===============================================================================
@@ -57,7 +56,7 @@ const { toFiniteNumber, isValidNumber } = Format;
57
56
  // 1. Master grid NEVER modified during planning (immutable during rebalance)
58
57
  // 2. Fills that arrive during rebalance are QUEUED, not lost
59
58
  // 3. Working grid is DISPOSABLE - if rebalance fails, discard it
60
- // 4. Only ONE rebalance plan active at a time (StateManager.rebalance)
59
+ // 4. Only ONE rebalance plan active at a time (_rebalanceState)
61
60
  // 5. Staleness detection aborts if master changes (fills, manual commands)
62
61
  //
63
62
  // FILL HANDLING DURING REBALANCE:
@@ -138,186 +137,7 @@ class COWRebalanceEngine {
138
137
  }
139
138
  }
140
139
  // ===============================================================================
141
- // SECTION 3: STATE MANAGER
142
- // ===============================================================================
143
- class StateManager {
144
- logger;
145
- rebalance;
146
- recovery;
147
- gridRegen;
148
- bootstrap;
149
- broadcast;
150
- signals;
151
- pipeline;
152
- constructor(options = {}) {
153
- this.logger = options.logger || null;
154
- this.reset();
155
- }
156
- reset() {
157
- this.rebalance = {
158
- state: REBALANCE_STATES.NORMAL,
159
- currentWorkingGrid: null
160
- };
161
- this.recovery = {
162
- attemptCount: 0,
163
- lastAttemptAt: 0,
164
- inFlight: false,
165
- lastFailureAt: 0
166
- };
167
- this.gridRegen = {
168
- buy: { armed: true, lastTriggeredAt: 0 },
169
- sell: { armed: true, lastTriggeredAt: 0 }
170
- };
171
- this.bootstrap = {
172
- isBootstrapping: false
173
- };
174
- this.broadcast = {
175
- isBroadcasting: false,
176
- startedAt: 0,
177
- };
178
- this.signals = {
179
- lastIllegalState: null,
180
- lastAccountingFailure: null
181
- };
182
- this.pipeline = {
183
- blockedSince: null,
184
- recoveryAttempted: false
185
- };
186
- }
187
- getRebalanceState() {
188
- return this.rebalance.state;
189
- }
190
- setRebalanceState(state) {
191
- this.rebalance.state = state;
192
- this.logger?.log(`[COW] Rebalance state: ${state}`, 'debug');
193
- }
194
- isRebalancing() {
195
- return this.rebalance.state === REBALANCE_STATES.REBALANCING;
196
- }
197
- isBroadcasting() {
198
- return this.rebalance.state === REBALANCE_STATES.BROADCASTING;
199
- }
200
- setWorkingGrid(workingGrid) {
201
- this.rebalance.currentWorkingGrid = workingGrid;
202
- }
203
- getWorkingGrid() {
204
- return this.rebalance.currentWorkingGrid;
205
- }
206
- clearWorkingGrid() {
207
- this.rebalance.currentWorkingGrid = null;
208
- }
209
- recordRecoveryAttempt() {
210
- this.recovery.attemptCount++;
211
- this.recovery.lastAttemptAt = Date.now();
212
- this.recovery.inFlight = true;
213
- }
214
- completeRecovery(success) {
215
- this.recovery.inFlight = false;
216
- if (!success) {
217
- this.recovery.lastFailureAt = Date.now();
218
- }
219
- }
220
- isRecoveryInFlight() {
221
- return this.recovery.inFlight;
222
- }
223
- getRecoveryStats() {
224
- return { ...this.recovery };
225
- }
226
- isSideArmed(side) {
227
- return this.gridRegen[side]?.armed ?? false;
228
- }
229
- disarmSide(side) {
230
- if (this.gridRegen[side]) {
231
- this.gridRegen[side].armed = false;
232
- this.gridRegen[side].lastTriggeredAt = Date.now();
233
- }
234
- }
235
- armSide(side) {
236
- if (this.gridRegen[side]) {
237
- this.gridRegen[side].armed = true;
238
- }
239
- }
240
- startBootstrap() {
241
- this.bootstrap.isBootstrapping = true;
242
- this.logger?.log('[BOOTSTRAP] Started', 'debug');
243
- }
244
- finishBootstrap() {
245
- this.bootstrap.isBootstrapping = false;
246
- this.logger?.log('[BOOTSTRAP] Finished', 'debug');
247
- }
248
- isBootstrapping() {
249
- return this.bootstrap.isBootstrapping;
250
- }
251
- startBroadcasting() {
252
- this.broadcast.isBroadcasting = true;
253
- this.broadcast.startedAt = Date.now();
254
- this.logger?.log?.('[BROADCAST] Flag set — fill processing will be deferred until stopBroadcasting()', 'debug');
255
- }
256
- stopBroadcasting() {
257
- this.broadcast.isBroadcasting = false;
258
- this.broadcast.startedAt = 0;
259
- }
260
- isBroadcastingActive() {
261
- if (this.broadcast.isBroadcasting && this.broadcast.startedAt > 0) {
262
- const elapsed = Date.now() - this.broadcast.startedAt;
263
- if (elapsed > 120000) {
264
- this.logger?.log?.('[BROADCAST] Auto-clearing stale broadcast flag after 120s', 'warn');
265
- this.broadcast.isBroadcasting = false;
266
- this.broadcast.startedAt = 0;
267
- }
268
- }
269
- return this.broadcast.isBroadcasting;
270
- }
271
- setIllegalStateSignal(signal) {
272
- this.signals.lastIllegalState = {
273
- ...signal,
274
- at: Date.now()
275
- };
276
- }
277
- consumeIllegalStateSignal() {
278
- const signal = this.signals.lastIllegalState;
279
- this.signals.lastIllegalState = null;
280
- return signal;
281
- }
282
- setAccountingFailureSignal(signal) {
283
- this.signals.lastAccountingFailure = {
284
- ...signal,
285
- at: Date.now()
286
- };
287
- }
288
- consumeAccountingFailureSignal() {
289
- const signal = this.signals.lastAccountingFailure;
290
- this.signals.lastAccountingFailure = null;
291
- return signal;
292
- }
293
- markPipelineBlocked() {
294
- if (!this.pipeline.blockedSince) {
295
- this.pipeline.blockedSince = Date.now();
296
- }
297
- }
298
- markPipelineClear() {
299
- this.pipeline.blockedSince = null;
300
- this.pipeline.recoveryAttempted = false;
301
- }
302
- getPipelineBlockedDuration() {
303
- return this.pipeline.blockedSince ? Date.now() - this.pipeline.blockedSince : 0;
304
- }
305
- isPipelineBlocked() {
306
- return this.pipeline.blockedSince !== null;
307
- }
308
- getState() {
309
- return {
310
- rebalance: { ...this.rebalance, currentWorkingGrid: null },
311
- recovery: { ...this.recovery },
312
- gridRegen: { ...this.gridRegen },
313
- bootstrap: { ...this.bootstrap },
314
- broadcast: { ...this.broadcast },
315
- pipeline: { ...this.pipeline }
316
- };
317
- }
318
- }
319
- // ===============================================================================
320
- // SECTION 4: ORDER MANAGER CLASS
140
+ // SECTION 3: ORDER MANAGER CLASS
321
141
  // ===============================================================================
322
142
  class OrderManager {
323
143
  config;
@@ -329,7 +149,14 @@ class OrderManager {
329
149
  accountant;
330
150
  strategy;
331
151
  sync;
332
- _state;
152
+ _rebalanceState;
153
+ _bootstrapping;
154
+ _broadcastingFlag;
155
+ _broadcastingStartedAt;
156
+ _illegalStateSignal;
157
+ _accountingFailureSignal;
158
+ _recoveryStateValue;
159
+ _gridRegenStateValue;
333
160
  _ordersByState;
334
161
  _ordersByType;
335
162
  targetSpreadCount;
@@ -354,15 +181,19 @@ class OrderManager {
354
181
  _recentlyRotatedOrderIds;
355
182
  _gridSidesUpdated;
356
183
  _pauseFundRecalc;
184
+ _pauseFundRecalcWatchdog;
357
185
  _pauseRecalcLogging;
186
+ _pauseRecalcLoggingWatchdog;
358
187
  _throwOnIllegalState;
359
188
  _pipelineBlockedSince;
360
189
  _recoveryAttempted;
190
+ _syncGeneration;
361
191
  _gridVersion;
362
192
  _gridPersistenceSuspendedReason;
363
193
  _pendingBroadcasts;
364
194
  _committedOrderIds;
365
195
  _gridDirtyAt;
196
+ _pendingRecovery;
366
197
  _metrics;
367
198
  _currentWorkingGrid;
368
199
  _cowEngine;
@@ -388,7 +219,23 @@ class OrderManager {
388
219
  this.accountant = new Accountant(this);
389
220
  this.strategy = new StrategyEngine(this);
390
221
  this.sync = new SyncEngine(this);
391
- this._state = new StateManager({ logger: this.logger });
222
+ this._rebalanceState = REBALANCE_STATES.NORMAL;
223
+ this._bootstrapping = false;
224
+ this._broadcastingFlag = false;
225
+ this._broadcastingStartedAt = 0;
226
+ this._illegalStateSignal = null;
227
+ this._accountingFailureSignal = null;
228
+ this._recoveryStateValue = {
229
+ phase: 'idle',
230
+ attemptCount: 0,
231
+ lastAttemptAt: 0,
232
+ inFlight: false,
233
+ lastFailureAt: 0
234
+ };
235
+ this._gridRegenStateValue = {
236
+ buy: { armed: true, lastTriggeredAt: 0 },
237
+ sell: { armed: true, lastTriggeredAt: 0 }
238
+ };
392
239
  // Index Sets use mutable mutation patterns controlled via _applyOrderUpdate
393
240
  // These are private implementation details and must NOT be mutated directly
394
241
  // All external code must go through the COW pipeline
@@ -423,7 +270,7 @@ class OrderManager {
423
270
  // Level 2: _gridLock (grid mutations)
424
271
  // Level 3: _syncLock (sync operations)
425
272
  // Level 4: _fundLock (fund operations — innermost)
426
- // WARNING: AsyncLock is NOT reentrant.
273
+ // Note: AsyncLock IS re-entrant (acquire detects nested calls via _holding).
427
274
  this._divergenceLock = new AsyncLock({ level: 0 });
428
275
  this._fillProcessingLock = new AsyncLock({ level: 1 });
429
276
  this._gridLock = new AsyncLock({ level: 2 });
@@ -432,15 +279,19 @@ class OrderManager {
432
279
  this._recentlyRotatedOrderIds = new Set();
433
280
  this._gridSidesUpdated = new Set();
434
281
  this._pauseFundRecalc = 0;
282
+ this._pauseFundRecalcWatchdog = null;
435
283
  this._pauseRecalcLogging = false;
284
+ this._pauseRecalcLoggingWatchdog = null;
436
285
  this._throwOnIllegalState = false;
437
286
  this._pipelineBlockedSince = null;
438
287
  this._recoveryAttempted = false;
288
+ this._syncGeneration = 0;
439
289
  this._gridVersion = 0;
440
290
  this._gridPersistenceSuspendedReason = null;
441
291
  this._pendingBroadcasts = new Map();
442
292
  this._committedOrderIds = new Set();
443
293
  this._gridDirtyAt = null;
294
+ this._pendingRecovery = null;
444
295
  this._metrics = {
445
296
  fundRecalcCount: 0,
446
297
  invariantViolations: { buy: 0, sell: 0 },
@@ -450,7 +301,8 @@ class OrderManager {
450
301
  lastSyncDurationMs: 0,
451
302
  metricsStartTime: Date.now()
452
303
  };
453
- this._state.startBootstrap();
304
+ this._bootstrapping = true;
305
+ this.logger?.log('[BOOTSTRAP] Started', 'debug');
454
306
  this._currentWorkingGrid = null;
455
307
  this._cowEngine = null;
456
308
  this._cleanExpiredLocks();
@@ -468,43 +320,77 @@ class OrderManager {
468
320
  }
469
321
  _clearWorkingGridRef() {
470
322
  this._currentWorkingGrid = null;
471
- this._state.clearWorkingGrid();
472
- this._state.setRebalanceState(REBALANCE_STATES.NORMAL);
323
+ this._rebalanceState = REBALANCE_STATES.NORMAL;
473
324
  }
474
325
  _setRebalanceState(state) {
475
- this._state.setRebalanceState(state);
326
+ this._rebalanceState = state;
327
+ this.logger?.log(`[COW] Rebalance state: ${state}`, 'debug');
476
328
  }
477
329
  /**
478
330
  * @returns {boolean}
479
331
  */
480
332
  isRebalancing() {
481
- return this._state.isRebalancing();
333
+ return this._rebalanceState === REBALANCE_STATES.REBALANCING;
482
334
  }
483
335
  /**
484
336
  * @returns {boolean}
485
337
  */
486
338
  isBroadcasting() {
487
- return this._state.isBroadcasting();
339
+ return this._rebalanceState === REBALANCE_STATES.BROADCASTING;
340
+ }
341
+ /**
342
+ * @returns {boolean}
343
+ */
344
+ isBootstrapping() {
345
+ return this._bootstrapping;
488
346
  }
489
347
  /**
348
+ * Whether a broadcast is currently active (no side-effects).
349
+ * @returns {boolean}
350
+ */
351
+ isBroadcastingActive() {
352
+ return this._broadcastingFlag;
353
+ }
354
+ /**
355
+ * Auto-clears a stale broadcast flag after 120s so a hung
356
+ * broadcast cannot permanently block rebalancing.
357
+ * Separated from isBroadcastingActive() so the side-effect only
358
+ * runs from one well-defined location (maintenance logic) instead
359
+ * of from every read call-site.
360
+ * @returns {void}
361
+ */
362
+ _clearStaleBroadcastFlag() {
363
+ if (this._broadcastingFlag && this._broadcastingStartedAt > 0) {
364
+ const elapsed = Date.now() - this._broadcastingStartedAt;
365
+ if (elapsed > 120000) {
366
+ this.logger?.log?.('[BROADCAST] Auto-clearing stale broadcast flag after 120s', 'warn');
367
+ this._broadcastingFlag = false;
368
+ this._broadcastingStartedAt = 0;
369
+ }
370
+ }
371
+ }
372
+ /**
373
+ * Whether the manager is in the middle of a rebalance plan or broadcast.
490
374
  * @returns {boolean}
491
375
  */
492
376
  isPlanningActive() {
493
- return this.isRebalancing() || this.isBroadcasting();
377
+ return this.isRebalancing() || this.isBroadcastingActive();
494
378
  }
495
379
  /**
496
380
  * @returns {void}
497
381
  */
498
382
  startBootstrap() {
499
- this._state.startBootstrap();
383
+ this._bootstrapping = true;
384
+ this.logger?.log('[BOOTSTRAP] Started', 'debug');
500
385
  }
501
386
  /**
502
387
  * @returns {import('./types').BootstrapResult}
503
388
  */
504
389
  finishBootstrap() {
505
390
  const result = { hadDrift: false, driftInfo: null };
506
- if (this._state.isBootstrapping()) {
507
- this._state.finishBootstrap();
391
+ if (this._bootstrapping) {
392
+ this._bootstrapping = false;
393
+ this.logger?.log('[BOOTSTRAP] Finished', 'debug');
508
394
  // Validate fund state at bootstrap completion - if drift exists here,
509
395
  // it's not transient (grid is now stable) and indicates a potential bug
510
396
  const driftCheck = this.checkFundDriftAfterFills();
@@ -522,13 +408,16 @@ class OrderManager {
522
408
  * @returns {void}
523
409
  */
524
410
  startBroadcasting() {
525
- this._state.startBroadcasting();
411
+ this._broadcastingFlag = true;
412
+ this._broadcastingStartedAt = Date.now();
413
+ this.logger?.log?.('[BROADCAST] Flag set — fill processing will be deferred until stopBroadcasting()', 'debug');
526
414
  }
527
415
  /**
528
416
  * @returns {void}
529
417
  */
530
418
  stopBroadcasting() {
531
- this._state.stopBroadcasting();
419
+ this._broadcastingFlag = false;
420
+ this._broadcastingStartedAt = 0;
532
421
  }
533
422
  /**
534
423
  * @returns {void}
@@ -613,7 +502,7 @@ class OrderManager {
613
502
  });
614
503
  }
615
504
  async _setAccountTotals(totals) {
616
- this.accountTotals = { ...(this.accountTotals || {}), ...totals };
505
+ this.accountTotals = { ...(this.accountTotals || {}), ...totals, _lastFetchedAt: Date.now() };
617
506
  if (!this.funds)
618
507
  this.resetFunds();
619
508
  await this._recalculateFunds();
@@ -678,31 +567,96 @@ class OrderManager {
678
567
  await this.accountant.recalculateFunds();
679
568
  }
680
569
  /**
570
+ * Suppress fund recalculation during batch mutations.
571
+ *
572
+ * Use when multiple _applyOrderUpdate calls would each trigger redundant
573
+ * recalculateFunds (e.g. grid load, grid init, fill batch). Must be paired
574
+ * with resumeFundRecalc() in a try/finally block.
575
+ *
576
+ * Supports nesting via depth counter — only the outermost resume triggers
577
+ * the single consolidated recalculation.
578
+ *
579
+ * For bulk mutation sites that also need to suppress debug RECALC logging
580
+ * during the loop, pair this with pauseRecalcLogging()/resumeRecalcLogging().
581
+ *
582
+ * Use independently of pauseRecalcLogging when you need recalc to still run
583
+ * per-operation but want to batch the trigger (e.g. _updateOrdersForSide
584
+ * needs per-call chain-free tracking but suppresses log spam).
585
+ *
681
586
  * @returns {void}
682
587
  */
683
588
  pauseFundRecalc() {
684
589
  this._pauseFundRecalc++;
590
+ // Safety watchdog: if pauseFundRecalc is not resumed within the timeout,
591
+ // force-reset the counter to prevent permanent suppression from a missed
592
+ // finally block. Only set when depth goes from 0 → 1 so only the outermost
593
+ // pause has a watchdog.
594
+ if (this._pauseFundRecalc === 1) {
595
+ if (this._pauseFundRecalcWatchdog) {
596
+ clearTimeout(this._pauseFundRecalcWatchdog);
597
+ }
598
+ this._pauseFundRecalcWatchdog = setTimeout(() => {
599
+ if (this._pauseFundRecalc > 0) {
600
+ this.logger?.log?.(`[MANAGER] pauseFundRecalc safety watchdog: resetting depth from ${this._pauseFundRecalc} to 0`, 'warn');
601
+ this._pauseFundRecalc = 0;
602
+ this._pauseFundRecalcWatchdog = null;
603
+ this._recalculateFunds().catch((err) => {
604
+ this.logger?.log?.(`[MANAGER] Watchdog recalc failed: ${err.message}`, 'error');
605
+ });
606
+ }
607
+ }, TIMING.SAFETY_PAUSE_TIMEOUT_MS);
608
+ }
685
609
  }
686
610
  /**
611
+ * Resume fund recalculation and trigger one consolidated recalculateFunds()
612
+ * when the outermost resume completes.
687
613
  * @returns {Promise<void>}
688
614
  */
689
615
  async resumeFundRecalc() {
690
616
  this._pauseFundRecalc = Math.max(0, this._pauseFundRecalc - 1);
691
617
  if (this._pauseFundRecalc === 0) {
618
+ if (this._pauseFundRecalcWatchdog) {
619
+ clearTimeout(this._pauseFundRecalcWatchdog);
620
+ this._pauseFundRecalcWatchdog = null;
621
+ }
692
622
  await this.recalculateFunds();
693
623
  }
694
624
  }
695
625
  /**
626
+ * Suppress only the debug-level [RECALC] log lines inside recalculateFunds().
627
+ *
628
+ * Lighter than pauseFundRecalc — recalculation still runs, only log output
629
+ * is suppressed. Use when individual _updateOrder calls must update chain-free
630
+ * balances but the iterative debug logging would flood output
631
+ * (e.g. _updateOrdersForSide grid resize loop).
632
+ *
633
+ * When paired with pauseFundRecalc/resumeFundRecalc (grid load/init),
634
+ * both calcs and logging are suppressed during the batch. When used alone
635
+ * (_updateOrdersForSide), recalc runs per-call but logs are suppressed.
636
+ *
696
637
  * @returns {void}
697
638
  */
698
639
  pauseRecalcLogging() {
699
640
  this._pauseRecalcLogging = true;
641
+ if (this._pauseRecalcLoggingWatchdog) {
642
+ clearTimeout(this._pauseRecalcLoggingWatchdog);
643
+ }
644
+ this._pauseRecalcLoggingWatchdog = setTimeout(() => {
645
+ this.logger?.log?.(`[MANAGER] pauseRecalcLogging safety watchdog: forcing resume after ${TIMING.SAFETY_PAUSE_TIMEOUT_MS}ms`, 'warn');
646
+ this._pauseRecalcLogging = false;
647
+ this._pauseRecalcLoggingWatchdog = null;
648
+ }, TIMING.SAFETY_PAUSE_TIMEOUT_MS);
700
649
  }
701
650
  /**
651
+ * Resume recalc debug logging.
702
652
  * @returns {void}
703
653
  */
704
654
  resumeRecalcLogging() {
705
655
  this._pauseRecalcLogging = false;
656
+ if (this._pauseRecalcLoggingWatchdog) {
657
+ clearTimeout(this._pauseRecalcLoggingWatchdog);
658
+ this._pauseRecalcLoggingWatchdog = null;
659
+ }
706
660
  }
707
661
  /**
708
662
  * @param {Array<import('./types').Order>} orders
@@ -817,11 +771,12 @@ class OrderManager {
817
771
  const fatalError = validation.errors.find(e => e.isFatal || e.code === 'ILLEGAL_SPREAD_STATE');
818
772
  if (fatalError) {
819
773
  this.logger.log(fatalError.message, 'error');
820
- this._state.setIllegalStateSignal({
774
+ this._illegalStateSignal = {
821
775
  id: order.id,
822
776
  context,
823
- message: fatalError.message
824
- });
777
+ message: fatalError.message,
778
+ at: Date.now()
779
+ };
825
780
  if (this._throwOnIllegalState) {
826
781
  const err = new Error(fatalError.message);
827
782
  err.code = fatalError.code;
@@ -860,7 +815,7 @@ class OrderManager {
860
815
  if (this._pauseFundRecalc === 0) {
861
816
  await this.recalculateFunds();
862
817
  }
863
- this._markGridDirty(context, id);
818
+ this._markGridDirty();
864
819
  return true;
865
820
  }
866
821
  _syncWorkingGridFromMasterMutation(orderId, context) {
@@ -868,7 +823,7 @@ class OrderManager {
868
823
  return;
869
824
  }
870
825
  try {
871
- this._currentWorkingGrid.markStale(`master mutation during ${this._rebalanceState.toLowerCase()} (${context})`);
826
+ this._currentWorkingGrid.markStale(`master mutation during ${(this._rebalanceState || '').toLowerCase()} (${context})`);
872
827
  this._currentWorkingGrid.syncFromMaster(this.orders, orderId, this._gridVersion);
873
828
  }
874
829
  catch (syncErr) {
@@ -898,11 +853,9 @@ class OrderManager {
898
853
  * other paths (e.g. raw `this.orders.set(...)` from a refactor) should
899
854
  * invoke this explicitly so the dirty-flag invariant still holds.
900
855
  *
901
- * @param {string} [reason='unknown'] - Context label for diagnostics
902
- * @param {string} [orderId=null] - Order that triggered the mutation
903
856
  * @returns {void}
904
857
  */
905
- _markGridDirty(reason = 'unknown', orderId = null) {
858
+ _markGridDirty() {
906
859
  if (this._gridDirtyAt == null) {
907
860
  this._gridDirtyAt = Date.now();
908
861
  }
@@ -974,7 +927,6 @@ class OrderManager {
974
927
  const rebalanceResult = await this.performSafeRebalance(orders, excl);
975
928
  return rebalanceResult;
976
929
  }
977
- const { WorkingGrid } = require('./working_grid');
978
930
  const workingGrid = new WorkingGrid(this.orders, { baseVersion: this._gridVersion });
979
931
  return {
980
932
  actions: [],
@@ -1142,13 +1094,17 @@ class OrderManager {
1142
1094
  * @returns {Object|null} The consumed signal or null
1143
1095
  */
1144
1096
  consumeIllegalStateSignal() {
1145
- return this._state.consumeIllegalStateSignal();
1097
+ const signal = this._illegalStateSignal;
1098
+ this._illegalStateSignal = null;
1099
+ return signal;
1146
1100
  }
1147
1101
  /**
1148
1102
  * @returns {Object|null} The consumed signal or null
1149
1103
  */
1150
1104
  consumeAccountingFailureSignal() {
1151
- return this._state.consumeAccountingFailureSignal();
1105
+ const signal = this._accountingFailureSignal;
1106
+ this._accountingFailureSignal = null;
1107
+ return signal;
1152
1108
  }
1153
1109
  /**
1154
1110
  * @param {Object} BitShares - BitShares API instance
@@ -1156,20 +1112,20 @@ class OrderManager {
1156
1112
  * @returns {Promise<Object>}
1157
1113
  */
1158
1114
  async checkSpreadCondition(BitShares, batchCb) {
1159
- return await Grid.checkSpreadCondition(this, BitShares, batchCb);
1115
+ return await checkSpreadCondition(this, BitShares, batchCb);
1160
1116
  }
1161
1117
  /**
1162
1118
  * @param {Function} batchCb - Batch callback
1163
1119
  * @returns {Promise<Object>}
1164
1120
  */
1165
1121
  async checkGridHealth(batchCb) {
1166
- return await Grid.checkGridHealth(this, batchCb);
1122
+ return await checkGridHealth(this, batchCb);
1167
1123
  }
1168
1124
  /**
1169
1125
  * @returns {Object} Current spread calculation
1170
1126
  */
1171
1127
  calculateCurrentSpread() {
1172
- return Grid.calculateCurrentSpread(this);
1128
+ return calculateCurrentSpread(this);
1173
1129
  }
1174
1130
  /**
1175
1131
  * @returns {import('./types').DriftCheckResult}
@@ -1214,16 +1170,14 @@ class OrderManager {
1214
1170
  if (recoveryInFlight) {
1215
1171
  reasons.push('recovery sync in-flight');
1216
1172
  }
1217
- if (broadcasting || this._state.isBroadcastingActive()) {
1173
+ if (broadcasting || this.isBroadcastingActive()) {
1218
1174
  reasons.push('broadcasting active orders');
1219
1175
  }
1220
1176
  if (reasons.length > 0 && !this._pipelineBlockedSince) {
1221
1177
  this._pipelineBlockedSince = Date.now();
1222
- this._state.markPipelineBlocked();
1223
1178
  }
1224
1179
  else if (reasons.length === 0) {
1225
1180
  this._pipelineBlockedSince = null;
1226
- this._state.markPipelineClear();
1227
1181
  }
1228
1182
  return {
1229
1183
  isEmpty: reasons.length === 0,
@@ -1241,7 +1195,6 @@ class OrderManager {
1241
1195
  return false;
1242
1196
  this.ordersNeedingPriceCorrection = [];
1243
1197
  this._pipelineBlockedSince = null;
1244
- this._state.markPipelineClear();
1245
1198
  return true;
1246
1199
  }
1247
1200
  /**
@@ -1435,6 +1388,13 @@ class OrderManager {
1435
1388
  }
1436
1389
  try {
1437
1390
  if (!skipRecalc) {
1391
+ // If the last accounting failure was due to stale accountTotals,
1392
+ // the commit proceeded without locking funds. Refresh accountTotals
1393
+ // before recalculateFunds() to avoid a false-positive drift recovery.
1394
+ if (this._lastAccountingFailure?.reason === 'stale') {
1395
+ this.logger.log('[COW] Stale accountTotals during commit; refreshing before recalculateFunds to avoid false recovery', 'warn');
1396
+ await this.fetchAccountTotals(this.accountId);
1397
+ }
1438
1398
  await this.recalculateFunds();
1439
1399
  }
1440
1400
  const duration = Date.now() - startTime;
@@ -1445,8 +1405,10 @@ class OrderManager {
1445
1405
  }
1446
1406
  catch (recalcErr) {
1447
1407
  this.logger.log(`[COW] Fund recalculation failed post-commit: ${recalcErr.message}`, 'error');
1448
- this._recoveryState = this._recoveryState || {};
1449
- this._recoveryState.lastFailureAt = Date.now();
1408
+ if (!this._recoveryStateValue) {
1409
+ this._recoveryStateValue = { phase: 'idle', attemptCount: 0, lastAttemptAt: 0, inFlight: false, lastFailureAt: 0 };
1410
+ }
1411
+ this._recoveryStateValue.lastFailureAt = Date.now();
1450
1412
  }
1451
1413
  finally {
1452
1414
  this._clearWorkingGridRef();
@@ -1461,7 +1423,7 @@ class OrderManager {
1461
1423
  validateGridStateForPersistence(options = {}) {
1462
1424
  const result = validateGridForPersistence(this.orders, this.accountTotals);
1463
1425
  const allowBootstrapTransient = options.allowBootstrapTransient !== false;
1464
- if (!result.isValid && allowBootstrapTransient && this._state.isBootstrapping()) {
1426
+ if (!result.isValid && allowBootstrapTransient && this._bootstrapping) {
1465
1427
  this.logger.log(`[BOOTSTRAP] Transient state (expected): ${result.reason}`, 'debug');
1466
1428
  return { isValid: true, reason: null };
1467
1429
  }
@@ -1521,7 +1483,14 @@ class OrderManager {
1521
1483
  getMetrics() {
1522
1484
  return {
1523
1485
  ...this._metrics,
1524
- state: this._state.getState(),
1486
+ state: {
1487
+ rebalance: { state: this._rebalanceState, currentWorkingGrid: null },
1488
+ recovery: { ...this._recoveryStateValue },
1489
+ gridRegen: { ...this._gridRegenStateValue },
1490
+ bootstrap: { isBootstrapping: this._bootstrapping },
1491
+ broadcast: { isBroadcasting: this._broadcastingFlag, startedAt: this._broadcastingStartedAt },
1492
+ pipeline: { blockedSince: this._pipelineBlockedSince, recoveryAttempted: this._recoveryAttempted }
1493
+ },
1525
1494
  currentTime: Date.now()
1526
1495
  };
1527
1496
  }
@@ -1550,52 +1519,53 @@ class OrderManager {
1550
1519
  comparePrecisions
1551
1520
  });
1552
1521
  }
1553
- get _rebalanceState() {
1554
- return this._state.getRebalanceState();
1555
- }
1556
- set _rebalanceState(value) {
1557
- this._state.setRebalanceState(value);
1558
- }
1559
1522
  get _lastIllegalState() {
1560
- return this._state.signals?.lastIllegalState || null;
1523
+ return this._illegalStateSignal || null;
1561
1524
  }
1562
1525
  set _lastIllegalState(value) {
1563
1526
  if (value) {
1564
- this._state.setIllegalStateSignal(value);
1527
+ this._illegalStateSignal = {
1528
+ ...value,
1529
+ at: Date.now()
1530
+ };
1565
1531
  }
1566
1532
  }
1567
1533
  get _lastAccountingFailure() {
1568
- return this._state.signals?.lastAccountingFailure || null;
1534
+ return this._accountingFailureSignal || null;
1569
1535
  }
1570
1536
  set _lastAccountingFailure(value) {
1571
1537
  if (value) {
1572
- this._state.setAccountingFailureSignal(value);
1538
+ this._accountingFailureSignal = {
1539
+ ...value,
1540
+ at: Date.now()
1541
+ };
1573
1542
  }
1574
1543
  }
1575
1544
  get _recoveryState() {
1576
- return this._state.recovery;
1545
+ return this._recoveryStateValue;
1577
1546
  }
1578
1547
  set _recoveryState(value) {
1579
1548
  const fallback = {
1549
+ phase: 'idle',
1580
1550
  attemptCount: 0,
1581
1551
  lastAttemptAt: 0,
1582
1552
  inFlight: false,
1583
1553
  lastFailureAt: 0,
1584
1554
  structuralResyncRequested: false
1585
1555
  };
1586
- this._state.recovery = {
1556
+ this._recoveryStateValue = {
1587
1557
  ...fallback,
1588
1558
  ...(value && typeof value === 'object' ? value : {})
1589
1559
  };
1590
1560
  }
1591
1561
  get _gridRegenState() {
1592
- return this._state.gridRegen;
1562
+ return this._gridRegenStateValue;
1593
1563
  }
1594
1564
  set _gridRegenState(value) {
1595
1565
  if (!value || typeof value !== 'object')
1596
1566
  return;
1597
1567
  const defaultSide = { armed: true, lastTriggeredAt: 0 };
1598
- this._state.gridRegen = {
1568
+ this._gridRegenStateValue = {
1599
1569
  buy: { ...defaultSide, ...(value.buy || {}) },
1600
1570
  sell: { ...defaultSide, ...(value.sell || {}) }
1601
1571
  };