dexbot 1.2.2 → 1.2.4
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/dist/credential-daemon.d.ts.map +1 -1
- package/dist/credential-daemon.js +102 -6
- package/dist/credential-daemon.js.map +1 -1
- package/dist/modules/bitshares-native/index.d.ts.map +1 -1
- package/dist/modules/bitshares-native/index.js +2 -1
- package/dist/modules/bitshares-native/index.js.map +1 -1
- package/dist/modules/bitshares-native/signing_client.d.ts +2 -0
- package/dist/modules/bitshares-native/signing_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/signing_client.js +30 -12
- package/dist/modules/bitshares-native/signing_client.js.map +1 -1
- package/dist/modules/credential_policy.js +2 -2
- package/dist/modules/credential_policy.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +22 -7
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +199 -109
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +9 -0
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/order/grid.d.ts +1 -0
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +124 -5
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +6 -3
- package/dist/modules/order/utils/order.js.map +1 -1
- package/package.json +1 -1
|
@@ -596,6 +596,42 @@ class DEXBot {
|
|
|
596
596
|
}
|
|
597
597
|
this.manager.logger.log(`[RECOVERY] Grid reloaded from persisted snapshot: ${this.manager.orders.size} orders, ` +
|
|
598
598
|
`${chainOpenOrders.length} on-chain orders synced`, 'info');
|
|
599
|
+
// Gap 2: Check for unmatched chain orders after reload + sync.
|
|
600
|
+
// If the sync resolved all unmatched entries, _lastUnmatchedChainOrders
|
|
601
|
+
// was cleared by the sync engine. If any remain, the persisted snapshot
|
|
602
|
+
// produced an inconsistent grid — reject so the structural resync
|
|
603
|
+
// falls through to requestGridReset (full rebuild from chain).
|
|
604
|
+
const remainingUnmatched = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
605
|
+
? this.manager._lastUnmatchedChainOrders
|
|
606
|
+
: [];
|
|
607
|
+
if (remainingUnmatched.length > 0) {
|
|
608
|
+
const sample = remainingUnmatched.slice(0, 3)
|
|
609
|
+
.map(o => this._formatUnmatchedChainOrderForLog(o))
|
|
610
|
+
.join(' | ');
|
|
611
|
+
this.manager.logger.log(`[RECOVERY] Persisted grid reloaded but ${remainingUnmatched.length} unmatched chain order(s) ` +
|
|
612
|
+
`remain${sample ? ` (${sample})` : ''}. Rejecting — full grid reset required.`, 'warn');
|
|
613
|
+
return { success: false, reason: `grid inconsistent after reload: ${remainingUnmatched.length} unmatched remain` };
|
|
614
|
+
}
|
|
615
|
+
// If the reloaded grid is still bloated, reject recovery so the
|
|
616
|
+
// caller falls through to a full grid reset (requestGridReset).
|
|
617
|
+
// Without this check, a bloated snapshot gets accepted as "success"
|
|
618
|
+
// and the structural-resync loop loads the same broken state forever.
|
|
619
|
+
//
|
|
620
|
+
// NOTE: loadGrid() already fires requestStructuralGridResync when it
|
|
621
|
+
// detects bloat internally, so the inner async resync may be in flight
|
|
622
|
+
// by the time this outer check runs. That's fine — the structural-resync
|
|
623
|
+
// gate (_structuralGridResyncRunning / _structuralGridResyncTimer) dedup's
|
|
624
|
+
// concurrent requests. This outer check exists so the synchronous return
|
|
625
|
+
// value is honest about the state; the inner resync is a safety net.
|
|
626
|
+
const { isGridBloated } = require('./order/grid');
|
|
627
|
+
const ordersArr = Array.from(this.manager.orders.values());
|
|
628
|
+
const bloatPostRecovery = isGridBloated(this.manager, ordersArr);
|
|
629
|
+
if (bloatPostRecovery.bloated) {
|
|
630
|
+
const d = bloatPostRecovery.details;
|
|
631
|
+
this.manager.logger.log(`[RECOVERY] Persisted grid reloaded but still bloated ` +
|
|
632
|
+
`(${d.gridSize} slots, max ${d.maxAllowed}). Rejecting — full grid reset required.`, 'warn');
|
|
633
|
+
return { success: false, reason: 'grid still bloated after reload' };
|
|
634
|
+
}
|
|
599
635
|
return { success: true };
|
|
600
636
|
}
|
|
601
637
|
catch (err) {
|
|
@@ -2794,6 +2830,45 @@ class DEXBot {
|
|
|
2794
2830
|
this.manager.logger.log(`[COW][UNCERTAIN] Discarded planned CREATEs (no chain match after ${recheckRound} recheck(s)): ${discarded
|
|
2795
2831
|
.map(d => d.slotId)
|
|
2796
2832
|
.join(', ')}`, 'warn');
|
|
2833
|
+
// Restore target grid sizes for discarded CREATEs so the slots are
|
|
2834
|
+
// not left as virtual/0 after recovery. Without this, the slot stays
|
|
2835
|
+
// at size 0 (set by the fill handler) and is never reactivated until
|
|
2836
|
+
// a fresh fill cycle triggers another rebalance that happens to succeed.
|
|
2837
|
+
// entry.order is the pending-broadcast target order (captured at broadcast
|
|
2838
|
+
// time); its .id matches entry.slotId — the lookup below keys on slotId.
|
|
2839
|
+
for (const entry of discarded) {
|
|
2840
|
+
if (entry.order && entry.slotId) {
|
|
2841
|
+
const current = this.manager.orders.get(entry.slotId);
|
|
2842
|
+
if (current && current.state === ORDER_STATES.VIRTUAL && !current.orderId) {
|
|
2843
|
+
try {
|
|
2844
|
+
await this.manager._applyOrderUpdate({ ...entry.order, state: ORDER_STATES.VIRTUAL, orderId: null }, 'uncertain-recovery-restore-size', { skipAccounting: true, fee: 0 });
|
|
2845
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Restored target size for discarded CREATE slot ${entry.slotId} (size: ${entry.order.size})`, 'info');
|
|
2846
|
+
}
|
|
2847
|
+
catch (restoreErr) {
|
|
2848
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Failed to restore size for slot ${entry.slotId}: ${restoreErr?.message || restoreErr}`, 'warn');
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
// ---- Structural resync safeguard after skipAccounting restore ----
|
|
2855
|
+
// The discarded CREATE restore above used skipAccounting: true to avoid
|
|
2856
|
+
// double-counting when the structural resync recalculates accounting
|
|
2857
|
+
// from scratch. Ensure one is scheduled — if already in-flight (timer
|
|
2858
|
+
// or running), the existing resync will handle the accounting fix.
|
|
2859
|
+
if (discarded.length > 0 && typeof this.manager?.requestStructuralGridResync === 'function') {
|
|
2860
|
+
const alreadyScheduled = this._structuralGridResyncRunning || this._structuralGridResyncTimer;
|
|
2861
|
+
if (!alreadyScheduled) {
|
|
2862
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Scheduling structural resync after discarded CREATE restore ` +
|
|
2863
|
+
`(skipAccounting used — resync needed for fund recalculation)`, 'info');
|
|
2864
|
+
this.manager.requestStructuralGridResync('cow-uncertain-accounting-repair', { discardedCount: discarded.length }).catch((err) => {
|
|
2865
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Failed to schedule accounting repair resync: ${err.message}`, 'error');
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
else {
|
|
2869
|
+
this.manager.logger.log(`[COW][UNCERTAIN] Structural resync already ${this._structuralGridResyncRunning ? 'running' : 'scheduled'}; ` +
|
|
2870
|
+
`it will repair accounting after discarded CREATE restore`, 'debug');
|
|
2871
|
+
}
|
|
2797
2872
|
}
|
|
2798
2873
|
this._clearPendingBroadcasts();
|
|
2799
2874
|
// Post-recovery safety net: if there are still unmatched chain
|
|
@@ -2861,18 +2936,22 @@ class DEXBot {
|
|
|
2861
2936
|
return { executed: false, hadRotation, uncertain: true, adopted, discarded };
|
|
2862
2937
|
}
|
|
2863
2938
|
/**
|
|
2864
|
-
* Auto-cancel a
|
|
2939
|
+
* Auto-cancel a price-drift orphan from the unmatched-order snapshot.
|
|
2940
|
+
*
|
|
2941
|
+
* Only cancels entries with reason === 'price-drift-orphan' — these are
|
|
2942
|
+
* surplus orders that drifted away from their slot price and have no
|
|
2943
|
+
* adoptable grid slot. All other unmatched orders (duplicate-price-level,
|
|
2944
|
+
* already-matched-slot, etc.) are adoptable positions that the structural
|
|
2945
|
+
* resync will integrate into the grid; cancelling them destroys capital.
|
|
2865
2946
|
*
|
|
2866
2947
|
* This is the post-recovery safety net: if, after
|
|
2867
|
-
* _reconcileAfterUncertainBroadcast runs, there are still
|
|
2868
|
-
*
|
|
2869
|
-
*
|
|
2870
|
-
* them per call. Per-cycle cap = 1 — the next cycle will pick up the
|
|
2871
|
-
* next unmatched order if more remain.
|
|
2948
|
+
* _reconcileAfterUncertainBroadcast runs, there are still price-drift
|
|
2949
|
+
* orphans, cancel ONE per cycle. Per-cycle cap = 1 (or 5 in recovery mode)
|
|
2950
|
+
* — the next cycle will pick up the next orphan if more remain.
|
|
2872
2951
|
*
|
|
2873
2952
|
* Safety conditions (ALL must hold):
|
|
2874
2953
|
* 1. _pendingBroadcasts is empty (no in-flight recovery)
|
|
2875
|
-
* 2. _lastUnmatchedChainOrders
|
|
2954
|
+
* 2. _lastUnmatchedChainOrders contains at least one price-drift-orphan
|
|
2876
2955
|
* 3. The current cycle has not already auto-cancelled an orphan
|
|
2877
2956
|
* (tracked via this._autoCancelOrphanCycleMarker)
|
|
2878
2957
|
*
|
|
@@ -2908,17 +2987,27 @@ class DEXBot {
|
|
|
2908
2987
|
if (unmatched.length === 0) {
|
|
2909
2988
|
return { cancelled: false, reason: 'no-unmatched' };
|
|
2910
2989
|
}
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2990
|
+
// Check for fingerprinted entries first — these came from a pending
|
|
2991
|
+
// broadcast (missing-create-result) and must be handled by the recovery
|
|
2992
|
+
// path, not by auto-cancel. The first fingerprinted entry is checked
|
|
2993
|
+
// regardless of its reason field.
|
|
2994
|
+
const fingerprinted = unmatched.find(u => u && u.fingerprint);
|
|
2995
|
+
if (fingerprinted) {
|
|
2996
|
+
return { cancelled: false, reason: 'fingerprinted-handle-via-recovery' };
|
|
2997
|
+
}
|
|
2998
|
+
// Only cancel price-drift orphans — these are surplus orders that
|
|
2999
|
+
// drifted away from their assigned slot price and have no adoptable
|
|
3000
|
+
// grid slot. All other unmatched orders (duplicate-price-level,
|
|
3001
|
+
// already-matched-slot, etc.) are adoptable positions that the structural
|
|
3002
|
+
// resync will integrate into the grid — cancelling them destroys capital.
|
|
3003
|
+
const target = unmatched.find(u => u && u.reason === 'price-drift-orphan');
|
|
3004
|
+
if (!target) {
|
|
3005
|
+
return { cancelled: false, reason: 'no-price-drift-orphan', message: 'no price-drift orphan to cancel; other unmatched orders are adoptable' };
|
|
3006
|
+
}
|
|
3007
|
+
const orderId = target.id || target.orderId || target.chainOrderId;
|
|
2914
3008
|
if (!orderId) {
|
|
2915
3009
|
return { cancelled: false, reason: 'no-orderId' };
|
|
2916
3010
|
}
|
|
2917
|
-
if (target?.fingerprint) {
|
|
2918
|
-
// Fingerprinted unmatched orders came from a pending broadcast.
|
|
2919
|
-
// The recovery path is the right place to handle them, not here.
|
|
2920
|
-
return { cancelled: false, reason: 'fingerprinted-handle-via-recovery' };
|
|
2921
|
-
}
|
|
2922
3011
|
if (!chainOrders?.cancelOrder) {
|
|
2923
3012
|
return { cancelled: false, reason: 'cancelOrder-unavailable' };
|
|
2924
3013
|
}
|
|
@@ -3776,111 +3865,112 @@ class DEXBot {
|
|
|
3776
3865
|
? Array.from(this.manager._pendingBroadcasts.values())
|
|
3777
3866
|
: [];
|
|
3778
3867
|
if (hasCreateActions && (unmatchedChainOrders.length > 0 || pendingBroadcasts.length > 0)) {
|
|
3779
|
-
// ----
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
let cancelledUnmatched = 0;
|
|
3784
|
-
if (unmatchedChainOrders.length > 0 && pendingBroadcasts.length === 0) {
|
|
3785
|
-
// Bounded-parallel cancellation: process unmatched orders in
|
|
3786
|
-
// concurrent batches (3 at a time) to avoid serialising 19+
|
|
3787
|
-
// individual cancel txs (~0.5s each = 10s batch hold).
|
|
3788
|
-
const CANCEL_CONCURRENCY = 3;
|
|
3789
|
-
const cancelable = unmatchedChainOrders.filter(u => {
|
|
3790
|
-
const oid = u?.id || u?.orderId || u?.chainOrderId;
|
|
3791
|
-
return Boolean(oid) && !u?.fingerprint;
|
|
3792
|
-
});
|
|
3793
|
-
for (let i = 0; i < cancelable.length; i += CANCEL_CONCURRENCY) {
|
|
3794
|
-
const batch = cancelable.slice(i, i + CANCEL_CONCURRENCY);
|
|
3795
|
-
const results = await Promise.allSettled(batch.map(async (unmatched) => {
|
|
3796
|
-
const orderId = unmatched.id || unmatched.orderId || unmatched.chainOrderId;
|
|
3797
|
-
this.manager.logger.log(`[COW] Auto-cancelling unmatched chain order ${orderId} ` +
|
|
3798
|
-
`(${this._formatUnmatchedChainOrderForLog(unmatched)}) to unblock CREATE batch.`, 'warn');
|
|
3799
|
-
await chainOrders.cancelOrder(this.account, this.privateKey, orderId);
|
|
3800
|
-
if (typeof chainOrders.recordOwnCancel === 'function') {
|
|
3801
|
-
chainOrders.recordOwnCancel(orderId);
|
|
3802
|
-
}
|
|
3803
|
-
}));
|
|
3804
|
-
for (const r of results) {
|
|
3805
|
-
if (r.status === 'fulfilled')
|
|
3806
|
-
cancelledUnmatched++;
|
|
3807
|
-
else
|
|
3808
|
-
this.manager.logger.log(`[COW] Failed to cancel unmatched chain order: ${r.reason?.message || r.reason}`, 'warn');
|
|
3809
|
-
}
|
|
3810
|
-
}
|
|
3811
|
-
if (cancelable.length > 0 && cancelledUnmatched >= cancelable.length) {
|
|
3812
|
-
this.manager.logger.log(`[COW] Cancelled all ${cancelledUnmatched} unmatched chain order(s); proceeding with CREATE batch.`, 'warn');
|
|
3813
|
-
// Optimistic clear: the cancel txs are sent but not yet
|
|
3814
|
-
// confirmed. If a cancel silently fails on chain, the
|
|
3815
|
-
// next sync cycle will re-detect the unmatched order and
|
|
3816
|
-
// re-enter this guard — that's safe because the cancel
|
|
3817
|
-
// is idempotent and the re-detection is non-blocking.
|
|
3818
|
-
this.manager._lastUnmatchedChainOrders = [];
|
|
3819
|
-
}
|
|
3820
|
-
else if (cancelable.length > 0) {
|
|
3821
|
-
this.manager.logger.log(`[COW] Cancelled ${cancelledUnmatched}/${cancelable.length} unmatched chain order(s); ` +
|
|
3822
|
-
`remaining must be handled by structural reconciliation.`, 'warn');
|
|
3823
|
-
}
|
|
3824
|
-
}
|
|
3825
|
-
// Re-check after auto-cancel attempt
|
|
3826
|
-
const remainingUnmatched = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
3827
|
-
? this.manager._lastUnmatchedChainOrders
|
|
3828
|
-
: [];
|
|
3829
|
-
if (remainingUnmatched.length > 0 || pendingBroadcasts.length > 0) {
|
|
3830
|
-
const blockers = [];
|
|
3831
|
-
if (remainingUnmatched.length > 0)
|
|
3832
|
-
blockers.push(`${remainingUnmatched.length} unmatched chain order(s)`);
|
|
3833
|
-
if (pendingBroadcasts.length > 0)
|
|
3834
|
-
blockers.push(`${pendingBroadcasts.length} pending broadcast(s)`);
|
|
3835
|
-
const reasonText = blockers.join(' and ');
|
|
3836
|
-
if (pendingBroadcasts.length > 0) {
|
|
3837
|
-
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${reasonText} from a prior uncertain ` +
|
|
3838
|
-
`broadcast. Running recovery before placing replacement orders.`, 'error');
|
|
3839
|
-
}
|
|
3840
|
-
else {
|
|
3841
|
-
const sample = remainingUnmatched
|
|
3842
|
-
.slice(0, 3)
|
|
3843
|
-
.map(order => this._formatUnmatchedChainOrderForLog(order))
|
|
3844
|
-
.join(' | ');
|
|
3845
|
-
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${reasonText} ` +
|
|
3846
|
-
`are not represented in the grid${sample ? ` (${sample})` : ''}. ` +
|
|
3847
|
-
`Run structural reconciliation before placing replacement orders.`, 'error');
|
|
3848
|
-
}
|
|
3868
|
+
// ---- Handle pending broadcasts first ----
|
|
3869
|
+
if (pendingBroadcasts.length > 0) {
|
|
3870
|
+
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${pendingBroadcasts.length} pending broadcast(s) from a prior uncertain ` +
|
|
3871
|
+
`broadcast. Running recovery before placing replacement orders.`, 'error');
|
|
3849
3872
|
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
3850
3873
|
if (this.manager._recoveryState)
|
|
3851
3874
|
this.manager._recoveryState.structuralResyncRequested = true;
|
|
3852
|
-
await this.manager.requestStructuralGridResync(pendingBroadcasts.
|
|
3853
|
-
? 'pending broadcasts before COW create'
|
|
3854
|
-
: 'unmatched chain orders before COW create', pendingBroadcasts.length > 0
|
|
3855
|
-
? { pendingBroadcasts: pendingBroadcasts.map(p => p.slotId) }
|
|
3856
|
-
: { unmatchedChainOrders: remainingUnmatched });
|
|
3875
|
+
await this.manager.requestStructuralGridResync('pending broadcasts before COW create', { pendingBroadcasts: pendingBroadcasts.map(p => p.slotId) });
|
|
3857
3876
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
}
|
|
3870
|
-
catch (recoverErr) {
|
|
3871
|
-
this.manager.logger.log(`[COW] Recovery from pending broadcasts failed: ${recoverErr?.message || recoverErr}`, 'error');
|
|
3872
|
-
}
|
|
3877
|
+
try {
|
|
3878
|
+
await this._reconcileAfterUncertainBroadcast(new BroadcastUncertainError('rejected CREATE batch had pending broadcasts', {
|
|
3879
|
+
operations: pendingBroadcasts.map(p => p.order),
|
|
3880
|
+
accountName: this.account,
|
|
3881
|
+
batchId: this._currentBatchId || null,
|
|
3882
|
+
payload: null,
|
|
3883
|
+
timeoutMs: null
|
|
3884
|
+
}), []);
|
|
3885
|
+
}
|
|
3886
|
+
catch (recoverErr) {
|
|
3887
|
+
this.manager.logger.log(`[COW] Recovery from pending broadcasts failed: ${recoverErr?.message || recoverErr}`, 'error');
|
|
3873
3888
|
}
|
|
3874
3889
|
return {
|
|
3875
3890
|
executed: false,
|
|
3876
3891
|
aborted: true,
|
|
3877
|
-
reason:
|
|
3878
|
-
unmatchedChainOrders: pendingBroadcasts.length > 0 ? [] : remainingUnmatched,
|
|
3879
|
-
pendingBroadcasts: pendingBroadcasts.map(p => p.slotId),
|
|
3892
|
+
reason: 'PENDING_BROADCASTS',
|
|
3880
3893
|
hadRotation: false
|
|
3881
3894
|
};
|
|
3882
3895
|
}
|
|
3883
|
-
//
|
|
3896
|
+
// ---- Unmatched orders: adopt instead of cancel ----
|
|
3897
|
+
// Unmatched orders are legitimate on-chain positions the grid
|
|
3898
|
+
// hasn't adopted yet. Cancelling them destroys capital and
|
|
3899
|
+
// creates gaps in the order book. Instead, re-sync to adopt
|
|
3900
|
+
// them into the grid, then reject (the sync invalidated the
|
|
3901
|
+
// working grid, so the existing COW plan is stale).
|
|
3902
|
+
// NOTE: syncFromOpenOrders may be a partial no-op under certain
|
|
3903
|
+
// lock conditions (e.g. _fillProcessingLock + !isReentrant).
|
|
3904
|
+
// Structural resync scheduled below handles adoption regardless.
|
|
3905
|
+
const unmatchedSample = unmatchedChainOrders
|
|
3906
|
+
.slice(0, 3)
|
|
3907
|
+
.map(o => this._formatUnmatchedChainOrderForLog(o))
|
|
3908
|
+
.join(' | ');
|
|
3909
|
+
this.manager.logger.log(`[COW] ${unmatchedChainOrders.length} unmatched chain order(s) blocking CREATES ` +
|
|
3910
|
+
(unmatchedSample ? `(${unmatchedSample})` : '') +
|
|
3911
|
+
` — adopting via sync instead of cancelling`, 'info');
|
|
3912
|
+
try {
|
|
3913
|
+
const accountRef = this.account;
|
|
3914
|
+
const freshSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
3915
|
+
if (freshSnapshot && freshSnapshot.length > 0) {
|
|
3916
|
+
const syncResult = await this.manager.syncFromOpenOrders(freshSnapshot, {
|
|
3917
|
+
skipAccounting: true,
|
|
3918
|
+
});
|
|
3919
|
+
// Only overwrite _lastUnmatchedChainOrders when sync actually
|
|
3920
|
+
// processed orders (filled + updated + corrected > 0).
|
|
3921
|
+
// Force-release and lock-contention early-exit paths return
|
|
3922
|
+
// empty arrays without touching _lastUnmatchedChainOrders —
|
|
3923
|
+
// overwriting with [] would drop stale unmatched entries.
|
|
3924
|
+
if (syncResult && Array.isArray(syncResult.unmatchedChainOrders)) {
|
|
3925
|
+
const processed = (syncResult.filledOrders?.length || 0) +
|
|
3926
|
+
(syncResult.updatedOrders?.length || 0) +
|
|
3927
|
+
(syncResult.ordersNeedingCorrection?.length || 0);
|
|
3928
|
+
if (processed > 0) {
|
|
3929
|
+
this.manager._lastUnmatchedChainOrders = syncResult.unmatchedChainOrders;
|
|
3930
|
+
this.manager.logger.log(`[COW] Adopted chain order(s) via sync: ${processed} processed, ` +
|
|
3931
|
+
`${syncResult.unmatchedChainOrders.length} still unmatched`, 'info');
|
|
3932
|
+
}
|
|
3933
|
+
else {
|
|
3934
|
+
// processed === 0 with unmatched still present means the
|
|
3935
|
+
// sync could not adopt the unmatched chain orders. This is
|
|
3936
|
+
// normal when called re-entrantly (inside _fillProcessingLock):
|
|
3937
|
+
// the sync engine runs inline and either timed out (unlikely
|
|
3938
|
+
// for a re-entrant call) or all chain orders were already
|
|
3939
|
+
// matched — leaving only stale unmatched entries.
|
|
3940
|
+
const syncUnmatchedCount = syncResult.unmatchedChainOrders.length;
|
|
3941
|
+
this.manager.logger.log(`[COW] Sync returned without processing (processed=0, ` +
|
|
3942
|
+
`unmatched=${syncUnmatchedCount} in result, ` +
|
|
3943
|
+
`_lastUnmatchedChainOrders=${unmatchedChainOrders.length}). ` +
|
|
3944
|
+
`Structural resync will handle adoption.`, syncUnmatchedCount > 0 ? 'warn' : 'debug');
|
|
3945
|
+
// If the sync result itself has unmatched entries that
|
|
3946
|
+
// differ from _lastUnmatchedChainOrders, adopt them now
|
|
3947
|
+
// so the stale tracker is more accurate for the resync.
|
|
3948
|
+
if (syncUnmatchedCount > 0 && syncUnmatchedCount !== unmatchedChainOrders.length) {
|
|
3949
|
+
this.manager._lastUnmatchedChainOrders = syncResult.unmatchedChainOrders.map((o) => ({ ...o }));
|
|
3950
|
+
this.manager.logger.log(`[COW] Updated _lastUnmatchedChainOrders from sync result: ` +
|
|
3951
|
+
`${unmatchedChainOrders.length} → ${syncUnmatchedCount}`, 'debug');
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
catch (syncErr) {
|
|
3958
|
+
this.manager.logger.log(`[COW] Failed to sync/unmatched orders: ${syncErr?.message || syncErr}`, 'warn');
|
|
3959
|
+
}
|
|
3960
|
+
// Working grid is stale after master grid mutation from sync.
|
|
3961
|
+
// Request structural resync to rebuild the grid on the next cycle.
|
|
3962
|
+
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
3963
|
+
if (this.manager._recoveryState)
|
|
3964
|
+
this.manager._recoveryState.structuralResyncRequested = true;
|
|
3965
|
+
await this.manager.requestStructuralGridResync('unmatched chain orders before COW create', { unmatchedChainOrders: unmatchedChainOrders });
|
|
3966
|
+
}
|
|
3967
|
+
this.manager.logger.log(`[COW] Rejecting CREATE batch after sync: working grid invalidated by master mutation`, 'info');
|
|
3968
|
+
return {
|
|
3969
|
+
executed: false,
|
|
3970
|
+
aborted: true,
|
|
3971
|
+
reason: 'UNMATCHED_CHAIN_ORDERS',
|
|
3972
|
+
hadRotation: false
|
|
3973
|
+
};
|
|
3884
3974
|
}
|
|
3885
3975
|
const { assetA, assetB } = this.manager.assets;
|
|
3886
3976
|
const operations = [];
|