@silentswap/react 0.0.86 → 0.0.87
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.
|
@@ -6,6 +6,10 @@ import { usePlatformHealthContext } from '../hooks/usePlatformHealth.js';
|
|
|
6
6
|
import { usePricesContext } from './PricesContext.js';
|
|
7
7
|
import { getAssetByCaip19, CALCULATION_DIRECTION_INPUT_TO_OUTPUT, CALCULATION_DIRECTION_OUTPUT_TO_INPUT, } from '@silentswap/sdk';
|
|
8
8
|
const XT_DELAY_DEBOUNCE_QUOTE = 550;
|
|
9
|
+
// After a reverse (output-to-input) calculation completes, ignore re-trigger attempts
|
|
10
|
+
// for this duration. Bridge quotes aren't perfectly deterministic, so the calculated
|
|
11
|
+
// input oscillates slightly each cycle — the cooldown prevents infinite loops.
|
|
12
|
+
const REVERSE_CALC_SETTLE_MS = 2000;
|
|
9
13
|
const SwapFormEstimatesContext = createContext(undefined);
|
|
10
14
|
export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, serviceFeeRate: serviceFeeRateOverride, }) {
|
|
11
15
|
const setSplits = useSwap((state) => state.setSplits);
|
|
@@ -24,6 +28,7 @@ export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, s
|
|
|
24
28
|
const programmaticInputChangeRef = useRef(false);
|
|
25
29
|
const shouldFetchAfterInputRecalcRef = useRef(false);
|
|
26
30
|
const isFetchingEstimatesRef = useRef(false);
|
|
31
|
+
const reverseCalcTimestampRef = useRef(0);
|
|
27
32
|
const [calculationDirection, setCalculationDirection] = React.useState(CALCULATION_DIRECTION_INPUT_TO_OUTPUT);
|
|
28
33
|
const calculationDirectionRef = useRef(CALCULATION_DIRECTION_INPUT_TO_OUTPUT);
|
|
29
34
|
// Refs for destination amount calculation (from useDestinationAmountCalculation)
|
|
@@ -37,6 +42,14 @@ export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, s
|
|
|
37
42
|
if (isDraggingSlider) {
|
|
38
43
|
return;
|
|
39
44
|
}
|
|
45
|
+
console.log('[SwapFormEstimates] scheduleFetchEstimates called', {
|
|
46
|
+
immediate,
|
|
47
|
+
direction,
|
|
48
|
+
effectiveDirection: direction || calculationDirectionRef.current || CALCULATION_DIRECTION_INPUT_TO_OUTPUT,
|
|
49
|
+
isFetching: isFetchingEstimatesRef.current,
|
|
50
|
+
cooldownMs: Date.now() - reverseCalcTimestampRef.current,
|
|
51
|
+
caller: new Error().stack?.split('\n')[2]?.trim(),
|
|
52
|
+
});
|
|
40
53
|
if (debounceTimerRef.current) {
|
|
41
54
|
clearTimeout(debounceTimerRef.current);
|
|
42
55
|
debounceTimerRef.current = null;
|
|
@@ -47,6 +60,12 @@ export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, s
|
|
|
47
60
|
calculationDirectionRef.current = effectiveDirection;
|
|
48
61
|
setCalculationDirection(effectiveDirection);
|
|
49
62
|
}
|
|
63
|
+
// Set the cooldown timestamp at the START of a reverse calc (not just in .finally())
|
|
64
|
+
// so that effects firing DURING the fetch (e.g. ref-change effect triggered by
|
|
65
|
+
// SilentSwapAppProvider re-renders) are already within the cooldown window.
|
|
66
|
+
if (effectiveDirection === CALCULATION_DIRECTION_OUTPUT_TO_INPUT) {
|
|
67
|
+
reverseCalcTimestampRef.current = Date.now();
|
|
68
|
+
}
|
|
50
69
|
isFetchingEstimatesRef.current = true;
|
|
51
70
|
const fetchPromise = immediate
|
|
52
71
|
? fetchEstimatesRef.current(effectiveDirection)
|
|
@@ -67,6 +86,13 @@ export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, s
|
|
|
67
86
|
console.error('Failed to fetch estimates:', err);
|
|
68
87
|
})
|
|
69
88
|
.finally(() => {
|
|
89
|
+
// When a reverse (output-to-input) calculation completes, it updates inputAmount
|
|
90
|
+
// programmatically. Record the timestamp so ALL re-trigger paths (initial-fetch
|
|
91
|
+
// effect AND ref-change effect) will skip during the cooldown window. This prevents
|
|
92
|
+
// infinite loops caused by non-deterministic bridge quotes oscillating slightly.
|
|
93
|
+
if (effectiveDirection === CALCULATION_DIRECTION_OUTPUT_TO_INPUT) {
|
|
94
|
+
reverseCalcTimestampRef.current = Date.now();
|
|
95
|
+
}
|
|
70
96
|
isFetchingEstimatesRef.current = false;
|
|
71
97
|
});
|
|
72
98
|
}, [isDraggingSlider]);
|
|
@@ -77,8 +103,25 @@ export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, s
|
|
|
77
103
|
React.useEffect(() => {
|
|
78
104
|
const prev = fetchEstimatesRef.current;
|
|
79
105
|
fetchEstimatesRef.current = fetchEstimates;
|
|
80
|
-
// When fetchEstimates changes and we had already fetched, re-trigger
|
|
106
|
+
// When fetchEstimates changes and we had already fetched, re-trigger —
|
|
107
|
+
// but NOT if we're currently fetching (avoids scheduling a duplicate) or
|
|
108
|
+
// if a reverse calc is in progress / recently completed (SilentSwapAppProvider
|
|
109
|
+
// re-renders frequently during fetches, giving fetchEstimates a new identity).
|
|
81
110
|
if (prev !== fetchEstimates && lastFetchedKeyRef.current) {
|
|
111
|
+
const cooldownMs = Date.now() - reverseCalcTimestampRef.current;
|
|
112
|
+
if (isFetchingEstimatesRef.current ||
|
|
113
|
+
cooldownMs < REVERSE_CALC_SETTLE_MS) {
|
|
114
|
+
console.log('[SwapFormEstimates] ref-change effect SKIPPED (loop guard)', {
|
|
115
|
+
isFetching: isFetchingEstimatesRef.current,
|
|
116
|
+
cooldownMs,
|
|
117
|
+
direction: calculationDirectionRef.current,
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
console.log('[SwapFormEstimates] ref-change effect TRIGGERING fetch', {
|
|
122
|
+
cooldownMs,
|
|
123
|
+
direction: calculationDirectionRef.current,
|
|
124
|
+
});
|
|
82
125
|
lastFetchedKeyRef.current = null;
|
|
83
126
|
scheduleFetchEstimates(true);
|
|
84
127
|
}
|
|
@@ -220,10 +263,12 @@ export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, s
|
|
|
220
263
|
}, [tokenIn, setSplits, setInputAmount, getPrice, getServiceFeeRate]);
|
|
221
264
|
// Handle slider change (splits changed)
|
|
222
265
|
const handleSliderChange = useCallback(() => {
|
|
266
|
+
reverseCalcTimestampRef.current = 0;
|
|
223
267
|
scheduleFetchEstimates();
|
|
224
268
|
}, [scheduleFetchEstimates]);
|
|
225
269
|
// Handle destination amount change (user manually edited)
|
|
226
270
|
const handleDestinationAmountChange = useCallback(() => {
|
|
271
|
+
reverseCalcTimestampRef.current = 0;
|
|
227
272
|
// Mark that this is a manual destination change
|
|
228
273
|
// This prevents performDestinationAmountCalculation from overwriting user's changes
|
|
229
274
|
isManualDestinationChangeRef.current = true;
|
|
@@ -240,6 +285,7 @@ export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, s
|
|
|
240
285
|
}, [recalculateSplitsFromAmounts, scheduleFetchEstimates]);
|
|
241
286
|
// Handle input amount change (user manually edited)
|
|
242
287
|
const handleInputAmountChange = useCallback(() => {
|
|
288
|
+
reverseCalcTimestampRef.current = 0;
|
|
243
289
|
// Mark as user-initiated change (not programmatic)
|
|
244
290
|
programmaticInputChangeRef.current = false;
|
|
245
291
|
shouldFetchAfterInputRecalcRef.current = false;
|
|
@@ -345,6 +391,14 @@ export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, s
|
|
|
345
391
|
if (programmaticInputChangeRef.current) {
|
|
346
392
|
return;
|
|
347
393
|
}
|
|
394
|
+
// Skip recalculation during the reverse-calc cooldown window.
|
|
395
|
+
// A reverse calc just set inputAmount programmatically — recalculating
|
|
396
|
+
// destination amounts from this input would overwrite the user's desired
|
|
397
|
+
// output values and could feed back into another reverse cycle.
|
|
398
|
+
if (Date.now() - reverseCalcTimestampRef.current < REVERSE_CALC_SETTLE_MS) {
|
|
399
|
+
console.log('[SwapFormEstimates] dest-calc effect SKIPPED (reverse calc cooldown)');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
348
402
|
const splitsKey = splits.join(',');
|
|
349
403
|
const hasSplitsChanged = prevSplitsRef.current !== splitsKey;
|
|
350
404
|
const hasInputAmountChanged = prevInputAmountForCalcRef.current !== inputAmount;
|
|
@@ -414,14 +468,28 @@ export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, s
|
|
|
414
468
|
useEffect(() => {
|
|
415
469
|
// Skip if we're already fetching to prevent loops
|
|
416
470
|
if (isFetchingEstimatesRef.current) {
|
|
471
|
+
console.log('[SwapFormEstimates] initial-fetch effect SKIPPED (already fetching)', { estimateKey });
|
|
417
472
|
// Update lastFetchedKeyRef to the new key to prevent re-triggering when fetch completes
|
|
418
473
|
if (estimateKey) {
|
|
419
474
|
lastFetchedKeyRef.current = estimateKey;
|
|
420
475
|
}
|
|
421
476
|
return;
|
|
422
477
|
}
|
|
478
|
+
// After a reverse calculation (output-to-input) completes, it programmatically
|
|
479
|
+
// updates inputAmount which changes estimateKey. Skip re-fetching during the
|
|
480
|
+
// cooldown window to avoid an infinite loop (bridge quotes aren't perfectly
|
|
481
|
+
// deterministic, so the calculated input oscillates slightly each cycle).
|
|
482
|
+
const cooldownMs = Date.now() - reverseCalcTimestampRef.current;
|
|
483
|
+
if (cooldownMs < REVERSE_CALC_SETTLE_MS) {
|
|
484
|
+
console.log('[SwapFormEstimates] initial-fetch effect SKIPPED (reverse calc cooldown)', { cooldownMs, estimateKey });
|
|
485
|
+
if (estimateKey) {
|
|
486
|
+
lastFetchedKeyRef.current = estimateKey;
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
423
490
|
// Only fetch if we have a valid estimate key and it's different from the last one we fetched
|
|
424
491
|
if (estimateKey && estimateKey !== lastFetchedKeyRef.current) {
|
|
492
|
+
console.log('[SwapFormEstimates] initial-fetch effect TRIGGERING fetch', { estimateKey, lastKey: lastFetchedKeyRef.current });
|
|
425
493
|
// Mark this key as fetched to prevent duplicate calls
|
|
426
494
|
lastFetchedKeyRef.current = estimateKey;
|
|
427
495
|
// Use the debounced fetch function to avoid rapid duplicate calls
|
|
@@ -280,10 +280,13 @@ export function useSilentQuote({ client, address, evmAddress, solAddress, wallet
|
|
|
280
280
|
if (sourceAssetMatch) {
|
|
281
281
|
const sourceChainId = parseInt(sourceAssetMatch[1]);
|
|
282
282
|
const tokenAddress = sourceAssetMatch[2];
|
|
283
|
-
await approveTokenSpending(sourceChainId, tokenAddress, effectiveAllowanceTarget, sourceAmountInUnits, evmSignerAddress);
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
const approvalHash = await approveTokenSpending(sourceChainId, tokenAddress, effectiveAllowanceTarget, sourceAmountInUnits, evmSignerAddress);
|
|
284
|
+
// Only delay if an approval tx was actually sent (null means allowance was already sufficient)
|
|
285
|
+
if (approvalHash) {
|
|
286
|
+
// Delay after approve so RPC/nodes confirm approval before next tx (avoids MetaMask RPC errors)
|
|
287
|
+
setCurrentStep('Waiting for network...');
|
|
288
|
+
await new Promise((resolve) => setTimeout(resolve, APPROVE_POST_DELAY_MS));
|
|
289
|
+
}
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
292
|
// Handle EVM bridge swaps
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silentswap/react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.87",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@bigmi/core": "^0.6.5",
|
|
26
26
|
"@ensdomains/ensjs": "^4.2.0",
|
|
27
|
-
"@silentswap/sdk": "0.0.
|
|
28
|
-
"@silentswap/ui-kit": "0.0.
|
|
27
|
+
"@silentswap/sdk": "0.0.87",
|
|
28
|
+
"@silentswap/ui-kit": "0.0.87",
|
|
29
29
|
"@solana/codecs-strings": "^5.1.0",
|
|
30
30
|
"@solana/kit": "^5.1.0",
|
|
31
31
|
"@solana/rpc": "^5.1.0",
|