@silentswap/react 0.0.41
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/README.md +48 -0
- package/dist/contexts/AssetsContext.d.ts +24 -0
- package/dist/contexts/AssetsContext.js +83 -0
- package/dist/contexts/BalancesContext.d.ts +28 -0
- package/dist/contexts/BalancesContext.js +533 -0
- package/dist/contexts/OrdersContext.d.ts +53 -0
- package/dist/contexts/OrdersContext.js +240 -0
- package/dist/contexts/PricesContext.d.ts +12 -0
- package/dist/contexts/PricesContext.js +109 -0
- package/dist/contexts/SilentSwapContext.d.ts +58 -0
- package/dist/contexts/SilentSwapContext.js +205 -0
- package/dist/hooks/silent/orderTrackingWebSocketManager.d.ts +48 -0
- package/dist/hooks/silent/orderTrackingWebSocketManager.js +284 -0
- package/dist/hooks/silent/solana-transaction.d.ts +60 -0
- package/dist/hooks/silent/solana-transaction.js +236 -0
- package/dist/hooks/silent/useAuth.d.ts +90 -0
- package/dist/hooks/silent/useAuth.js +269 -0
- package/dist/hooks/silent/useBridgeExecution.d.ts +39 -0
- package/dist/hooks/silent/useBridgeExecution.js +877 -0
- package/dist/hooks/silent/useOrderSigning.d.ts +34 -0
- package/dist/hooks/silent/useOrderSigning.js +133 -0
- package/dist/hooks/silent/useOrderTracking.d.ts +174 -0
- package/dist/hooks/silent/useOrderTracking.js +524 -0
- package/dist/hooks/silent/useQuoteCalculation.d.ts +50 -0
- package/dist/hooks/silent/useQuoteCalculation.js +331 -0
- package/dist/hooks/silent/useQuoteFetching.d.ts +18 -0
- package/dist/hooks/silent/useQuoteFetching.js +54 -0
- package/dist/hooks/silent/useRefund.d.ts +26 -0
- package/dist/hooks/silent/useRefund.js +134 -0
- package/dist/hooks/silent/useSilentClient.d.ts +16 -0
- package/dist/hooks/silent/useSilentClient.js +32 -0
- package/dist/hooks/silent/useSilentOrders.d.ts +174 -0
- package/dist/hooks/silent/useSilentOrders.js +73 -0
- package/dist/hooks/silent/useSilentQuote.d.ts +88 -0
- package/dist/hooks/silent/useSilentQuote.js +381 -0
- package/dist/hooks/silent/useWallet.d.ts +76 -0
- package/dist/hooks/silent/useWallet.js +203 -0
- package/dist/hooks/useAssetPrice.d.ts +8 -0
- package/dist/hooks/useAssetPrice.js +47 -0
- package/dist/hooks/useContacts.d.ts +52 -0
- package/dist/hooks/useContacts.js +259 -0
- package/dist/hooks/useEgressEstimates.d.ts +32 -0
- package/dist/hooks/useEgressEstimates.js +230 -0
- package/dist/hooks/useHiddenSwapFees.d.ts +22 -0
- package/dist/hooks/useHiddenSwapFees.js +81 -0
- package/dist/hooks/useOrderEstimates.d.ts +37 -0
- package/dist/hooks/useOrderEstimates.js +393 -0
- package/dist/hooks/useOutputAssetInfo.d.ts +12 -0
- package/dist/hooks/useOutputAssetInfo.js +38 -0
- package/dist/hooks/usePrices.d.ts +60 -0
- package/dist/hooks/usePrices.js +188 -0
- package/dist/hooks/useQuote.d.ts +73 -0
- package/dist/hooks/useQuote.js +507 -0
- package/dist/hooks/useResetSwapForm.d.ts +16 -0
- package/dist/hooks/useResetSwapForm.js +68 -0
- package/dist/hooks/useSlippageUsd.d.ts +11 -0
- package/dist/hooks/useSlippageUsd.js +19 -0
- package/dist/hooks/useSolanaAdapter.d.ts +15 -0
- package/dist/hooks/useSolanaAdapter.js +55 -0
- package/dist/hooks/useStatus.d.ts +25 -0
- package/dist/hooks/useStatus.js +60 -0
- package/dist/hooks/useSwap.d.ts +67 -0
- package/dist/hooks/useSwap.js +285 -0
- package/dist/hooks/useTransaction.d.ts +119 -0
- package/dist/hooks/useTransaction.js +353 -0
- package/dist/hooks/useTransactionAddress.d.ts +11 -0
- package/dist/hooks/useTransactionAddress.js +26 -0
- package/dist/hooks/useUsdValue.d.ts +7 -0
- package/dist/hooks/useUsdValue.js +19 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +41 -0
- package/dist/stories/SilentSwapOverview.stories.d.ts +10 -0
- package/dist/stories/SilentSwapOverview.stories.js +364 -0
- package/dist/stories/useAuth.stories.d.ts +6 -0
- package/dist/stories/useAuth.stories.js +55 -0
- package/dist/stories/useSilentClient.stories.d.ts +9 -0
- package/dist/stories/useSilentClient.stories.js +39 -0
- package/dist/stories/useSilentOrders.stories.d.ts +1 -0
- package/dist/stories/useSilentOrders.stories.js +1 -0
- package/dist/stories/useSilentQuote.stories.d.ts +6 -0
- package/dist/stories/useSilentQuote.stories.js +267 -0
- package/dist/stories/useTransaction.stories.d.ts +6 -0
- package/dist/stories/useTransaction.stories.js +121 -0
- package/dist/utils/formatters.d.ts +33 -0
- package/dist/utils/formatters.js +82 -0
- package/package.json +67 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Output stage enum matching Svelte's OutputStage (string values)
|
|
4
|
+
*/
|
|
5
|
+
export var OutputStage;
|
|
6
|
+
(function (OutputStage) {
|
|
7
|
+
OutputStage["NONE"] = "NONE";
|
|
8
|
+
OutputStage["INIT"] = "INIT";
|
|
9
|
+
OutputStage["FUNDED"] = "FUNDED";
|
|
10
|
+
OutputStage["REDEEMED"] = "REDEEMED";
|
|
11
|
+
OutputStage["IBC_SENT"] = "IBC_SENT";
|
|
12
|
+
OutputStage["IBC_RCVD"] = "IBC_RCVD";
|
|
13
|
+
OutputStage["BRIDGE_SENT"] = "BRIDGE_SENT";
|
|
14
|
+
OutputStage["BRIDGE_CFRM"] = "BRIDGE_CFRM";
|
|
15
|
+
OutputStage["BRIDGE_RCVD"] = "BRIDGE_RCVD";
|
|
16
|
+
OutputStage["SWAP_USDC_GAS"] = "SWAP_USDC_GAS";
|
|
17
|
+
OutputStage["SWAP_USDC_TRG"] = "SWAP_USDC_TRG";
|
|
18
|
+
OutputStage["LTRL_TRG_SENT"] = "LTRL_TRG_SENT";
|
|
19
|
+
OutputStage["LTRL_TRG_RCVD"] = "LTRL_TRG_RCVD";
|
|
20
|
+
OutputStage["SWAP_TRG_DST"] = "SWAP_TRG_DST";
|
|
21
|
+
OutputStage["XFER_TRG_DST"] = "XFER_TRG_DST";
|
|
22
|
+
OutputStage["REFUND_NATIVE"] = "REFUND_NATIVE";
|
|
23
|
+
OutputStage["FINALIZED"] = "FINALIZED";
|
|
24
|
+
})(OutputStage || (OutputStage = {}));
|
|
25
|
+
/**
|
|
26
|
+
* Get human-readable status text from output stage
|
|
27
|
+
*/
|
|
28
|
+
export function getStatusTextFromStage(stage) {
|
|
29
|
+
switch (stage) {
|
|
30
|
+
case OutputStage.NONE:
|
|
31
|
+
return 'Uncertain state';
|
|
32
|
+
case OutputStage.INIT:
|
|
33
|
+
return 'Verifying deposit';
|
|
34
|
+
case OutputStage.FUNDED:
|
|
35
|
+
return 'Initializing';
|
|
36
|
+
case OutputStage.REDEEMED:
|
|
37
|
+
return 'Anonymizing';
|
|
38
|
+
case OutputStage.IBC_SENT:
|
|
39
|
+
return 'Moving';
|
|
40
|
+
case OutputStage.IBC_RCVD:
|
|
41
|
+
return 'Staging';
|
|
42
|
+
case OutputStage.BRIDGE_SENT:
|
|
43
|
+
case OutputStage.BRIDGE_CFRM:
|
|
44
|
+
case OutputStage.BRIDGE_RCVD:
|
|
45
|
+
return 'Bridging';
|
|
46
|
+
case OutputStage.SWAP_USDC_GAS:
|
|
47
|
+
return 'Fueling';
|
|
48
|
+
case OutputStage.SWAP_USDC_TRG:
|
|
49
|
+
return 'Swapping';
|
|
50
|
+
case OutputStage.LTRL_TRG_SENT:
|
|
51
|
+
case OutputStage.LTRL_TRG_RCVD:
|
|
52
|
+
case OutputStage.SWAP_TRG_DST:
|
|
53
|
+
return 'Finalizing';
|
|
54
|
+
case OutputStage.XFER_TRG_DST:
|
|
55
|
+
case OutputStage.REFUND_NATIVE:
|
|
56
|
+
case OutputStage.FINALIZED:
|
|
57
|
+
return 'Complete';
|
|
58
|
+
default:
|
|
59
|
+
return 'Processing';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get progress value (0-1) from output stage
|
|
64
|
+
*/
|
|
65
|
+
export function getProgressFromStage(stage) {
|
|
66
|
+
switch (stage) {
|
|
67
|
+
case OutputStage.NONE:
|
|
68
|
+
return 0;
|
|
69
|
+
case OutputStage.INIT:
|
|
70
|
+
return 0;
|
|
71
|
+
case OutputStage.FUNDED:
|
|
72
|
+
return 0.1;
|
|
73
|
+
case OutputStage.REDEEMED:
|
|
74
|
+
return 0.15;
|
|
75
|
+
case OutputStage.IBC_SENT:
|
|
76
|
+
return 0.25;
|
|
77
|
+
case OutputStage.IBC_RCVD:
|
|
78
|
+
return 0.35;
|
|
79
|
+
case OutputStage.BRIDGE_SENT:
|
|
80
|
+
return 0.5;
|
|
81
|
+
case OutputStage.BRIDGE_CFRM:
|
|
82
|
+
return 0.6;
|
|
83
|
+
case OutputStage.BRIDGE_RCVD:
|
|
84
|
+
return 0.7;
|
|
85
|
+
case OutputStage.SWAP_USDC_GAS:
|
|
86
|
+
return 0.8;
|
|
87
|
+
case OutputStage.SWAP_USDC_TRG:
|
|
88
|
+
return 0.9;
|
|
89
|
+
case OutputStage.LTRL_TRG_SENT:
|
|
90
|
+
case OutputStage.LTRL_TRG_RCVD:
|
|
91
|
+
case OutputStage.SWAP_TRG_DST:
|
|
92
|
+
return 0.95;
|
|
93
|
+
case OutputStage.XFER_TRG_DST:
|
|
94
|
+
case OutputStage.REFUND_NATIVE:
|
|
95
|
+
case OutputStage.FINALIZED:
|
|
96
|
+
return 1;
|
|
97
|
+
default:
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* React hook for tracking SilentSwap orders via WebSocket
|
|
103
|
+
*/
|
|
104
|
+
export function useOrderTracking({ client, orderId: initialOrderId, viewingAuth: initialAuth, onStatusUpdate, onError, onComplete, } = {}) {
|
|
105
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
106
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
107
|
+
const [error, setError] = useState(null);
|
|
108
|
+
const [orderStatus, setOrderStatus] = useState(null);
|
|
109
|
+
const [deposit, setDeposit] = useState(null);
|
|
110
|
+
const [outputs, setOutputs] = useState([]);
|
|
111
|
+
const [progresses, setProgresses] = useState([undefined]);
|
|
112
|
+
const [statusTexts, setStatusTexts] = useState(['Connecting']);
|
|
113
|
+
const [completedTimestamp, setCompletedTimestamp] = useState(null);
|
|
114
|
+
const [isComplete, setIsComplete] = useState(false);
|
|
115
|
+
const wsRef = useRef(null);
|
|
116
|
+
const orderIdRef = useRef(initialOrderId);
|
|
117
|
+
const authRef = useRef(initialAuth);
|
|
118
|
+
const requestCounterRef = useRef(0);
|
|
119
|
+
const responseHandlersRef = useRef(new Map());
|
|
120
|
+
const reconnectTimeoutRef = useRef(null);
|
|
121
|
+
const lastReceivedRef = useRef(0);
|
|
122
|
+
const isReconnectingRef = useRef(false);
|
|
123
|
+
const reconnectAttemptsRef = useRef(0);
|
|
124
|
+
const maxReconnectAttempts = 5; // Maximum number of reconnection attempts
|
|
125
|
+
const reconnectDelayRef = useRef(1000); // Initial delay in ms, will increase exponentially
|
|
126
|
+
// Track the last connected orderId/auth to prevent unnecessary reconnections
|
|
127
|
+
// These are used in both the auto-connect useEffect and the onclose handler
|
|
128
|
+
const lastConnectedOrderIdRef = useRef(undefined);
|
|
129
|
+
const lastConnectedAuthRef = useRef(undefined);
|
|
130
|
+
// Update output state when status changes
|
|
131
|
+
const updateOutput = useCallback((index, stage, timestamp = Date.now(), asset) => {
|
|
132
|
+
const progress = getProgressFromStage(stage);
|
|
133
|
+
const status = getStatusTextFromStage(stage);
|
|
134
|
+
setProgresses((prev) => {
|
|
135
|
+
const newProgresses = [...prev];
|
|
136
|
+
newProgresses[index] = progress;
|
|
137
|
+
return newProgresses;
|
|
138
|
+
});
|
|
139
|
+
setStatusTexts((prev) => {
|
|
140
|
+
const newTexts = [...prev];
|
|
141
|
+
newTexts[index] = status;
|
|
142
|
+
return newTexts;
|
|
143
|
+
});
|
|
144
|
+
setOutputs((prev) => {
|
|
145
|
+
const newOutputs = [...prev];
|
|
146
|
+
newOutputs[index] = {
|
|
147
|
+
...newOutputs[index],
|
|
148
|
+
index,
|
|
149
|
+
stage,
|
|
150
|
+
timestamp,
|
|
151
|
+
...(asset ? { asset } : {}),
|
|
152
|
+
// Preserve recipient if it exists
|
|
153
|
+
...(prev[index]?.recipient ? { recipient: prev[index].recipient } : {}),
|
|
154
|
+
};
|
|
155
|
+
return newOutputs;
|
|
156
|
+
});
|
|
157
|
+
// Check if all outputs are complete
|
|
158
|
+
setProgresses((currentProgresses) => {
|
|
159
|
+
if (currentProgresses.every((p) => (p ?? 0) >= 1)) {
|
|
160
|
+
setIsComplete(true);
|
|
161
|
+
setCompletedTimestamp(timestamp);
|
|
162
|
+
onComplete?.();
|
|
163
|
+
}
|
|
164
|
+
return currentProgresses;
|
|
165
|
+
});
|
|
166
|
+
}, [onComplete]);
|
|
167
|
+
// Submit JSON-RPC request
|
|
168
|
+
const submitRequest = useCallback((method, params, handler) => {
|
|
169
|
+
const ws = wsRef.current;
|
|
170
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
171
|
+
console.warn('WebSocket not connected, cannot send request');
|
|
172
|
+
return -1;
|
|
173
|
+
}
|
|
174
|
+
const requestId = requestCounterRef.current++;
|
|
175
|
+
const request = {
|
|
176
|
+
jsonrpc: '2.0',
|
|
177
|
+
id: requestId,
|
|
178
|
+
method,
|
|
179
|
+
params,
|
|
180
|
+
};
|
|
181
|
+
if (handler) {
|
|
182
|
+
responseHandlersRef.current.set(requestId, handler);
|
|
183
|
+
}
|
|
184
|
+
ws.send(JSON.stringify(request));
|
|
185
|
+
return requestId;
|
|
186
|
+
}, []);
|
|
187
|
+
// Connect to WebSocket using JSON-RPC protocol (matching Svelte implementation)
|
|
188
|
+
const connect = useCallback((orderId, auth) => {
|
|
189
|
+
orderIdRef.current = orderId;
|
|
190
|
+
authRef.current = auth;
|
|
191
|
+
setIsLoading(true);
|
|
192
|
+
setError(null);
|
|
193
|
+
isReconnectingRef.current = false;
|
|
194
|
+
// Clear any existing reconnection timeout
|
|
195
|
+
if (reconnectTimeoutRef.current) {
|
|
196
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
197
|
+
reconnectTimeoutRef.current = null;
|
|
198
|
+
}
|
|
199
|
+
// Close existing connection
|
|
200
|
+
if (wsRef.current) {
|
|
201
|
+
wsRef.current.close();
|
|
202
|
+
wsRef.current = null;
|
|
203
|
+
}
|
|
204
|
+
// Clear response handlers
|
|
205
|
+
responseHandlersRef.current.clear();
|
|
206
|
+
requestCounterRef.current = 0;
|
|
207
|
+
// Reset reconnection attempts and delay for new connection
|
|
208
|
+
reconnectAttemptsRef.current = 0;
|
|
209
|
+
reconnectDelayRef.current = 1000;
|
|
210
|
+
// Get WebSocket URL (matching Svelte implementation)
|
|
211
|
+
// Use env var if available, otherwise use client baseUrl, otherwise construct from location
|
|
212
|
+
// IMPORTANT: URL should be just /websocket with NO path or query params
|
|
213
|
+
// Auth is sent via JSON-RPC connect method, not in URL
|
|
214
|
+
let wsUrl;
|
|
215
|
+
if (typeof window !== 'undefined' && window.__WEBSOCKET_URL__) {
|
|
216
|
+
// WebSocket URL from window (for runtime override)
|
|
217
|
+
wsUrl = window.__WEBSOCKET_URL__;
|
|
218
|
+
}
|
|
219
|
+
else if (client) {
|
|
220
|
+
// Use client baseUrl (convert http/https to ws/wss)
|
|
221
|
+
const baseUrl = client.baseUrl;
|
|
222
|
+
try {
|
|
223
|
+
const url = new URL(baseUrl);
|
|
224
|
+
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
225
|
+
wsUrl = `${protocol}//${url.host}/websocket`;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// If URL parsing fails, try simple string replacement
|
|
229
|
+
wsUrl = baseUrl.replace(/^https?:\/\//, 'wss://').replace(/^http:\/\//, 'ws://') + '/websocket';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (typeof window !== 'undefined') {
|
|
233
|
+
// Construct from current location
|
|
234
|
+
wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/websocket`;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
wsUrl = 'ws://localhost/websocket';
|
|
238
|
+
}
|
|
239
|
+
// Ensure URL doesn't have any path or query params (just /websocket)
|
|
240
|
+
// This is critical: the WebSocket URL must be exactly: wss://domain/websocket (no params)
|
|
241
|
+
try {
|
|
242
|
+
const url = new URL(wsUrl);
|
|
243
|
+
// Force protocol to ws/wss (remove any existing path/query)
|
|
244
|
+
const protocol = url.protocol === 'https:' || url.protocol === 'wss:' ? 'wss:' : 'ws:';
|
|
245
|
+
// Reconstruct URL with only host and /websocket path
|
|
246
|
+
wsUrl = `${protocol}//${url.host}/websocket`;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// If URL parsing fails, try to clean it manually
|
|
250
|
+
// Remove any query params, hash, or extra paths
|
|
251
|
+
wsUrl = wsUrl.split('?')[0].split('#')[0].replace(/\/+$/, '') + '/websocket';
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const ws = new WebSocket(wsUrl);
|
|
255
|
+
wsRef.current = ws;
|
|
256
|
+
ws.onopen = () => {
|
|
257
|
+
lastReceivedRef.current = Date.now();
|
|
258
|
+
setIsConnected(true);
|
|
259
|
+
setIsLoading(false);
|
|
260
|
+
isReconnectingRef.current = false;
|
|
261
|
+
// Reset reconnection attempts on successful connection
|
|
262
|
+
reconnectAttemptsRef.current = 0;
|
|
263
|
+
reconnectDelayRef.current = 1000;
|
|
264
|
+
// Send connect JSON-RPC request (matching Svelte)
|
|
265
|
+
const connectRequestId = submitRequest('connect', {
|
|
266
|
+
auth: {
|
|
267
|
+
orderId,
|
|
268
|
+
viewingAuth: auth,
|
|
269
|
+
},
|
|
270
|
+
}, (error, result) => {
|
|
271
|
+
if (error) {
|
|
272
|
+
const err = new Error(error.message || 'Connection error');
|
|
273
|
+
setError(err);
|
|
274
|
+
setIsLoading(false);
|
|
275
|
+
onError?.(err);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// Handle connected response
|
|
279
|
+
const status = result;
|
|
280
|
+
setOrderStatus(status);
|
|
281
|
+
if (status.deposit) {
|
|
282
|
+
setDeposit({
|
|
283
|
+
amount: status.deposit.amount,
|
|
284
|
+
timestamp: status.deposit.timestamp,
|
|
285
|
+
duration: status.deposit.duration,
|
|
286
|
+
orderId: status.deposit.orderId,
|
|
287
|
+
tx: status.deposit.tx,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// Initialize outputs
|
|
291
|
+
const numOutputs = status.outputs?.length ?? 1;
|
|
292
|
+
setProgresses(new Array(numOutputs).fill(undefined));
|
|
293
|
+
setStatusTexts(new Array(numOutputs).fill('Connecting'));
|
|
294
|
+
// Process initial output statuses
|
|
295
|
+
status.outputs?.forEach((output, index) => {
|
|
296
|
+
// Set output with recipient information (matching Svelte)
|
|
297
|
+
setOutputs((prev) => {
|
|
298
|
+
const newOutputs = [...prev];
|
|
299
|
+
newOutputs[index] = {
|
|
300
|
+
index,
|
|
301
|
+
stage: output.stage,
|
|
302
|
+
timestamp: output.timestamp,
|
|
303
|
+
recipient: output.recipient,
|
|
304
|
+
asset: output.output,
|
|
305
|
+
txs: output.txs,
|
|
306
|
+
};
|
|
307
|
+
return newOutputs;
|
|
308
|
+
});
|
|
309
|
+
// Update progress and status
|
|
310
|
+
updateOutput(index, output.stage, output.timestamp, output.output);
|
|
311
|
+
});
|
|
312
|
+
// Register status handler for subsequent updates (matching Svelte pattern)
|
|
313
|
+
// Status updates come as responses to the same request ID
|
|
314
|
+
if (connectRequestId >= 0) {
|
|
315
|
+
responseHandlersRef.current.set(connectRequestId, (statusError, statusResult) => {
|
|
316
|
+
if (statusError) {
|
|
317
|
+
const err = new Error(statusError.message || 'Status error');
|
|
318
|
+
setError(err);
|
|
319
|
+
onError?.(err);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Handle status update
|
|
323
|
+
const update = statusResult;
|
|
324
|
+
handleStatusUpdate(update);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
onStatusUpdate?.(status);
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
ws.onmessage = (event) => {
|
|
331
|
+
lastReceivedRef.current = Date.now();
|
|
332
|
+
try {
|
|
333
|
+
const response = JSON.parse(event.data);
|
|
334
|
+
// Validate JSON-RPC response
|
|
335
|
+
if (response.jsonrpc !== '2.0' || typeof response.id !== 'number') {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// Get handler(s) for this request ID (matching Svelte pattern)
|
|
339
|
+
// Status updates come as multiple responses to the same request ID
|
|
340
|
+
// So we DON'T delete the handler - it stays registered for subsequent updates
|
|
341
|
+
const handler = responseHandlersRef.current.get(response.id);
|
|
342
|
+
if (handler) {
|
|
343
|
+
// Call handler but DON'T delete it - status updates reuse the same request ID
|
|
344
|
+
handler(response.error, response.result);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (parseError) {
|
|
348
|
+
console.warn('Failed to parse WebSocket message:', parseError);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
ws.onerror = (event) => {
|
|
352
|
+
const err = new Error('WebSocket connection error');
|
|
353
|
+
setError(err);
|
|
354
|
+
setIsLoading(false);
|
|
355
|
+
// Don't call onError here to prevent infinite error logging
|
|
356
|
+
// The error will be handled by onclose reconnection logic
|
|
357
|
+
};
|
|
358
|
+
ws.onclose = async () => {
|
|
359
|
+
setIsConnected(false);
|
|
360
|
+
responseHandlersRef.current.clear();
|
|
361
|
+
// Automatic reconnection with retry limit and exponential backoff
|
|
362
|
+
// Only reconnect if we still have valid orderId/auth and we're not already reconnecting
|
|
363
|
+
// Also check that the orderId/auth matches what we're supposed to be tracking
|
|
364
|
+
if (orderIdRef.current &&
|
|
365
|
+
authRef.current &&
|
|
366
|
+
!isReconnectingRef.current &&
|
|
367
|
+
orderIdRef.current === lastConnectedOrderIdRef.current &&
|
|
368
|
+
authRef.current === lastConnectedAuthRef.current) {
|
|
369
|
+
// Check if we've exceeded max reconnection attempts
|
|
370
|
+
if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
|
|
371
|
+
const err = new Error(`WebSocket connection failed after ${maxReconnectAttempts} attempts`);
|
|
372
|
+
setError(err);
|
|
373
|
+
setIsLoading(false);
|
|
374
|
+
onError?.(err);
|
|
375
|
+
// Reset attempts after a longer delay (30 seconds) to allow retry later
|
|
376
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
377
|
+
reconnectAttemptsRef.current = 0;
|
|
378
|
+
reconnectDelayRef.current = 1000;
|
|
379
|
+
}, 30000);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
isReconnectingRef.current = true;
|
|
383
|
+
reconnectAttemptsRef.current += 1;
|
|
384
|
+
// Exponential backoff: delay increases with each attempt (1s, 2s, 4s, 8s, 16s)
|
|
385
|
+
const currentDelay = reconnectDelayRef.current;
|
|
386
|
+
reconnectDelayRef.current = Math.min(currentDelay * 2, 30000); // Cap at 30 seconds
|
|
387
|
+
// If last received was a while ago, add extra delay
|
|
388
|
+
const timeSinceLastReceived = Date.now() - lastReceivedRef.current;
|
|
389
|
+
const extraDelay = timeSinceLastReceived > 90000 ? 5000 : 0;
|
|
390
|
+
const totalDelay = currentDelay + extraDelay;
|
|
391
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
392
|
+
// Double-check that orderId/auth still match before reconnecting
|
|
393
|
+
if (orderIdRef.current &&
|
|
394
|
+
authRef.current &&
|
|
395
|
+
orderIdRef.current === lastConnectedOrderIdRef.current &&
|
|
396
|
+
authRef.current === lastConnectedAuthRef.current) {
|
|
397
|
+
isReconnectingRef.current = false; // Reset before connecting
|
|
398
|
+
connect(orderIdRef.current, authRef.current);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
// Order changed, don't reconnect
|
|
402
|
+
isReconnectingRef.current = false;
|
|
403
|
+
reconnectAttemptsRef.current = 0;
|
|
404
|
+
reconnectDelayRef.current = 1000;
|
|
405
|
+
}
|
|
406
|
+
}, totalDelay);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
const error = err instanceof Error ? err : new Error('Failed to connect to order tracking');
|
|
412
|
+
setError(error);
|
|
413
|
+
setIsLoading(false);
|
|
414
|
+
onError?.(error);
|
|
415
|
+
}
|
|
416
|
+
}, [submitRequest, updateOutput, onStatusUpdate, onError]);
|
|
417
|
+
// Handle status updates
|
|
418
|
+
const handleStatusUpdate = useCallback((update) => {
|
|
419
|
+
switch (update.type) {
|
|
420
|
+
case 'deposit': {
|
|
421
|
+
setDeposit({
|
|
422
|
+
amount: update.data.amount,
|
|
423
|
+
timestamp: update.data.timestamp,
|
|
424
|
+
duration: update.data.duration,
|
|
425
|
+
orderId: update.data.orderId,
|
|
426
|
+
tx: update.data.tx,
|
|
427
|
+
});
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case 'stage': {
|
|
431
|
+
updateOutput(update.data.index, update.data.stage, update.data.timestamp, update.data.asset);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case 'transaction': {
|
|
435
|
+
setOutputs((prev) => {
|
|
436
|
+
const newOutputs = [...prev];
|
|
437
|
+
if (newOutputs[update.data.index]) {
|
|
438
|
+
const existingTxs = newOutputs[update.data.index].txs || {};
|
|
439
|
+
newOutputs[update.data.index] = {
|
|
440
|
+
...newOutputs[update.data.index],
|
|
441
|
+
txs: {
|
|
442
|
+
...existingTxs,
|
|
443
|
+
[update.data.kind]: {
|
|
444
|
+
txId: update.data.txId,
|
|
445
|
+
chain: update.data.chain,
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return newOutputs;
|
|
451
|
+
});
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case 'error': {
|
|
455
|
+
const err = new Error(update.data.message || 'Order tracking error');
|
|
456
|
+
setError(err);
|
|
457
|
+
onError?.(err);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}, [updateOutput, onError]);
|
|
462
|
+
// Disconnect from WebSocket
|
|
463
|
+
const disconnect = useCallback(() => {
|
|
464
|
+
// Clear reconnection timeout
|
|
465
|
+
if (reconnectTimeoutRef.current) {
|
|
466
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
467
|
+
reconnectTimeoutRef.current = null;
|
|
468
|
+
}
|
|
469
|
+
isReconnectingRef.current = true; // Prevent reconnection
|
|
470
|
+
orderIdRef.current = undefined;
|
|
471
|
+
authRef.current = undefined;
|
|
472
|
+
if (wsRef.current) {
|
|
473
|
+
wsRef.current.close();
|
|
474
|
+
wsRef.current = null;
|
|
475
|
+
}
|
|
476
|
+
responseHandlersRef.current.clear();
|
|
477
|
+
setIsConnected(false);
|
|
478
|
+
}, []);
|
|
479
|
+
// Auto-connect if orderId and auth are provided
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
// Only connect if orderId/auth changed or if not yet connected
|
|
482
|
+
// Also check if we're already connected to the same orderId/auth to prevent reconnection
|
|
483
|
+
const shouldConnect = initialOrderId &&
|
|
484
|
+
initialAuth &&
|
|
485
|
+
client &&
|
|
486
|
+
(lastConnectedOrderIdRef.current !== initialOrderId || lastConnectedAuthRef.current !== initialAuth) &&
|
|
487
|
+
!isReconnectingRef.current &&
|
|
488
|
+
(!wsRef.current || wsRef.current.readyState === WebSocket.CLOSED);
|
|
489
|
+
if (shouldConnect) {
|
|
490
|
+
lastConnectedOrderIdRef.current = initialOrderId;
|
|
491
|
+
lastConnectedAuthRef.current = initialAuth;
|
|
492
|
+
connect(initialOrderId, initialAuth);
|
|
493
|
+
}
|
|
494
|
+
return () => {
|
|
495
|
+
// Only disconnect if we're actually disconnecting (orderId/auth cleared)
|
|
496
|
+
if (!initialOrderId || !initialAuth) {
|
|
497
|
+
lastConnectedOrderIdRef.current = undefined;
|
|
498
|
+
lastConnectedAuthRef.current = undefined;
|
|
499
|
+
disconnect();
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
// Only depend on initialOrderId, initialAuth, and client
|
|
503
|
+
// Don't include connect/disconnect to prevent infinite loops
|
|
504
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
505
|
+
}, [initialOrderId, initialAuth, client]);
|
|
506
|
+
return {
|
|
507
|
+
// State
|
|
508
|
+
isConnected,
|
|
509
|
+
isLoading,
|
|
510
|
+
error,
|
|
511
|
+
orderStatus,
|
|
512
|
+
deposit,
|
|
513
|
+
outputs,
|
|
514
|
+
progresses,
|
|
515
|
+
statusTexts,
|
|
516
|
+
completedTimestamp,
|
|
517
|
+
isComplete,
|
|
518
|
+
// Methods
|
|
519
|
+
connect,
|
|
520
|
+
disconnect,
|
|
521
|
+
getStatusText: getStatusTextFromStage,
|
|
522
|
+
getProgress: getProgressFromStage,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { QuoteResponse, HdFacilitatorGroup, BridgeProvider, AssetInfo, QuoteRequest } from '@silentswap/sdk';
|
|
2
|
+
import type { SilentSwapWallet } from './useWallet.js';
|
|
3
|
+
export interface Destination {
|
|
4
|
+
asset: string;
|
|
5
|
+
contact: string;
|
|
6
|
+
amount: string;
|
|
7
|
+
priceUsd?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface QuoteCalculationResult {
|
|
10
|
+
quote: QuoteResponse;
|
|
11
|
+
usdcAmount: string;
|
|
12
|
+
facilitatorGroup: HdFacilitatorGroup | undefined;
|
|
13
|
+
bridgeProvider: BridgeProvider;
|
|
14
|
+
allowanceTarget: `0x${string}` | undefined;
|
|
15
|
+
}
|
|
16
|
+
export interface UseQuoteCalculationOptions {
|
|
17
|
+
/** User's address (EVM hex address or Solana base58 address) */
|
|
18
|
+
address: `0x${string}` | string;
|
|
19
|
+
/** EVM address */
|
|
20
|
+
evmAddress: `0x${string}` | string;
|
|
21
|
+
/** SilentSwap wallet instance */
|
|
22
|
+
wallet: SilentSwapWallet | null;
|
|
23
|
+
/** Depositor address from client */
|
|
24
|
+
depositorAddress: `0x${string}`;
|
|
25
|
+
/** Get quote function */
|
|
26
|
+
getQuote: (request: QuoteRequest) => Promise<QuoteResponse | null>;
|
|
27
|
+
/** Get price function */
|
|
28
|
+
getPrice: (asset: AssetInfo) => Promise<number>;
|
|
29
|
+
/** Set destinations callback */
|
|
30
|
+
setDestinations: (updater: Destination[] | ((prev: Destination[]) => Destination[])) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface UseQuoteCalculationReturn {
|
|
33
|
+
/** Calculate quote and return result */
|
|
34
|
+
calculateQuote: (sourceAsset: string, sourceAmount: string, destinations: Destination[], splits: number[]) => Promise<QuoteCalculationResult | null>;
|
|
35
|
+
/** Loading state for quote calculation */
|
|
36
|
+
loadingAmounts: boolean;
|
|
37
|
+
/** Deposit amount in USDC */
|
|
38
|
+
depositAmountUsdc: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Hook for calculating quotes and USDC amounts
|
|
42
|
+
*
|
|
43
|
+
* Extracted from useSilentQuote to improve modularity and reusability.
|
|
44
|
+
* Handles:
|
|
45
|
+
* - USDC amount calculation for different asset types (Solana, EVM, direct USDC)
|
|
46
|
+
* - Facilitator group resolution
|
|
47
|
+
* - Quote request construction and fetching
|
|
48
|
+
* - Destination amount updates
|
|
49
|
+
*/
|
|
50
|
+
export declare function useQuoteCalculation({ address, evmAddress, wallet, depositorAddress, getQuote, getPrice, setDestinations, }: UseQuoteCalculationOptions): UseQuoteCalculationReturn;
|