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