dexbot 1.4.6 → 1.4.8
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/credential-daemon.d.ts.map +1 -1
- package/dist/credential-daemon.js +416 -92
- package/dist/credential-daemon.js.map +1 -1
- package/dist/dexbot.d.ts.map +1 -1
- package/dist/dexbot.js +61 -27
- package/dist/dexbot.js.map +1 -1
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +14 -24
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +21 -0
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/broadcast_failure.d.ts +21 -0
- package/dist/modules/broadcast_failure.d.ts.map +1 -0
- package/dist/modules/broadcast_failure.js +95 -0
- package/dist/modules/broadcast_failure.js.map +1 -0
- package/dist/modules/chain_keys.d.ts +2 -2
- package/dist/modules/chain_keys.d.ts.map +1 -1
- package/dist/modules/chain_keys.js +30 -56
- package/dist/modules/chain_keys.js.map +1 -1
- package/dist/modules/chain_orders.d.ts +79 -1
- package/dist/modules/chain_orders.d.ts.map +1 -1
- package/dist/modules/chain_orders.js +154 -10
- package/dist/modules/chain_orders.js.map +1 -1
- package/dist/modules/constants.d.ts +1 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +21 -7
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/daemon_node_health.d.ts +33 -0
- package/dist/modules/daemon_node_health.d.ts.map +1 -0
- package/dist/modules/daemon_node_health.js +120 -0
- package/dist/modules/daemon_node_health.js.map +1 -0
- package/dist/modules/dexbot_class.d.ts +12 -3
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +19 -5
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +46 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +754 -64
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_credential_client.d.ts +15 -0
- package/dist/modules/dexbot_credential_client.d.ts.map +1 -1
- package/dist/modules/dexbot_credential_client.js +114 -141
- package/dist/modules/dexbot_credential_client.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +45 -15
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +5 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +238 -85
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +95 -33
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +24 -2
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/key_store.d.ts.map +1 -1
- package/dist/modules/key_store.js +5 -4
- package/dist/modules/key_store.js.map +1 -1
- package/dist/modules/launcher/credential_daemon.d.ts.map +1 -1
- package/dist/modules/launcher/credential_daemon.js +15 -3
- package/dist/modules/launcher/credential_daemon.js.map +1 -1
- package/dist/modules/node_failure_ledger.d.ts +39 -0
- package/dist/modules/node_failure_ledger.d.ts.map +1 -0
- package/dist/modules/node_failure_ledger.js +88 -0
- package/dist/modules/node_failure_ledger.js.map +1 -0
- package/dist/modules/node_health_cache.d.ts +2 -1
- package/dist/modules/node_health_cache.d.ts.map +1 -1
- package/dist/modules/node_health_cache.js +25 -0
- package/dist/modules/node_health_cache.js.map +1 -1
- package/dist/modules/node_manager.d.ts +3 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +41 -10
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +26 -20
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +281 -139
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +66 -14
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +136 -56
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +6 -5
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +388 -61
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +98 -4
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +379 -92
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +24 -1
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts +16 -3
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +341 -253
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +79 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +18 -0
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +44 -1
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +91 -5
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +5 -1
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +7 -5
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/withPoolRef.d.ts.map +1 -1
- package/dist/modules/order/utils/withPoolRef.js +39 -4
- package/dist/modules/order/utils/withPoolRef.js.map +1 -1
- package/dist/modules/socket_json_client.d.ts +14 -0
- package/dist/modules/socket_json_client.d.ts.map +1 -0
- package/dist/modules/socket_json_client.js +100 -0
- package/dist/modules/socket_json_client.js.map +1 -0
- package/package.json +2 -1
- package/scripts/clear-all.sh +4 -4
- package/scripts/clear-logs.sh +4 -4
|
@@ -392,6 +392,18 @@ class SyncEngine {
|
|
|
392
392
|
const preSyncCorrectionLen = Array.isArray(mgr.ordersNeedingPriceCorrection)
|
|
393
393
|
? mgr.ordersNeedingPriceCorrection.length
|
|
394
394
|
: 0;
|
|
395
|
+
// If fill accounting applied balances but the order update
|
|
396
|
+
// failed, flag forces a fresh chain-balance fetch to correct
|
|
397
|
+
// the negative free balance before the next sync cycle.
|
|
398
|
+
if (mgr.accountTotalsStale) {
|
|
399
|
+
mgr.accountTotalsStale = false;
|
|
400
|
+
try {
|
|
401
|
+
await mgr.fetchAccountTotals(mgr.accountId || mgr.config?.accountId);
|
|
402
|
+
}
|
|
403
|
+
catch (fetchErr) {
|
|
404
|
+
mgr.logger?.log?.(`[SYNC] Stale-totals refresh failed: ${(0, errors_1.getErrorMessage)(fetchErr)}`, 'warn');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
395
407
|
const innerResult = await this._doSyncFromOpenOrders(chainOrders, options);
|
|
396
408
|
// Re-check generation after sync completes: if forceRelease fired
|
|
397
409
|
// during the sync, this result is orphaned — discard it.
|
|
@@ -707,7 +719,12 @@ class SyncEngine {
|
|
|
707
719
|
else {
|
|
708
720
|
const spreadOrder = (0, order_1.convertToSpreadPlaceholder)(gridOrder);
|
|
709
721
|
await mgr._applyOrderUpdate(spreadOrder, 'sync-pass1-filled', { skipAccounting: skipAccounting, fee: 0 });
|
|
710
|
-
|
|
722
|
+
// Push the filled order with its REAL side (chain order
|
|
723
|
+
// type), not the SPREAD placeholder: downstream fill
|
|
724
|
+
// processing (deriveTargetBoundary) derives boundary
|
|
725
|
+
// movement from fill.type, and a SPREAD type would
|
|
726
|
+
// silently drop the shift for this completed order.
|
|
727
|
+
filledOrders.push({ ...gridOrder, type: chainOrder.type });
|
|
711
728
|
updatedOrders.push(spreadOrder);
|
|
712
729
|
}
|
|
713
730
|
}
|
|
@@ -759,7 +776,18 @@ class SyncEngine {
|
|
|
759
776
|
await mgr._applyOrderUpdate(spreadOrder, 'sync-cleanup-phantom', { skipAccounting: skipAccounting, fee: 0 });
|
|
760
777
|
// Only genuine disappearances (had orderId) count as fills.
|
|
761
778
|
if (hadOrderId) {
|
|
762
|
-
|
|
779
|
+
// A SPREAD slot can carry an on-chain order (spread-correction
|
|
780
|
+
// activation). Resolve the real side before pushing: the fill
|
|
781
|
+
// drives deriveTargetBoundary, which only shifts on BUY/SELL —
|
|
782
|
+
// a SPREAD-typed fill would silently drop the boundary crawl
|
|
783
|
+
// for this completed order. Same price-vs-startPrice convention
|
|
784
|
+
// as the funds check (manager.ts) since no chain order remains
|
|
785
|
+
// to resolve the side from.
|
|
786
|
+
let resolvedType = currentGridOrder.type;
|
|
787
|
+
if (resolvedType === constants_1.ORDER_TYPES.SPREAD) {
|
|
788
|
+
resolvedType = (0, order_1.resolveSpreadOrderSide)(currentGridOrder.price, mgr.config.startPrice);
|
|
789
|
+
}
|
|
790
|
+
filledOrders.push({ ...currentGridOrder, type: resolvedType });
|
|
763
791
|
}
|
|
764
792
|
}
|
|
765
793
|
}
|
|
@@ -1000,7 +1028,22 @@ class SyncEngine {
|
|
|
1000
1028
|
return null;
|
|
1001
1029
|
}
|
|
1002
1030
|
_computeFillContext(mgr, matchedGridOrder, paysAssetId, paysAmountRaw) {
|
|
1003
|
-
|
|
1031
|
+
// A SPREAD slot can carry an on-chain order (e.g. spread-correction
|
|
1032
|
+
// activation). Resolve the real side from the fill's pays asset so the
|
|
1033
|
+
// transition result is BUY/SELL — never SPREAD with an on-chain state,
|
|
1034
|
+
// which validateOrder rejects as fatal ILLEGAL_SPREAD_STATE. When the
|
|
1035
|
+
// pays asset matches neither side (malformed fill event), fall back to
|
|
1036
|
+
// the price-vs-startPrice convention so the type can never leak SPREAD
|
|
1037
|
+
// into a fill result (which would drop the boundary shift downstream).
|
|
1038
|
+
let orderType = matchedGridOrder.type;
|
|
1039
|
+
if (orderType === constants_1.ORDER_TYPES.SPREAD) {
|
|
1040
|
+
if (paysAssetId === mgr.assets.assetB.id)
|
|
1041
|
+
orderType = constants_1.ORDER_TYPES.BUY;
|
|
1042
|
+
else if (paysAssetId === mgr.assets.assetA.id)
|
|
1043
|
+
orderType = constants_1.ORDER_TYPES.SELL;
|
|
1044
|
+
else
|
|
1045
|
+
orderType = (0, order_1.resolveSpreadOrderSide)(matchedGridOrder.price, mgr.config.startPrice);
|
|
1046
|
+
}
|
|
1004
1047
|
const currentSize = toFiniteNumber(matchedGridOrder.size);
|
|
1005
1048
|
const precision = (orderType === constants_1.ORDER_TYPES.SELL) ? mgr.assets.assetA.precision : mgr.assets.assetB.precision;
|
|
1006
1049
|
let filledAmount = 0;
|
|
@@ -1035,49 +1078,45 @@ class SyncEngine {
|
|
|
1035
1078
|
}
|
|
1036
1079
|
const gridAlsoEmpty = currentSizeIntFromGrid <= 0;
|
|
1037
1080
|
let isEffectivelyFull = resolvedChainConfirmsEmpty || gridAlsoEmpty;
|
|
1038
|
-
let ghostOrderId;
|
|
1039
|
-
// P4: Inline dust detection — if remaining size is below the minimum
|
|
1040
|
-
// absolute order size, treat as full fill to trigger rotation instead
|
|
1041
|
-
// of leaving a sub-minimum remnant on chain that would accumulate as dust.
|
|
1042
|
-
if (!isEffectivelyFull && newSize > 0) {
|
|
1043
|
-
const minSize = (0, math_1.getMinAbsoluteOrderSize)(orderType, mgr.assets);
|
|
1044
|
-
if (newSize < minSize) {
|
|
1045
|
-
mgr.logger.log(`[SYNC] Order ${matchedGridOrder.orderId} (slot ${matchedGridOrder.id}) ` +
|
|
1046
|
-
`remaining size ${newSize} below minimum ${minSize}. Treating as full fill.`, 'info');
|
|
1047
|
-
isEffectivelyFull = true;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
1081
|
if (!isEffectivelyFull) {
|
|
1051
1082
|
const otherPrecision = (orderType === constants_1.ORDER_TYPES.SELL) ? mgr.assets.assetB.precision : mgr.assets.assetA.precision;
|
|
1052
|
-
const
|
|
1053
|
-
const otherSize = (orderType === constants_1.ORDER_TYPES.SELL) ? newSize *
|
|
1083
|
+
const otherSidePrice = matchedGridOrder.price;
|
|
1084
|
+
const otherSize = (orderType === constants_1.ORDER_TYPES.SELL) ? newSize * otherSidePrice : newSize / otherSidePrice;
|
|
1054
1085
|
if ((0, math_1.floatToBlockchainInt)(otherSize, otherPrecision) <= 0) {
|
|
1055
|
-
|
|
1086
|
+
// A fill event is authoritative: the order WAS on-chain and this
|
|
1087
|
+
// fill consumed the tradeable remainder. A fill is a fill — we do
|
|
1088
|
+
// not need to preserve the orderId as a "ghost" PARTIAL to block
|
|
1089
|
+
// a duplicate CREATE. Any residual dust that remains on-chain
|
|
1090
|
+
// after rounding is cancelled by the normal reconciliation /
|
|
1091
|
+
// duplicate-price cancel path, not by stalling the grid. Treating
|
|
1092
|
+
// every sub-dust other-side fill as a real full fill lets rotation
|
|
1093
|
+
// immediately plan the opposite-side replacement, preserving the
|
|
1094
|
+
// grid invariant (each filled side produces a new order on the
|
|
1095
|
+
// other side). This replaces the legacy ghost mechanism whose
|
|
1096
|
+
// preserved orderIds caused instantly-filled replacement orders
|
|
1097
|
+
// to deadlock the COW create pipeline.
|
|
1098
|
+
mgr.logger.log(`[SYNC] Order ${matchedGridOrder.orderId} (slot ${matchedGridOrder.id}) other-side (${otherSize}) rounds to 0. ` +
|
|
1099
|
+
`Fill is authoritative: treating as full fill for rotation; residual dust left for reconciliation cancel.`, 'info');
|
|
1056
1100
|
isEffectivelyFull = true;
|
|
1057
|
-
ghostOrderId = matchedGridOrder.orderId;
|
|
1058
1101
|
}
|
|
1059
1102
|
}
|
|
1060
1103
|
let fullUpdate;
|
|
1061
|
-
let ghostUpdate;
|
|
1062
1104
|
let partialUpdate;
|
|
1063
1105
|
let filledOrderResult;
|
|
1064
1106
|
if (isEffectivelyFull) {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
}
|
|
1073
|
-
else {
|
|
1074
|
-
fullUpdate = (0, order_1.convertToSpreadPlaceholder)(matchedGridOrder);
|
|
1075
|
-
}
|
|
1076
|
-
filledOrderResult = { ...matchedGridOrder, blockNum, historyId, isMaker };
|
|
1107
|
+
fullUpdate = (0, order_1.convertToSpreadPlaceholder)(matchedGridOrder);
|
|
1108
|
+
filledOrderResult = {
|
|
1109
|
+
...matchedGridOrder,
|
|
1110
|
+
type: orderType,
|
|
1111
|
+
blockNum,
|
|
1112
|
+
historyId,
|
|
1113
|
+
isMaker
|
|
1114
|
+
};
|
|
1077
1115
|
}
|
|
1078
1116
|
else {
|
|
1079
1117
|
filledOrderResult = {
|
|
1080
1118
|
...matchedGridOrder,
|
|
1119
|
+
type: orderType,
|
|
1081
1120
|
size: filledAmount,
|
|
1082
1121
|
isPartial: true,
|
|
1083
1122
|
blockNum,
|
|
@@ -1085,7 +1124,7 @@ class SyncEngine {
|
|
|
1085
1124
|
isMaker
|
|
1086
1125
|
};
|
|
1087
1126
|
const { btsFeeState, ...matchedWithoutDeferredFee } = matchedGridOrder;
|
|
1088
|
-
let updatedOrder = { ...matchedWithoutDeferredFee, state: constants_1.ORDER_STATES.PARTIAL };
|
|
1127
|
+
let updatedOrder = { ...matchedWithoutDeferredFee, type: orderType, state: constants_1.ORDER_STATES.PARTIAL };
|
|
1089
1128
|
if (updatedOrder.rawOnChain && updatedOrder.rawOnChain.for_sale !== undefined) {
|
|
1090
1129
|
const baselineForSale = (chainRefetched && Number.isFinite(effectiveRawForSale))
|
|
1091
1130
|
? effectiveRawForSale
|
|
@@ -1105,11 +1144,8 @@ class SyncEngine {
|
|
|
1105
1144
|
}
|
|
1106
1145
|
return {
|
|
1107
1146
|
isFull: isEffectivelyFull,
|
|
1108
|
-
isGhost: !!ghostOrderId,
|
|
1109
|
-
ghostOrderId,
|
|
1110
1147
|
filledOrder: filledOrderResult,
|
|
1111
1148
|
fullUpdate,
|
|
1112
|
-
ghostUpdate,
|
|
1113
1149
|
partialUpdate,
|
|
1114
1150
|
newSize
|
|
1115
1151
|
};
|
|
@@ -1143,105 +1179,124 @@ class SyncEngine {
|
|
|
1143
1179
|
mgr.logger.log(`[SYNC] Missing replay-safe fill key for order ${orderId} block ${blockNum}; deferring to open-orders sync`, 'warn');
|
|
1144
1180
|
return { filledOrders: [], updatedOrders: [], partialFill: false, requiresOpenOrdersSync: true };
|
|
1145
1181
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1182
|
+
// Raise the fill-batch in-flight guard BEFORE the accountTotals refresh
|
|
1183
|
+
// (mirrors syncFromFillHistoryBatch): the refresh returns post-fill
|
|
1184
|
+
// balances while the grid still holds the filled order as committed, so
|
|
1185
|
+
// a fund-invariant check in this window would fire a spurious CRITICAL.
|
|
1186
|
+
// The guard is lowered before the final consolidated recalc so the
|
|
1187
|
+
// settled (re-anchored) state is still verified.
|
|
1188
|
+
mgr._fillBatchInFlight = (mgr._fillBatchInFlight ?? 0) + 1;
|
|
1148
1189
|
try {
|
|
1149
|
-
mgr.
|
|
1190
|
+
const totalsGate = await mgr.refreshAccountTotalsIfStale();
|
|
1191
|
+
if (!totalsGate.ok) {
|
|
1192
|
+
mgr.logger.log(`[SYNC] Deferring fill ${historyId}: accountTotals refresh failed (stale snapshot). Will be re-processed on the next cycle.`, 'warn');
|
|
1193
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false, requiresOpenOrdersSync: false, deferred: true };
|
|
1194
|
+
}
|
|
1195
|
+
const orderIdsToLock = new Set([orderId]);
|
|
1196
|
+
mgr.lockOrders([...orderIdsToLock]);
|
|
1150
1197
|
try {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
mgr.
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
mgr.
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1198
|
+
mgr.pauseFundRecalc();
|
|
1199
|
+
try {
|
|
1200
|
+
const appliedAccounting = await mgr.accountant.processFillAccounting(fillOp, fillKey, { persistenceMode });
|
|
1201
|
+
if (!appliedAccounting) {
|
|
1202
|
+
mgr.logger.log(`[SYNC] Replay detected for fill ${fillKey}; skipping duplicate order mutation`, 'debug');
|
|
1203
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false };
|
|
1204
|
+
}
|
|
1205
|
+
const assetAPrecision = mgr.assets?.assetA?.precision;
|
|
1206
|
+
const assetBPrecision = mgr.assets?.assetB?.precision;
|
|
1207
|
+
if (assetAPrecision === undefined || assetBPrecision === undefined) {
|
|
1208
|
+
mgr.logger?.log?.('Error: manager.assets precision missing in syncFromFillHistory', 'error');
|
|
1209
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false };
|
|
1210
|
+
}
|
|
1211
|
+
const matchedGridOrder = this._findMatchingGridOrder(mgr, orderId);
|
|
1212
|
+
if (!matchedGridOrder) {
|
|
1213
|
+
mgr.logger.log(`[SYNC] Fill for order ${orderId} ignored: order not found in active grid.`, 'debug');
|
|
1214
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false };
|
|
1215
|
+
}
|
|
1216
|
+
const ctx = this._computeFillContext(mgr, matchedGridOrder, paysAssetId, paysAmountRaw);
|
|
1217
|
+
mgr.logger.log(`[SYNC] Order ${orderId} (${ctx.orderType}) currentSize=${ctx.currentSize}, filledAmount=${ctx.filledAmount}`, 'debug');
|
|
1218
|
+
let effectiveRawForSale = ctx.rawForSaleInt;
|
|
1219
|
+
let chainConfirmsEmpty = false;
|
|
1220
|
+
let chainRefetched = false;
|
|
1221
|
+
if (ctx.driftSignal) {
|
|
1222
|
+
try {
|
|
1223
|
+
const fresh = await chainOrders.readSingleOrder(orderId, 3000);
|
|
1224
|
+
if (fresh) {
|
|
1225
|
+
const freshForSale = toFiniteNumber(fresh.for_sale, undefined);
|
|
1226
|
+
if (Number.isFinite(freshForSale)) {
|
|
1227
|
+
effectiveRawForSale = freshForSale;
|
|
1228
|
+
chainRefetched = true;
|
|
1229
|
+
mgr.logger.log(`[SYNC] Drift detected for ${orderId} (cached=${ctx.rawForSaleInt} < grid=${ctx.currentSizeIntFromGrid}); ` +
|
|
1230
|
+
`refetched for_sale=${freshForSale}`, 'warn');
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
chainConfirmsEmpty = true;
|
|
1235
|
+
mgr.logger.log(`[SYNC] Drift refetch for ${orderId} returned null; chain confirms empty`, 'info');
|
|
1182
1236
|
}
|
|
1183
1237
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
mgr.logger.log(`[SYNC] Drift refetch for ${orderId} returned null; chain confirms empty`, 'info');
|
|
1238
|
+
catch (refetchErr) {
|
|
1239
|
+
mgr.logger.log(`[SYNC] Drift refetch for ${orderId} failed; falling back to cache: ${refetchErr?.message || refetchErr}`, 'warn');
|
|
1187
1240
|
}
|
|
1188
1241
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
mgr.logger.log(`[SYNC] Ghost full fill for order ${orderId} (slot ${matchedGridOrder.id}): preserving orderId ${result.ghostOrderId} as PARTIAL to block duplicate CREATE.`, 'info');
|
|
1213
|
-
const ghostOk = await mgr._updateOrder(result.ghostUpdate, 'handle-fill-ghost', { skipAccounting: false, fee: 0 });
|
|
1214
|
-
if (ghostOk === false) {
|
|
1215
|
-
mgr.logger.log(`[SYNC] Failed to apply ghost fill state for order ${orderId}`, 'warn');
|
|
1242
|
+
const result = await this._computeFillTransitionResult(mgr, {
|
|
1243
|
+
matchedGridOrder,
|
|
1244
|
+
orderType: ctx.orderType,
|
|
1245
|
+
precision: ctx.precision,
|
|
1246
|
+
filledAmount: ctx.filledAmount,
|
|
1247
|
+
filledAmountInt: (0, math_1.floatToBlockchainInt)(ctx.filledAmount, ctx.precision),
|
|
1248
|
+
currentSizeIntFromGrid: ctx.currentSizeIntFromGrid,
|
|
1249
|
+
rawForSaleInt: ctx.rawForSaleInt,
|
|
1250
|
+
chainRefetched,
|
|
1251
|
+
chainConfirmsEmpty,
|
|
1252
|
+
effectiveRawForSale,
|
|
1253
|
+
blockNum,
|
|
1254
|
+
historyId,
|
|
1255
|
+
isMaker
|
|
1256
|
+
});
|
|
1257
|
+
const filledOrders = [];
|
|
1258
|
+
const updatedOrders = [];
|
|
1259
|
+
if (result.isFull) {
|
|
1260
|
+
mgr.logger.log(`[SYNC] Full fill for order ${orderId} (slot ${matchedGridOrder.id}).`, 'info');
|
|
1261
|
+
const fullOk = await mgr._updateOrder(result.fullUpdate, 'handle-fill-full', { skipAccounting: false, fee: 0 });
|
|
1262
|
+
if (fullOk === false) {
|
|
1263
|
+
mgr.logger.log(`[SYNC] Failed to convert filled order ${orderId} to spread placeholder; marking totals stale for next sync cycle`, 'warn');
|
|
1264
|
+
mgr.accountTotalsStale = true;
|
|
1216
1265
|
}
|
|
1217
1266
|
filledOrders.push(result.filledOrder);
|
|
1218
|
-
return { filledOrders, updatedOrders, partialFill: false };
|
|
1219
1267
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1268
|
+
else {
|
|
1269
|
+
mgr.logger.log(`[SYNC] Partial fill for order ${orderId} (slot ${matchedGridOrder.id}): newSize=${result.newSize}`, 'info');
|
|
1270
|
+
const partialOk = await mgr._updateOrder(result.partialUpdate, 'handle-fill-partial', { skipAccounting: false, fee: 0 });
|
|
1271
|
+
if (partialOk === false) {
|
|
1272
|
+
mgr.logger.log(`[SYNC] Failed to update partially filled order ${orderId}; marking totals stale for next sync cycle`, 'warn');
|
|
1273
|
+
mgr.accountTotalsStale = true;
|
|
1274
|
+
}
|
|
1275
|
+
updatedOrders.push(result.partialUpdate);
|
|
1276
|
+
filledOrders.push(result.filledOrder);
|
|
1224
1277
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1278
|
+
// Re-anchor accountTotals to authoritative post-fill chain truth
|
|
1279
|
+
// after the fill's optimistic accounting + grid update (see
|
|
1280
|
+
// syncFromFillHistoryBatch for rationale).
|
|
1281
|
+
await mgr.reanchorAccountTotals('fill-sync');
|
|
1282
|
+
return { filledOrders, updatedOrders, partialFill: result.isFull ? false : true };
|
|
1227
1283
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
updatedOrders.push(result.partialUpdate);
|
|
1235
|
-
filledOrders.push(result.filledOrder);
|
|
1236
|
-
return { filledOrders, updatedOrders, partialFill: true };
|
|
1284
|
+
finally {
|
|
1285
|
+
// Lower the guard before the final consolidated recalc so the
|
|
1286
|
+
// settled (re-anchored) state is still verified.
|
|
1287
|
+
mgr._fillBatchInFlight = Math.max(0, (mgr._fillBatchInFlight ?? 0) - 1);
|
|
1288
|
+
await mgr.resumeFundRecalc();
|
|
1237
1289
|
}
|
|
1238
1290
|
}
|
|
1239
1291
|
finally {
|
|
1240
|
-
|
|
1292
|
+
mgr.unlockOrders([...orderIdsToLock]);
|
|
1241
1293
|
}
|
|
1242
1294
|
}
|
|
1243
1295
|
finally {
|
|
1244
|
-
|
|
1296
|
+
// Safety net: ensure the guard never leaks across cycles on early
|
|
1297
|
+
// returns (deferral) or exceptions in the refresh/lock region.
|
|
1298
|
+
if ((mgr._fillBatchInFlight ?? 0) > 0)
|
|
1299
|
+
mgr._fillBatchInFlight--;
|
|
1245
1300
|
}
|
|
1246
1301
|
}
|
|
1247
1302
|
/**
|
|
@@ -1294,171 +1349,197 @@ class SyncEngine {
|
|
|
1294
1349
|
}
|
|
1295
1350
|
// Phase 2: Lock all unique order IDs once
|
|
1296
1351
|
const allOrderIds = [...new Set(fillEntries.map(e => e.orderId))];
|
|
1297
|
-
|
|
1352
|
+
// Raise the fill-batch in-flight guard BEFORE the accountTotals refresh.
|
|
1353
|
+
// The refresh fetches post-fill balances, but the grid still holds the
|
|
1354
|
+
// filled orders as committed until the batch's grid mutation (Phase 7),
|
|
1355
|
+
// so any fund-invariant check in this window would fire a spurious
|
|
1356
|
+
// CRITICAL with diff == the fills' size. The guard is lowered right
|
|
1357
|
+
// before the final consolidated recalc so the settled state is still
|
|
1358
|
+
// verified.
|
|
1359
|
+
mgr._fillBatchInFlight = (mgr._fillBatchInFlight ?? 0) + 1;
|
|
1298
1360
|
try {
|
|
1299
|
-
mgr.
|
|
1361
|
+
const totalsGate = await mgr.refreshAccountTotalsIfStale();
|
|
1362
|
+
if (!totalsGate.ok) {
|
|
1363
|
+
mgr.logger.log('[SYNC] Deferring fill batch: accountTotals refresh failed (stale snapshot). Fills will be re-processed on the next cycle.', 'warn');
|
|
1364
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false, requiresOpenOrdersSync: false, deferred: true };
|
|
1365
|
+
}
|
|
1366
|
+
mgr.lockOrders(allOrderIds);
|
|
1300
1367
|
try {
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
mgr.
|
|
1368
|
+
mgr.pauseFundRecalc();
|
|
1369
|
+
try {
|
|
1370
|
+
// Phase 3: Process accounting for each fill
|
|
1371
|
+
const validEntries = [];
|
|
1372
|
+
for (const entry of fillEntries) {
|
|
1373
|
+
try {
|
|
1374
|
+
const appliedAccounting = await mgr.accountant.processFillAccounting(entry.fillOp, entry.fillKey, { persistenceMode });
|
|
1375
|
+
if (!appliedAccounting) {
|
|
1376
|
+
mgr.logger.log(`[SYNC] Replay detected for fill ${entry.fillKey}; skipping duplicate order mutation`, 'debug');
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
validEntries.push(entry);
|
|
1380
|
+
}
|
|
1381
|
+
catch (acctErr) {
|
|
1382
|
+
mgr.logger.log(`[SYNC] Accounting error for fill ${entry.fillKey}: ${(0, errors_1.getErrorMessage)(acctErr)}`, 'error');
|
|
1308
1383
|
continue;
|
|
1309
1384
|
}
|
|
1310
|
-
validEntries.push(entry);
|
|
1311
1385
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
continue;
|
|
1386
|
+
if (validEntries.length === 0) {
|
|
1387
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false, requiresOpenOrdersSync: anyRequiresSync };
|
|
1315
1388
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
const assetBPrecision = mgr.assets?.assetB?.precision;
|
|
1325
|
-
if (assetAPrecision === undefined || assetBPrecision === undefined) {
|
|
1326
|
-
mgr.logger?.log?.('Error: manager.assets precision missing in syncFromFillHistoryBatch', 'error');
|
|
1327
|
-
return { filledOrders: [], updatedOrders: [], partialFill: false, requiresOpenOrdersSync: anyRequiresSync };
|
|
1328
|
-
}
|
|
1329
|
-
for (const entry of validEntries) {
|
|
1330
|
-
const { orderId, paysAmountRaw, paysAssetId } = entry;
|
|
1331
|
-
const matchedGridOrder = this._findMatchingGridOrder(mgr, orderId);
|
|
1332
|
-
if (!matchedGridOrder) {
|
|
1333
|
-
mgr.logger.log(`[SYNC] Fill for order ${orderId} ignored: order not found in active grid.`, 'debug');
|
|
1334
|
-
anyRequiresSync = true;
|
|
1335
|
-
continue;
|
|
1389
|
+
// Phase 4: Build per-fill context + identify drift candidates
|
|
1390
|
+
const entryContexts = [];
|
|
1391
|
+
const driftOrderIds = new Set();
|
|
1392
|
+
const assetAPrecision = mgr.assets?.assetA?.precision;
|
|
1393
|
+
const assetBPrecision = mgr.assets?.assetB?.precision;
|
|
1394
|
+
if (assetAPrecision === undefined || assetBPrecision === undefined) {
|
|
1395
|
+
mgr.logger?.log?.('Error: manager.assets precision missing in syncFromFillHistoryBatch', 'error');
|
|
1396
|
+
return { filledOrders: [], updatedOrders: [], partialFill: false, requiresOpenOrdersSync: anyRequiresSync };
|
|
1336
1397
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1398
|
+
for (const entry of validEntries) {
|
|
1399
|
+
const { orderId, paysAmountRaw, paysAssetId } = entry;
|
|
1400
|
+
const matchedGridOrder = this._findMatchingGridOrder(mgr, orderId);
|
|
1401
|
+
if (!matchedGridOrder) {
|
|
1402
|
+
mgr.logger.log(`[SYNC] Fill for order ${orderId} ignored: order not found in active grid.`, 'debug');
|
|
1403
|
+
anyRequiresSync = true;
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const ctx = this._computeFillContext(mgr, matchedGridOrder, paysAssetId, paysAmountRaw);
|
|
1407
|
+
mgr.logger.log(`[SYNC] Order ${orderId} (${ctx.orderType}) currentSize=${ctx.currentSize}, filledAmount=${ctx.filledAmount}`, 'debug');
|
|
1408
|
+
if (ctx.driftSignal) {
|
|
1409
|
+
driftOrderIds.add(orderId);
|
|
1410
|
+
}
|
|
1411
|
+
entryContexts.push({
|
|
1412
|
+
entry, matchedGridOrder,
|
|
1413
|
+
orderType: ctx.orderType,
|
|
1414
|
+
precision: ctx.precision,
|
|
1415
|
+
filledAmount: ctx.filledAmount,
|
|
1416
|
+
filledAmountInt: (0, math_1.floatToBlockchainInt)(ctx.filledAmount, ctx.precision),
|
|
1417
|
+
currentSizeIntFromGrid: ctx.currentSizeIntFromGrid,
|
|
1418
|
+
rawForSaleInt: ctx.rawForSaleInt
|
|
1419
|
+
});
|
|
1341
1420
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
effectiveRawForSale: freshForSale
|
|
1365
|
-
});
|
|
1421
|
+
// Phase 5: Batch drift refetch — one get_objects call for all order IDs
|
|
1422
|
+
const refetchMap = new Map();
|
|
1423
|
+
if (driftOrderIds.size > 0) {
|
|
1424
|
+
try {
|
|
1425
|
+
const batchResults = await chainOrders.batchReadOrders([...driftOrderIds], 3000);
|
|
1426
|
+
for (const [orderId, freshOrder] of batchResults) {
|
|
1427
|
+
if (freshOrder) {
|
|
1428
|
+
const freshForSale = toFiniteNumber(freshOrder.for_sale, undefined);
|
|
1429
|
+
if (Number.isFinite(freshForSale)) {
|
|
1430
|
+
refetchMap.set(orderId, {
|
|
1431
|
+
chainConfirmsEmpty: Math.round(freshForSale) <= 0,
|
|
1432
|
+
chainRefetched: true,
|
|
1433
|
+
effectiveRawForSale: freshForSale
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
refetchMap.set(orderId, {
|
|
1438
|
+
chainConfirmsEmpty: false,
|
|
1439
|
+
chainRefetched: true,
|
|
1440
|
+
effectiveRawForSale: null
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1366
1443
|
}
|
|
1367
1444
|
else {
|
|
1368
1445
|
refetchMap.set(orderId, {
|
|
1369
|
-
chainConfirmsEmpty:
|
|
1446
|
+
chainConfirmsEmpty: true,
|
|
1370
1447
|
chainRefetched: true,
|
|
1371
1448
|
effectiveRawForSale: null
|
|
1372
1449
|
});
|
|
1373
1450
|
}
|
|
1374
1451
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1452
|
+
for (const [orderId, { chainRefetched, chainConfirmsEmpty, effectiveRawForSale }] of refetchMap) {
|
|
1453
|
+
if (chainRefetched && effectiveRawForSale != null) {
|
|
1454
|
+
mgr.logger.log(`[SYNC] Drift detected for ${orderId}; batch-refetched for_sale=${effectiveRawForSale}`, 'warn');
|
|
1455
|
+
}
|
|
1456
|
+
else if (chainConfirmsEmpty) {
|
|
1457
|
+
mgr.logger.log(`[SYNC] Drift refetch for ${orderId} returned null; chain confirms empty`, 'info');
|
|
1458
|
+
}
|
|
1381
1459
|
}
|
|
1382
1460
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
mgr.logger.log(`[SYNC] Drift detected for ${orderId}; batch-refetched for_sale=${effectiveRawForSale}`, 'warn');
|
|
1386
|
-
}
|
|
1387
|
-
else if (chainConfirmsEmpty) {
|
|
1388
|
-
mgr.logger.log(`[SYNC] Drift refetch for ${orderId} returned null; chain confirms empty`, 'info');
|
|
1389
|
-
}
|
|
1461
|
+
catch (refetchErr) {
|
|
1462
|
+
mgr.logger.log(`[SYNC] Batch drift refetch failed for ${driftOrderIds.size} orders; falling back to cache: ${refetchErr?.message || refetchErr}`, 'warn');
|
|
1390
1463
|
}
|
|
1391
1464
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
historyId,
|
|
1421
|
-
isMaker
|
|
1422
|
-
});
|
|
1423
|
-
if (result.isFull) {
|
|
1424
|
-
if (result.isGhost) {
|
|
1425
|
-
mgr.logger.log(`[SYNC] Ghost full fill for order ${orderId} (slot ${matchedGridOrder.id}): ` +
|
|
1426
|
-
`preserving orderId ${result.ghostOrderId} as PARTIAL to block duplicate CREATE.`, 'info');
|
|
1427
|
-
gridUpdates.push({ id: matchedGridOrder.id, ...result.ghostUpdate, context: 'handle-fill-ghost' });
|
|
1428
|
-
}
|
|
1429
|
-
else {
|
|
1465
|
+
// Phase 6: Compute state transitions and collect grid updates
|
|
1466
|
+
const gridUpdates = [];
|
|
1467
|
+
const filledOrders = [];
|
|
1468
|
+
const updatedOrders = [];
|
|
1469
|
+
let anyPartialFill = false;
|
|
1470
|
+
for (const ctx of entryContexts) {
|
|
1471
|
+
const { entry, matchedGridOrder, orderType, precision, filledAmount, filledAmountInt, currentSizeIntFromGrid, rawForSaleInt } = ctx;
|
|
1472
|
+
const { orderId, blockNum, historyId, isMaker } = entry;
|
|
1473
|
+
const refetchInfo = refetchMap.get(orderId);
|
|
1474
|
+
const chainRefetched = refetchInfo?.chainRefetched || false;
|
|
1475
|
+
const chainConfirmsEmpty = refetchInfo?.chainConfirmsEmpty || false;
|
|
1476
|
+
const effectiveRawForSale = refetchInfo?.effectiveRawForSale != null ? refetchInfo.effectiveRawForSale : rawForSaleInt;
|
|
1477
|
+
const result = await this._computeFillTransitionResult(mgr, {
|
|
1478
|
+
matchedGridOrder,
|
|
1479
|
+
orderType,
|
|
1480
|
+
precision,
|
|
1481
|
+
filledAmount,
|
|
1482
|
+
filledAmountInt,
|
|
1483
|
+
currentSizeIntFromGrid,
|
|
1484
|
+
rawForSaleInt,
|
|
1485
|
+
chainRefetched,
|
|
1486
|
+
chainConfirmsEmpty,
|
|
1487
|
+
effectiveRawForSale,
|
|
1488
|
+
blockNum,
|
|
1489
|
+
historyId,
|
|
1490
|
+
isMaker
|
|
1491
|
+
});
|
|
1492
|
+
if (result.isFull) {
|
|
1430
1493
|
mgr.logger.log(`[SYNC] Full fill for order ${orderId} (slot ${matchedGridOrder.id}).`, 'info');
|
|
1431
1494
|
gridUpdates.push({ id: matchedGridOrder.id, ...result.fullUpdate, context: 'handle-fill-full' });
|
|
1495
|
+
filledOrders.push(result.filledOrder);
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
mgr.logger.log(`[SYNC] Partial fill for order ${orderId} (slot ${matchedGridOrder.id}): newSize=${result.newSize}`, 'info');
|
|
1499
|
+
gridUpdates.push({ id: matchedGridOrder.id, ...result.partialUpdate, context: 'handle-fill-partial' });
|
|
1500
|
+
updatedOrders.push(result.partialUpdate);
|
|
1501
|
+
filledOrders.push(result.filledOrder);
|
|
1502
|
+
anyPartialFill = true;
|
|
1432
1503
|
}
|
|
1433
|
-
filledOrders.push(result.filledOrder);
|
|
1434
1504
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1505
|
+
// Phase 7: Single grid batch update — acquires _gridLock once
|
|
1506
|
+
if (gridUpdates.length > 0) {
|
|
1507
|
+
const updateObjects = gridUpdates.map(u => {
|
|
1508
|
+
const { context, ...orderData } = u;
|
|
1509
|
+
return orderData;
|
|
1510
|
+
});
|
|
1511
|
+
const batchOk = await mgr.applyGridUpdateBatch(updateObjects, 'handle-fill-batch', { skipAccounting: false, fee: 0 });
|
|
1512
|
+
if (batchOk === false) {
|
|
1513
|
+
mgr.logger.log('[SYNC] Batch grid update failed for some fills; marking totals stale for next sync cycle', 'warn');
|
|
1514
|
+
mgr.accountTotalsStale = true;
|
|
1515
|
+
}
|
|
1441
1516
|
}
|
|
1517
|
+
// Re-anchor accountTotals to authoritative post-fill chain truth
|
|
1518
|
+
// after the batch's accounting + grid mutation. The up-front
|
|
1519
|
+
// stale-totals refresh returns post-fill balances, so the batch's
|
|
1520
|
+
// processFillAccounting pays/receives adjustments would otherwise
|
|
1521
|
+
// double-count; the forced fetch overwrites them with the
|
|
1522
|
+
// authoritative values and hands re-commitments
|
|
1523
|
+
// (tryDeductFromChainFree) a fresh snapshot.
|
|
1524
|
+
await mgr.reanchorAccountTotals('fill-batch');
|
|
1525
|
+
return { filledOrders, updatedOrders, partialFill: anyPartialFill, requiresOpenOrdersSync: anyRequiresSync };
|
|
1442
1526
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
});
|
|
1449
|
-
const batchOk = await mgr.applyGridUpdateBatch(updateObjects, 'handle-fill-batch', { skipAccounting: false, fee: 0 });
|
|
1450
|
-
if (batchOk === false) {
|
|
1451
|
-
mgr.logger.log('[SYNC] Batch grid update failed for some fills; next sync cycle will reconcile', 'warn');
|
|
1452
|
-
}
|
|
1527
|
+
finally {
|
|
1528
|
+
// Lower the guard before the final consolidated recalc so the
|
|
1529
|
+
// settled (re-anchored) state is still verified.
|
|
1530
|
+
mgr._fillBatchInFlight = Math.max(0, (mgr._fillBatchInFlight ?? 0) - 1);
|
|
1531
|
+
await mgr.resumeFundRecalc();
|
|
1453
1532
|
}
|
|
1454
|
-
return { filledOrders, updatedOrders, partialFill: anyPartialFill, requiresOpenOrdersSync: anyRequiresSync };
|
|
1455
1533
|
}
|
|
1456
1534
|
finally {
|
|
1457
|
-
|
|
1535
|
+
mgr.unlockOrders(allOrderIds);
|
|
1458
1536
|
}
|
|
1459
1537
|
}
|
|
1460
1538
|
finally {
|
|
1461
|
-
|
|
1539
|
+
// Safety net: ensure the guard never leaks across cycles on early
|
|
1540
|
+
// returns (deferral) or exceptions in the refresh/lock region.
|
|
1541
|
+
if ((mgr._fillBatchInFlight ?? 0) > 0)
|
|
1542
|
+
mgr._fillBatchInFlight--;
|
|
1462
1543
|
}
|
|
1463
1544
|
}
|
|
1464
1545
|
/**
|
|
@@ -1549,14 +1630,21 @@ class SyncEngine {
|
|
|
1549
1630
|
}
|
|
1550
1631
|
}
|
|
1551
1632
|
const newState = isPartialPlacement ? constants_1.ORDER_STATES.PARTIAL : constants_1.ORDER_STATES.ACTIVE;
|
|
1552
|
-
|
|
1633
|
+
let normalizedExpectedType = (expectedType === constants_1.ORDER_TYPES.BUY || expectedType === constants_1.ORDER_TYPES.SELL)
|
|
1553
1634
|
? expectedType
|
|
1554
1635
|
: null;
|
|
1636
|
+
// Defensive: a SPREAD slot must never transition to an on-chain
|
|
1637
|
+
// state (validateOrder rejects SPREAD+ACTIVE/PARTIAL as fatal).
|
|
1638
|
+
// Derive the side from the slot price vs start price — same
|
|
1639
|
+
// convention as the funds check (manager.ts).
|
|
1640
|
+
if (!normalizedExpectedType && gridOrder.type === constants_1.ORDER_TYPES.SPREAD) {
|
|
1641
|
+
normalizedExpectedType = (0, order_1.resolveSpreadOrderSide)(gridOrder.price, mgr.config.startPrice);
|
|
1642
|
+
}
|
|
1555
1643
|
const updatedOrder = {
|
|
1556
1644
|
...gridOrder,
|
|
1557
1645
|
type: normalizedExpectedType || gridOrder.type,
|
|
1558
1646
|
state: newState,
|
|
1559
|
-
orderId: chainOrderId
|
|
1647
|
+
orderId: chainOrderId,
|
|
1560
1648
|
};
|
|
1561
1649
|
// Restore btsFeeState from raw chain order data if provided.
|
|
1562
1650
|
// After a grid reset, in-memory orders have no fee state but the
|