dexbot 1.1.13 → 1.2.0

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 (127) hide show
  1. package/README.md +1 -0
  2. package/dist/market_adapter/market_adapter.js +1 -1
  3. package/dist/market_adapter/market_adapter.js.map +1 -1
  4. package/dist/modules/bitshares-native/lru_cache.d.ts +20 -0
  5. package/dist/modules/bitshares-native/lru_cache.d.ts.map +1 -0
  6. package/dist/modules/bitshares-native/lru_cache.js +45 -0
  7. package/dist/modules/bitshares-native/lru_cache.js.map +1 -0
  8. package/dist/modules/bitshares-native/resolvers.d.ts +2 -18
  9. package/dist/modules/bitshares-native/resolvers.d.ts.map +1 -1
  10. package/dist/modules/bitshares-native/resolvers.js +2 -41
  11. package/dist/modules/bitshares-native/resolvers.js.map +1 -1
  12. package/dist/modules/bitshares-native/serial/chain_constants.d.ts +1 -0
  13. package/dist/modules/bitshares-native/serial/chain_constants.d.ts.map +1 -1
  14. package/dist/modules/bitshares-native/serial/chain_constants.js +3 -0
  15. package/dist/modules/bitshares-native/serial/chain_constants.js.map +1 -1
  16. package/dist/modules/bitshares-native/signing_client.d.ts.map +1 -1
  17. package/dist/modules/bitshares-native/signing_client.js +12 -1
  18. package/dist/modules/bitshares-native/signing_client.js.map +1 -1
  19. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  20. package/dist/modules/bitshares-native/subscriptions.js +42 -3
  21. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  22. package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
  23. package/dist/modules/bitshares-native/transport.js +77 -27
  24. package/dist/modules/bitshares-native/transport.js.map +1 -1
  25. package/dist/modules/bitshares-native/tx/builder.d.ts.map +1 -1
  26. package/dist/modules/bitshares-native/tx/builder.js +26 -16
  27. package/dist/modules/bitshares-native/tx/builder.js.map +1 -1
  28. package/dist/modules/bitshares-native/tx/tx_cache.d.ts +17 -0
  29. package/dist/modules/bitshares-native/tx/tx_cache.d.ts.map +1 -0
  30. package/dist/modules/bitshares-native/tx/tx_cache.js +62 -0
  31. package/dist/modules/bitshares-native/tx/tx_cache.js.map +1 -0
  32. package/dist/modules/bot_settings.d.ts.map +1 -1
  33. package/dist/modules/bot_settings.js +2 -1
  34. package/dist/modules/bot_settings.js.map +1 -1
  35. package/dist/modules/chain_orders.d.ts +4 -1
  36. package/dist/modules/chain_orders.d.ts.map +1 -1
  37. package/dist/modules/chain_orders.js +30 -1
  38. package/dist/modules/chain_orders.js.map +1 -1
  39. package/dist/modules/config.d.ts +6 -0
  40. package/dist/modules/config.d.ts.map +1 -1
  41. package/dist/modules/config.js +11 -0
  42. package/dist/modules/config.js.map +1 -1
  43. package/dist/modules/constants.d.ts +17 -1
  44. package/dist/modules/constants.d.ts.map +1 -1
  45. package/dist/modules/constants.js +84 -18
  46. package/dist/modules/constants.js.map +1 -1
  47. package/dist/modules/credit_runtime.d.ts.map +1 -1
  48. package/dist/modules/credit_runtime.js +40 -29
  49. package/dist/modules/credit_runtime.js.map +1 -1
  50. package/dist/modules/dexbot_class.d.ts +16 -12
  51. package/dist/modules/dexbot_class.d.ts.map +1 -1
  52. package/dist/modules/dexbot_class.js +183 -78
  53. package/dist/modules/dexbot_class.js.map +1 -1
  54. package/dist/modules/dexbot_credential_client.d.ts +1 -0
  55. package/dist/modules/dexbot_credential_client.d.ts.map +1 -1
  56. package/dist/modules/dexbot_credential_client.js +15 -6
  57. package/dist/modules/dexbot_credential_client.js.map +1 -1
  58. package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -5
  59. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  60. package/dist/modules/dexbot_maintenance_runtime.js +78 -42
  61. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  62. package/dist/modules/key_store.d.ts.map +1 -1
  63. package/dist/modules/key_store.js +2 -0
  64. package/dist/modules/key_store.js.map +1 -1
  65. package/dist/modules/launcher/child_env.d.ts.map +1 -1
  66. package/dist/modules/launcher/child_env.js +1 -0
  67. package/dist/modules/launcher/child_env.js.map +1 -1
  68. package/dist/modules/launcher/launch_modes.d.ts +2 -0
  69. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  70. package/dist/modules/launcher/launch_modes.js +7 -6
  71. package/dist/modules/launcher/launch_modes.js.map +1 -1
  72. package/dist/modules/node_manager.d.ts +9 -0
  73. package/dist/modules/node_manager.d.ts.map +1 -1
  74. package/dist/modules/node_manager.js +35 -17
  75. package/dist/modules/node_manager.js.map +1 -1
  76. package/dist/modules/order/accounting.d.ts.map +1 -1
  77. package/dist/modules/order/accounting.js +56 -13
  78. package/dist/modules/order/accounting.js.map +1 -1
  79. package/dist/modules/order/async_lock.d.ts +33 -4
  80. package/dist/modules/order/async_lock.d.ts.map +1 -1
  81. package/dist/modules/order/async_lock.js +69 -7
  82. package/dist/modules/order/async_lock.js.map +1 -1
  83. package/dist/modules/order/grid.d.ts +435 -450
  84. package/dist/modules/order/grid.d.ts.map +1 -1
  85. package/dist/modules/order/grid.js +1621 -1548
  86. package/dist/modules/order/grid.js.map +1 -1
  87. package/dist/modules/order/grid_reconcile.d.ts +3 -73
  88. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  89. package/dist/modules/order/grid_reconcile.js +11 -843
  90. package/dist/modules/order/grid_reconcile.js.map +1 -1
  91. package/dist/modules/order/grid_reconcile_internal.d.ts +8 -0
  92. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -0
  93. package/dist/modules/order/grid_reconcile_internal.js +771 -0
  94. package/dist/modules/order/grid_reconcile_internal.js.map +1 -0
  95. package/dist/modules/order/logger.d.ts +1 -1
  96. package/dist/modules/order/logger.d.ts.map +1 -1
  97. package/dist/modules/order/logger.js +26 -2
  98. package/dist/modules/order/logger.js.map +1 -1
  99. package/dist/modules/order/manager.d.ts +40 -3
  100. package/dist/modules/order/manager.d.ts.map +1 -1
  101. package/dist/modules/order/manager.js +97 -15
  102. package/dist/modules/order/manager.js.map +1 -1
  103. package/dist/modules/order/runner.js +2 -2
  104. package/dist/modules/order/runner.js.map +1 -1
  105. package/dist/modules/order/strategy.js +3 -3
  106. package/dist/modules/order/strategy.js.map +1 -1
  107. package/dist/modules/order/sync_engine.d.ts +1 -1
  108. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  109. package/dist/modules/order/sync_engine.js +121 -29
  110. package/dist/modules/order/sync_engine.js.map +1 -1
  111. package/dist/modules/order/utils/order.d.ts +17 -0
  112. package/dist/modules/order/utils/order.d.ts.map +1 -1
  113. package/dist/modules/order/utils/order.js +56 -19
  114. package/dist/modules/order/utils/order.js.map +1 -1
  115. package/dist/modules/order/utils/system.js +4 -4
  116. package/dist/modules/order/utils/system.js.map +1 -1
  117. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  118. package/dist/modules/order/utils/validate.js +6 -8
  119. package/dist/modules/order/utils/validate.js.map +1 -1
  120. package/dist/scripts/print_grid.js +3 -3
  121. package/dist/scripts/print_grid.js.map +1 -1
  122. package/dist/scripts/test-credit-renewal.js +1 -1
  123. package/dist/scripts/test-credit-renewal.js.map +1 -1
  124. package/dist/unlock.d.ts.map +1 -1
  125. package/dist/unlock.js +26 -12
  126. package/dist/unlock.js.map +1 -1
  127. package/package.json +14 -4
@@ -1,838 +1,12 @@
1
1
  "use strict";
2
- /**
3
- * modules/order/grid_reconcile.ts - Grid Reconciliation Module
4
- *
5
- * Reconciles the local grid against the on-chain state. Used both at startup
6
- * (resume-from-persisted-grid) and from the maintenance runtime's targeted
7
- * drift reconciliation path.
8
- *
9
- * Renamed from startup_reconcile.ts — the function is a general grid/chain
10
- * reconciler, not a startup-only concern; maintenance depends on the same
11
- * logic. See modules/dexbot_maintenance_runtime.ts for the runtime caller.
12
- *
13
- * Purpose:
14
- * - Resume previously persisted grids from storage
15
- * - Match persisted grids with current blockchain state
16
- * - Reconcile VIRTUAL orders with actual open orders
17
- * - Activate appropriate orders based on blockchain state
18
- * - Detect and handle partial order dust
19
- * - Trigger rebalancing when needed
20
- *
21
- * ===============================================================================
22
- * EXPORTS (3 functions)
23
- * ===============================================================================
24
- *
25
- * PUBLIC FUNCTIONS:
26
- * 1. reconcileGridOrders(manager, logger) - Main reconciliation function (async)
27
- * Reconciles blockchain orders with persisted grid, activates as needed
28
- * Handles partial orders and triggers rebalancing if dust detected
29
- * Returns null or rebalance result
30
- *
31
- * 2. attemptResumePersistedGridByPriceMatch(manager, persistedGrid) - Resume from storage (async)
32
- * Matches persisted grid to current market state using price proximity
33
- * Validates grid structure, locks/activates orders as appropriate
34
- * Returns { success, reason, activatedIds } or { success: false, reason }
35
- *
36
- * 3. decideStartupGridAction(manager, chainOrders, persistedGrid) - Determine startup action
37
- * Decides whether to resume persisted grid, create new grid, or abort
38
- * Returns { action, reason } where action is 'resume', 'create_new', or 'abort'
39
- *
40
- * INTERNAL HELPERS:
41
- * 4. _countActiveOnGrid(manager, type) - Count active/partial orders by type
42
- * 5. _pickVirtualSlotsToActivate(manager, type, count) - Pick virtual slots for activation
43
- * 6. _isGridEdgeFullyActive(manager, orderType, updateCount) - Check if grid edge is fully active on one side
44
- * 7. _findLargestOrder(unmatchedOrders, updateCount) - Find the largest unmatched chain order
45
- * 8. _cancelLargestOrder({...}) - Cancel the largest unmatched chain order (async)
46
- * 9. _createOrderFromGrid({...}) - Create a new on-chain order from a grid slot (async)
47
- * 10. _cancelChainOrder({...}) - Cancel a specific on-chain order (async)
48
- * 11. _recoverStartupSyncFailure({...}) - Handle sync failure recovery at startup (async)
49
- * 12. _executeStartupUpdateBatch({...}) - Execute startup updates in one batch (async)
50
- * 13. _executeStartupSingleUpdate({...}) - Execute one startup update (async)
51
- * 14. _executeStartupSequentialUpdateFallback({...}) - Sequential backup after batch retries (async)
52
- * 15. _reconcileStartupSide({...}) - Reconcile one side (buy/sell) against chain at startup (async)
53
- *
54
- * ===============================================================================
55
- *
56
- * STARTUP FLOW:
57
- * 1. Load persisted grid from storage (if exists)
58
- * 2. Call decideStartupGridAction() to determine action
59
- * 3. If resume: attemptResumePersistedGridByPriceMatch()
60
- * 4. Call reconcileGridOrders() to sync with blockchain
61
- * 5. Activate appropriate orders
62
- * 6. Check for dust partials and trigger rebalancing if needed
63
- *
64
- * ===============================================================================
65
- */
66
- const { ORDER_TYPES, ORDER_STATES, TIMING, BTS_PRECISION } = require('../constants');
67
- const { getMinAbsoluteOrderSize, getAssetFees, blockchainToFloat, calculatePriceTolerance } = require('./utils/math');
68
- const { isOrderPlaced, parseChainOrder, buildCreateOrderArgs, isOrderOnChain, buildOutsideInPairGroups, extractBatchOperationResults } = require('./utils/order');
2
+ const { _countActiveOnGrid, _pickVirtualSlotsToActivate, _getStartupSideComparators, _isGridEdgeFullyActive, _findLargestOrder, _cancelLargestOrder, _createOrderFromGrid, _cancelChainOrder, _recoverStartupSyncFailure, _refreshStartupUpdatePlans, _prepareStartupUpdatePlan, _finalizeStartupUpdate, _executeStartupUpdateBatch, _executeStartupSingleUpdate, _executeStartupSequentialUpdateFallback, _createStartupOrderWithHandling, _extractBatchOperationResults, _resolveGroupRecovery, _executeStartupCreateGroupBatch, _buildOutsideInCreateGroups, _executePlannedStartupCreates, _reconcileStartupSide, } = require('./grid_reconcile_internal');
3
+ const { ORDER_TYPES, ORDER_STATES } = require('../constants');
4
+ const { calculatePriceTolerance } = require('./utils/math');
5
+ const { isOrderPlaced, parseChainOrder, isOrderOnChain, buildCreateOrderArgs, buildOutsideInPairGroups, extractBatchOperationResults } = require('./utils/order');
69
6
  const { resolveAccountRef } = require('./utils/system');
70
7
  const Format = require('./format');
71
8
  const SUSPECTED_DUPLICATE_TOLERANCE_MULTIPLIER = 5;
72
9
  /**
73
- * Count active orders on the grid for a given type.
74
- * @param {Object} manager - OrderManager instance.
75
- * @param {string} type - ORDER_TYPES value.
76
- * @returns {number} Count of active and partial orders with orderId.
77
- * @private
78
- */
79
- function _countActiveOnGrid(manager, type) {
80
- const active = manager.getOrdersByTypeAndState(type, ORDER_STATES.ACTIVE).filter((o) => o && o.orderId);
81
- const partial = manager.getOrdersByTypeAndState(type, ORDER_STATES.PARTIAL).filter((o) => o && o.orderId);
82
- return active.length + partial.length;
83
- }
84
- /**
85
- * Pick virtual slots to activate based on type and count.
86
- * @param {Object} manager - OrderManager instance.
87
- * @param {string} type - ORDER_TYPES value.
88
- * @param {number} count - Number of slots to pick.
89
- * @returns {Array<Object>} Array of picked virtual slots.
90
- * @private
91
- */
92
- function _pickVirtualSlotsToActivate(manager, type, count) {
93
- if (count <= 0)
94
- return [];
95
- // CRITICAL FIX: Filter by type BEFORE sorting
96
- // Only get slots of the requested type (SELL or BUY), not a mix
97
- const slotsOfType = Array.from(manager.orders.values())
98
- .filter((slot) => slot && slot.type === type)
99
- .sort((a, b) => type === ORDER_TYPES.BUY ? b.price - a.price : a.price - b.price);
100
- let effectiveMin = 0;
101
- try {
102
- effectiveMin = getMinAbsoluteOrderSize(type, manager.assets);
103
- }
104
- catch (e) {
105
- effectiveMin = 0;
106
- }
107
- const valid = [];
108
- for (const slot of slotsOfType) {
109
- if (valid.length >= count)
110
- break;
111
- if (!slot.orderId && slot.state === ORDER_STATES.VIRTUAL) {
112
- // Role invariant: Only pick slots that make sense for this type based on current market pivot
113
- // (Strategy will enforce this strictly, but we filter here for cleaner activation)
114
- if (slot.id && (Number(slot.size) || 0) >= effectiveMin) {
115
- valid.push(slot);
116
- }
117
- }
118
- }
119
- return valid;
120
- }
121
- function _getStartupSideComparators(orderType, assets) {
122
- const isSell = orderType === ORDER_TYPES.SELL;
123
- const sortUpdateComparator = isSell
124
- ? (a, b) => (parseChainOrder(a, assets)?.price || 0) - (parseChainOrder(b, assets)?.price || 0)
125
- : (a, b) => (parseChainOrder(b, assets)?.price || 0) - (parseChainOrder(a, assets)?.price || 0);
126
- const sortExcessCancelComparator = isSell
127
- ? (a, b) => (b.parsed.price || 0) - (a.parsed.price || 0)
128
- : (a, b) => (a.parsed.price || 0) - (b.parsed.price || 0);
129
- const sortMatchedCancelComparator = isSell
130
- ? (a, b) => (b.price || 0) - (a.price || 0)
131
- : (a, b) => (a.price || 0) - (b.price || 0);
132
- return {
133
- sortUpdateComparator,
134
- sortExcessCancelComparator,
135
- sortMatchedCancelComparator,
136
- };
137
- }
138
- /**
139
- * Detect if grid edge is fully occupied with active orders.
140
- * When all outermost (furthest from market) orders are ACTIVE with orderId,
141
- * we're at grid edge and all balance is committed to those orders.
142
- *
143
- * @param {Object} manager - OrderManager instance
144
- * @param {string} orderType - ORDER_TYPES.BUY or ORDER_TYPES.SELL
145
- * @param {number} updateCount - Number of orders being updated
146
- * @returns {boolean} true if edge orders are all active
147
- * @private
148
- */
149
- function _isGridEdgeFullyActive(manager, orderType, updateCount) {
150
- if (!manager || updateCount <= 0)
151
- return false;
152
- // Get all orders of this type
153
- const allOrders = Array.from(manager.orders.values()).filter((o) => o.type === orderType);
154
- if (allOrders.length === 0)
155
- return false;
156
- // Sort: for BUY (highest to lowest price), for SELL (lowest to highest)
157
- // This puts market edge first, grid edge (furthest) last
158
- const sorted = orderType === ORDER_TYPES.BUY
159
- ? allOrders.sort((a, b) => (b.price || 0) - (a.price || 0)) // Buy: high to low price
160
- : allOrders.sort((a, b) => (a.price || 0) - (b.price || 0)); // Sell: low to high price
161
- // Get the outermost orders (last N in sorted = furthest from market)
162
- const outerEdgeCount = Math.min(updateCount, sorted.length);
163
- const edgeOrders = sorted.slice(-outerEdgeCount);
164
- // Check if ALL edge orders are ACTIVE (placed on blockchain)
165
- // Empty array check prevents vacuous truth: every([]) returns true
166
- if (edgeOrders.length === 0)
167
- return false;
168
- const allEdgeActive = edgeOrders.every((o) => isOrderPlaced(o));
169
- return allEdgeActive;
170
- }
171
- /**
172
- * Find the largest order among those being updated.
173
- * Returns both the order and its index in unmatchedOrders for pairing with gridOrders.
174
- * @param {Array<Object>} unmatchedOrders - Array of unmatched on-chain orders
175
- * @param {number} updateCount - Number of orders being updated (first N)
176
- * @returns {{order: Object, index: number}|null} Largest order and its index, or null if none found
177
- * @private
178
- */
179
- function _findLargestOrder(unmatchedOrders, updateCount) {
180
- if (!Array.isArray(unmatchedOrders) || unmatchedOrders.length === 0)
181
- return null;
182
- const ordersToCheck = unmatchedOrders.slice(0, updateCount);
183
- let largestOrder = null;
184
- let largestIndex = -1;
185
- let largestSize = 0;
186
- for (let i = 0; i < ordersToCheck.length; i++) {
187
- const order = ordersToCheck[i];
188
- const size = Number(order.for_sale) || 0;
189
- if (size > largestSize) {
190
- largestSize = size;
191
- largestOrder = order;
192
- largestIndex = i;
193
- }
194
- }
195
- return largestIndex >= 0 ? { order: largestOrder, index: largestIndex } : null;
196
- }
197
- /**
198
- * Cancel the largest unmatched order to free up maximum funds.
199
- * This is more efficient than reducing to size 1 and then updating twice.
200
- * Returns the grid slot index and grid order that needs to be filled.
201
- * @param {Object} params - Destructured parameters
202
- * @param {Function} params.chainOrders - Chain order query function
203
- * @param {string} params.account - Account ID or name
204
- * @param {string} params.privateKey - Active/owner private key
205
- * @param {Object} params.manager - OrderManager instance
206
- * @param {Array<Object>} params.unmatchedOrders - Unmatched on-chain orders
207
- * @param {number} params.updateCount - Number of orders being updated
208
- * @param {string} params.orderType - ORDER_TYPES.BUY or ORDER_TYPES.SELL
209
- * @param {boolean} params.dryRun - If true, skip actual cancellation
210
- * @returns {Promise<{gridIndex: number, gridOrder: Object}|null>} Grid slot info or null
211
- * @private
212
- */
213
- async function _cancelLargestOrder({ chainOrders, account, privateKey, manager, unmatchedOrders, updateCount, orderType, dryRun, fillLockAlreadyHeld = false }) {
214
- if (dryRun)
215
- return null;
216
- if (!Array.isArray(unmatchedOrders) || unmatchedOrders.length === 0)
217
- return null;
218
- const logger = manager && manager.logger;
219
- // Find the largest order among those being updated
220
- const largestInfo = _findLargestOrder(unmatchedOrders, updateCount);
221
- if (!largestInfo)
222
- return null;
223
- const { order: largestOrder, index: largestIndex } = largestInfo;
224
- const originalSize = Number(largestOrder.for_sale) || 0;
225
- const orderId = largestOrder.id;
226
- logger?.log?.(`Grid edge detected: cancelling largest order ${orderId} (size ${originalSize}) to free up funds`, 'info');
227
- try {
228
- // Cancel the largest order on blockchain and release untracked funds.
229
- await _cancelChainOrder({
230
- chainOrders,
231
- account,
232
- privateKey,
233
- manager,
234
- chainOrderId: orderId,
235
- chainOrderObj: largestOrder,
236
- releaseUntrackedFunds: true,
237
- dryRun,
238
- fillLockAlreadyHeld,
239
- });
240
- logger?.log?.(`Cancelled largest order ${orderId}`, 'info');
241
- // Mark for removal from unmatched list (handled by caller)
242
- // Return info needed to create this order fresh later
243
- return { index: largestIndex, orderType };
244
- }
245
- catch (err) {
246
- logger?.log?.(`Warning: Could not cancel largest order ${orderId}: ${err.message}`, 'warn');
247
- return null;
248
- }
249
- }
250
- /**
251
- * Create a new chain order from a grid slot.
252
- * @param {Object} params - Creation parameters.
253
- * @param {Object} params.chainOrders - Chain orders module.
254
- * @param {string} params.account - Account name.
255
- * @param {string} params.privateKey - Private key.
256
- * @param {Object} params.manager - OrderManager instance.
257
- * @param {Object} params.gridOrder - Grid order object.
258
- * @param {boolean} params.dryRun - Whether to simulate.
259
- * @returns {Promise<void>}
260
- * @private
261
- */
262
- async function _createOrderFromGrid({ chainOrders, account, privateKey, manager, gridOrder, dryRun, fillLockAlreadyHeld = false }) {
263
- if (dryRun)
264
- return;
265
- // ATOMIC RE-VERIFICATION: Ensure slot is still virtual and hasn't been filled by recovery sync
266
- const currentSlot = manager.orders.get(gridOrder.id);
267
- if (currentSlot && currentSlot.orderId) {
268
- manager.logger?.log?.(`[_createOrderFromGrid] SKIP: Slot ${gridOrder.id} already has orderId ${currentSlot.orderId}`, 'warn');
269
- return;
270
- }
271
- const { amountToSell, sellAssetId, minToReceive, receiveAssetId } = buildCreateOrderArgs(gridOrder, manager.assets.assetA, manager.assets.assetB);
272
- const result = await chainOrders.createOrder(account, privateKey, amountToSell, sellAssetId, minToReceive, receiveAssetId, null, false);
273
- if (result && result.skipped) {
274
- const logger = manager && manager.logger;
275
- logger?.log?.(`[_createOrderFromGrid] Skipped slot ${gridOrder.id}: order amounts too small to place on-chain`, 'warn');
276
- return;
277
- }
278
- const operationResults = extractBatchOperationResults(result) || [];
279
- const chainOrderId = operationResults[0] && operationResults[0][1];
280
- if (chainOrderId) {
281
- const btsFeeData = getAssetFees('BTS');
282
- // Centralized Fund Tracking: Use manager's sync core to handle state transition and fund deduction
283
- // CRITICAL: Use _applySync (lock-free) since caller holds _gridLock
284
- await manager._applySync({
285
- gridOrderId: gridOrder.id,
286
- chainOrderId,
287
- isPartialPlacement: false,
288
- fee: btsFeeData.createFee
289
- }, 'createOrder', { gridLockAlreadyHeld: true });
290
- }
291
- else {
292
- // CRITICAL FIX: Recovery sync if order extraction fails
293
- const logger = manager && manager.logger;
294
- logger?.log?.(`[_createOrderFromGrid] CRITICAL: createOrder succeeded but chainOrderId extraction failed`, 'error');
295
- try {
296
- const freshChainOrders = await chainOrders.readOpenOrders(resolveAccountRef(manager, account), TIMING.CONNECTION_TIMEOUT_MS);
297
- // CRITICAL FIX: Use skipAccounting: false - order discovery must update accounting
298
- // Orphan order requires fund deduction to prevent phantom capital
299
- await manager.syncFromOpenOrders(freshChainOrders, {
300
- skipAccounting: false,
301
- source: 'chainOrderIdExtractionFailure',
302
- gridLockAlreadyHeld: true,
303
- fillLockAlreadyHeld,
304
- protectCommittedOrders: true
305
- });
306
- }
307
- catch (syncErr) {
308
- logger?.log?.(`[_createOrderFromGrid] Recovery sync failed: ${syncErr.message}`, 'error');
309
- }
310
- }
311
- }
312
- /**
313
- * Cancel a chain order and sync with manager.
314
- * @param {Object} params - Cancellation parameters.
315
- * @param {Object} params.chainOrders - Chain orders module.
316
- * @param {string} params.account - Account name.
317
- * @param {string} params.privateKey - Private key.
318
- * @param {Object} params.manager - OrderManager instance.
319
- * @param {string} params.chainOrderId - ID of the chain order to cancel.
320
- * @param {boolean} params.dryRun - Whether to simulate.
321
- * @param {Object} [params.chainOrderObj] - Raw chain order object (needed for fund release).
322
- * @param {boolean} [params.releaseUntrackedFunds=false] - If true, release the cancelled order's
323
- * committed funds via addToChainFree. Use only for unmatched chain orders that have no
324
- * corresponding ACTIVE/PARTIAL grid slot (where synchronizeWithChain cannot release them).
325
- * @returns {Promise<void>}
326
- * @private
327
- */
328
- async function _cancelChainOrder({ chainOrders, account, privateKey, manager, chainOrderId, dryRun, chainOrderObj, releaseUntrackedFunds = false, fillLockAlreadyHeld = false }) {
329
- if (dryRun)
330
- return;
331
- const cancelResult = await chainOrders.cancelOrder(account, privateKey, chainOrderId);
332
- if (cancelResult?.verifiedAfterFailure) {
333
- const freshChainOrders = await chainOrders.readOpenOrders(resolveAccountRef(manager, account), TIMING.CONNECTION_TIMEOUT_MS);
334
- await manager.syncFromOpenOrders(freshChainOrders, {
335
- skipAccounting: false,
336
- source: 'cancelOrder',
337
- gridLockAlreadyHeld: true,
338
- fillLockAlreadyHeld,
339
- protectCommittedOrders: true
340
- });
341
- }
342
- else {
343
- // CRITICAL: Use _applySync (lock-free) since caller holds _gridLock
344
- await manager._applySync(chainOrderId, 'cancelOrder', { gridLockAlreadyHeld: true });
345
- }
346
- // Unmatched chain orders are not represented as ACTIVE/PARTIAL grid slots, so
347
- // synchronizeWithChain('cancelOrder') cannot release their commitment.
348
- if (releaseUntrackedFunds && manager.accountant && chainOrderObj) {
349
- const parsed = parseChainOrder(chainOrderObj, manager.assets);
350
- if (parsed && parsed.size > 0) {
351
- await manager.accountant.addToChainFree(parsed.type, parsed.size, 'startup-cancel-unmatched');
352
- }
353
- }
354
- }
355
- async function _recoverStartupSyncFailure({ chainOrders, manager, account, logger, triggerMessage, source, fillLockAlreadyHeld = false }) {
356
- try {
357
- logger?.log?.(triggerMessage, 'warn');
358
- const freshChainOrders = await chainOrders.readOpenOrders(resolveAccountRef(manager, account), TIMING.CONNECTION_TIMEOUT_MS);
359
- await manager.syncFromOpenOrders(freshChainOrders, {
360
- skipAccounting: false,
361
- source,
362
- gridLockAlreadyHeld: true,
363
- fillLockAlreadyHeld,
364
- protectCommittedOrders: true
365
- });
366
- return freshChainOrders;
367
- }
368
- catch (syncErr) {
369
- logger?.log?.(`Startup: Recovery sync failed: ${syncErr.message}`, 'error');
370
- return null;
371
- }
372
- }
373
- function _refreshStartupUpdatePlans(updatePlans, chainOpenOrders) {
374
- if (!Array.isArray(updatePlans) || updatePlans.length === 0)
375
- return [];
376
- const chainById = new Map((Array.isArray(chainOpenOrders) ? chainOpenOrders : [])
377
- .filter(o => o && o.id)
378
- .map(o => [o.id, o]));
379
- const refreshed = [];
380
- for (const plan of updatePlans) {
381
- if (!plan?.chainOrderId || !plan?.gridOrder?.id)
382
- continue;
383
- const freshChainOrder = chainById.get(plan.chainOrderId);
384
- if (!freshChainOrder)
385
- continue;
386
- refreshed.push({
387
- ...plan,
388
- chainOrderObj: freshChainOrder,
389
- });
390
- }
391
- return refreshed;
392
- }
393
- function _prepareStartupUpdatePlan(plan, manager, logger) {
394
- const chainOrderId = plan?.chainOrderId;
395
- const gridOrder = plan?.gridOrder;
396
- if (!chainOrderId || !gridOrder?.id)
397
- return null;
398
- const currentSlot = manager.orders.get(gridOrder.id);
399
- if (!currentSlot || (currentSlot.orderId && currentSlot.orderId !== chainOrderId)) {
400
- logger?.log?.(`Startup: Skip update ${chainOrderId} -> ${gridOrder.id}; slot already mapped (${currentSlot?.orderId || 'none'})`, 'warn');
401
- return null;
402
- }
403
- if (currentSlot.orderId === chainOrderId) {
404
- logger?.log?.(`Startup: Skip update ${chainOrderId} -> ${gridOrder.id}; slot already matched`, 'debug');
405
- return null;
406
- }
407
- const { amountToSell, minToReceive } = buildCreateOrderArgs(gridOrder, manager.assets.assetA, manager.assets.assetB);
408
- return {
409
- plan,
410
- chainOrderId,
411
- gridOrder,
412
- parsedChain: parseChainOrder(plan.chainOrderObj, manager.assets),
413
- updateParams: {
414
- newPrice: gridOrder.price,
415
- amountToSell,
416
- minToReceive,
417
- orderType: gridOrder.type,
418
- }
419
- };
420
- }
421
- async function _finalizeStartupUpdate({ manager, preparedUpdate }) {
422
- const { plan, parsedChain } = preparedUpdate;
423
- if (parsedChain && parsedChain.size > 0 && manager.accountant) {
424
- await manager.accountant.addToChainFree(plan.gridOrder.type, parsedChain.size, 'startup-align');
425
- }
426
- // Extract deferred_fee from the raw chain order object so the fee
427
- // lifecycle (cancel refunds, fill maker discounts) reconstructs
428
- // the correct btsFeeState after a grid reset.
429
- // deferred_fee from chain is in raw satoshis (BTS precision 5).
430
- // The fee lifecycle operates in float BTS units, so convert here.
431
- const rawDeferredFee = Format.toFiniteNumber(plan.chainOrderObj?.deferred_fee, null);
432
- const deferredFeeFloat = rawDeferredFee !== null ? blockchainToFloat(rawDeferredFee, BTS_PRECISION) : null;
433
- const btsFeeData = getAssetFees('BTS');
434
- await manager._applySync({
435
- gridOrderId: plan.gridOrder.id,
436
- chainOrderId: plan.chainOrderId,
437
- isPartialPlacement: false,
438
- fee: btsFeeData.updateFee,
439
- skipAccounting: false,
440
- deferredFee: deferredFeeFloat,
441
- }, 'createOrder', { gridLockAlreadyHeld: true });
442
- }
443
- async function _executeStartupUpdateBatch({ updatePlans, chainOrders, account, privateKey, manager, dryRun, }) {
444
- if (!Array.isArray(updatePlans) || updatePlans.length === 0 || dryRun) {
445
- return { executed: false, prepared: 0, skipped: true };
446
- }
447
- if (typeof chainOrders?.buildUpdateOrderOp !== 'function' || typeof chainOrders?.executeBatch !== 'function') {
448
- throw new Error('chainOrders does not support batch update operations');
449
- }
450
- const logger = manager?.logger;
451
- const prepared = [];
452
- for (const plan of updatePlans) {
453
- const preparedPlan = _prepareStartupUpdatePlan(plan, manager, logger);
454
- if (!preparedPlan)
455
- continue;
456
- const buildResult = await chainOrders.buildUpdateOrderOp(account, preparedPlan.chainOrderId, preparedPlan.updateParams, plan.chainOrderObj || null);
457
- if (!buildResult) {
458
- logger?.log?.(`Startup: Skip update ${preparedPlan.chainOrderId} -> ${preparedPlan.gridOrder.id}; no blockchain delta`, 'debug');
459
- continue;
460
- }
461
- prepared.push({
462
- ...preparedPlan,
463
- op: buildResult.op,
464
- });
465
- }
466
- if (prepared.length === 0) {
467
- return { executed: false, prepared: 0, skipped: true };
468
- }
469
- logger?.log?.(`Startup: Broadcasting update batch (${prepared.length} op${prepared.length > 1 ? 's' : ''})`, 'info');
470
- await chainOrders.executeBatch(account, privateKey, prepared.map(p => p.op));
471
- for (const entry of prepared) {
472
- await _finalizeStartupUpdate({ manager, preparedUpdate: entry });
473
- }
474
- return { executed: true, prepared: prepared.length, skipped: false };
475
- }
476
- async function _executeStartupSingleUpdate({ plan, chainOrders, account, privateKey, manager, dryRun, }) {
477
- if (dryRun)
478
- return { executed: false, skipped: true };
479
- if (typeof chainOrders?.updateOrder !== 'function') {
480
- throw new Error('chainOrders.updateOrder is required for sequential update fallback');
481
- }
482
- const logger = manager?.logger;
483
- const prepared = _prepareStartupUpdatePlan(plan, manager, logger);
484
- if (!prepared)
485
- return { executed: false, skipped: true };
486
- const result = await chainOrders.updateOrder(account, privateKey, prepared.chainOrderId, prepared.updateParams);
487
- if (!result) {
488
- logger?.log?.(`Startup: Skip update ${prepared.chainOrderId} -> ${prepared.gridOrder.id}; no blockchain delta`, 'debug');
489
- return { executed: false, skipped: true };
490
- }
491
- await _finalizeStartupUpdate({ manager, preparedUpdate: prepared });
492
- return { executed: true, skipped: false };
493
- }
494
- async function _executeStartupSequentialUpdateFallback({ updatePlans, chainOrders, account, privateKey, manager, dryRun, fillLockAlreadyHeld = false, }) {
495
- if (!Array.isArray(updatePlans) || updatePlans.length === 0 || dryRun) {
496
- return { executed: 0, skipped: 0, failed: 0 };
497
- }
498
- const logger = manager?.logger;
499
- let queue = updatePlans.slice(0);
500
- let executed = 0;
501
- let skipped = 0;
502
- let failed = 0;
503
- logger?.log?.(`Startup: Falling back to sequential updates for ${queue.length} pending order(s)`, 'warn');
504
- while (queue.length > 0) {
505
- const plan = queue.shift();
506
- if (!plan)
507
- continue;
508
- try {
509
- const result = await _executeStartupSingleUpdate({
510
- plan,
511
- chainOrders,
512
- account,
513
- privateKey,
514
- manager,
515
- dryRun,
516
- });
517
- if (result.executed)
518
- executed++;
519
- else
520
- skipped++;
521
- }
522
- catch (err) {
523
- failed++;
524
- logger?.log?.(`Startup: Sequential update failed for ${plan.chainOrderId} -> ${plan.gridOrderId || plan.gridOrder?.id}: ${err.message}`, 'error');
525
- const refreshedChainOrders = await _recoverStartupSyncFailure({
526
- chainOrders,
527
- manager,
528
- account,
529
- logger,
530
- triggerMessage: `Startup: Triggering recovery sync after sequential update failure for ${plan.chainOrderId}`,
531
- source: 'startupReconcileSequentialUpdateFailure',
532
- fillLockAlreadyHeld,
533
- });
534
- queue = _refreshStartupUpdatePlans(queue, refreshedChainOrders);
535
- }
536
- }
537
- return { executed, skipped, failed };
538
- }
539
- async function _createStartupOrderWithHandling({ chainOrders, account, privateKey, manager, gridOrder, orderLabel, dryRun, recovery, fillLockAlreadyHeld = false }) {
540
- try {
541
- await _createOrderFromGrid({ chainOrders, account, privateKey, manager, gridOrder, dryRun, fillLockAlreadyHeld });
542
- }
543
- catch (err) {
544
- manager?.logger?.log?.(`Startup: Failed to create ${orderLabel}: ${err.message}`, 'error');
545
- if (recovery && recovery.triggerMessage && recovery.source) {
546
- await _recoverStartupSyncFailure({
547
- chainOrders,
548
- manager,
549
- account,
550
- logger: manager?.logger,
551
- triggerMessage: recovery.triggerMessage,
552
- source: recovery.source,
553
- fillLockAlreadyHeld,
554
- });
555
- }
556
- }
557
- }
558
- // _extractBatchOperationResults — thin wrapper over shared utility,
559
- // returns empty array (not null) for callers that iterate directly.
560
- function _extractBatchOperationResults(result) {
561
- return extractBatchOperationResults(result) || [];
562
- }
563
- function _resolveGroupRecovery(group, fallbackMessage, fallbackSource) {
564
- for (const plan of group || []) {
565
- if (plan?.recovery?.triggerMessage && plan?.recovery?.source) {
566
- return { triggerMessage: plan.recovery.triggerMessage, source: plan.recovery.source };
567
- }
568
- }
569
- return { triggerMessage: fallbackMessage, source: fallbackSource };
570
- }
571
- async function _executeStartupCreateGroupBatch({ group, chainOrders, account, privateKey, manager, dryRun, groupIndex, totalGroups, fillLockAlreadyHeld = false, }) {
572
- if (!Array.isArray(group) || group.length === 0 || dryRun)
573
- return;
574
- if (typeof chainOrders?.buildCreateOrderOp !== 'function' || typeof chainOrders?.executeBatch !== 'function') {
575
- throw new Error('chainOrders does not support batch create operations');
576
- }
577
- const logger = manager?.logger;
578
- const prepared = [];
579
- for (const plan of group) {
580
- const gridOrder = plan?.gridOrder;
581
- if (!gridOrder || !gridOrder.id)
582
- continue;
583
- const currentSlot = manager.orders.get(gridOrder.id);
584
- if (currentSlot?.orderId) {
585
- logger?.log?.(`Startup: Skip create ${plan.orderLabel} - slot ${gridOrder.id} already has orderId ${currentSlot.orderId}`, 'warn');
586
- continue;
587
- }
588
- const { amountToSell, sellAssetId, minToReceive, receiveAssetId } = buildCreateOrderArgs(gridOrder, manager.assets.assetA, manager.assets.assetB);
589
- const buildResult = await chainOrders.buildCreateOrderOp(account, amountToSell, sellAssetId, minToReceive, receiveAssetId, null);
590
- if (!buildResult) {
591
- logger?.log?.(`Startup: Skip create ${plan.orderLabel} - order amounts too small to place on-chain`, 'warn');
592
- continue;
593
- }
594
- prepared.push({ plan, op: buildResult.op });
595
- }
596
- if (prepared.length === 0)
597
- return;
598
- const recovery = _resolveGroupRecovery(group, `Startup: Triggering recovery sync after create group ${groupIndex + 1}/${totalGroups} failure`, 'startupCreateGroupFailure');
599
- try {
600
- logger?.log?.(`Startup: Broadcasting create group ${groupIndex + 1}/${totalGroups} in single batch (${prepared.length} op${prepared.length > 1 ? 's' : ''})`, 'info');
601
- const batchResult = await chainOrders.executeBatch(account, privateKey, prepared.map(p => p.op));
602
- const opResults = _extractBatchOperationResults(batchResult);
603
- const btsFeeData = getAssetFees('BTS');
604
- let missingChainOrderId = false;
605
- for (let i = 0; i < prepared.length; i++) {
606
- const chainOrderId = opResults[i] && opResults[i][1];
607
- const plan = prepared[i].plan;
608
- if (!chainOrderId) {
609
- logger?.log?.(`Startup: create result missing chainOrderId for ${plan.orderLabel}`, 'error');
610
- missingChainOrderId = true;
611
- continue;
612
- }
613
- await manager._applySync({
614
- gridOrderId: plan.gridOrder.id,
615
- chainOrderId,
616
- isPartialPlacement: false,
617
- fee: btsFeeData.createFee
618
- }, 'createOrder', { gridLockAlreadyHeld: true });
619
- }
620
- if (missingChainOrderId) {
621
- await _recoverStartupSyncFailure({
622
- chainOrders,
623
- manager,
624
- account,
625
- logger,
626
- triggerMessage: recovery.triggerMessage,
627
- source: recovery.source,
628
- fillLockAlreadyHeld,
629
- });
630
- }
631
- }
632
- catch (err) {
633
- logger?.log?.(`Startup: Failed to create group ${groupIndex + 1}/${totalGroups}: ${err.message}`, 'error');
634
- await _recoverStartupSyncFailure({
635
- chainOrders,
636
- manager,
637
- account,
638
- logger,
639
- triggerMessage: recovery.triggerMessage,
640
- source: recovery.source,
641
- fillLockAlreadyHeld,
642
- });
643
- }
644
- }
645
- function _buildOutsideInCreateGroups(createPlans) {
646
- return buildOutsideInPairGroups(createPlans, {
647
- isValid: p => Boolean(p?.gridOrder),
648
- getType: p => p.orderType,
649
- getPrice: p => p.gridOrder?.price,
650
- });
651
- }
652
- async function _executePlannedStartupCreates({ createPlans, chainOrders, account, privateKey, manager, dryRun, fillLockAlreadyHeld = false, }) {
653
- const logger = manager?.logger;
654
- const groups = _buildOutsideInCreateGroups(createPlans);
655
- if (groups.length === 0)
656
- return;
657
- logger?.log?.(`Startup: Executing ${createPlans.length} planned create(s) in ${groups.length} outside->center group(s)`, 'info');
658
- for (let i = 0; i < groups.length; i++) {
659
- const group = groups[i];
660
- const labels = group.map(p => `${p.orderType.toUpperCase()}:${p.gridOrder?.id}`).join(', ');
661
- logger?.log?.(`Startup: Create group ${i + 1}/${groups.length} (${labels})`, 'info');
662
- const canBatchCreate = typeof chainOrders?.buildCreateOrderOp === 'function' && typeof chainOrders?.executeBatch === 'function';
663
- if (group.length > 1 && canBatchCreate) {
664
- await _executeStartupCreateGroupBatch({
665
- group,
666
- chainOrders,
667
- account,
668
- privateKey,
669
- manager,
670
- dryRun,
671
- groupIndex: i,
672
- totalGroups: groups.length,
673
- fillLockAlreadyHeld,
674
- });
675
- continue;
676
- }
677
- if (group.length > 1 && !canBatchCreate) {
678
- logger?.log?.('Startup: Batch create helpers unavailable; falling back to sequential creates for this group', 'warn');
679
- }
680
- for (const plan of group) {
681
- await _createStartupOrderWithHandling({
682
- chainOrders,
683
- account,
684
- privateKey,
685
- manager,
686
- gridOrder: plan.gridOrder,
687
- orderLabel: plan.orderLabel,
688
- dryRun,
689
- recovery: plan.recovery,
690
- fillLockAlreadyHeld,
691
- });
692
- }
693
- }
694
- }
695
- async function _reconcileStartupSide({ orderType, targetCount, chainSideOrders, unmatchedSideOrders, manager, chainOrders, account, privateKey, dryRun, plannedCreates, plannedUpdates, fillLockAlreadyHeld = false, }) {
696
- const logger = manager?.logger;
697
- const sideUpper = orderType === ORDER_TYPES.SELL ? 'SELL' : 'BUY';
698
- const balanceKey = orderType === ORDER_TYPES.SELL ? 'sellFree' : 'buyFree';
699
- const balanceSymbol = orderType === ORDER_TYPES.SELL ? manager.assets.assetA.symbol : manager.assets.assetB.symbol;
700
- const { sortUpdateComparator, sortExcessCancelComparator, sortMatchedCancelComparator, } = _getStartupSideComparators(orderType, manager.assets);
701
- const matchedOnGrid = _countActiveOnGrid(manager, orderType);
702
- const neededSlots = Math.max(0, targetCount - matchedOnGrid);
703
- const desiredSlots = _pickVirtualSlotsToActivate(manager, orderType, neededSlots);
704
- const sortedUnmatched = unmatchedSideOrders.slice(0).sort(sortUpdateComparator);
705
- const updateCount = Math.min(sortedUnmatched.length, desiredSlots.length);
706
- let cancelledIndex = null;
707
- let projectedSideBalance = Number(manager.accountTotals?.[balanceKey] || 0);
708
- logger?.log?.(`Startup ${sideUpper}: matchedOnGrid=${matchedOnGrid}, needSlots=${neededSlots}, unmatched=${sortedUnmatched.length}, updates=${updateCount}`, 'info');
709
- if (updateCount > 0 && _isGridEdgeFullyActive(manager, orderType, updateCount)) {
710
- logger?.log?.(`Startup: ${sideUpper} grid edge is fully active, cancelling largest order to free funds`, 'info');
711
- const cancelInfo = await _cancelLargestOrder({
712
- chainOrders,
713
- account,
714
- privateKey,
715
- manager,
716
- unmatchedOrders: sortedUnmatched,
717
- updateCount,
718
- orderType,
719
- dryRun,
720
- fillLockAlreadyHeld,
721
- });
722
- if (cancelInfo)
723
- cancelledIndex = cancelInfo.index;
724
- }
725
- for (let i = 0; i < updateCount; i++) {
726
- if (cancelledIndex !== null && i === cancelledIndex)
727
- continue;
728
- const chainOrder = sortedUnmatched[i];
729
- const gridOrder = desiredSlots[i];
730
- const gridSize = Number(gridOrder.size) || 0;
731
- const parsedChain = parseChainOrder(chainOrder, manager.assets);
732
- const currentSize = parsedChain ? parsedChain.size : 0;
733
- const sizeIncrease = Math.max(0, gridSize - currentSize);
734
- const currentAssetBalance = projectedSideBalance;
735
- if (sizeIncrease > currentAssetBalance) {
736
- logger?.log?.(`Startup: Skipping ${sideUpper} update ${chainOrder.id} - insufficient balance for increase (need +${Format.formatSizeByOrderType(sizeIncrease, orderType, manager.assets)} ${balanceSymbol}, have ${Format.formatSizeByOrderType(currentAssetBalance, orderType, manager.assets)} ${balanceSymbol})`, 'warn');
737
- continue;
738
- }
739
- logger?.log?.(`Startup: Updating chain ${sideUpper} ${chainOrder.id} -> grid ${gridOrder.id} (price=${Format.formatPrice6(gridOrder.price)}, size=${Format.formatSizeByOrderType(gridOrder.size, orderType, manager.assets)})`, 'info');
740
- plannedUpdates.push({
741
- orderType,
742
- chainOrderId: chainOrder.id,
743
- chainOrderObj: chainOrder,
744
- gridOrderId: gridOrder.id,
745
- gridOrder: { ...gridOrder },
746
- });
747
- projectedSideBalance += (currentSize - gridSize);
748
- }
749
- if (cancelledIndex !== null && !dryRun) {
750
- const targetGridOrder = desiredSlots[cancelledIndex];
751
- if (targetGridOrder) {
752
- logger?.log?.(`Startup: Creating new ${sideUpper} for cancelled slot at grid ${targetGridOrder.id} (price=${Format.formatPrice6(targetGridOrder.price)}, size=${Format.formatSizeByOrderType(targetGridOrder.size, orderType, manager.assets)})`, 'info');
753
- plannedCreates.push({
754
- orderType,
755
- gridOrder: targetGridOrder,
756
- orderLabel: `${sideUpper} for cancelled slot`,
757
- recovery: {
758
- triggerMessage: `Startup: Triggering recovery sync after ${sideUpper} creation failure`,
759
- source: 'phase3CreationFailure',
760
- },
761
- });
762
- }
763
- }
764
- const processedUnmatched = sortedUnmatched.slice(updateCount);
765
- const chainCount = chainSideOrders.length;
766
- const createCount = Math.max(0, targetCount - chainCount);
767
- const remainingSlots = desiredSlots.slice(updateCount);
768
- for (let i = 0; i < Math.min(createCount, remainingSlots.length); i++) {
769
- const gridOrder = remainingSlots[i];
770
- logger?.log?.(`Startup: Creating ${sideUpper} for grid ${gridOrder.id} (price=${Format.formatPrice6(gridOrder.price)}, size=${Format.formatSizeByOrderType(gridOrder.size, orderType, manager.assets)})`, 'info');
771
- plannedCreates.push({
772
- orderType,
773
- gridOrder,
774
- orderLabel: sideUpper,
775
- // Intentionally no recovery metadata here.
776
- // This preserves legacy behavior: regular virtual-slot creates only log failures,
777
- // while cancelled-slot replacement creates trigger a startup recovery sync.
778
- });
779
- }
780
- let cancelCount = Math.max(0, chainCount - targetCount);
781
- if (cancelCount > 0) {
782
- const parsedUnmatched = processedUnmatched
783
- .map(co => ({ chain: co, parsed: parseChainOrder(co, manager.assets) }))
784
- .filter(x => x.parsed)
785
- .sort(sortExcessCancelComparator);
786
- for (const x of parsedUnmatched) {
787
- if (cancelCount <= 0)
788
- break;
789
- logger?.log?.(`Startup: Cancelling excess ${sideUpper} chain order ${x.chain.id}`, 'info');
790
- try {
791
- await _cancelChainOrder({
792
- chainOrders,
793
- account,
794
- privateKey,
795
- manager,
796
- chainOrderId: x.chain.id,
797
- chainOrderObj: x.chain,
798
- releaseUntrackedFunds: true,
799
- dryRun,
800
- fillLockAlreadyHeld,
801
- });
802
- logger?.log?.(`Startup: Successfully cancelled excess ${sideUpper} order ${x.chain.id}`, 'info');
803
- cancelCount--;
804
- }
805
- catch (err) {
806
- logger?.log?.(`Startup: Failed to cancel ${sideUpper} ${x.chain.id}: ${err.message}`, 'error');
807
- }
808
- }
809
- if (cancelCount > 0) {
810
- const activeOrders = manager.getOrdersByTypeAndState(orderType, ORDER_STATES.ACTIVE)
811
- .filter(o => o && o.orderId)
812
- .sort(sortMatchedCancelComparator);
813
- for (const o of activeOrders) {
814
- if (cancelCount <= 0)
815
- break;
816
- logger?.log?.(`Startup: Cancelling excess matched ${sideUpper} ${o.orderId} (grid ${o.id})`, 'warn');
817
- try {
818
- await _cancelChainOrder({ chainOrders, account, privateKey, manager, chainOrderId: o.orderId, dryRun, chainOrderObj: o, fillLockAlreadyHeld });
819
- logger?.log?.(`Startup: Successfully cancelled excess matched ${sideUpper} order ${o.orderId} (grid ${o.id})`, 'info');
820
- cancelCount--;
821
- }
822
- catch (err) {
823
- logger?.log?.(`Startup: Failed to cancel matched ${sideUpper} ${o.orderId}: ${err.message}`, 'error');
824
- }
825
- }
826
- }
827
- }
828
- return {
829
- chainCount,
830
- };
831
- }
832
- /**
833
- * Attempt to resume a persisted grid when orderIds don't match (e.g. orders.json out of sync),
834
- * by matching existing on-chain open orders to grid orders using price+size matching.
835
- *
836
10
  * Returns { resumed: boolean, matchedCount: number }.
837
11
  * @param {Object} params - Destructured parameters
838
12
  * @param {Object} params.manager - OrderManager instance
@@ -842,7 +16,7 @@ async function _reconcileStartupSide({ orderType, targetCount, chainSideOrders,
842
16
  * @param {Function} params.storeGrid - Grid persistence callback
843
17
  * @returns {Promise<{resumed: boolean, matchedCount: number}>}
844
18
  */
845
- async function attemptResumePersistedGridByPriceMatch({ manager, persistedGrid, chainOpenOrders, logger, storeGrid, fillLockAlreadyHeld = false, }) {
19
+ async function attemptResumePersistedGridByPriceMatch({ manager, persistedGrid, chainOpenOrders, logger, storeGrid, }) {
846
20
  if (!Array.isArray(persistedGrid) || persistedGrid.length === 0)
847
21
  return { resumed: false, matchedCount: 0 };
848
22
  if (!Array.isArray(chainOpenOrders) || chainOpenOrders.length === 0)
@@ -851,9 +25,9 @@ async function attemptResumePersistedGridByPriceMatch({ manager, persistedGrid,
851
25
  return { resumed: false, matchedCount: 0 };
852
26
  try {
853
27
  logger && logger.log && logger.log('No matching active order IDs found. Attempting to match by price...', 'info');
854
- const Grid = require('./grid');
855
- await Grid.loadGrid(manager, persistedGrid);
856
- await manager.synchronizeWithChain(chainOpenOrders, 'readOpenOrders', { fillLockAlreadyHeld });
28
+ const { loadGrid } = require('./grid');
29
+ await loadGrid(manager, persistedGrid);
30
+ await manager.synchronizeWithChain(chainOpenOrders, 'readOpenOrders', {});
857
31
  const matchedOrderIds = new Set(Array.from(manager.orders.values())
858
32
  .filter((o) => o && isOrderOnChain(o))
859
33
  .map((o) => o.orderId)
@@ -890,7 +64,7 @@ async function attemptResumePersistedGridByPriceMatch({ manager, persistedGrid,
890
64
  * @param {Function} [params.attemptResumeFn=attemptResumePersistedGridByPriceMatch] - Resume function
891
65
  * @returns {Promise<Object>} { shouldRegenerate, hasActiveMatch, resumedByPrice, matchedCount }
892
66
  */
893
- async function decideStartupGridAction({ persistedGrid, chainOpenOrders, manager, logger, storeGrid, attemptResumeFn = attemptResumePersistedGridByPriceMatch, fillLockAlreadyHeld = false, }) {
67
+ async function decideStartupGridAction({ persistedGrid, chainOpenOrders, manager, logger, storeGrid, attemptResumeFn = attemptResumePersistedGridByPriceMatch, }) {
894
68
  const persisted = Array.isArray(persistedGrid) ? persistedGrid : [];
895
69
  const chain = Array.isArray(chainOpenOrders) ? chainOpenOrders : [];
896
70
  if (persisted.length === 0) {
@@ -902,7 +76,7 @@ async function decideStartupGridAction({ persistedGrid, chainOpenOrders, manager
902
76
  return { shouldRegenerate: false, hasActiveMatch: true, resumedByPrice: false, matchedCount: 0 };
903
77
  }
904
78
  if (chain.length > 0) {
905
- const resume = await attemptResumeFn({ manager, persistedGrid: persisted, chainOpenOrders: chain, logger, storeGrid, fillLockAlreadyHeld });
79
+ const resume = await attemptResumeFn({ manager, persistedGrid: persisted, chainOpenOrders: chain, logger, storeGrid });
906
80
  return { shouldRegenerate: !resume.resumed, hasActiveMatch: false, resumedByPrice: !!resume.resumed, matchedCount: resume.matchedCount || 0 };
907
81
  }
908
82
  return { shouldRegenerate: true, hasActiveMatch: false, resumedByPrice: false, matchedCount: 0 };
@@ -926,7 +100,7 @@ async function decideStartupGridAction({ persistedGrid, chainOpenOrders, manager
926
100
  * @param {Array<Object>} params.chainOpenOrders - On-chain open orders
927
101
  * @returns {Promise<Object>} Reconciliation result
928
102
  */
929
- async function reconcileGridOrders({ manager, config, account, privateKey, chainOrders, chainOpenOrders, fillLockAlreadyHeld = false, }) {
103
+ async function reconcileGridOrders({ manager, config, account, privateKey, chainOrders, chainOpenOrders, }) {
930
104
  // Parameter validation
931
105
  if (!manager || typeof manager.synchronizeWithChain !== 'function') {
932
106
  throw new Error('reconcileGridOrders: manager must be provided with synchronizeWithChain method');
@@ -1026,7 +200,6 @@ async function reconcileGridOrders({ manager, config, account, privateKey, chain
1026
200
  dryRun,
1027
201
  chainOrderObj: u.chain,
1028
202
  releaseUntrackedFunds: true,
1029
- fillLockAlreadyHeld,
1030
203
  });
1031
204
  cancelledDuplicateIds.add(p.orderId);
1032
205
  logger?.log?.(`Cancelled unmatched duplicate chain order ${p.orderId} at price ${Format.formatPrice6(p.price)} — ` +
@@ -1067,7 +240,6 @@ async function reconcileGridOrders({ manager, config, account, privateKey, chain
1067
240
  dryRun,
1068
241
  plannedCreates,
1069
242
  plannedUpdates,
1070
- fillLockAlreadyHeld,
1071
243
  });
1072
244
  const buyResult = await _reconcileStartupSide({
1073
245
  orderType: ORDER_TYPES.BUY,
@@ -1081,7 +253,6 @@ async function reconcileGridOrders({ manager, config, account, privateKey, chain
1081
253
  dryRun,
1082
254
  plannedCreates,
1083
255
  plannedUpdates,
1084
- fillLockAlreadyHeld,
1085
256
  });
1086
257
  return { plannedCreates, plannedUpdates, chainSellCount: sellResult.chainCount, chainBuyCount: buyResult.chainCount };
1087
258
  });
@@ -1119,7 +290,6 @@ async function reconcileGridOrders({ manager, config, account, privateKey, chain
1119
290
  logger,
1120
291
  triggerMessage: `Startup: Triggering recovery sync after update batch failure (attempt ${attempt}/${maxBatchAttempts})`,
1121
292
  source: 'startupReconcileUpdateBatchFailure',
1122
- fillLockAlreadyHeld,
1123
293
  });
1124
294
  updatePlans = _refreshStartupUpdatePlans(updatePlans, refreshedChainOrders);
1125
295
  if (updatePlans.length === 0) {
@@ -1146,7 +316,6 @@ async function reconcileGridOrders({ manager, config, account, privateKey, chain
1146
316
  privateKey,
1147
317
  manager,
1148
318
  dryRun,
1149
- fillLockAlreadyHeld,
1150
319
  });
1151
320
  if (fallbackResult.executed > 0 || fallbackResult.skipped > 0 || fallbackResult.failed > 0) {
1152
321
  logger?.log?.(`Startup: Sequential fallback complete (updated=${fallbackResult.executed}, skipped=${fallbackResult.skipped}, failed=${fallbackResult.failed})`, fallbackResult.failed > 0 ? 'warn' : 'info');
@@ -1165,7 +334,6 @@ async function reconcileGridOrders({ manager, config, account, privateKey, chain
1165
334
  privateKey,
1166
335
  manager,
1167
336
  dryRun,
1168
- fillLockAlreadyHeld,
1169
337
  });
1170
338
  }
1171
339
  let finalChainSellCount = chainSellCount;