@silentswap/react 0.0.78 → 0.0.80
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 +12 -0
- package/dist/contexts/OrdersContext.js +63 -0
- package/dist/contexts/SwapFormEstimatesContext.d.ts +16 -0
- package/dist/contexts/SwapFormEstimatesContext.js +445 -0
- package/dist/contexts/orderTrackingConnection.d.ts +19 -0
- package/dist/contexts/orderTrackingConnection.js +327 -0
- package/dist/contexts/orderTrackingTypes.d.ts +149 -0
- package/dist/contexts/orderTrackingTypes.js +151 -0
- package/dist/hooks/silent/useBridgeExecution.d.ts +1 -1
- package/dist/hooks/silent/useBridgeExecution.js +47 -41
- package/dist/hooks/silent/useOrderTracking.d.ts +9 -143
- package/dist/hooks/silent/useOrderTracking.js +38 -514
- package/dist/hooks/silent/useSilentQuote.js +5 -3
- package/dist/hooks/usePlatformHealth.js +4 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +2 -1
- package/package.json +3 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { NaiveBase58, RefundEligibility } from '@silentswap/sdk';
|
|
3
3
|
import type { SilentSwapWallet } from '../hooks/silent/useWallet.js';
|
|
4
|
+
import { type OrderTrackingState, type OrderTrackingOptions } from './orderTrackingTypes.js';
|
|
4
5
|
export type OrdersContextOrderMetadata = {
|
|
5
6
|
sourceAsset?: {
|
|
6
7
|
caip19: string;
|
|
@@ -29,16 +30,23 @@ export type FacilitatorGroup = () => Promise<{
|
|
|
29
30
|
}>;
|
|
30
31
|
}>;
|
|
31
32
|
export declare const useWalletFacilitatorGroups: (wallet: SilentSwapWallet | null, setFacilitatorGroups: (groups: FacilitatorGroup[]) => void) => void;
|
|
33
|
+
export type OrderDestination = {
|
|
34
|
+
asset: string;
|
|
35
|
+
contact: string;
|
|
36
|
+
amount: string;
|
|
37
|
+
};
|
|
32
38
|
export type OrdersContextType = {
|
|
33
39
|
orders: OrdersContextOrder[];
|
|
34
40
|
loading: boolean;
|
|
35
41
|
facilitatorGroups: FacilitatorGroup[];
|
|
36
42
|
orderIdToViewingAuth: Record<string, NaiveBase58>;
|
|
37
43
|
orderIdToDefaultPublicKey: Record<string, string>;
|
|
44
|
+
orderIdToDestinations: Record<string, OrderDestination[]>;
|
|
38
45
|
addFacilitatorGroup: (group: FacilitatorGroup) => void;
|
|
39
46
|
setFacilitatorGroups: (groups: FacilitatorGroup[]) => void;
|
|
40
47
|
clearFacilitatorGroups: () => void;
|
|
41
48
|
setOrderDefaultPublicKey: (orderId: string, publicKey: string) => void;
|
|
49
|
+
setOrderDestinations: (orderId: string, destinations: OrderDestination[]) => void;
|
|
42
50
|
refreshOrders: () => void;
|
|
43
51
|
getOrderAgeText: (modified?: number) => string;
|
|
44
52
|
getStatusInfo: (status: string, refundEligibility?: RefundEligibility | null) => {
|
|
@@ -46,6 +54,10 @@ export type OrdersContextType = {
|
|
|
46
54
|
pulsing: boolean;
|
|
47
55
|
color: string;
|
|
48
56
|
};
|
|
57
|
+
orderTrackingByKey: Record<string, OrderTrackingState>;
|
|
58
|
+
connectOrderTracking: (orderId: string, auth: string, options: OrderTrackingOptions) => void;
|
|
59
|
+
disconnectOrderTracking: (orderId: string, auth: string) => void;
|
|
60
|
+
getOrderTrackingState: (orderId: string, auth: string) => OrderTrackingState;
|
|
49
61
|
};
|
|
50
62
|
export declare const OrdersProvider: React.FC<{
|
|
51
63
|
children: React.ReactNode;
|
|
@@ -4,6 +4,8 @@ import { createContext, useContext, useState, useEffect, useCallback, useMemo, u
|
|
|
4
4
|
import { bytes_to_base58, DepositStatus, hex_to_bytes } from '@silentswap/sdk';
|
|
5
5
|
import { useLocalStorage } from 'usehooks-ts';
|
|
6
6
|
import { useSilentSwap } from './SilentSwapContext.js';
|
|
7
|
+
import { createOrderTrackingManager } from './orderTrackingConnection.js';
|
|
8
|
+
import { getOrderTrackingCacheKey, DEFAULT_ORDER_TRACKING_STATE, } from './orderTrackingTypes.js';
|
|
7
9
|
export const useWalletFacilitatorGroups = (wallet, setFacilitatorGroups) => {
|
|
8
10
|
// Automatically set facilitator groups from wallet (matches Svelte behavior)
|
|
9
11
|
// In Svelte: UserState.facilitatorGroups = a_accounts.map(g => g.group) when wallet loads
|
|
@@ -28,6 +30,49 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
28
30
|
const [facilitatorGroups, setFacilitatorGroupsState] = useState([]);
|
|
29
31
|
const [orderIdToViewingAuth, setOrderIdToViewingAuth] = useLocalStorage('orderIdToViewingAuth', {}, { initializeWithValue: true });
|
|
30
32
|
const [orderIdToDefaultPublicKey, setOrderIdToDefaultPublicKey] = useLocalStorage('orderIdToDefaultPublicKey', {}, { initializeWithValue: true });
|
|
33
|
+
const [orderIdToDestinations, setOrderIdToDestinationsState] = useLocalStorage('orderIdToDestinations', {}, { initializeWithValue: true });
|
|
34
|
+
// Order tracking: WebSocket connections and status cache live in context
|
|
35
|
+
const [orderTrackingByKey, setOrderTrackingByKey] = useState({});
|
|
36
|
+
const orderTrackingStateRef = useRef(new Map());
|
|
37
|
+
const activeTrackingKeysRef = useRef(new Set());
|
|
38
|
+
const optionsByKeyRef = useRef(new Map());
|
|
39
|
+
// Stable API object: only setStateForKey needs a ref so it always sees latest setOrderTrackingByKey
|
|
40
|
+
const setStateForKeyRef = useRef(null);
|
|
41
|
+
setStateForKeyRef.current = (key, state) => {
|
|
42
|
+
orderTrackingStateRef.current.set(key, state);
|
|
43
|
+
setOrderTrackingByKey((prev) => ({ ...prev, [key]: state }));
|
|
44
|
+
};
|
|
45
|
+
const orderTrackingApiRef = useRef({
|
|
46
|
+
setStateForKey(key, state) {
|
|
47
|
+
setStateForKeyRef.current(key, state);
|
|
48
|
+
},
|
|
49
|
+
getStateForKey(key) {
|
|
50
|
+
return orderTrackingStateRef.current.get(key);
|
|
51
|
+
},
|
|
52
|
+
isKeyActive(key) {
|
|
53
|
+
return activeTrackingKeysRef.current.has(key);
|
|
54
|
+
},
|
|
55
|
+
getOptionsForKey(key) {
|
|
56
|
+
return optionsByKeyRef.current.get(key);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const orderTrackingManager = useMemo(() => createOrderTrackingManager(orderTrackingApiRef), []);
|
|
60
|
+
const connectOrderTracking = useCallback((orderId, auth, options) => {
|
|
61
|
+
const key = getOrderTrackingCacheKey(orderId, auth);
|
|
62
|
+
activeTrackingKeysRef.current.add(key);
|
|
63
|
+
optionsByKeyRef.current.set(key, options);
|
|
64
|
+
orderTrackingManager.connect(orderId, auth, key, options);
|
|
65
|
+
}, [orderTrackingManager]);
|
|
66
|
+
const disconnectOrderTracking = useCallback((orderId, auth) => {
|
|
67
|
+
const key = getOrderTrackingCacheKey(orderId, auth);
|
|
68
|
+
activeTrackingKeysRef.current.delete(key);
|
|
69
|
+
optionsByKeyRef.current.delete(key);
|
|
70
|
+
orderTrackingManager.disconnect(orderId, auth);
|
|
71
|
+
}, [orderTrackingManager]);
|
|
72
|
+
const getOrderTrackingState = useCallback((orderId, auth) => {
|
|
73
|
+
const key = getOrderTrackingCacheKey(orderId, auth);
|
|
74
|
+
return orderTrackingByKey[key] ?? DEFAULT_ORDER_TRACKING_STATE;
|
|
75
|
+
}, [orderTrackingByKey]);
|
|
31
76
|
// Generate viewing auth token from facilitator group
|
|
32
77
|
const facilitator_group_authorize_order_view = useCallback(async (groupGetter) => {
|
|
33
78
|
try {
|
|
@@ -190,6 +235,12 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
190
235
|
[orderId]: publicKey,
|
|
191
236
|
}));
|
|
192
237
|
}, [setOrderIdToDefaultPublicKey]);
|
|
238
|
+
const setOrderDestinations = useCallback((orderId, destinations) => {
|
|
239
|
+
setOrderIdToDestinationsState((prev) => ({
|
|
240
|
+
...prev,
|
|
241
|
+
[orderId]: destinations,
|
|
242
|
+
}));
|
|
243
|
+
}, [setOrderIdToDestinationsState]);
|
|
193
244
|
const clearFacilitatorGroups = useCallback(() => {
|
|
194
245
|
setFacilitatorGroupsState([]);
|
|
195
246
|
setOrders([]);
|
|
@@ -238,26 +289,38 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
238
289
|
facilitatorGroups,
|
|
239
290
|
orderIdToViewingAuth,
|
|
240
291
|
orderIdToDefaultPublicKey,
|
|
292
|
+
orderIdToDestinations,
|
|
241
293
|
addFacilitatorGroup,
|
|
242
294
|
setFacilitatorGroups,
|
|
243
295
|
clearFacilitatorGroups,
|
|
244
296
|
setOrderDefaultPublicKey,
|
|
297
|
+
setOrderDestinations,
|
|
245
298
|
refreshOrders,
|
|
246
299
|
getOrderAgeText,
|
|
247
300
|
getStatusInfo,
|
|
301
|
+
orderTrackingByKey,
|
|
302
|
+
connectOrderTracking,
|
|
303
|
+
disconnectOrderTracking,
|
|
304
|
+
getOrderTrackingState,
|
|
248
305
|
}), [
|
|
249
306
|
orders,
|
|
250
307
|
loading,
|
|
251
308
|
facilitatorGroups,
|
|
252
309
|
orderIdToViewingAuth,
|
|
253
310
|
orderIdToDefaultPublicKey,
|
|
311
|
+
orderIdToDestinations,
|
|
254
312
|
addFacilitatorGroup,
|
|
255
313
|
setFacilitatorGroups,
|
|
256
314
|
clearFacilitatorGroups,
|
|
257
315
|
setOrderDefaultPublicKey,
|
|
316
|
+
setOrderDestinations,
|
|
258
317
|
refreshOrders,
|
|
259
318
|
getOrderAgeText,
|
|
260
319
|
getStatusInfo,
|
|
320
|
+
orderTrackingByKey,
|
|
321
|
+
connectOrderTracking,
|
|
322
|
+
disconnectOrderTracking,
|
|
323
|
+
getOrderTrackingState,
|
|
261
324
|
]);
|
|
262
325
|
return _jsx(OrdersContext.Provider, { value: value, children: children });
|
|
263
326
|
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Order tracking WebSocket and cache logic.
|
|
3
|
+
* Lives in context via createOrderTrackingManager(); state updates go through setStateForKey.
|
|
4
|
+
*/
|
|
5
|
+
import type { OrderTrackingState, OrderTrackingOptions } from './orderTrackingTypes.js';
|
|
6
|
+
export type OrderTrackingManagerApi = {
|
|
7
|
+
setStateForKey: (key: string, state: OrderTrackingState) => void;
|
|
8
|
+
getStateForKey: (key: string) => OrderTrackingState | undefined;
|
|
9
|
+
isKeyActive: (key: string) => boolean;
|
|
10
|
+
getOptionsForKey: (key: string) => OrderTrackingOptions | undefined;
|
|
11
|
+
};
|
|
12
|
+
/** Ref-like object so the manager always reads the current API (avoids storing manager in a ref). */
|
|
13
|
+
export type OrderTrackingManagerApiRef = {
|
|
14
|
+
current: OrderTrackingManagerApi;
|
|
15
|
+
};
|
|
16
|
+
export declare function createOrderTrackingManager(apiRef: OrderTrackingManagerApiRef): {
|
|
17
|
+
connect: (orderId: string, auth: string, key: string, options: OrderTrackingOptions) => void;
|
|
18
|
+
disconnect: (orderId: string, auth: string) => void;
|
|
19
|
+
};
|