@silentswap/react 0.1.57 → 0.1.58
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/BalancesContext.js +12 -1
- package/dist/contexts/OrdersContext.js +58 -8
- package/dist/contexts/PricesContext.js +109 -30
- package/dist/hooks/silent/useAuth.js +19 -12
- package/dist/hooks/useSwap.d.ts +13 -0
- package/dist/hooks/useSwap.js +35 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
|
@@ -10,9 +10,18 @@ import { useAssetsContext } from './AssetsContext.js';
|
|
|
10
10
|
import { usePrices } from '../hooks/usePrices.js';
|
|
11
11
|
import { isSolanaAsset, parseSolanaCaip19, isSolanaNativeToken, isSplToken, isEvmNativeToken, isBitcoinAsset, isBitcoinMainnetAsset, SB58_CHAIN_ID_SOLANA_MAINNET, A_VIEM_CHAINS, BITCOIN_CHAIN_ID, TRON_CHAIN_ID, H_RPCS, } from '@silentswap/sdk';
|
|
12
12
|
const BalancesContext = createContext(undefined);
|
|
13
|
+
// Cache for viem public clients keyed by chain id. Clients are immutable for
|
|
14
|
+
// a given chain/rpc and are safe to share across refetches — recreating one
|
|
15
|
+
// costs 10-50ms (transport init + viem setup) which adds up across 5-20 chains
|
|
16
|
+
// on every refetch trigger. Stored as `unknown` to avoid polluting the call
|
|
17
|
+
// site's narrow generic types inferred from createPublicClient.
|
|
18
|
+
const _viemClientCache = new Map();
|
|
13
19
|
// Utility: Create viem client with custom RPC endpoints where available
|
|
14
20
|
// Uses H_RPCS from @silentswap/sdk (rpc.ts) as the single source of truth
|
|
15
21
|
const createViemClient = (chain) => {
|
|
22
|
+
const cached = _viemClientCache.get(chain.id);
|
|
23
|
+
if (cached)
|
|
24
|
+
return cached;
|
|
16
25
|
const customRpc = H_RPCS[chain.id];
|
|
17
26
|
const chainWithCustomRpc = customRpc
|
|
18
27
|
? {
|
|
@@ -25,10 +34,12 @@ const createViemClient = (chain) => {
|
|
|
25
34
|
},
|
|
26
35
|
}
|
|
27
36
|
: chain;
|
|
28
|
-
|
|
37
|
+
const client = createPublicClient({
|
|
29
38
|
chain: chainWithCustomRpc,
|
|
30
39
|
transport: http(), // viem http transport with default timeout
|
|
31
40
|
});
|
|
41
|
+
_viemClientCache.set(chain.id, client);
|
|
42
|
+
return client;
|
|
32
43
|
};
|
|
33
44
|
// Utility: Separate EVM assets into ERC-20 and native
|
|
34
45
|
const separateEvmAssets = (chainAssets) => {
|
|
@@ -24,6 +24,35 @@ export const useWalletFacilitatorGroups = (wallet, setFacilitatorGroups) => {
|
|
|
24
24
|
}, [wallet, setFacilitatorGroups]);
|
|
25
25
|
};
|
|
26
26
|
const OrdersContext = createContext(undefined);
|
|
27
|
+
// Tuning for /recent fetch batching + retry. Rate limits are observed on accounts
|
|
28
|
+
// with many facilitator groups; keep concurrency low and retry 429/5xx with backoff.
|
|
29
|
+
const RECENT_ORDER_CONCURRENCY = 3;
|
|
30
|
+
const MAX_RECENT_ORDER_RETRIES = 4;
|
|
31
|
+
const RECENT_ORDER_BACKOFF_BASE_MS = 500;
|
|
32
|
+
const RECENT_ORDER_BACKOFF_MAX_MS = 8_000;
|
|
33
|
+
const RECENT_ORDER_BACKOFF_JITTER_MS = 250;
|
|
34
|
+
const parseRetryAfterHeader = (header) => {
|
|
35
|
+
if (!header)
|
|
36
|
+
return null;
|
|
37
|
+
const asSeconds = Number(header);
|
|
38
|
+
if (Number.isFinite(asSeconds))
|
|
39
|
+
return Math.max(0, asSeconds * 1000);
|
|
40
|
+
const asDate = Date.parse(header);
|
|
41
|
+
if (Number.isFinite(asDate))
|
|
42
|
+
return Math.max(0, asDate - Date.now());
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
const runWithConcurrency = async (items, limit, worker) => {
|
|
46
|
+
let cursor = 0;
|
|
47
|
+
const size = Math.max(1, Math.min(limit, items.length));
|
|
48
|
+
const runners = Array.from({ length: size }, async () => {
|
|
49
|
+
while (cursor < items.length) {
|
|
50
|
+
const index = cursor++;
|
|
51
|
+
await worker(items[index], index);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
await Promise.all(runners);
|
|
55
|
+
};
|
|
27
56
|
export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
28
57
|
const [orders, setOrders] = useState([]);
|
|
29
58
|
const [loading, setLoading] = useState(false);
|
|
@@ -107,12 +136,30 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
107
136
|
return [];
|
|
108
137
|
}
|
|
109
138
|
try {
|
|
110
|
-
|
|
111
|
-
const d_res = await fetch(`${baseUrl}/recent?${new URLSearchParams({
|
|
139
|
+
const url = `${baseUrl}/recent?${new URLSearchParams({
|
|
112
140
|
auth: sb58_auth_view,
|
|
113
|
-
})}
|
|
114
|
-
//
|
|
115
|
-
|
|
141
|
+
})}`;
|
|
142
|
+
// Retry on rate-limits (429) and transient server errors (5xx) with
|
|
143
|
+
// Retry-After / exponential backoff so accounts with many facilitator
|
|
144
|
+
// groups don't silently lose order data under burst throttling.
|
|
145
|
+
let d_res;
|
|
146
|
+
let sx_res = '';
|
|
147
|
+
for (let attempt = 0; attempt <= MAX_RECENT_ORDER_RETRIES; attempt++) {
|
|
148
|
+
d_res = await fetch(url);
|
|
149
|
+
if (d_res.status !== 429 && d_res.status < 500) {
|
|
150
|
+
sx_res = await d_res.text();
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (attempt === MAX_RECENT_ORDER_RETRIES) {
|
|
154
|
+
throw new Error(`Recent orders request failed with status ${d_res.status}`);
|
|
155
|
+
}
|
|
156
|
+
const retryAfterMs = parseRetryAfterHeader(d_res.headers.get('Retry-After'));
|
|
157
|
+
const backoffMs = retryAfterMs ?? Math.min(RECENT_ORDER_BACKOFF_BASE_MS * 2 ** attempt + Math.random() * RECENT_ORDER_BACKOFF_JITTER_MS, RECENT_ORDER_BACKOFF_MAX_MS);
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
159
|
+
}
|
|
160
|
+
if (!d_res || !d_res.ok) {
|
|
161
|
+
throw new Error(`Recent orders request failed with status ${d_res?.status ?? 'unknown'}`);
|
|
162
|
+
}
|
|
116
163
|
// Attempt to parse JSON (with safe parsing like Svelte)
|
|
117
164
|
let a_orders;
|
|
118
165
|
try {
|
|
@@ -165,8 +212,11 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
165
212
|
}
|
|
166
213
|
try {
|
|
167
214
|
const a_all_orders = [];
|
|
168
|
-
// Fetch orders for each facilitator group
|
|
169
|
-
|
|
215
|
+
// Fetch orders for each facilitator group, capped at RECENT_ORDER_CONCURRENCY
|
|
216
|
+
// to avoid bursting /recent with many parallel requests (which trips 429s for
|
|
217
|
+
// accounts with many facilitator groups). Per-request retry is handled inside
|
|
218
|
+
// request_recent_orders.
|
|
219
|
+
await runWithConcurrency(currentGroups, RECENT_ORDER_CONCURRENCY, async (f_group) => {
|
|
170
220
|
try {
|
|
171
221
|
const sb58_auth_view = await facilitator_group_authorize_order_view(f_group);
|
|
172
222
|
const a_recents = await request_recent_orders(sb58_auth_view);
|
|
@@ -184,7 +234,7 @@ export const OrdersProvider = ({ children, baseUrl: baseUrlProp }) => {
|
|
|
184
234
|
catch (error) {
|
|
185
235
|
console.warn('Failed to load orders for facilitator group:', error);
|
|
186
236
|
}
|
|
187
|
-
})
|
|
237
|
+
});
|
|
188
238
|
// Sort by modification time (newest first)
|
|
189
239
|
a_all_orders.sort((g_a, g_b) => (g_b.modified ?? 0) - (g_a.modified ?? 0));
|
|
190
240
|
setOrders(a_all_orders);
|
|
@@ -1,30 +1,110 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import { createContext, useContext, useState, useCallback, useMemo, useRef } from 'react';
|
|
4
4
|
import { usePrices } from '../hooks/usePrices.js';
|
|
5
5
|
const PRICE_STALE_AGE = 10 * 1000; // 10 seconds in milliseconds
|
|
6
|
+
const FAILURE_RETRY_MS = 30_000; // 30 seconds throttle for assets that failed to resolve a price
|
|
7
|
+
const FAILED_ASSETS_MAX = 1000; // cap on tracked failures to avoid unbounded growth
|
|
6
8
|
const PricesContext = createContext(undefined);
|
|
7
9
|
export const PricesProvider = ({ children }) => {
|
|
8
|
-
const { getPrice: fetchPrice } = usePrices();
|
|
10
|
+
const { getPrice: fetchPrice, getPrices: fetchPrices } = usePrices();
|
|
9
11
|
const [priceData, setPriceData] = useState({});
|
|
10
12
|
const [loadingAssets, setLoadingAssets] = useState(new Set());
|
|
11
|
-
//
|
|
13
|
+
// Batch queue: collects assets from multiple getPrice() calls and flushes
|
|
14
|
+
// them in a single API call. Uses a short timer window (not queueMicrotask)
|
|
15
|
+
// so calls made from components that mount on subsequent ticks still
|
|
16
|
+
// coalesce into one CoinGecko request instead of producing one per mount.
|
|
17
|
+
const batchQueueRef = useRef(new Map());
|
|
18
|
+
const batchScheduledRef = useRef(false);
|
|
19
|
+
const BATCH_WINDOW_MS = 50;
|
|
20
|
+
// Synchronous in-flight guard for fetchAndCachePrice. React state (loadingAssets)
|
|
21
|
+
// is not updated synchronously, so repeated calls within the same render pass would
|
|
22
|
+
// otherwise all see a stale `has()` === false and proceed concurrently.
|
|
23
|
+
const inFlightRef = useRef(new Set());
|
|
24
|
+
// Tracks the timestamp of the last failed fetch (unresolved price or thrown error)
|
|
25
|
+
// per caip19, so we can throttle re-enqueueing of assets that CoinGecko does not list.
|
|
26
|
+
// Bounded via FAILED_ASSETS_MAX with LRU-style eviction (Map iterates insertion order,
|
|
27
|
+
// so deleting+re-setting moves an entry to the end).
|
|
28
|
+
const failedAssetsRef = useRef(new Map());
|
|
29
|
+
const recordFailure = useCallback((caip19, now) => {
|
|
30
|
+
const map = failedAssetsRef.current;
|
|
31
|
+
if (map.has(caip19))
|
|
32
|
+
map.delete(caip19); // refresh insertion order
|
|
33
|
+
map.set(caip19, now);
|
|
34
|
+
if (map.size > FAILED_ASSETS_MAX) {
|
|
35
|
+
const oldest = map.keys().next().value;
|
|
36
|
+
if (oldest !== undefined)
|
|
37
|
+
map.delete(oldest);
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
// Flush all queued assets in a single batch API call
|
|
41
|
+
const flushBatch = useCallback(async () => {
|
|
42
|
+
batchScheduledRef.current = false;
|
|
43
|
+
const batch = Array.from(batchQueueRef.current.values());
|
|
44
|
+
batchQueueRef.current.clear();
|
|
45
|
+
if (batch.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
// Mark all as loading
|
|
48
|
+
setLoadingAssets((prev) => {
|
|
49
|
+
const next = new Set(prev);
|
|
50
|
+
batch.forEach((asset) => next.add(asset.caip19));
|
|
51
|
+
return next;
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const prices = await fetchPrices(batch);
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
batch.forEach((asset, i) => {
|
|
57
|
+
const price = prices[i];
|
|
58
|
+
if (typeof price === 'number' && price > 0) {
|
|
59
|
+
failedAssetsRef.current.delete(asset.caip19);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
recordFailure(asset.caip19, now);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
setPriceData((prev) => {
|
|
66
|
+
const next = { ...prev };
|
|
67
|
+
batch.forEach((asset, i) => {
|
|
68
|
+
const price = prices[i];
|
|
69
|
+
// Only mark fresh when we have a valid resolved price; leave unresolved
|
|
70
|
+
// entries untouched so they remain stale and get re-enqueued.
|
|
71
|
+
if (typeof price === 'number' && price > 0) {
|
|
72
|
+
next[asset.caip19] = { price, timestamp: now };
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return next;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.warn('Failed to batch fetch prices:', error);
|
|
80
|
+
// Do not write timestamps on transient failure — entries stay stale so the
|
|
81
|
+
// next getPrice() call will re-enqueue them. Throttle retries via failedAssetsRef.
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
batch.forEach((asset) => {
|
|
84
|
+
recordFailure(asset.caip19, now);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
setLoadingAssets((prev) => {
|
|
89
|
+
const next = new Set(prev);
|
|
90
|
+
batch.forEach((asset) => next.delete(asset.caip19));
|
|
91
|
+
return next;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}, [fetchPrices, recordFailure]);
|
|
95
|
+
// Fetch a single asset price (used by refetchPrice only)
|
|
12
96
|
const fetchAndCachePrice = useCallback(async (asset) => {
|
|
13
97
|
const caip19 = asset.caip19;
|
|
14
|
-
|
|
15
|
-
if (loadingAssets.has(caip19)) {
|
|
98
|
+
if (inFlightRef.current.has(caip19))
|
|
16
99
|
return;
|
|
17
|
-
|
|
100
|
+
inFlightRef.current.add(caip19);
|
|
18
101
|
setLoadingAssets((prev) => new Set(prev).add(caip19));
|
|
19
102
|
try {
|
|
20
103
|
const price = await fetchPrice(asset);
|
|
21
104
|
const now = Date.now();
|
|
22
105
|
setPriceData((prev) => ({
|
|
23
106
|
...prev,
|
|
24
|
-
[caip19]: {
|
|
25
|
-
price,
|
|
26
|
-
timestamp: now,
|
|
27
|
-
},
|
|
107
|
+
[caip19]: { price, timestamp: now },
|
|
28
108
|
}));
|
|
29
109
|
}
|
|
30
110
|
catch (error) {
|
|
@@ -32,20 +112,18 @@ export const PricesProvider = ({ children }) => {
|
|
|
32
112
|
const now = Date.now();
|
|
33
113
|
setPriceData((prev) => ({
|
|
34
114
|
...prev,
|
|
35
|
-
[caip19]: {
|
|
36
|
-
price: undefined,
|
|
37
|
-
timestamp: now,
|
|
38
|
-
},
|
|
115
|
+
[caip19]: { price: undefined, timestamp: now },
|
|
39
116
|
}));
|
|
40
117
|
}
|
|
41
118
|
finally {
|
|
119
|
+
inFlightRef.current.delete(caip19);
|
|
42
120
|
setLoadingAssets((prev) => {
|
|
43
121
|
const next = new Set(prev);
|
|
44
122
|
next.delete(caip19);
|
|
45
123
|
return next;
|
|
46
124
|
});
|
|
47
125
|
}
|
|
48
|
-
}, [fetchPrice
|
|
126
|
+
}, [fetchPrice]);
|
|
49
127
|
// Check if price is stale (older than 10 seconds)
|
|
50
128
|
const isPriceStale = useCallback((caip19) => {
|
|
51
129
|
const data = priceData[caip19];
|
|
@@ -54,33 +132,34 @@ export const PricesProvider = ({ children }) => {
|
|
|
54
132
|
const age = Date.now() - data.timestamp;
|
|
55
133
|
return age > PRICE_STALE_AGE;
|
|
56
134
|
}, [priceData]);
|
|
57
|
-
// Get price from cache or
|
|
135
|
+
// Get price from cache or queue for batch fetch
|
|
58
136
|
const getPrice = useCallback((asset) => {
|
|
59
137
|
if (!asset)
|
|
60
138
|
return undefined;
|
|
61
139
|
const caip19 = asset.caip19;
|
|
62
140
|
const data = priceData[caip19];
|
|
63
|
-
// If no cached data,
|
|
64
|
-
if (!data) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
141
|
+
// If no cached data or stale, queue for batch fetch
|
|
142
|
+
if (!data || isPriceStale(caip19)) {
|
|
143
|
+
const lastFailure = failedAssetsRef.current.get(caip19);
|
|
144
|
+
const isFailureThrottled = lastFailure !== undefined && Date.now() - lastFailure < FAILURE_RETRY_MS;
|
|
145
|
+
if (!isFailureThrottled &&
|
|
146
|
+
!loadingAssets.has(caip19) &&
|
|
147
|
+
!batchQueueRef.current.has(caip19)) {
|
|
148
|
+
batchQueueRef.current.set(caip19, asset);
|
|
149
|
+
if (!batchScheduledRef.current) {
|
|
150
|
+
batchScheduledRef.current = true;
|
|
151
|
+
setTimeout(() => flushBatch(), BATCH_WINDOW_MS);
|
|
152
|
+
}
|
|
74
153
|
}
|
|
75
154
|
}
|
|
76
155
|
// Return cached price (even if stale, to avoid flickering)
|
|
77
|
-
return data
|
|
78
|
-
}, [priceData, loadingAssets,
|
|
156
|
+
return data?.price;
|
|
157
|
+
}, [priceData, loadingAssets, isPriceStale, flushBatch]);
|
|
79
158
|
// Check if price is loading
|
|
80
159
|
const isLoading = useCallback((caip19) => {
|
|
81
160
|
return loadingAssets.has(caip19);
|
|
82
161
|
}, [loadingAssets]);
|
|
83
|
-
// Refetch price for
|
|
162
|
+
// Refetch price for a specific asset (explicit, not batched)
|
|
84
163
|
const refetchPrice = useCallback(async (asset) => {
|
|
85
164
|
await fetchAndCachePrice(asset);
|
|
86
165
|
}, [fetchAndCachePrice]);
|
|
@@ -298,22 +298,29 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
298
298
|
}
|
|
299
299
|
prevAddressRef.current = normalizedAddress;
|
|
300
300
|
}, [normalizedAddress]);
|
|
301
|
-
// Reset
|
|
302
|
-
//
|
|
303
|
-
//
|
|
301
|
+
// Reset auto-auth tracking when walletClient transitions to a new non-null
|
|
302
|
+
// reference. A new walletClient means a new wallet session (first connect,
|
|
303
|
+
// reconnect after disconnect, or wallet switch) — the auto-auth effect must
|
|
304
|
+
// re-evaluate. Without this, the hasAutoAuthenticatedRef flag from a prior
|
|
305
|
+
// session survives disconnect/reconnect (wagmi keeps the hook mounted) and
|
|
306
|
+
// silently blocks the sign-in prompt on the new session.
|
|
307
|
+
//
|
|
308
|
+
// Note we also reset in the happy path (previous auth succeeded), not only
|
|
309
|
+
// when `error` was set — cached auth may have been cleared externally
|
|
310
|
+
// (localStorage wipe, incognito, user script) so we must not assume the
|
|
311
|
+
// previous success is still valid for the new session.
|
|
304
312
|
const prevWalletClientRef = useRef(walletClient);
|
|
305
313
|
useEffect(() => {
|
|
306
314
|
if (prevWalletClientRef.current !== walletClient && walletClient) {
|
|
307
|
-
|
|
308
|
-
|
|
315
|
+
console.log('[useAuth] walletClient changed, resetting auto-auth flags');
|
|
316
|
+
hasAutoAuthenticatedRef.current = false;
|
|
317
|
+
autoAuthRetryCountRef.current = 0;
|
|
318
|
+
setAuthExhausted(false);
|
|
319
|
+
if (error)
|
|
309
320
|
setError(null);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (retryTimerRef.current) {
|
|
314
|
-
clearTimeout(retryTimerRef.current);
|
|
315
|
-
retryTimerRef.current = null;
|
|
316
|
-
}
|
|
321
|
+
if (retryTimerRef.current) {
|
|
322
|
+
clearTimeout(retryTimerRef.current);
|
|
323
|
+
retryTimerRef.current = null;
|
|
317
324
|
}
|
|
318
325
|
}
|
|
319
326
|
prevWalletClientRef.current = walletClient;
|
package/dist/hooks/useSwap.d.ts
CHANGED
|
@@ -65,3 +65,16 @@ export declare const useSwap: import("zustand").UseBoundStore<Omit<import("zusta
|
|
|
65
65
|
getOptions: () => Partial<import("zustand/middleware").PersistOptions<SwapState, unknown, unknown>>;
|
|
66
66
|
};
|
|
67
67
|
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Triggers a one-time rehydration of the swap store from localStorage and
|
|
70
|
+
* returns whether it has finished.
|
|
71
|
+
*
|
|
72
|
+
* Pair with `skipHydration: true` in the persist config: consumers gate their
|
|
73
|
+
* first render on `useSwapStoreHydration()` so they read the persisted values
|
|
74
|
+
* on the very first render, avoiding the classic defaults-then-persisted
|
|
75
|
+
* re-render flash.
|
|
76
|
+
*
|
|
77
|
+
* Safe during SSR — returns `true` so server-rendered markup doesn't wait on
|
|
78
|
+
* localStorage access that can't happen there.
|
|
79
|
+
*/
|
|
80
|
+
export declare const useSwapStoreHydration: () => boolean;
|
package/dist/hooks/useSwap.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
1
2
|
import { create } from 'zustand';
|
|
2
3
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
3
4
|
import { getAssetByCaip19 } from '@silentswap/sdk';
|
|
@@ -263,6 +264,10 @@ const useSwapStore = create()(persist((set, get) => ({
|
|
|
263
264
|
}), {
|
|
264
265
|
name: 'swap-store',
|
|
265
266
|
storage: createJSONStorage(() => localStorage),
|
|
267
|
+
// Hydrate manually via useSwapStoreHydration() so the first
|
|
268
|
+
// render of consumers already sees the persisted state, instead
|
|
269
|
+
// of flipping from defaults → persisted in a second render.
|
|
270
|
+
skipHydration: true,
|
|
266
271
|
// Persist privacy mode, tokenIn, and inputAmount
|
|
267
272
|
partialize: (state) => ({
|
|
268
273
|
privacyEnabled: state.privacyEnabled,
|
|
@@ -295,3 +300,33 @@ if (typeof window === 'undefined') {
|
|
|
295
300
|
useSwapStore.getState(); // Cache the snapshot on server
|
|
296
301
|
}
|
|
297
302
|
export const useSwap = useSwapStore;
|
|
303
|
+
/**
|
|
304
|
+
* Triggers a one-time rehydration of the swap store from localStorage and
|
|
305
|
+
* returns whether it has finished.
|
|
306
|
+
*
|
|
307
|
+
* Pair with `skipHydration: true` in the persist config: consumers gate their
|
|
308
|
+
* first render on `useSwapStoreHydration()` so they read the persisted values
|
|
309
|
+
* on the very first render, avoiding the classic defaults-then-persisted
|
|
310
|
+
* re-render flash.
|
|
311
|
+
*
|
|
312
|
+
* Safe during SSR — returns `true` so server-rendered markup doesn't wait on
|
|
313
|
+
* localStorage access that can't happen there.
|
|
314
|
+
*/
|
|
315
|
+
export const useSwapStoreHydration = () => {
|
|
316
|
+
const [hydrated, setHydrated] = useState(() => {
|
|
317
|
+
if (typeof window === 'undefined')
|
|
318
|
+
return true;
|
|
319
|
+
return useSwapStore.persist.hasHydrated();
|
|
320
|
+
});
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
if (useSwapStore.persist.hasHydrated()) {
|
|
323
|
+
setHydrated(true);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const unsub = useSwapStore.persist.onFinishHydration(() => setHydrated(true));
|
|
327
|
+
// Kick off rehydration once on mount.
|
|
328
|
+
void useSwapStore.persist.rehydrate();
|
|
329
|
+
return unsub;
|
|
330
|
+
}, []);
|
|
331
|
+
return hydrated;
|
|
332
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { useTransaction } from './hooks/useTransaction.js';
|
|
|
25
25
|
export { useQuote } from './hooks/useQuote.js';
|
|
26
26
|
export { useOrderEstimates } from './hooks/useOrderEstimates.js';
|
|
27
27
|
export { usePrices } from './hooks/usePrices.js';
|
|
28
|
-
export { useSwap, getSourceAssetCaip19, X_RANGE_SLIDER_MIN_GAP, OUTPUT_LIMIT, DEFAULT_SOURCE_ASSET, DEFAULT_DEST_ASSET, } from './hooks/useSwap.js';
|
|
28
|
+
export { useSwap, useSwapStoreHydration, getSourceAssetCaip19, X_RANGE_SLIDER_MIN_GAP, OUTPUT_LIMIT, DEFAULT_SOURCE_ASSET, DEFAULT_DEST_ASSET, } from './hooks/useSwap.js';
|
|
29
29
|
export type { Destination, SwapState } from './hooks/useSwap.js';
|
|
30
30
|
export { useEgressEstimates } from './hooks/useEgressEstimates.js';
|
|
31
31
|
export type { UseEgressEstimatesOptions } from './hooks/useEgressEstimates.js';
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { useTransaction } from './hooks/useTransaction.js';
|
|
|
17
17
|
export { useQuote } from './hooks/useQuote.js';
|
|
18
18
|
export { useOrderEstimates } from './hooks/useOrderEstimates.js';
|
|
19
19
|
export { usePrices } from './hooks/usePrices.js';
|
|
20
|
-
export { useSwap, getSourceAssetCaip19, X_RANGE_SLIDER_MIN_GAP, OUTPUT_LIMIT, DEFAULT_SOURCE_ASSET, DEFAULT_DEST_ASSET, } from './hooks/useSwap.js';
|
|
20
|
+
export { useSwap, useSwapStoreHydration, getSourceAssetCaip19, X_RANGE_SLIDER_MIN_GAP, OUTPUT_LIMIT, DEFAULT_SOURCE_ASSET, DEFAULT_DEST_ASSET, } from './hooks/useSwap.js';
|
|
21
21
|
export { useEgressEstimates } from './hooks/useEgressEstimates.js';
|
|
22
22
|
export { useStatus } from './hooks/useStatus.js';
|
|
23
23
|
export { PlatformHealthProvider, usePlatformHealthContext } from './hooks/usePlatformHealth.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silentswap/react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.58",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@bigmi/core": "^0.6.5",
|
|
26
26
|
"@ensdomains/ensjs": "^4.2.0",
|
|
27
|
-
"@silentswap/sdk": "0.1.
|
|
28
|
-
"@silentswap/ui-kit": "0.1.
|
|
27
|
+
"@silentswap/sdk": "0.1.58",
|
|
28
|
+
"@silentswap/ui-kit": "0.1.58",
|
|
29
29
|
"@solana/codecs-strings": "^5.1.0",
|
|
30
30
|
"@solana/kit": "^5.1.0",
|
|
31
31
|
"@solana/rpc": "^5.1.0",
|