dexbot 1.2.5 → 1.2.6
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 -1
- package/dist/dexbot.js +1 -1
- package/dist/dexbot.js.map +1 -1
- package/dist/modules/account_orders.d.ts +8 -1
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +24 -1
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/chain_orders.d.ts +16 -0
- package/dist/modules/chain_orders.d.ts.map +1 -1
- package/dist/modules/chain_orders.js +41 -0
- package/dist/modules/chain_orders.js.map +1 -1
- package/dist/modules/constants.d.ts +0 -3
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +0 -14
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +10 -2
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +176 -57
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +2 -2
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +3 -3
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +8 -9
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +7 -0
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +2 -18
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +22 -49
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/async_lock.d.ts +10 -0
- package/dist/modules/order/async_lock.d.ts.map +1 -1
- package/dist/modules/order/async_lock.js +60 -5
- package/dist/modules/order/async_lock.js.map +1 -1
- package/dist/modules/order/grid.js +2 -2
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/manager.d.ts +8 -1
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +28 -14
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts +2 -49
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +4 -132
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts +53 -6
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +384 -204
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +8 -3
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.js +1 -1
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +1 -1
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +3 -2
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/runtime_settings.d.ts +3 -0
- package/dist/modules/runtime_settings.d.ts.map +1 -1
- package/dist/modules/runtime_settings.js +34 -1
- package/dist/modules/runtime_settings.js.map +1 -1
- package/dist/modules/types.d.ts +29 -4
- package/dist/modules/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -40,25 +40,30 @@
|
|
|
40
40
|
* Pass 1: Match grid orders to chain (known grid → chain, includes orphan cleanup)
|
|
41
41
|
* Pass 2: Add missing chain orders (unknown chain → grid)
|
|
42
42
|
*
|
|
43
|
-
* FILL PROCESSING (
|
|
44
|
-
* 5. syncFromFillHistory(fill) - Process fill event synchronously
|
|
43
|
+
* FILL PROCESSING (2 methods)
|
|
44
|
+
* 5. syncFromFillHistory(fill) - Process single fill event synchronously
|
|
45
45
|
* Updates grid order state based on fill data
|
|
46
46
|
* Updates fund state and accounting
|
|
47
47
|
* Handles both maker and taker fills
|
|
48
48
|
*
|
|
49
|
+
* 6. syncFromFillHistoryBatch(fills) - Process multiple fill events in batch
|
|
50
|
+
* Acquires _gridLock once for all fills in block group
|
|
51
|
+
* Batches drift refetch into one get_objects RPC call
|
|
52
|
+
* Applies all grid mutations in a single applyGridUpdateBatch
|
|
53
|
+
*
|
|
49
54
|
* FULL SYNCHRONIZATION (1 method - async)
|
|
50
|
-
*
|
|
55
|
+
* 7. synchronizeWithChain(chainData, source) - Full sync (fetch + sync) (async)
|
|
51
56
|
* Fetches fresh account balances
|
|
52
57
|
* Calls syncFromOpenOrders() with chain data
|
|
53
58
|
* Source: event type that triggered sync (fill, poll, broadcast, etc.)
|
|
54
59
|
*
|
|
55
60
|
* ACCOUNT STATE (2 methods - async)
|
|
56
|
-
*
|
|
61
|
+
* 8. fetchAccountBalancesAndSetTotals(accountId) - Fetch account totals (async)
|
|
57
62
|
* Retrieves BUY/SELL totals and free balances from blockchain
|
|
58
63
|
* Sets manager.accountTotals
|
|
59
64
|
* Triggers fund recalculation
|
|
60
65
|
*
|
|
61
|
-
*
|
|
66
|
+
* 9. initializeAssets() - Initialize asset metadata (async)
|
|
62
67
|
* Fetches asset precision and other metadata
|
|
63
68
|
* Sets manager.assets
|
|
64
69
|
* Called once at bot startup
|
|
@@ -91,28 +96,7 @@ const { blockchainToFloat, floatToBlockchainInt, hasValidAccountTotals, calculat
|
|
|
91
96
|
const { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, convertToSpreadPlaceholder, virtualizeOrder, buildFillKey, isOrderPlaced, isOrderOnChain, hasOnChainId, isOrderVirtual, isPhantomOrder } = require('./utils/order');
|
|
92
97
|
const { resolveProcessedFillPersistenceMode } = require('./processed_fill_store');
|
|
93
98
|
const { lookupAsset } = require('./utils/system');
|
|
94
|
-
|
|
95
|
-
* Compare two raw chain order objects for on-chain equivalence.
|
|
96
|
-
* @param {Object|null} a - First chain order
|
|
97
|
-
* @param {Object|null} b - Second chain order
|
|
98
|
-
* @returns {boolean} True if orders are equivalent
|
|
99
|
-
*/
|
|
100
|
-
function hasEquivalentRawOnChainOrder(a, b) {
|
|
101
|
-
if (!a && !b)
|
|
102
|
-
return true;
|
|
103
|
-
if (!a || !b)
|
|
104
|
-
return false;
|
|
105
|
-
const aBase = a.sell_price?.base || {};
|
|
106
|
-
const bBase = b.sell_price?.base || {};
|
|
107
|
-
const aQuote = a.sell_price?.quote || {};
|
|
108
|
-
const bQuote = b.sell_price?.quote || {};
|
|
109
|
-
return String(a.id ?? '') === String(b.id ?? '') &&
|
|
110
|
-
String(a.for_sale ?? '') === String(b.for_sale ?? '') &&
|
|
111
|
-
String(aBase.amount ?? '') === String(bBase.amount ?? '') &&
|
|
112
|
-
String(aBase.asset_id ?? '') === String(bBase.asset_id ?? '') &&
|
|
113
|
-
String(aQuote.amount ?? '') === String(bQuote.amount ?? '') &&
|
|
114
|
-
String(aQuote.asset_id ?? '') === String(bQuote.asset_id ?? '');
|
|
115
|
-
}
|
|
99
|
+
const chainOrders = require('../chain_orders');
|
|
116
100
|
function describeNearestAdoptionCandidates(mgr, chainOrder, precision, calcTolerance, matchedGridOrderIds = null) {
|
|
117
101
|
if (!mgr?.orders || !chainOrder || typeof precision !== 'number')
|
|
118
102
|
return 'candidate diagnostics unavailable';
|
|
@@ -181,9 +165,6 @@ function describeNearestAdoptionCandidates(mgr, chainOrder, precision, calcToler
|
|
|
181
165
|
` reason=${reasons.join('|') || 'unknown'}`;
|
|
182
166
|
}).join('; ');
|
|
183
167
|
}
|
|
184
|
-
function _getDriftToleranceMultiplier(mgr) {
|
|
185
|
-
return mgr?.config?.gridLimits?.PRICE_DRIFT_TOLERANCE_MULTIPLIER;
|
|
186
|
-
}
|
|
187
168
|
/**
|
|
188
169
|
* Find the closest same-side (or spread) candidate slot for a chain order and
|
|
189
170
|
* return a "price-drift-orphan" tag if the slot's price diff is larger than the
|
|
@@ -235,7 +216,8 @@ function computeOutOfToleranceDriftTag(mgr, chainOrder, calcToleranceFn) {
|
|
|
235
216
|
const tolerance = (calcToleranceFn ? (calcToleranceFn(slotPrice, effectiveSize, orderType) || 0) : 0);
|
|
236
217
|
if (priceDiff <= tolerance)
|
|
237
218
|
continue;
|
|
238
|
-
const
|
|
219
|
+
const driftMultiplier = mgr?.config?.gridLimits?.PRICE_DRIFT_TOLERANCE_MULTIPLIER;
|
|
220
|
+
const driftBudget = tolerance * driftMultiplier;
|
|
239
221
|
if (driftBudget > 0 && priceDiff > driftBudget)
|
|
240
222
|
continue;
|
|
241
223
|
if (!bestDrift || priceDiff < bestDrift.priceDiff) {
|
|
@@ -963,6 +945,121 @@ class SyncEngine {
|
|
|
963
945
|
}
|
|
964
946
|
return { filledOrders, updatedOrders, ordersNeedingCorrection, unmatchedChainOrders };
|
|
965
947
|
}
|
|
948
|
+
// ------------------------------------------------------------------
|
|
949
|
+
// Shared fill-processing helpers (used by syncFromFillHistory and
|
|
950
|
+
// syncFromFillHistoryBatch)
|
|
951
|
+
// ------------------------------------------------------------------
|
|
952
|
+
_findMatchingGridOrder(mgr, orderId) {
|
|
953
|
+
for (const gridOrder of mgr.orders.values()) {
|
|
954
|
+
if (gridOrder.orderId === orderId && (gridOrder.state === ORDER_STATES.ACTIVE || gridOrder.state === ORDER_STATES.PARTIAL)) {
|
|
955
|
+
return gridOrder;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
_computeFillContext(mgr, matchedGridOrder, paysAssetId, paysAmountRaw) {
|
|
961
|
+
const orderType = matchedGridOrder.type;
|
|
962
|
+
const currentSize = toFiniteNumber(matchedGridOrder.size);
|
|
963
|
+
const precision = (orderType === ORDER_TYPES.SELL) ? mgr.assets.assetA.precision : mgr.assets.assetB.precision;
|
|
964
|
+
let filledAmount = 0;
|
|
965
|
+
if (orderType === ORDER_TYPES.SELL) {
|
|
966
|
+
if (paysAssetId === mgr.assets.assetA.id)
|
|
967
|
+
filledAmount = blockchainToFloat(paysAmountRaw, precision, true);
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
if (paysAssetId === mgr.assets.assetB.id)
|
|
971
|
+
filledAmount = blockchainToFloat(paysAmountRaw, precision, true);
|
|
972
|
+
}
|
|
973
|
+
const currentSizeIntFromGrid = floatToBlockchainInt(currentSize, precision);
|
|
974
|
+
const rawForSaleInt = toFiniteNumber(matchedGridOrder?.rawOnChain?.for_sale, null);
|
|
975
|
+
const driftSignal = Number.isFinite(rawForSaleInt) && Math.round(rawForSaleInt) < currentSizeIntFromGrid;
|
|
976
|
+
return { orderType, currentSize, precision, filledAmount, currentSizeIntFromGrid, rawForSaleInt, driftSignal };
|
|
977
|
+
}
|
|
978
|
+
async _computeFillTransitionResult(mgr, params) {
|
|
979
|
+
const { matchedGridOrder, orderType, precision, filledAmount, filledAmountInt, currentSizeIntFromGrid, rawForSaleInt, chainRefetched, chainConfirmsEmpty, effectiveRawForSale, blockNum, historyId, isMaker } = params;
|
|
980
|
+
let resolvedChainConfirmsEmpty = chainConfirmsEmpty;
|
|
981
|
+
if (chainRefetched && Number.isFinite(effectiveRawForSale) && Math.round(effectiveRawForSale) <= 0) {
|
|
982
|
+
resolvedChainConfirmsEmpty = true;
|
|
983
|
+
}
|
|
984
|
+
const currentSizeInt = Number.isFinite(effectiveRawForSale)
|
|
985
|
+
? Math.max(0, Math.round(effectiveRawForSale))
|
|
986
|
+
: currentSizeIntFromGrid;
|
|
987
|
+
const newSizeInt = Math.max(0, currentSizeInt - filledAmountInt);
|
|
988
|
+
const newSize = blockchainToFloat(newSizeInt, precision, true);
|
|
989
|
+
if (Number.isFinite(effectiveRawForSale) && currentSizeInt !== currentSizeIntFromGrid) {
|
|
990
|
+
mgr.logger.log(`[SYNC] Using rawOnChain.for_sale baseline for ${matchedGridOrder.orderId}: raw=${currentSizeInt}, grid=${currentSizeIntFromGrid}` +
|
|
991
|
+
(chainRefetched ? ' (refetched)' : ''), 'debug');
|
|
992
|
+
}
|
|
993
|
+
const gridAlsoEmpty = currentSizeIntFromGrid <= 0;
|
|
994
|
+
let isEffectivelyFull = resolvedChainConfirmsEmpty || gridAlsoEmpty;
|
|
995
|
+
let ghostOrderId;
|
|
996
|
+
if (!isEffectivelyFull) {
|
|
997
|
+
const otherPrecision = (orderType === ORDER_TYPES.SELL) ? mgr.assets.assetB.precision : mgr.assets.assetA.precision;
|
|
998
|
+
const price = matchedGridOrder.price;
|
|
999
|
+
const otherSize = (orderType === ORDER_TYPES.SELL) ? newSize * price : newSize / price;
|
|
1000
|
+
if (floatToBlockchainInt(otherSize, otherPrecision) <= 0) {
|
|
1001
|
+
mgr.logger.log(`[SYNC] Order ${matchedGridOrder.orderId} (slot ${matchedGridOrder.id}) other-side (${otherSize}) rounds to 0. Treating as full fill to trigger rotation.`, 'info');
|
|
1002
|
+
isEffectivelyFull = true;
|
|
1003
|
+
ghostOrderId = matchedGridOrder.orderId;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
let fullUpdate;
|
|
1007
|
+
let ghostUpdate;
|
|
1008
|
+
let partialUpdate;
|
|
1009
|
+
let filledOrderResult;
|
|
1010
|
+
if (isEffectivelyFull) {
|
|
1011
|
+
if (ghostOrderId) {
|
|
1012
|
+
ghostUpdate = {
|
|
1013
|
+
...matchedGridOrder,
|
|
1014
|
+
size: 0,
|
|
1015
|
+
state: ORDER_STATES.PARTIAL,
|
|
1016
|
+
orderId: ghostOrderId,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
fullUpdate = convertToSpreadPlaceholder(matchedGridOrder);
|
|
1021
|
+
}
|
|
1022
|
+
filledOrderResult = { ...matchedGridOrder, blockNum, historyId, isMaker };
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
filledOrderResult = {
|
|
1026
|
+
...matchedGridOrder,
|
|
1027
|
+
size: filledAmount,
|
|
1028
|
+
isPartial: true,
|
|
1029
|
+
blockNum,
|
|
1030
|
+
historyId,
|
|
1031
|
+
isMaker
|
|
1032
|
+
};
|
|
1033
|
+
const { btsFeeState, ...matchedWithoutDeferredFee } = matchedGridOrder;
|
|
1034
|
+
let updatedOrder = { ...matchedWithoutDeferredFee, state: ORDER_STATES.PARTIAL };
|
|
1035
|
+
if (updatedOrder.rawOnChain && updatedOrder.rawOnChain.for_sale !== undefined) {
|
|
1036
|
+
const baselineForSale = (chainRefetched && Number.isFinite(effectiveRawForSale))
|
|
1037
|
+
? effectiveRawForSale
|
|
1038
|
+
: toFiniteNumber(updatedOrder.rawOnChain.for_sale);
|
|
1039
|
+
const nextForSaleInt = Math.max(0, Math.round(baselineForSale) - filledAmountInt);
|
|
1040
|
+
updatedOrder.rawOnChain = {
|
|
1041
|
+
...updatedOrder.rawOnChain,
|
|
1042
|
+
for_sale: String(nextForSaleInt),
|
|
1043
|
+
fetchedAt: Date.now()
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
const nextOrder = await applyChainSizeToGridOrder(mgr, updatedOrder, newSize);
|
|
1047
|
+
if (nextOrder) {
|
|
1048
|
+
updatedOrder = { ...updatedOrder, ...nextOrder };
|
|
1049
|
+
}
|
|
1050
|
+
partialUpdate = updatedOrder;
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
isFull: isEffectivelyFull,
|
|
1054
|
+
isGhost: !!ghostOrderId,
|
|
1055
|
+
ghostOrderId,
|
|
1056
|
+
filledOrder: filledOrderResult,
|
|
1057
|
+
fullUpdate,
|
|
1058
|
+
ghostUpdate,
|
|
1059
|
+
partialUpdate,
|
|
1060
|
+
newSize
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
966
1063
|
/**
|
|
967
1064
|
* Process one incremental fill-history event.
|
|
968
1065
|
* @param {Object} fill - Fill history event object
|
|
@@ -981,7 +1078,7 @@ class SyncEngine {
|
|
|
981
1078
|
if (fillOp.is_maker === undefined) {
|
|
982
1079
|
mgr.logger.log(`[SYNC] is_maker flag missing from fill data for order ${orderId}; defaulting to maker`, 'warn');
|
|
983
1080
|
}
|
|
984
|
-
const isMaker = fillOp.is_maker !== false;
|
|
1081
|
+
const isMaker = fillOp.is_maker !== false;
|
|
985
1082
|
const fillKey = buildFillKey({ orderId, blockNum, historyId });
|
|
986
1083
|
const paysAmountRaw = toFiniteNumber(fillOp.pays?.amount);
|
|
987
1084
|
const paysAssetId = fillOp.pays ? fillOp.pays.asset_id : null;
|
|
@@ -992,231 +1089,96 @@ class SyncEngine {
|
|
|
992
1089
|
mgr.logger.log(`[SYNC] Missing replay-safe fill key for order ${orderId} block ${blockNum}; deferring to open-orders sync`, 'warn');
|
|
993
1090
|
return { filledOrders: [], updatedOrders: [], partialFill: false, requiresOpenOrdersSync: true };
|
|
994
1091
|
}
|
|
995
|
-
// Lock the order to prevent concurrent modifications from createOrder/cancelOrder/sync
|
|
996
|
-
// TOCTOU: lock before processFillAccounting so _buildBtsDeferredRefundAdjustment
|
|
997
|
-
// reads order state (btsFeeState) under the same lock as the state update below.
|
|
998
1092
|
const orderIdsToLock = new Set([orderId]);
|
|
999
1093
|
mgr.lockOrders([...orderIdsToLock]);
|
|
1000
1094
|
try {
|
|
1001
1095
|
mgr.pauseFundRecalc();
|
|
1002
1096
|
try {
|
|
1003
|
-
// Optimistically update account totals to reflect the fill.
|
|
1004
|
-
// Under lock — btsFeeState on mgr.orders is stable for _buildBtsDeferredRefundAdjustment.
|
|
1005
1097
|
const appliedAccounting = await mgr.accountant.processFillAccounting(fillOp, fillKey, { persistenceMode });
|
|
1006
1098
|
if (!appliedAccounting) {
|
|
1007
1099
|
mgr.logger.log(`[SYNC] Replay detected for fill ${fillKey}; skipping duplicate order mutation`, 'debug');
|
|
1008
1100
|
return { filledOrders: [], updatedOrders: [], partialFill: false };
|
|
1009
1101
|
}
|
|
1010
|
-
const paysAmount = paysAmountRaw;
|
|
1011
1102
|
const assetAPrecision = mgr.assets?.assetA?.precision;
|
|
1012
1103
|
const assetBPrecision = mgr.assets?.assetB?.precision;
|
|
1013
1104
|
if (assetAPrecision === undefined || assetBPrecision === undefined) {
|
|
1014
1105
|
mgr.logger?.log?.('Error: manager.assets precision missing in syncFromFillHistory', 'error');
|
|
1015
1106
|
return { filledOrders: [], updatedOrders: [], partialFill: false };
|
|
1016
1107
|
}
|
|
1017
|
-
|
|
1018
|
-
for (const gridOrder of mgr.orders.values()) {
|
|
1019
|
-
if (gridOrder.orderId === orderId && (gridOrder.state === ORDER_STATES.ACTIVE || gridOrder.state === ORDER_STATES.PARTIAL)) {
|
|
1020
|
-
matchedGridOrder = gridOrder;
|
|
1021
|
-
break;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1108
|
+
const matchedGridOrder = this._findMatchingGridOrder(mgr, orderId);
|
|
1024
1109
|
if (!matchedGridOrder) {
|
|
1025
1110
|
mgr.logger.log(`[SYNC] Fill for order ${orderId} ignored: order not found in active grid.`, 'debug');
|
|
1026
1111
|
return { filledOrders: [], updatedOrders: [], partialFill: false };
|
|
1027
1112
|
}
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const precision = (orderType === ORDER_TYPES.SELL) ? assetAPrecision : assetBPrecision;
|
|
1032
|
-
let filledAmount = 0;
|
|
1033
|
-
if (orderType === ORDER_TYPES.SELL) {
|
|
1034
|
-
if (paysAssetId === mgr.assets.assetA.id)
|
|
1035
|
-
filledAmount = blockchainToFloat(paysAmount, precision, true);
|
|
1036
|
-
}
|
|
1037
|
-
else {
|
|
1038
|
-
if (paysAssetId === mgr.assets.assetB.id)
|
|
1039
|
-
filledAmount = blockchainToFloat(paysAmount, precision, true);
|
|
1040
|
-
}
|
|
1041
|
-
mgr.logger.log(`[SYNC] Order ${orderId} (${orderType}) currentSize=${currentSize}, filledAmount=${filledAmount}`, 'debug');
|
|
1042
|
-
const currentSizeIntFromGrid = floatToBlockchainInt(currentSize, precision);
|
|
1043
|
-
const rawForSaleInt = toFiniteNumber(matchedGridOrder?.rawOnChain?.for_sale, null);
|
|
1044
|
-
let effectiveRawForSale = rawForSaleInt;
|
|
1113
|
+
const ctx = this._computeFillContext(mgr, matchedGridOrder, paysAssetId, paysAmountRaw);
|
|
1114
|
+
mgr.logger.log(`[SYNC] Order ${orderId} (${ctx.orderType}) currentSize=${ctx.currentSize}, filledAmount=${ctx.filledAmount}`, 'debug');
|
|
1115
|
+
let effectiveRawForSale = ctx.rawForSaleInt;
|
|
1045
1116
|
let chainConfirmsEmpty = false;
|
|
1046
1117
|
let chainRefetched = false;
|
|
1047
|
-
|
|
1048
|
-
// Drift-only refetch: the cache is self-correcting across
|
|
1049
|
-
// sequential websocket fills, but if a fill was ever missed
|
|
1050
|
-
// (reconnect, restart, reorg, external activity), the cache and
|
|
1051
|
-
// the grid can disagree.
|
|
1052
|
-
// The only signal we can detect from the in-memory state is a
|
|
1053
|
-
// consistent drift: cached for_sale < grid size. Chain `for_sale`
|
|
1054
|
-
// can only decrease, so a smaller cached value is an obvious
|
|
1055
|
-
// inconsistency that warrants a targeted refetch.
|
|
1056
|
-
//
|
|
1057
|
-
// We do NOT refetch on a time-based TTL. A stale-but-consistent
|
|
1058
|
-
// cache is still correct; a fresh-but-wrong cache is the actual
|
|
1059
|
-
// bug we're guarding against, and the drift signal catches that.
|
|
1060
|
-
// The refetch is best-effort: a network error falls back to the
|
|
1061
|
-
// cache with a warning, and the open-orders sync is the ultimate
|
|
1062
|
-
// recovery channel.
|
|
1063
|
-
// -----------------------------------------------------------------
|
|
1064
|
-
const driftSignal = Number.isFinite(rawForSaleInt)
|
|
1065
|
-
&& Math.round(rawForSaleInt) < currentSizeIntFromGrid;
|
|
1066
|
-
if (driftSignal) {
|
|
1118
|
+
if (ctx.driftSignal) {
|
|
1067
1119
|
try {
|
|
1068
|
-
const
|
|
1069
|
-
const fresh = await readSingleOrder(orderId, 3000);
|
|
1120
|
+
const fresh = await chainOrders.readSingleOrder(orderId, 3000);
|
|
1070
1121
|
if (fresh) {
|
|
1071
1122
|
const freshForSale = toFiniteNumber(fresh.for_sale, null);
|
|
1072
1123
|
if (Number.isFinite(freshForSale)) {
|
|
1073
1124
|
effectiveRawForSale = freshForSale;
|
|
1074
1125
|
chainRefetched = true;
|
|
1075
|
-
mgr.logger.log(`[SYNC] Drift detected for ${orderId} (cached=${rawForSaleInt} < grid=${currentSizeIntFromGrid}); ` +
|
|
1126
|
+
mgr.logger.log(`[SYNC] Drift detected for ${orderId} (cached=${ctx.rawForSaleInt} < grid=${ctx.currentSizeIntFromGrid}); ` +
|
|
1076
1127
|
`refetched for_sale=${freshForSale}`, 'warn');
|
|
1077
1128
|
}
|
|
1078
1129
|
}
|
|
1079
1130
|
else {
|
|
1080
|
-
// Chain returned null — the order is genuinely absent.
|
|
1081
|
-
// This is the authoritative "chain confirms empty"
|
|
1082
|
-
// signal (vs. an RPC failure which would have thrown).
|
|
1083
1131
|
chainConfirmsEmpty = true;
|
|
1084
1132
|
mgr.logger.log(`[SYNC] Drift refetch for ${orderId} returned null; chain confirms empty`, 'info');
|
|
1085
1133
|
}
|
|
1086
1134
|
}
|
|
1087
1135
|
catch (refetchErr) {
|
|
1088
|
-
mgr.logger.log(`[SYNC] Drift refetch for ${orderId} failed; falling back to cache:
|
|
1089
|
-
`${refetchErr?.message || refetchErr}`, 'warn');
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
// Chain-confirmed-empty gate: only true when the drift refetch
|
|
1093
|
-
// returned null (i.e., the chain is definitively saying the order
|
|
1094
|
-
// is gone). We deliberately do not trust a cached 0 from a
|
|
1095
|
-
// never-refetched source — that would misclassify a 0.003 XRP
|
|
1096
|
-
// residual as a full fill.
|
|
1097
|
-
if (chainRefetched
|
|
1098
|
-
&& Number.isFinite(effectiveRawForSale)
|
|
1099
|
-
&& Math.round(effectiveRawForSale) <= 0) {
|
|
1100
|
-
chainConfirmsEmpty = true;
|
|
1101
|
-
}
|
|
1102
|
-
const currentSizeInt = Number.isFinite(effectiveRawForSale)
|
|
1103
|
-
? Math.max(0, Math.round(effectiveRawForSale))
|
|
1104
|
-
: currentSizeIntFromGrid;
|
|
1105
|
-
const filledAmountInt = floatToBlockchainInt(filledAmount, precision);
|
|
1106
|
-
const newSizeInt = Math.max(0, currentSizeInt - filledAmountInt);
|
|
1107
|
-
const newSize = blockchainToFloat(newSizeInt, precision, true); // needed for partial fills
|
|
1108
|
-
if (Number.isFinite(effectiveRawForSale) && currentSizeInt !== currentSizeIntFromGrid) {
|
|
1109
|
-
mgr.logger.log(`[SYNC] Using rawOnChain.for_sale baseline for ${orderId}: raw=${currentSizeInt}, grid=${currentSizeIntFromGrid}` +
|
|
1110
|
-
(chainRefetched ? ' (refetched)' : ''), 'debug');
|
|
1111
|
-
}
|
|
1112
|
-
// CRITICAL (v0.5.1 Robustness + chain-confirmed-empty gate):
|
|
1113
|
-
// An order is "effectively" full if EITHER:
|
|
1114
|
-
// 1. The chain has confirmed the order is gone (the drift
|
|
1115
|
-
// refetch above returned null), OR
|
|
1116
|
-
// 2. The grid itself is at 0 (no drift between grid and chain), OR
|
|
1117
|
-
// 3. The OTHER side rounds to 0 (the chain will close the order
|
|
1118
|
-
// regardless of the residual on the size side — see
|
|
1119
|
-
// test_ghost_order_fix.ts for the intended behaviour).
|
|
1120
|
-
//
|
|
1121
|
-
// We deliberately do NOT use `newSizeInt <= 0` as a fast-path
|
|
1122
|
-
// full-fill signal: with a stale cached rawOnChain, that arithmetic
|
|
1123
|
-
// can be wrong and misclassify a partial fill as full. The
|
|
1124
|
-
// 30-satoshi residual that triggered the bot-stale incident is the
|
|
1125
|
-
// motivating case.
|
|
1126
|
-
const gridAlsoEmpty = currentSizeIntFromGrid <= 0;
|
|
1127
|
-
let isEffectivelyFull = chainConfirmsEmpty || gridAlsoEmpty;
|
|
1128
|
-
let ghostOrderId;
|
|
1129
|
-
if (!isEffectivelyFull) {
|
|
1130
|
-
// Check the "other" side's precision. If the remaining amount to receive/pay
|
|
1131
|
-
// rounds to 0 on the blockchain, the order will be closed regardless of newSizeInt.
|
|
1132
|
-
const otherPrecision = (orderType === ORDER_TYPES.SELL) ? assetBPrecision : assetAPrecision;
|
|
1133
|
-
const price = matchedGridOrder.price;
|
|
1134
|
-
const otherSize = (orderType === ORDER_TYPES.SELL) ? newSize * price : newSize / price;
|
|
1135
|
-
if (floatToBlockchainInt(otherSize, otherPrecision) <= 0) {
|
|
1136
|
-
mgr.logger.log(`[SYNC] Order ${orderId} (slot ${matchedGridOrder.id}) other-side (${otherSize}) rounds to 0. Treating as full fill to trigger rotation.`, 'info');
|
|
1137
|
-
isEffectivelyFull = true;
|
|
1138
|
-
ghostOrderId = matchedGridOrder.orderId;
|
|
1136
|
+
mgr.logger.log(`[SYNC] Drift refetch for ${orderId} failed; falling back to cache: ${refetchErr?.message || refetchErr}`, 'warn');
|
|
1139
1137
|
}
|
|
1140
1138
|
}
|
|
1139
|
+
const result = await this._computeFillTransitionResult(mgr, {
|
|
1140
|
+
matchedGridOrder,
|
|
1141
|
+
orderType: ctx.orderType,
|
|
1142
|
+
precision: ctx.precision,
|
|
1143
|
+
filledAmount: ctx.filledAmount,
|
|
1144
|
+
filledAmountInt: floatToBlockchainInt(ctx.filledAmount, ctx.precision),
|
|
1145
|
+
currentSizeIntFromGrid: ctx.currentSizeIntFromGrid,
|
|
1146
|
+
rawForSaleInt: ctx.rawForSaleInt,
|
|
1147
|
+
chainRefetched,
|
|
1148
|
+
chainConfirmsEmpty,
|
|
1149
|
+
effectiveRawForSale,
|
|
1150
|
+
blockNum,
|
|
1151
|
+
historyId,
|
|
1152
|
+
isMaker
|
|
1153
|
+
});
|
|
1141
1154
|
const filledOrders = [];
|
|
1142
1155
|
const updatedOrders = [];
|
|
1143
|
-
if (
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
historyId: historyId,
|
|
1148
|
-
isMaker: isMaker // Preserve maker/taker flag for accurate fee calculation
|
|
1149
|
-
};
|
|
1150
|
-
if (ghostOrderId) {
|
|
1151
|
-
// Ghost order: the chain order still exists (other-side
|
|
1152
|
-
// rounding to 0) but the chain hasn't closed it yet. Keep
|
|
1153
|
-
// the slot PARTIAL with its orderId so guards
|
|
1154
|
-
// (validateCreateTargetSlots, reconcileGrid) see the slot
|
|
1155
|
-
// as occupied and will NOT generate a duplicate CREATE.
|
|
1156
|
-
// The best-effort cancel in dexbot_class cleans up the
|
|
1157
|
-
// chain order; the next sync cycle virtualizes the
|
|
1158
|
-
// confirmed-free slot.
|
|
1159
|
-
mgr.logger.log(`[SYNC] Ghost full fill for order ${orderId} (slot ${matchedGridOrder.id}): ` +
|
|
1160
|
-
`preserving orderId ${ghostOrderId} as PARTIAL to block duplicate CREATE.`, 'info');
|
|
1161
|
-
const ghostOrder = {
|
|
1162
|
-
...matchedGridOrder,
|
|
1163
|
-
size: 0,
|
|
1164
|
-
state: ORDER_STATES.PARTIAL,
|
|
1165
|
-
orderId: ghostOrderId,
|
|
1166
|
-
};
|
|
1167
|
-
const ghostOk = await mgr._updateOrder(ghostOrder, 'handle-fill-ghost', { skipAccounting: false, fee: 0 });
|
|
1156
|
+
if (result.isFull) {
|
|
1157
|
+
if (result.isGhost) {
|
|
1158
|
+
mgr.logger.log(`[SYNC] Ghost full fill for order ${orderId} (slot ${matchedGridOrder.id}): preserving orderId ${result.ghostOrderId} as PARTIAL to block duplicate CREATE.`, 'info');
|
|
1159
|
+
const ghostOk = await mgr._updateOrder(result.ghostUpdate, 'handle-fill-ghost', { skipAccounting: false, fee: 0 });
|
|
1168
1160
|
if (ghostOk === false) {
|
|
1169
1161
|
mgr.logger.log(`[SYNC] Failed to apply ghost fill state for order ${orderId}`, 'warn');
|
|
1170
1162
|
}
|
|
1171
|
-
filledOrders.push(filledOrder);
|
|
1172
|
-
return { filledOrders, updatedOrders, partialFill: false, ghostOrderId };
|
|
1163
|
+
filledOrders.push(result.filledOrder);
|
|
1164
|
+
return { filledOrders, updatedOrders, partialFill: false, ghostOrderId: result.ghostOrderId };
|
|
1173
1165
|
}
|
|
1174
1166
|
mgr.logger.log(`[SYNC] Full fill for order ${orderId} (slot ${matchedGridOrder.id}).`, 'info');
|
|
1175
|
-
const
|
|
1176
|
-
const fullOk = await mgr._updateOrder(spreadOrder, 'handle-fill-full', { skipAccounting: false, fee: 0 });
|
|
1167
|
+
const fullOk = await mgr._updateOrder(result.fullUpdate, 'handle-fill-full', { skipAccounting: false, fee: 0 });
|
|
1177
1168
|
if (fullOk === false) {
|
|
1178
1169
|
mgr.logger.log(`[SYNC] Failed to convert filled order ${orderId} to spread placeholder`, 'warn');
|
|
1179
1170
|
}
|
|
1180
|
-
filledOrders.push(filledOrder);
|
|
1171
|
+
filledOrders.push(result.filledOrder);
|
|
1181
1172
|
return { filledOrders, updatedOrders, partialFill: false };
|
|
1182
1173
|
}
|
|
1183
1174
|
else {
|
|
1184
|
-
mgr.logger.log(`[SYNC] Partial fill for order ${orderId} (slot ${matchedGridOrder.id}): newSize=${newSize}`, 'info');
|
|
1185
|
-
const
|
|
1186
|
-
...matchedGridOrder,
|
|
1187
|
-
size: filledAmount,
|
|
1188
|
-
isPartial: true,
|
|
1189
|
-
blockNum: blockNum,
|
|
1190
|
-
historyId: historyId,
|
|
1191
|
-
isMaker: isMaker // Preserve maker/taker flag for accurate fee calculation
|
|
1192
|
-
};
|
|
1193
|
-
const { btsFeeState, ...matchedWithoutDeferredFee } = matchedGridOrder;
|
|
1194
|
-
let updatedOrder = { ...matchedWithoutDeferredFee, state: ORDER_STATES.PARTIAL };
|
|
1195
|
-
// Update cached raw order integer instead of deleting it.
|
|
1196
|
-
// If the drift refetch above pulled a fresh chain value, use
|
|
1197
|
-
// it as the new baseline (subtracting this fill) and stamp
|
|
1198
|
-
// fetchedAt so the cache is consistent for the next fill.
|
|
1199
|
-
if (updatedOrder.rawOnChain && updatedOrder.rawOnChain.for_sale !== undefined) {
|
|
1200
|
-
const baselineForSale = (chainRefetched && Number.isFinite(effectiveRawForSale))
|
|
1201
|
-
? effectiveRawForSale
|
|
1202
|
-
: toFiniteNumber(updatedOrder.rawOnChain.for_sale);
|
|
1203
|
-
const nextForSaleInt = Math.max(0, Math.round(baselineForSale) - filledAmountInt);
|
|
1204
|
-
updatedOrder.rawOnChain = {
|
|
1205
|
-
...updatedOrder.rawOnChain,
|
|
1206
|
-
for_sale: String(nextForSaleInt),
|
|
1207
|
-
fetchedAt: Date.now()
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
const nextOrder = await applyChainSizeToGridOrder(mgr, updatedOrder, newSize);
|
|
1211
|
-
if (nextOrder) {
|
|
1212
|
-
updatedOrder = { ...updatedOrder, ...nextOrder };
|
|
1213
|
-
}
|
|
1214
|
-
const partialOk = await mgr._updateOrder(updatedOrder, 'handle-fill-partial', { skipAccounting: false, fee: 0 });
|
|
1175
|
+
mgr.logger.log(`[SYNC] Partial fill for order ${orderId} (slot ${matchedGridOrder.id}): newSize=${result.newSize}`, 'info');
|
|
1176
|
+
const partialOk = await mgr._updateOrder(result.partialUpdate, 'handle-fill-partial', { skipAccounting: false, fee: 0 });
|
|
1215
1177
|
if (partialOk === false) {
|
|
1216
1178
|
mgr.logger.log(`[SYNC] Failed to update partially filled order ${orderId}`, 'warn');
|
|
1217
1179
|
}
|
|
1218
|
-
updatedOrders.push(
|
|
1219
|
-
filledOrders.push(
|
|
1180
|
+
updatedOrders.push(result.partialUpdate);
|
|
1181
|
+
filledOrders.push(result.filledOrder);
|
|
1220
1182
|
return { filledOrders, updatedOrders, partialFill: true };
|
|
1221
1183
|
}
|
|
1222
1184
|
}
|
|
@@ -1228,6 +1190,225 @@ class SyncEngine {
|
|
|
1228
1190
|
mgr.unlockOrders([...orderIdsToLock]);
|
|
1229
1191
|
}
|
|
1230
1192
|
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Process multiple fill-history events in a batch, acquiring _gridLock once.
|
|
1195
|
+
*
|
|
1196
|
+
* Locking: all order IDs locked once up-front; fund recalc paused once.
|
|
1197
|
+
*
|
|
1198
|
+
* @param {Array} fills - Array of fill history event objects (same block group)
|
|
1199
|
+
* @param {Object} [options] - Persistence mode options
|
|
1200
|
+
* @returns {Promise<import('./types').BatchSyncResult>}
|
|
1201
|
+
*/
|
|
1202
|
+
async syncFromFillHistoryBatch(fills, options = {}) {
|
|
1203
|
+
const mgr = this.manager;
|
|
1204
|
+
const persistenceMode = resolveProcessedFillPersistenceMode(options);
|
|
1205
|
+
if (!Array.isArray(fills) || fills.length === 0) {
|
|
1206
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false, ghostOrderIds: [] };
|
|
1207
|
+
}
|
|
1208
|
+
// Phase 1: Extract & validate fill data
|
|
1209
|
+
const fillEntries = [];
|
|
1210
|
+
let anyRequiresSync = false;
|
|
1211
|
+
for (const fill of fills) {
|
|
1212
|
+
if (!fill || !fill.op || !fill.op[1])
|
|
1213
|
+
continue;
|
|
1214
|
+
const fillOp = fill.op[1];
|
|
1215
|
+
const blockNum = fill.block_num;
|
|
1216
|
+
const historyId = fill.id;
|
|
1217
|
+
const orderId = fillOp.order_id;
|
|
1218
|
+
if (fillOp.is_maker === undefined) {
|
|
1219
|
+
mgr.logger.log(`[SYNC] is_maker flag missing from fill data for order ${orderId}; defaulting to maker`, 'warn');
|
|
1220
|
+
}
|
|
1221
|
+
const isMaker = fillOp.is_maker !== false;
|
|
1222
|
+
const fillKey = buildFillKey({ orderId, blockNum, historyId });
|
|
1223
|
+
if (!fillKey) {
|
|
1224
|
+
mgr.logger.log(`[SYNC] Missing replay-safe fill key for order ${orderId} block ${blockNum}; deferring to open-orders sync`, 'warn');
|
|
1225
|
+
anyRequiresSync = true;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
const paysAmountRaw = toFiniteNumber(fillOp.pays?.amount);
|
|
1229
|
+
const paysAssetId = fillOp.pays ? fillOp.pays.asset_id : null;
|
|
1230
|
+
const receivesAmountRaw = toFiniteNumber(fillOp.receives?.amount);
|
|
1231
|
+
const receivesAssetId = fillOp.receives ? fillOp.receives.asset_id : null;
|
|
1232
|
+
mgr.logger.log(`[SYNC] Processing fill ${historyId} at block ${blockNum} for order ${orderId} (maker=${isMaker}). Pays=${paysAmountRaw}@${paysAssetId}, Receives=${receivesAmountRaw}@${receivesAssetId}`, 'debug');
|
|
1233
|
+
fillEntries.push({
|
|
1234
|
+
fill, fillOp, blockNum, historyId, orderId, fillKey,
|
|
1235
|
+
isMaker, paysAmountRaw, paysAssetId, receivesAmountRaw, receivesAssetId
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
if (fillEntries.length === 0) {
|
|
1239
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false, ghostOrderIds: [], requiresOpenOrdersSync: anyRequiresSync };
|
|
1240
|
+
}
|
|
1241
|
+
// Phase 2: Lock all unique order IDs once
|
|
1242
|
+
const allOrderIds = [...new Set(fillEntries.map(e => e.orderId))];
|
|
1243
|
+
mgr.lockOrders(allOrderIds);
|
|
1244
|
+
try {
|
|
1245
|
+
mgr.pauseFundRecalc();
|
|
1246
|
+
try {
|
|
1247
|
+
// Phase 3: Process accounting for each fill
|
|
1248
|
+
const validEntries = [];
|
|
1249
|
+
for (const entry of fillEntries) {
|
|
1250
|
+
try {
|
|
1251
|
+
const appliedAccounting = await mgr.accountant.processFillAccounting(entry.fillOp, entry.fillKey, { persistenceMode });
|
|
1252
|
+
if (!appliedAccounting) {
|
|
1253
|
+
mgr.logger.log(`[SYNC] Replay detected for fill ${entry.fillKey}; skipping duplicate order mutation`, 'debug');
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
validEntries.push(entry);
|
|
1257
|
+
}
|
|
1258
|
+
catch (acctErr) {
|
|
1259
|
+
mgr.logger.log(`[SYNC] Accounting error for fill ${entry.fillKey}: ${acctErr.message}`, 'error');
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (validEntries.length === 0) {
|
|
1264
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false, ghostOrderIds: [], requiresOpenOrdersSync: anyRequiresSync };
|
|
1265
|
+
}
|
|
1266
|
+
// Phase 4: Build per-fill context + identify drift candidates
|
|
1267
|
+
const entryContexts = [];
|
|
1268
|
+
const driftOrderIds = new Set();
|
|
1269
|
+
const assetAPrecision = mgr.assets?.assetA?.precision;
|
|
1270
|
+
const assetBPrecision = mgr.assets?.assetB?.precision;
|
|
1271
|
+
if (assetAPrecision === undefined || assetBPrecision === undefined) {
|
|
1272
|
+
mgr.logger?.log?.('Error: manager.assets precision missing in syncFromFillHistoryBatch', 'error');
|
|
1273
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false, ghostOrderIds: [], requiresOpenOrdersSync: anyRequiresSync };
|
|
1274
|
+
}
|
|
1275
|
+
for (const entry of validEntries) {
|
|
1276
|
+
const { orderId, paysAmountRaw, paysAssetId } = entry;
|
|
1277
|
+
const matchedGridOrder = this._findMatchingGridOrder(mgr, orderId);
|
|
1278
|
+
if (!matchedGridOrder) {
|
|
1279
|
+
mgr.logger.log(`[SYNC] Fill for order ${orderId} ignored: order not found in active grid.`, 'debug');
|
|
1280
|
+
anyRequiresSync = true;
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
const ctx = this._computeFillContext(mgr, matchedGridOrder, paysAssetId, paysAmountRaw);
|
|
1284
|
+
mgr.logger.log(`[SYNC] Order ${orderId} (${ctx.orderType}) currentSize=${ctx.currentSize}, filledAmount=${ctx.filledAmount}`, 'debug');
|
|
1285
|
+
if (ctx.driftSignal) {
|
|
1286
|
+
driftOrderIds.add(orderId);
|
|
1287
|
+
}
|
|
1288
|
+
entryContexts.push({
|
|
1289
|
+
entry, matchedGridOrder,
|
|
1290
|
+
orderType: ctx.orderType,
|
|
1291
|
+
precision: ctx.precision,
|
|
1292
|
+
filledAmount: ctx.filledAmount,
|
|
1293
|
+
filledAmountInt: floatToBlockchainInt(ctx.filledAmount, ctx.precision),
|
|
1294
|
+
currentSizeIntFromGrid: ctx.currentSizeIntFromGrid,
|
|
1295
|
+
rawForSaleInt: ctx.rawForSaleInt
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
// Phase 5: Batch drift refetch — one get_objects call for all order IDs
|
|
1299
|
+
const refetchMap = new Map();
|
|
1300
|
+
if (driftOrderIds.size > 0) {
|
|
1301
|
+
try {
|
|
1302
|
+
const batchResults = await chainOrders.batchReadOrders([...driftOrderIds], 3000);
|
|
1303
|
+
for (const [orderId, freshOrder] of batchResults) {
|
|
1304
|
+
if (freshOrder) {
|
|
1305
|
+
const freshForSale = toFiniteNumber(freshOrder.for_sale, null);
|
|
1306
|
+
if (Number.isFinite(freshForSale)) {
|
|
1307
|
+
refetchMap.set(orderId, {
|
|
1308
|
+
chainConfirmsEmpty: Math.round(freshForSale) <= 0,
|
|
1309
|
+
chainRefetched: true,
|
|
1310
|
+
effectiveRawForSale: freshForSale
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
refetchMap.set(orderId, {
|
|
1315
|
+
chainConfirmsEmpty: false,
|
|
1316
|
+
chainRefetched: true,
|
|
1317
|
+
effectiveRawForSale: null
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
else {
|
|
1322
|
+
refetchMap.set(orderId, {
|
|
1323
|
+
chainConfirmsEmpty: true,
|
|
1324
|
+
chainRefetched: true,
|
|
1325
|
+
effectiveRawForSale: null
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
for (const [orderId, { chainRefetched, chainConfirmsEmpty, effectiveRawForSale }] of refetchMap) {
|
|
1330
|
+
if (chainRefetched && effectiveRawForSale != null) {
|
|
1331
|
+
mgr.logger.log(`[SYNC] Drift detected for ${orderId}; batch-refetched for_sale=${effectiveRawForSale}`, 'warn');
|
|
1332
|
+
}
|
|
1333
|
+
else if (chainConfirmsEmpty) {
|
|
1334
|
+
mgr.logger.log(`[SYNC] Drift refetch for ${orderId} returned null; chain confirms empty`, 'info');
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
catch (refetchErr) {
|
|
1339
|
+
mgr.logger.log(`[SYNC] Batch drift refetch failed for ${driftOrderIds.size} orders; falling back to cache: ${refetchErr?.message || refetchErr}`, 'warn');
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
// Phase 6: Compute state transitions and collect grid updates
|
|
1343
|
+
const gridUpdates = [];
|
|
1344
|
+
const filledOrders = [];
|
|
1345
|
+
const updatedOrders = [];
|
|
1346
|
+
const ghostOrderIds = [];
|
|
1347
|
+
let anyPartialFill = false;
|
|
1348
|
+
for (const ctx of entryContexts) {
|
|
1349
|
+
const { entry, matchedGridOrder, orderType, precision, filledAmount, filledAmountInt, currentSizeIntFromGrid, rawForSaleInt } = ctx;
|
|
1350
|
+
const { orderId, blockNum, historyId, isMaker } = entry;
|
|
1351
|
+
const refetchInfo = refetchMap.get(orderId);
|
|
1352
|
+
const chainRefetched = refetchInfo?.chainRefetched || false;
|
|
1353
|
+
const chainConfirmsEmpty = refetchInfo?.chainConfirmsEmpty || false;
|
|
1354
|
+
const effectiveRawForSale = refetchInfo?.effectiveRawForSale != null ? refetchInfo.effectiveRawForSale : rawForSaleInt;
|
|
1355
|
+
const result = await this._computeFillTransitionResult(mgr, {
|
|
1356
|
+
matchedGridOrder,
|
|
1357
|
+
orderType,
|
|
1358
|
+
precision,
|
|
1359
|
+
filledAmount,
|
|
1360
|
+
filledAmountInt,
|
|
1361
|
+
currentSizeIntFromGrid,
|
|
1362
|
+
rawForSaleInt,
|
|
1363
|
+
chainRefetched,
|
|
1364
|
+
chainConfirmsEmpty,
|
|
1365
|
+
effectiveRawForSale,
|
|
1366
|
+
blockNum,
|
|
1367
|
+
historyId,
|
|
1368
|
+
isMaker
|
|
1369
|
+
});
|
|
1370
|
+
if (result.isFull) {
|
|
1371
|
+
if (result.isGhost) {
|
|
1372
|
+
mgr.logger.log(`[SYNC] Ghost full fill for order ${orderId} (slot ${matchedGridOrder.id}): ` +
|
|
1373
|
+
`preserving orderId ${result.ghostOrderId} as PARTIAL to block duplicate CREATE.`, 'info');
|
|
1374
|
+
gridUpdates.push({ id: matchedGridOrder.id, ...result.ghostUpdate, context: 'handle-fill-ghost' });
|
|
1375
|
+
ghostOrderIds.push(result.ghostOrderId);
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
mgr.logger.log(`[SYNC] Full fill for order ${orderId} (slot ${matchedGridOrder.id}).`, 'info');
|
|
1379
|
+
gridUpdates.push({ id: matchedGridOrder.id, ...result.fullUpdate, context: 'handle-fill-full' });
|
|
1380
|
+
}
|
|
1381
|
+
filledOrders.push(result.filledOrder);
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
mgr.logger.log(`[SYNC] Partial fill for order ${orderId} (slot ${matchedGridOrder.id}): newSize=${result.newSize}`, 'info');
|
|
1385
|
+
gridUpdates.push({ id: matchedGridOrder.id, ...result.partialUpdate, context: 'handle-fill-partial' });
|
|
1386
|
+
updatedOrders.push(result.partialUpdate);
|
|
1387
|
+
filledOrders.push(result.filledOrder);
|
|
1388
|
+
anyPartialFill = true;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// Phase 7: Single grid batch update — acquires _gridLock once
|
|
1392
|
+
if (gridUpdates.length > 0) {
|
|
1393
|
+
const updateObjects = gridUpdates.map(u => {
|
|
1394
|
+
const { context, ...orderData } = u;
|
|
1395
|
+
return orderData;
|
|
1396
|
+
});
|
|
1397
|
+
const batchOk = await mgr.applyGridUpdateBatch(updateObjects, 'handle-fill-batch', { skipAccounting: false, fee: 0 });
|
|
1398
|
+
if (batchOk === false) {
|
|
1399
|
+
mgr.logger.log('[SYNC] Batch grid update failed for some fills; next sync cycle will reconcile', 'warn');
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return { filledOrders, updatedOrders, partialFill: anyPartialFill, ghostOrderIds, requiresOpenOrdersSync: anyRequiresSync };
|
|
1403
|
+
}
|
|
1404
|
+
finally {
|
|
1405
|
+
await mgr.resumeFundRecalc();
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
finally {
|
|
1409
|
+
mgr.unlockOrders(allOrderIds);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1231
1412
|
/**
|
|
1232
1413
|
* High-level dispatcher for different blockchain synchronization sources.
|
|
1233
1414
|
* Routes to the appropriate sync strategy based on the data source.
|
|
@@ -1513,14 +1694,13 @@ class SyncEngine {
|
|
|
1513
1694
|
const assetBId = mgr.assets?.assetB?.id;
|
|
1514
1695
|
if (!assetAId || !assetBId)
|
|
1515
1696
|
return;
|
|
1516
|
-
const { getOnChainAssetBalances } = require('../chain_orders');
|
|
1517
1697
|
const { NATIVE_CLIENT } = require('../constants');
|
|
1518
1698
|
const assetList = [assetAId, assetBId];
|
|
1519
1699
|
// For non-BTS pairs, also fetch core asset (BTS) balance for fee management
|
|
1520
1700
|
if (mgr.config.assetA !== 'BTS' && mgr.config.assetB !== 'BTS') {
|
|
1521
1701
|
assetList.push(NATIVE_CLIENT.CHAIN.CORE_ASSET_ID);
|
|
1522
1702
|
}
|
|
1523
|
-
const lookup = await getOnChainAssetBalances(accountIdOrName, assetList);
|
|
1703
|
+
const lookup = await chainOrders.getOnChainAssetBalances(accountIdOrName, assetList);
|
|
1524
1704
|
const aInfo = lookup?.[assetAId] || lookup?.[mgr.config.assetA];
|
|
1525
1705
|
const bInfo = lookup?.[assetBId] || lookup?.[mgr.config.assetB];
|
|
1526
1706
|
if (aInfo && bInfo) {
|