@silentswap/react 0.0.77 → 0.0.79
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/contexts/OrdersContext.d.ts +7 -0
- package/dist/contexts/OrdersContext.js +11 -0
- package/dist/contexts/SwapFormEstimatesContext.d.ts +16 -0
- package/dist/contexts/SwapFormEstimatesContext.js +445 -0
- package/dist/hooks/silent/useOrderTracking.d.ts +9 -2
- package/dist/hooks/silent/useOrderTracking.js +378 -269
- package/dist/hooks/usePlatformHealth.js +4 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/package.json +3 -3
|
@@ -29,16 +29,23 @@ export type FacilitatorGroup = () => Promise<{
|
|
|
29
29
|
}>;
|
|
30
30
|
}>;
|
|
31
31
|
export declare const useWalletFacilitatorGroups: (wallet: SilentSwapWallet | null, setFacilitatorGroups: (groups: FacilitatorGroup[]) => void) => void;
|
|
32
|
+
export type OrderDestination = {
|
|
33
|
+
asset: string;
|
|
34
|
+
contact: string;
|
|
35
|
+
amount: string;
|
|
36
|
+
};
|
|
32
37
|
export type OrdersContextType = {
|
|
33
38
|
orders: OrdersContextOrder[];
|
|
34
39
|
loading: boolean;
|
|
35
40
|
facilitatorGroups: FacilitatorGroup[];
|
|
36
41
|
orderIdToViewingAuth: Record<string, NaiveBase58>;
|
|
37
42
|
orderIdToDefaultPublicKey: Record<string, string>;
|
|
43
|
+
orderIdToDestinations: Record<string, OrderDestination[]>;
|
|
38
44
|
addFacilitatorGroup: (group: FacilitatorGroup) => void;
|
|
39
45
|
setFacilitatorGroups: (groups: FacilitatorGroup[]) => void;
|
|
40
46
|
clearFacilitatorGroups: () => void;
|
|
41
47
|
setOrderDefaultPublicKey: (orderId: string, publicKey: string) => void;
|
|
48
|
+
setOrderDestinations: (orderId: string, destinations: OrderDestination[]) => void;
|
|
42
49
|
refreshOrders: () => void;
|
|
43
50
|
getOrderAgeText: (modified?: number) => string;
|
|
44
51
|
getStatusInfo: (status: string, refundEligibility?: RefundEligibility | null) => {
|
|
@@ -28,6 +28,7 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
28
28
|
const [facilitatorGroups, setFacilitatorGroupsState] = useState([]);
|
|
29
29
|
const [orderIdToViewingAuth, setOrderIdToViewingAuth] = useLocalStorage('orderIdToViewingAuth', {}, { initializeWithValue: true });
|
|
30
30
|
const [orderIdToDefaultPublicKey, setOrderIdToDefaultPublicKey] = useLocalStorage('orderIdToDefaultPublicKey', {}, { initializeWithValue: true });
|
|
31
|
+
const [orderIdToDestinations, setOrderIdToDestinationsState] = useLocalStorage('orderIdToDestinations', {}, { initializeWithValue: true });
|
|
31
32
|
// Generate viewing auth token from facilitator group
|
|
32
33
|
const facilitator_group_authorize_order_view = useCallback(async (groupGetter) => {
|
|
33
34
|
try {
|
|
@@ -190,6 +191,12 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
190
191
|
[orderId]: publicKey,
|
|
191
192
|
}));
|
|
192
193
|
}, [setOrderIdToDefaultPublicKey]);
|
|
194
|
+
const setOrderDestinations = useCallback((orderId, destinations) => {
|
|
195
|
+
setOrderIdToDestinationsState((prev) => ({
|
|
196
|
+
...prev,
|
|
197
|
+
[orderId]: destinations,
|
|
198
|
+
}));
|
|
199
|
+
}, [setOrderIdToDestinationsState]);
|
|
193
200
|
const clearFacilitatorGroups = useCallback(() => {
|
|
194
201
|
setFacilitatorGroupsState([]);
|
|
195
202
|
setOrders([]);
|
|
@@ -238,10 +245,12 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
238
245
|
facilitatorGroups,
|
|
239
246
|
orderIdToViewingAuth,
|
|
240
247
|
orderIdToDefaultPublicKey,
|
|
248
|
+
orderIdToDestinations,
|
|
241
249
|
addFacilitatorGroup,
|
|
242
250
|
setFacilitatorGroups,
|
|
243
251
|
clearFacilitatorGroups,
|
|
244
252
|
setOrderDefaultPublicKey,
|
|
253
|
+
setOrderDestinations,
|
|
245
254
|
refreshOrders,
|
|
246
255
|
getOrderAgeText,
|
|
247
256
|
getStatusInfo,
|
|
@@ -251,10 +260,12 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
251
260
|
facilitatorGroups,
|
|
252
261
|
orderIdToViewingAuth,
|
|
253
262
|
orderIdToDefaultPublicKey,
|
|
263
|
+
orderIdToDestinations,
|
|
254
264
|
addFacilitatorGroup,
|
|
255
265
|
setFacilitatorGroups,
|
|
256
266
|
clearFacilitatorGroups,
|
|
257
267
|
setOrderDefaultPublicKey,
|
|
268
|
+
setOrderDestinations,
|
|
258
269
|
refreshOrders,
|
|
259
270
|
getOrderAgeText,
|
|
260
271
|
getStatusInfo,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type CalculationDirection } from '@silentswap/sdk';
|
|
3
|
+
import type { AssetInfo } from '@silentswap/sdk';
|
|
4
|
+
export interface SwapFormEstimatesContextType {
|
|
5
|
+
handleSliderChange: () => void;
|
|
6
|
+
handleDestinationAmountChange: () => void;
|
|
7
|
+
handleInputAmountChange: () => void;
|
|
8
|
+
calculationDirection: CalculationDirection;
|
|
9
|
+
}
|
|
10
|
+
export interface SwapFormEstimatesProviderProps {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
tokenIn: AssetInfo | null;
|
|
13
|
+
fetchEstimates: (direction?: CalculationDirection) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, }: SwapFormEstimatesProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export declare function useSwapFormEstimatesContext(): SwapFormEstimatesContextType;
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import React, { createContext, useContext, useCallback, useRef, useMemo, useEffect } from 'react';
|
|
4
|
+
import { useSwap, X_RANGE_SLIDER_MIN_GAP } from '../hooks/useSwap.js';
|
|
5
|
+
import { usePricesContext } from './PricesContext.js';
|
|
6
|
+
import { usePlatformHealthContext } from '../hooks/usePlatformHealth.js';
|
|
7
|
+
import { getAssetByCaip19, CALCULATION_DIRECTION_INPUT_TO_OUTPUT, CALCULATION_DIRECTION_OUTPUT_TO_INPUT, } from '@silentswap/sdk';
|
|
8
|
+
const XT_DELAY_DEBOUNCE_QUOTE = 550;
|
|
9
|
+
const SwapFormEstimatesContext = createContext(undefined);
|
|
10
|
+
export function SwapFormEstimatesProvider({ children, tokenIn, fetchEstimates, }) {
|
|
11
|
+
const setSplits = useSwap((state) => state.setSplits);
|
|
12
|
+
const setInputAmount = useSwap((state) => state.setInputAmount);
|
|
13
|
+
const inputAmount = useSwap((state) => state.inputAmount);
|
|
14
|
+
const destinations = useSwap((state) => state.destinations);
|
|
15
|
+
const splits = useSwap((state) => state.splits);
|
|
16
|
+
const isDraggingSlider = useSwap((state) => state.isDraggingSlider);
|
|
17
|
+
const updateDestinationAmount = useSwap((state) => state.updateDestinationAmount);
|
|
18
|
+
const { getPrice } = usePricesContext();
|
|
19
|
+
const { getServiceFeeRate } = usePlatformHealthContext();
|
|
20
|
+
const fetchEstimatesRef = useRef(fetchEstimates);
|
|
21
|
+
const debounceTimerRef = useRef(null);
|
|
22
|
+
const isRecalculatingInputRef = useRef(false);
|
|
23
|
+
const programmaticInputChangeRef = useRef(false);
|
|
24
|
+
const shouldFetchAfterInputRecalcRef = useRef(false);
|
|
25
|
+
const isFetchingEstimatesRef = useRef(false);
|
|
26
|
+
const [calculationDirection, setCalculationDirection] = React.useState(CALCULATION_DIRECTION_INPUT_TO_OUTPUT);
|
|
27
|
+
const calculationDirectionRef = useRef(CALCULATION_DIRECTION_INPUT_TO_OUTPUT);
|
|
28
|
+
// Refs for destination amount calculation (from useDestinationAmountCalculation)
|
|
29
|
+
const prevSplitsRef = useRef('');
|
|
30
|
+
const prevInputAmountForCalcRef = useRef('');
|
|
31
|
+
const rafScheduledRef = useRef(false);
|
|
32
|
+
const pendingUpdateRef = useRef(null);
|
|
33
|
+
const isManualDestinationChangeRef = useRef(false);
|
|
34
|
+
// Update fetchEstimates ref
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
fetchEstimatesRef.current = fetchEstimates;
|
|
37
|
+
}, [fetchEstimates]);
|
|
38
|
+
// Debounced fetch estimates
|
|
39
|
+
const scheduleFetchEstimates = useCallback((immediate = false, direction) => {
|
|
40
|
+
if (isDraggingSlider) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (debounceTimerRef.current) {
|
|
44
|
+
clearTimeout(debounceTimerRef.current);
|
|
45
|
+
debounceTimerRef.current = null;
|
|
46
|
+
}
|
|
47
|
+
const effectiveDirection = direction || calculationDirectionRef.current || CALCULATION_DIRECTION_INPUT_TO_OUTPUT;
|
|
48
|
+
// Update state and ref when direction changes
|
|
49
|
+
if (effectiveDirection !== calculationDirectionRef.current) {
|
|
50
|
+
calculationDirectionRef.current = effectiveDirection;
|
|
51
|
+
setCalculationDirection(effectiveDirection);
|
|
52
|
+
}
|
|
53
|
+
isFetchingEstimatesRef.current = true;
|
|
54
|
+
const fetchPromise = immediate
|
|
55
|
+
? fetchEstimatesRef.current(effectiveDirection)
|
|
56
|
+
: new Promise((resolve) => {
|
|
57
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
58
|
+
fetchEstimatesRef
|
|
59
|
+
.current(effectiveDirection)
|
|
60
|
+
.then(resolve)
|
|
61
|
+
.catch((err) => {
|
|
62
|
+
console.error('Failed to fetch estimates:', err);
|
|
63
|
+
resolve();
|
|
64
|
+
});
|
|
65
|
+
debounceTimerRef.current = null;
|
|
66
|
+
}, XT_DELAY_DEBOUNCE_QUOTE);
|
|
67
|
+
});
|
|
68
|
+
fetchPromise
|
|
69
|
+
.catch((err) => {
|
|
70
|
+
console.error('Failed to fetch estimates:', err);
|
|
71
|
+
})
|
|
72
|
+
.finally(() => {
|
|
73
|
+
isFetchingEstimatesRef.current = false;
|
|
74
|
+
});
|
|
75
|
+
}, [isDraggingSlider]);
|
|
76
|
+
// Recalculate splits from destination amounts
|
|
77
|
+
const recalculateSplitsFromAmounts = useCallback(() => {
|
|
78
|
+
if (!tokenIn)
|
|
79
|
+
return;
|
|
80
|
+
// Get latest state from Zustand store to avoid stale closures
|
|
81
|
+
const currentState = useSwap.getState();
|
|
82
|
+
const currentDestinations = currentState.destinations;
|
|
83
|
+
const currentInputAmount = currentState.inputAmount;
|
|
84
|
+
const currentSplits = currentState.splits;
|
|
85
|
+
console.log('[SwapFormEstimates] Recalculating splits from amounts destinations', JSON.stringify({
|
|
86
|
+
destinations: currentDestinations.map((d) => ({ asset: d.asset, amount: d.amount, priceUsd: d.priceUsd })),
|
|
87
|
+
inputAmount: currentInputAmount,
|
|
88
|
+
}, null, 2));
|
|
89
|
+
// Calculate USD values for each destination
|
|
90
|
+
// Always use getPrice to ensure we have the latest price, not stale dest.priceUsd
|
|
91
|
+
const destinationUsdValues = currentDestinations.map((dest, idx) => {
|
|
92
|
+
if (!dest.amount || parseFloat(dest.amount) <= 0)
|
|
93
|
+
return 0;
|
|
94
|
+
const amount = parseFloat(dest.amount) || 0;
|
|
95
|
+
let price = undefined;
|
|
96
|
+
if (dest.asset) {
|
|
97
|
+
const assetInfo = getAssetByCaip19(dest.asset);
|
|
98
|
+
if (assetInfo) {
|
|
99
|
+
// Always use getPrice to get the latest price
|
|
100
|
+
price = getPrice(assetInfo);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Fallback to dest.priceUsd only if getPrice returns undefined/null
|
|
104
|
+
if (price == null) {
|
|
105
|
+
price = dest.priceUsd;
|
|
106
|
+
}
|
|
107
|
+
const usdValue = price && price > 0 ? amount * price : 0;
|
|
108
|
+
console.log(`[SwapFormEstimates] Destination ${idx} USD calculation`, JSON.stringify({
|
|
109
|
+
index: idx,
|
|
110
|
+
asset: dest.asset,
|
|
111
|
+
amount,
|
|
112
|
+
price,
|
|
113
|
+
destPriceUsd: dest.priceUsd,
|
|
114
|
+
usdValue,
|
|
115
|
+
}, null, 2));
|
|
116
|
+
return usdValue;
|
|
117
|
+
});
|
|
118
|
+
const totalUsd = destinationUsdValues.reduce((sum, val) => sum + val, 0);
|
|
119
|
+
console.log('[SwapFormEstimates] Total destination USD', JSON.stringify({
|
|
120
|
+
totalUsd,
|
|
121
|
+
destinationUsdValues,
|
|
122
|
+
}, null, 2));
|
|
123
|
+
// When user manually edits outputs, trigger reverse calculation using estimate system
|
|
124
|
+
// The estimate system will calculate the required input amount using EXACT_OUTPUT tradeType
|
|
125
|
+
// and srcChainTokenInAmount: 'auto' for ingress quotes
|
|
126
|
+
// IMPORTANT: When calculating input from outputs, we need to account for the service fee
|
|
127
|
+
// that will be deducted. So if user wants $100 in outputs, we need to calculate input
|
|
128
|
+
// such that: inputUsd - (inputUsd * serviceFeeRate) = outputUsd
|
|
129
|
+
// Solving: inputUsd = outputUsd / (1 - serviceFeeRate)
|
|
130
|
+
if (totalUsd > 0 && tokenIn && !isRecalculatingInputRef.current) {
|
|
131
|
+
const currentInputAmountNum = parseFloat(currentInputAmount) || 0;
|
|
132
|
+
const tokenInPrice = getPrice(tokenIn);
|
|
133
|
+
if (tokenInPrice != null && tokenInPrice > 0) {
|
|
134
|
+
// Calculate required input USD accounting for service fee
|
|
135
|
+
// Formula: inputUsd = outputUsd / (1 - serviceFeeRate)
|
|
136
|
+
const serviceFeeRate = getServiceFeeRate() ?? 0.01; // Default to 1% if not available
|
|
137
|
+
const requiredInputUsd = totalUsd / (1 - serviceFeeRate);
|
|
138
|
+
const currentInputUsd = currentInputAmountNum * tokenInPrice;
|
|
139
|
+
const USD_TOLERANCE = 0.01;
|
|
140
|
+
const difference = Math.abs(requiredInputUsd - currentInputUsd);
|
|
141
|
+
console.log('[SwapFormEstimates] Triggering reverse calculation via estimate system', JSON.stringify({
|
|
142
|
+
currentInputAmount: currentInputAmountNum,
|
|
143
|
+
currentInputUsd,
|
|
144
|
+
totalUsd,
|
|
145
|
+
requiredInputUsd,
|
|
146
|
+
serviceFeeRate,
|
|
147
|
+
tokenInPrice,
|
|
148
|
+
difference,
|
|
149
|
+
exceedsTolerance: difference > USD_TOLERANCE,
|
|
150
|
+
}, null, 2));
|
|
151
|
+
// If there's a significant difference, trigger reverse calculation
|
|
152
|
+
// The estimate system will calculate the required input amount automatically
|
|
153
|
+
if (difference > USD_TOLERANCE) {
|
|
154
|
+
isRecalculatingInputRef.current = true;
|
|
155
|
+
programmaticInputChangeRef.current = true;
|
|
156
|
+
shouldFetchAfterInputRecalcRef.current = true;
|
|
157
|
+
calculationDirectionRef.current = CALCULATION_DIRECTION_OUTPUT_TO_INPUT;
|
|
158
|
+
setCalculationDirection(CALCULATION_DIRECTION_OUTPUT_TO_INPUT);
|
|
159
|
+
// Fetch estimates with reverse calculation direction
|
|
160
|
+
// The estimate system will:
|
|
161
|
+
// 1. Use EXACT_OUTPUT tradeType for egress quotes with dstChainTokenOutAmount = output value
|
|
162
|
+
// 2. Use EXACT_OUTPUT tradeType for ingress quotes with srcChainTokenInAmount: 'auto'
|
|
163
|
+
// 3. Calculate the required input amount and return it in result.calculatedInputAmount
|
|
164
|
+
scheduleFetchEstimates(false, CALCULATION_DIRECTION_OUTPUT_TO_INPUT);
|
|
165
|
+
// Reset the flag after a delay to allow state updates to propagate
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
isRecalculatingInputRef.current = false;
|
|
168
|
+
}, 100);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Recalculate splits based on USD proportions
|
|
173
|
+
// Always recalculate splits when destinations change, even if input was recalculated
|
|
174
|
+
// This ensures the slider reflects the new proportions
|
|
175
|
+
if (totalUsd > 0 && currentSplits.length === currentDestinations.length) {
|
|
176
|
+
const newSplits = [];
|
|
177
|
+
let cumulative = 0;
|
|
178
|
+
for (let i = 0; i < destinationUsdValues.length; i++) {
|
|
179
|
+
const proportion = destinationUsdValues[i] / totalUsd;
|
|
180
|
+
cumulative += proportion;
|
|
181
|
+
const newSplit = Math.max(cumulative, i > 0 ? newSplits[i - 1] + X_RANGE_SLIDER_MIN_GAP : X_RANGE_SLIDER_MIN_GAP);
|
|
182
|
+
newSplits.push(Math.min(newSplit, 1.0));
|
|
183
|
+
}
|
|
184
|
+
// Ensure last split is exactly 1.0
|
|
185
|
+
newSplits[newSplits.length - 1] = 1.0;
|
|
186
|
+
// Normalize splits to ensure minimum gaps
|
|
187
|
+
for (let i = newSplits.length - 2; i >= 0; i--) {
|
|
188
|
+
const maxAllowed = newSplits[i + 1] - X_RANGE_SLIDER_MIN_GAP;
|
|
189
|
+
if (newSplits[i] > maxAllowed) {
|
|
190
|
+
newSplits[i] = maxAllowed;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Only update if different
|
|
194
|
+
const SPLIT_TOLERANCE = 0.0001;
|
|
195
|
+
const splitsAreDifferent = newSplits.some((newSplit, i) => {
|
|
196
|
+
const currentSplit = currentSplits[i] || 0;
|
|
197
|
+
return Math.abs(newSplit - currentSplit) > SPLIT_TOLERANCE;
|
|
198
|
+
});
|
|
199
|
+
if (splitsAreDifferent) {
|
|
200
|
+
console.log('[SwapFormEstimates] Updating splits:', {
|
|
201
|
+
from: currentSplits,
|
|
202
|
+
to: newSplits,
|
|
203
|
+
destinationUsdValues,
|
|
204
|
+
totalUsd,
|
|
205
|
+
});
|
|
206
|
+
// Always create a new array reference to ensure Zustand detects the change
|
|
207
|
+
setSplits([...newSplits]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}, [tokenIn, setSplits, setInputAmount, getPrice, getServiceFeeRate]);
|
|
211
|
+
// Handle slider change (splits changed)
|
|
212
|
+
const handleSliderChange = useCallback(() => {
|
|
213
|
+
scheduleFetchEstimates();
|
|
214
|
+
}, [scheduleFetchEstimates]);
|
|
215
|
+
// Handle destination amount change (user manually edited)
|
|
216
|
+
const handleDestinationAmountChange = useCallback(() => {
|
|
217
|
+
// Mark that this is a manual destination change
|
|
218
|
+
// This prevents performDestinationAmountCalculation from overwriting user's changes
|
|
219
|
+
isManualDestinationChangeRef.current = true;
|
|
220
|
+
recalculateSplitsFromAmounts();
|
|
221
|
+
// Clear the flag after recalculation completes
|
|
222
|
+
// Use setTimeout to ensure state updates have propagated
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
isManualDestinationChangeRef.current = false;
|
|
225
|
+
}, 150);
|
|
226
|
+
// If input amount was not recalculated, fetch estimates directly
|
|
227
|
+
if (!shouldFetchAfterInputRecalcRef.current) {
|
|
228
|
+
scheduleFetchEstimates();
|
|
229
|
+
}
|
|
230
|
+
}, [recalculateSplitsFromAmounts, scheduleFetchEstimates]);
|
|
231
|
+
// Handle input amount change (user manually edited)
|
|
232
|
+
const handleInputAmountChange = useCallback(() => {
|
|
233
|
+
// Mark as user-initiated change (not programmatic)
|
|
234
|
+
programmaticInputChangeRef.current = false;
|
|
235
|
+
shouldFetchAfterInputRecalcRef.current = false;
|
|
236
|
+
// Reset recalculation flag if it was set (in case of programmatic change that completed)
|
|
237
|
+
isRecalculatingInputRef.current = false;
|
|
238
|
+
// Set direction to input-to-output (normal flow)
|
|
239
|
+
calculationDirectionRef.current = CALCULATION_DIRECTION_INPUT_TO_OUTPUT;
|
|
240
|
+
setCalculationDirection(CALCULATION_DIRECTION_INPUT_TO_OUTPUT);
|
|
241
|
+
// Just fetch estimates with new input amount
|
|
242
|
+
// This will recalculate output amounts based on the new input
|
|
243
|
+
scheduleFetchEstimates();
|
|
244
|
+
}, [scheduleFetchEstimates]);
|
|
245
|
+
// Function to perform destination amount calculation from splits and inputAmount
|
|
246
|
+
// This ensures displayAmount updates immediately when slider is dragged
|
|
247
|
+
// Uses USD values to correctly calculate amounts across different tokens
|
|
248
|
+
const performDestinationAmountCalculation = useCallback((splitsToUse, inputAmountToUse) => {
|
|
249
|
+
// Use destinations from closure (will be fresh due to dependency array)
|
|
250
|
+
const currentDestinations = destinations;
|
|
251
|
+
if (!inputAmountToUse || parseFloat(inputAmountToUse) <= 0 || splitsToUse.length === 0) {
|
|
252
|
+
// Clear amounts if no input - batch updates
|
|
253
|
+
const updates = [];
|
|
254
|
+
currentDestinations.forEach((_, index) => {
|
|
255
|
+
if (currentDestinations[index].amount !== '') {
|
|
256
|
+
updates.push({ index, amount: '' });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
// Apply all updates at once
|
|
260
|
+
updates.forEach(({ index, amount }) => {
|
|
261
|
+
updateDestinationAmount(index, amount);
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const inputAmountNum = parseFloat(inputAmountToUse);
|
|
266
|
+
if (isNaN(inputAmountNum))
|
|
267
|
+
return;
|
|
268
|
+
// Get input token price (tokenIn from closure will be fresh due to dependency array)
|
|
269
|
+
const inputTokenPrice = tokenIn ? getPrice(tokenIn) : undefined;
|
|
270
|
+
if (!inputTokenPrice || inputTokenPrice <= 0) {
|
|
271
|
+
// Can't calculate without input price - skip calculation
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Calculate total USD value of input amount
|
|
275
|
+
const totalUsdValue = inputAmountNum * inputTokenPrice;
|
|
276
|
+
// Apply service fee to get actionable USD (amount available for outputs after fee deduction)
|
|
277
|
+
// Formula: serviceFeeUsd = depositUsd * serviceFeeRate
|
|
278
|
+
// actionableUsd = depositUsd - serviceFeeUsd
|
|
279
|
+
const serviceFeeRate = getServiceFeeRate() ?? 0.01; // Default to 1% if not available
|
|
280
|
+
const serviceFeeUsd = totalUsdValue * serviceFeeRate;
|
|
281
|
+
const actionableUsd = totalUsdValue - serviceFeeUsd;
|
|
282
|
+
// Normalize splits for single destination - ensure it's always [1.0]
|
|
283
|
+
const normalizedSplits = currentDestinations.length === 1 ? [1.0] : splitsToUse;
|
|
284
|
+
// Recalculate each destination amount based on splits using USD values
|
|
285
|
+
// Formula:
|
|
286
|
+
// 1. Calculate USD value for this destination: actionableUsd * splitDiff (after service fee)
|
|
287
|
+
// 2. Convert USD to destination token: usdValue / destTokenPrice
|
|
288
|
+
// This ensures correct amounts when swapping between different tokens
|
|
289
|
+
// Batch all updates to minimize re-renders
|
|
290
|
+
const updates = [];
|
|
291
|
+
currentDestinations.forEach((_, index) => {
|
|
292
|
+
const prevSplit = index > 0 ? normalizedSplits[index - 1] : 0;
|
|
293
|
+
const currentSplit = normalizedSplits[index] ?? 1;
|
|
294
|
+
const splitDiff = currentSplit - prevSplit;
|
|
295
|
+
// Calculate USD value for this destination (after service fee)
|
|
296
|
+
const destinationUsdValue = actionableUsd * splitDiff;
|
|
297
|
+
// Get destination asset info and price
|
|
298
|
+
const destAsset = currentDestinations[index]?.asset;
|
|
299
|
+
if (!destAsset) {
|
|
300
|
+
return; // Skip if no asset
|
|
301
|
+
}
|
|
302
|
+
const destAssetInfo = getAssetByCaip19(destAsset);
|
|
303
|
+
const destTokenPrice = destAssetInfo ? getPrice(destAssetInfo) : currentDestinations[index]?.priceUsd;
|
|
304
|
+
if (!destTokenPrice || destTokenPrice <= 0) {
|
|
305
|
+
return; // Skip if no price available
|
|
306
|
+
}
|
|
307
|
+
// Convert USD value to destination token amount
|
|
308
|
+
const newAmount = (destinationUsdValue / destTokenPrice).toFixed(destAssetInfo?.precision ?? 6);
|
|
309
|
+
// Only update if amount has changed
|
|
310
|
+
if (currentDestinations[index].amount !== newAmount) {
|
|
311
|
+
updates.push({ index, amount: newAmount });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
// Apply all updates
|
|
315
|
+
updates.forEach(({ index, amount }) => {
|
|
316
|
+
updateDestinationAmount(index, amount);
|
|
317
|
+
});
|
|
318
|
+
}, [destinations, tokenIn, getPrice, updateDestinationAmount, getServiceFeeRate]);
|
|
319
|
+
// Effect to recalculate destination amounts when splits or inputAmount changes
|
|
320
|
+
// This ensures displayAmount updates immediately when slider is dragged
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
// Skip recalculation if user manually changed destination amounts
|
|
323
|
+
// (we only want to update input amount, not overwrite user's manual changes)
|
|
324
|
+
if (isManualDestinationChangeRef.current) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// Skip recalculation if we're currently recalculating input amount
|
|
328
|
+
// This prevents infinite loops when input amount is programmatically updated
|
|
329
|
+
if (isRecalculatingInputRef.current) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// Skip recalculation if input change was programmatic (from output-to-input calculation)
|
|
333
|
+
// When output-to-input calculates and sets input amount, we don't want to recalculate outputs
|
|
334
|
+
// as that would trigger another fetch and cause an infinite loop
|
|
335
|
+
if (programmaticInputChangeRef.current) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const splitsKey = splits.join(',');
|
|
339
|
+
const hasSplitsChanged = prevSplitsRef.current !== splitsKey;
|
|
340
|
+
const hasInputAmountChanged = prevInputAmountForCalcRef.current !== inputAmount;
|
|
341
|
+
// Only recalculate if splits or inputAmount actually changed
|
|
342
|
+
if (!hasSplitsChanged && !hasInputAmountChanged) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Update refs immediately to prevent re-running
|
|
346
|
+
prevSplitsRef.current = splitsKey;
|
|
347
|
+
prevInputAmountForCalcRef.current = inputAmount;
|
|
348
|
+
// During drag, throttle updates using requestAnimationFrame to avoid blocking pointer events
|
|
349
|
+
// But still allow updates to happen (unlike before where we skipped entirely)
|
|
350
|
+
if (isDraggingSlider) {
|
|
351
|
+
// Store pending update
|
|
352
|
+
pendingUpdateRef.current = { splits, inputAmount };
|
|
353
|
+
// Schedule update if not already scheduled
|
|
354
|
+
if (!rafScheduledRef.current) {
|
|
355
|
+
rafScheduledRef.current = true;
|
|
356
|
+
requestAnimationFrame(() => {
|
|
357
|
+
if (pendingUpdateRef.current) {
|
|
358
|
+
performDestinationAmountCalculation(pendingUpdateRef.current.splits, pendingUpdateRef.current.inputAmount);
|
|
359
|
+
pendingUpdateRef.current = null;
|
|
360
|
+
}
|
|
361
|
+
rafScheduledRef.current = false;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// Not dragging - update immediately
|
|
367
|
+
// Also flush any pending updates from when we were dragging
|
|
368
|
+
if (pendingUpdateRef.current) {
|
|
369
|
+
performDestinationAmountCalculation(pendingUpdateRef.current.splits, pendingUpdateRef.current.inputAmount);
|
|
370
|
+
pendingUpdateRef.current = null;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
performDestinationAmountCalculation(splits, inputAmount);
|
|
374
|
+
}
|
|
375
|
+
}, [splits, inputAmount, tokenIn, destinations, isDraggingSlider, performDestinationAmountCalculation]);
|
|
376
|
+
// Create a unique key for the current estimate request
|
|
377
|
+
// This key changes when any relevant input changes, triggering a new fetch
|
|
378
|
+
const estimateKey = useMemo(() => {
|
|
379
|
+
if (!tokenIn?.caip19 || !inputAmount || parseFloat(inputAmount) <= 0) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
if (destinations.length === 0 || !destinations.every((d) => d.asset)) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
if (splits.length === 0 || splits.length !== destinations.length) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
// Create a unique key from all relevant inputs
|
|
389
|
+
return [tokenIn.caip19, inputAmount, destinations.map((d) => d.asset).join(','), splits.join(',')].join('|');
|
|
390
|
+
}, [tokenIn?.caip19, inputAmount, destinations, splits]);
|
|
391
|
+
// Track the last estimate key we fetched for to avoid duplicate fetches
|
|
392
|
+
const lastFetchedKeyRef = useRef(null);
|
|
393
|
+
// Reset the fetched key when tokenIn changes to allow fetching for new tokens
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
const tokenInId = tokenIn?.caip19 || null;
|
|
396
|
+
const lastKeyToken = lastFetchedKeyRef.current?.split('|')[0] || null;
|
|
397
|
+
// If token changed, reset the ref to allow fetching for the new token
|
|
398
|
+
if (tokenInId && lastKeyToken && tokenInId !== lastKeyToken) {
|
|
399
|
+
lastFetchedKeyRef.current = null;
|
|
400
|
+
}
|
|
401
|
+
}, [tokenIn?.caip19]);
|
|
402
|
+
// Initial fetch of estimates when we have required data
|
|
403
|
+
// This ensures estimates are loaded on page first load or when data becomes available
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
// Skip if we're already fetching to prevent loops
|
|
406
|
+
if (isFetchingEstimatesRef.current) {
|
|
407
|
+
// Update lastFetchedKeyRef to the new key to prevent re-triggering when fetch completes
|
|
408
|
+
if (estimateKey) {
|
|
409
|
+
lastFetchedKeyRef.current = estimateKey;
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// Only fetch if we have a valid estimate key and it's different from the last one we fetched
|
|
414
|
+
if (estimateKey && estimateKey !== lastFetchedKeyRef.current) {
|
|
415
|
+
// Mark this key as fetched to prevent duplicate calls
|
|
416
|
+
lastFetchedKeyRef.current = estimateKey;
|
|
417
|
+
// Use the debounced fetch function to avoid rapid duplicate calls
|
|
418
|
+
// Use immediate=true for initial load to get estimates faster
|
|
419
|
+
scheduleFetchEstimates(true);
|
|
420
|
+
}
|
|
421
|
+
return () => { };
|
|
422
|
+
}, [estimateKey, scheduleFetchEstimates, tokenIn?.caip19]);
|
|
423
|
+
const value = useMemo(() => ({
|
|
424
|
+
handleSliderChange,
|
|
425
|
+
handleDestinationAmountChange,
|
|
426
|
+
handleInputAmountChange,
|
|
427
|
+
calculationDirection,
|
|
428
|
+
}), [handleSliderChange, handleDestinationAmountChange, handleInputAmountChange, calculationDirection]);
|
|
429
|
+
// Cleanup timers on unmount
|
|
430
|
+
React.useEffect(() => {
|
|
431
|
+
return () => {
|
|
432
|
+
if (debounceTimerRef.current) {
|
|
433
|
+
clearTimeout(debounceTimerRef.current);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}, []);
|
|
437
|
+
return _jsx(SwapFormEstimatesContext.Provider, { value: value, children: children });
|
|
438
|
+
}
|
|
439
|
+
export function useSwapFormEstimatesContext() {
|
|
440
|
+
const context = useContext(SwapFormEstimatesContext);
|
|
441
|
+
if (context === undefined) {
|
|
442
|
+
throw new Error('useSwapFormEstimatesContext must be used within SwapFormEstimatesProvider');
|
|
443
|
+
}
|
|
444
|
+
return context;
|
|
445
|
+
}
|
|
@@ -66,7 +66,7 @@ export type OrderTransactions = {
|
|
|
66
66
|
export type OrderStatus = {
|
|
67
67
|
priority?: string;
|
|
68
68
|
signer: string;
|
|
69
|
-
deposit
|
|
69
|
+
deposit?: OrderDeposit;
|
|
70
70
|
outputs: Array<{
|
|
71
71
|
stage: OutputStage;
|
|
72
72
|
timestamp: number;
|
|
@@ -143,6 +143,8 @@ export interface UseOrderTrackingOptions {
|
|
|
143
143
|
onStatusUpdate?: (status: OrderStatus) => void;
|
|
144
144
|
onError?: (error: Error) => void;
|
|
145
145
|
onComplete?: () => void;
|
|
146
|
+
/** Fetch USD price for an asset when priceUsd is missing from WS response (matching Svelte k_asset.priceUsd()) */
|
|
147
|
+
fetchAssetPrice?: (caip19: string) => Promise<number>;
|
|
146
148
|
}
|
|
147
149
|
export interface UseOrderTrackingReturn {
|
|
148
150
|
isConnected: boolean;
|
|
@@ -170,5 +172,10 @@ export declare function getStatusTextFromStage(stage: OutputStage): string;
|
|
|
170
172
|
export declare function getProgressFromStage(stage: OutputStage): number;
|
|
171
173
|
/**
|
|
172
174
|
* React hook for tracking SilentSwap orders via WebSocket
|
|
175
|
+
*
|
|
176
|
+
* Uses a module-level singleton: one WebSocket per order, reused across
|
|
177
|
+
* React lifecycle events (Strict Mode, re-renders, effect re-runs).
|
|
178
|
+
* Reconnection matches Svelte ws_order_subscribe: immediate reconnect,
|
|
179
|
+
* 5s delay if idle >90s, no exponential backoff.
|
|
173
180
|
*/
|
|
174
|
-
export declare function useOrderTracking({ client, orderId: initialOrderId, viewingAuth: initialAuth, onStatusUpdate, onError, onComplete, }?: UseOrderTrackingOptions): UseOrderTrackingReturn;
|
|
181
|
+
export declare function useOrderTracking({ client, orderId: initialOrderId, viewingAuth: initialAuth, onStatusUpdate, onError, onComplete, fetchAssetPrice, }?: UseOrderTrackingOptions): UseOrderTrackingReturn;
|