@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.
@@ -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
- return createPublicClient({
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
- // Request current orders from backend using facilitator auth
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
- // Parse response body
115
- const sx_res = await d_res.text();
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
- await Promise.all(currentGroups.map(async (f_group) => {
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
- // Fetch price for an asset and cache it with timestamp
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
- // Skip if already loading
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, loadingAssets]);
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 trigger fetch
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, trigger fetch
64
- if (!data) {
65
- if (!loadingAssets.has(caip19)) {
66
- fetchAndCachePrice(asset);
67
- }
68
- return undefined;
69
- }
70
- // If price is stale, trigger refetch in background
71
- if (isPriceStale(caip19)) {
72
- if (!loadingAssets.has(caip19)) {
73
- fetchAndCachePrice(asset);
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.price;
78
- }, [priceData, loadingAssets, fetchAndCachePrice, isPriceStale]);
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 an asset
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 error and retry flag when walletClient changes (e.g. user reconnects
302
- // with a different wallet, or WalletConnect session is re-established).
303
- // Without this, a failed auto-auth permanently blocks retries.
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
- if (error) {
308
- console.log('[useAuth] walletClient changed, clearing error to allow retry');
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
- hasAutoAuthenticatedRef.current = false;
311
- autoAuthRetryCountRef.current = 0;
312
- setAuthExhausted(false);
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;
@@ -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;
@@ -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.57",
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.57",
28
- "@silentswap/ui-kit": "0.1.57",
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",