@silentswap/react 0.0.79 → 0.0.81
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 +5 -0
- package/dist/contexts/OrdersContext.js +53 -6
- package/dist/contexts/orderTrackingConnection.d.ts +19 -0
- package/dist/contexts/orderTrackingConnection.js +330 -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 +7 -148
- package/dist/hooks/silent/useOrderTracking.js +37 -622
- package/dist/hooks/silent/useSilentQuote.js +5 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/package.json +3 -3
|
@@ -1,632 +1,47 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
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
|
-
const wsCache = new Map();
|
|
102
|
-
const pendingCleanups = new Map();
|
|
103
|
-
const STATUS_UPDATE_TYPES = ['deposit', 'stage', 'transaction', 'error'];
|
|
104
|
-
function getWsCacheKey(orderId, auth) {
|
|
105
|
-
return `${orderId}|${auth}`;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Type guard: result is a full order status snapshot (priority, outputs, optional metadata)
|
|
109
|
-
* rather than a discriminated StatusUpdate (type + data). Server may push full status
|
|
110
|
-
* with no `type` field.
|
|
111
|
-
*/
|
|
112
|
-
function isFullOrderStatus(result) {
|
|
113
|
-
if (typeof result !== 'object' || result === null)
|
|
114
|
-
return false;
|
|
115
|
-
const r = result;
|
|
116
|
-
const hasOutputs = Array.isArray(r.outputs) && r.outputs.length > 0;
|
|
117
|
-
const hasType = typeof r.type === 'string' && STATUS_UPDATE_TYPES.includes(r.type);
|
|
118
|
-
return hasOutputs && !hasType;
|
|
119
|
-
}
|
|
120
|
-
/** True if status has at least one output at FINALIZED */
|
|
121
|
-
function hasFinalizedOutput(status) {
|
|
122
|
-
return status.outputs?.some((o) => o.stage === OutputStage.FINALIZED) ?? false;
|
|
123
|
-
}
|
|
124
|
-
// Build WebSocket URL from client config (pure function, no React deps)
|
|
125
|
-
function buildWsUrl(client) {
|
|
126
|
-
let wsUrl;
|
|
127
|
-
if (typeof window !== 'undefined' && window.__WEBSOCKET_URL__) {
|
|
128
|
-
wsUrl = window.__WEBSOCKET_URL__;
|
|
129
|
-
}
|
|
130
|
-
else if (client) {
|
|
131
|
-
const baseUrl = client.baseUrl;
|
|
132
|
-
try {
|
|
133
|
-
const url = new URL(baseUrl);
|
|
134
|
-
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
135
|
-
wsUrl = `${protocol}//${url.host}/websocket`;
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
wsUrl = baseUrl.replace(/^https?:\/\//, 'wss://').replace(/^http:\/\//, 'ws://') + '/websocket';
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
else if (typeof window !== 'undefined') {
|
|
142
|
-
wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/websocket`;
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
wsUrl = 'ws://localhost/websocket';
|
|
146
|
-
}
|
|
147
|
-
// Ensure URL is exactly: wss://domain/websocket (no extra path/query)
|
|
148
|
-
try {
|
|
149
|
-
const url = new URL(wsUrl);
|
|
150
|
-
const protocol = url.protocol === 'https:' || url.protocol === 'wss:' ? 'wss:' : 'ws:';
|
|
151
|
-
wsUrl = `${protocol}//${url.host}/websocket`;
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
wsUrl = wsUrl.split('?')[0].split('#')[0].replace(/\/+$/, '') + '/websocket';
|
|
155
|
-
}
|
|
156
|
-
return wsUrl;
|
|
157
|
-
}
|
|
158
|
-
// ─── Hook ───────────────────────────────────────────────────────────────────
|
|
159
|
-
/**
|
|
160
|
-
* React hook for tracking SilentSwap orders via WebSocket
|
|
161
|
-
*
|
|
162
|
-
* Uses a module-level singleton: one WebSocket per order, reused across
|
|
163
|
-
* React lifecycle events (Strict Mode, re-renders, effect re-runs).
|
|
164
|
-
* Reconnection matches Svelte ws_order_subscribe: immediate reconnect,
|
|
165
|
-
* 5s delay if idle >90s, no exponential backoff.
|
|
2
|
+
* Order tracking via WebSocket — state and connections live in OrdersContext.
|
|
3
|
+
* This hook subscribes to tracking for (orderId, auth) and returns the state from context.
|
|
166
4
|
*/
|
|
5
|
+
import { useEffect, useMemo } from 'react';
|
|
6
|
+
import { useOrdersContext } from '../../contexts/OrdersContext.js';
|
|
7
|
+
import { getOrderTrackingCacheKey, getStatusTextFromStage, getProgressFromStage, DEFAULT_ORDER_TRACKING_STATE, } from '../../contexts/orderTrackingTypes.js';
|
|
8
|
+
// Re-export types and helpers for consumers
|
|
9
|
+
export { OutputStage, getStatusTextFromStage, getProgressFromStage, getOrderTrackingCacheKey, } from '../../contexts/orderTrackingTypes.js';
|
|
167
10
|
export function useOrderTracking({ client, orderId: initialOrderId, viewingAuth: initialAuth, onStatusUpdate, onError, onComplete, fetchAssetPrice, } = {}) {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
onStatusUpdateRef.current = onStatusUpdate;
|
|
185
|
-
onErrorRef.current = onError;
|
|
186
|
-
onCompleteRef.current = onComplete;
|
|
187
|
-
fetchAssetPriceRef.current = fetchAssetPrice;
|
|
188
|
-
clientRef.current = client;
|
|
189
|
-
// Per-instance refs (point to the shared cached connection)
|
|
190
|
-
const wsRef = useRef(null);
|
|
191
|
-
const orderIdRef = useRef(undefined);
|
|
192
|
-
const authRef = useRef(undefined);
|
|
193
|
-
const cachedConnRef = useRef(null);
|
|
194
|
-
const reconnectTimeoutRef = useRef(null);
|
|
195
|
-
/** Latest order status (for same order) so we never overwrite finalized with stale "Verifying deposit" */
|
|
196
|
-
const orderStatusRef = useRef(null);
|
|
197
|
-
// Update output state when status changes
|
|
198
|
-
const updateOutput = useCallback((index, stage, timestamp = Date.now(), asset) => {
|
|
199
|
-
const progress = getProgressFromStage(stage);
|
|
200
|
-
const status = getStatusTextFromStage(stage);
|
|
201
|
-
setProgresses((prev) => {
|
|
202
|
-
const newProgresses = [...prev];
|
|
203
|
-
newProgresses[index] = progress;
|
|
204
|
-
return newProgresses;
|
|
205
|
-
});
|
|
206
|
-
setStatusTexts((prev) => {
|
|
207
|
-
const newTexts = [...prev];
|
|
208
|
-
newTexts[index] = status;
|
|
209
|
-
return newTexts;
|
|
210
|
-
});
|
|
211
|
-
setOutputs((prev) => {
|
|
212
|
-
const newOutputs = [...prev];
|
|
213
|
-
newOutputs[index] = {
|
|
214
|
-
...newOutputs[index],
|
|
215
|
-
index,
|
|
216
|
-
stage,
|
|
217
|
-
timestamp,
|
|
218
|
-
...(asset ? { asset } : {}),
|
|
219
|
-
...(prev[index]?.recipient ? { recipient: prev[index].recipient } : {}),
|
|
220
|
-
};
|
|
221
|
-
return newOutputs;
|
|
222
|
-
});
|
|
223
|
-
// Check if all outputs are complete
|
|
224
|
-
// Use Math.min for earliest completion time (matching Svelte)
|
|
225
|
-
setProgresses((currentProgresses) => {
|
|
226
|
-
if (currentProgresses.every((p) => (p ?? 0) >= 1)) {
|
|
227
|
-
setIsComplete(true);
|
|
228
|
-
setCompletedTimestamp((prev) => prev ? Math.min(prev, timestamp) : timestamp);
|
|
229
|
-
onCompleteRef.current?.();
|
|
230
|
-
}
|
|
231
|
-
return currentProgresses;
|
|
232
|
-
});
|
|
233
|
-
// Fetch USD price if missing (matching Svelte: k_asset?.priceUsd())
|
|
234
|
-
if (asset && asset.priceUsd === undefined && fetchAssetPriceRef.current) {
|
|
235
|
-
fetchAssetPriceRef.current(asset.caip19).then((price) => {
|
|
236
|
-
if (typeof price === 'number' && price > 0) {
|
|
237
|
-
setOutputs((prev) => {
|
|
238
|
-
const newOutputs = [...prev];
|
|
239
|
-
if (newOutputs[index]?.asset) {
|
|
240
|
-
newOutputs[index] = {
|
|
241
|
-
...newOutputs[index],
|
|
242
|
-
asset: { ...newOutputs[index].asset, priceUsd: price },
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
return newOutputs;
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}).catch(() => { });
|
|
249
|
-
}
|
|
250
|
-
}, []);
|
|
251
|
-
// Apply full OrderStatus to state (and cache). Used for initial connect response and server pushes with no `type`.
|
|
252
|
-
// Never overwrite a more advanced state (e.g. FINALIZED) with a stale one (e.g. "Verifying deposit") when messages arrive out of order.
|
|
253
|
-
const applyFullOrderStatusToState = useCallback((status, cacheConn) => {
|
|
254
|
-
const current = orderStatusRef.current;
|
|
255
|
-
const currentFinalized = current && hasFinalizedOutput(current);
|
|
256
|
-
const incomingFinalized = hasFinalizedOutput(status);
|
|
257
|
-
if (currentFinalized && !incomingFinalized) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
orderStatusRef.current = status;
|
|
261
|
-
if (cacheConn) {
|
|
262
|
-
cacheConn.initialStatus = status;
|
|
263
|
-
}
|
|
264
|
-
setOrderStatus(status);
|
|
265
|
-
if (status.deposit) {
|
|
266
|
-
setDeposit({
|
|
267
|
-
amount: status.deposit.amount,
|
|
268
|
-
timestamp: status.deposit.timestamp,
|
|
269
|
-
duration: status.deposit.duration,
|
|
270
|
-
orderId: status.deposit.orderId,
|
|
271
|
-
tx: status.deposit.tx,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
const numOutputs = status.outputs?.length ?? 1;
|
|
275
|
-
setProgresses(new Array(numOutputs).fill(undefined));
|
|
276
|
-
setStatusTexts(new Array(numOutputs).fill('Connecting'));
|
|
277
|
-
status.outputs?.forEach((output, index) => {
|
|
278
|
-
const outputAsset = output.output ?? (output.asset ? {
|
|
279
|
-
caip19: output.asset,
|
|
280
|
-
amount: output.value || '0',
|
|
281
|
-
decimals: 0,
|
|
282
|
-
} : undefined);
|
|
283
|
-
setOutputs((prev) => {
|
|
284
|
-
const newOutputs = [...prev];
|
|
285
|
-
newOutputs[index] = {
|
|
286
|
-
index,
|
|
287
|
-
stage: output.stage,
|
|
288
|
-
timestamp: output.timestamp,
|
|
289
|
-
recipient: output.recipient,
|
|
290
|
-
asset: outputAsset,
|
|
291
|
-
txs: output.txs,
|
|
292
|
-
};
|
|
293
|
-
return newOutputs;
|
|
294
|
-
});
|
|
295
|
-
updateOutput(index, output.stage, output.timestamp, outputAsset);
|
|
296
|
-
});
|
|
297
|
-
onStatusUpdateRef.current?.(status);
|
|
298
|
-
}, [updateOutput]);
|
|
299
|
-
// Handle status updates from WebSocket — also caches in the connection for future subscribers
|
|
300
|
-
const handleStatusUpdate = useCallback((update) => {
|
|
301
|
-
// Cache the update so new subscribers get it immediately
|
|
302
|
-
cachedConnRef.current?.statusUpdates.push(update);
|
|
303
|
-
switch (update.type) {
|
|
304
|
-
case 'deposit': {
|
|
305
|
-
setDeposit({
|
|
306
|
-
amount: update.data.amount,
|
|
307
|
-
timestamp: update.data.timestamp,
|
|
308
|
-
duration: update.data.duration,
|
|
309
|
-
orderId: update.data.orderId,
|
|
310
|
-
tx: update.data.tx,
|
|
311
|
-
});
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
case 'stage': {
|
|
315
|
-
updateOutput(update.data.index, update.data.stage, update.data.timestamp, update.data.asset);
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
case 'transaction': {
|
|
319
|
-
setOutputs((prev) => {
|
|
320
|
-
const newOutputs = [...prev];
|
|
321
|
-
if (newOutputs[update.data.index]) {
|
|
322
|
-
const existingTxs = newOutputs[update.data.index].txs || {};
|
|
323
|
-
newOutputs[update.data.index] = {
|
|
324
|
-
...newOutputs[update.data.index],
|
|
325
|
-
txs: {
|
|
326
|
-
...existingTxs,
|
|
327
|
-
[update.data.kind]: {
|
|
328
|
-
txId: update.data.txId,
|
|
329
|
-
chain: update.data.chain,
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
return newOutputs;
|
|
335
|
-
});
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
case 'error': {
|
|
339
|
-
const err = new Error(update.data.message || 'Order tracking error');
|
|
340
|
-
setError(err);
|
|
341
|
-
onErrorRef.current?.(err);
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}, [updateOutput]);
|
|
346
|
-
// Submit JSON-RPC request on the cached connection
|
|
347
|
-
const submitRequest = useCallback((conn, method, params, handler) => {
|
|
348
|
-
if (conn.ws.readyState !== WebSocket.OPEN) {
|
|
349
|
-
console.warn('WebSocket not connected, cannot send request');
|
|
350
|
-
return -1;
|
|
351
|
-
}
|
|
352
|
-
const requestId = conn.requestCounter++;
|
|
353
|
-
const request = {
|
|
354
|
-
jsonrpc: '2.0',
|
|
355
|
-
id: requestId,
|
|
356
|
-
method,
|
|
357
|
-
params,
|
|
358
|
-
};
|
|
359
|
-
if (handler) {
|
|
360
|
-
conn.responseHandlers.set(requestId, handler);
|
|
361
|
-
}
|
|
362
|
-
conn.ws.send(JSON.stringify(request));
|
|
363
|
-
return requestId;
|
|
364
|
-
}, []);
|
|
365
|
-
// Connect to WebSocket — uses module-level singleton cache
|
|
366
|
-
const connect = useCallback((orderId, auth) => {
|
|
367
|
-
const key = getWsCacheKey(orderId, auth);
|
|
368
|
-
// Cancel any pending cleanup for this order (React Strict Mode recovery)
|
|
369
|
-
const pendingCleanup = pendingCleanups.get(key);
|
|
370
|
-
if (pendingCleanup) {
|
|
371
|
-
clearTimeout(pendingCleanup);
|
|
372
|
-
pendingCleanups.delete(key);
|
|
373
|
-
}
|
|
374
|
-
// Clear any pending reconnect timeout
|
|
375
|
-
if (reconnectTimeoutRef.current) {
|
|
376
|
-
clearTimeout(reconnectTimeoutRef.current);
|
|
377
|
-
reconnectTimeoutRef.current = null;
|
|
378
|
-
}
|
|
379
|
-
orderIdRef.current = orderId;
|
|
380
|
-
authRef.current = auth;
|
|
381
|
-
// ── Singleton: reuse existing active connection for this order ──
|
|
382
|
-
const cached = wsCache.get(key);
|
|
383
|
-
if (cached && (cached.ws.readyState === WebSocket.OPEN || cached.ws.readyState === WebSocket.CONNECTING)) {
|
|
384
|
-
// Close any WS for a different order
|
|
385
|
-
if (wsRef.current && wsRef.current !== cached.ws) {
|
|
386
|
-
wsRef.current.close();
|
|
387
|
-
}
|
|
388
|
-
wsRef.current = cached.ws;
|
|
389
|
-
cachedConnRef.current = cached;
|
|
390
|
-
if (cached.ws.readyState === WebSocket.OPEN) {
|
|
391
|
-
setIsConnected(true);
|
|
392
|
-
setIsLoading(false);
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
setIsLoading(true);
|
|
396
|
-
}
|
|
397
|
-
// Hydrate React state from cached data so new subscribers skip skeletons
|
|
398
|
-
if (cached.initialStatus) {
|
|
399
|
-
const status = cached.initialStatus;
|
|
400
|
-
orderStatusRef.current = status;
|
|
401
|
-
setOrderStatus(status);
|
|
402
|
-
if (status.deposit) {
|
|
403
|
-
setDeposit({
|
|
404
|
-
amount: status.deposit.amount,
|
|
405
|
-
timestamp: status.deposit.timestamp,
|
|
406
|
-
duration: status.deposit.duration,
|
|
407
|
-
orderId: status.deposit.orderId,
|
|
408
|
-
tx: status.deposit.tx,
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
const numOutputs = status.outputs?.length ?? 1;
|
|
412
|
-
setProgresses(new Array(numOutputs).fill(undefined));
|
|
413
|
-
setStatusTexts(new Array(numOutputs).fill('Connecting'));
|
|
414
|
-
status.outputs?.forEach((output, index) => {
|
|
415
|
-
// Use resolved output.output if available, otherwise construct from top-level fields
|
|
416
|
-
// At early stages (INIT), output.output is undefined but asset/value are present
|
|
417
|
-
const outputAsset = output.output ?? (output.asset ? {
|
|
418
|
-
caip19: output.asset,
|
|
419
|
-
amount: output.value || '0',
|
|
420
|
-
decimals: 0,
|
|
421
|
-
} : undefined);
|
|
422
|
-
setOutputs((prev) => {
|
|
423
|
-
const newOutputs = [...prev];
|
|
424
|
-
newOutputs[index] = {
|
|
425
|
-
index,
|
|
426
|
-
stage: output.stage,
|
|
427
|
-
timestamp: output.timestamp,
|
|
428
|
-
recipient: output.recipient,
|
|
429
|
-
asset: outputAsset,
|
|
430
|
-
txs: output.txs,
|
|
431
|
-
};
|
|
432
|
-
return newOutputs;
|
|
433
|
-
});
|
|
434
|
-
updateOutput(index, output.stage, output.timestamp, outputAsset);
|
|
435
|
-
});
|
|
436
|
-
// Replay any status updates that arrived after the initial connect
|
|
437
|
-
for (const update of cached.statusUpdates) {
|
|
438
|
-
handleStatusUpdate(update);
|
|
439
|
-
}
|
|
440
|
-
onStatusUpdateRef.current?.(status);
|
|
441
|
-
}
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
// ── No active connection — create new WebSocket ──
|
|
445
|
-
// Close any existing WS for a different order
|
|
446
|
-
if (wsRef.current) {
|
|
447
|
-
wsRef.current.close();
|
|
448
|
-
wsRef.current = null;
|
|
449
|
-
}
|
|
450
|
-
// Remove stale cache entry
|
|
451
|
-
wsCache.delete(key);
|
|
452
|
-
// So first response for this order is not skipped (ref may still hold previous order's finalized state)
|
|
453
|
-
orderStatusRef.current = null;
|
|
454
|
-
setIsLoading(true);
|
|
455
|
-
setError(null);
|
|
456
|
-
const wsUrl = buildWsUrl(clientRef.current);
|
|
457
|
-
try {
|
|
458
|
-
const ws = new WebSocket(wsUrl);
|
|
459
|
-
// Create cached connection entry
|
|
460
|
-
const conn = {
|
|
461
|
-
ws,
|
|
462
|
-
orderId,
|
|
463
|
-
auth,
|
|
464
|
-
requestCounter: 0,
|
|
465
|
-
responseHandlers: new Map(),
|
|
466
|
-
lastReceived: 0,
|
|
467
|
-
reconnectTimeout: null,
|
|
468
|
-
initialStatus: null,
|
|
469
|
-
statusUpdates: [],
|
|
470
|
-
};
|
|
471
|
-
wsCache.set(key, conn);
|
|
472
|
-
wsRef.current = ws;
|
|
473
|
-
cachedConnRef.current = conn;
|
|
474
|
-
ws.onopen = () => {
|
|
475
|
-
conn.lastReceived = Date.now();
|
|
476
|
-
setIsConnected(true);
|
|
477
|
-
setIsLoading(false);
|
|
478
|
-
// Send connect JSON-RPC request (matching Svelte)
|
|
479
|
-
const connectRequestId = submitRequest(conn, 'connect', {
|
|
480
|
-
auth: {
|
|
481
|
-
orderId,
|
|
482
|
-
viewingAuth: auth,
|
|
483
|
-
},
|
|
484
|
-
}, (rpcError, result) => {
|
|
485
|
-
if (rpcError) {
|
|
486
|
-
const err = new Error(rpcError.message || 'Connection error');
|
|
487
|
-
setError(err);
|
|
488
|
-
setIsLoading(false);
|
|
489
|
-
onErrorRef.current?.(err);
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
// Handle connected response — cache for future subscribers
|
|
493
|
-
const status = result;
|
|
494
|
-
conn.statusUpdates = [];
|
|
495
|
-
applyFullOrderStatusToState(status, conn);
|
|
496
|
-
// Register status handler for subsequent updates (matching Svelte pattern).
|
|
497
|
-
// Server may push full status (priority, outputs, metadata) with no `type` — treat as OrderStatus.
|
|
498
|
-
if (connectRequestId >= 0) {
|
|
499
|
-
conn.responseHandlers.set(connectRequestId, (statusError, statusResult) => {
|
|
500
|
-
if (statusError) {
|
|
501
|
-
const err = new Error(statusError.message || 'Status error');
|
|
502
|
-
setError(err);
|
|
503
|
-
onErrorRef.current?.(err);
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
if (isFullOrderStatus(statusResult)) {
|
|
507
|
-
applyFullOrderStatusToState(statusResult, conn);
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
handleStatusUpdate(statusResult);
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
onStatusUpdateRef.current?.(status);
|
|
514
|
-
});
|
|
515
|
-
};
|
|
516
|
-
ws.onmessage = (event) => {
|
|
517
|
-
conn.lastReceived = Date.now();
|
|
518
|
-
try {
|
|
519
|
-
const response = JSON.parse(event.data);
|
|
520
|
-
if (response.jsonrpc !== '2.0' || typeof response.id !== 'number') {
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
// Status updates reuse the same request ID — don't delete handler
|
|
524
|
-
const handler = conn.responseHandlers.get(response.id);
|
|
525
|
-
if (handler) {
|
|
526
|
-
handler(response.error, response.result);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
catch (parseError) {
|
|
530
|
-
console.warn('Failed to parse WebSocket message:', parseError);
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
|
-
ws.onerror = () => {
|
|
534
|
-
setError(new Error('WebSocket connection error'));
|
|
535
|
-
setIsLoading(false);
|
|
536
|
-
};
|
|
537
|
-
// Reconnection on close (matching Svelte ws_order_subscribe):
|
|
538
|
-
// Always reconnect if still tracking, 5s delay if idle >90s
|
|
539
|
-
ws.onclose = () => {
|
|
540
|
-
// Remove from cache (stale)
|
|
541
|
-
if (wsCache.get(key)?.ws === ws) {
|
|
542
|
-
wsCache.delete(key);
|
|
543
|
-
}
|
|
544
|
-
// If this WS was replaced by a new one, don't reconnect
|
|
545
|
-
if (wsRef.current !== ws)
|
|
546
|
-
return;
|
|
547
|
-
setIsConnected(false);
|
|
548
|
-
conn.responseHandlers.clear();
|
|
549
|
-
conn.initialStatus = null;
|
|
550
|
-
conn.statusUpdates = [];
|
|
551
|
-
// Reconnect if still tracking this order (matching Svelte)
|
|
552
|
-
if (orderIdRef.current === orderId && authRef.current === auth) {
|
|
553
|
-
const idle = Date.now() - conn.lastReceived > 90_000;
|
|
554
|
-
if (idle) {
|
|
555
|
-
reconnectTimeoutRef.current = setTimeout(() => {
|
|
556
|
-
if (orderIdRef.current === orderId && authRef.current === auth) {
|
|
557
|
-
connect(orderId, auth);
|
|
558
|
-
}
|
|
559
|
-
}, 5000);
|
|
560
|
-
}
|
|
561
|
-
else {
|
|
562
|
-
connect(orderId, auth);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
catch (err) {
|
|
568
|
-
const connectError = err instanceof Error ? err : new Error('Failed to connect to order tracking');
|
|
569
|
-
setError(connectError);
|
|
570
|
-
setIsLoading(false);
|
|
571
|
-
onErrorRef.current?.(connectError);
|
|
572
|
-
}
|
|
573
|
-
}, [submitRequest, updateOutput, handleStatusUpdate, applyFullOrderStatusToState]);
|
|
574
|
-
// Disconnect — schedules delayed cleanup so WS survives React Strict Mode re-mount
|
|
575
|
-
const disconnect = useCallback(() => {
|
|
576
|
-
// Clear any pending reconnect timeout
|
|
577
|
-
if (reconnectTimeoutRef.current) {
|
|
578
|
-
clearTimeout(reconnectTimeoutRef.current);
|
|
579
|
-
reconnectTimeoutRef.current = null;
|
|
580
|
-
}
|
|
581
|
-
const orderId = orderIdRef.current;
|
|
582
|
-
const auth = authRef.current;
|
|
583
|
-
// Clear tracked order so onclose won't reconnect
|
|
584
|
-
orderIdRef.current = undefined;
|
|
585
|
-
authRef.current = undefined;
|
|
586
|
-
orderStatusRef.current = null;
|
|
587
|
-
const ws = wsRef.current;
|
|
588
|
-
wsRef.current = null;
|
|
589
|
-
cachedConnRef.current = null;
|
|
590
|
-
// Schedule delayed cleanup — if connect() is called quickly (Strict Mode),
|
|
591
|
-
// it will cancel this timeout and reuse the cached WS
|
|
592
|
-
if (ws && orderId && auth) {
|
|
593
|
-
const key = getWsCacheKey(orderId, auth);
|
|
594
|
-
pendingCleanups.set(key, setTimeout(() => {
|
|
595
|
-
pendingCleanups.delete(key);
|
|
596
|
-
// Only close if still in cache (wasn't reclaimed by a new connect)
|
|
597
|
-
if (wsCache.get(key)?.ws === ws) {
|
|
598
|
-
wsCache.delete(key);
|
|
599
|
-
ws.close();
|
|
600
|
-
}
|
|
601
|
-
}, 500));
|
|
602
|
-
}
|
|
603
|
-
setIsConnected(false);
|
|
604
|
-
}, []);
|
|
605
|
-
// Auto-connect when orderId/auth are provided
|
|
606
|
-
// connect and disconnect have stable identities (all deps are stable)
|
|
11
|
+
const ctx = useOrdersContext();
|
|
12
|
+
const stableState = useMemo(() => {
|
|
13
|
+
const key = initialOrderId && initialAuth
|
|
14
|
+
? getOrderTrackingCacheKey(initialOrderId, initialAuth)
|
|
15
|
+
: '';
|
|
16
|
+
const state = key ? ctx.getOrderTrackingState(initialOrderId, initialAuth) : null;
|
|
17
|
+
return state ?? DEFAULT_ORDER_TRACKING_STATE;
|
|
18
|
+
}, [initialOrderId, initialAuth, ctx]);
|
|
19
|
+
const options = useMemo(() => ({
|
|
20
|
+
client,
|
|
21
|
+
onStatusUpdate,
|
|
22
|
+
onError,
|
|
23
|
+
onComplete,
|
|
24
|
+
fetchAssetPrice,
|
|
25
|
+
}), [client, onStatusUpdate, onError, onComplete, fetchAssetPrice]);
|
|
26
|
+
const { connectOrderTracking, disconnectOrderTracking } = ctx;
|
|
607
27
|
useEffect(() => {
|
|
608
|
-
if (initialOrderId
|
|
609
|
-
|
|
610
|
-
|
|
28
|
+
if (!initialOrderId || !initialAuth || !client)
|
|
29
|
+
return;
|
|
30
|
+
connectOrderTracking(initialOrderId, initialAuth, options);
|
|
611
31
|
return () => {
|
|
612
|
-
|
|
32
|
+
disconnectOrderTracking(initialOrderId, initialAuth);
|
|
613
33
|
};
|
|
614
|
-
}, [initialOrderId, initialAuth,
|
|
34
|
+
}, [initialOrderId, initialAuth, client, options, connectOrderTracking, disconnectOrderTracking]);
|
|
615
35
|
return {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
completedTimestamp,
|
|
626
|
-
isComplete,
|
|
627
|
-
// Methods
|
|
628
|
-
connect,
|
|
629
|
-
disconnect,
|
|
36
|
+
...stableState,
|
|
37
|
+
connect: (orderId, auth) => {
|
|
38
|
+
ctx.connectOrderTracking(orderId, auth, options);
|
|
39
|
+
},
|
|
40
|
+
disconnect: () => {
|
|
41
|
+
if (initialOrderId && initialAuth) {
|
|
42
|
+
ctx.disconnectOrderTracking(initialOrderId, initialAuth);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
630
45
|
getStatusText: getStatusTextFromStage,
|
|
631
46
|
getProgress: getProgressFromStage,
|
|
632
47
|
};
|