dexbot 1.5.2 → 1.6.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/CHANGELOG.md +79 -0
- package/README.md +4 -3
- package/analysis/README.md +1 -2
- package/analysis/ama_fitting/fetch_lp_candles.ts +1 -9
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/analyze_derivatives.ts +1 -1
- package/analysis/bot_usage/discover_bot_accounts.ts +12 -6
- package/analysis/resolve_source.ts +11 -1
- package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
- package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
- package/analysis/tradingview/README.md +32 -10
- package/analysis/tradingview/analyze_tradingview.ts +118 -3
- package/analysis/tradingview/h-bts_tradingview.html +1570 -0
- package/analysis/tradingview/t-bts_tradingview.html +1570 -0
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +1392 -125
- package/analysis/trend_detection/package.json +1 -1
- package/claw/docs/DEXBOT2_TUNING_CHEAT_SHEET.md +1 -1
- package/claw/modules/dexbot_profiles.ts +22 -2
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/skills/launcher-ops/references/launcher-workflow.md +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/claw/tests/test_dexbot_profiles.ts +65 -0
- package/dist/analysis/ama_fitting/fetch_lp_candles.js +1 -9
- package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -1
- package/dist/analysis/analyze_derivatives.js +1 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.js +11 -6
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +2 -0
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
- package/dist/analysis/resolve_source.d.ts +1 -0
- package/dist/analysis/resolve_source.d.ts.map +1 -1
- package/dist/analysis/resolve_source.js +8 -1
- package/dist/analysis/resolve_source.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +8 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +137 -3
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1387 -125
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/bot.js +1 -1
- package/dist/bot.js.map +1 -1
- package/dist/dexbot.js +3 -3
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/ama_signal_runner.js +2 -2
- package/dist/market_adapter/candle_utils.d.ts +6 -0
- package/dist/market_adapter/candle_utils.d.ts.map +1 -1
- package/dist/market_adapter/candle_utils.js +6 -0
- package/dist/market_adapter/candle_utils.js.map +1 -1
- package/dist/market_adapter/core/asymmetric_bounds.d.ts +33 -1
- package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
- package/dist/market_adapter/core/asymmetric_bounds.js +38 -1
- package/dist/market_adapter/core/asymmetric_bounds.js.map +1 -1
- package/dist/market_adapter/core/kibana_candles.d.ts +17 -1
- package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_candles.js +101 -19
- package/dist/market_adapter/core/kibana_candles.js.map +1 -1
- package/dist/market_adapter/core/kibana_client.d.ts +2 -0
- package/dist/market_adapter/core/kibana_client.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_client.js +31 -4
- package/dist/market_adapter/core/kibana_client.js.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.js +0 -6
- package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_book_data.d.ts +14 -0
- package/dist/market_adapter/inputs/fetch_book_data.d.ts.map +1 -0
- package/dist/market_adapter/inputs/fetch_book_data.js +138 -0
- package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -0
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +16 -2
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +109 -281
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts +87 -0
- package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js +570 -0
- package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -0
- package/dist/market_adapter/inputs/kibana_source.d.ts.map +1 -1
- package/dist/market_adapter/inputs/kibana_source.js +10 -5
- package/dist/market_adapter/inputs/kibana_source.js.map +1 -1
- package/dist/market_adapter/inputs/window_cache.d.ts +166 -0
- package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -0
- package/dist/market_adapter/inputs/window_cache.js +500 -0
- package/dist/market_adapter/inputs/window_cache.js.map +1 -0
- package/dist/market_adapter/interval_utils.d.ts +8 -1
- package/dist/market_adapter/interval_utils.d.ts.map +1 -1
- package/dist/market_adapter/interval_utils.js +14 -1
- package/dist/market_adapter/interval_utils.js.map +1 -1
- package/dist/modules/account_bots.d.ts +1 -0
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +21 -5
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/account_orders.d.ts +25 -4
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +67 -4
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/bitshares-native/chain_client.d.ts +2 -0
- package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/chain_client.js +3 -1
- package/dist/modules/bitshares-native/chain_client.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +13 -3
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts +2 -0
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +122 -28
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +59 -8
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/bot_settings.d.ts.map +1 -1
- package/dist/modules/bot_settings.js +11 -0
- package/dist/modules/bot_settings.js.map +1 -1
- package/dist/modules/constants.d.ts +13 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +68 -4
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +10 -18
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +57 -2
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +62 -37
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +797 -569
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +41 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +135 -5
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +79 -12
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +585 -97
- 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 +13 -0
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts +15 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +36 -7
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/fund_registry.d.ts +1 -1
- package/dist/modules/fund_registry.js +1 -1
- package/dist/modules/node_connect_policy.d.ts +52 -0
- package/dist/modules/node_connect_policy.d.ts.map +1 -0
- package/dist/modules/node_connect_policy.js +35 -0
- package/dist/modules/node_connect_policy.js.map +1 -0
- package/dist/modules/node_manager.d.ts +32 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +76 -9
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +30 -0
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +176 -3
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +1 -0
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +170 -51
- 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 +5 -3
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +182 -6
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +66 -3
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +238 -12
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts +3 -0
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +79 -7
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +90 -1
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +20 -12
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +33 -12
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +308 -28
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +862 -38
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +37 -30
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +130 -101
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +3 -2
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +11 -3
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/modules/paths.d.ts +2 -0
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +1 -0
- package/dist/modules/paths.js.map +1 -1
- package/dist/modules/runtime_settings.d.ts +13 -0
- package/dist/modules/runtime_settings.d.ts.map +1 -1
- package/dist/modules/runtime_settings.js +20 -0
- package/dist/modules/runtime_settings.js.map +1 -1
- package/dist/modules/utils/errors.d.ts +16 -0
- package/dist/modules/utils/errors.d.ts.map +1 -1
- package/dist/modules/utils/errors.js +31 -0
- package/dist/modules/utils/errors.js.map +1 -1
- package/dist/pm2.js +7 -7
- package/dist/scripts/analyze-credit.d.ts.map +1 -1
- package/dist/scripts/analyze-credit.js +13 -3
- package/dist/scripts/analyze-credit.js.map +1 -1
- package/dist/scripts/analyze-orders.js +2 -2
- package/dist/scripts/analyze-orders.js.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.d.ts.map +1 -1
- package/dist/scripts/diagnose-kibana-candles.js +0 -1
- package/dist/scripts/diagnose-kibana-candles.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/scripts/tv.d.ts +24 -2
- package/dist/scripts/tv.d.ts.map +1 -1
- package/dist/scripts/tv.js +190 -65
- package/dist/scripts/tv.js.map +1 -1
- package/docs/BITSHARES_ONBOARDING.md +9 -2
- package/docs/COW_INVARIANTS.md +18 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +9 -7
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +21 -9
- package/docs/GRID_RECALCULATION.md +5 -6
- package/docs/GRID_RECONCILE.md +12 -2
- package/docs/MPA_CREDIT_USAGE.md +2 -2
- package/docs/README.md +5 -6
- package/docs/architecture.md +0 -58
- package/docs/developer_guide.md +2 -1
- package/docs/docker.md +1 -1
- package/market_adapter/README.md +4 -0
- package/modules/README.md +1 -1
- package/package.json +1 -1
- package/scripts/README.md +5 -3
- package/tests/README.md +2 -2
|
@@ -14,11 +14,12 @@ import { BroadcastUncertainError as BroadcastUncertainErrorBinding } from './dex
|
|
|
14
14
|
const BroadcastUncertainError = BroadcastUncertainErrorBinding;
|
|
15
15
|
import * as orderUtils from './order/utils/order.js';
|
|
16
16
|
import { sleep } from './order/utils/system.js';
|
|
17
|
-
const { buildCreateOrderArgs, buildCreateOpFingerprint, extractBatchOperationResults, formatUnmatchedChainOrder, convertToSpreadPlaceholder, toRailHolePlaceholder, buildOutsideInPairGroups, isOrderPlaced, } = orderUtils;
|
|
17
|
+
const { buildCreateOrderArgs, buildCreateOpFingerprint, extractBatchOperationResults, formatUnmatchedChainOrder, convertToSpreadPlaceholder, toRailHolePlaceholder, buildOutsideInPairGroups, isOrderPlaced, chainOrderUnchangedFromCache, detectCrossedBookPlan, collectKnownOnChainOrderIds, } = orderUtils;
|
|
18
18
|
import * as validate from './order/utils/validate.js';
|
|
19
19
|
const { validateCreateTargetSlots, evaluateCommit, hasExecutableActions, stampGapEvacuationRotation } = validate;
|
|
20
20
|
import * as math from './order/utils/math.js';
|
|
21
|
-
const { validateOrderSize, findCrossedOrder, priceSlotEqual, isEvacuationRotationAllowed, isEvacuationSizeStillValid, getSellStartIdx, getPrecisionByOrderType, isSlotIndexInGapBand } = math;
|
|
21
|
+
const { validateOrderSize, findCrossedOrder, priceSlotEqual, isEvacuationRotationAllowed, isEvacuationSizeStillValid, getSellStartIdx, getPrecisionByOrderType, isSlotIndexInGapBand, getAssetFeesSafe, blockchainToFloat, floatToBlockchainInt, quantizeFloat } = math;
|
|
22
|
+
import { parseSlotIndex } from './order/utils/slot.js';
|
|
22
23
|
/**
|
|
23
24
|
* Re-verify a B-stamped gap-evacuation rotation against the LIVE committed
|
|
24
25
|
* geometry (boundaryIdx/_gapSlots at execution time). The stamp freezes
|
|
@@ -48,7 +49,6 @@ export function isEvacuationStampStillValid(liveBoundary, liveGapSlots, sourceId
|
|
|
48
49
|
const dstInRail = orderType === ORDER_TYPES.SELL ? Number(dstIdx) >= sellStartIdx : Number(dstIdx) <= b;
|
|
49
50
|
return dstInRail;
|
|
50
51
|
}
|
|
51
|
-
import { parseSlotIndex } from './order/utils/slot.js';
|
|
52
52
|
function hasSlotPriceCollision(items, targetPrice, precision, excludeId, predicate) {
|
|
53
53
|
for (const it of items) {
|
|
54
54
|
if (predicate && !predicate(it))
|
|
@@ -69,8 +69,6 @@ function hasSlotPriceCollision(items, targetPrice, precision, excludeId, predica
|
|
|
69
69
|
}
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
|
-
// Lazy accessor so test mocks on the math module export take effect at call time.
|
|
73
|
-
function getAssetFeesSafe(...args) { return require('./order/utils/math').getAssetFeesSafe(...args); }
|
|
74
72
|
import * as constantsModule from './constants.js';
|
|
75
73
|
const { COW_ACTIONS, COW_PERFORMANCE, ORDER_STATES, ORDER_TYPES, REBALANCE_STATES, } = constantsModule;
|
|
76
74
|
import { acquireIfNotHeld } from './order/async_lock.js';
|
|
@@ -79,8 +77,6 @@ const Format = FormatModule;
|
|
|
79
77
|
import * as workingGridModule from './order/working_grid.js';
|
|
80
78
|
const { WorkingGrid } = workingGridModule;
|
|
81
79
|
import { getErrorMessage } from './utils/errors.js';
|
|
82
|
-
import { createRequire } from 'node:module';
|
|
83
|
-
const require = createRequire(import.meta.url);
|
|
84
80
|
// Maximum number of times the pre-broadcast staleness guard may re-plan the
|
|
85
81
|
// batch from a fresh master before proceeding anyway. Bounded so a master
|
|
86
82
|
// grid that keeps mutating (fill bursts, sync loops) can never livelock the
|
|
@@ -99,11 +95,7 @@ const SINGLE_FLIGHT_MAX_WAIT_MS = 120000;
|
|
|
99
95
|
* @returns {Array<Array>}
|
|
100
96
|
*/
|
|
101
97
|
function buildOutsideInPairGroupsForOrders(orders) {
|
|
102
|
-
return
|
|
103
|
-
isValid: Boolean,
|
|
104
|
-
getType: (o) => o.type,
|
|
105
|
-
getPrice: (o) => o.price,
|
|
106
|
-
});
|
|
98
|
+
return buildOutsideInPairGroupsWithAdapter(orders, (o) => o);
|
|
107
99
|
}
|
|
108
100
|
/**
|
|
109
101
|
* Build outside-in pair groups for create entry contexts.
|
|
@@ -111,12 +103,128 @@ function buildOutsideInPairGroupsForOrders(orders) {
|
|
|
111
103
|
* @returns {Array<Array>}
|
|
112
104
|
*/
|
|
113
105
|
function buildOutsideInPairGroupsForCreateEntries(createEntries) {
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
return buildOutsideInPairGroupsWithAdapter(createEntries, (e) => e?.context?.order);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Shared outside-in delegation: project each item to its order, then group
|
|
110
|
+
* with the single order adapter. The ForOrders/ForCreateEntries wrappers
|
|
111
|
+
* differ only in this projection.
|
|
112
|
+
* @param {Array} items
|
|
113
|
+
* @param {Function} toOrder - Project an item to its order object
|
|
114
|
+
* @returns {Array<Array>}
|
|
115
|
+
*/
|
|
116
|
+
function buildOutsideInPairGroupsWithAdapter(items, toOrder) {
|
|
117
|
+
return buildOutsideInPairGroups(items, {
|
|
118
|
+
isValid: (item) => Boolean(toOrder(item)),
|
|
119
|
+
getType: (item) => toOrder(item)?.type,
|
|
120
|
+
getPrice: (item) => toOrder(item)?.price,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a create-op fingerprint for a specific slot, used for
|
|
125
|
+
* pending-broadcast matching and chain-order fingerprinting.
|
|
126
|
+
* Consolidates the fingerprint construction pattern found in
|
|
127
|
+
* recordPendingBroadcast, buildChainOrderFingerprint, findChainOrderForSlot,
|
|
128
|
+
* and verifyCreateAbsent.
|
|
129
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
130
|
+
* @param {Object} order - The order object
|
|
131
|
+
* @param {Object} finalInts - The final integer tuple { sell, receive, sellAssetId, receiveAssetId }
|
|
132
|
+
* @param {string} slotId - The slot identifier
|
|
133
|
+
* @returns {string|null}
|
|
134
|
+
*/
|
|
135
|
+
function createOpFingerprintForSlot(bot, order, finalInts, slotId) {
|
|
136
|
+
if (!order || !finalInts || !slotId)
|
|
137
|
+
return null;
|
|
138
|
+
return buildCreateOpFingerprint({
|
|
139
|
+
side: order.type,
|
|
140
|
+
assetA: bot.manager?.assets?.assetA?.id,
|
|
141
|
+
assetB: bot.manager?.assets?.assetB?.id,
|
|
142
|
+
sellInt: finalInts.sell,
|
|
143
|
+
receiveInt: finalInts.receive,
|
|
144
|
+
slotId,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Read the pending-broadcast entries as an array. The manager map is
|
|
149
|
+
* created lazily by recordPendingBroadcast, so every reader must guard
|
|
150
|
+
* on `instanceof Map` — this helper is the single spelling of that guard.
|
|
151
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
152
|
+
* @returns {Array}
|
|
153
|
+
*/
|
|
154
|
+
function getPendingBroadcasts(bot) {
|
|
155
|
+
return (bot.manager && bot.manager._pendingBroadcasts instanceof Map)
|
|
156
|
+
? Array.from(bot.manager._pendingBroadcasts.values())
|
|
157
|
+
: [];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Count pending-broadcast entries without materializing the array.
|
|
161
|
+
* Same instanceof Map guard as getPendingBroadcasts, for hot paths that
|
|
162
|
+
* only need the count (e.g. the orphan auto-cancel gate).
|
|
163
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
164
|
+
* @returns {number}
|
|
165
|
+
*/
|
|
166
|
+
function countPendingBroadcasts(bot) {
|
|
167
|
+
return (bot.manager && bot.manager._pendingBroadcasts instanceof Map)
|
|
168
|
+
? bot.manager._pendingBroadcasts.size
|
|
169
|
+
: 0;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Probe the chain snapshot for a pending-broadcast entry's slot.
|
|
173
|
+
* Single spelling of the findChainOrderForSlot probe literal used by
|
|
174
|
+
* reconcile (first pass + re-read), the poll confirmer, and the
|
|
175
|
+
* create-absence verifier.
|
|
176
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
177
|
+
* @param {Array} chainSnapshot
|
|
178
|
+
* @param {Object} entry - Pending entry ({ slotId, finalInts, orderType, fingerprint })
|
|
179
|
+
* @returns {Object|null}
|
|
180
|
+
*/
|
|
181
|
+
function findChainOrderForPendingEntry(bot, chainSnapshot, entry) {
|
|
182
|
+
if (!entry?.slotId)
|
|
183
|
+
return null;
|
|
184
|
+
return findChainOrderForSlot(bot, chainSnapshot, entry.slotId, {
|
|
185
|
+
sell: entry.finalInts?.sell,
|
|
186
|
+
receive: entry.finalInts?.receive,
|
|
187
|
+
orderType: entry.orderType,
|
|
188
|
+
fingerprint: entry.fingerprint,
|
|
118
189
|
});
|
|
119
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Check whether a chain read is authoritative enough to make
|
|
193
|
+
* definitive absence/lPresence decisions. An authoritative read is
|
|
194
|
+
* non-empty, non-truncated, and non-null.
|
|
195
|
+
* @param {Object} readResult - The result of readOpenOrdersWithMeta
|
|
196
|
+
* @returns {boolean}
|
|
197
|
+
*/
|
|
198
|
+
function isAuthoritativeChainRead(readResult) {
|
|
199
|
+
if (!readResult || !Array.isArray(readResult.orders))
|
|
200
|
+
return false;
|
|
201
|
+
if (readResult.truncated)
|
|
202
|
+
return false;
|
|
203
|
+
return readResult.orders.length > 0;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Build a rawOnChain metadata object from final integer values.
|
|
207
|
+
* Used in processBatchResults for size-update, create, and rotation
|
|
208
|
+
* contexts to enrich order objects with on-chain identity.
|
|
209
|
+
* Returns null when the order id (or final ints) is missing: downstream
|
|
210
|
+
* chainOrderUnchangedFromCache treats a null cache as "not unchanged" and
|
|
211
|
+
* defers, which is the safer direction versus comparing an id-less object.
|
|
212
|
+
* @param {string} orderId - The on-chain order id
|
|
213
|
+
* @param {Object} finalInts - The final integer tuple
|
|
214
|
+
* @returns {Object|null}
|
|
215
|
+
*/
|
|
216
|
+
function rawOnChainFromInts(orderId, finalInts) {
|
|
217
|
+
if (!orderId || !finalInts)
|
|
218
|
+
return null;
|
|
219
|
+
return {
|
|
220
|
+
id: orderId,
|
|
221
|
+
for_sale: String(finalInts.sell),
|
|
222
|
+
sell_price: {
|
|
223
|
+
base: { amount: String(finalInts.sell), asset_id: finalInts.sellAssetId },
|
|
224
|
+
quote: { amount: String(finalInts.receive), asset_id: finalInts.receiveAssetId },
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
120
228
|
/**
|
|
121
229
|
* Extract operation results from a batch transaction result.
|
|
122
230
|
* @param {Object|Array|null} result
|
|
@@ -222,14 +330,7 @@ function recordPendingBroadcast(bot, entry) {
|
|
|
222
330
|
if (!bot.manager._pendingBroadcasts || !(bot.manager._pendingBroadcasts instanceof Map)) {
|
|
223
331
|
bot.manager._pendingBroadcasts = new Map();
|
|
224
332
|
}
|
|
225
|
-
const fingerprint =
|
|
226
|
-
side: entry.order.type,
|
|
227
|
-
assetA: bot.manager?.assets?.assetA?.id,
|
|
228
|
-
assetB: bot.manager?.assets?.assetB?.id,
|
|
229
|
-
sellInt: entry.finalInts?.sell,
|
|
230
|
-
receiveInt: entry.finalInts?.receive,
|
|
231
|
-
slotId: entry.order.id
|
|
232
|
-
});
|
|
333
|
+
const fingerprint = createOpFingerprintForSlot(bot, entry.order, entry.finalInts, entry.order.id);
|
|
233
334
|
if (!fingerprint) {
|
|
234
335
|
bot.manager.logger.log?.(`[COW] Skipped pending-broadcast record: could not build fingerprint for ${entry.order?.id || 'unknown'}`, 'warn');
|
|
235
336
|
return;
|
|
@@ -510,72 +611,24 @@ async function reconcileAfterUncertainBroadcast(bot, err, opContexts, options =
|
|
|
510
611
|
return acquireIfNotHeld(bot.manager?._fillProcessingLock, () => reconcileAfterUncertainBroadcastImpl(bot, err, opContexts, options));
|
|
511
612
|
}
|
|
512
613
|
/**
|
|
513
|
-
*
|
|
614
|
+
* Match pending broadcasts against a chain snapshot (first pass by slot
|
|
615
|
+
* probe, second pass by fingerprint across all pending slots), then
|
|
616
|
+
* re-read the chain for discarded CREATEs to close the TOCTOU window.
|
|
617
|
+
* Returns `{ adopted, discarded }`, or `{ deferred }` when the re-read
|
|
618
|
+
* is ambiguous/failed and the caller must defer (pending protection kept).
|
|
514
619
|
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
515
|
-
* @param {
|
|
516
|
-
* @param {Array
|
|
517
|
-
* @param {
|
|
518
|
-
* @
|
|
620
|
+
* @param {Array} pending
|
|
621
|
+
* @param {Array} chainSnapshot
|
|
622
|
+
* @param {Array} opContexts
|
|
623
|
+
* @param {*} accountRef
|
|
624
|
+
* @param {*} err
|
|
519
625
|
*/
|
|
520
|
-
async function
|
|
521
|
-
const startedAt = Date.now();
|
|
522
|
-
const pending = (bot.manager && bot.manager._pendingBroadcasts instanceof Map)
|
|
523
|
-
? Array.from(bot.manager._pendingBroadcasts.values())
|
|
524
|
-
: [];
|
|
525
|
-
const createContextCount = opContexts.filter((c) => c && c.kind === 'create').length;
|
|
526
|
-
const nonCreateContextCount = opContexts.length - createContextCount;
|
|
527
|
-
bot.manager.logger.log(`[COW][UNCERTAIN] batchId=${err?.batchId || 'n/a'} ops=${opContexts.length} ` +
|
|
528
|
-
`creates=${createContextCount} nonCreates=${nonCreateContextCount} ` +
|
|
529
|
-
`staleSinceMs=${err?.timeoutMs || 'n/a'}. Entering reconcile-then-decide.`, 'warn');
|
|
530
|
-
if (!chainOrders?.readOpenOrdersWithMeta) {
|
|
531
|
-
bot.manager.logger.log('[COW][UNCERTAIN] readOpenOrdersWithMeta unavailable; falling back to structural resync only.', 'error');
|
|
532
|
-
if (typeof bot.manager.requestStructuralGridResync === 'function') {
|
|
533
|
-
await bot.manager.requestStructuralGridResync('broadcast uncertain — readOpenOrders unavailable', { batchId: err?.batchId || null });
|
|
534
|
-
}
|
|
535
|
-
clearPendingBroadcasts(bot.manager?._pendingBroadcasts);
|
|
536
|
-
return { executed: false, hadRotation: false, uncertain: true };
|
|
537
|
-
}
|
|
538
|
-
// 1. Read the chain
|
|
539
|
-
const accountRef = bot.accountId || bot.account?.id || bot.account;
|
|
540
|
-
let chainSnapshot = [];
|
|
541
|
-
let chainReadTruncated = false;
|
|
542
|
-
try {
|
|
543
|
-
const chainRead = await chainOrders.readOpenOrdersWithMeta(accountRef);
|
|
544
|
-
chainSnapshot = chainRead.orders;
|
|
545
|
-
chainReadTruncated = chainRead.truncated;
|
|
546
|
-
}
|
|
547
|
-
catch (readErr) {
|
|
548
|
-
bot.manager.logger.log(`[COW][UNCERTAIN] readOpenOrders failed: ${readErr?.message || readErr}. ` +
|
|
549
|
-
`Falling back to structural resync.`, 'error');
|
|
550
|
-
if (typeof bot.manager.requestStructuralGridResync === 'function') {
|
|
551
|
-
await bot.manager.requestStructuralGridResync('broadcast uncertain — readOpenOrders failed', { batchId: err?.batchId || null, error: readErr?.message || String(readErr) });
|
|
552
|
-
}
|
|
553
|
-
clearPendingBroadcasts(bot.manager?._pendingBroadcasts);
|
|
554
|
-
return { executed: false, hadRotation: false, uncertain: true };
|
|
555
|
-
}
|
|
556
|
-
// 1.5. Empty/truncated-read guard: an empty snapshot is ambiguous — the
|
|
557
|
-
// account is either genuinely empty or the node is lagging behind the
|
|
558
|
-
// just-broadcast transaction. A truncated snapshot (get_full_accounts
|
|
559
|
-
// capped limit_orders; fresh creates sort last in the by_account index
|
|
560
|
-
// and are the first entries omitted) is equally ambiguous: the batch's
|
|
561
|
-
// creates may simply be missing from the returned window. Treating every
|
|
562
|
-
// pending broadcast as discarded would clear the pending-broadcast
|
|
563
|
-
// protection and let the next cycle re-CREATE slots whose orders may
|
|
564
|
-
// actually be on chain (duplicate orders). Keep the protection and let
|
|
565
|
-
// the structural resync adopt any landed orders.
|
|
566
|
-
if (pending.length > 0 && (chainSnapshot.length === 0 || chainReadTruncated)) {
|
|
567
|
-
return await deferUncertainBroadcastRead(bot, `${chainSnapshot.length === 0 ? 'Empty' : 'Truncated'} chain read for ${pending.length} pending broadcast(s)`, '(node may be lagging or the result set capped; no discard decisions made)', 'uncertain broadcast — empty/truncated chain read', { batchId: err?.batchId || null, truncated: chainReadTruncated });
|
|
568
|
-
}
|
|
626
|
+
async function matchPendingToChain(bot, pending, chainSnapshot, opContexts, accountRef, err) {
|
|
569
627
|
const adopted = [];
|
|
570
628
|
let discarded = [];
|
|
571
629
|
// 2. For each pending broadcast, look for a chain match.
|
|
572
630
|
for (const entry of pending) {
|
|
573
|
-
const match =
|
|
574
|
-
sell: entry.finalInts?.sell,
|
|
575
|
-
receive: entry.finalInts?.receive,
|
|
576
|
-
orderType: entry.orderType,
|
|
577
|
-
fingerprint: entry.fingerprint,
|
|
578
|
-
});
|
|
631
|
+
const match = findChainOrderForPendingEntry(bot, chainSnapshot, entry);
|
|
579
632
|
if (match) {
|
|
580
633
|
adopted.push({ entry, match });
|
|
581
634
|
}
|
|
@@ -624,21 +677,18 @@ async function reconcileAfterUncertainBroadcastImpl(bot, err, opContexts, _optio
|
|
|
624
677
|
// protection and let the next cycle re-create (duplicate) an
|
|
625
678
|
// order that actually landed in the TOCTOU window. Keep the
|
|
626
679
|
// pending-broadcast protection and defer to a structural resync.
|
|
627
|
-
if (!
|
|
680
|
+
if (!isAuthoritativeChainRead(freshRead)) {
|
|
628
681
|
const ambiguous = !freshRead || !Array.isArray(freshRead.orders) || freshRead.orders.length === 0;
|
|
629
|
-
return
|
|
682
|
+
return {
|
|
683
|
+
deferred: await deferUncertainBroadcastRead(bot, `${ambiguous ? 'Empty' : 'Truncated'} re-read for ${createDiscarded.length} discarded CREATE(s)`, '(absence is not authoritative on an ambiguous re-read)', 'uncertain broadcast — ambiguous re-read for discarded creates', { batchId: err?.batchId || null, truncated: !ambiguous })
|
|
684
|
+
};
|
|
630
685
|
}
|
|
631
686
|
const freshChain = freshRead.orders;
|
|
632
687
|
const remainingDiscarded = [];
|
|
633
688
|
for (const entry of discarded) {
|
|
634
689
|
const ctx = opContexts[entry.ctxIndex];
|
|
635
690
|
if (ctx && ctx.kind === 'create') {
|
|
636
|
-
const match =
|
|
637
|
-
sell: entry.finalInts?.sell,
|
|
638
|
-
receive: entry.finalInts?.receive,
|
|
639
|
-
orderType: entry.orderType,
|
|
640
|
-
fingerprint: entry.fingerprint,
|
|
641
|
-
});
|
|
691
|
+
const match = findChainOrderForPendingEntry(bot, freshChain, entry);
|
|
642
692
|
if (match) {
|
|
643
693
|
adopted.push({ entry, match });
|
|
644
694
|
bot.manager.logger.log(`[COW][UNCERTAIN] Late-adopted discarded CREATE for slot ${entry.slotId} (${match.id}) via fresh chain read`, 'info');
|
|
@@ -654,13 +704,24 @@ async function reconcileAfterUncertainBroadcastImpl(bot, err, opContexts, _optio
|
|
|
654
704
|
discarded = remainingDiscarded;
|
|
655
705
|
}
|
|
656
706
|
catch (reReadErr) {
|
|
657
|
-
return
|
|
707
|
+
return {
|
|
708
|
+
deferred: await deferUncertainBroadcastRead(bot, `Fresh chain read for late adoption FAILED (${getErrorMessage(reReadErr)})`, '(absence is not authoritative on a failed re-read)', 'uncertain broadcast — failed re-read for discarded creates', { batchId: err?.batchId || null })
|
|
709
|
+
};
|
|
658
710
|
}
|
|
659
711
|
}
|
|
660
712
|
}
|
|
661
|
-
|
|
713
|
+
return { adopted, discarded };
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Adopt matched pending broadcasts into the master grid (CREATE slots
|
|
717
|
+
* synchronize with the chain) and clear their pending entries.
|
|
718
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
719
|
+
* @param {Array} adopted
|
|
720
|
+
* @param {Array} opContexts
|
|
721
|
+
* @returns {Promise<number>} adoptedCount
|
|
722
|
+
*/
|
|
723
|
+
async function adoptMatchedEntries(bot, adopted, opContexts) {
|
|
662
724
|
let adoptedCount = 0;
|
|
663
|
-
let discardedCount = 0;
|
|
664
725
|
for (const { entry, match } of adopted) {
|
|
665
726
|
adoptedCount++;
|
|
666
727
|
const plannedOpCtx = opContexts[entry.ctxIndex];
|
|
@@ -674,6 +735,7 @@ async function reconcileAfterUncertainBroadcastImpl(bot, err, opContexts, _optio
|
|
|
674
735
|
chainOrderId,
|
|
675
736
|
expectedType,
|
|
676
737
|
fee: btsFeeData?.createFee || 0,
|
|
738
|
+
order: plannedOpCtx.order ?? entry.order ?? null,
|
|
677
739
|
}, 'createOrder');
|
|
678
740
|
}
|
|
679
741
|
catch (syncErr) {
|
|
@@ -685,6 +747,19 @@ async function reconcileAfterUncertainBroadcastImpl(bot, err, opContexts, _optio
|
|
|
685
747
|
bot.manager._pendingBroadcasts.delete(entry.fingerprint);
|
|
686
748
|
}
|
|
687
749
|
}
|
|
750
|
+
return adoptedCount;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Restore discarded CREATE slots to creation-uncertain state and clear
|
|
754
|
+
* their pending entries; residual pending entries that survive the
|
|
755
|
+
* decide phase are cleaned defensively.
|
|
756
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
757
|
+
* @param {Array} discarded
|
|
758
|
+
* @param {Array} opContexts
|
|
759
|
+
* @returns {Promise<number>} discardedCount
|
|
760
|
+
*/
|
|
761
|
+
async function restoreDiscardedCreates(bot, discarded, opContexts) {
|
|
762
|
+
let discardedCount = 0;
|
|
688
763
|
for (const entry of discarded) {
|
|
689
764
|
discardedCount++;
|
|
690
765
|
const plannedOpCtx = opContexts[entry.ctxIndex];
|
|
@@ -761,12 +836,48 @@ async function reconcileAfterUncertainBroadcastImpl(bot, err, opContexts, _optio
|
|
|
761
836
|
}
|
|
762
837
|
}
|
|
763
838
|
}
|
|
839
|
+
else {
|
|
840
|
+
// Slot missing from master (grid reset raced the uncertain
|
|
841
|
+
// broadcast). Deleting the pending entry below without
|
|
842
|
+
// restoring anything would leave a clean hole the next
|
|
843
|
+
// cycle re-CREATEs — duplicating a possibly-landed order.
|
|
844
|
+
// Materialize the creation-uncertain slot from the
|
|
845
|
+
// broadcast-time descriptor so the next sync's orphan
|
|
846
|
+
// adoption reconciles a landed order into it.
|
|
847
|
+
const missingType = entry.order.type;
|
|
848
|
+
if (missingType === ORDER_TYPES.BUY || missingType === ORDER_TYPES.SELL) {
|
|
849
|
+
bot.manager.logger.log(`[COW][UNCERTAIN] Slot ${entry.order.id} missing from master after discard — materializing creation-uncertain state ` +
|
|
850
|
+
`(type=${missingType}, size: ${entry.order.size}); next sync adoption will reconcile any landed order`, 'warn');
|
|
851
|
+
const updates = [{
|
|
852
|
+
id: entry.order.id,
|
|
853
|
+
type: missingType,
|
|
854
|
+
size: entry.order.size,
|
|
855
|
+
price: entry.order.price,
|
|
856
|
+
state: ORDER_STATES.VIRTUAL,
|
|
857
|
+
createUncertain: true,
|
|
858
|
+
orderId: null,
|
|
859
|
+
rawOnChain: null,
|
|
860
|
+
}];
|
|
861
|
+
if (typeof bot.manager.applyGridUpdateBatch === 'function') {
|
|
862
|
+
await bot.manager.applyGridUpdateBatch(updates, 'uncertain-broadcast-discard-restore');
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
bot.manager.logger.log(`[COW][UNCERTAIN] Slot ${entry.order.id} missing from master after discard with unrecognized type ${missingType} — cannot reconstruct creation-uncertain state; next sync must adopt any landed order as an orphan`, 'error');
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
bot.manager.logger.log(`[COW][UNCERTAIN] Discarded CREATE for slot ${entry.slotId} has no usable placement descriptor (id=${entry.order?.id ?? 'none'}, size=${entry.order?.size ?? 'none'}, type=${entry.order?.type ?? 'none'}) — nothing restored; next sync must adopt any landed order as an orphan`, 'error');
|
|
764
872
|
}
|
|
765
873
|
}
|
|
766
874
|
catch (restoreErr) {
|
|
767
875
|
bot.manager.logger.log(`[COW][UNCERTAIN] Failed to restore slot ${entry.slotId} after discard: ${restoreErr?.message || restoreErr}`, 'error');
|
|
768
876
|
}
|
|
769
877
|
}
|
|
878
|
+
else {
|
|
879
|
+
bot.manager.logger.log(`[COW][UNCERTAIN] Discarded pending broadcast for slot ${entry.slotId} is not a recognizable CREATE (no create opContext, no order id/type) — skipping restore; next sync must adopt any landed order as an orphan`, 'error');
|
|
880
|
+
}
|
|
770
881
|
// Remove from pending broadcasts.
|
|
771
882
|
if (entry.fingerprint && bot.manager._pendingBroadcasts?.has(entry.fingerprint)) {
|
|
772
883
|
bot.manager._pendingBroadcasts.delete(entry.fingerprint);
|
|
@@ -786,6 +897,101 @@ async function reconcileAfterUncertainBroadcastImpl(bot, err, opContexts, _optio
|
|
|
786
897
|
}
|
|
787
898
|
bot.manager._pendingBroadcasts.clear();
|
|
788
899
|
}
|
|
900
|
+
return discardedCount;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Request a structural resync when chain orders remain unaccounted for
|
|
904
|
+
* after the uncertain-broadcast reconciliation.
|
|
905
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
906
|
+
* @param {*} err
|
|
907
|
+
* @param {Array} pending
|
|
908
|
+
* @param {Array} chainSnapshot
|
|
909
|
+
* @param {Array} adopted
|
|
910
|
+
* @param {number} adoptedCount
|
|
911
|
+
* @param {number} discardedCount
|
|
912
|
+
*/
|
|
913
|
+
async function resyncIfUnreconciled(bot, err, pending, chainSnapshot, adopted, adoptedCount, discardedCount) {
|
|
914
|
+
// 7. Request structural resync if any chain orders remain unaccounted for
|
|
915
|
+
// after the reconciliation, ensuring the next cycle re-plans from a clean
|
|
916
|
+
// chain snapshot.
|
|
917
|
+
const alreadyScheduled = bot._structuralGridResyncRunning || bot._structuralGridResyncTimer;
|
|
918
|
+
if (!alreadyScheduled && chainSnapshot.length > 0) {
|
|
919
|
+
const reconciledOrderIds = new Set(adopted.map((a) => a.match?.id).filter(Boolean));
|
|
920
|
+
const unreconciledCount = chainSnapshot.filter((o) => !reconciledOrderIds.has(o.id)).length;
|
|
921
|
+
if (unreconciledCount > 0) {
|
|
922
|
+
bot.manager.logger.log(`[COW][UNCERTAIN] ${unreconciledCount} chain order(s) remain unreconciled after uncertain broadcast recovery. ` +
|
|
923
|
+
`These may be legitimate pre-existing orders or leftovers from a prior cycle. Requesting structural resync.`, 'warn');
|
|
924
|
+
await requestStructuralResync(bot, 'unreconciled orders after uncertain broadcast', {
|
|
925
|
+
batchId: err?.batchId || null,
|
|
926
|
+
pendingCount: pending.length,
|
|
927
|
+
adoptedCount,
|
|
928
|
+
discardedCount,
|
|
929
|
+
unreconciledCount
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Reconcile a broadcast whose chain state is unknown (implementation).
|
|
936
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
937
|
+
* @param {BroadcastUncertainError} err
|
|
938
|
+
* @param {Array<Object>} opContexts
|
|
939
|
+
* @param {Object} options
|
|
940
|
+
* @returns {Promise<Object>}
|
|
941
|
+
*/
|
|
942
|
+
async function reconcileAfterUncertainBroadcastImpl(bot, err, opContexts, _options) {
|
|
943
|
+
const startedAt = Date.now();
|
|
944
|
+
const pending = getPendingBroadcasts(bot);
|
|
945
|
+
const createContextCount = opContexts.filter((c) => c && c.kind === 'create').length;
|
|
946
|
+
const nonCreateContextCount = opContexts.length - createContextCount;
|
|
947
|
+
bot.manager.logger.log(`[COW][UNCERTAIN] batchId=${err?.batchId || 'n/a'} ops=${opContexts.length} ` +
|
|
948
|
+
`creates=${createContextCount} nonCreates=${nonCreateContextCount} ` +
|
|
949
|
+
`staleSinceMs=${err?.timeoutMs || 'n/a'}. Entering reconcile-then-decide.`, 'warn');
|
|
950
|
+
if (!chainOrders?.readOpenOrdersWithMeta) {
|
|
951
|
+
bot.manager.logger.log('[COW][UNCERTAIN] readOpenOrdersWithMeta unavailable; falling back to structural resync only.', 'error');
|
|
952
|
+
await requestStructuralResync(bot, 'broadcast uncertain — readOpenOrders unavailable', { batchId: err?.batchId || null });
|
|
953
|
+
clearPendingBroadcasts(bot.manager?._pendingBroadcasts);
|
|
954
|
+
return { executed: false, hadRotation: false, uncertain: true };
|
|
955
|
+
}
|
|
956
|
+
// 1. Read the chain
|
|
957
|
+
const accountRef = bot.accountId || bot.account?.id || bot.account;
|
|
958
|
+
let chainSnapshot = [];
|
|
959
|
+
let chainReadTruncated = false;
|
|
960
|
+
try {
|
|
961
|
+
const chainRead = await chainOrders.readOpenOrdersWithMeta(accountRef);
|
|
962
|
+
chainSnapshot = chainRead.orders;
|
|
963
|
+
chainReadTruncated = chainRead.truncated;
|
|
964
|
+
}
|
|
965
|
+
catch (readErr) {
|
|
966
|
+
bot.manager.logger.log(`[COW][UNCERTAIN] readOpenOrders failed: ${readErr?.message || readErr}. ` +
|
|
967
|
+
`Falling back to structural resync.`, 'error');
|
|
968
|
+
await requestStructuralResync(bot, 'broadcast uncertain — readOpenOrders failed', { batchId: err?.batchId || null, error: readErr?.message || String(readErr) });
|
|
969
|
+
clearPendingBroadcasts(bot.manager?._pendingBroadcasts);
|
|
970
|
+
return { executed: false, hadRotation: false, uncertain: true };
|
|
971
|
+
}
|
|
972
|
+
// 1.5. Empty/truncated-read guard: an empty snapshot is ambiguous — the
|
|
973
|
+
// account is either genuinely empty or the node is lagging behind the
|
|
974
|
+
// just-broadcast transaction. A truncated snapshot (get_full_accounts
|
|
975
|
+
// capped limit_orders; fresh creates sort last in the by_account index
|
|
976
|
+
// and are the first entries omitted) is equally ambiguous: the batch's
|
|
977
|
+
// creates may simply be missing from the returned window. Treating every
|
|
978
|
+
// pending broadcast as discarded would clear the pending-broadcast
|
|
979
|
+
// protection and let the next cycle re-CREATE slots whose orders may
|
|
980
|
+
// actually be on chain (duplicate orders). Keep the protection and let
|
|
981
|
+
// the structural resync adopt any landed orders.
|
|
982
|
+
if (pending.length > 0 && (chainSnapshot.length === 0 || chainReadTruncated)) {
|
|
983
|
+
return await deferUncertainBroadcastRead(bot, `${chainSnapshot.length === 0 ? 'Empty' : 'Truncated'} chain read for ${pending.length} pending broadcast(s)`, '(node may be lagging or the result set capped; no discard decisions made)', 'uncertain broadcast — empty/truncated chain read', { batchId: err?.batchId || null, truncated: chainReadTruncated });
|
|
984
|
+
}
|
|
985
|
+
// 2-3a. Match pending broadcasts to the chain: slot probe, fingerprint
|
|
986
|
+
// second pass, and a TOCTOU re-read for discarded CREATEs (see
|
|
987
|
+
// matchPendingToChain). A deferred re-read returns its result directly.
|
|
988
|
+
const matched = await matchPendingToChain(bot, pending, chainSnapshot, opContexts, accountRef, err);
|
|
989
|
+
if (matched.deferred)
|
|
990
|
+
return matched.deferred;
|
|
991
|
+
const { adopted, discarded } = matched;
|
|
992
|
+
// 3b. Apply decisions
|
|
993
|
+
const adoptedCount = await adoptMatchedEntries(bot, adopted, opContexts);
|
|
994
|
+
let discardedCount = await restoreDiscardedCreates(bot, discarded, opContexts);
|
|
789
995
|
// 5. Log structured summary
|
|
790
996
|
const elapsed = Date.now() - startedAt;
|
|
791
997
|
bot.manager.logger.log(`[COW][UNCERTAIN] Reconciled: ${adoptedCount} adopted, ${discardedCount} discarded ` +
|
|
@@ -805,27 +1011,7 @@ async function reconcileAfterUncertainBroadcastImpl(bot, err, opContexts, _optio
|
|
|
805
1011
|
// 7. Request structural resync if any chain orders remain unaccounted for
|
|
806
1012
|
// after the reconciliation, ensuring the next cycle re-plans from a clean
|
|
807
1013
|
// chain snapshot.
|
|
808
|
-
|
|
809
|
-
if (!alreadyScheduled && chainSnapshot.length > 0) {
|
|
810
|
-
const reconciledOrderIds = new Set(adopted.map((a) => a.match?.id).filter(Boolean));
|
|
811
|
-
const unreconciledCount = chainSnapshot.filter((o) => !reconciledOrderIds.has(o.id)).length;
|
|
812
|
-
if (unreconciledCount > 0) {
|
|
813
|
-
bot.manager.logger.log(`[COW][UNCERTAIN] ${unreconciledCount} chain order(s) remain unreconciled after uncertain broadcast recovery. ` +
|
|
814
|
-
`These may be legitimate pre-existing orders or leftovers from a prior cycle. Requesting structural resync.`, 'warn');
|
|
815
|
-
if (typeof bot.manager.requestStructuralGridResync === 'function') {
|
|
816
|
-
await bot.manager.requestStructuralGridResync('unreconciled orders after uncertain broadcast', {
|
|
817
|
-
batchId: err?.batchId || null,
|
|
818
|
-
pendingCount: pending.length,
|
|
819
|
-
adoptedCount,
|
|
820
|
-
discardedCount,
|
|
821
|
-
unreconciledCount
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
else {
|
|
825
|
-
bot._warn?.('[COW][UNCERTAIN] requestStructuralGridResync unavailable; cannot schedule structural resync.');
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
1014
|
+
await resyncIfUnreconciled(bot, err, pending, chainSnapshot, adopted, adoptedCount, discardedCount);
|
|
829
1015
|
return { executed: false, hadRotation: false, uncertain: true, adoptedCount, discardedCount };
|
|
830
1016
|
}
|
|
831
1017
|
/**
|
|
@@ -846,9 +1032,7 @@ async function autoCancelOneUnmatchedOrphan(bot) {
|
|
|
846
1032
|
bot._autoCancelOrphanCycleMarker = cycleId;
|
|
847
1033
|
bot._autoCancelOrphanSubCount = 0;
|
|
848
1034
|
}
|
|
849
|
-
const pending = (bot
|
|
850
|
-
? bot.manager._pendingBroadcasts.size
|
|
851
|
-
: 0;
|
|
1035
|
+
const pending = countPendingBroadcasts(bot);
|
|
852
1036
|
if (pending > 0) {
|
|
853
1037
|
return { cancelled: false, reason: 'pending-broadcasts-active' };
|
|
854
1038
|
}
|
|
@@ -864,7 +1048,7 @@ async function autoCancelOneUnmatchedOrphan(bot) {
|
|
|
864
1048
|
}
|
|
865
1049
|
const target = unmatched.find((u) => u && u.reason === 'price-drift-orphan');
|
|
866
1050
|
if (!target) {
|
|
867
|
-
return { cancelled: false, reason: 'no-price-drift-orphan', message: 'no price-drift orphan to cancel;
|
|
1051
|
+
return { cancelled: false, reason: 'no-price-drift-orphan', message: 'no price-drift orphan to cancel; remaining unmatched orders need no cancellation (adoptable or deferred holds)' };
|
|
868
1052
|
}
|
|
869
1053
|
const orderId = target.id || target.orderId || target.chainOrderId;
|
|
870
1054
|
if (!orderId) {
|
|
@@ -945,14 +1129,7 @@ function verifyCreateAbsent(bot, freshChain, ctx) {
|
|
|
945
1129
|
sell: ctx.finalInts.sell,
|
|
946
1130
|
receive: ctx.finalInts.receive,
|
|
947
1131
|
orderType: ctx.order.type,
|
|
948
|
-
fingerprint:
|
|
949
|
-
side: ctx.order.type,
|
|
950
|
-
assetA: bot.manager?.assets?.assetA?.id,
|
|
951
|
-
assetB: bot.manager?.assets?.assetB?.id,
|
|
952
|
-
sellInt: ctx.finalInts.sell,
|
|
953
|
-
receiveInt: ctx.finalInts.receive,
|
|
954
|
-
slotId: ctx.order.id
|
|
955
|
-
})
|
|
1132
|
+
fingerprint: createOpFingerprintForSlot(bot, ctx.order, ctx.finalInts, ctx.order.id)
|
|
956
1133
|
});
|
|
957
1134
|
return match ? 'landed' : 'absent';
|
|
958
1135
|
}
|
|
@@ -1033,7 +1210,11 @@ async function executeWithRetryOnUncertain(bot, operations, opContexts) {
|
|
|
1033
1210
|
const accountRef = bot.accountId || bot.account?.id || bot.account;
|
|
1034
1211
|
const freshRead = await chainOrders.readOpenOrdersWithMeta(accountRef);
|
|
1035
1212
|
const freshChain = freshRead.orders;
|
|
1036
|
-
|
|
1213
|
+
// A truncated read (get_full_accounts caps limit_orders, and
|
|
1214
|
+
// fresh creates sort last in the by_account index) omits the
|
|
1215
|
+
// very orders this batch may have landed — 'absent' is not
|
|
1216
|
+
// authoritative here, degrade to 'unknown' and defer.
|
|
1217
|
+
if (isAuthoritativeChainRead(freshRead)) {
|
|
1037
1218
|
absence = 'absent';
|
|
1038
1219
|
for (const ctx of opContexts) {
|
|
1039
1220
|
if (!ctx)
|
|
@@ -1045,13 +1226,6 @@ async function executeWithRetryOnUncertain(bot, operations, opContexts) {
|
|
|
1045
1226
|
}
|
|
1046
1227
|
}
|
|
1047
1228
|
}
|
|
1048
|
-
// A truncated read (get_full_accounts caps limit_orders, and
|
|
1049
|
-
// fresh creates sort last in the by_account index) omits the
|
|
1050
|
-
// very orders this batch may have landed — 'absent' is not
|
|
1051
|
-
// authoritative here, degrade to 'unknown' and defer.
|
|
1052
|
-
if (freshRead.truncated && absence === 'absent') {
|
|
1053
|
-
absence = 'unknown';
|
|
1054
|
-
}
|
|
1055
1229
|
}
|
|
1056
1230
|
catch (verifyErr) {
|
|
1057
1231
|
bot.manager.logger.log(`[COW] Pre-retry chain verification failed (non-fatal): ${verifyErr?.message || verifyErr}`, 'warn');
|
|
@@ -1085,31 +1259,6 @@ async function executeWithRetryOnUncertain(bot, operations, opContexts) {
|
|
|
1085
1259
|
}
|
|
1086
1260
|
}
|
|
1087
1261
|
}
|
|
1088
|
-
/**
|
|
1089
|
-
* Whether a chain order still matches the cached pre-update state the
|
|
1090
|
-
* limit_order_update delta was built from. Only a provably-unchanged order
|
|
1091
|
-
* makes a re-broadcast of the identical delta safe (it applies to the same
|
|
1092
|
-
* base). Any other state (target applied, filled, resized) must defer.
|
|
1093
|
-
* @param {Object} chainOrder - Raw chain order object (get_full_accounts)
|
|
1094
|
-
* @param {Object|null} cachedRaw - The rawOnChain cache captured at build time
|
|
1095
|
-
* @returns {boolean}
|
|
1096
|
-
*/
|
|
1097
|
-
function chainOrderUnchangedFromCache(chainOrder, cachedRaw) {
|
|
1098
|
-
if (!chainOrder || !cachedRaw)
|
|
1099
|
-
return false;
|
|
1100
|
-
const base = chainOrder.sell_price?.base;
|
|
1101
|
-
const quote = chainOrder.sell_price?.quote;
|
|
1102
|
-
const cachedBase = cachedRaw.sell_price?.base?.amount;
|
|
1103
|
-
const cachedQuote = cachedRaw.sell_price?.quote?.amount;
|
|
1104
|
-
const cachedForSale = cachedRaw.for_sale;
|
|
1105
|
-
if (base === undefined || quote === undefined)
|
|
1106
|
-
return false;
|
|
1107
|
-
if (cachedForSale === undefined || cachedBase === undefined || cachedQuote === undefined)
|
|
1108
|
-
return false;
|
|
1109
|
-
return String(base.amount ?? '') === String(cachedBase)
|
|
1110
|
-
&& String(quote.amount ?? '') === String(cachedQuote)
|
|
1111
|
-
&& String(chainOrder.for_sale ?? '') === String(cachedForSale);
|
|
1112
|
-
}
|
|
1113
1262
|
/**
|
|
1114
1263
|
* Summarize a partial (non-atomic) broadcast for the batch-failure catch log.
|
|
1115
1264
|
* Supports both the pair-mode grouped path (groupsBroadcast/groupsTotal) and
|
|
@@ -1347,7 +1496,6 @@ function validateOperationFunds(bot, operations, assetA, assetB) {
|
|
|
1347
1496
|
if (!operations || operations.length === 0) {
|
|
1348
1497
|
return { isValid: true, summary: 'No operations to validate' };
|
|
1349
1498
|
}
|
|
1350
|
-
const { blockchainToFloat, floatToBlockchainInt, quantizeFloat } = require('./order/utils/math');
|
|
1351
1499
|
let snap = bot.manager?.getChainFundsSnapshot?.();
|
|
1352
1500
|
if (!snap) {
|
|
1353
1501
|
snap = { chainFreeSell: 0, chainFreeBuy: 0 };
|
|
@@ -1842,12 +1990,21 @@ function buildCowResultFromPlan(bot, plan) {
|
|
|
1842
1990
|
});
|
|
1843
1991
|
}
|
|
1844
1992
|
}
|
|
1993
|
+
// Refill-slot wire (boundary-hold): producers attach the slot ids of
|
|
1994
|
+
// hole-CREATEs that justify this plan's boundary shift; the executor
|
|
1995
|
+
// holds the committed boundary when a listed refill is guard-skipped.
|
|
1996
|
+
// Absent/non-array => undefined (guarded default at execution, never
|
|
1997
|
+
// fail-open). Spread-correction plans never set this (disjoint bypass).
|
|
1998
|
+
const refillSlotIds = Array.isArray(plan?.refillSlotIds)
|
|
1999
|
+
? plan.refillSlotIds.filter((id) => typeof id === 'string' && id.length > 0)
|
|
2000
|
+
: undefined;
|
|
1845
2001
|
return {
|
|
1846
2002
|
workingGrid,
|
|
1847
2003
|
workingIndexes: workingGrid.getIndexes(),
|
|
1848
2004
|
workingBoundary,
|
|
1849
2005
|
actions,
|
|
1850
|
-
origin: plan?.origin
|
|
2006
|
+
origin: plan?.origin,
|
|
2007
|
+
...(refillSlotIds !== undefined ? { refillSlotIds } : {})
|
|
1851
2008
|
};
|
|
1852
2009
|
}
|
|
1853
2010
|
/**
|
|
@@ -1960,14 +2117,7 @@ async function pollChainForConfirmation(bot, opContexts, options = {}) {
|
|
|
1960
2117
|
receive: ctx.finalInts.receive,
|
|
1961
2118
|
orderType: ctx.order.type,
|
|
1962
2119
|
fingerprint: createContexts.length > 0
|
|
1963
|
-
?
|
|
1964
|
-
side: ctx.order.type,
|
|
1965
|
-
assetA: bot.manager?.assets?.assetA?.id,
|
|
1966
|
-
assetB: bot.manager?.assets?.assetB?.id,
|
|
1967
|
-
sellInt: ctx.finalInts.sell,
|
|
1968
|
-
receiveInt: ctx.finalInts.receive,
|
|
1969
|
-
slotId: ctx.order.id
|
|
1970
|
-
})
|
|
2120
|
+
? createOpFingerprintForSlot(bot, ctx.order, ctx.finalInts, ctx.order.id)
|
|
1971
2121
|
: undefined
|
|
1972
2122
|
});
|
|
1973
2123
|
if (match) {
|
|
@@ -2001,6 +2151,106 @@ async function pollChainForConfirmation(bot, opContexts, options = {}) {
|
|
|
2001
2151
|
`${remaining.length} unconfirmed. Falling back to reconciliation.`, 'warn');
|
|
2002
2152
|
return { allConfirmed: false, confirmed, unconfirmed: remaining, confirmedChainIds: [...matchedChainIds] };
|
|
2003
2153
|
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Normalize a producer-supplied refillSlotIds wire into a Set.
|
|
2156
|
+
* Absent/empty/non-array => empty (guarded default, never fail-open).
|
|
2157
|
+
*/
|
|
2158
|
+
function toRefillSlotIdSet(refillSlotIds) {
|
|
2159
|
+
const set = new Set();
|
|
2160
|
+
if (Array.isArray(refillSlotIds)) {
|
|
2161
|
+
for (const id of refillSlotIds) {
|
|
2162
|
+
if (typeof id === 'string' && id.length > 0)
|
|
2163
|
+
set.add(id);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return set;
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Boundary-hold decision for guard-skipped refills.
|
|
2170
|
+
* When a skipped slot is one of the plan's refill slots — a hole-CREATE that
|
|
2171
|
+
* justified the planned boundary shift — the committed boundary is kept: the
|
|
2172
|
+
* slot was never placed (CREATE skip) or restored to master (UPDATE skip), so
|
|
2173
|
+
* committing the planned boundary would strand empty rail holes past it
|
|
2174
|
+
* (91->94 with 91-94 empty self-legalizes via resolveGapBand).
|
|
2175
|
+
* Unrelated vetoes (skip ids outside the refill set) never pin geometry.
|
|
2176
|
+
* The grid still commits; only the boundary value is held (same discipline
|
|
2177
|
+
* as the overrun-hold in validateBoundaryCommit).
|
|
2178
|
+
*/
|
|
2179
|
+
function resolveRefillBoundaryHold(workingBoundary, committedBoundary, skippedUpdateSlotIds, clampedUpdateSlotIds, refillSlotIds, skippedCreateSlotIds = undefined) {
|
|
2180
|
+
const refills = toRefillSlotIdSet(refillSlotIds);
|
|
2181
|
+
const heldRefillSlotIds = [];
|
|
2182
|
+
if (refills.size > 0) {
|
|
2183
|
+
const seen = new Set();
|
|
2184
|
+
const skipCollections = skippedCreateSlotIds !== undefined
|
|
2185
|
+
? [skippedUpdateSlotIds, clampedUpdateSlotIds, skippedCreateSlotIds]
|
|
2186
|
+
: [skippedUpdateSlotIds, clampedUpdateSlotIds];
|
|
2187
|
+
for (const coll of skipCollections) {
|
|
2188
|
+
if (!coll || typeof coll[Symbol.iterator] !== 'function')
|
|
2189
|
+
continue;
|
|
2190
|
+
for (const id of coll) {
|
|
2191
|
+
if (typeof id === 'string' && refills.has(id) && !seen.has(id)) {
|
|
2192
|
+
seen.add(id);
|
|
2193
|
+
heldRefillSlotIds.push(id);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
return {
|
|
2199
|
+
effectiveBoundary: heldRefillSlotIds.length > 0 ? committedBoundary : workingBoundary,
|
|
2200
|
+
heldRefillSlotIds
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Track consecutive boundary-hold batches on the manager (ops visibility).
|
|
2205
|
+
*
|
|
2206
|
+
* A single hold is normal maker discipline: the guard vetoed stale-priced
|
|
2207
|
+
* refills, so the committed boundary stays instead of advancing past
|
|
2208
|
+
* stranded rail holes. A growing run means the grid is trailing the market
|
|
2209
|
+
* (plans keep pricing refills against a racing guard pivot, typically while
|
|
2210
|
+
* fill batches run on backlogged state) and only fresh fills unstick it —
|
|
2211
|
+
* worth escalating so it cannot hide inside per-batch warns.
|
|
2212
|
+
*
|
|
2213
|
+
* Also records a hold signature (`_lastHeldPlanSignature`) for the
|
|
2214
|
+
* identical-held-plan suppression in `performSafeRebalance`, and the caller
|
|
2215
|
+
* escalates a long run to a guard-aware structural re-center. A fill-less
|
|
2216
|
+
* re-plan from unchanged master re-derives the identical plan and re-hits
|
|
2217
|
+
* the identical guard blocks, so it is suppressed rather than re-broadcast;
|
|
2218
|
+
* the heal path is a fresh fill-driven plan or the re-center.
|
|
2219
|
+
* @param {any} manager - Order manager (mutable tracking fields)
|
|
2220
|
+
* @param {boolean} held - Whether this batch held the boundary
|
|
2221
|
+
* @param {any} keptBoundary - Committed boundary that was kept
|
|
2222
|
+
* @param {any} plannedBoundary - Boundary the plan wanted
|
|
2223
|
+
* @param {string[]} heldSlotIds - Refill slots skipped this batch
|
|
2224
|
+
* @returns {number} Consecutive-hold count after this batch (0 when clear)
|
|
2225
|
+
*/
|
|
2226
|
+
function trackBoundaryHold(manager, held, keptBoundary, plannedBoundary, heldSlotIds) {
|
|
2227
|
+
if (!manager)
|
|
2228
|
+
return 0;
|
|
2229
|
+
const prev = Number(manager._consecutiveBoundaryHolds) || 0;
|
|
2230
|
+
const consecutive = held ? prev + 1 : 0;
|
|
2231
|
+
manager._consecutiveBoundaryHolds = consecutive;
|
|
2232
|
+
if (held) {
|
|
2233
|
+
manager._lastBoundaryHoldInfo = {
|
|
2234
|
+
at: Date.now(),
|
|
2235
|
+
kept: keptBoundary,
|
|
2236
|
+
planned: plannedBoundary,
|
|
2237
|
+
slots: Array.isArray(heldSlotIds) ? [...heldSlotIds] : [],
|
|
2238
|
+
};
|
|
2239
|
+
// Signature for the identical-held-plan suppression in
|
|
2240
|
+
// performSafeRebalance: a fill-less replan with the same boundary,
|
|
2241
|
+
// pivot and fill timestamp can only reproduce this hold.
|
|
2242
|
+
manager._lastHeldPlanSignature = {
|
|
2243
|
+
boundaryIdx: keptBoundary,
|
|
2244
|
+
pivot: manager._lastFilledPrice ?? null,
|
|
2245
|
+
fillsAt: manager._lastFilledAt ?? 0,
|
|
2246
|
+
wire: Array.isArray(heldSlotIds) ? [...heldSlotIds] : [],
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
else {
|
|
2250
|
+
manager._lastHeldPlanSignature = null;
|
|
2251
|
+
}
|
|
2252
|
+
return consecutive;
|
|
2253
|
+
}
|
|
2004
2254
|
/**
|
|
2005
2255
|
* Restore skipped update slots in the working grid to master state.
|
|
2006
2256
|
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
@@ -2182,87 +2432,6 @@ async function waitForCowBroadcastSingleFlight(bot, label) {
|
|
|
2182
2432
|
* @param {Object} [options={}] - Internal execution options (replanDepth)
|
|
2183
2433
|
* @returns {Promise<Object>}
|
|
2184
2434
|
*/
|
|
2185
|
-
/**
|
|
2186
|
-
* PRE-BROADCAST CROSSED-BOOK ASSERT (defense-in-depth, any-writer detection).
|
|
2187
|
-
*
|
|
2188
|
-
* Simulates the post-batch book: currently placed master orders plus this
|
|
2189
|
-
* batch's action overlay (CREATEs add, CANCELs remove, UPDATEs reprice/move).
|
|
2190
|
-
* Returns a detail string when a planned BUY would price at-or-above a planned
|
|
2191
|
-
* SELL — a state no honest planner produces — so the caller can refuse the
|
|
2192
|
-
* broadcast instead of paying for adverse fills. Placed order prices are
|
|
2193
|
-
* independent of grid geometry, so this catches boundary overruns regardless
|
|
2194
|
-
* of which writer produced them.
|
|
2195
|
-
*
|
|
2196
|
-
* Detector only: any internal failure returns null (never blocks a broadcast).
|
|
2197
|
-
*/
|
|
2198
|
-
function detectCrossedBookPlan(manager, actions) {
|
|
2199
|
-
try {
|
|
2200
|
-
const startPrice = Number(manager?.config?.startPrice);
|
|
2201
|
-
const book = new Map();
|
|
2202
|
-
for (const o of Array.from(manager?.orders?.values?.() ?? [])) {
|
|
2203
|
-
if (!o || !o.orderId || o.price == null)
|
|
2204
|
-
continue;
|
|
2205
|
-
const price = Number(o.price);
|
|
2206
|
-
if (!Number.isFinite(price))
|
|
2207
|
-
continue;
|
|
2208
|
-
let type = o.type;
|
|
2209
|
-
if (type !== ORDER_TYPES.BUY && type !== ORDER_TYPES.SELL) {
|
|
2210
|
-
// Legacy SPREAD-typed placed order: derive side from the same
|
|
2211
|
-
// price-vs-startPrice convention used across the codebase.
|
|
2212
|
-
if (!Number.isFinite(startPrice))
|
|
2213
|
-
continue;
|
|
2214
|
-
type = price < startPrice ? ORDER_TYPES.BUY : ORDER_TYPES.SELL;
|
|
2215
|
-
}
|
|
2216
|
-
book.set(String(o.id), { type, price });
|
|
2217
|
-
}
|
|
2218
|
-
for (const a of actions ?? []) {
|
|
2219
|
-
const id = String(a.id ?? a.orderId ?? '');
|
|
2220
|
-
if (a.type === COW_ACTIONS.CANCEL) {
|
|
2221
|
-
if (id)
|
|
2222
|
-
book.delete(id);
|
|
2223
|
-
}
|
|
2224
|
-
else if (a.type === COW_ACTIONS.UPDATE) {
|
|
2225
|
-
const newPrice = Number(a.newPrice ?? a.order?.price);
|
|
2226
|
-
const newType = a.order?.type;
|
|
2227
|
-
if (id && Number.isFinite(newPrice)) {
|
|
2228
|
-
const entry = book.get(id);
|
|
2229
|
-
const type = (newType === ORDER_TYPES.BUY || newType === ORDER_TYPES.SELL)
|
|
2230
|
-
? newType
|
|
2231
|
-
: entry?.type;
|
|
2232
|
-
if (entry)
|
|
2233
|
-
book.delete(id);
|
|
2234
|
-
const key = String(a.newGridId ?? id);
|
|
2235
|
-
if (type === ORDER_TYPES.BUY || type === ORDER_TYPES.SELL) {
|
|
2236
|
-
book.set(key, { type, price: newPrice });
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
else if (a.type === COW_ACTIONS.CREATE) {
|
|
2241
|
-
const price = Number(a.order?.price);
|
|
2242
|
-
const type = a.order?.type;
|
|
2243
|
-
if (!Number.isFinite(price) || (type !== ORDER_TYPES.BUY && type !== ORDER_TYPES.SELL))
|
|
2244
|
-
continue;
|
|
2245
|
-
if (id)
|
|
2246
|
-
book.set(id, { type, price });
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
let maxBuy = -Infinity;
|
|
2250
|
-
let minSell = Infinity;
|
|
2251
|
-
for (const { type, price } of book.values()) {
|
|
2252
|
-
if (type === ORDER_TYPES.BUY && price > maxBuy)
|
|
2253
|
-
maxBuy = price;
|
|
2254
|
-
else if (type === ORDER_TYPES.SELL && price < minSell)
|
|
2255
|
-
minSell = price;
|
|
2256
|
-
}
|
|
2257
|
-
if (Number.isFinite(maxBuy) && Number.isFinite(minSell) && minSell <= maxBuy) {
|
|
2258
|
-
return `bestPlacedBuy=${maxBuy} >= bestPlacedSell=${minSell}`;
|
|
2259
|
-
}
|
|
2260
|
-
return null;
|
|
2261
|
-
}
|
|
2262
|
-
catch {
|
|
2263
|
-
return null;
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
2435
|
/**
|
|
2267
2436
|
* Derive an update action's planned target size (shared by the rotation and
|
|
2268
2437
|
* plain size-update op builders).
|
|
@@ -2305,58 +2474,22 @@ function clampPostFillUpdateSize(bot, masterOrder, newSize, action) {
|
|
|
2305
2474
|
}
|
|
2306
2475
|
return target;
|
|
2307
2476
|
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
}
|
|
2325
|
-
// Single-flight COW broadcast guard (entry check): never broadcast two
|
|
2326
|
-
// batches concurrently. Overlapping broadcasts plan from the same base
|
|
2327
|
-
// grid version; when the first commits it bumps _gridVersion, so the
|
|
2328
|
-
// second's commit is refused (base-version mismatch) -> adopt-from-chain
|
|
2329
|
-
// -> snapshot reload that can drop the adopted order and produce an
|
|
2330
|
-
// orphan fill. This entry wait is an optimization; the authoritative
|
|
2331
|
-
// atomic check-and-set happens right before the broadcast below.
|
|
2332
|
-
if (await waitForCowBroadcastSingleFlight(bot, 'entry')) {
|
|
2333
|
-
popPushedWorkingGrid(bot, cowResult);
|
|
2334
|
-
return { executed: false, aborted: true, reason: 'SHUTDOWN_IN_PROGRESS', hadRotation: false };
|
|
2335
|
-
}
|
|
2336
|
-
// DRAIN PENDING CORRECTIONS before the batch is planned/broadcast.
|
|
2337
|
-
// Cancel-only corrections (duplicate-price orphans) queued by an earlier
|
|
2338
|
-
// sync must not sit while batches run back-to-back (startup create
|
|
2339
|
-
// groups, fill bursts) — with the open-orders sync loop disabled they
|
|
2340
|
-
// would otherwise linger indefinitely, keep blocking same-level CREATEs,
|
|
2341
|
-
// and risk cancelling the wrong side of a duplicate later. Draining here
|
|
2342
|
-
// also keeps this batch's collision checks (chain_orphan_collision)
|
|
2343
|
-
// honest: orphaned chain orders already queued for cancellation are
|
|
2344
|
-
// resolved before the plan validates its CREATE targets against them.
|
|
2345
|
-
const pendingCorrectionCount = Array.isArray(bot.manager?.ordersNeedingPriceCorrection)
|
|
2346
|
-
? bot.manager.ordersNeedingPriceCorrection.length
|
|
2347
|
-
: 0;
|
|
2348
|
-
if (pendingCorrectionCount > 0 && !bot._shuttingDown) {
|
|
2349
|
-
try {
|
|
2350
|
-
bot.manager.logger.log(`[COW] Draining ${pendingCorrectionCount} pending correction(s) before batch`, 'info');
|
|
2351
|
-
const drainResult = await orderUtils.correctAllPriceMismatches(bot.manager, bot.account, bot.privateKey, chainOrders);
|
|
2352
|
-
if (drainResult?.failed > 0) {
|
|
2353
|
-
bot.manager.logger.log(`[COW] ${drainResult.failed} correction(s) failed pre-batch; remaining entries retry on next sync/maintenance tick`, 'warn');
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
catch (drainErr) {
|
|
2357
|
-
bot.manager.logger.log(`[COW] Pre-batch correction drain failed: ${getErrorMessage(drainErr)}`, 'warn');
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Pre-broadcast guard chain for a COW batch: create-slot validation (with
|
|
2479
|
+
* tolerance-violation filtering), recovery-exhausted block, pending-broadcast
|
|
2480
|
+
* and unmatched-chain-order guards (with adoption sync), and the crossed-book
|
|
2481
|
+
* gate. Every refusal pops the pushed working grid before returning.
|
|
2482
|
+
*
|
|
2483
|
+
* Mutates cowResult.actions in place (tolerance-violating CREATEs are
|
|
2484
|
+
* filtered); the caller holds the same array reference.
|
|
2485
|
+
*
|
|
2486
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
2487
|
+
* @param {Object} cowResult - Rebalance/COW result carrying actions
|
|
2488
|
+
* @returns {Promise<Object>} { proceed:false, result } on refusal, or
|
|
2489
|
+
* { proceed:true, crossingCandidates, intraBatchCandidates } on pass
|
|
2490
|
+
*/
|
|
2491
|
+
async function runPreBroadcastGuards(bot, cowResult) {
|
|
2492
|
+
const { actions } = cowResult;
|
|
2360
2493
|
const chainOrderCandidates = Array.isArray(bot.manager?._lastUnmatchedChainOrders)
|
|
2361
2494
|
? bot.manager._lastUnmatchedChainOrders
|
|
2362
2495
|
: [];
|
|
@@ -2398,11 +2531,14 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2398
2531
|
if (hasHardOccupiedViolation) {
|
|
2399
2532
|
popPushedWorkingGrid(bot, cowResult);
|
|
2400
2533
|
return {
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2534
|
+
proceed: false,
|
|
2535
|
+
result: {
|
|
2536
|
+
executed: false,
|
|
2537
|
+
aborted: true,
|
|
2538
|
+
reason: 'CREATE_SLOT_OCCUPIED',
|
|
2539
|
+
violations: createSlotValidation.violations,
|
|
2540
|
+
hadRotation: false
|
|
2541
|
+
}
|
|
2406
2542
|
};
|
|
2407
2543
|
}
|
|
2408
2544
|
const violatingIds = createSlotValidation.violatingTargetIds;
|
|
@@ -2424,7 +2560,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2424
2560
|
// returns: a pushed working grid must be popped here or the
|
|
2425
2561
|
// caller would leak the stack entry.
|
|
2426
2562
|
popPushedWorkingGrid(bot, cowResult);
|
|
2427
|
-
return { executed: false, hadRotation: false };
|
|
2563
|
+
return { proceed: false, result: { executed: false, hadRotation: false } };
|
|
2428
2564
|
}
|
|
2429
2565
|
}
|
|
2430
2566
|
}
|
|
@@ -2436,27 +2572,29 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2436
2572
|
`Waiting for next fill or sync cycle to reset recovery state.`, 'warn');
|
|
2437
2573
|
popPushedWorkingGrid(bot, cowResult);
|
|
2438
2574
|
return {
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2575
|
+
proceed: false,
|
|
2576
|
+
result: {
|
|
2577
|
+
executed: false,
|
|
2578
|
+
aborted: true,
|
|
2579
|
+
reason: 'RECOVERY_EXHAUSTED',
|
|
2580
|
+
hadRotation: false
|
|
2581
|
+
}
|
|
2443
2582
|
};
|
|
2444
2583
|
}
|
|
2445
2584
|
const unmatchedChainOrders = Array.isArray(bot.manager?._lastUnmatchedChainOrders)
|
|
2446
2585
|
? bot.manager._lastUnmatchedChainOrders
|
|
2447
2586
|
: [];
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2587
|
+
// Out-of-grid holds are permanent by design (live orders held outside
|
|
2588
|
+
// the frozen rail): they can never be adopted and collide with nothing,
|
|
2589
|
+
// so they must not block CREATES — otherwise one dip-protection hold
|
|
2590
|
+
// freezes the whole grid. Only adoptable/cancellable orphans block.
|
|
2591
|
+
const blockingUnmatched = unmatchedChainOrders.filter((u) => !orderUtils.isNonBlockingUnmatchedOrder(u));
|
|
2592
|
+
const pendingBroadcasts = getPendingBroadcasts(bot);
|
|
2593
|
+
if (hasCreateActions && (blockingUnmatched.length > 0 || pendingBroadcasts.length > 0)) {
|
|
2452
2594
|
if (pendingBroadcasts.length > 0) {
|
|
2453
2595
|
bot.manager.logger.log(`[COW] Rejecting CREATE batch: ${pendingBroadcasts.length} pending broadcast(s) from a prior uncertain ` +
|
|
2454
2596
|
`broadcast. Running recovery before placing replacement orders.`, 'error');
|
|
2455
|
-
|
|
2456
|
-
if (bot.manager._recoveryState)
|
|
2457
|
-
bot.manager._recoveryState = { ...bot.manager._recoveryState, structuralResyncRequested: true };
|
|
2458
|
-
await bot.manager.requestStructuralGridResync('pending broadcasts before COW create', { pendingBroadcasts: pendingBroadcasts.map((p) => p.slotId) });
|
|
2459
|
-
}
|
|
2597
|
+
await requestStructuralResync(bot, 'pending broadcasts before COW create', { pendingBroadcasts: pendingBroadcasts.map((p) => p.slotId) });
|
|
2460
2598
|
try {
|
|
2461
2599
|
await reconcileAfterUncertainBroadcast(bot, new BroadcastUncertainError('rejected CREATE batch had pending broadcasts', {
|
|
2462
2600
|
operations: pendingBroadcasts.map((p) => p.order),
|
|
@@ -2471,17 +2609,20 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2471
2609
|
}
|
|
2472
2610
|
popPushedWorkingGrid(bot, cowResult);
|
|
2473
2611
|
return {
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2612
|
+
proceed: false,
|
|
2613
|
+
result: {
|
|
2614
|
+
executed: false,
|
|
2615
|
+
aborted: true,
|
|
2616
|
+
reason: 'PENDING_BROADCASTS',
|
|
2617
|
+
hadRotation: false
|
|
2618
|
+
}
|
|
2478
2619
|
};
|
|
2479
2620
|
}
|
|
2480
|
-
const unmatchedSample =
|
|
2621
|
+
const unmatchedSample = blockingUnmatched
|
|
2481
2622
|
.slice(0, 3)
|
|
2482
2623
|
.map((o) => formatUnmatchedChainOrderForLog(o))
|
|
2483
2624
|
.join(' | ');
|
|
2484
|
-
bot.manager.logger.log(`[COW] ${
|
|
2625
|
+
bot.manager.logger.log(`[COW] ${blockingUnmatched.length} unmatched chain order(s) blocking CREATES ` +
|
|
2485
2626
|
(unmatchedSample ? `(${unmatchedSample})` : '') +
|
|
2486
2627
|
` — adopting via sync instead of cancelling`, 'info');
|
|
2487
2628
|
try {
|
|
@@ -2491,8 +2632,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2491
2632
|
// freshest orders; syncing on it would virtualize live slots and
|
|
2492
2633
|
// re-create duplicates. Defer the adoption to a clean read — the
|
|
2493
2634
|
// unmatched orders keep blocking CREATEs until then.
|
|
2494
|
-
if (freshRead
|
|
2495
|
-
bot.manager.logger.log('[COW] Post-guard chain snapshot
|
|
2635
|
+
if (!isAuthoritativeChainRead(freshRead)) {
|
|
2636
|
+
bot.manager.logger.log('[COW] Post-guard chain snapshot not authoritative; skipping adoption sync (partial snapshot would virtualize live slots) — unmatched chain orders keep blocking CREATEs', 'warn');
|
|
2496
2637
|
}
|
|
2497
2638
|
else if (freshRead.orders && freshRead.orders.length > 0) {
|
|
2498
2639
|
const freshSnapshot = freshRead.orders;
|
|
@@ -2532,18 +2673,17 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2532
2673
|
catch (syncErr) {
|
|
2533
2674
|
bot.manager.logger.log(`[COW] Failed to sync/unmatched orders: ${syncErr?.message || syncErr}`, 'warn');
|
|
2534
2675
|
}
|
|
2535
|
-
|
|
2536
|
-
if (bot.manager._recoveryState)
|
|
2537
|
-
bot.manager._recoveryState = { ...bot.manager._recoveryState, structuralResyncRequested: true };
|
|
2538
|
-
await bot.manager.requestStructuralGridResync('unmatched chain orders before COW create', { unmatchedChainOrders: unmatchedChainOrders });
|
|
2539
|
-
}
|
|
2676
|
+
await requestStructuralResync(bot, 'unmatched chain orders before COW create', { unmatchedChainOrders: unmatchedChainOrders });
|
|
2540
2677
|
bot.manager.logger.log(`[COW] Rejecting CREATE batch after sync: working grid invalidated by master mutation`, 'info');
|
|
2541
2678
|
popPushedWorkingGrid(bot, cowResult);
|
|
2542
2679
|
return {
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2680
|
+
proceed: false,
|
|
2681
|
+
result: {
|
|
2682
|
+
executed: false,
|
|
2683
|
+
aborted: true,
|
|
2684
|
+
reason: 'UNMATCHED_CHAIN_ORDERS',
|
|
2685
|
+
hadRotation: false
|
|
2686
|
+
}
|
|
2547
2687
|
};
|
|
2548
2688
|
}
|
|
2549
2689
|
// Crossing-check candidate set (master + pending-broadcast + unmatched
|
|
@@ -2551,7 +2691,6 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2551
2691
|
// it reflects any sync they triggered.
|
|
2552
2692
|
const crossingCandidates = buildCrossingCandidates(bot);
|
|
2553
2693
|
const intraBatchCandidates = [];
|
|
2554
|
-
const { assetA, assetB } = bot.manager.assets;
|
|
2555
2694
|
// CROSSED-BOOK GATE: refuse to broadcast any batch whose simulated result
|
|
2556
2695
|
// prices a BUY at-or-above a SELL (see detectCrossedBookPlan).
|
|
2557
2696
|
const crossedBookDetail = detectCrossedBookPlan(bot.manager, actions);
|
|
@@ -2559,17 +2698,103 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2559
2698
|
bot.manager.logger.log(`[COW] Rejecting batch pre-broadcast: crossed book detected (${crossedBookDetail})`, 'error');
|
|
2560
2699
|
popPushedWorkingGrid(bot, cowResult);
|
|
2561
2700
|
return {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2701
|
+
proceed: false,
|
|
2702
|
+
result: {
|
|
2703
|
+
executed: false,
|
|
2704
|
+
aborted: true,
|
|
2705
|
+
reason: 'CROSSED_BOOK',
|
|
2706
|
+
detail: crossedBookDetail,
|
|
2707
|
+
hadRotation: false
|
|
2708
|
+
}
|
|
2567
2709
|
};
|
|
2568
2710
|
}
|
|
2711
|
+
return { proceed: true, crossingCandidates, intraBatchCandidates };
|
|
2712
|
+
}
|
|
2713
|
+
async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
2714
|
+
const replanDepth = Number.isFinite(Number(options?.replanDepth)) ? Number(options.replanDepth) : 0;
|
|
2715
|
+
bot._currentCycleId = (Number.isFinite(Number(bot._currentCycleId)) ? Number(bot._currentCycleId) : 0) + 1;
|
|
2716
|
+
const { workingGrid, workingIndexes, workingBoundary, actions } = cowResult;
|
|
2717
|
+
// Boundary-hold value: computed pre-broadcast after the skip-restore and
|
|
2718
|
+
// frozen for every downstream commit path (success + uncertain-catch).
|
|
2719
|
+
// workingBoundary itself stays untouched (audit trail).
|
|
2720
|
+
let effectiveBoundary = workingBoundary;
|
|
2721
|
+
// True when the refill hold pinned the committed boundary over the plan's
|
|
2722
|
+
// target. Hoisted out of the broadcast try block: the uncertain-broadcast
|
|
2723
|
+
// catch commits too, and a held boundary must keep the owed fill crawls
|
|
2724
|
+
// there as well (see _commitWorkingGrid pending-crawl bookkeeping).
|
|
2725
|
+
let boundaryHeld = false;
|
|
2726
|
+
// Consecutive-hold re-center tuning (see the escalation block below).
|
|
2727
|
+
const boundaryHoldTiming = constantsModule?.TIMING || {};
|
|
2728
|
+
const holdResyncThreshold = Number(boundaryHoldTiming.BOUNDARY_HOLD_RESYNC_THRESHOLD) > 0
|
|
2729
|
+
? Number(boundaryHoldTiming.BOUNDARY_HOLD_RESYNC_THRESHOLD)
|
|
2730
|
+
: 4;
|
|
2731
|
+
const holdResyncCooldownMs = Number(boundaryHoldTiming.BOUNDARY_HOLD_RESYNC_COOLDOWN_MS) > 0
|
|
2732
|
+
? Number(boundaryHoldTiming.BOUNDARY_HOLD_RESYNC_COOLDOWN_MS)
|
|
2733
|
+
: 5 * 60 * 1000;
|
|
2734
|
+
if (bot.config.dryRun) {
|
|
2735
|
+
const cancelCount = actions.filter((a) => a.type === COW_ACTIONS.CANCEL).length;
|
|
2736
|
+
const createCount = actions.filter((a) => a.type === COW_ACTIONS.CREATE).length;
|
|
2737
|
+
const updateCount = actions.filter((a) => a.type === COW_ACTIONS.UPDATE).length;
|
|
2738
|
+
if (cancelCount > 0)
|
|
2739
|
+
bot.manager.logger.log(`Dry run: would cancel ${cancelCount} orders`, 'info');
|
|
2740
|
+
if (createCount > 0)
|
|
2741
|
+
bot.manager.logger.log(`Dry run: would place ${createCount} new orders`, 'info');
|
|
2742
|
+
if (updateCount > 0)
|
|
2743
|
+
bot.manager.logger.log(`Dry run: would update ${updateCount} orders`, 'info');
|
|
2744
|
+
popPushedWorkingGrid(bot, cowResult);
|
|
2745
|
+
return { executed: true, hadRotation: false };
|
|
2746
|
+
}
|
|
2747
|
+
// Single-flight COW broadcast guard (entry check): never broadcast two
|
|
2748
|
+
// batches concurrently. Overlapping broadcasts plan from the same base
|
|
2749
|
+
// grid version; when the first commits it bumps _gridVersion, so the
|
|
2750
|
+
// second's commit is refused (base-version mismatch) -> adopt-from-chain
|
|
2751
|
+
// -> snapshot reload that can drop the adopted order and produce an
|
|
2752
|
+
// orphan fill. This entry wait is an optimization; the authoritative
|
|
2753
|
+
// atomic check-and-set happens right before the broadcast below.
|
|
2754
|
+
if (await waitForCowBroadcastSingleFlight(bot, 'entry')) {
|
|
2755
|
+
popPushedWorkingGrid(bot, cowResult);
|
|
2756
|
+
return { executed: false, aborted: true, reason: 'SHUTDOWN_IN_PROGRESS', hadRotation: false };
|
|
2757
|
+
}
|
|
2758
|
+
// DRAIN PENDING CORRECTIONS before the batch is planned/broadcast.
|
|
2759
|
+
// Cancel-only corrections (duplicate-price orphans) queued by an earlier
|
|
2760
|
+
// sync must not sit while batches run back-to-back (startup create
|
|
2761
|
+
// groups, fill bursts) — with the open-orders sync loop disabled they
|
|
2762
|
+
// would otherwise linger indefinitely, keep blocking same-level CREATEs,
|
|
2763
|
+
// and risk cancelling the wrong side of a duplicate later. Draining here
|
|
2764
|
+
// also keeps this batch's collision checks (chain_orphan_collision)
|
|
2765
|
+
// honest: orphaned chain orders already queued for cancellation are
|
|
2766
|
+
// resolved before the plan validates its CREATE targets against them.
|
|
2767
|
+
const pendingCorrectionCount = Array.isArray(bot.manager?.ordersNeedingPriceCorrection)
|
|
2768
|
+
? bot.manager.ordersNeedingPriceCorrection.length
|
|
2769
|
+
: 0;
|
|
2770
|
+
if (pendingCorrectionCount > 0 && !bot._shuttingDown) {
|
|
2771
|
+
try {
|
|
2772
|
+
bot.manager.logger.log(`[COW] Draining ${pendingCorrectionCount} pending correction(s) before batch`, 'info');
|
|
2773
|
+
const drainResult = await orderUtils.correctAllPriceMismatches(bot.manager, bot.account, bot.privateKey, chainOrders);
|
|
2774
|
+
if (drainResult?.failed > 0) {
|
|
2775
|
+
bot.manager.logger.log(`[COW] ${drainResult.failed} correction(s) failed pre-batch; remaining entries retry on next sync/maintenance tick`, 'warn');
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
catch (drainErr) {
|
|
2779
|
+
bot.manager.logger.log(`[COW] Pre-batch correction drain failed: ${getErrorMessage(drainErr)}`, 'warn');
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
// Pre-broadcast guard chain (create-slot validation, recovery-exhausted
|
|
2783
|
+
// block, pending/unmatched guards, crossed-book gate) — see
|
|
2784
|
+
// runPreBroadcastGuards. Refusals already popped the working grid.
|
|
2785
|
+
const guards = await runPreBroadcastGuards(bot, cowResult);
|
|
2786
|
+
if (!guards.proceed)
|
|
2787
|
+
return guards.result;
|
|
2788
|
+
const { crossingCandidates, intraBatchCandidates } = guards;
|
|
2789
|
+
const { assetA, assetB } = bot.manager.assets;
|
|
2569
2790
|
const operations = [];
|
|
2570
2791
|
const opContexts = [];
|
|
2571
2792
|
const skippedUpdateSlotIds = new Set();
|
|
2572
2793
|
let skippedUpdateCount = 0;
|
|
2794
|
+
// Guard-skipped CREATE slot ids (hole-refills never placed). Fed to the
|
|
2795
|
+
// boundary-hold intersect alongside the UPDATE sets above — a skipped
|
|
2796
|
+
// refill CREATE strands its rail hole exactly like a restored UPDATE.
|
|
2797
|
+
const skippedCreateSlotIds = new Set();
|
|
2573
2798
|
// Per-batch LAST-FILL-GUARD disposition counters. Per-action pass lines
|
|
2574
2799
|
// would spam big batches, so the guard emits one batch summary instead
|
|
2575
2800
|
// (see the summary after the action loop below).
|
|
@@ -2619,6 +2844,16 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2619
2844
|
bot._markGridActivity('batch start');
|
|
2620
2845
|
bot.manager._setRebalanceState(REBALANCE_STATES.BROADCASTING);
|
|
2621
2846
|
bot.manager.startBroadcasting();
|
|
2847
|
+
// P3 — freeze the guard pivot once per batch: per-action refreshes
|
|
2848
|
+
// mutated the pivot mid-batch (02:03 pivots drifted 0.001523→0.001529
|
|
2849
|
+
// across 20 checks), so early actions were judged against a different
|
|
2850
|
+
// pivot than later ones. The batch summary still reports whether this
|
|
2851
|
+
// freeze moved the pivot under the plan.
|
|
2852
|
+
try {
|
|
2853
|
+
if (refreshLastFillPivotFromQueue(bot))
|
|
2854
|
+
lastFillGuardPivotRefreshed = true;
|
|
2855
|
+
}
|
|
2856
|
+
catch { /* best-effort */ }
|
|
2622
2857
|
for (const action of actions) {
|
|
2623
2858
|
if (action.type === COW_ACTIONS.CANCEL) {
|
|
2624
2859
|
try {
|
|
@@ -2645,6 +2880,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2645
2880
|
const sizeValidation = validateOrderSizeForExecution(bot, order.size, order.type, order, order.size);
|
|
2646
2881
|
if (!sizeValidation.isValid) {
|
|
2647
2882
|
bot.manager.logger.log(`Skipping create op for ${action.id}: ${sizeValidation.reason}`, 'warn');
|
|
2883
|
+
if (action.id)
|
|
2884
|
+
skippedCreateSlotIds.add(action.id);
|
|
2648
2885
|
continue;
|
|
2649
2886
|
}
|
|
2650
2887
|
const liveSlot = bot.manager.orders.get(order.id);
|
|
@@ -2669,6 +2906,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2669
2906
|
`same-batch CREATE ${batchCollision.id} already at ` +
|
|
2670
2907
|
`price ${Format.formatPrice6(batchCollision.order.price)}. ` +
|
|
2671
2908
|
`The next reconcile cycle will resolve the mismatch.`, 'warn');
|
|
2909
|
+
if (order.id)
|
|
2910
|
+
skippedCreateSlotIds.add(order.id);
|
|
2672
2911
|
continue;
|
|
2673
2912
|
}
|
|
2674
2913
|
// CROSSING-PLACEMENT GUARD (create variant): the batch-level
|
|
@@ -2687,6 +2926,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2687
2926
|
bot.manager.logger.log(`[COW-CROSS-GUARD] Skipping CREATE for ${order.id} at ` +
|
|
2688
2927
|
`${Format.formatPrice6(createPrice)}: crosses live ` +
|
|
2689
2928
|
`${crossedOrderLabel(effectiveCrossed)}; re-planned after its cancel confirms.`, 'warn');
|
|
2929
|
+
if (order.id)
|
|
2930
|
+
skippedCreateSlotIds.add(order.id);
|
|
2690
2931
|
continue;
|
|
2691
2932
|
}
|
|
2692
2933
|
// LAST-FILL PRICE GUARD: pivot ± halfIncrement (BUY < pivot*(1-half), SELL > pivot*(1+half)).
|
|
@@ -2699,20 +2940,15 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2699
2940
|
const isCorrectionCreate = actionOrigin === 'spread-correction'
|
|
2700
2941
|
|| (actionOrigin == null && batchOrigin === 'spread-correction');
|
|
2701
2942
|
if (!isCorrectionCreate) {
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
}
|
|
2706
|
-
catch { /* best-effort */ }
|
|
2707
|
-
const lastPrice = bot.manager?._lastFilledPrice;
|
|
2708
|
-
const lastType = bot.manager?._lastFilledType;
|
|
2709
|
-
const inc = resolveLastFillGuardIncrement(bot);
|
|
2710
|
-
const check = isLastFillGuardBlocked(createPrice, order.size, order.type, lastPrice, lastType, inc);
|
|
2711
|
-
lastFillGuardStats.checked++;
|
|
2943
|
+
const { check, refreshed } = runLastFillGuardCheck(bot, createPrice, order.size, order.type, lastFillGuardStats, true);
|
|
2944
|
+
if (refreshed)
|
|
2945
|
+
lastFillGuardPivotRefreshed = true;
|
|
2712
2946
|
if (check.blocked) {
|
|
2713
2947
|
lastFillGuardStats.skipped++;
|
|
2714
2948
|
const dir = order.type === ORDER_TYPES.BUY ? 'above' : 'below';
|
|
2715
|
-
bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${order.type} CREATE for ${order.id} at ${Format.formatPrice6(createPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)}); re-planned after market moves`, '
|
|
2949
|
+
bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${order.type} CREATE for ${order.id} at ${Format.formatPrice6(createPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)}); re-planned after market moves`, 'debug');
|
|
2950
|
+
if (order.id)
|
|
2951
|
+
skippedCreateSlotIds.add(order.id);
|
|
2716
2952
|
continue;
|
|
2717
2953
|
}
|
|
2718
2954
|
lastFillGuardStats.passed++;
|
|
@@ -2726,6 +2962,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2726
2962
|
const buildResult = await chainOrders.buildCreateOrderOp(bot.account, args.amountToSell, args.sellAssetId, args.minToReceive, args.receiveAssetId, null);
|
|
2727
2963
|
if (!buildResult) {
|
|
2728
2964
|
bot.manager.logger.log(`Skipping create op for ${action.id}: amounts would round to 0 on blockchain`, 'warn');
|
|
2965
|
+
if (action.id)
|
|
2966
|
+
skippedCreateSlotIds.add(action.id);
|
|
2729
2967
|
continue;
|
|
2730
2968
|
}
|
|
2731
2969
|
operations.push(buildResult.op);
|
|
@@ -2779,6 +3017,15 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2779
3017
|
}
|
|
2780
3018
|
const rotationSizeValidation = validateOrderSizeForExecution(bot, newSize, orderType, action.order, newSize);
|
|
2781
3019
|
if (!rotationSizeValidation.isValid) {
|
|
3020
|
+
// Record like every sibling skip site: the skip set
|
|
3021
|
+
// feeds both the working-grid restore and the
|
|
3022
|
+
// boundary hold (a skipped refill must not let the
|
|
3023
|
+
// committed boundary advance past its empty slot).
|
|
3024
|
+
skippedUpdateCount++;
|
|
3025
|
+
if (action.id)
|
|
3026
|
+
skippedUpdateSlotIds.add(action.id);
|
|
3027
|
+
if (action.newGridId)
|
|
3028
|
+
skippedUpdateSlotIds.add(action.newGridId);
|
|
2782
3029
|
bot.manager.logger.log(`Skipping rotation update ${action.id} -> ${action.newGridId}: ${rotationSizeValidation.reason}`, 'warn');
|
|
2783
3030
|
continue;
|
|
2784
3031
|
}
|
|
@@ -2928,16 +3175,9 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2928
3175
|
lastFillGuardStats.bypassed++;
|
|
2929
3176
|
}
|
|
2930
3177
|
else {
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
}
|
|
2935
|
-
catch { /* best-effort */ }
|
|
2936
|
-
const lastPrice = bot.manager?._lastFilledPrice;
|
|
2937
|
-
const lastType = bot.manager?._lastFilledType;
|
|
2938
|
-
const inc = resolveLastFillGuardIncrement(bot);
|
|
2939
|
-
const check = isLastFillGuardBlocked(newPrice, newSize, orderType, lastPrice, lastType, inc);
|
|
2940
|
-
lastFillGuardStats.checked++;
|
|
3178
|
+
const { check, refreshed } = runLastFillGuardCheck(bot, newPrice, newSize, orderType, lastFillGuardStats, true);
|
|
3179
|
+
if (refreshed)
|
|
3180
|
+
lastFillGuardPivotRefreshed = true;
|
|
2941
3181
|
if (check.blocked) {
|
|
2942
3182
|
lastFillGuardStats.skipped++;
|
|
2943
3183
|
skippedUpdateCount++;
|
|
@@ -2946,7 +3186,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2946
3186
|
if (action.newGridId)
|
|
2947
3187
|
skippedUpdateSlotIds.add(action.newGridId);
|
|
2948
3188
|
const dir = orderType === ORDER_TYPES.BUY ? 'above' : 'below';
|
|
2949
|
-
bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${orderType} UPDATE for ${action.id} -> ${action.newGridId} at ${Format.formatPrice6(newPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)})`, '
|
|
3189
|
+
bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${orderType} UPDATE for ${action.id} -> ${action.newGridId} at ${Format.formatPrice6(newPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)})`, 'debug');
|
|
2950
3190
|
continue;
|
|
2951
3191
|
}
|
|
2952
3192
|
lastFillGuardStats.passed++;
|
|
@@ -3062,24 +3302,19 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3062
3302
|
}
|
|
3063
3303
|
// LAST-FILL GUARD (fallback variant): this CREATE
|
|
3064
3304
|
// replaces a rotation UPDATE at a repriced level,
|
|
3065
|
-
// so it obeys the same guard
|
|
3066
|
-
// pivot — no origin bypass, same as rotations.
|
|
3305
|
+
// so it obeys the same guard against the frozen
|
|
3306
|
+
// batch pivot — no origin bypass, same as rotations.
|
|
3067
3307
|
try {
|
|
3068
|
-
|
|
3308
|
+
const { check: fbCheck, refreshed: fbRefreshed } = runLastFillGuardCheck(bot, fbPrice, fbSize, fbType, lastFillGuardStats, true);
|
|
3309
|
+
if (fbRefreshed)
|
|
3069
3310
|
lastFillGuardPivotRefreshed = true;
|
|
3070
|
-
}
|
|
3071
|
-
catch { /* best-effort */ }
|
|
3072
|
-
try {
|
|
3073
|
-
const fbInc = resolveLastFillGuardIncrement(bot);
|
|
3074
|
-
const fbCheck = isLastFillGuardBlocked(fbPrice, fbSize, fbType, bot.manager?._lastFilledPrice, bot.manager?._lastFilledType, fbInc);
|
|
3075
|
-
lastFillGuardStats.checked++;
|
|
3076
3311
|
if (fbCheck.blocked) {
|
|
3077
3312
|
lastFillGuardStats.skipped++;
|
|
3078
3313
|
const fbDir = fbType === ORDER_TYPES.BUY ? 'above' : 'below';
|
|
3079
3314
|
bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping CREATE fallback for ${targetSlotId} at ` +
|
|
3080
3315
|
`${Format.formatPrice6(fbPrice)}: ${fbDir} last filled ` +
|
|
3081
3316
|
`${Format.formatPrice6(fbCheck.pivot)} (halfInc ${fbCheck.halfInc}% thr ` +
|
|
3082
|
-
`${Format.formatPrice6(fbCheck.threshold)}); re-planned after market moves`, '
|
|
3317
|
+
`${Format.formatPrice6(fbCheck.threshold)}); re-planned after market moves`, 'debug');
|
|
3083
3318
|
continue;
|
|
3084
3319
|
}
|
|
3085
3320
|
lastFillGuardStats.passed++;
|
|
@@ -3121,10 +3356,10 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3121
3356
|
// are what incident reconstruction needs. Origin folds in here as
|
|
3122
3357
|
// mode=bypassed(<origin>); there is no second source of truth.
|
|
3123
3358
|
// Cold (guard off) is warn, not info — a disabled guard must say so.
|
|
3124
|
-
// pivotRefreshed surfaces whether
|
|
3125
|
-
//
|
|
3126
|
-
//
|
|
3127
|
-
//
|
|
3359
|
+
// pivotRefreshed surfaces whether the batch-start freeze picked up a
|
|
3360
|
+
// queued fill (the refresh itself stays debug). The pivot is frozen
|
|
3361
|
+
// for the whole batch, so every action was checked against the same
|
|
3362
|
+
// pivot printed here.
|
|
3128
3363
|
try {
|
|
3129
3364
|
const totalGuarded = lastFillGuardStats.checked + lastFillGuardStats.bypassed;
|
|
3130
3365
|
if (totalGuarded > 0) {
|
|
@@ -3160,6 +3395,47 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3160
3395
|
}
|
|
3161
3396
|
bot.manager.logger.log(`[COW] Re-synced ${clampedUpdateSlotIds.size} post-fill-clamped slot(s) from master before commit`, 'debug');
|
|
3162
3397
|
}
|
|
3398
|
+
// BOUNDARY HOLD: skipped refill slots strand empty rail holes past the
|
|
3399
|
+
// planned boundary (commit gate skips empties, self-legalizing) — hold
|
|
3400
|
+
// the committed boundary; intersect-only, grid still commits.
|
|
3401
|
+
const refillHold = resolveRefillBoundaryHold(workingBoundary, bot.manager.boundaryIdx, skippedUpdateSlotIds, clampedUpdateSlotIds, cowResult?.refillSlotIds, skippedCreateSlotIds);
|
|
3402
|
+
effectiveBoundary = refillHold.effectiveBoundary;
|
|
3403
|
+
boundaryHeld = refillHold.heldRefillSlotIds.length > 0;
|
|
3404
|
+
if (refillHold.heldRefillSlotIds.length > 0) {
|
|
3405
|
+
bot.manager.logger.log(`[COW] Boundary hold: ${refillHold.heldRefillSlotIds.length} refill slot(s) skipped ` +
|
|
3406
|
+
`(${refillHold.heldRefillSlotIds.join(', ')}) — keeping ${bot.manager.boundaryIdx} over planned ${workingBoundary}`, 'warn');
|
|
3407
|
+
}
|
|
3408
|
+
// Consecutive-hold tracking (INV-COW-007 visibility): escalate a
|
|
3409
|
+
// growing run — the grid is trailing the market and only fresh
|
|
3410
|
+
// fills unstick it. Cleared automatically on the first clean batch.
|
|
3411
|
+
const consecutiveHolds = trackBoundaryHold(bot.manager, refillHold.heldRefillSlotIds.length > 0, bot.manager.boundaryIdx, workingBoundary, refillHold.heldRefillSlotIds);
|
|
3412
|
+
if (consecutiveHolds >= 3) {
|
|
3413
|
+
bot.manager.logger.log(`[COW] Boundary held ${consecutiveHolds} consecutive batches ` +
|
|
3414
|
+
`(keeping ${bot.manager.boundaryIdx} over planned ${workingBoundary}). ` +
|
|
3415
|
+
`Grid is trailing the market — refills re-price once the guard pivot settles; ` +
|
|
3416
|
+
`heals on the next fill-driven plan. Investigate only if the run keeps growing without new fills.`, 'warn');
|
|
3417
|
+
}
|
|
3418
|
+
// Guard-aware re-center escalation (INV-COW-007 heal path): a run of
|
|
3419
|
+
// holds carrying fresh fills means the grid is trailing the market and
|
|
3420
|
+
// will not heal from fill-less replans (they re-derive the identical
|
|
3421
|
+
// veto). Request a structural resync that re-derives centers on the
|
|
3422
|
+
// live pivot; the cooldown prevents resync storms. requestStructuralGridResync
|
|
3423
|
+
// re-defers while this batch is still in flight, so it runs in a clean context.
|
|
3424
|
+
if (boundaryHeld && consecutiveHolds >= holdResyncThreshold) {
|
|
3425
|
+
const freshFills = Array.isArray(cowResult?.fills) && cowResult.fills.length > 0;
|
|
3426
|
+
const lastResyncAt = Number(bot.manager._lastBoundaryHoldResyncAt) || 0;
|
|
3427
|
+
if (freshFills && (Date.now() - lastResyncAt) >= holdResyncCooldownMs) {
|
|
3428
|
+
bot.manager._lastBoundaryHoldResyncAt = Date.now();
|
|
3429
|
+
bot.manager.logger.log(`[COW] Boundary held ${consecutiveHolds} consecutive batches with fresh fills; ` +
|
|
3430
|
+
`requesting guard-aware structural re-center (cooldown ${Math.round(holdResyncCooldownMs / 1000)}s)`, 'warn');
|
|
3431
|
+
try {
|
|
3432
|
+
void bot.manager.requestStructuralGridResync?.('boundary-hold-trailing-market', { reason: 'boundary-hold-trailing-market' });
|
|
3433
|
+
}
|
|
3434
|
+
catch (err) {
|
|
3435
|
+
bot.manager.logger.log(`[COW] Structural re-center request failed (non-fatal): ${getErrorMessage(err)}`, 'warn');
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3163
3439
|
if (operations.length === 0) {
|
|
3164
3440
|
// Pop the working grid: in the re-plan recursion the fresh plan's
|
|
3165
3441
|
// grid was pushed by performSafeRebalance, and nothing downstream
|
|
@@ -3255,14 +3531,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3255
3531
|
sell: item.ctx?.finalInts?.sell,
|
|
3256
3532
|
receive: item.ctx?.finalInts?.receive,
|
|
3257
3533
|
orderType: item.ctx?.order?.type,
|
|
3258
|
-
fingerprint:
|
|
3259
|
-
side: item.ctx?.order?.type,
|
|
3260
|
-
assetA: bot.manager?.assets?.assetA?.id,
|
|
3261
|
-
assetB: bot.manager?.assets?.assetB?.id,
|
|
3262
|
-
sellInt: item.ctx?.finalInts?.sell,
|
|
3263
|
-
receiveInt: item.ctx?.finalInts?.receive,
|
|
3264
|
-
slotId
|
|
3265
|
-
})
|
|
3534
|
+
fingerprint: createOpFingerprintForSlot(bot, item.ctx?.order, item.ctx?.finalInts, slotId)
|
|
3266
3535
|
});
|
|
3267
3536
|
if (match?.id) {
|
|
3268
3537
|
workingGrid.set(slotId, { ...slot, orderId: match.id });
|
|
@@ -3305,29 +3574,17 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3305
3574
|
// options.result, so a later throw in this frame (e.g.
|
|
3306
3575
|
// processBatchResults after a successful commit) cannot pop a
|
|
3307
3576
|
// second time for the same grid in the batch catch below.
|
|
3308
|
-
const commitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes,
|
|
3577
|
+
const commitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes, effectiveBoundary, { skipRecalc: true, result: cowResult, boundaryHeld });
|
|
3309
3578
|
if (!commitOk) {
|
|
3310
3579
|
// Master changed during broadcast (e.g. a fill landed and was
|
|
3311
3580
|
// processed concurrently) so the commit was refused. The batch
|
|
3312
3581
|
// is on chain; adopt the placed orders from the chain so master
|
|
3313
3582
|
// converges instead of remaining divergent until a later sync.
|
|
3314
|
-
bot
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
// cannot duplicate the placed orders, and let the structural
|
|
3320
|
-
// resync adopt them from the chain.
|
|
3321
|
-
bot.manager.logger.log('[COW] Commit refused and chain adoption unavailable; keeping pending-broadcast protection pending structural resync', 'error');
|
|
3322
|
-
await requestStructuralResync(bot, 'commit refused after broadcast (chain adoption unavailable)', { reason: 'chain-adoption-unavailable' });
|
|
3323
|
-
return { executed: false, hadRotation: false, commitRefused: true, chainAdoptionPending: true };
|
|
3324
|
-
}
|
|
3325
|
-
// Deduct create fees for the placed orders (mirrors
|
|
3326
|
-
// processBatchResults, which the refused path bypasses).
|
|
3327
|
-
await applyAdoptionFeeAccounting(bot, executedContexts);
|
|
3328
|
-
await restoreBoundaryAfterAdoption(bot, workingBoundary);
|
|
3329
|
-
await persistGridAndClearPendingBroadcasts(bot, '[COW]');
|
|
3330
|
-
return { executed: false, hadRotation: false, commitRefused: true };
|
|
3583
|
+
return await recoverRefusedCommit(bot, chainOrders, '[COW]', { placedResults: result, placedContexts: executedContexts }, executedContexts, effectiveBoundary, {
|
|
3584
|
+
failureResyncReason: 'commit refused after broadcast (chain adoption unavailable)',
|
|
3585
|
+
failureLogMessage: 'Commit refused and chain adoption unavailable; keeping pending-broadcast protection pending structural resync',
|
|
3586
|
+
preAdoptLogMessage: 'Commit refused after broadcast; adopting placed orders from chain to keep master in sync',
|
|
3587
|
+
});
|
|
3331
3588
|
}
|
|
3332
3589
|
const batchResult = await processBatchResults(bot, result, executedContexts);
|
|
3333
3590
|
const persistResult = await bot.manager.persistGrid();
|
|
@@ -3342,10 +3599,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3342
3599
|
bot.manager.logger.log(`[COW][PERSIST-GUARD] Retry also skipped/invalid ` +
|
|
3343
3600
|
`(${retryResult.reason || 'no reason'}). Master grid in memory ` +
|
|
3344
3601
|
`is ahead of disk snapshot; structural resync requested.`, 'error');
|
|
3345
|
-
|
|
3346
|
-
bot.manager._recoveryState = { ...bot.manager._recoveryState, structuralResyncRequested: true };
|
|
3347
|
-
await bot.manager.requestStructuralGridResync('persistence guard triggered after COW batch', { persistReason: retryResult.reason || 'unknown' });
|
|
3348
|
-
}
|
|
3602
|
+
await requestStructuralResync(bot, 'persistence guard triggered after COW batch', { persistReason: retryResult.reason || 'unknown' });
|
|
3349
3603
|
}
|
|
3350
3604
|
else {
|
|
3351
3605
|
delete bot.manager._persistenceWarning;
|
|
@@ -3410,22 +3664,17 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3410
3664
|
// _commitWorkingGrid pops on every settle path and clears
|
|
3411
3665
|
// the push marker via options.result, so a later throw here
|
|
3412
3666
|
// must not pop again in the batch catch below.
|
|
3413
|
-
const pollCommitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes,
|
|
3667
|
+
const pollCommitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes, effectiveBoundary, { skipRecalc: true, result: cowResult, boundaryHeld });
|
|
3414
3668
|
if (!pollCommitOk) {
|
|
3415
3669
|
// Master moved while polling — same recovery as the
|
|
3416
3670
|
// refused-commit path: adopt from chain, keep pending
|
|
3417
3671
|
// protection if adoption is unavailable.
|
|
3418
|
-
bot
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
}
|
|
3425
|
-
await applyAdoptionFeeAccounting(bot, opContexts);
|
|
3426
|
-
await restoreBoundaryAfterAdoption(bot, workingBoundary);
|
|
3427
|
-
await persistGridAndClearPendingBroadcasts(bot, '[COW][UNCERTAIN]');
|
|
3428
|
-
return { executed: false, hadRotation: false, commitRefused: true, uncertainResolved: true };
|
|
3672
|
+
return await recoverRefusedCommit(bot, chainOrders, '[COW][UNCERTAIN]', { placedContexts: opContexts, polledCreateIds: confirmation.confirmedChainIds }, opContexts, effectiveBoundary, {
|
|
3673
|
+
successReturn: { executed: false, hadRotation: false, commitRefused: true, uncertainResolved: true },
|
|
3674
|
+
failureResyncReason: 'poll-confirmed commit refused (chain adoption unavailable)',
|
|
3675
|
+
failureLogMessage: 'Poll-refused commit with unavailable chain adoption; keeping pending protection pending structural resync',
|
|
3676
|
+
preAdoptLogMessage: 'Poll-confirmed commit refused; adopting placed orders from chain',
|
|
3677
|
+
});
|
|
3429
3678
|
}
|
|
3430
3679
|
// Enrich master grid with chain-assigned order IDs and amounts;
|
|
3431
3680
|
// accounting enabled so the adopted orders' capital is locked
|
|
@@ -3436,19 +3685,16 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3436
3685
|
// on a failed adoption would let the next cycle re-create the
|
|
3437
3686
|
// VIRTUAL slots as duplicate on-chain orders. Keep the
|
|
3438
3687
|
// protection and defer to a structural resync instead.
|
|
3439
|
-
const pollAdoptedOk = await adoptPlacedBatchFromChain(bot, chainOrders, '[COW][UNCERTAIN]', { placedContexts: opContexts, polledCreateIds: confirmation.confirmedChainIds });
|
|
3440
|
-
if (!pollAdoptedOk) {
|
|
3441
|
-
bot.manager.logger.log('[COW][UNCERTAIN] Poll-confirmed commit with unavailable chain adoption; keeping pending protection pending structural resync', 'error');
|
|
3442
|
-
await requestStructuralResync(bot, 'poll-confirmed commit (chain adoption unavailable)', { reason: 'chain-adoption-unavailable' });
|
|
3443
|
-
return { executed: false, hadRotation: false, commitRefused: false, chainAdoptionPending: true };
|
|
3444
|
-
}
|
|
3445
3688
|
// The commit happened without processBatchResults (no success
|
|
3446
3689
|
// result to extract); deduct create fees so the optimistic
|
|
3447
|
-
// balance reflects the on-chain cost
|
|
3448
|
-
|
|
3449
|
-
await
|
|
3450
|
-
|
|
3451
|
-
|
|
3690
|
+
// balance reflects the on-chain cost (shared with the
|
|
3691
|
+
// refused-commit paths via recoverRefusedCommit).
|
|
3692
|
+
return await recoverRefusedCommit(bot, chainOrders, '[COW][UNCERTAIN]', { placedContexts: opContexts, polledCreateIds: confirmation.confirmedChainIds }, opContexts, effectiveBoundary, {
|
|
3693
|
+
extraReturn: { commitRefused: false },
|
|
3694
|
+
successReturn: { executed: true, hadRotation: false, uncertainResolved: true },
|
|
3695
|
+
failureResyncReason: 'poll-confirmed commit (chain adoption unavailable)',
|
|
3696
|
+
failureLogMessage: 'Poll-confirmed commit with unavailable chain adoption; keeping pending protection pending structural resync',
|
|
3697
|
+
});
|
|
3452
3698
|
}
|
|
3453
3699
|
}
|
|
3454
3700
|
catch (pollErr) {
|
|
@@ -3502,8 +3748,10 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3502
3748
|
* @param {Object} [details={}] - Details passed to the resync handler
|
|
3503
3749
|
*/
|
|
3504
3750
|
async function requestStructuralResync(bot, reason, details = {}) {
|
|
3505
|
-
if (typeof bot.manager?.requestStructuralGridResync !== 'function')
|
|
3751
|
+
if (typeof bot.manager?.requestStructuralGridResync !== 'function') {
|
|
3752
|
+
bot._warn?.(`[COW] requestStructuralGridResync unavailable; cannot schedule structural resync (reason: ${reason}).`);
|
|
3506
3753
|
return;
|
|
3754
|
+
}
|
|
3507
3755
|
if (bot.manager._recoveryState) {
|
|
3508
3756
|
bot.manager._recoveryState = { ...bot.manager._recoveryState, structuralResyncRequested: true };
|
|
3509
3757
|
}
|
|
@@ -3523,91 +3771,6 @@ async function requestStructuralResync(bot, reason, details = {}) {
|
|
|
3523
3771
|
* @param {string} logPrefix - Log prefix for sync failure messages
|
|
3524
3772
|
* @returns {Promise<boolean>}
|
|
3525
3773
|
*/
|
|
3526
|
-
/**
|
|
3527
|
-
* Collect every on-chain order id master currently needs to converge against,
|
|
3528
|
-
* so adoption can re-read them by id (immune to the get_full_accounts window
|
|
3529
|
-
* truncation) instead of relying on a partial window read.
|
|
3530
|
-
*
|
|
3531
|
-
* Sources:
|
|
3532
|
-
* - master's own tracked order ids (existing on-chain orders);
|
|
3533
|
-
* - the batch's fresh CREATE ids extracted from the broadcast result
|
|
3534
|
-
* (operation_results[i][1] aligns positionally with placedContexts[i]).
|
|
3535
|
-
*
|
|
3536
|
-
* @param {any} mgr - bot.manager
|
|
3537
|
-
* @param {any} placedResults - broadcast result (has operation_results); null when unavailable
|
|
3538
|
-
* @param {any[]} placedContexts - opContexts (aligned with operation_results); null when unavailable
|
|
3539
|
-
* @param {string[]|null} [extraCreateIds=null] - fresh CREATE chain ids from another
|
|
3540
|
-
* authoritative source (e.g. the uncertain-broadcast poll confirmation) when
|
|
3541
|
-
* no broadcast result exists; merged into createIds so the lagging-create
|
|
3542
|
-
* retry guards them
|
|
3543
|
-
* @returns {string[]} Unique, well-formed 1.7.x order ids
|
|
3544
|
-
*/
|
|
3545
|
-
function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts, extraCreateIds = null) {
|
|
3546
|
-
const masterIds = new Set();
|
|
3547
|
-
const grid = mgr && mgr.grid;
|
|
3548
|
-
if (Array.isArray(grid)) {
|
|
3549
|
-
for (const slot of grid) {
|
|
3550
|
-
if (slot && slot.orderId && /^1\.7\.\d+$/.test(String(slot.orderId))) {
|
|
3551
|
-
masterIds.add(String(slot.orderId));
|
|
3552
|
-
}
|
|
3553
|
-
}
|
|
3554
|
-
}
|
|
3555
|
-
// Master tracked ids live in the orders Map (mgr.grid is legacy and
|
|
3556
|
-
// unset on OrderManager — without this the by-id set omits every
|
|
3557
|
-
// pre-existing ACTIVE order and pass-1 phantom cleanup would virtualize
|
|
3558
|
-
// them as fills on a partial snapshot).
|
|
3559
|
-
if (mgr && mgr.orders instanceof Map) {
|
|
3560
|
-
for (const slot of mgr.orders.values()) {
|
|
3561
|
-
if (slot && slot.orderId && /^1\.7\.\d+$/.test(String(slot.orderId))) {
|
|
3562
|
-
masterIds.add(String(slot.orderId));
|
|
3563
|
-
}
|
|
3564
|
-
}
|
|
3565
|
-
}
|
|
3566
|
-
const createIds = new Set();
|
|
3567
|
-
if (placedResults && Array.isArray(placedContexts)) {
|
|
3568
|
-
const opResults = extractBatchOperationResults(placedResults);
|
|
3569
|
-
if (Array.isArray(opResults)) {
|
|
3570
|
-
for (let i = 0; i < placedContexts.length; i++) {
|
|
3571
|
-
const ctx = placedContexts[i];
|
|
3572
|
-
if (!ctx || ctx.kind !== 'create')
|
|
3573
|
-
continue;
|
|
3574
|
-
const opResult = opResults[i] && opResults[i][1];
|
|
3575
|
-
if (opResult && /^1\.7\.\d+$/.test(String(opResult))) {
|
|
3576
|
-
createIds.add(String(opResult));
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
}
|
|
3581
|
-
if (Array.isArray(extraCreateIds)) {
|
|
3582
|
-
for (const id of extraCreateIds) {
|
|
3583
|
-
if (id && /^1\.7\.\d+$/.test(String(id)))
|
|
3584
|
-
createIds.add(String(id));
|
|
3585
|
-
}
|
|
3586
|
-
}
|
|
3587
|
-
// Existing chain ids referenced by non-create op contexts (cancel /
|
|
3588
|
-
// rotation / size-update). Pre-existing orders whose absence is expected
|
|
3589
|
-
// (cancels/fills in this batch), so they join the by-id set but never
|
|
3590
|
-
// the lagging-create guard.
|
|
3591
|
-
if (Array.isArray(placedContexts)) {
|
|
3592
|
-
for (const ctx of placedContexts) {
|
|
3593
|
-
if (!ctx || ctx.kind === 'create')
|
|
3594
|
-
continue;
|
|
3595
|
-
const refs = [];
|
|
3596
|
-
if (ctx.kind === 'cancel' && ctx.order)
|
|
3597
|
-
refs.push(ctx.order.orderId);
|
|
3598
|
-
else if (ctx.kind === 'rotation' && ctx.rotation?.oldOrder)
|
|
3599
|
-
refs.push(ctx.rotation.oldOrder.orderId);
|
|
3600
|
-
else if (ctx.kind === 'size-update' && ctx.updateInfo?.partialOrder)
|
|
3601
|
-
refs.push(ctx.updateInfo.partialOrder.orderId);
|
|
3602
|
-
for (const id of refs) {
|
|
3603
|
-
if (id && /^1\.7\.\d+$/.test(String(id)))
|
|
3604
|
-
masterIds.add(String(id));
|
|
3605
|
-
}
|
|
3606
|
-
}
|
|
3607
|
-
}
|
|
3608
|
-
const all = new Set([...masterIds, ...createIds]);
|
|
3609
|
-
return { masterIds: [...masterIds], createIds: [...createIds], all: [...all] };
|
|
3610
|
-
}
|
|
3611
3774
|
/**
|
|
3612
3775
|
* Converge master with the chain after a COW commit was refused (master moved
|
|
3613
3776
|
* during broadcast) or an uncertain broadcast was poll-confirmed.
|
|
@@ -3760,8 +3923,8 @@ async function adoptPlacedBatchFromChain(bot, chainOrders, logPrefix, opts = {})
|
|
|
3760
3923
|
}
|
|
3761
3924
|
// FALLBACK: window read (ambiguous when truncated).
|
|
3762
3925
|
const freshRead = await readOpenOrdersWithMetaSafe(chainOrders, accountRef);
|
|
3763
|
-
if (freshRead
|
|
3764
|
-
bot.manager.logger.log(`${logPrefix} Chain read TRUNCATED after batch broadcast; adoption deferred (pending-broadcast protection kept)`, 'warn');
|
|
3926
|
+
if (!isAuthoritativeChainRead(freshRead)) {
|
|
3927
|
+
bot.manager.logger.log(`${logPrefix} Chain read ${freshRead?.truncated ? 'TRUNCATED' : 'EMPTY'} after batch broadcast; adoption deferred (pending-broadcast protection kept)`, 'warn');
|
|
3765
3928
|
return false;
|
|
3766
3929
|
}
|
|
3767
3930
|
const freshChain = freshRead.orders;
|
|
@@ -3791,6 +3954,83 @@ async function persistGridAndClearPendingBroadcasts(bot, logPrefix) {
|
|
|
3791
3954
|
}
|
|
3792
3955
|
clearPendingBroadcasts(bot.manager?._pendingBroadcasts);
|
|
3793
3956
|
}
|
|
3957
|
+
/**
|
|
3958
|
+
* Recover from a refused commit after a COW batch broadcast.
|
|
3959
|
+
* Adopts placed orders from the chain, applies fee accounting, restores
|
|
3960
|
+
* the boundary, and persists. All three commit-refused paths
|
|
3961
|
+
* (success-path refused, poll-refused, and poll-confirmed uncertain)
|
|
3962
|
+
* share this exact sequence.
|
|
3963
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
3964
|
+
* @param {Object} chainOrders - Chain orders module
|
|
3965
|
+
* @param {string} logPrefix - Log prefix for messages
|
|
3966
|
+
* @param {Object} adoptOpts - Options passed to adoptPlacedBatchFromChain
|
|
3967
|
+
* @param {Array} contexts - Op contexts for fee accounting
|
|
3968
|
+
* @param {Object} workingBoundary - The working boundary
|
|
3969
|
+
* @param {Object} [opts={}] - Named options (object form: adjacent string
|
|
3970
|
+
* options were positional before, a transposition would compile silently).
|
|
3971
|
+
* @param {Object} [opts.extraReturn={}] - Extra fields merged into the failure return object
|
|
3972
|
+
* @param {Object|null} [opts.successReturn=null] - Return object on successful adoption
|
|
3973
|
+
* (defaults to the commit-refused shape; the poll-confirmed path passes its
|
|
3974
|
+
* executed:true shape)
|
|
3975
|
+
* @param {string} [opts.failureResyncReason='commit refused after broadcast (chain adoption unavailable)']
|
|
3976
|
+
* @param {string} [opts.failureLogMessage='Commit refused and chain adoption unavailable; keeping pending-broadcast protection pending structural resync']
|
|
3977
|
+
* - Failure-path log body (prefixed with logPrefix); each call site passes
|
|
3978
|
+
* its original wording so log greps keep matching.
|
|
3979
|
+
* @param {string|null} [opts.preAdoptLogMessage=null] - Optional warn logged before
|
|
3980
|
+
* the adoption attempt (the success-path and poll-refused sites log one;
|
|
3981
|
+
* the poll-confirmed site never did)
|
|
3982
|
+
* @returns {Promise<Object>} The commit-refused return object
|
|
3983
|
+
*/
|
|
3984
|
+
async function recoverRefusedCommit(bot, chainOrders, logPrefix, adoptOpts, contexts, workingBoundary, opts = {}) {
|
|
3985
|
+
const { extraReturn = {}, successReturn = null, failureResyncReason = 'commit refused after broadcast (chain adoption unavailable)', failureLogMessage = 'Commit refused and chain adoption unavailable; keeping pending-broadcast protection pending structural resync', preAdoptLogMessage = null, } = opts;
|
|
3986
|
+
if (preAdoptLogMessage) {
|
|
3987
|
+
bot.manager.logger.log(`${logPrefix} ${preAdoptLogMessage}`, 'warn');
|
|
3988
|
+
}
|
|
3989
|
+
const adopted = await adoptPlacedBatchFromChain(bot, chainOrders, logPrefix, adoptOpts);
|
|
3990
|
+
if (!adopted) {
|
|
3991
|
+
bot.manager.logger.log(`${logPrefix} ${failureLogMessage}`, 'error');
|
|
3992
|
+
await requestStructuralResync(bot, failureResyncReason, { reason: 'chain-adoption-unavailable' });
|
|
3993
|
+
return { executed: false, hadRotation: false, commitRefused: true, chainAdoptionPending: true, ...extraReturn };
|
|
3994
|
+
}
|
|
3995
|
+
await applyAdoptionFeeAccounting(bot, contexts);
|
|
3996
|
+
await restoreBoundaryAfterAdoption(bot, workingBoundary);
|
|
3997
|
+
await persistGridAndClearPendingBroadcasts(bot, logPrefix);
|
|
3998
|
+
return successReturn ?? { executed: false, hadRotation: false, commitRefused: true, ...extraReturn };
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Run the last-fill guard probe: optionally refresh the pivot from
|
|
4002
|
+
* still-queued fills, read the durable pivot, and evaluate
|
|
4003
|
+
* isLastFillGuardBlocked. Consolidates the identical probe pattern in the
|
|
4004
|
+
* CREATE, UPDATE-rotation, and CREATE-fallback guards (origin bypasses and
|
|
4005
|
+
* skip logging stay at the call sites, which differ per action kind).
|
|
4006
|
+
* Batch callers pass skipRefresh=true: the batch-start freeze owns refreshes
|
|
4007
|
+
* so every action in a batch is judged against the same pivot.
|
|
4008
|
+
* @param {Object} bot
|
|
4009
|
+
* @param {number} price - Target order price
|
|
4010
|
+
* @param {number} size - Order size
|
|
4011
|
+
* @param {string} type - ORDER_TYPES.BUY/SELL
|
|
4012
|
+
* @param {Object} stats - lastFillGuardStats tracker (checked++ here)
|
|
4013
|
+
* @param {boolean} [skipRefresh=false] - Skip the queued-fill pivot refresh
|
|
4014
|
+
* @returns {{check: Object, refreshed: boolean}}
|
|
4015
|
+
*/
|
|
4016
|
+
function runLastFillGuardCheck(bot, price, size, type, stats, skipRefresh = false) {
|
|
4017
|
+
let refreshed = false;
|
|
4018
|
+
// The batch-start freeze (see broadcast loop head) owns pivot refreshes;
|
|
4019
|
+
// per-action refreshes are disabled so every action in a batch is judged
|
|
4020
|
+
// against the same pivot. Kept opt-in for non-batch callers.
|
|
4021
|
+
if (!skipRefresh) {
|
|
4022
|
+
try {
|
|
4023
|
+
refreshed = !!refreshLastFillPivotFromQueue(bot);
|
|
4024
|
+
}
|
|
4025
|
+
catch { /* best-effort */ }
|
|
4026
|
+
}
|
|
4027
|
+
const lastPrice = bot.manager?._lastFilledPrice;
|
|
4028
|
+
const lastType = bot.manager?._lastFilledType;
|
|
4029
|
+
const inc = resolveLastFillGuardIncrement(bot);
|
|
4030
|
+
const check = isLastFillGuardBlocked(price, size, type, lastPrice, lastType, inc);
|
|
4031
|
+
stats.checked++;
|
|
4032
|
+
return { check, refreshed };
|
|
4033
|
+
}
|
|
3794
4034
|
/**
|
|
3795
4035
|
* Apply BTS create-fee accounting for a batch that bypassed the normal
|
|
3796
4036
|
* processBatchResults pipeline (commit refused after broadcast, or
|
|
@@ -3827,6 +4067,7 @@ async function applyAdoptionFeeAccounting(bot, contexts) {
|
|
|
3827
4067
|
isPartialPlacement: false,
|
|
3828
4068
|
expectedType: ctx.order.type,
|
|
3829
4069
|
fee: btsFeeData?.createFee || 0,
|
|
4070
|
+
order: ctx.order ?? null,
|
|
3830
4071
|
}, 'createOrder');
|
|
3831
4072
|
}
|
|
3832
4073
|
catch (feeErr) {
|
|
@@ -3866,6 +4107,22 @@ async function applyAdoptionFeeAccounting(bot, contexts) {
|
|
|
3866
4107
|
}
|
|
3867
4108
|
}
|
|
3868
4109
|
}
|
|
4110
|
+
/**
|
|
4111
|
+
* Table-driven optimistic fee call for processBatchResults: the cancel,
|
|
4112
|
+
* size-update, and both rotation branches all invoke
|
|
4113
|
+
* updateOptimisticFreeBalance(oldOrder, newOrder, context, fee, false)
|
|
4114
|
+
* under the same accountant guard — one spelling instead of four.
|
|
4115
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
4116
|
+
* @param {Object} oldOrder
|
|
4117
|
+
* @param {Object} newOrder
|
|
4118
|
+
* @param {string} context - Fee context ('fill-cancel' | 'order-update')
|
|
4119
|
+
* @param {number} fee - BTS fee amount
|
|
4120
|
+
*/
|
|
4121
|
+
async function applyOptimisticFeeBalance(bot, oldOrder, newOrder, context, fee) {
|
|
4122
|
+
if (oldOrder && newOrder && bot.manager.accountant) {
|
|
4123
|
+
await bot.manager.accountant.updateOptimisticFreeBalance(oldOrder, newOrder, context, fee || 0, false);
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
3869
4126
|
/**
|
|
3870
4127
|
* Process results from batch transaction execution.
|
|
3871
4128
|
* Updates order state, synchronizes with chain, and deducts BTS fees.
|
|
@@ -3893,27 +4150,16 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
3893
4150
|
bot.manager.logger.log(`Cancelled surplus order ${ctx.order.id} (${ctx.order.orderId})`, 'info');
|
|
3894
4151
|
const oldOrder = ctx.order;
|
|
3895
4152
|
const committedOrder = oldOrder?.id ? bot.manager.orders.get(oldOrder.id) : null;
|
|
3896
|
-
|
|
3897
|
-
await bot.manager.accountant.updateOptimisticFreeBalance(oldOrder, committedOrder, 'fill-cancel', btsFeeData?.cancelFee || 0, false);
|
|
3898
|
-
}
|
|
4153
|
+
await applyOptimisticFeeBalance(bot, oldOrder, committedOrder, 'fill-cancel', btsFeeData?.cancelFee || 0);
|
|
3899
4154
|
}
|
|
3900
4155
|
else if (ctx.kind === 'size-update') {
|
|
3901
4156
|
const oldOrder = ctx.updateInfo.partialOrder;
|
|
3902
4157
|
const ord = bot.manager.orders.get(oldOrder.id);
|
|
3903
|
-
|
|
3904
|
-
await bot.manager.accountant.updateOptimisticFreeBalance(oldOrder, ord, 'order-update', btsFeeData?.updateFee || 0, false);
|
|
3905
|
-
}
|
|
4158
|
+
await applyOptimisticFeeBalance(bot, oldOrder, ord, 'order-update', btsFeeData?.updateFee || 0);
|
|
3906
4159
|
if (ord) {
|
|
3907
4160
|
const updatedSlot = { ...ord, size: ctx.updateInfo.newSize };
|
|
3908
4161
|
if (ctx.finalInts) {
|
|
3909
|
-
updatedSlot.rawOnChain =
|
|
3910
|
-
id: ord.orderId,
|
|
3911
|
-
for_sale: String(ctx.finalInts.sell),
|
|
3912
|
-
sell_price: {
|
|
3913
|
-
base: { amount: String(ctx.finalInts.sell), asset_id: ctx.finalInts.sellAssetId },
|
|
3914
|
-
quote: { amount: String(ctx.finalInts.receive), asset_id: ctx.finalInts.receiveAssetId }
|
|
3915
|
-
}
|
|
3916
|
-
};
|
|
4162
|
+
updatedSlot.rawOnChain = rawOnChainFromInts(ord.orderId, ctx.finalInts);
|
|
3917
4163
|
}
|
|
3918
4164
|
updatesToApply.push({ order: updatedSlot, context: 'post-update-metadata' });
|
|
3919
4165
|
}
|
|
@@ -3924,7 +4170,8 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
3924
4170
|
const chainOrderId = res && res[1];
|
|
3925
4171
|
if (chainOrderId) {
|
|
3926
4172
|
await bot.manager.synchronizeWithChain({
|
|
3927
|
-
gridOrderId: ctx.order.id, chainOrderId, expectedType: ctx.order.type, fee: btsFeeData?.createFee || 0
|
|
4173
|
+
gridOrderId: ctx.order.id, chainOrderId, expectedType: ctx.order.type, fee: btsFeeData?.createFee || 0,
|
|
4174
|
+
order: ctx.order ?? null,
|
|
3928
4175
|
}, 'createOrder');
|
|
3929
4176
|
if (ctx.finalInts) {
|
|
3930
4177
|
const syncedOrder = bot.manager.orders.get(ctx.order.id);
|
|
@@ -3932,14 +4179,7 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
3932
4179
|
updatesToApply.push({
|
|
3933
4180
|
order: {
|
|
3934
4181
|
...syncedOrder,
|
|
3935
|
-
rawOnChain:
|
|
3936
|
-
id: chainOrderId,
|
|
3937
|
-
for_sale: String(ctx.finalInts.sell),
|
|
3938
|
-
sell_price: {
|
|
3939
|
-
base: { amount: String(ctx.finalInts.sell), asset_id: ctx.finalInts.sellAssetId },
|
|
3940
|
-
quote: { amount: String(ctx.finalInts.receive), asset_id: ctx.finalInts.receiveAssetId }
|
|
3941
|
-
}
|
|
3942
|
-
}
|
|
4182
|
+
rawOnChain: rawOnChainFromInts(chainOrderId, ctx.finalInts)
|
|
3943
4183
|
},
|
|
3944
4184
|
context: 'post-placement-metadata'
|
|
3945
4185
|
});
|
|
@@ -3969,20 +4209,11 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
3969
4209
|
const { oldOrder, newPrice, newGridId, newSize, type } = rotation;
|
|
3970
4210
|
if (!newGridId) {
|
|
3971
4211
|
const ord = bot.manager.orders.get(oldOrder.id || rotation.id);
|
|
3972
|
-
|
|
3973
|
-
await bot.manager.accountant.updateOptimisticFreeBalance(oldOrder, ord, 'order-update', btsFeeData?.updateFee || 0, false);
|
|
3974
|
-
}
|
|
4212
|
+
await applyOptimisticFeeBalance(bot, oldOrder, ord, 'order-update', btsFeeData?.updateFee || 0);
|
|
3975
4213
|
if (ord) {
|
|
3976
4214
|
const updatedSlot = { ...ord, size: newSize };
|
|
3977
4215
|
if (ctx.finalInts) {
|
|
3978
|
-
updatedSlot.rawOnChain =
|
|
3979
|
-
id: ord.orderId,
|
|
3980
|
-
for_sale: String(ctx.finalInts.sell),
|
|
3981
|
-
sell_price: {
|
|
3982
|
-
base: { amount: String(ctx.finalInts.sell), asset_id: ctx.finalInts.sellAssetId },
|
|
3983
|
-
quote: { amount: String(ctx.finalInts.receive), asset_id: ctx.finalInts.receiveAssetId }
|
|
3984
|
-
}
|
|
3985
|
-
};
|
|
4216
|
+
updatedSlot.rawOnChain = rawOnChainFromInts(ord.orderId, ctx.finalInts);
|
|
3986
4217
|
}
|
|
3987
4218
|
updatesToApply.push({ order: updatedSlot, context: 'post-update-metadata' });
|
|
3988
4219
|
}
|
|
@@ -4013,18 +4244,9 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
4013
4244
|
orderId: oldOrder?.orderId || slot.orderId || null
|
|
4014
4245
|
};
|
|
4015
4246
|
if (ctx.finalInts) {
|
|
4016
|
-
updatedSlot.rawOnChain =
|
|
4017
|
-
id: updatedSlot.orderId,
|
|
4018
|
-
for_sale: String(ctx.finalInts.sell),
|
|
4019
|
-
sell_price: {
|
|
4020
|
-
base: { amount: String(ctx.finalInts.sell), asset_id: ctx.finalInts.sellAssetId },
|
|
4021
|
-
quote: { amount: String(ctx.finalInts.receive), asset_id: ctx.finalInts.receiveAssetId }
|
|
4022
|
-
}
|
|
4023
|
-
};
|
|
4024
|
-
}
|
|
4025
|
-
if (oldOrder && updatedSlot && bot.manager.accountant) {
|
|
4026
|
-
await bot.manager.accountant.updateOptimisticFreeBalance(oldOrder, updatedSlot, 'order-update', btsFeeData?.updateFee || 0, false);
|
|
4247
|
+
updatedSlot.rawOnChain = rawOnChainFromInts(updatedSlot.orderId, ctx.finalInts);
|
|
4027
4248
|
}
|
|
4249
|
+
await applyOptimisticFeeBalance(bot, oldOrder, updatedSlot, 'order-update', btsFeeData?.updateFee || 0);
|
|
4028
4250
|
if (oldOrder?.id && oldOrder.id !== newGridId) {
|
|
4029
4251
|
const currentSource = bot.manager.orders.get(oldOrder.id);
|
|
4030
4252
|
if (currentSource && currentSource.orderId) {
|
|
@@ -4051,7 +4273,10 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
4051
4273
|
updateOperationCount
|
|
4052
4274
|
};
|
|
4053
4275
|
}
|
|
4054
|
-
export { isLastFillGuardBlocked, refreshLastFillPivotFromQueue, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries, extractOperationResults, findMissingCreateResultContexts, markMissingCreateResultsAsStructuralBlocker, formatUnmatchedChainOrderForLog, recordPendingBroadcast, clearPendingBroadcasts, clearPendingBroadcastsForSlots, popPushedWorkingGrid, buildChainOrderFingerprint, normalizeChainOrderForPendingMatch, findChainOrderForSlot, reconcileAfterUncertainBroadcast, reconcileAfterUncertainBroadcastImpl, autoCancelOneUnmatchedOrphan, shouldExecuteCreatePairMode, executeWithRetryOnUncertain, executeChunkedWithRetryOnUncertain, formatPartialBroadcastSummary, executeOperationsWithStrategy, validateOperationFunds, resolveIdealSizeForValidation, validateOrderSizeForExecution, buildActionsFromPlan, buildCowResultFromPlan, restoreSkippedUpdateSlotsInWorkingGrid, applyRotationTransitionsToWorkingGrid, pollChainForConfirmation, updateOrdersOnChainBatchCOW, processBatchResults, adoptPlacedBatchFromChain };
|
|
4276
|
+
export { isLastFillGuardBlocked, refreshLastFillPivotFromQueue, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries, extractOperationResults, findMissingCreateResultContexts, markMissingCreateResultsAsStructuralBlocker, formatUnmatchedChainOrderForLog, recordPendingBroadcast, clearPendingBroadcasts, clearPendingBroadcastsForSlots, popPushedWorkingGrid, buildChainOrderFingerprint, normalizeChainOrderForPendingMatch, findChainOrderForSlot, reconcileAfterUncertainBroadcast, reconcileAfterUncertainBroadcastImpl, autoCancelOneUnmatchedOrphan, shouldExecuteCreatePairMode, executeWithRetryOnUncertain, executeChunkedWithRetryOnUncertain, formatPartialBroadcastSummary, executeOperationsWithStrategy, validateOperationFunds, resolveIdealSizeForValidation, validateOrderSizeForExecution, buildActionsFromPlan, buildCowResultFromPlan, restoreSkippedUpdateSlotsInWorkingGrid, applyRotationTransitionsToWorkingGrid, pollChainForConfirmation, updateOrdersOnChainBatchCOW, processBatchResults, adoptPlacedBatchFromChain, resolveRefillBoundaryHold, toRefillSlotIdSet, trackBoundaryHold };
|
|
4277
|
+
// Exported for regression tests (issue #23 sibling): the uncertain-broadcast
|
|
4278
|
+
// discard path must never drop a placement silently when master lost the slot.
|
|
4279
|
+
export { restoreDiscardedCreates };
|
|
4055
4280
|
export default {
|
|
4056
4281
|
buildOutsideInPairGroupsForOrders,
|
|
4057
4282
|
buildOutsideInPairGroupsForCreateEntries,
|
|
@@ -4080,6 +4305,9 @@ export default {
|
|
|
4080
4305
|
buildActionsFromPlan,
|
|
4081
4306
|
buildCowResultFromPlan,
|
|
4082
4307
|
restoreSkippedUpdateSlotsInWorkingGrid,
|
|
4308
|
+
resolveRefillBoundaryHold,
|
|
4309
|
+
toRefillSlotIdSet,
|
|
4310
|
+
trackBoundaryHold,
|
|
4083
4311
|
applyRotationTransitionsToWorkingGrid,
|
|
4084
4312
|
pollChainForConfirmation,
|
|
4085
4313
|
updateOrdersOnChainBatchCOW,
|