dexbot 1.5.2 → 1.5.3
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 +17 -0
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/resolve_source.ts +3 -0
- 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 +5 -1
- package/analysis/tradingview/analyze_tradingview.ts +27 -1
- 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 +351 -22
- package/analysis/trend_detection/package.json +1 -1
- 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/tests/test_claw_mcp_transport.ts +2 -2
- package/dist/analysis/resolve_source.d.ts.map +1 -1
- package/dist/analysis/resolve_source.js +3 -0
- package/dist/analysis/resolve_source.js.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +3 -0
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +27 -2
- package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +348 -22
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.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/modules/dexbot_class.d.ts +2 -18
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +37 -37
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +655 -556
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +31 -51
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +28 -37
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/manager.d.ts +2 -1
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +10 -1
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +74 -0
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +9 -11
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +9 -11
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +51 -16
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +203 -29
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +1 -30
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +31 -98
- 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 +5 -1
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +6 -5
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
- package/docs/README.md +2 -2
- package/package.json +1 -1
|
@@ -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,
|
|
118
121
|
});
|
|
119
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,
|
|
189
|
+
});
|
|
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
|
}
|
|
@@ -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,55 @@ 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
|
+
}
|
|
2004
2203
|
/**
|
|
2005
2204
|
* Restore skipped update slots in the working grid to master state.
|
|
2006
2205
|
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
@@ -2182,87 +2381,6 @@ async function waitForCowBroadcastSingleFlight(bot, label) {
|
|
|
2182
2381
|
* @param {Object} [options={}] - Internal execution options (replanDepth)
|
|
2183
2382
|
* @returns {Promise<Object>}
|
|
2184
2383
|
*/
|
|
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
2384
|
/**
|
|
2267
2385
|
* Derive an update action's planned target size (shared by the rotation and
|
|
2268
2386
|
* plain size-update op builders).
|
|
@@ -2305,58 +2423,22 @@ function clampPostFillUpdateSize(bot, masterOrder, newSize, action) {
|
|
|
2305
2423
|
}
|
|
2306
2424
|
return target;
|
|
2307
2425
|
}
|
|
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
|
-
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Pre-broadcast guard chain for a COW batch: create-slot validation (with
|
|
2428
|
+
* tolerance-violation filtering), recovery-exhausted block, pending-broadcast
|
|
2429
|
+
* and unmatched-chain-order guards (with adoption sync), and the crossed-book
|
|
2430
|
+
* gate. Every refusal pops the pushed working grid before returning.
|
|
2431
|
+
*
|
|
2432
|
+
* Mutates cowResult.actions in place (tolerance-violating CREATEs are
|
|
2433
|
+
* filtered); the caller holds the same array reference.
|
|
2434
|
+
*
|
|
2435
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
2436
|
+
* @param {Object} cowResult - Rebalance/COW result carrying actions
|
|
2437
|
+
* @returns {Promise<Object>} { proceed:false, result } on refusal, or
|
|
2438
|
+
* { proceed:true, crossingCandidates, intraBatchCandidates } on pass
|
|
2439
|
+
*/
|
|
2440
|
+
async function runPreBroadcastGuards(bot, cowResult) {
|
|
2441
|
+
const { actions } = cowResult;
|
|
2360
2442
|
const chainOrderCandidates = Array.isArray(bot.manager?._lastUnmatchedChainOrders)
|
|
2361
2443
|
? bot.manager._lastUnmatchedChainOrders
|
|
2362
2444
|
: [];
|
|
@@ -2398,11 +2480,14 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2398
2480
|
if (hasHardOccupiedViolation) {
|
|
2399
2481
|
popPushedWorkingGrid(bot, cowResult);
|
|
2400
2482
|
return {
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2483
|
+
proceed: false,
|
|
2484
|
+
result: {
|
|
2485
|
+
executed: false,
|
|
2486
|
+
aborted: true,
|
|
2487
|
+
reason: 'CREATE_SLOT_OCCUPIED',
|
|
2488
|
+
violations: createSlotValidation.violations,
|
|
2489
|
+
hadRotation: false
|
|
2490
|
+
}
|
|
2406
2491
|
};
|
|
2407
2492
|
}
|
|
2408
2493
|
const violatingIds = createSlotValidation.violatingTargetIds;
|
|
@@ -2424,7 +2509,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2424
2509
|
// returns: a pushed working grid must be popped here or the
|
|
2425
2510
|
// caller would leak the stack entry.
|
|
2426
2511
|
popPushedWorkingGrid(bot, cowResult);
|
|
2427
|
-
return { executed: false, hadRotation: false };
|
|
2512
|
+
return { proceed: false, result: { executed: false, hadRotation: false } };
|
|
2428
2513
|
}
|
|
2429
2514
|
}
|
|
2430
2515
|
}
|
|
@@ -2436,27 +2521,24 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2436
2521
|
`Waiting for next fill or sync cycle to reset recovery state.`, 'warn');
|
|
2437
2522
|
popPushedWorkingGrid(bot, cowResult);
|
|
2438
2523
|
return {
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2524
|
+
proceed: false,
|
|
2525
|
+
result: {
|
|
2526
|
+
executed: false,
|
|
2527
|
+
aborted: true,
|
|
2528
|
+
reason: 'RECOVERY_EXHAUSTED',
|
|
2529
|
+
hadRotation: false
|
|
2530
|
+
}
|
|
2443
2531
|
};
|
|
2444
2532
|
}
|
|
2445
2533
|
const unmatchedChainOrders = Array.isArray(bot.manager?._lastUnmatchedChainOrders)
|
|
2446
2534
|
? bot.manager._lastUnmatchedChainOrders
|
|
2447
2535
|
: [];
|
|
2448
|
-
const pendingBroadcasts = (bot
|
|
2449
|
-
? Array.from(bot.manager._pendingBroadcasts.values())
|
|
2450
|
-
: [];
|
|
2536
|
+
const pendingBroadcasts = getPendingBroadcasts(bot);
|
|
2451
2537
|
if (hasCreateActions && (unmatchedChainOrders.length > 0 || pendingBroadcasts.length > 0)) {
|
|
2452
2538
|
if (pendingBroadcasts.length > 0) {
|
|
2453
2539
|
bot.manager.logger.log(`[COW] Rejecting CREATE batch: ${pendingBroadcasts.length} pending broadcast(s) from a prior uncertain ` +
|
|
2454
2540
|
`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
|
-
}
|
|
2541
|
+
await requestStructuralResync(bot, 'pending broadcasts before COW create', { pendingBroadcasts: pendingBroadcasts.map((p) => p.slotId) });
|
|
2460
2542
|
try {
|
|
2461
2543
|
await reconcileAfterUncertainBroadcast(bot, new BroadcastUncertainError('rejected CREATE batch had pending broadcasts', {
|
|
2462
2544
|
operations: pendingBroadcasts.map((p) => p.order),
|
|
@@ -2471,10 +2553,13 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2471
2553
|
}
|
|
2472
2554
|
popPushedWorkingGrid(bot, cowResult);
|
|
2473
2555
|
return {
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2556
|
+
proceed: false,
|
|
2557
|
+
result: {
|
|
2558
|
+
executed: false,
|
|
2559
|
+
aborted: true,
|
|
2560
|
+
reason: 'PENDING_BROADCASTS',
|
|
2561
|
+
hadRotation: false
|
|
2562
|
+
}
|
|
2478
2563
|
};
|
|
2479
2564
|
}
|
|
2480
2565
|
const unmatchedSample = unmatchedChainOrders
|
|
@@ -2491,8 +2576,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2491
2576
|
// freshest orders; syncing on it would virtualize live slots and
|
|
2492
2577
|
// re-create duplicates. Defer the adoption to a clean read — the
|
|
2493
2578
|
// unmatched orders keep blocking CREATEs until then.
|
|
2494
|
-
if (freshRead
|
|
2495
|
-
bot.manager.logger.log('[COW] Post-guard chain snapshot
|
|
2579
|
+
if (!isAuthoritativeChainRead(freshRead)) {
|
|
2580
|
+
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
2581
|
}
|
|
2497
2582
|
else if (freshRead.orders && freshRead.orders.length > 0) {
|
|
2498
2583
|
const freshSnapshot = freshRead.orders;
|
|
@@ -2532,18 +2617,17 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2532
2617
|
catch (syncErr) {
|
|
2533
2618
|
bot.manager.logger.log(`[COW] Failed to sync/unmatched orders: ${syncErr?.message || syncErr}`, 'warn');
|
|
2534
2619
|
}
|
|
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
|
-
}
|
|
2620
|
+
await requestStructuralResync(bot, 'unmatched chain orders before COW create', { unmatchedChainOrders: unmatchedChainOrders });
|
|
2540
2621
|
bot.manager.logger.log(`[COW] Rejecting CREATE batch after sync: working grid invalidated by master mutation`, 'info');
|
|
2541
2622
|
popPushedWorkingGrid(bot, cowResult);
|
|
2542
2623
|
return {
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2624
|
+
proceed: false,
|
|
2625
|
+
result: {
|
|
2626
|
+
executed: false,
|
|
2627
|
+
aborted: true,
|
|
2628
|
+
reason: 'UNMATCHED_CHAIN_ORDERS',
|
|
2629
|
+
hadRotation: false
|
|
2630
|
+
}
|
|
2547
2631
|
};
|
|
2548
2632
|
}
|
|
2549
2633
|
// Crossing-check candidate set (master + pending-broadcast + unmatched
|
|
@@ -2551,7 +2635,6 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2551
2635
|
// it reflects any sync they triggered.
|
|
2552
2636
|
const crossingCandidates = buildCrossingCandidates(bot);
|
|
2553
2637
|
const intraBatchCandidates = [];
|
|
2554
|
-
const { assetA, assetB } = bot.manager.assets;
|
|
2555
2638
|
// CROSSED-BOOK GATE: refuse to broadcast any batch whose simulated result
|
|
2556
2639
|
// prices a BUY at-or-above a SELL (see detectCrossedBookPlan).
|
|
2557
2640
|
const crossedBookDetail = detectCrossedBookPlan(bot.manager, actions);
|
|
@@ -2559,17 +2642,90 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2559
2642
|
bot.manager.logger.log(`[COW] Rejecting batch pre-broadcast: crossed book detected (${crossedBookDetail})`, 'error');
|
|
2560
2643
|
popPushedWorkingGrid(bot, cowResult);
|
|
2561
2644
|
return {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2645
|
+
proceed: false,
|
|
2646
|
+
result: {
|
|
2647
|
+
executed: false,
|
|
2648
|
+
aborted: true,
|
|
2649
|
+
reason: 'CROSSED_BOOK',
|
|
2650
|
+
detail: crossedBookDetail,
|
|
2651
|
+
hadRotation: false
|
|
2652
|
+
}
|
|
2567
2653
|
};
|
|
2568
2654
|
}
|
|
2655
|
+
return { proceed: true, crossingCandidates, intraBatchCandidates };
|
|
2656
|
+
}
|
|
2657
|
+
async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
2658
|
+
const replanDepth = Number.isFinite(Number(options?.replanDepth)) ? Number(options.replanDepth) : 0;
|
|
2659
|
+
bot._currentCycleId = (Number.isFinite(Number(bot._currentCycleId)) ? Number(bot._currentCycleId) : 0) + 1;
|
|
2660
|
+
const { workingGrid, workingIndexes, workingBoundary, actions } = cowResult;
|
|
2661
|
+
// Boundary-hold value: computed pre-broadcast after the skip-restore and
|
|
2662
|
+
// frozen for every downstream commit path (success + uncertain-catch).
|
|
2663
|
+
// workingBoundary itself stays untouched (audit trail).
|
|
2664
|
+
let effectiveBoundary = workingBoundary;
|
|
2665
|
+
if (bot.config.dryRun) {
|
|
2666
|
+
const cancelCount = actions.filter((a) => a.type === COW_ACTIONS.CANCEL).length;
|
|
2667
|
+
const createCount = actions.filter((a) => a.type === COW_ACTIONS.CREATE).length;
|
|
2668
|
+
const updateCount = actions.filter((a) => a.type === COW_ACTIONS.UPDATE).length;
|
|
2669
|
+
if (cancelCount > 0)
|
|
2670
|
+
bot.manager.logger.log(`Dry run: would cancel ${cancelCount} orders`, 'info');
|
|
2671
|
+
if (createCount > 0)
|
|
2672
|
+
bot.manager.logger.log(`Dry run: would place ${createCount} new orders`, 'info');
|
|
2673
|
+
if (updateCount > 0)
|
|
2674
|
+
bot.manager.logger.log(`Dry run: would update ${updateCount} orders`, 'info');
|
|
2675
|
+
popPushedWorkingGrid(bot, cowResult);
|
|
2676
|
+
return { executed: true, hadRotation: false };
|
|
2677
|
+
}
|
|
2678
|
+
// Single-flight COW broadcast guard (entry check): never broadcast two
|
|
2679
|
+
// batches concurrently. Overlapping broadcasts plan from the same base
|
|
2680
|
+
// grid version; when the first commits it bumps _gridVersion, so the
|
|
2681
|
+
// second's commit is refused (base-version mismatch) -> adopt-from-chain
|
|
2682
|
+
// -> snapshot reload that can drop the adopted order and produce an
|
|
2683
|
+
// orphan fill. This entry wait is an optimization; the authoritative
|
|
2684
|
+
// atomic check-and-set happens right before the broadcast below.
|
|
2685
|
+
if (await waitForCowBroadcastSingleFlight(bot, 'entry')) {
|
|
2686
|
+
popPushedWorkingGrid(bot, cowResult);
|
|
2687
|
+
return { executed: false, aborted: true, reason: 'SHUTDOWN_IN_PROGRESS', hadRotation: false };
|
|
2688
|
+
}
|
|
2689
|
+
// DRAIN PENDING CORRECTIONS before the batch is planned/broadcast.
|
|
2690
|
+
// Cancel-only corrections (duplicate-price orphans) queued by an earlier
|
|
2691
|
+
// sync must not sit while batches run back-to-back (startup create
|
|
2692
|
+
// groups, fill bursts) — with the open-orders sync loop disabled they
|
|
2693
|
+
// would otherwise linger indefinitely, keep blocking same-level CREATEs,
|
|
2694
|
+
// and risk cancelling the wrong side of a duplicate later. Draining here
|
|
2695
|
+
// also keeps this batch's collision checks (chain_orphan_collision)
|
|
2696
|
+
// honest: orphaned chain orders already queued for cancellation are
|
|
2697
|
+
// resolved before the plan validates its CREATE targets against them.
|
|
2698
|
+
const pendingCorrectionCount = Array.isArray(bot.manager?.ordersNeedingPriceCorrection)
|
|
2699
|
+
? bot.manager.ordersNeedingPriceCorrection.length
|
|
2700
|
+
: 0;
|
|
2701
|
+
if (pendingCorrectionCount > 0 && !bot._shuttingDown) {
|
|
2702
|
+
try {
|
|
2703
|
+
bot.manager.logger.log(`[COW] Draining ${pendingCorrectionCount} pending correction(s) before batch`, 'info');
|
|
2704
|
+
const drainResult = await orderUtils.correctAllPriceMismatches(bot.manager, bot.account, bot.privateKey, chainOrders);
|
|
2705
|
+
if (drainResult?.failed > 0) {
|
|
2706
|
+
bot.manager.logger.log(`[COW] ${drainResult.failed} correction(s) failed pre-batch; remaining entries retry on next sync/maintenance tick`, 'warn');
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
catch (drainErr) {
|
|
2710
|
+
bot.manager.logger.log(`[COW] Pre-batch correction drain failed: ${getErrorMessage(drainErr)}`, 'warn');
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
// Pre-broadcast guard chain (create-slot validation, recovery-exhausted
|
|
2714
|
+
// block, pending/unmatched guards, crossed-book gate) — see
|
|
2715
|
+
// runPreBroadcastGuards. Refusals already popped the working grid.
|
|
2716
|
+
const guards = await runPreBroadcastGuards(bot, cowResult);
|
|
2717
|
+
if (!guards.proceed)
|
|
2718
|
+
return guards.result;
|
|
2719
|
+
const { crossingCandidates, intraBatchCandidates } = guards;
|
|
2720
|
+
const { assetA, assetB } = bot.manager.assets;
|
|
2569
2721
|
const operations = [];
|
|
2570
2722
|
const opContexts = [];
|
|
2571
2723
|
const skippedUpdateSlotIds = new Set();
|
|
2572
2724
|
let skippedUpdateCount = 0;
|
|
2725
|
+
// Guard-skipped CREATE slot ids (hole-refills never placed). Fed to the
|
|
2726
|
+
// boundary-hold intersect alongside the UPDATE sets above — a skipped
|
|
2727
|
+
// refill CREATE strands its rail hole exactly like a restored UPDATE.
|
|
2728
|
+
const skippedCreateSlotIds = new Set();
|
|
2573
2729
|
// Per-batch LAST-FILL-GUARD disposition counters. Per-action pass lines
|
|
2574
2730
|
// would spam big batches, so the guard emits one batch summary instead
|
|
2575
2731
|
// (see the summary after the action loop below).
|
|
@@ -2645,6 +2801,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2645
2801
|
const sizeValidation = validateOrderSizeForExecution(bot, order.size, order.type, order, order.size);
|
|
2646
2802
|
if (!sizeValidation.isValid) {
|
|
2647
2803
|
bot.manager.logger.log(`Skipping create op for ${action.id}: ${sizeValidation.reason}`, 'warn');
|
|
2804
|
+
if (action.id)
|
|
2805
|
+
skippedCreateSlotIds.add(action.id);
|
|
2648
2806
|
continue;
|
|
2649
2807
|
}
|
|
2650
2808
|
const liveSlot = bot.manager.orders.get(order.id);
|
|
@@ -2669,6 +2827,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2669
2827
|
`same-batch CREATE ${batchCollision.id} already at ` +
|
|
2670
2828
|
`price ${Format.formatPrice6(batchCollision.order.price)}. ` +
|
|
2671
2829
|
`The next reconcile cycle will resolve the mismatch.`, 'warn');
|
|
2830
|
+
if (order.id)
|
|
2831
|
+
skippedCreateSlotIds.add(order.id);
|
|
2672
2832
|
continue;
|
|
2673
2833
|
}
|
|
2674
2834
|
// CROSSING-PLACEMENT GUARD (create variant): the batch-level
|
|
@@ -2687,6 +2847,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2687
2847
|
bot.manager.logger.log(`[COW-CROSS-GUARD] Skipping CREATE for ${order.id} at ` +
|
|
2688
2848
|
`${Format.formatPrice6(createPrice)}: crosses live ` +
|
|
2689
2849
|
`${crossedOrderLabel(effectiveCrossed)}; re-planned after its cancel confirms.`, 'warn');
|
|
2850
|
+
if (order.id)
|
|
2851
|
+
skippedCreateSlotIds.add(order.id);
|
|
2690
2852
|
continue;
|
|
2691
2853
|
}
|
|
2692
2854
|
// LAST-FILL PRICE GUARD: pivot ± halfIncrement (BUY < pivot*(1-half), SELL > pivot*(1+half)).
|
|
@@ -2699,20 +2861,15 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2699
2861
|
const isCorrectionCreate = actionOrigin === 'spread-correction'
|
|
2700
2862
|
|| (actionOrigin == null && batchOrigin === 'spread-correction');
|
|
2701
2863
|
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++;
|
|
2864
|
+
const { check, refreshed } = runLastFillGuardCheck(bot, createPrice, order.size, order.type, lastFillGuardStats);
|
|
2865
|
+
if (refreshed)
|
|
2866
|
+
lastFillGuardPivotRefreshed = true;
|
|
2712
2867
|
if (check.blocked) {
|
|
2713
2868
|
lastFillGuardStats.skipped++;
|
|
2714
2869
|
const dir = order.type === ORDER_TYPES.BUY ? 'above' : 'below';
|
|
2715
2870
|
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`, 'warn');
|
|
2871
|
+
if (order.id)
|
|
2872
|
+
skippedCreateSlotIds.add(order.id);
|
|
2716
2873
|
continue;
|
|
2717
2874
|
}
|
|
2718
2875
|
lastFillGuardStats.passed++;
|
|
@@ -2726,6 +2883,8 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2726
2883
|
const buildResult = await chainOrders.buildCreateOrderOp(bot.account, args.amountToSell, args.sellAssetId, args.minToReceive, args.receiveAssetId, null);
|
|
2727
2884
|
if (!buildResult) {
|
|
2728
2885
|
bot.manager.logger.log(`Skipping create op for ${action.id}: amounts would round to 0 on blockchain`, 'warn');
|
|
2886
|
+
if (action.id)
|
|
2887
|
+
skippedCreateSlotIds.add(action.id);
|
|
2729
2888
|
continue;
|
|
2730
2889
|
}
|
|
2731
2890
|
operations.push(buildResult.op);
|
|
@@ -2928,16 +3087,9 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2928
3087
|
lastFillGuardStats.bypassed++;
|
|
2929
3088
|
}
|
|
2930
3089
|
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++;
|
|
3090
|
+
const { check, refreshed } = runLastFillGuardCheck(bot, newPrice, newSize, orderType, lastFillGuardStats);
|
|
3091
|
+
if (refreshed)
|
|
3092
|
+
lastFillGuardPivotRefreshed = true;
|
|
2941
3093
|
if (check.blocked) {
|
|
2942
3094
|
lastFillGuardStats.skipped++;
|
|
2943
3095
|
skippedUpdateCount++;
|
|
@@ -3065,14 +3217,9 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3065
3217
|
// so it obeys the same guard with a refreshed
|
|
3066
3218
|
// pivot — no origin bypass, same as rotations.
|
|
3067
3219
|
try {
|
|
3068
|
-
|
|
3220
|
+
const { check: fbCheck, refreshed: fbRefreshed } = runLastFillGuardCheck(bot, fbPrice, fbSize, fbType, lastFillGuardStats);
|
|
3221
|
+
if (fbRefreshed)
|
|
3069
3222
|
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
3223
|
if (fbCheck.blocked) {
|
|
3077
3224
|
lastFillGuardStats.skipped++;
|
|
3078
3225
|
const fbDir = fbType === ORDER_TYPES.BUY ? 'above' : 'below';
|
|
@@ -3160,6 +3307,15 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3160
3307
|
}
|
|
3161
3308
|
bot.manager.logger.log(`[COW] Re-synced ${clampedUpdateSlotIds.size} post-fill-clamped slot(s) from master before commit`, 'debug');
|
|
3162
3309
|
}
|
|
3310
|
+
// BOUNDARY HOLD: skipped refill slots strand empty rail holes past the
|
|
3311
|
+
// planned boundary (commit gate skips empties, self-legalizing) — hold
|
|
3312
|
+
// the committed boundary; intersect-only, grid still commits.
|
|
3313
|
+
const refillHold = resolveRefillBoundaryHold(workingBoundary, bot.manager.boundaryIdx, skippedUpdateSlotIds, clampedUpdateSlotIds, cowResult?.refillSlotIds, skippedCreateSlotIds);
|
|
3314
|
+
effectiveBoundary = refillHold.effectiveBoundary;
|
|
3315
|
+
if (refillHold.heldRefillSlotIds.length > 0) {
|
|
3316
|
+
bot.manager.logger.log(`[COW] Boundary hold: ${refillHold.heldRefillSlotIds.length} refill slot(s) skipped ` +
|
|
3317
|
+
`(${refillHold.heldRefillSlotIds.join(', ')}) — keeping ${bot.manager.boundaryIdx} over planned ${workingBoundary}`, 'warn');
|
|
3318
|
+
}
|
|
3163
3319
|
if (operations.length === 0) {
|
|
3164
3320
|
// Pop the working grid: in the re-plan recursion the fresh plan's
|
|
3165
3321
|
// grid was pushed by performSafeRebalance, and nothing downstream
|
|
@@ -3255,14 +3411,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3255
3411
|
sell: item.ctx?.finalInts?.sell,
|
|
3256
3412
|
receive: item.ctx?.finalInts?.receive,
|
|
3257
3413
|
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
|
-
})
|
|
3414
|
+
fingerprint: createOpFingerprintForSlot(bot, item.ctx?.order, item.ctx?.finalInts, slotId)
|
|
3266
3415
|
});
|
|
3267
3416
|
if (match?.id) {
|
|
3268
3417
|
workingGrid.set(slotId, { ...slot, orderId: match.id });
|
|
@@ -3305,29 +3454,17 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3305
3454
|
// options.result, so a later throw in this frame (e.g.
|
|
3306
3455
|
// processBatchResults after a successful commit) cannot pop a
|
|
3307
3456
|
// second time for the same grid in the batch catch below.
|
|
3308
|
-
const commitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes,
|
|
3457
|
+
const commitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes, effectiveBoundary, { skipRecalc: true, result: cowResult });
|
|
3309
3458
|
if (!commitOk) {
|
|
3310
3459
|
// Master changed during broadcast (e.g. a fill landed and was
|
|
3311
3460
|
// processed concurrently) so the commit was refused. The batch
|
|
3312
3461
|
// is on chain; adopt the placed orders from the chain so master
|
|
3313
3462
|
// 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 };
|
|
3463
|
+
return await recoverRefusedCommit(bot, chainOrders, '[COW]', { placedResults: result, placedContexts: executedContexts }, executedContexts, effectiveBoundary, {
|
|
3464
|
+
failureResyncReason: 'commit refused after broadcast (chain adoption unavailable)',
|
|
3465
|
+
failureLogMessage: 'Commit refused and chain adoption unavailable; keeping pending-broadcast protection pending structural resync',
|
|
3466
|
+
preAdoptLogMessage: 'Commit refused after broadcast; adopting placed orders from chain to keep master in sync',
|
|
3467
|
+
});
|
|
3331
3468
|
}
|
|
3332
3469
|
const batchResult = await processBatchResults(bot, result, executedContexts);
|
|
3333
3470
|
const persistResult = await bot.manager.persistGrid();
|
|
@@ -3342,10 +3479,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3342
3479
|
bot.manager.logger.log(`[COW][PERSIST-GUARD] Retry also skipped/invalid ` +
|
|
3343
3480
|
`(${retryResult.reason || 'no reason'}). Master grid in memory ` +
|
|
3344
3481
|
`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
|
-
}
|
|
3482
|
+
await requestStructuralResync(bot, 'persistence guard triggered after COW batch', { persistReason: retryResult.reason || 'unknown' });
|
|
3349
3483
|
}
|
|
3350
3484
|
else {
|
|
3351
3485
|
delete bot.manager._persistenceWarning;
|
|
@@ -3410,22 +3544,17 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3410
3544
|
// _commitWorkingGrid pops on every settle path and clears
|
|
3411
3545
|
// the push marker via options.result, so a later throw here
|
|
3412
3546
|
// must not pop again in the batch catch below.
|
|
3413
|
-
const pollCommitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes,
|
|
3547
|
+
const pollCommitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes, effectiveBoundary, { skipRecalc: true, result: cowResult });
|
|
3414
3548
|
if (!pollCommitOk) {
|
|
3415
3549
|
// Master moved while polling — same recovery as the
|
|
3416
3550
|
// refused-commit path: adopt from chain, keep pending
|
|
3417
3551
|
// 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 };
|
|
3552
|
+
return await recoverRefusedCommit(bot, chainOrders, '[COW][UNCERTAIN]', { placedContexts: opContexts, polledCreateIds: confirmation.confirmedChainIds }, opContexts, effectiveBoundary, {
|
|
3553
|
+
successReturn: { executed: false, hadRotation: false, commitRefused: true, uncertainResolved: true },
|
|
3554
|
+
failureResyncReason: 'poll-confirmed commit refused (chain adoption unavailable)',
|
|
3555
|
+
failureLogMessage: 'Poll-refused commit with unavailable chain adoption; keeping pending protection pending structural resync',
|
|
3556
|
+
preAdoptLogMessage: 'Poll-confirmed commit refused; adopting placed orders from chain',
|
|
3557
|
+
});
|
|
3429
3558
|
}
|
|
3430
3559
|
// Enrich master grid with chain-assigned order IDs and amounts;
|
|
3431
3560
|
// accounting enabled so the adopted orders' capital is locked
|
|
@@ -3436,19 +3565,16 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3436
3565
|
// on a failed adoption would let the next cycle re-create the
|
|
3437
3566
|
// VIRTUAL slots as duplicate on-chain orders. Keep the
|
|
3438
3567
|
// 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
3568
|
// The commit happened without processBatchResults (no success
|
|
3446
3569
|
// result to extract); deduct create fees so the optimistic
|
|
3447
|
-
// balance reflects the on-chain cost
|
|
3448
|
-
|
|
3449
|
-
await
|
|
3450
|
-
|
|
3451
|
-
|
|
3570
|
+
// balance reflects the on-chain cost (shared with the
|
|
3571
|
+
// refused-commit paths via recoverRefusedCommit).
|
|
3572
|
+
return await recoverRefusedCommit(bot, chainOrders, '[COW][UNCERTAIN]', { placedContexts: opContexts, polledCreateIds: confirmation.confirmedChainIds }, opContexts, effectiveBoundary, {
|
|
3573
|
+
extraReturn: { commitRefused: false },
|
|
3574
|
+
successReturn: { executed: true, hadRotation: false, uncertainResolved: true },
|
|
3575
|
+
failureResyncReason: 'poll-confirmed commit (chain adoption unavailable)',
|
|
3576
|
+
failureLogMessage: 'Poll-confirmed commit with unavailable chain adoption; keeping pending protection pending structural resync',
|
|
3577
|
+
});
|
|
3452
3578
|
}
|
|
3453
3579
|
}
|
|
3454
3580
|
catch (pollErr) {
|
|
@@ -3502,8 +3628,10 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3502
3628
|
* @param {Object} [details={}] - Details passed to the resync handler
|
|
3503
3629
|
*/
|
|
3504
3630
|
async function requestStructuralResync(bot, reason, details = {}) {
|
|
3505
|
-
if (typeof bot.manager?.requestStructuralGridResync !== 'function')
|
|
3631
|
+
if (typeof bot.manager?.requestStructuralGridResync !== 'function') {
|
|
3632
|
+
bot._warn?.(`[COW] requestStructuralGridResync unavailable; cannot schedule structural resync (reason: ${reason}).`);
|
|
3506
3633
|
return;
|
|
3634
|
+
}
|
|
3507
3635
|
if (bot.manager._recoveryState) {
|
|
3508
3636
|
bot.manager._recoveryState = { ...bot.manager._recoveryState, structuralResyncRequested: true };
|
|
3509
3637
|
}
|
|
@@ -3523,91 +3651,6 @@ async function requestStructuralResync(bot, reason, details = {}) {
|
|
|
3523
3651
|
* @param {string} logPrefix - Log prefix for sync failure messages
|
|
3524
3652
|
* @returns {Promise<boolean>}
|
|
3525
3653
|
*/
|
|
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
3654
|
/**
|
|
3612
3655
|
* Converge master with the chain after a COW commit was refused (master moved
|
|
3613
3656
|
* during broadcast) or an uncertain broadcast was poll-confirmed.
|
|
@@ -3760,8 +3803,8 @@ async function adoptPlacedBatchFromChain(bot, chainOrders, logPrefix, opts = {})
|
|
|
3760
3803
|
}
|
|
3761
3804
|
// FALLBACK: window read (ambiguous when truncated).
|
|
3762
3805
|
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');
|
|
3806
|
+
if (!isAuthoritativeChainRead(freshRead)) {
|
|
3807
|
+
bot.manager.logger.log(`${logPrefix} Chain read ${freshRead?.truncated ? 'TRUNCATED' : 'EMPTY'} after batch broadcast; adoption deferred (pending-broadcast protection kept)`, 'warn');
|
|
3765
3808
|
return false;
|
|
3766
3809
|
}
|
|
3767
3810
|
const freshChain = freshRead.orders;
|
|
@@ -3791,6 +3834,75 @@ async function persistGridAndClearPendingBroadcasts(bot, logPrefix) {
|
|
|
3791
3834
|
}
|
|
3792
3835
|
clearPendingBroadcasts(bot.manager?._pendingBroadcasts);
|
|
3793
3836
|
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Recover from a refused commit after a COW batch broadcast.
|
|
3839
|
+
* Adopts placed orders from the chain, applies fee accounting, restores
|
|
3840
|
+
* the boundary, and persists. All three commit-refused paths
|
|
3841
|
+
* (success-path refused, poll-refused, and poll-confirmed uncertain)
|
|
3842
|
+
* share this exact sequence.
|
|
3843
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
3844
|
+
* @param {Object} chainOrders - Chain orders module
|
|
3845
|
+
* @param {string} logPrefix - Log prefix for messages
|
|
3846
|
+
* @param {Object} adoptOpts - Options passed to adoptPlacedBatchFromChain
|
|
3847
|
+
* @param {Array} contexts - Op contexts for fee accounting
|
|
3848
|
+
* @param {Object} workingBoundary - The working boundary
|
|
3849
|
+
* @param {Object} [opts={}] - Named options (object form: adjacent string
|
|
3850
|
+
* options were positional before, a transposition would compile silently).
|
|
3851
|
+
* @param {Object} [opts.extraReturn={}] - Extra fields merged into the failure return object
|
|
3852
|
+
* @param {Object|null} [opts.successReturn=null] - Return object on successful adoption
|
|
3853
|
+
* (defaults to the commit-refused shape; the poll-confirmed path passes its
|
|
3854
|
+
* executed:true shape)
|
|
3855
|
+
* @param {string} [opts.failureResyncReason='commit refused after broadcast (chain adoption unavailable)']
|
|
3856
|
+
* @param {string} [opts.failureLogMessage='Commit refused and chain adoption unavailable; keeping pending-broadcast protection pending structural resync']
|
|
3857
|
+
* - Failure-path log body (prefixed with logPrefix); each call site passes
|
|
3858
|
+
* its original wording so log greps keep matching.
|
|
3859
|
+
* @param {string|null} [opts.preAdoptLogMessage=null] - Optional warn logged before
|
|
3860
|
+
* the adoption attempt (the success-path and poll-refused sites log one;
|
|
3861
|
+
* the poll-confirmed site never did)
|
|
3862
|
+
* @returns {Promise<Object>} The commit-refused return object
|
|
3863
|
+
*/
|
|
3864
|
+
async function recoverRefusedCommit(bot, chainOrders, logPrefix, adoptOpts, contexts, workingBoundary, opts = {}) {
|
|
3865
|
+
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;
|
|
3866
|
+
if (preAdoptLogMessage) {
|
|
3867
|
+
bot.manager.logger.log(`${logPrefix} ${preAdoptLogMessage}`, 'warn');
|
|
3868
|
+
}
|
|
3869
|
+
const adopted = await adoptPlacedBatchFromChain(bot, chainOrders, logPrefix, adoptOpts);
|
|
3870
|
+
if (!adopted) {
|
|
3871
|
+
bot.manager.logger.log(`${logPrefix} ${failureLogMessage}`, 'error');
|
|
3872
|
+
await requestStructuralResync(bot, failureResyncReason, { reason: 'chain-adoption-unavailable' });
|
|
3873
|
+
return { executed: false, hadRotation: false, commitRefused: true, chainAdoptionPending: true, ...extraReturn };
|
|
3874
|
+
}
|
|
3875
|
+
await applyAdoptionFeeAccounting(bot, contexts);
|
|
3876
|
+
await restoreBoundaryAfterAdoption(bot, workingBoundary);
|
|
3877
|
+
await persistGridAndClearPendingBroadcasts(bot, logPrefix);
|
|
3878
|
+
return successReturn ?? { executed: false, hadRotation: false, commitRefused: true, ...extraReturn };
|
|
3879
|
+
}
|
|
3880
|
+
/**
|
|
3881
|
+
* Run the last-fill guard probe: refresh the pivot from still-queued fills,
|
|
3882
|
+
* read the durable pivot, and evaluate isLastFillGuardBlocked. Consolidates
|
|
3883
|
+
* the identical probe pattern in the CREATE, UPDATE-rotation, and
|
|
3884
|
+
* CREATE-fallback guards (origin bypasses and skip logging stay at the
|
|
3885
|
+
* call sites, which differ per action kind).
|
|
3886
|
+
* @param {Object} bot
|
|
3887
|
+
* @param {number} price - Target order price
|
|
3888
|
+
* @param {number} size - Order size
|
|
3889
|
+
* @param {string} type - ORDER_TYPES.BUY/SELL
|
|
3890
|
+
* @param {Object} stats - lastFillGuardStats tracker (checked++ here)
|
|
3891
|
+
* @returns {{check: Object, refreshed: boolean}}
|
|
3892
|
+
*/
|
|
3893
|
+
function runLastFillGuardCheck(bot, price, size, type, stats) {
|
|
3894
|
+
let refreshed = false;
|
|
3895
|
+
try {
|
|
3896
|
+
refreshed = !!refreshLastFillPivotFromQueue(bot);
|
|
3897
|
+
}
|
|
3898
|
+
catch { /* best-effort */ }
|
|
3899
|
+
const lastPrice = bot.manager?._lastFilledPrice;
|
|
3900
|
+
const lastType = bot.manager?._lastFilledType;
|
|
3901
|
+
const inc = resolveLastFillGuardIncrement(bot);
|
|
3902
|
+
const check = isLastFillGuardBlocked(price, size, type, lastPrice, lastType, inc);
|
|
3903
|
+
stats.checked++;
|
|
3904
|
+
return { check, refreshed };
|
|
3905
|
+
}
|
|
3794
3906
|
/**
|
|
3795
3907
|
* Apply BTS create-fee accounting for a batch that bypassed the normal
|
|
3796
3908
|
* processBatchResults pipeline (commit refused after broadcast, or
|
|
@@ -3827,6 +3939,7 @@ async function applyAdoptionFeeAccounting(bot, contexts) {
|
|
|
3827
3939
|
isPartialPlacement: false,
|
|
3828
3940
|
expectedType: ctx.order.type,
|
|
3829
3941
|
fee: btsFeeData?.createFee || 0,
|
|
3942
|
+
order: ctx.order ?? null,
|
|
3830
3943
|
}, 'createOrder');
|
|
3831
3944
|
}
|
|
3832
3945
|
catch (feeErr) {
|
|
@@ -3866,6 +3979,22 @@ async function applyAdoptionFeeAccounting(bot, contexts) {
|
|
|
3866
3979
|
}
|
|
3867
3980
|
}
|
|
3868
3981
|
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Table-driven optimistic fee call for processBatchResults: the cancel,
|
|
3984
|
+
* size-update, and both rotation branches all invoke
|
|
3985
|
+
* updateOptimisticFreeBalance(oldOrder, newOrder, context, fee, false)
|
|
3986
|
+
* under the same accountant guard — one spelling instead of four.
|
|
3987
|
+
* @param {import('./dexbot_class.js').DEXBot} bot
|
|
3988
|
+
* @param {Object} oldOrder
|
|
3989
|
+
* @param {Object} newOrder
|
|
3990
|
+
* @param {string} context - Fee context ('fill-cancel' | 'order-update')
|
|
3991
|
+
* @param {number} fee - BTS fee amount
|
|
3992
|
+
*/
|
|
3993
|
+
async function applyOptimisticFeeBalance(bot, oldOrder, newOrder, context, fee) {
|
|
3994
|
+
if (oldOrder && newOrder && bot.manager.accountant) {
|
|
3995
|
+
await bot.manager.accountant.updateOptimisticFreeBalance(oldOrder, newOrder, context, fee || 0, false);
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3869
3998
|
/**
|
|
3870
3999
|
* Process results from batch transaction execution.
|
|
3871
4000
|
* Updates order state, synchronizes with chain, and deducts BTS fees.
|
|
@@ -3893,27 +4022,16 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
3893
4022
|
bot.manager.logger.log(`Cancelled surplus order ${ctx.order.id} (${ctx.order.orderId})`, 'info');
|
|
3894
4023
|
const oldOrder = ctx.order;
|
|
3895
4024
|
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
|
-
}
|
|
4025
|
+
await applyOptimisticFeeBalance(bot, oldOrder, committedOrder, 'fill-cancel', btsFeeData?.cancelFee || 0);
|
|
3899
4026
|
}
|
|
3900
4027
|
else if (ctx.kind === 'size-update') {
|
|
3901
4028
|
const oldOrder = ctx.updateInfo.partialOrder;
|
|
3902
4029
|
const ord = bot.manager.orders.get(oldOrder.id);
|
|
3903
|
-
|
|
3904
|
-
await bot.manager.accountant.updateOptimisticFreeBalance(oldOrder, ord, 'order-update', btsFeeData?.updateFee || 0, false);
|
|
3905
|
-
}
|
|
4030
|
+
await applyOptimisticFeeBalance(bot, oldOrder, ord, 'order-update', btsFeeData?.updateFee || 0);
|
|
3906
4031
|
if (ord) {
|
|
3907
4032
|
const updatedSlot = { ...ord, size: ctx.updateInfo.newSize };
|
|
3908
4033
|
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
|
-
};
|
|
4034
|
+
updatedSlot.rawOnChain = rawOnChainFromInts(ord.orderId, ctx.finalInts);
|
|
3917
4035
|
}
|
|
3918
4036
|
updatesToApply.push({ order: updatedSlot, context: 'post-update-metadata' });
|
|
3919
4037
|
}
|
|
@@ -3924,7 +4042,8 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
3924
4042
|
const chainOrderId = res && res[1];
|
|
3925
4043
|
if (chainOrderId) {
|
|
3926
4044
|
await bot.manager.synchronizeWithChain({
|
|
3927
|
-
gridOrderId: ctx.order.id, chainOrderId, expectedType: ctx.order.type, fee: btsFeeData?.createFee || 0
|
|
4045
|
+
gridOrderId: ctx.order.id, chainOrderId, expectedType: ctx.order.type, fee: btsFeeData?.createFee || 0,
|
|
4046
|
+
order: ctx.order ?? null,
|
|
3928
4047
|
}, 'createOrder');
|
|
3929
4048
|
if (ctx.finalInts) {
|
|
3930
4049
|
const syncedOrder = bot.manager.orders.get(ctx.order.id);
|
|
@@ -3932,14 +4051,7 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
3932
4051
|
updatesToApply.push({
|
|
3933
4052
|
order: {
|
|
3934
4053
|
...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
|
-
}
|
|
4054
|
+
rawOnChain: rawOnChainFromInts(chainOrderId, ctx.finalInts)
|
|
3943
4055
|
},
|
|
3944
4056
|
context: 'post-placement-metadata'
|
|
3945
4057
|
});
|
|
@@ -3969,20 +4081,11 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
3969
4081
|
const { oldOrder, newPrice, newGridId, newSize, type } = rotation;
|
|
3970
4082
|
if (!newGridId) {
|
|
3971
4083
|
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
|
-
}
|
|
4084
|
+
await applyOptimisticFeeBalance(bot, oldOrder, ord, 'order-update', btsFeeData?.updateFee || 0);
|
|
3975
4085
|
if (ord) {
|
|
3976
4086
|
const updatedSlot = { ...ord, size: newSize };
|
|
3977
4087
|
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
|
-
};
|
|
4088
|
+
updatedSlot.rawOnChain = rawOnChainFromInts(ord.orderId, ctx.finalInts);
|
|
3986
4089
|
}
|
|
3987
4090
|
updatesToApply.push({ order: updatedSlot, context: 'post-update-metadata' });
|
|
3988
4091
|
}
|
|
@@ -4013,18 +4116,9 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
4013
4116
|
orderId: oldOrder?.orderId || slot.orderId || null
|
|
4014
4117
|
};
|
|
4015
4118
|
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);
|
|
4119
|
+
updatedSlot.rawOnChain = rawOnChainFromInts(updatedSlot.orderId, ctx.finalInts);
|
|
4027
4120
|
}
|
|
4121
|
+
await applyOptimisticFeeBalance(bot, oldOrder, updatedSlot, 'order-update', btsFeeData?.updateFee || 0);
|
|
4028
4122
|
if (oldOrder?.id && oldOrder.id !== newGridId) {
|
|
4029
4123
|
const currentSource = bot.manager.orders.get(oldOrder.id);
|
|
4030
4124
|
if (currentSource && currentSource.orderId) {
|
|
@@ -4051,7 +4145,10 @@ async function processBatchResults(bot, result, opContexts) {
|
|
|
4051
4145
|
updateOperationCount
|
|
4052
4146
|
};
|
|
4053
4147
|
}
|
|
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 };
|
|
4148
|
+
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 };
|
|
4149
|
+
// Exported for regression tests (issue #23 sibling): the uncertain-broadcast
|
|
4150
|
+
// discard path must never drop a placement silently when master lost the slot.
|
|
4151
|
+
export { restoreDiscardedCreates };
|
|
4055
4152
|
export default {
|
|
4056
4153
|
buildOutsideInPairGroupsForOrders,
|
|
4057
4154
|
buildOutsideInPairGroupsForCreateEntries,
|
|
@@ -4080,6 +4177,8 @@ export default {
|
|
|
4080
4177
|
buildActionsFromPlan,
|
|
4081
4178
|
buildCowResultFromPlan,
|
|
4082
4179
|
restoreSkippedUpdateSlotsInWorkingGrid,
|
|
4180
|
+
resolveRefillBoundaryHold,
|
|
4181
|
+
toRefillSlotIdSet,
|
|
4083
4182
|
applyRotationTransitionsToWorkingGrid,
|
|
4084
4183
|
pollChainForConfirmation,
|
|
4085
4184
|
updateOrdersOnChainBatchCOW,
|