dexbot 1.5.0 → 1.5.2
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 +44 -0
- package/analysis/ama_fitting/package.json +1 -1
- 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/dexbot.js +5 -5
- package/dist/dexbot.js.map +1 -1
- package/dist/modules/account_orders.d.ts +7 -1
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +34 -1
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/constants.d.ts +2 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +12 -0
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +1 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +19 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +335 -78
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +7 -1
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +54 -4
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +7 -0
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +34 -0
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +199 -5
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts +122 -15
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +310 -46
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +22 -17
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +7 -3
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +98 -28
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +52 -6
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +156 -21
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +17 -2
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +213 -17
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +70 -4
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +136 -5
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +117 -5
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +274 -13
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +14 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +51 -3
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +29 -2
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +207 -35
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/scripts/analyze-credit.d.ts.map +1 -1
- package/dist/scripts/analyze-credit.js +53 -33
- package/dist/scripts/analyze-credit.js.map +1 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +7 -5
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +3 -1
- package/docs/GRID_RECONCILE.md +1 -0
- package/docs/LOGGING.md +3 -0
- package/docs/README.md +2 -2
- package/docs/WORKFLOW.md +1 -1
- package/package.json +1 -1
|
@@ -14,11 +14,41 @@ 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, buildOutsideInPairGroups, isOrderPlaced, } = orderUtils;
|
|
17
|
+
const { buildCreateOrderArgs, buildCreateOpFingerprint, extractBatchOperationResults, formatUnmatchedChainOrder, convertToSpreadPlaceholder, toRailHolePlaceholder, buildOutsideInPairGroups, isOrderPlaced, } = orderUtils;
|
|
18
18
|
import * as validate from './order/utils/validate.js';
|
|
19
|
-
const { validateCreateTargetSlots, evaluateCommit, hasExecutableActions } = validate;
|
|
19
|
+
const { validateCreateTargetSlots, evaluateCommit, hasExecutableActions, stampGapEvacuationRotation } = validate;
|
|
20
20
|
import * as math from './order/utils/math.js';
|
|
21
|
-
const { validateOrderSize, findCrossedOrder, priceSlotEqual } = math;
|
|
21
|
+
const { validateOrderSize, findCrossedOrder, priceSlotEqual, isEvacuationRotationAllowed, isEvacuationSizeStillValid, getSellStartIdx, getPrecisionByOrderType, isSlotIndexInGapBand } = math;
|
|
22
|
+
/**
|
|
23
|
+
* Re-verify a B-stamped gap-evacuation rotation against the LIVE committed
|
|
24
|
+
* geometry (boundaryIdx/_gapSlots at execution time). The stamp freezes
|
|
25
|
+
* plan-build geometry, but a boundary commit can land between plan-build and
|
|
26
|
+
* execution; when the live geometry is finite and the source is no longer
|
|
27
|
+
* in-band (or the dest no longer rail), the stamp is stale and the rotation
|
|
28
|
+
* must be re-proven by the unstamped live probe instead of bypassing the
|
|
29
|
+
* last-fill guard. Non-finite live geometry cannot disprove the stamp and
|
|
30
|
+
* keeps the plan-build authority (fail-open to the stamp, never to the guard).
|
|
31
|
+
*/
|
|
32
|
+
export function isEvacuationStampStillValid(liveBoundary, liveGapSlots, sourceId, destId, orderType) {
|
|
33
|
+
const b = Number(liveBoundary);
|
|
34
|
+
const g = Number(liveGapSlots);
|
|
35
|
+
if (orderType !== ORDER_TYPES.BUY && orderType !== ORDER_TYPES.SELL)
|
|
36
|
+
return false;
|
|
37
|
+
if (!Number.isFinite(b) || !Number.isFinite(g) || g < 0)
|
|
38
|
+
return true;
|
|
39
|
+
const srcIdx = parseSlotIndex(sourceId);
|
|
40
|
+
const dstIdx = parseSlotIndex(destId);
|
|
41
|
+
if (srcIdx === null || srcIdx === undefined || dstIdx === null || dstIdx === undefined)
|
|
42
|
+
return false;
|
|
43
|
+
if (!isSlotIndexInGapBand(srcIdx, b, g))
|
|
44
|
+
return false;
|
|
45
|
+
if (isSlotIndexInGapBand(dstIdx, b, g))
|
|
46
|
+
return false;
|
|
47
|
+
const sellStartIdx = getSellStartIdx(b, g);
|
|
48
|
+
const dstInRail = orderType === ORDER_TYPES.SELL ? Number(dstIdx) >= sellStartIdx : Number(dstIdx) <= b;
|
|
49
|
+
return dstInRail;
|
|
50
|
+
}
|
|
51
|
+
import { parseSlotIndex } from './order/utils/slot.js';
|
|
22
52
|
function hasSlotPriceCollision(items, targetPrice, precision, excludeId, predicate) {
|
|
23
53
|
for (const it of items) {
|
|
24
54
|
if (predicate && !predicate(it))
|
|
@@ -250,28 +280,18 @@ function crossedOrderLabel(crossed) {
|
|
|
250
280
|
* an UPDATE-only rotation batch can re-price across an un-adopted chain
|
|
251
281
|
* order that master-grid-only checks cannot see (the pending/unmatched
|
|
252
282
|
* batch guards fire only for CREATE batches).
|
|
283
|
+
*
|
|
284
|
+
* Delegates to the shared order-utils builder so every crossing guard (COW
|
|
285
|
+
* create/rotation/fallback, startup reconcile placement, price-correction)
|
|
286
|
+
* sees the same set. Pending entries are pushed as wrappers (slotId +
|
|
287
|
+
* order): their inner order has no chain id yet, so orderId-only
|
|
288
|
+
* predicates would blind the guard to them — the shared
|
|
289
|
+
* isCrossingCheckCandidate predicate accepts slot-id-only wrappers.
|
|
253
290
|
* @param {Object} bot
|
|
254
291
|
* @returns {any[]}
|
|
255
292
|
*/
|
|
256
293
|
function buildCrossingCandidates(bot) {
|
|
257
|
-
|
|
258
|
-
if (!mgr)
|
|
259
|
-
return [];
|
|
260
|
-
const candidates = mgr.orders instanceof Map ? [...mgr.orders.values()] : [];
|
|
261
|
-
if (mgr._pendingBroadcasts instanceof Map) {
|
|
262
|
-
for (const entry of mgr._pendingBroadcasts.values()) {
|
|
263
|
-
const o = entry?.order;
|
|
264
|
-
if (o && o.type != null && o.price != null)
|
|
265
|
-
candidates.push(o);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (Array.isArray(mgr._lastUnmatchedChainOrders)) {
|
|
269
|
-
for (const o of mgr._lastUnmatchedChainOrders) {
|
|
270
|
-
if (o && o.type != null && o.price != null)
|
|
271
|
-
candidates.push(o);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return candidates;
|
|
294
|
+
return orderUtils.buildCrossingCheckCandidates(bot?.manager);
|
|
275
295
|
}
|
|
276
296
|
/**
|
|
277
297
|
* Drop only the pending-broadcast entries for the given CREATE slots.
|
|
@@ -1446,7 +1466,13 @@ function validateOrderSizeForExecution(bot, size, type, orderLike = null, fallba
|
|
|
1446
1466
|
* last fill @x with increment i: BUY < x*(1 - i/2/100), SELL > x*(1 + i/2/100)
|
|
1447
1467
|
* e.g. x=1000, i=0.5% => BUY < 997.5, SELL > 1002.5
|
|
1448
1468
|
* Cold (pivot null or lastType null) => disabled. Spread-correction CREATES
|
|
1449
|
-
* bypass per-action (see broadcast sites);
|
|
1469
|
+
* bypass per-action (see broadcast sites); gap-evacuation rotation UPDATEs
|
|
1470
|
+
* bypass only via the violation-reducing allowance (origin='gap-evacuation',
|
|
1471
|
+
* UPDATE-only, frozen B-stamp or live-proven isEvacuationRotationAllowed) —
|
|
1472
|
+
* see the UPDATE rotation guard block.
|
|
1473
|
+
* The pivot is the latest fill of either side and is never expired: it stays
|
|
1474
|
+
* the durable mark (last sold level floors new sells; buy fills pull it down
|
|
1475
|
+
* and re-open the sell side, buy-below-sell is never gated).
|
|
1450
1476
|
* @param {number} price - Target order price
|
|
1451
1477
|
* @param {number} size - Order size (unused, kept for compat)
|
|
1452
1478
|
* @param {string} type - ORDER_TYPES.BUY/SELL
|
|
@@ -1612,7 +1638,49 @@ function buildActionsFromPlan(_bot, plan) {
|
|
|
1612
1638
|
// inherit the spread-correction bypass. Actions built outside this helper
|
|
1613
1639
|
// carry no origin and default to guarded (safe default).
|
|
1614
1640
|
const planOrigin = normalizedPlan?.origin;
|
|
1615
|
-
|
|
1641
|
+
// B-stamp inputs: the plan-level boundary + gap width frozen AT
|
|
1642
|
+
// PLAN-BUILD TIME. The stamp carries this geometry so the execution
|
|
1643
|
+
// guard can bypass on it — and re-verifies it against the LIVE
|
|
1644
|
+
// committed geometry at execution time (isEvacuationStampStillValid),
|
|
1645
|
+
// downgrading stale stamps to the live probe.
|
|
1646
|
+
const frozenBoundaryRaw = Number(normalizedPlan?.boundaryIdx ?? _bot?.manager?.boundaryIdx);
|
|
1647
|
+
const frozenGapRaw = Number(normalizedPlan?.gapSlots ?? _bot?.manager?._gapSlots);
|
|
1648
|
+
const hasFrozenGeometry = Number.isFinite(frozenBoundaryRaw) && Number.isFinite(frozenGapRaw) && frozenGapRaw >= 0;
|
|
1649
|
+
const withOrigin = (action, sourceMaster = null) => {
|
|
1650
|
+
if (!planOrigin)
|
|
1651
|
+
return action;
|
|
1652
|
+
// Origin only rides UPDATEs (where the guard reads it). Gap-plan
|
|
1653
|
+
// CREATEs carry a provably dead origin — the spread-correction
|
|
1654
|
+
// CREATE check falls back to the batch origin, so dropping the
|
|
1655
|
+
// per-action origin there is safe. Other plan origins keep riding
|
|
1656
|
+
// non-UPDATE actions unchanged.
|
|
1657
|
+
if (action?.type !== COW_ACTIONS.UPDATE) {
|
|
1658
|
+
return planOrigin === 'gap-evacuation' ? { ...action } : { ...action, origin: planOrigin };
|
|
1659
|
+
}
|
|
1660
|
+
if (planOrigin !== 'gap-evacuation')
|
|
1661
|
+
return { ...action, origin: planOrigin };
|
|
1662
|
+
if (!hasFrozenGeometry)
|
|
1663
|
+
return { ...action, origin: planOrigin };
|
|
1664
|
+
// Route through the real stampler: geometry (source in-band, dest
|
|
1665
|
+
// rail), bit-exact non-growing size (side precision) and outward
|
|
1666
|
+
// repricing must all PROVE — a plan-level origin alone never grants
|
|
1667
|
+
// the bypass. Without a source master the proof is impossible, so
|
|
1668
|
+
// the action stays unstamped (the live probe at execution can still
|
|
1669
|
+
// re-prove it from the master grid).
|
|
1670
|
+
const base = { ...action, origin: planOrigin };
|
|
1671
|
+
if (!sourceMaster)
|
|
1672
|
+
return { ...base, origin: undefined };
|
|
1673
|
+
// The stampler wants the ORDER type (SELL/BUY), which rides on
|
|
1674
|
+
// action.order.type — action.type is the COW action kind ('update').
|
|
1675
|
+
const rotOrderType = action?.order?.type ?? action?.type;
|
|
1676
|
+
const proved = stampGapEvacuationRotation(base, sourceMaster, action.newPrice, action.newSize, rotOrderType, frozenBoundaryRaw, frozenGapRaw, _bot?.manager?.assets);
|
|
1677
|
+
// Proof refused: strip the origin so the action is exactly an
|
|
1678
|
+
// unstamped rotation (never an origin claim without a stamp).
|
|
1679
|
+
if (!proved?.evacBoundary && !proved?.evacGapSlots) {
|
|
1680
|
+
proved.origin = undefined;
|
|
1681
|
+
}
|
|
1682
|
+
return proved;
|
|
1683
|
+
};
|
|
1616
1684
|
const actions = [];
|
|
1617
1685
|
for (const o of ordersToCancel) {
|
|
1618
1686
|
if (o?.orderId) {
|
|
@@ -1646,7 +1714,7 @@ function buildActionsFromPlan(_bot, plan) {
|
|
|
1646
1714
|
price: newPrice,
|
|
1647
1715
|
size: newSize
|
|
1648
1716
|
}
|
|
1649
|
-
}));
|
|
1717
|
+
}, oldOrder));
|
|
1650
1718
|
}
|
|
1651
1719
|
for (const o of ordersToUpdate) {
|
|
1652
1720
|
const partialOrder = o?.partialOrder || o;
|
|
@@ -1697,12 +1765,31 @@ function buildCowResultFromPlan(bot, plan) {
|
|
|
1697
1765
|
? requestedBoundary
|
|
1698
1766
|
: bot.manager.boundaryIdx;
|
|
1699
1767
|
const actions = buildActionsFromPlan(bot, plan);
|
|
1768
|
+
// Rail-aware hole projection (Phase 2): a cleared in-rail slot stays a
|
|
1769
|
+
// rail-typed VIRTUAL hole with its booked size preserved (rotation
|
|
1770
|
+
// sources keep the remainder the guard/plan must see); only true
|
|
1771
|
+
// gap-band slots become side-neutral SPREAD holes.
|
|
1772
|
+
const toWorkingHole = (slot) => {
|
|
1773
|
+
try {
|
|
1774
|
+
const idx = parseSlotIndex(slot?.id);
|
|
1775
|
+
const b = Number(workingBoundary);
|
|
1776
|
+
const gapSlots = Number(bot?.manager?._gapSlots);
|
|
1777
|
+
if (idx !== null && idx !== undefined && Number.isFinite(b) && Number.isFinite(gapSlots) && gapSlots >= 0) {
|
|
1778
|
+
if (Number(idx) <= b)
|
|
1779
|
+
return toRailHolePlaceholder(slot, ORDER_TYPES.BUY);
|
|
1780
|
+
if (Number(idx) >= getSellStartIdx(b, gapSlots))
|
|
1781
|
+
return toRailHolePlaceholder(slot, ORDER_TYPES.SELL);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
catch { /* fall through to SPREAD */ }
|
|
1785
|
+
return convertToSpreadPlaceholder(slot);
|
|
1786
|
+
};
|
|
1700
1787
|
for (const action of actions) {
|
|
1701
1788
|
if (action.type === COW_ACTIONS.CANCEL) {
|
|
1702
1789
|
const current = workingGrid.get(action.id);
|
|
1703
1790
|
if (!current)
|
|
1704
1791
|
continue;
|
|
1705
|
-
workingGrid.set(action.id,
|
|
1792
|
+
workingGrid.set(action.id, toWorkingHole(current));
|
|
1706
1793
|
}
|
|
1707
1794
|
else if (action.type === COW_ACTIONS.CREATE) {
|
|
1708
1795
|
if (!action.id || !action.order)
|
|
@@ -1720,7 +1807,7 @@ function buildCowResultFromPlan(bot, plan) {
|
|
|
1720
1807
|
if (action.newGridId && action.newGridId !== action.id) {
|
|
1721
1808
|
const current = workingGrid.get(action.id);
|
|
1722
1809
|
if (current) {
|
|
1723
|
-
workingGrid.set(action.id,
|
|
1810
|
+
workingGrid.set(action.id, toWorkingHole(current));
|
|
1724
1811
|
}
|
|
1725
1812
|
const targetId = action.newGridId;
|
|
1726
1813
|
const targetCurrent = workingGrid.get(targetId) || { id: targetId };
|
|
@@ -1781,7 +1868,11 @@ function applyRotationTransitionsToWorkingGrid(bot, workingGrid, executedContext
|
|
|
1781
1868
|
continue;
|
|
1782
1869
|
const { rotation } = ctx;
|
|
1783
1870
|
const { oldOrder, newGridId, newPrice, newSize, type } = rotation;
|
|
1784
|
-
// Source slot → VIRTUAL (if it's a different slot)
|
|
1871
|
+
// Source slot → VIRTUAL (if it's a different slot). Phase 2: the
|
|
1872
|
+
// source stays a RAIL-TYPED hole with its booked size preserved —
|
|
1873
|
+
// only state/orderId/rawOnChain are cleared, never type or size
|
|
1874
|
+
// (no SPREAD retype, no zeroing). In-rail sources must remain
|
|
1875
|
+
// visible to candidate-selection and evacuation geometry.
|
|
1785
1876
|
if (oldOrder?.id && oldOrder.id !== newGridId) {
|
|
1786
1877
|
const sourceSlot = workingGrid.get(oldOrder.id);
|
|
1787
1878
|
if (sourceSlot && sourceSlot.orderId) {
|
|
@@ -1834,10 +1925,14 @@ async function pollChainForConfirmation(bot, opContexts, options = {}) {
|
|
|
1834
1925
|
// Only CREATE operations can be confirmed by polling (they appear as new orders on chain)
|
|
1835
1926
|
const createContexts = opContexts.filter((ctx) => ctx && ctx.kind === 'create' && ctx.finalInts && ctx.order);
|
|
1836
1927
|
if (createContexts.length === 0) {
|
|
1837
|
-
return { allConfirmed: false, confirmed: [], unconfirmed: [...opContexts] };
|
|
1928
|
+
return { allConfirmed: false, confirmed: [], unconfirmed: [...opContexts], confirmedChainIds: [] };
|
|
1838
1929
|
}
|
|
1839
1930
|
const accountRef = bot.accountId || bot.account?.id || bot.account;
|
|
1840
1931
|
let remaining = [...createContexts];
|
|
1932
|
+
// Chain ids of the poll-matched fresh creates. Retained (not just
|
|
1933
|
+
// logged) so the poll-confirmed adoption path can re-read them BY ID —
|
|
1934
|
+
// the window fallback cannot prove a lagging node has them yet.
|
|
1935
|
+
const matchedChainIds = [];
|
|
1841
1936
|
for (let attempt = 1; attempt <= maxPollRetries; attempt++) {
|
|
1842
1937
|
try {
|
|
1843
1938
|
const chainRead = await readOpenOrdersWithMetaSafe(chainOrders, accountRef);
|
|
@@ -1877,6 +1972,8 @@ async function pollChainForConfirmation(bot, opContexts, options = {}) {
|
|
|
1877
1972
|
});
|
|
1878
1973
|
if (match) {
|
|
1879
1974
|
bot.manager.logger.log(`[COW][POLL] Confirmed CREATE for slot ${ctx.order.id} on chain as ${match.id}`, 'debug');
|
|
1975
|
+
if (match.id && /^1\.7\.\d+$/.test(String(match.id)))
|
|
1976
|
+
matchedChainIds.push(String(match.id));
|
|
1880
1977
|
}
|
|
1881
1978
|
else {
|
|
1882
1979
|
stillUnconfirmed.push(ctx);
|
|
@@ -1885,7 +1982,7 @@ async function pollChainForConfirmation(bot, opContexts, options = {}) {
|
|
|
1885
1982
|
if (stillUnconfirmed.length === 0) {
|
|
1886
1983
|
const confirmed = createContexts;
|
|
1887
1984
|
bot.manager.logger.log(`[COW][POLL] All ${confirmed.length} CREATE(s) confirmed on chain after ${attempt} poll(s)`, 'info');
|
|
1888
|
-
return { allConfirmed: true, confirmed, unconfirmed: [] };
|
|
1985
|
+
return { allConfirmed: true, confirmed, unconfirmed: [], confirmedChainIds: [...matchedChainIds] };
|
|
1889
1986
|
}
|
|
1890
1987
|
remaining = stillUnconfirmed;
|
|
1891
1988
|
if (attempt < maxPollRetries) {
|
|
@@ -1902,7 +1999,7 @@ async function pollChainForConfirmation(bot, opContexts, options = {}) {
|
|
|
1902
1999
|
const confirmed = createContexts.filter((ctx) => !remaining.includes(ctx));
|
|
1903
2000
|
bot.manager.logger.log(`[COW][POLL] ${confirmed.length}/${createContexts.length} CREATE(s) confirmed after ${maxPollRetries} polls; ` +
|
|
1904
2001
|
`${remaining.length} unconfirmed. Falling back to reconciliation.`, 'warn');
|
|
1905
|
-
return { allConfirmed: false, confirmed, unconfirmed: remaining };
|
|
2002
|
+
return { allConfirmed: false, confirmed, unconfirmed: remaining, confirmedChainIds: [...matchedChainIds] };
|
|
1906
2003
|
}
|
|
1907
2004
|
/**
|
|
1908
2005
|
* Restore skipped update slots in the working grid to master state.
|
|
@@ -2277,6 +2374,12 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2277
2374
|
case 'chain_orphan_collision':
|
|
2278
2375
|
reason = `unmatched on-chain order ${violation.currentOrderId} at same price`;
|
|
2279
2376
|
break;
|
|
2377
|
+
case 'same_batch_price_duplicate':
|
|
2378
|
+
reason = `another CREATE in the same batch at same broadcast price (duplicate of ${violation.duplicateOf})`;
|
|
2379
|
+
break;
|
|
2380
|
+
case 'create_price_invalid':
|
|
2381
|
+
reason = `broadcast price is not a finite number`;
|
|
2382
|
+
break;
|
|
2280
2383
|
default:
|
|
2281
2384
|
reason = `existing orderId=${violation.currentOrderId}`;
|
|
2282
2385
|
}
|
|
@@ -2577,10 +2680,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2577
2680
|
// an opposite-side order cancelled in a later chunk would
|
|
2578
2681
|
// otherwise coexist with this create mid-broadcast and
|
|
2579
2682
|
// self-trade (production incident class).
|
|
2580
|
-
const createCrossed = findCrossedOrder(crossingCandidates, createPrice, order.type, bot.manager.assets, (o) =>
|
|
2581
|
-
const oid = o?.orderId || o?.chainOrderId;
|
|
2582
|
-
return o && oid && !cancelOpIndexByOrderId.has(oid);
|
|
2583
|
-
});
|
|
2683
|
+
const createCrossed = findCrossedOrder(crossingCandidates, createPrice, order.type, bot.manager.assets, (o) => orderUtils.isCrossingCheckCandidate(o, null, cancelOpIndexByOrderId));
|
|
2584
2684
|
const intraBatchCrossed = createCrossed ? null : findCrossedOrder(intraBatchCandidates, createPrice, order.type, bot.manager.assets);
|
|
2585
2685
|
const effectiveCrossed = createCrossed || intraBatchCrossed;
|
|
2586
2686
|
if (effectiveCrossed) {
|
|
@@ -2694,13 +2794,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2694
2794
|
// fatal fund assert). Skipping is safe: the slot keeps its
|
|
2695
2795
|
// old commitment and the next plan re-evaluates once
|
|
2696
2796
|
// the crossed order's cancel confirms.
|
|
2697
|
-
const crossedOrder = findCrossedOrder([...crossingCandidates, ...intraBatchCandidates], newPrice, orderType, bot.manager.assets, (o) =>
|
|
2698
|
-
const oid = o?.orderId || o?.chainOrderId;
|
|
2699
|
-
return o
|
|
2700
|
-
&& oid
|
|
2701
|
-
&& oid !== action.orderId
|
|
2702
|
-
&& !cancelOpIndexByOrderId.has(oid);
|
|
2703
|
-
});
|
|
2797
|
+
const crossedOrder = findCrossedOrder([...crossingCandidates, ...intraBatchCandidates], newPrice, orderType, bot.manager.assets, (o) => orderUtils.isCrossingCheckCandidate(o, action.orderId, cancelOpIndexByOrderId));
|
|
2704
2798
|
if (crossedOrder) {
|
|
2705
2799
|
skippedUpdateCount++;
|
|
2706
2800
|
if (action.id)
|
|
@@ -2712,35 +2806,151 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2712
2806
|
`${crossedOrderLabel(crossedOrder)}; re-planned after its cancel confirms.`, 'warn');
|
|
2713
2807
|
continue;
|
|
2714
2808
|
}
|
|
2715
|
-
// LAST-FILL PRICE GUARD (UPDATE rotation): pivot ± halfIncrement — same as CREATE
|
|
2716
|
-
//
|
|
2717
|
-
//
|
|
2809
|
+
// LAST-FILL PRICE GUARD (UPDATE rotation): pivot ± halfIncrement — same as CREATE,
|
|
2810
|
+
// plus the GAP-EVACUATION ALLOWANCE (violation-reducing): a rotation stamped
|
|
2811
|
+
// origin='gap-evacuation' at plan-build moves a live order OUT of the gap band
|
|
2812
|
+
// onto its rail with bit-exact non-growing size — it shrinks the violation
|
|
2813
|
+
// surface instead of adding exposure, so it bypasses the guard. The bypass
|
|
2814
|
+
// is UPDATE-only (CREATEs carry no source slot and can never qualify) and
|
|
2815
|
+
// honors the frozen B-stamp (evacBoundary/evacGapSlots captured when the
|
|
2816
|
+
// plan was built — never re-derived here, since plans can validate against
|
|
2817
|
+
// a later-committed boundary):
|
|
2818
|
+
// - B-stamped: plan-build geometry already proved the evacuation; bypass
|
|
2819
|
+
// outright (logged + counted as bypassed).
|
|
2820
|
+
// - Unstamped gap-evacuation: prove it live from oldPrice/oldSize read
|
|
2821
|
+
// off the MASTER grid above (bot.manager.orders.get, before rotation
|
|
2822
|
+
// pre-application virtualizes sources — after that the lookup lies);
|
|
2823
|
+
// fail closed (block) when unresolvable; otherwise run the pure
|
|
2824
|
+
// isEvacuationRotationAllowed decision and bypass only on allow.
|
|
2825
|
+
// Everything else obeys the guard (per-action scoping: a rotation entry
|
|
2826
|
+
// merged into another plan inherits no bypass).
|
|
2718
2827
|
// The pivot is refreshed from still-queued fills first: fills detected mid-broadcast
|
|
2719
2828
|
// sit in _incomingFillQueue until the fill cycle ends, and without this the later
|
|
2720
2829
|
// chunks of a long broadcast would be checked against a stale pivot.
|
|
2721
2830
|
try {
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2831
|
+
let bypassedEvacuation = false;
|
|
2832
|
+
const rotationOrigin = action?.origin;
|
|
2833
|
+
if (rotationOrigin === 'gap-evacuation') {
|
|
2834
|
+
const frozenB = Number(action?.evacBoundary);
|
|
2835
|
+
const frozenG = Number(action?.evacGapSlots);
|
|
2836
|
+
let stampUsable = Number.isFinite(frozenB) && Number.isFinite(frozenG);
|
|
2837
|
+
if (stampUsable) {
|
|
2838
|
+
// Stale-stamp check: the stamp froze PLAN-BUILD
|
|
2839
|
+
// geometry; if a boundary commit landed between
|
|
2840
|
+
// plan-build and execution and the source is no
|
|
2841
|
+
// longer in-band (or dest no longer rail) under
|
|
2842
|
+
// the LIVE geometry, re-prove via the unstamped
|
|
2843
|
+
// live probe below instead of bypassing outright.
|
|
2844
|
+
const stampValid = isEvacuationStampStillValid(bot.manager?.boundaryIdx, bot.manager?._gapSlots, action.id, action.newGridId, orderType);
|
|
2845
|
+
if (!stampValid) {
|
|
2846
|
+
stampUsable = false;
|
|
2847
|
+
bot.manager.logger.log(`[LAST-FILL-GUARD] Stamped evacuation for ${action.id} -> ${action.newGridId} is stale under live ` +
|
|
2848
|
+
`boundary ${bot.manager?.boundaryIdx}/gap ${bot.manager?._gapSlots} — re-proving live`, 'warn');
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (stampUsable) {
|
|
2852
|
+
// Stamped-size re-proof: the stamp proved the
|
|
2853
|
+
// plan-time size, but an unprocessed fill can
|
|
2854
|
+
// land between plan-build and execution and
|
|
2855
|
+
// shrink the booked remaining below the
|
|
2856
|
+
// planned size (the PARTIAL-only growth guard
|
|
2857
|
+
// above misses it when the slot is not yet
|
|
2858
|
+
// PARTIAL). Re-prove against the LIVE master
|
|
2859
|
+
// size; invalidating the stamp routes into
|
|
2860
|
+
// the unstamped probe below, whose
|
|
2861
|
+
// isEvacuationRotationAllowed also rejects
|
|
2862
|
+
// growth and then falls through to the
|
|
2863
|
+
// normal last-fill guard.
|
|
2864
|
+
let stampPrecision = null;
|
|
2865
|
+
try {
|
|
2866
|
+
stampPrecision = getPrecisionByOrderType(bot.manager.assets, orderType);
|
|
2867
|
+
}
|
|
2868
|
+
catch {
|
|
2869
|
+
stampPrecision = null;
|
|
2870
|
+
}
|
|
2871
|
+
if (!isEvacuationSizeStillValid(newSize, Number(masterOrder?.size), stampPrecision)) {
|
|
2872
|
+
stampUsable = false;
|
|
2873
|
+
bot.manager.logger.log(`[LAST-FILL-GUARD] Stamped evacuation for ${action.id} -> ${action.newGridId} lost its size cover: ` +
|
|
2874
|
+
`planned ${Format.formatAmount(newSize)} exceeds booked remaining ${Format.formatAmount(Number(masterOrder?.size))} — re-proving live`, 'warn');
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
if (stampUsable) {
|
|
2878
|
+
bypassedEvacuation = true;
|
|
2879
|
+
bot.manager.logger.log(`[LAST-FILL-GUARD] Allowing ${orderType} evacuation UPDATE for ${action.id} -> ${action.newGridId} at ${Format.formatPrice6(newPrice)}: ` +
|
|
2880
|
+
`gap-evacuation stamped at plan-build (boundary ${frozenB}, gap ${frozenG})`, 'warn');
|
|
2881
|
+
}
|
|
2882
|
+
else {
|
|
2883
|
+
// Unstamped: prove violation-reducing live. masterOrder was
|
|
2884
|
+
// captured from the master grid before any rotation
|
|
2885
|
+
// pre-application — it must NOT be re-read afterwards.
|
|
2886
|
+
const oldPrice = Number(masterOrder?.price);
|
|
2887
|
+
const oldSize = Number(masterOrder?.size);
|
|
2888
|
+
if (!Number.isFinite(oldPrice) || !(oldPrice > 0) || !Number.isFinite(oldSize) || !(oldSize > 0)) {
|
|
2889
|
+
lastFillGuardStats.checked++;
|
|
2890
|
+
lastFillGuardStats.skipped++;
|
|
2891
|
+
skippedUpdateCount++;
|
|
2892
|
+
if (action.id)
|
|
2893
|
+
skippedUpdateSlotIds.add(action.id);
|
|
2894
|
+
if (action.newGridId)
|
|
2895
|
+
skippedUpdateSlotIds.add(action.newGridId);
|
|
2896
|
+
bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${orderType} UPDATE for ${action.id} -> ${action.newGridId}: ` +
|
|
2897
|
+
`gap-evacuation source unresolvable from master grid (fail-closed)`, 'warn');
|
|
2898
|
+
continue;
|
|
2899
|
+
}
|
|
2900
|
+
let sidePrecision = null;
|
|
2901
|
+
try {
|
|
2902
|
+
sidePrecision = getPrecisionByOrderType(bot.manager.assets, orderType);
|
|
2903
|
+
}
|
|
2904
|
+
catch {
|
|
2905
|
+
sidePrecision = null;
|
|
2906
|
+
}
|
|
2907
|
+
// Geometry re-proof under the LIVE boundary: if the
|
|
2908
|
+
// source is no longer in-band (or dest no longer rail),
|
|
2909
|
+
// this is not an evacuation anymore — fall through to
|
|
2910
|
+
// the normal guard instead of bypassing via the probe.
|
|
2911
|
+
// Non-finite live geometry cannot disprove; probe proceeds.
|
|
2912
|
+
if (isEvacuationStampStillValid(bot.manager?.boundaryIdx, bot.manager?._gapSlots, action.id, action.newGridId, orderType)) {
|
|
2913
|
+
const evacCheck = isEvacuationRotationAllowed(oldPrice, oldSize, newPrice, newSize, orderType, sidePrecision);
|
|
2914
|
+
if (evacCheck.allowed) {
|
|
2915
|
+
bypassedEvacuation = true;
|
|
2916
|
+
bot.manager.logger.log(`[LAST-FILL-GUARD] Allowing ${orderType} evacuation UPDATE for ${action.id} -> ${action.newGridId} at ${Format.formatPrice6(newPrice)}: ` +
|
|
2917
|
+
`${evacCheck.reason} (probed live: ${Format.formatPrice6(oldPrice)} -> ${Format.formatPrice6(newPrice)})`, 'warn');
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
else {
|
|
2921
|
+
bot.manager.logger.log(`[LAST-FILL-GUARD] Unstamped evacuation probe for ${action.id} -> ${action.newGridId} rejected: ` +
|
|
2922
|
+
`source/dest no longer evacuation geometry under live boundary ${bot.manager?.boundaryIdx}/gap ${bot.manager?._gapSlots} — normal guard applies`, 'warn');
|
|
2923
|
+
}
|
|
2924
|
+
// Not allowed: fall through to the normal guard below.
|
|
2925
|
+
}
|
|
2725
2926
|
}
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2927
|
+
if (bypassedEvacuation) {
|
|
2928
|
+
lastFillGuardStats.bypassed++;
|
|
2929
|
+
}
|
|
2930
|
+
else {
|
|
2931
|
+
try {
|
|
2932
|
+
if (refreshLastFillPivotFromQueue(bot))
|
|
2933
|
+
lastFillGuardPivotRefreshed = true;
|
|
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++;
|
|
2941
|
+
if (check.blocked) {
|
|
2942
|
+
lastFillGuardStats.skipped++;
|
|
2943
|
+
skippedUpdateCount++;
|
|
2944
|
+
if (action.id)
|
|
2945
|
+
skippedUpdateSlotIds.add(action.id);
|
|
2946
|
+
if (action.newGridId)
|
|
2947
|
+
skippedUpdateSlotIds.add(action.newGridId);
|
|
2948
|
+
const dir = orderType === ORDER_TYPES.BUY ? 'above' : 'below';
|
|
2949
|
+
bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${orderType} UPDATE for ${action.id} -> ${action.newGridId} at ${Format.formatPrice6(newPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)})`, 'warn');
|
|
2950
|
+
continue;
|
|
2951
|
+
}
|
|
2952
|
+
lastFillGuardStats.passed++;
|
|
2742
2953
|
}
|
|
2743
|
-
lastFillGuardStats.passed++;
|
|
2744
2954
|
}
|
|
2745
2955
|
catch (_e) { /* best-effort */ }
|
|
2746
2956
|
const { amountToSell, minToReceive } = buildCreateOrderArgs({ type: orderType, size: newSize, price: newPrice }, assetA, assetB);
|
|
@@ -2843,10 +3053,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
2843
3053
|
// replaces — no crossing of an opposite-side
|
|
2844
3054
|
// live order whose cancel is not already
|
|
2845
3055
|
// queued at an earlier op position.
|
|
2846
|
-
const fbCrossed = findCrossedOrder(crossingCandidates, fbPrice, fbType, bot.manager.assets, (o) =>
|
|
2847
|
-
const oid = o?.orderId || o?.chainOrderId;
|
|
2848
|
-
return o && oid && !cancelOpIndexByOrderId.has(oid);
|
|
2849
|
-
});
|
|
3056
|
+
const fbCrossed = findCrossedOrder(crossingCandidates, fbPrice, fbType, bot.manager.assets, (o) => orderUtils.isCrossingCheckCandidate(o, null, cancelOpIndexByOrderId));
|
|
2850
3057
|
if (fbCrossed) {
|
|
2851
3058
|
bot.manager.logger.log(`[COW-CROSS-GUARD] Skipping CREATE fallback for ${targetSlotId} at ` +
|
|
2852
3059
|
`${Format.formatPrice6(fbPrice)}: new ${fbType} crosses live ` +
|
|
@@ -3209,7 +3416,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3209
3416
|
// refused-commit path: adopt from chain, keep pending
|
|
3210
3417
|
// protection if adoption is unavailable.
|
|
3211
3418
|
bot.manager.logger.log(`[COW][UNCERTAIN] Poll-confirmed commit refused; adopting placed orders from chain`, 'warn');
|
|
3212
|
-
const pollAdopted = await adoptPlacedBatchFromChain(bot, chainOrders, '[COW][UNCERTAIN]');
|
|
3419
|
+
const pollAdopted = await adoptPlacedBatchFromChain(bot, chainOrders, '[COW][UNCERTAIN]', { placedContexts: opContexts, polledCreateIds: confirmation.confirmedChainIds });
|
|
3213
3420
|
if (!pollAdopted) {
|
|
3214
3421
|
bot.manager.logger.log('[COW][UNCERTAIN] Poll-refused commit with unavailable chain adoption; keeping pending protection pending structural resync', 'error');
|
|
3215
3422
|
await requestStructuralResync(bot, 'poll-confirmed commit refused (chain adoption unavailable)', { reason: 'chain-adoption-unavailable' });
|
|
@@ -3229,7 +3436,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
|
|
|
3229
3436
|
// on a failed adoption would let the next cycle re-create the
|
|
3230
3437
|
// VIRTUAL slots as duplicate on-chain orders. Keep the
|
|
3231
3438
|
// protection and defer to a structural resync instead.
|
|
3232
|
-
const pollAdoptedOk = await adoptPlacedBatchFromChain(bot, chainOrders, '[COW][UNCERTAIN]');
|
|
3439
|
+
const pollAdoptedOk = await adoptPlacedBatchFromChain(bot, chainOrders, '[COW][UNCERTAIN]', { placedContexts: opContexts, polledCreateIds: confirmation.confirmedChainIds });
|
|
3233
3440
|
if (!pollAdoptedOk) {
|
|
3234
3441
|
bot.manager.logger.log('[COW][UNCERTAIN] Poll-confirmed commit with unavailable chain adoption; keeping pending protection pending structural resync', 'error');
|
|
3235
3442
|
await requestStructuralResync(bot, 'poll-confirmed commit (chain adoption unavailable)', { reason: 'chain-adoption-unavailable' });
|
|
@@ -3329,9 +3536,13 @@ async function requestStructuralResync(bot, reason, details = {}) {
|
|
|
3329
3536
|
* @param {any} mgr - bot.manager
|
|
3330
3537
|
* @param {any} placedResults - broadcast result (has operation_results); null when unavailable
|
|
3331
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
|
|
3332
3543
|
* @returns {string[]} Unique, well-formed 1.7.x order ids
|
|
3333
3544
|
*/
|
|
3334
|
-
function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts) {
|
|
3545
|
+
function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts, extraCreateIds = null) {
|
|
3335
3546
|
const masterIds = new Set();
|
|
3336
3547
|
const grid = mgr && mgr.grid;
|
|
3337
3548
|
if (Array.isArray(grid)) {
|
|
@@ -3341,6 +3552,17 @@ function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts) {
|
|
|
3341
3552
|
}
|
|
3342
3553
|
}
|
|
3343
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
|
+
}
|
|
3344
3566
|
const createIds = new Set();
|
|
3345
3567
|
if (placedResults && Array.isArray(placedContexts)) {
|
|
3346
3568
|
const opResults = extractBatchOperationResults(placedResults);
|
|
@@ -3356,6 +3578,33 @@ function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts) {
|
|
|
3356
3578
|
}
|
|
3357
3579
|
}
|
|
3358
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
|
+
}
|
|
3359
3608
|
const all = new Set([...masterIds, ...createIds]);
|
|
3360
3609
|
return { masterIds: [...masterIds], createIds: [...createIds], all: [...all] };
|
|
3361
3610
|
}
|
|
@@ -3382,6 +3631,10 @@ function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts) {
|
|
|
3382
3631
|
* @param {Object} [opts]
|
|
3383
3632
|
* @param {any} [opts.placedResults] - broadcast result carrying operation_results
|
|
3384
3633
|
* @param {any[]} [opts.placedContexts] - opContexts aligned with operation_results
|
|
3634
|
+
* @param {string[]} [opts.polledCreateIds] - fresh CREATE chain ids confirmed by
|
|
3635
|
+
* the uncertain-broadcast poll (no broadcast result exists on that path);
|
|
3636
|
+
* routes the poll call sites through the by-id path with its lagging-create
|
|
3637
|
+
* retry instead of the unguarded window fallback
|
|
3385
3638
|
* @returns {Promise<boolean>} true if master was adopted from the chain
|
|
3386
3639
|
*/
|
|
3387
3640
|
/**
|
|
@@ -3414,16 +3667,20 @@ async function restoreBoundaryAfterAdoption(bot, workingBoundary) {
|
|
|
3414
3667
|
}
|
|
3415
3668
|
}
|
|
3416
3669
|
async function adoptPlacedBatchFromChain(bot, chainOrders, logPrefix, opts = {}) {
|
|
3417
|
-
const { placedResults = null, placedContexts = null } = opts || {};
|
|
3670
|
+
const { placedResults = null, placedContexts = null, polledCreateIds = null } = opts || {};
|
|
3418
3671
|
try {
|
|
3419
3672
|
const mgr = bot.manager;
|
|
3420
3673
|
const accountRef = bot.accountId || bot.account?.id || bot.account;
|
|
3421
|
-
// PREFERRED: re-read the exact placed/existing orders by id.
|
|
3422
|
-
//
|
|
3423
|
-
//
|
|
3424
|
-
//
|
|
3425
|
-
|
|
3426
|
-
|
|
3674
|
+
// PREFERRED: re-read the exact placed/existing orders by id. The by-id
|
|
3675
|
+
// set needs the broadcast result (freshest CREATE ids), the
|
|
3676
|
+
// poll-confirmed CREATE ids, or at minimum the contexts referencing
|
|
3677
|
+
// existing chain orders — without any id hints the set would be
|
|
3678
|
+
// incomplete and would wrongly sync master against a partial picture.
|
|
3679
|
+
const haveIdHints = Boolean(placedResults)
|
|
3680
|
+
|| (Array.isArray(placedContexts) && placedContexts.length > 0)
|
|
3681
|
+
|| (Array.isArray(polledCreateIds) && polledCreateIds.length > 0);
|
|
3682
|
+
const { all: knownIds, createIds } = haveIdHints
|
|
3683
|
+
? collectKnownOnChainOrderIds(mgr, placedResults, placedContexts, polledCreateIds)
|
|
3427
3684
|
: { all: [], createIds: [] };
|
|
3428
3685
|
if (knownIds.length > 0 && typeof chainOrders.batchReadOrders === 'function') {
|
|
3429
3686
|
// Retry/backoff (fix #6): a fresh CREATE absent from the first read
|