dexbot 1.6.1 → 1.6.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/analysis/README.md +2 -2
  3. package/analysis/ama_fitting/package.json +1 -1
  4. package/analysis/grid_correction_check.ts +80 -7
  5. package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
  6. package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
  7. package/analysis/tradingview/h-bts_tradingview.html +1570 -0
  8. package/analysis/tradingview/t-bts_tradingview.html +1570 -0
  9. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +19 -5
  10. package/analysis/trend_detection/package.json +1 -1
  11. package/claw/package.json +1 -1
  12. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  13. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  14. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  15. package/dist/analysis/grid_correction_check.d.ts +5 -1
  16. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  17. package/dist/analysis/grid_correction_check.js +81 -7
  18. package/dist/analysis/grid_correction_check.js.map +1 -1
  19. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  20. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +19 -5
  21. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  22. package/dist/market_adapter/inputs/fetch_book_data.js +2 -2
  23. package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -1
  24. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +22 -3
  25. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  26. package/dist/market_adapter/inputs/fetch_lp_data.js +15 -15
  27. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  28. package/dist/market_adapter/inputs/kibana_feed_source.js +2 -2
  29. package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -1
  30. package/dist/market_adapter/inputs/window_cache.d.ts +54 -8
  31. package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -1
  32. package/dist/market_adapter/inputs/window_cache.js +328 -87
  33. package/dist/market_adapter/inputs/window_cache.js.map +1 -1
  34. package/dist/modules/bitshares-native/subscriptions.d.ts +1 -1
  35. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  36. package/dist/modules/bitshares-native/subscriptions.js +18 -4
  37. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  38. package/dist/modules/bitshares_client.d.ts.map +1 -1
  39. package/dist/modules/bitshares_client.js +10 -0
  40. package/dist/modules/bitshares_client.js.map +1 -1
  41. package/dist/modules/constants.d.ts +7 -3
  42. package/dist/modules/constants.d.ts.map +1 -1
  43. package/dist/modules/constants.js +44 -20
  44. package/dist/modules/constants.js.map +1 -1
  45. package/dist/modules/credit_runtime.d.ts.map +1 -1
  46. package/dist/modules/credit_runtime.js +24 -6
  47. package/dist/modules/credit_runtime.js.map +1 -1
  48. package/dist/modules/dexbot_class.d.ts +32 -7
  49. package/dist/modules/dexbot_class.d.ts.map +1 -1
  50. package/dist/modules/dexbot_class.js +55 -11
  51. package/dist/modules/dexbot_class.js.map +1 -1
  52. package/dist/modules/dexbot_cow_runtime.d.ts +137 -7
  53. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  54. package/dist/modules/dexbot_cow_runtime.js +955 -45
  55. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  56. package/dist/modules/dexbot_fill_runtime.d.ts +28 -1
  57. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  58. package/dist/modules/dexbot_fill_runtime.js +116 -3
  59. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  60. package/dist/modules/dexbot_maintenance_runtime.d.ts +46 -0
  61. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  62. package/dist/modules/dexbot_maintenance_runtime.js +193 -6
  63. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  64. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  65. package/dist/modules/dexbot_startup_runtime.js +6 -1
  66. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  67. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  68. package/dist/modules/dexbot_state_recovery.js +16 -3
  69. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  70. package/dist/modules/order/grid.d.ts +3 -1
  71. package/dist/modules/order/grid.d.ts.map +1 -1
  72. package/dist/modules/order/grid.js +47 -6
  73. package/dist/modules/order/grid.js.map +1 -1
  74. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  75. package/dist/modules/order/grid_reconcile_internal.js +39 -4
  76. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  77. package/dist/modules/order/manager.d.ts.map +1 -1
  78. package/dist/modules/order/manager.js +3 -1
  79. package/dist/modules/order/manager.js.map +1 -1
  80. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  81. package/dist/modules/order/sync_engine.js +200 -15
  82. package/dist/modules/order/sync_engine.js.map +1 -1
  83. package/dist/modules/order/utils/math.d.ts +60 -14
  84. package/dist/modules/order/utils/math.d.ts.map +1 -1
  85. package/dist/modules/order/utils/math.js +87 -20
  86. package/dist/modules/order/utils/math.js.map +1 -1
  87. package/dist/modules/order/utils/order.d.ts +95 -7
  88. package/dist/modules/order/utils/order.d.ts.map +1 -1
  89. package/dist/modules/order/utils/order.js +277 -15
  90. package/dist/modules/order/utils/order.js.map +1 -1
  91. package/dist/modules/utils/errors.d.ts +22 -0
  92. package/dist/modules/utils/errors.d.ts.map +1 -1
  93. package/dist/modules/utils/errors.js +30 -0
  94. package/dist/modules/utils/errors.js.map +1 -1
  95. package/docs/COPY_ON_WRITE_MASTER_PLAN.md +1 -1
  96. package/docs/DEXBOT_COMPARISON.md +3 -7
  97. package/docs/EVOLUTION.md +9 -7
  98. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +4 -9
  99. package/docs/LIFECYCLE.md +2 -2
  100. package/docs/README.md +9 -1
  101. package/docs/architecture.md +7 -7
  102. package/docs/developer_guide.md +4 -4
  103. package/market_adapter/README.md +1 -1
  104. package/package.json +1 -1
@@ -98,11 +98,33 @@ import * as Format from './format.js';
98
98
  import { lookupAsset, sleep, resolveAccountRef } from './utils/system.js';
99
99
  import * as chainOrders from '../chain_orders.js';
100
100
  import * as client from '../bitshares_client.js';
101
+ /**
102
+ * Test seam for targeted single-order refetches (drift refetch + sub-dust
103
+ * residual verification). When manager._readSingleOrderFn is a function it
104
+ * is used instead of the chain read. The chain path starts with
105
+ * waitForConnected (30s timeout against live nodes); tests that drive fill
106
+ * paths with a drift signal or a sub-dust residual would otherwise hang on
107
+ * that connect for a read whose result they do not even assert on.
108
+ * Production never sets this; default is the chain.
109
+ */
110
+ function readSingleOrderSeam(mgr, orderId, timeoutMs) {
111
+ const seam = mgr?._readSingleOrderFn;
112
+ if (typeof seam === 'function')
113
+ return seam(orderId, timeoutMs);
114
+ return chainOrders.readSingleOrder(orderId, timeoutMs);
115
+ }
116
+ /** Same seam for the batched drift refetch (batch path). */
117
+ function batchReadOrdersSeam(mgr, orderIds, timeoutMs) {
118
+ const seam = mgr?._batchReadOrdersFn;
119
+ if (typeof seam === 'function')
120
+ return seam(orderIds, timeoutMs);
121
+ return chainOrders.batchReadOrders(orderIds, timeoutMs);
122
+ }
101
123
  const { BitShares } = client;
102
124
  import { NATIVE_CLIENT } from '../constants.js';
103
125
  const { toFiniteNumber } = Format;
104
- import { blockchainToFloat, floatToBlockchainInt, calculatePriceTolerance, getAssetFees, getBtsSide, getSellStartIdx, slotIndexForPrice, isChainPriceOutOfGrid, isSlotInRail, isSlotIndexInGapBand, priceSlotEqual } from './utils/math.js';
105
- import { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, convertToSpreadPlaceholder, virtualizeOrder, buildFillKey, isOrderPlaced, hasOnChainId, isOrderVirtual, resolveSpreadOrderSide, duplicateOrphanLogInfo } from './utils/order.js';
126
+ import { blockchainToFloat, floatToBlockchainInt, calculatePriceTolerance, getAssetFees, getBtsSide, getSellStartIdx, slotIndexForPrice, isChainPriceOutOfGrid, isSlotInRail, isSlotIndexInGapBand, priceForSlot, priceSlotEqual } from './utils/math.js';
127
+ import { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, convertToSpreadPlaceholder, virtualizeOrder, buildFillKey, isOrderPlaced, hasOnChainId, isOrderVirtual, resolveSpreadOrderSide, duplicateOrphanLogInfo, _stampCorrectionProvenance } from './utils/order.js';
106
128
  import { parseSlotIndex } from './utils/slot.js';
107
129
  import { resolveProcessedFillPersistenceMode } from './processed_fill_store.js';
108
130
  import { getErrorMessage } from '../utils/errors.js';
@@ -117,6 +139,9 @@ import { getErrorMessage } from '../utils/errors.js';
117
139
  * Test seam: when manager._confirmEmptyReadFn is a function it is used
118
140
  * instead of the chain read (must resolve to a raw order array, or null
119
141
  * when the re-read itself is ambiguous). A throwing seam is 'ambiguous'.
142
+ * When manager._skipEmptyReadConfirmDelay is truthy the production
143
+ * SYNC_EMPTY_READ_CONFIRM_DELAY_MS pacing is skipped (tests); the confirm
144
+ * semantics (confirmed / contradicted / ambiguous / unavailable) are unchanged.
120
145
  *
121
146
  * @param {Object} mgr - OrderManager instance (accountId, config, logger).
122
147
  * @returns {Promise<string>} 'confirmed' (still empty — accept),
@@ -130,7 +155,9 @@ async function confirmSuspectEmptyRead(mgr) {
130
155
  if (mgr?.config?.dryRun)
131
156
  return 'unavailable';
132
157
  const seam = mgr?._confirmEmptyReadFn;
133
- const delay = Math.max(0, Number(TIMING.SYNC_EMPTY_READ_CONFIRM_DELAY_MS) || 0);
158
+ const delay = mgr?._skipEmptyReadConfirmDelay
159
+ ? 0
160
+ : Math.max(0, Number(TIMING.SYNC_EMPTY_READ_CONFIRM_DELAY_MS) || 0);
134
161
  if (typeof seam === 'function') {
135
162
  if (delay > 0)
136
163
  await sleep(delay);
@@ -264,7 +291,75 @@ function describeNearestAdoptionCandidates(mgr, chainOrder, precision, calcToler
264
291
  * @param {Function} calcToleranceFn - calculatePriceTolerance function
265
292
  * @returns {{candidateSlotId: string, candidateSlotPrice: number, priceDiff: number, tolerance: number}|null}
266
293
  */
294
+ /**
295
+ * Assert (non-fatally) that an adopted slot still carries its own genesis
296
+ * level, not a price inherited from the chain order.
297
+ *
298
+ * Both adoption paths keep the slot's price by NOT assigning it — an absence of
299
+ * a write. That is correct but invisible: nothing would notice if a later edit
300
+ * added `price: chainOrder.price` back, and nothing would notice a slot whose
301
+ * price had already been corrupted before it reached adoption. This makes the
302
+ * rule explicit and warns when it is broken.
303
+ *
304
+ * Deliberately non-throwing: adoption already has handled rejection paths, and
305
+ * turning a warning into a crash here would strand a live chain order. The
306
+ * caller's existing rejection handling stays the enforcement; this is the
307
+ * signal that surfaces the corruption in the log.
308
+ *
309
+ * @returns {boolean} true when the slot's price is its genesis level (or cannot
310
+ * be judged), false when it demonstrably is not.
311
+ */
312
+ function adoptedSlotKeepsItsOwnPrice(mgr, adopted, chainOrderId, path) {
313
+ try {
314
+ const genesis = mgr?._genesis;
315
+ const levels = genesis?.priceLevels;
316
+ if (!Array.isArray(levels) || levels.length === 0)
317
+ return true; // no ladder: unjudgeable, fail open
318
+ const idx = parseSlotIndex(adopted?.id);
319
+ if (idx === null || !Number.isFinite(idx) || idx < 0 || idx >= levels.length)
320
+ return true;
321
+ const expected = Number(priceForSlot(idx, genesis));
322
+ const got = Number(adopted?.price);
323
+ if (!Number.isFinite(expected) || expected <= 0)
324
+ return true;
325
+ if (!Number.isFinite(got))
326
+ return true;
327
+ const diff = Math.abs(got - expected);
328
+ const rel = diff / Math.max(1e-12, Math.abs(expected));
329
+ if (rel > 1e-9 && diff > 1e-12) {
330
+ mgr.logger?.log?.(`[SYNC] Adoption (${path}) for ${chainOrderId} into slot ${adopted.id}: slot price ${got} is NOT the slot's ` +
331
+ `genesis level ${expected} (drift ${(rel * 100).toFixed(3)}%). Adoption must not write the chain order's price ` +
332
+ `into slot.price — the slot keeps its own level. Adopting anyway (the chain order is real); investigate what ` +
333
+ `corrupted this slot's price.`, 'warn');
334
+ return false;
335
+ }
336
+ return true;
337
+ }
338
+ catch {
339
+ return true; // never let an observability check break adoption
340
+ }
341
+ }
267
342
  async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawChainOrders, matchedGridOrderIds, chainOrderIdsOnGrid, filledOrders, updatedOrders, skipAccounting) {
343
+ // ADOPTION NEVER TAKES THE CHAIN ORDER'S PRICE.
344
+ //
345
+ // Two different prices are conflated on this path, and only one of them is
346
+ // a grid position:
347
+ //
348
+ // slot.price what this slot's LEVEL is -> must equal priceForSlot(idx, genesis)
349
+ // chainOrder.price where the order RESTS on chain -> may differ legitimately
350
+ //
351
+ // They legitimately differ after a grid regeneration: the grid is
352
+ // recentered, old chain orders keep resting at the OLD grid's levels, and
353
+ // adoption adopts the order into the nearest slot (slotIndexForPrice). That
354
+ // is normal and is why this function accepts a differing chainOrder.price.
355
+ //
356
+ // It must not WRITE it. The resting price is chain metadata about where the
357
+ // order was placed; it disappears on the next rotation/cancel. Writing it
358
+ // into slot.price is the S1 corruption this whole invariant exists to
359
+ // prevent, so `price` is deliberately absent from the assignments below and
360
+ // the slot keeps its own level in `bestMatch` (spread from `slot`).
361
+ //
362
+ // Size is the opposite: size IS real state, so the chain value is adopted.
268
363
  let bestMatch = { ...slot };
269
364
  const wasVirtual = slot.state === ORDER_STATES.VIRTUAL;
270
365
  const wasPartial = slot.state === ORDER_STATES.PARTIAL;
@@ -329,6 +424,7 @@ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawC
329
424
  else if (wasPartial) {
330
425
  bestMatch.state = ORDER_STATES.PARTIAL;
331
426
  }
427
+ adoptedSlotKeepsItsOwnPrice(mgr, bestMatch, chainOrderId, 'genesis');
332
428
  const applied = await mgr._applyOrderUpdate(bestMatch, 'sync-pass2-orphan', { skipAccounting: skipAccounting, fee: 0 });
333
429
  if (applied === false) {
334
430
  // Same un-poisoning as the filled path above: the slot counts as
@@ -798,18 +894,30 @@ class SyncEngine {
798
894
  async _performSyncFromOpenOrders(mgr, assetAPrecision, assetBPrecision, parsedChainOrders, rawChainOrders, chainOrderIdsOnGrid, matchedGridOrderIds, filledOrders, updatedOrders, ordersNeedingCorrection, unmatchedChainOrders, options) {
799
895
  const skipAccounting = options?.skipAccounting ?? true;
800
896
  const queueCorrection = (entry) => {
801
- ordersNeedingCorrection.push(entry);
897
+ // Provenance: classify the detector so a stale replay at drain
898
+ // time logs who queued it and when. Classification from entry
899
+ // flags: cancelOnly orphans vs surplus/type-mismatch cancels vs
900
+ // plain price updates.
901
+ const source = entry?.cancelOnly === true
902
+ ? 'sync-duplicate-orphan'
903
+ : entry?.isSurplus === true
904
+ ? 'sync-type-mismatch'
905
+ : 'sync-price-mismatch';
906
+ ordersNeedingCorrection.push(_stampCorrectionProvenance({ ...entry }, source));
802
907
  if (!Array.isArray(mgr.ordersNeedingPriceCorrection))
803
908
  return;
804
909
  const existingIndex = mgr.ordersNeedingPriceCorrection.findIndex((queued) => queued?.chainOrderId === entry.chainOrderId && Boolean(queued?.isSurplus) === Boolean(entry.isSurplus));
805
910
  if (existingIndex >= 0) {
806
- mgr.ordersNeedingPriceCorrection[existingIndex] = {
911
+ // Merge: fresh detection overwrites price/size intent, but
912
+ // first-seen provenance survives (entry without provenance
913
+ // keeps the original queuedAt/queuedBy).
914
+ mgr.ordersNeedingPriceCorrection[existingIndex] = _stampCorrectionProvenance({
807
915
  ...mgr.ordersNeedingPriceCorrection[existingIndex],
808
916
  ...entry
809
- };
917
+ }, source);
810
918
  }
811
919
  else {
812
- mgr.ordersNeedingPriceCorrection.push({ ...entry });
920
+ mgr.ordersNeedingPriceCorrection.push(_stampCorrectionProvenance({ ...entry }, source));
813
921
  }
814
922
  };
815
923
  // ====================================================================
@@ -1219,6 +1327,20 @@ class SyncEngine {
1219
1327
  });
1220
1328
  if (adoptedSlot && !matchedGridOrderIds.has(adoptedSlot.id) && !adoptedSlot.orderId) {
1221
1329
  const precision = (chainOrder.type === ORDER_TYPES.SELL) ? assetAPrecision : assetBPrecision;
1330
+ // Legacy path parity with the genesis adoption path: the
1331
+ // slot's price is its GENESIS level and is never overwritten
1332
+ // with the chain order's price. Adopting chainOrder.price
1333
+ // here made the slot's price whichever price happened to be
1334
+ // on the book, which is how an off-grid order could become a
1335
+ // grid level and then be re-emitted as a plan price. The
1336
+ // size below is still the chain value: size is real state,
1337
+ // price is a ladder position.
1338
+ const legacyRail = isSlotInRail(mgr.boundaryIdx, mgr._gapSlots ?? 0, chainOrder.type, adoptedSlot);
1339
+ if (!legacyRail) {
1340
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'out-of-rail-deferred', candidateSlotId: adoptedSlot.id });
1341
+ mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}) — NOT adopted into slot ${adoptedSlot.id}: slot is outside the active rail for a ${chainOrder.type}; deferred`, 'warn');
1342
+ continue;
1343
+ }
1222
1344
  const chainInt = floatToBlockchainInt(chainOrder.size, precision);
1223
1345
  const adoptedRaw = rawChainOrders.get(chainOrderId);
1224
1346
  const adoptedState = chainInt > 0 ? ORDER_STATES.PARTIAL : ORDER_STATES.VIRTUAL;
@@ -1226,16 +1348,32 @@ class SyncEngine {
1226
1348
  const rawFee = toFiniteNumber(adoptedRaw.deferred_fee, null);
1227
1349
  return rawFee !== null && rawFee > 0 ? { deferredFee: blockchainToFloat(rawFee, BTS_PRECISION) } : undefined;
1228
1350
  })() : undefined;
1351
+ // ADOPTION NEVER TAKES THE CHAIN ORDER'S PRICE (legacy parity).
1352
+ //
1353
+ // Same rule as adoptChainOrderIntoSlot, stated here because
1354
+ // this path is the one that used to get it wrong: it set
1355
+ // `price: chainOrder.price`, so a slot's id and its price
1356
+ // could disagree — an off-grid order became a grid level and
1357
+ // was then re-emitted as a plan price (S1).
1358
+ //
1359
+ // `...adoptedSlot` below carries the slot's own level and
1360
+ // `price` is deliberately NOT reassigned. A differing
1361
+ // chainOrder.price is expected (a regeneration leaves old
1362
+ // orders resting at old levels) and is not corruption; it
1363
+ // simply is not this slot's price.
1364
+ //
1365
+ // Size differs: size is real state, so the chain value is
1366
+ // adopted. Price is a ladder position; it is not.
1229
1367
  const adoptedOrder = {
1230
1368
  ...adoptedSlot,
1231
1369
  orderId: chainOrderId,
1232
1370
  type: chainOrder.type,
1233
1371
  state: adoptedState,
1234
1372
  size: chainOrder.size,
1235
- price: chainOrder.price,
1236
1373
  rawOnChain: adoptedRaw ? { ...adoptedRaw, fetchedAt: Date.now() } : adoptedRaw,
1237
1374
  ...(adoptedBtsFeeState ? { btsFeeState: adoptedBtsFeeState } : {}),
1238
1375
  };
1376
+ adoptedSlotKeepsItsOwnPrice(mgr, adoptedOrder, chainOrderId, 'legacy-fallback');
1239
1377
  const applied = await mgr._applyOrderUpdate(adoptedOrder, 'sync-pass2-adopt-orphan', { skipAccounting: skipAccounting, fee: 0 });
1240
1378
  if (applied === false) {
1241
1379
  // Fatal rejection: parity with the genesis adoption path —
@@ -1392,7 +1530,7 @@ class SyncEngine {
1392
1530
  }
1393
1531
  else {
1394
1532
  try {
1395
- const residualOrder = await chainOrders.readSingleOrder(matchedGridOrder.orderId, 3000);
1533
+ const residualOrder = await readSingleOrderSeam(mgr, matchedGridOrder.orderId, 3000);
1396
1534
  residualForSale = residualOrder ? toFiniteNumber(residualOrder.for_sale, null) : null;
1397
1535
  }
1398
1536
  catch (residualErr) {
@@ -1538,7 +1676,7 @@ class SyncEngine {
1538
1676
  let chainRefetched = false;
1539
1677
  if (ctx.driftSignal) {
1540
1678
  try {
1541
- const fresh = await chainOrders.readSingleOrder(orderId, 3000);
1679
+ const fresh = await readSingleOrderSeam(mgr, orderId, 3000);
1542
1680
  if (fresh) {
1543
1681
  const freshForSale = toFiniteNumber(fresh.for_sale, null);
1544
1682
  if (freshForSale !== null && Number.isFinite(freshForSale)) {
@@ -1749,7 +1887,7 @@ class SyncEngine {
1749
1887
  const refetchMap = new Map();
1750
1888
  if (driftOrderIds.size > 0) {
1751
1889
  try {
1752
- const batchResults = await chainOrders.batchReadOrders([...driftOrderIds], 3000);
1890
+ const batchResults = await batchReadOrdersSeam(mgr, [...driftOrderIds], 3000);
1753
1891
  for (const [orderId, freshOrder] of batchResults) {
1754
1892
  if (freshOrder) {
1755
1893
  const freshForSale = toFiniteNumber(freshOrder.for_sale, null);
@@ -2049,8 +2187,30 @@ class SyncEngine {
2049
2187
  ? rawType
2050
2188
  : null;
2051
2189
  const descriptorPrice = toFiniteNumber(placedDescriptor?.price, NaN);
2190
+ // Resolve the slot's authority: the GENESIS LEVEL,
2191
+ // not the descriptor. Used for BOTH price and type.
2192
+ // Deriving the type from descriptorPrice (as this
2193
+ // once did) let a corrupt descriptor pick the side
2194
+ // while the price was corrected to the ladder, so a
2195
+ // slot whose level is on the BUY side could
2196
+ // materialize as SELL @ <buy-level> — price and type
2197
+ // disagreeing in one order object.
2198
+ const materializeIdx = parseSlotIndex(gridOrderId);
2199
+ let ladderLevel = null;
2200
+ try {
2201
+ const genesis = mgr._genesis;
2202
+ if (materializeIdx !== null && Array.isArray(genesis?.priceLevels) && genesis.priceLevels.length > 0) {
2203
+ const onLadder = Number(priceForSlot(materializeIdx, genesis));
2204
+ if (Number.isFinite(onLadder) && onLadder > 0)
2205
+ ladderLevel = onLadder;
2206
+ }
2207
+ }
2208
+ catch { /* fall back to the descriptor below */ }
2209
+ // Only fall back to the descriptor when the ladder
2210
+ // cannot speak (migration / unparseable id).
2052
2211
  if (!materializeType && Number.isFinite(descriptorPrice)) {
2053
- materializeType = resolveSpreadOrderSide(descriptorPrice, mgr.config.startPrice);
2212
+ const sidePrice = ladderLevel ?? descriptorPrice;
2213
+ materializeType = resolveSpreadOrderSide(sidePrice, mgr.config.startPrice);
2054
2214
  }
2055
2215
  const descriptorSize = toFiniteNumber(placedDescriptor?.size, NaN);
2056
2216
  const hasDescriptor = !!materializeType
@@ -2065,14 +2225,39 @@ class SyncEngine {
2065
2225
  // slot-scheme metadata are absent. The next
2066
2226
  // readOpenOrders pass repopulates rawOnChain
2067
2227
  // from the live order (slot-scheme ids only).
2228
+ //
2229
+ // PRICE comes from the GENESIS LADDER, not
2230
+ // from the descriptor. The descriptor is
2231
+ // whatever order object the caller carried
2232
+ // in, and writing it here made a slot's price
2233
+ // whichever price that object happened to
2234
+ // hold — the same carried-price-into-
2235
+ // slot.price class as the adoption writer,
2236
+ // and for a slot being materialized at a new
2237
+ // index it can be a price from a different
2238
+ // grid entirely. The descriptor price is kept
2239
+ // only when genesis is unavailable
2240
+ // (migration), where there is no ladder to
2241
+ // derive from.
2242
+ let materializePrice = descriptorPrice;
2243
+ let priceSource = 'descriptor';
2244
+ if (ladderLevel !== null && materializeIdx !== null) {
2245
+ materializePrice = ladderLevel;
2246
+ priceSource = 'genesis';
2247
+ }
2068
2248
  const materializedOrder = {
2069
2249
  id: gridOrderId,
2070
2250
  type: materializeType,
2071
- price: descriptorPrice,
2251
+ price: materializePrice,
2072
2252
  size: descriptorSize,
2073
2253
  state: isPartialPlacement ? ORDER_STATES.PARTIAL : ORDER_STATES.ACTIVE,
2074
2254
  orderId: chainOrderId,
2075
2255
  };
2256
+ if (priceSource === 'descriptor' && materializeIdx !== null) {
2257
+ mgr.logger?.log?.(`[SYNC] createOrder for ${gridOrderId}: materialized with the DESCRIPTOR price ` +
2258
+ `${descriptorPrice} because no genesis ladder was available to derive ` +
2259
+ `the slot level — verify this slot's price on the next sync`, 'warn');
2260
+ }
2076
2261
  if (chainData.deferredFee !== undefined && chainData.deferredFee !== null) {
2077
2262
  materializedOrder.btsFeeState = { deferredFee: Math.max(0, chainData.deferredFee) };
2078
2263
  }
@@ -2081,10 +2266,10 @@ class SyncEngine {
2081
2266
  fee: fee,
2082
2267
  });
2083
2268
  if (materialized === false) {
2084
- mgr.logger?.log?.(`[SYNC] createOrder linkage FAILED: grid order ${gridOrderId} not in master and materializing chain order ${chainOrderId} (${materializeType} @${descriptorPrice} x${descriptorSize}) was rejected — chain order left untracked`, 'error');
2269
+ mgr.logger?.log?.(`[SYNC] createOrder linkage FAILED: grid order ${gridOrderId} not in master and materializing chain order ${chainOrderId} (${materializeType} @${materializePrice} x${descriptorSize}) was rejected — chain order left untracked`, 'error');
2085
2270
  }
2086
2271
  else {
2087
- mgr.logger?.log?.(`[SYNC] createOrder for unknown grid order ${gridOrderId}: materialized ${materializeType} @${descriptorPrice} x${descriptorSize} -> ${chainOrderId} (master lost the slot mid-broadcast)`, 'warn');
2272
+ mgr.logger?.log?.(`[SYNC] createOrder for unknown grid order ${gridOrderId}: materialized ${materializeType} @${materializePrice} x${descriptorSize} (price source: ${priceSource}) -> ${chainOrderId} (master lost the slot mid-broadcast)`, 'warn');
2088
2273
  }
2089
2274
  }
2090
2275
  }