@silentswap/react 0.0.78 → 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.
@@ -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: OrderDeposit;
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;