@solana/connector 0.1.5 → 0.1.7

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.
Files changed (38) hide show
  1. package/README.md +110 -45
  2. package/dist/{chunk-VMSZJPR5.mjs → chunk-APQGEW7S.mjs} +82 -11
  3. package/dist/chunk-APQGEW7S.mjs.map +1 -0
  4. package/dist/{chunk-3STZXVXD.mjs → chunk-JK47EFJT.mjs} +940 -381
  5. package/dist/chunk-JK47EFJT.mjs.map +1 -0
  6. package/dist/{chunk-QKVL45F6.mjs → chunk-TQRJYZNK.mjs} +5 -3
  7. package/dist/chunk-TQRJYZNK.mjs.map +1 -0
  8. package/dist/{chunk-JUZVCBAI.js → chunk-VA6LKXCQ.js} +85 -10
  9. package/dist/chunk-VA6LKXCQ.js.map +1 -0
  10. package/dist/{chunk-NQXK7PGX.js → chunk-VZ5Y6DIM.js} +19 -17
  11. package/dist/chunk-VZ5Y6DIM.js.map +1 -0
  12. package/dist/{chunk-ULUYX23Q.js → chunk-Z22V3D4E.js} +949 -388
  13. package/dist/chunk-Z22V3D4E.js.map +1 -0
  14. package/dist/compat.d.mts +1 -1
  15. package/dist/compat.d.ts +1 -1
  16. package/dist/headless.d.mts +99 -7
  17. package/dist/headless.d.ts +99 -7
  18. package/dist/headless.js +128 -112
  19. package/dist/headless.mjs +2 -2
  20. package/dist/index.d.mts +5 -4
  21. package/dist/index.d.ts +5 -4
  22. package/dist/index.js +163 -139
  23. package/dist/index.mjs +3 -3
  24. package/dist/react.d.mts +179 -13
  25. package/dist/react.d.ts +179 -13
  26. package/dist/react.js +36 -28
  27. package/dist/react.mjs +2 -2
  28. package/dist/{transaction-signer-D9d8nxwb.d.mts → transaction-signer-CpGEvp7S.d.mts} +1 -1
  29. package/dist/{transaction-signer-D9d8nxwb.d.ts → transaction-signer-CpGEvp7S.d.ts} +1 -1
  30. package/dist/{wallet-standard-shim--YcrQNRt.d.ts → wallet-standard-shim-D4CYG5sU.d.mts} +35 -6
  31. package/dist/{wallet-standard-shim-Dx7H8Ctf.d.mts → wallet-standard-shim-DiMvGjOk.d.ts} +35 -6
  32. package/package.json +1 -1
  33. package/dist/chunk-3STZXVXD.mjs.map +0 -1
  34. package/dist/chunk-JUZVCBAI.js.map +0 -1
  35. package/dist/chunk-NQXK7PGX.js.map +0 -1
  36. package/dist/chunk-QKVL45F6.mjs.map +0 -1
  37. package/dist/chunk-ULUYX23Q.js.map +0 -1
  38. package/dist/chunk-VMSZJPR5.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { installPolyfills, ConnectorErrorBoundary, isMainnetCluster, isDevnetCluster, isTestnetCluster, isLocalCluster, getClusterExplorerUrl, getClusterType, formatAddress, copyAddressToClipboard, createTransactionSigner, createKitTransactionSigner, NetworkError, getTransactionUrl, ConnectorClient } from './chunk-VMSZJPR5.mjs';
1
+ import { installPolyfills, ConnectorErrorBoundary, isMainnetCluster, isDevnetCluster, isTestnetCluster, isLocalCluster, getClusterExplorerUrl, getClusterType, formatAddress, copyAddressToClipboard, createTransactionSigner, createKitTransactionSigner, NetworkError, formatLamportsToSolSafe, getTransactionUrl, ConnectorClient, formatBigIntBalance, formatBigIntUsd } from './chunk-APQGEW7S.mjs';
2
2
  import { createLogger, __publicField, createSolanaClient, prepareTransaction } from './chunk-QL3IT3TS.mjs';
3
3
  import React, { createContext, useContext, useSyncExternalStore, useMemo, useState, useCallback, useRef, useEffect } from 'react';
4
4
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
@@ -54,15 +54,15 @@ function ConnectorProviderInternal({
54
54
  "solana:mainnet",
55
55
  "solana:devnet",
56
56
  "solana:testnet"
57
- ];
58
- registerMwa({
57
+ ], mwaConfig = {
59
58
  appIdentity: mobile.appIdentity,
60
59
  authorizationCache: mobile.authorizationCache ?? createDefaultAuthorizationCache(),
61
60
  chains: mobile.chains ?? defaultChains,
62
61
  chainSelector: mobile.chainSelector ?? createDefaultChainSelector(),
63
62
  remoteHostAuthority: mobile.remoteHostAuthority,
64
63
  onWalletNotFound: mobile.onWalletNotFound ?? createDefaultWalletNotFoundHandler()
65
- });
64
+ };
65
+ registerMwa(mwaConfig);
66
66
  } catch {
67
67
  }
68
68
  })(), () => {
@@ -299,73 +299,493 @@ function useTransactionPreparer() {
299
299
  [prepare, ready]
300
300
  );
301
301
  }
302
- var LAMPORTS_PER_SOL2 = 1000000000n;
303
- function formatSol(lamports, decimals = 4) {
304
- return (Number(lamports) / Number(LAMPORTS_PER_SOL2)).toLocaleString(void 0, {
305
- minimumFractionDigits: 0,
306
- maximumFractionDigits: decimals
307
- }) + " SOL";
308
- }
309
- function useBalance() {
310
- let { address: address$1, connected } = useAccount(); useCluster(); let client = useSolanaClient(), [lamports, setLamports] = useState(0n), [tokens, setTokens] = useState([]), [isLoading, setIsLoading] = useState(false), [error, setError] = useState(null), [lastUpdated, setLastUpdated] = useState(null), hasDataRef = useRef(false), rpcClient = client?.client ?? null, fetchBalance = useCallback(async () => {
311
- if (!connected || !address$1 || !rpcClient) {
312
- setLamports(0n), setTokens([]), hasDataRef.current = false;
302
+ var DEFAULT_CACHE_TIME_MS = 300 * 1e3, MAX_STORE_SIZE = 100, store = /* @__PURE__ */ new Map();
303
+ function evictStaleEntries() {
304
+ if (store.size <= MAX_STORE_SIZE) return;
305
+ let evictable = [];
306
+ for (let [key, entry] of store)
307
+ entry.subscribers.size === 0 && evictable.push([key, entry]);
308
+ evictable.sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt);
309
+ for (let [key, entry] of evictable) {
310
+ if (store.size <= MAX_STORE_SIZE) break;
311
+ entry.gcTimeoutId && clearTimeout(entry.gcTimeoutId), entry.intervalId && clearInterval(entry.intervalId), entry.abortController && entry.abortController.abort(), store.delete(key);
312
+ }
313
+ }
314
+ function getOrCreateEntry(key) {
315
+ let existing = store.get(key);
316
+ if (existing)
317
+ return existing.lastAccessedAt = Date.now(), existing;
318
+ let entry = {
319
+ snapshot: {
320
+ data: void 0,
321
+ error: null,
322
+ status: "idle",
323
+ updatedAt: null,
324
+ isFetching: false
325
+ },
326
+ subscribers: /* @__PURE__ */ new Set(),
327
+ promise: null,
328
+ abortController: null,
329
+ cacheTimeMs: DEFAULT_CACHE_TIME_MS,
330
+ gcTimeoutId: null,
331
+ queryFn: null,
332
+ intervalCounts: /* @__PURE__ */ new Map(),
333
+ activeIntervalMs: null,
334
+ intervalId: null,
335
+ lastAccessedAt: Date.now()
336
+ };
337
+ return store.set(key, entry), evictStaleEntries(), entry;
338
+ }
339
+ function emit(entry) {
340
+ for (let cb of entry.subscribers)
341
+ cb();
342
+ }
343
+ function setSnapshot(entry, next) {
344
+ let prev = entry.snapshot;
345
+ prev.status === next.status && prev.isFetching === next.isFetching && prev.updatedAt === next.updatedAt && prev.error === next.error && prev.data === next.data || (entry.snapshot = next, emit(entry));
346
+ }
347
+ function getMinIntervalMs(entry) {
348
+ let min = null;
349
+ for (let [ms, count] of entry.intervalCounts)
350
+ count <= 0 || (min === null || ms < min) && (min = ms);
351
+ return min;
352
+ }
353
+ function startOrRestartInterval(key, entry) {
354
+ let nextMs = getMinIntervalMs(entry);
355
+ if (nextMs === null) {
356
+ entry.intervalId && clearInterval(entry.intervalId), entry.intervalId = null, entry.activeIntervalMs = null;
357
+ return;
358
+ }
359
+ entry.activeIntervalMs === nextMs && entry.intervalId || (entry.intervalId && clearInterval(entry.intervalId), entry.activeIntervalMs = nextMs, entry.intervalId = setInterval(() => {
360
+ let fn = entry.queryFn;
361
+ fn && fetchSharedQuery(key, fn, { force: true });
362
+ }, nextMs));
363
+ }
364
+ function subscribeSharedQuery(key, onChange, cacheTimeMs) {
365
+ let entry = getOrCreateEntry(key);
366
+ return entry.subscribers.add(onChange), typeof cacheTimeMs == "number" && (entry.cacheTimeMs = Math.max(entry.cacheTimeMs, cacheTimeMs)), entry.gcTimeoutId && (clearTimeout(entry.gcTimeoutId), entry.gcTimeoutId = null), () => {
367
+ if (entry.subscribers.delete(onChange), entry.subscribers.size > 0) return;
368
+ entry.abortController && entry.abortController.abort(), entry.abortController = null, entry.promise = null, entry.intervalId && clearInterval(entry.intervalId), entry.intervalId = null, entry.activeIntervalMs = null, entry.intervalCounts.clear();
369
+ let gcDelayMs = entry.cacheTimeMs ?? DEFAULT_CACHE_TIME_MS;
370
+ entry.gcTimeoutId = setTimeout(() => {
371
+ entry.subscribers.size === 0 && store.delete(key);
372
+ }, gcDelayMs);
373
+ };
374
+ }
375
+ async function fetchSharedQuery(key, queryFn, options = {}) {
376
+ let entry = getOrCreateEntry(key);
377
+ entry.queryFn = queryFn;
378
+ let staleTimeMs = options.staleTimeMs ?? 0, now = Date.now();
379
+ if (options.force !== true && entry.snapshot.status === "success" && entry.snapshot.updatedAt !== null && now - entry.snapshot.updatedAt < staleTimeMs && entry.snapshot.data !== void 0)
380
+ return entry.snapshot.data;
381
+ if (entry.promise)
382
+ return entry.promise;
383
+ let controller = new AbortController();
384
+ entry.abortController = controller, options.signal && (options.signal.aborted ? controller.abort() : options.signal.addEventListener("abort", () => controller.abort(), { once: true }));
385
+ let isFirstLoad = entry.snapshot.status === "idle" && entry.snapshot.updatedAt === null;
386
+ setSnapshot(entry, {
387
+ ...entry.snapshot,
388
+ status: isFirstLoad ? "loading" : entry.snapshot.status,
389
+ isFetching: true,
390
+ error: null
391
+ });
392
+ let promise = (async () => {
393
+ try {
394
+ let data = await queryFn(controller.signal);
395
+ return setSnapshot(entry, {
396
+ data,
397
+ error: null,
398
+ status: "success",
399
+ updatedAt: Date.now(),
400
+ isFetching: false
401
+ }), data;
402
+ } catch (cause) {
403
+ if (controller.signal.aborted)
404
+ return setSnapshot(entry, {
405
+ data: entry.snapshot.data,
406
+ error: null,
407
+ status: entry.snapshot.status === "idle" ? "idle" : entry.snapshot.status,
408
+ updatedAt: entry.snapshot.updatedAt,
409
+ isFetching: false
410
+ }), entry.snapshot.data;
411
+ let error = cause instanceof Error ? cause : new Error(String(cause));
412
+ throw setSnapshot(entry, {
413
+ data: entry.snapshot.data,
414
+ error,
415
+ status: "error",
416
+ updatedAt: entry.snapshot.updatedAt,
417
+ isFetching: false
418
+ }), error;
419
+ } finally {
420
+ entry.promise = null, entry.abortController = null;
421
+ }
422
+ })();
423
+ return entry.promise = promise, promise;
424
+ }
425
+ function useSharedQuery(key, queryFn, options = {}) {
426
+ let {
427
+ enabled = true,
428
+ staleTimeMs = 0,
429
+ cacheTimeMs,
430
+ refetchOnMount = "stale",
431
+ refetchIntervalMs = false,
432
+ select
433
+ } = options, queryFnRef = useRef(queryFn);
434
+ queryFnRef.current = queryFn;
435
+ let stableQueryFn = useCallback((signal) => queryFnRef.current(signal), []), subscribe = useCallback(
436
+ (onChange) => key ? subscribeSharedQuery(key, onChange, cacheTimeMs) : () => {
437
+ },
438
+ [key, cacheTimeMs]
439
+ ), emptySnapshot = useMemo(
440
+ () => ({
441
+ data: void 0,
442
+ error: null,
443
+ status: "idle",
444
+ updatedAt: null,
445
+ isFetching: false
446
+ }),
447
+ []
448
+ ), getSnapshot = useCallback(() => key ? getOrCreateEntry(key).snapshot : emptySnapshot, [key, emptySnapshot]), snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot), selectedData = useMemo(() => select ? select(snapshot.data) : snapshot.data, [snapshot.data, select]), prevSelectedRef = useRef(selectedData), stableSelectedData = useMemo(() => Object.is(prevSelectedRef.current, selectedData) ? prevSelectedRef.current : (prevSelectedRef.current = selectedData, selectedData), [selectedData]), fetchedKeyRef = useRef(null);
449
+ useEffect(() => {
450
+ if (!key || !enabled) {
451
+ fetchedKeyRef.current = null;
313
452
  return;
314
453
  }
315
- setIsLoading(true), setError(null);
454
+ let entry = getOrCreateEntry(key);
455
+ if (entry.queryFn = stableQueryFn, fetchedKeyRef.current === key && entry.snapshot.status === "success")
456
+ return;
457
+ let current = entry.snapshot, isStale = current.updatedAt === null || Date.now() - current.updatedAt >= staleTimeMs;
458
+ refetchOnMount === true || current.status === "idle" || refetchOnMount === "stale" && isStale ? (fetchedKeyRef.current = key, fetchSharedQuery(key, stableQueryFn, {
459
+ staleTimeMs,
460
+ force: refetchOnMount === true
461
+ }).catch(() => {
462
+ })) : fetchedKeyRef.current = key;
463
+ }, [key, enabled, staleTimeMs, refetchOnMount, stableQueryFn]), useEffect(() => {
464
+ if (!key || !enabled || refetchIntervalMs === false) return;
465
+ let entry = getOrCreateEntry(key);
466
+ entry.queryFn = stableQueryFn;
467
+ let prev = entry.intervalCounts.get(refetchIntervalMs) ?? 0;
468
+ return entry.intervalCounts.set(refetchIntervalMs, prev + 1), startOrRestartInterval(key, entry), () => {
469
+ let count = entry.intervalCounts.get(refetchIntervalMs) ?? 0;
470
+ count <= 1 ? entry.intervalCounts.delete(refetchIntervalMs) : entry.intervalCounts.set(refetchIntervalMs, count - 1), startOrRestartInterval(key, entry);
471
+ };
472
+ }, [key, enabled, refetchIntervalMs, stableQueryFn]);
473
+ let refetch = useCallback(
474
+ async (refetchOptions) => {
475
+ if (key)
476
+ return fetchSharedQuery(key, stableQueryFn, {
477
+ force: true,
478
+ signal: refetchOptions?.signal
479
+ });
480
+ },
481
+ [key, stableQueryFn]
482
+ ), abort = useCallback(() => {
483
+ if (!key) return;
484
+ let entry = store.get(key);
485
+ entry?.abortController && entry.abortController.abort();
486
+ }, [key]);
487
+ return useMemo(
488
+ () => ({
489
+ data: stableSelectedData,
490
+ error: snapshot.error,
491
+ status: snapshot.status,
492
+ updatedAt: snapshot.updatedAt,
493
+ isFetching: snapshot.isFetching,
494
+ refetch,
495
+ abort
496
+ }),
497
+ [stableSelectedData, snapshot.error, snapshot.status, snapshot.updatedAt, snapshot.isFetching, refetch, abort]
498
+ );
499
+ }
500
+ function invalidateSharedQuery(key) {
501
+ let entry = store.get(key);
502
+ entry && setSnapshot(entry, {
503
+ ...entry.snapshot,
504
+ updatedAt: null
505
+ // Mark as stale
506
+ });
507
+ }
508
+ function clearSharedQueryCache() {
509
+ for (let [, entry] of store)
510
+ entry.intervalId && clearInterval(entry.intervalId), entry.gcTimeoutId && clearTimeout(entry.gcTimeoutId), entry.abortController && entry.abortController.abort();
511
+ store.clear();
512
+ }
513
+
514
+ // src/hooks/_internal/use-wallet-assets.ts
515
+ var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112";
516
+ function isRecord(value) {
517
+ return typeof value == "object" && value !== null;
518
+ }
519
+ function parseBigInt(value) {
520
+ if (typeof value == "bigint") return value;
521
+ if (typeof value == "number" && Number.isSafeInteger(value)) return BigInt(value);
522
+ if (typeof value == "string")
316
523
  try {
317
- let rpc = rpcClient.rpc, walletAddress = address(address$1), balanceResult = await rpc.getBalance(walletAddress).send();
318
- setLamports(balanceResult.value);
319
- try {
320
- let tokenProgramId = address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), tokenAccountsResult = await rpc.getTokenAccountsByOwner(walletAddress, { programId: tokenProgramId }, { encoding: "jsonParsed" }).send(), tokenBalances = [];
321
- for (let account of tokenAccountsResult.value) {
322
- let parsed = account.account.data;
323
- if (parsed?.parsed?.info) {
324
- let info = parsed.parsed.info, amount = BigInt(info.tokenAmount?.amount || "0"), decimals = info.tokenAmount?.decimals || 0, formatted = (Number(amount) / Math.pow(10, decimals)).toLocaleString(void 0, {
325
- minimumFractionDigits: 0,
326
- maximumFractionDigits: Math.min(decimals, 6)
327
- });
328
- amount > 0n && tokenBalances.push({
329
- mint: info.mint,
330
- amount,
331
- decimals,
332
- formatted
333
- });
334
- }
335
- }
336
- setTokens(tokenBalances);
337
- } catch (tokenError) {
338
- console.warn("Failed to fetch token balances:", tokenError), setTokens([]);
524
+ return BigInt(value);
525
+ } catch {
526
+ return 0n;
527
+ }
528
+ return 0n;
529
+ }
530
+ function getParsedTokenAccountInfo(data) {
531
+ if (!isRecord(data)) return null;
532
+ let parsed = data.parsed;
533
+ if (!isRecord(parsed)) return null;
534
+ let info = parsed.info;
535
+ return isRecord(info) ? info : null;
536
+ }
537
+ function parseTokenAccount(account, programId) {
538
+ let info = getParsedTokenAccountInfo(account.account.data);
539
+ if (!info) return null;
540
+ let mint = typeof info.mint == "string" ? info.mint : null, owner = typeof info.owner == "string" ? info.owner : null;
541
+ if (!mint || !owner) return null;
542
+ let tokenAmount = isRecord(info.tokenAmount) ? info.tokenAmount : null, amount = parseBigInt(tokenAmount?.amount), decimals = typeof tokenAmount?.decimals == "number" ? tokenAmount.decimals : 0, state = typeof info.state == "string" ? info.state : void 0;
543
+ return {
544
+ pubkey: String(account.pubkey),
545
+ mint,
546
+ owner,
547
+ amount,
548
+ decimals,
549
+ isFrozen: state === "frozen",
550
+ programId
551
+ };
552
+ }
553
+ function useWalletAssets(options = {}) {
554
+ let {
555
+ enabled = true,
556
+ staleTimeMs = 0,
557
+ cacheTimeMs = 300 * 1e3,
558
+ // 5 minutes
559
+ refetchOnMount = "stale",
560
+ refetchIntervalMs = false,
561
+ client: clientOverride,
562
+ select
563
+ } = options, { address: address$1, connected } = useAccount(), { client: providerClient } = useSolanaClient(), rpcClient = clientOverride ?? providerClient, key = useMemo(() => {
564
+ if (!enabled || !connected || !address$1 || !rpcClient) return null;
565
+ let rpcUrl = rpcClient.urlOrMoniker instanceof URL ? rpcClient.urlOrMoniker.toString() : String(rpcClient.urlOrMoniker);
566
+ return JSON.stringify(["wallet-assets", rpcUrl, address$1]);
567
+ }, [enabled, connected, address$1, rpcClient]), queryFn = useCallback(
568
+ async (signal) => {
569
+ if (!connected || !address$1 || !rpcClient)
570
+ return { lamports: 0n, tokenAccounts: [] };
571
+ if (signal.aborted)
572
+ throw new DOMException("Query aborted", "AbortError");
573
+ let rpc = rpcClient.rpc, walletAddress = address(address$1), tokenProgramId = address(TOKEN_PROGRAM_ID), token2022ProgramId = address(TOKEN_2022_PROGRAM_ID), [balanceResult, tokenAccountsResult, token2022AccountsResult] = await Promise.all([
574
+ rpc.getBalance(walletAddress).send(),
575
+ rpc.getTokenAccountsByOwner(walletAddress, { programId: tokenProgramId }, { encoding: "jsonParsed" }).send(),
576
+ rpc.getTokenAccountsByOwner(
577
+ walletAddress,
578
+ { programId: token2022ProgramId },
579
+ { encoding: "jsonParsed" }
580
+ ).send()
581
+ ]);
582
+ if (signal.aborted)
583
+ throw new DOMException("Query aborted", "AbortError");
584
+ let tokenAccounts = [];
585
+ for (let account of tokenAccountsResult.value) {
586
+ let parsed = parseTokenAccount(account, "token");
587
+ parsed && tokenAccounts.push(parsed);
339
588
  }
340
- hasDataRef.current = true, setLastUpdated(/* @__PURE__ */ new Date());
341
- } catch (err) {
342
- hasDataRef.current || (setError(err), console.error("Failed to fetch balance:", err));
343
- } finally {
344
- setIsLoading(false);
589
+ for (let account of token2022AccountsResult.value) {
590
+ let parsed = parseTokenAccount(account, "token-2022");
591
+ parsed && tokenAccounts.push(parsed);
592
+ }
593
+ return {
594
+ lamports: balanceResult.value,
595
+ tokenAccounts
596
+ };
597
+ },
598
+ [connected, address$1, rpcClient]
599
+ ), { data, error, status, updatedAt, isFetching, refetch, abort } = useSharedQuery(
600
+ key,
601
+ queryFn,
602
+ {
603
+ enabled,
604
+ staleTimeMs,
605
+ cacheTimeMs,
606
+ refetchOnMount,
607
+ refetchIntervalMs,
608
+ select
345
609
  }
346
- }, [connected, address$1, rpcClient]);
347
- useEffect(() => {
348
- fetchBalance();
349
- }, [fetchBalance]), useEffect(() => {
350
- if (!connected) return;
351
- let interval = setInterval(fetchBalance, 3e4);
352
- return () => clearInterval(interval);
353
- }, [connected, fetchBalance]);
354
- let solBalance = useMemo(() => Number(lamports) / Number(LAMPORTS_PER_SOL2), [lamports]), formattedSol = useMemo(() => formatSol(lamports), [lamports]);
610
+ ), isLoading = status === "loading" || status === "idle";
611
+ return useMemo(
612
+ () => ({
613
+ data,
614
+ isLoading,
615
+ isFetching,
616
+ error,
617
+ refetch,
618
+ abort,
619
+ updatedAt
620
+ }),
621
+ [data, isLoading, isFetching, error, refetch, abort, updatedAt]
622
+ );
623
+ }
624
+
625
+ // src/hooks/use-balance.ts
626
+ var LAMPORTS_PER_SOL2 = 1000000000n;
627
+ function formatTokenAccount(account) {
628
+ let formatted = formatBigIntBalance(account.amount, account.decimals, {
629
+ maxDecimals: Math.min(account.decimals, 6)
630
+ });
631
+ return {
632
+ mint: account.mint,
633
+ amount: account.amount,
634
+ decimals: account.decimals,
635
+ formatted
636
+ };
637
+ }
638
+ function selectBalance(assets) {
639
+ if (!assets)
640
+ return { lamports: 0n, tokens: [] };
641
+ let tokens = assets.tokenAccounts.filter((account) => account.amount > 0n).map(formatTokenAccount);
642
+ return {
643
+ lamports: assets.lamports,
644
+ tokens
645
+ };
646
+ }
647
+ function useBalance(options = {}) {
648
+ let {
649
+ enabled = true,
650
+ autoRefresh = true,
651
+ refreshInterval = 3e4,
652
+ staleTimeMs = 0,
653
+ cacheTimeMs = 300 * 1e3,
654
+ // 5 minutes
655
+ refetchOnMount = "stale",
656
+ client: clientOverride
657
+ } = options, {
658
+ data,
659
+ error,
660
+ isFetching,
661
+ updatedAt,
662
+ refetch: sharedRefetch,
663
+ abort
664
+ } = useWalletAssets({
665
+ enabled,
666
+ staleTimeMs,
667
+ cacheTimeMs,
668
+ refetchOnMount,
669
+ refetchIntervalMs: autoRefresh ? refreshInterval : false,
670
+ client: clientOverride,
671
+ select: selectBalance
672
+ }), lamports = data?.lamports ?? 0n, tokens = data?.tokens ?? [], solBalance = useMemo(() => Number(lamports) / Number(LAMPORTS_PER_SOL2), [lamports]), formattedSol = useMemo(() => formatLamportsToSolSafe(lamports, { maxDecimals: 4, suffix: true }), [lamports]), visibleError = updatedAt ? null : error, refetch = useCallback(
673
+ async (opts) => {
674
+ await sharedRefetch(opts);
675
+ },
676
+ [sharedRefetch]
677
+ );
355
678
  return useMemo(
356
679
  () => ({
357
680
  solBalance,
358
681
  lamports,
359
682
  formattedSol,
360
683
  tokens,
361
- isLoading,
362
- error,
363
- refetch: fetchBalance,
364
- lastUpdated
684
+ isLoading: isFetching,
685
+ error: visibleError,
686
+ refetch,
687
+ abort,
688
+ lastUpdated: updatedAt ? new Date(updatedAt) : null
365
689
  }),
366
- [solBalance, lamports, formattedSol, tokens, isLoading, error, fetchBalance, lastUpdated]
690
+ [solBalance, lamports, formattedSol, tokens, isFetching, visibleError, refetch, abort, updatedAt]
367
691
  );
368
692
  }
693
+
694
+ // src/utils/abort.ts
695
+ function createTimeoutSignal(ms) {
696
+ if (typeof AbortSignal.timeout == "function")
697
+ return { signal: AbortSignal.timeout(ms), cleanup: () => {
698
+ } };
699
+ let controller = new AbortController(), timeoutId = setTimeout(() => controller.abort(), ms);
700
+ return {
701
+ signal: controller.signal,
702
+ cleanup: () => clearTimeout(timeoutId)
703
+ };
704
+ }
705
+
706
+ // src/hooks/_internal/solana-token-list.ts
707
+ var CLUSTER_CHAIN_IDS = {
708
+ mainnet: 101,
709
+ testnet: 102,
710
+ devnet: 103,
711
+ localnet: 103,
712
+ // Use devnet tokens for localnet
713
+ custom: 101
714
+ // Default to mainnet for custom clusters
715
+ }, TOKEN_LIST_API_BASE_URL = "https://token-list-api.solana.cloud/v1/mints";
716
+ function getTokenListApiUrl(cluster = "mainnet") {
717
+ let chainId = CLUSTER_CHAIN_IDS[cluster];
718
+ return `${TOKEN_LIST_API_BASE_URL}?chainId=${chainId}`;
719
+ }
720
+ var DEFAULT_TIMEOUT_MS = 1e4, TOKEN_LIST_CACHE_MAX_SIZE = 1500, MAX_ADDRESSES_PER_REQUEST = 100, tokenListCache = /* @__PURE__ */ new Map();
721
+ function getCachedTokenListMetadata(mint) {
722
+ let value = tokenListCache.get(mint);
723
+ if (value)
724
+ return tokenListCache.delete(mint), tokenListCache.set(mint, value), value;
725
+ }
726
+ function setCachedTokenListMetadata(mint, value) {
727
+ if (tokenListCache.has(mint) && tokenListCache.delete(mint), tokenListCache.set(mint, value), tokenListCache.size > TOKEN_LIST_CACHE_MAX_SIZE) {
728
+ let oldestKey = tokenListCache.keys().next().value;
729
+ oldestKey && tokenListCache.delete(oldestKey);
730
+ }
731
+ }
732
+ function createLinkedSignal(externalSignal, timeoutMs) {
733
+ let controller = new AbortController(), onAbort = () => controller.abort();
734
+ externalSignal && (externalSignal.aborted ? controller.abort() : externalSignal.addEventListener("abort", onAbort, { once: true }));
735
+ let { signal: timeoutSignal, cleanup: cleanupTimeout } = createTimeoutSignal(timeoutMs), onTimeoutAbort = () => controller.abort();
736
+ return timeoutSignal.aborted ? controller.abort() : timeoutSignal.addEventListener("abort", onTimeoutAbort, { once: true }), {
737
+ signal: controller.signal,
738
+ cleanup: () => {
739
+ cleanupTimeout(), externalSignal && externalSignal.removeEventListener("abort", onAbort), timeoutSignal.removeEventListener("abort", onTimeoutAbort);
740
+ }
741
+ };
742
+ }
743
+ async function fetchSolanaTokenListMetadata(mints, options = {}) {
744
+ let results = /* @__PURE__ */ new Map();
745
+ if (!mints.length) return results;
746
+ let seen = /* @__PURE__ */ new Set(), uncached = [];
747
+ for (let mint of mints) {
748
+ let normalized = mint?.trim();
749
+ if (!normalized || seen.has(normalized)) continue;
750
+ seen.add(normalized);
751
+ let cached = getCachedTokenListMetadata(normalized);
752
+ cached ? results.set(normalized, cached) : uncached.push(normalized);
753
+ }
754
+ if (!uncached.length) return results;
755
+ let timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS, apiUrl = getTokenListApiUrl(options.cluster);
756
+ for (let i = 0; i < uncached.length && !options.signal?.aborted; i += MAX_ADDRESSES_PER_REQUEST) {
757
+ let batch = uncached.slice(i, i + MAX_ADDRESSES_PER_REQUEST), { signal, cleanup } = createLinkedSignal(options.signal, timeoutMs);
758
+ try {
759
+ let response = await fetch(apiUrl, {
760
+ method: "POST",
761
+ headers: { "Content-Type": "application/json" },
762
+ body: JSON.stringify({ addresses: batch }),
763
+ signal
764
+ });
765
+ if (!response.ok) {
766
+ console.warn("[token-list] Solana Token List API error:", response.status, response.statusText);
767
+ continue;
768
+ }
769
+ let data = await response.json();
770
+ if (!data?.content?.length) continue;
771
+ for (let item of data.content)
772
+ item?.address && (results.set(item.address, item), setCachedTokenListMetadata(item.address, item));
773
+ } catch (error) {
774
+ console.warn("[token-list] Solana Token List API failed:", error);
775
+ } finally {
776
+ cleanup();
777
+ }
778
+ }
779
+ return results;
780
+ }
781
+
782
+ // src/utils/image.ts
783
+ function transformImageUrl(url, imageProxy) {
784
+ if (url)
785
+ return imageProxy ? `${imageProxy}${encodeURIComponent(url)}` : url;
786
+ }
787
+
788
+ // src/hooks/use-transactions.ts
369
789
  function formatDate(timestamp) {
370
790
  if (!timestamp)
371
791
  return { date: "Unknown", time: "" };
@@ -377,6 +797,45 @@ function formatDate(timestamp) {
377
797
  });
378
798
  return { date: formattedDate, time: formattedTime };
379
799
  }
800
+ var KNOWN_PROGRAMS = {
801
+ "11111111111111111111111111111111": "System Program",
802
+ TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "Token Program",
803
+ ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL: "Associated Token",
804
+ JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: "Jupiter",
805
+ whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc: "Orca Whirlpool",
806
+ "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": "Raydium AMM",
807
+ Stake11111111111111111111111111111111111111: "Stake Program",
808
+ metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s: "Metaplex"
809
+ }, DEFAULT_IGNORED_PROGRAM_IDS = /* @__PURE__ */ new Set([
810
+ "11111111111111111111111111111111",
811
+ // System Program
812
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
813
+ // Token Program
814
+ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
815
+ // Associated Token
816
+ ]);
817
+ function resolveProgramName(programId, programLabels) {
818
+ return programLabels?.[programId] ?? KNOWN_PROGRAMS[programId];
819
+ }
820
+ function pickPrimaryProgramId(programIds) {
821
+ for (let id of programIds)
822
+ if (!DEFAULT_IGNORED_PROGRAM_IDS.has(id)) return id;
823
+ return programIds.values().next().value;
824
+ }
825
+ function getParsedInstructionTypes(message) {
826
+ if (!Array.isArray(message.instructions)) return;
827
+ let types = [];
828
+ for (let ix of message.instructions) {
829
+ if (!ix || typeof ix != "object") continue;
830
+ let parsed = ix.parsed;
831
+ if (!parsed || typeof parsed != "object" || !("type" in parsed)) continue;
832
+ let t = parsed.type;
833
+ if (typeof t == "string" && (types.push(t), types.length >= 10))
834
+ break;
835
+ }
836
+ let unique = [...new Set(types)];
837
+ return unique.length ? unique : void 0;
838
+ }
380
839
  function isAccountKey(value) {
381
840
  return typeof value == "object" && value !== null && "pubkey" in value && typeof value.pubkey == "string";
382
841
  }
@@ -401,6 +860,13 @@ function isTransactionWithMeta(value) {
401
860
  function isTransactionMessage(value) {
402
861
  return typeof value == "object" && value !== null && "accountKeys" in value && Array.isArray(value.accountKeys);
403
862
  }
863
+ function coerceMaybeAddressString(value) {
864
+ if (typeof value == "string") return value;
865
+ if (value && typeof value == "object") {
866
+ let str = String(value);
867
+ if (str && str !== "[object Object]") return str;
868
+ }
869
+ }
404
870
  function getAccountKeys(message) {
405
871
  return Array.isArray(message.accountKeys) ? message.accountKeys.map((key) => typeof key == "string" ? key : isAccountKey(key) ? key.pubkey : "").filter(Boolean) : [];
406
872
  }
@@ -432,12 +898,12 @@ function parseTokenTransfers(meta, accountKeys, walletAddress) {
432
898
  return null;
433
899
  let preTokenBalances = Array.isArray(meta.preTokenBalances) ? meta.preTokenBalances : [], postTokenBalances = Array.isArray(meta.postTokenBalances) ? meta.postTokenBalances : [], ourPreTokens = preTokenBalances.filter((balance) => {
434
900
  if (!isTokenBalance(balance)) return false;
435
- let accountKey = accountKeys[balance.accountIndex];
436
- return accountKey && accountKey.trim() === walletAddress.trim() || balance.owner && balance.owner.trim() === walletAddress.trim();
901
+ let accountKey = accountKeys[balance.accountIndex], owner = coerceMaybeAddressString(balance.owner);
902
+ return accountKey && accountKey.trim() === walletAddress.trim() || owner && owner.trim() === walletAddress.trim();
437
903
  }), ourPostTokens = postTokenBalances.filter((balance) => {
438
904
  if (!isTokenBalance(balance)) return false;
439
- let accountKey = accountKeys[balance.accountIndex];
440
- return accountKey && accountKey.trim() === walletAddress.trim() || balance.owner && balance.owner.trim() === walletAddress.trim();
905
+ let accountKey = accountKeys[balance.accountIndex], owner = coerceMaybeAddressString(balance.owner);
906
+ return accountKey && accountKey.trim() === walletAddress.trim() || owner && owner.trim() === walletAddress.trim();
441
907
  }), allMints = /* @__PURE__ */ new Set();
442
908
  for (let token of ourPreTokens)
443
909
  isTokenBalance(token) && allMints.add(token.mint);
@@ -481,6 +947,52 @@ function parseTokenTransfers(meta, accountKeys, walletAddress) {
481
947
  }
482
948
  return null;
483
949
  }
950
+ function parseTokenAccountClosure(meta, accountKeys, walletAddress) {
951
+ if (!isTransactionMeta(meta))
952
+ return null;
953
+ let preTokenBalances = Array.isArray(meta.preTokenBalances) ? meta.preTokenBalances : [], postTokenBalances = Array.isArray(meta.postTokenBalances) ? meta.postTokenBalances : [], ourPreTokens = preTokenBalances.filter((balance) => {
954
+ if (!isTokenBalance(balance)) return false;
955
+ let accountKey = accountKeys[balance.accountIndex], owner = coerceMaybeAddressString(balance.owner);
956
+ return accountKey && accountKey.trim() === walletAddress.trim() || owner && owner.trim() === walletAddress.trim();
957
+ }), ourPostTokens = postTokenBalances.filter((balance) => {
958
+ if (!isTokenBalance(balance)) return false;
959
+ let accountKey = accountKeys[balance.accountIndex], owner = coerceMaybeAddressString(balance.owner);
960
+ return accountKey && accountKey.trim() === walletAddress.trim() || owner && owner.trim() === walletAddress.trim();
961
+ }), postKeys = /* @__PURE__ */ new Set();
962
+ for (let token of ourPostTokens)
963
+ isTokenBalance(token) && postKeys.add(`${token.accountIndex}:${token.mint}`);
964
+ for (let token of ourPreTokens) {
965
+ if (!isTokenBalance(token)) continue;
966
+ let key = `${token.accountIndex}:${token.mint}`;
967
+ if (!postKeys.has(key))
968
+ return { tokenMint: token.mint };
969
+ }
970
+ return null;
971
+ }
972
+ function parseSwapTokens(meta, accountKeys, walletAddress, solChange) {
973
+ if (!isTransactionMeta(meta))
974
+ return {};
975
+ let preTokenBalances = Array.isArray(meta.preTokenBalances) ? meta.preTokenBalances : [], postTokenBalances = Array.isArray(meta.postTokenBalances) ? meta.postTokenBalances : [], ourPreTokens = preTokenBalances.filter((balance) => {
976
+ if (!isTokenBalance(balance)) return false;
977
+ let accountKey = accountKeys[balance.accountIndex], owner = coerceMaybeAddressString(balance.owner);
978
+ return accountKey && accountKey.trim() === walletAddress.trim() || owner && owner.trim() === walletAddress.trim();
979
+ }), ourPostTokens = postTokenBalances.filter((balance) => {
980
+ if (!isTokenBalance(balance)) return false;
981
+ let accountKey = accountKeys[balance.accountIndex], owner = coerceMaybeAddressString(balance.owner);
982
+ return accountKey && accountKey.trim() === walletAddress.trim() || owner && owner.trim() === walletAddress.trim();
983
+ }), allMints = /* @__PURE__ */ new Set();
984
+ for (let token of ourPreTokens)
985
+ isTokenBalance(token) && allMints.add(token.mint);
986
+ for (let token of ourPostTokens)
987
+ isTokenBalance(token) && allMints.add(token.mint);
988
+ let fromToken, toToken;
989
+ for (let mint of allMints) {
990
+ let preBal = ourPreTokens.find((b) => isTokenBalance(b) && b.mint === mint), postBal = ourPostTokens.find((b) => isTokenBalance(b) && b.mint === mint), preAmount = isTokenBalance(preBal) && isUiTokenAmount(preBal.uiTokenAmount) ? Number(preBal.uiTokenAmount.amount) : 0, change = (isTokenBalance(postBal) && isUiTokenAmount(postBal.uiTokenAmount) ? Number(postBal.uiTokenAmount.amount) : 0) - preAmount;
991
+ change < 0 && !fromToken ? fromToken = { mint } : change > 0 && !toToken && (toToken = { mint });
992
+ }
993
+ let WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112";
994
+ return solChange < -1e-3 && !fromToken ? fromToken = { mint: WRAPPED_SOL_MINT } : solChange > 1e-3 && !toToken && (toToken = { mint: WRAPPED_SOL_MINT }), { fromToken, toToken };
995
+ }
484
996
  function formatAmount(tokenAmount, tokenDecimals, direction, solChange) {
485
997
  if (tokenAmount !== void 0 && tokenDecimals !== void 0 && direction !== void 0) {
486
998
  let sign = direction === "in" ? "+" : "-", maxDecimals = Math.min(tokenDecimals, 6);
@@ -489,52 +1001,59 @@ function formatAmount(tokenAmount, tokenDecimals, direction, solChange) {
489
1001
  if (solChange !== 0)
490
1002
  return `${solChange > 0 ? "+" : ""}${solChange.toFixed(4)} SOL`;
491
1003
  }
492
- var tokenMetadataCache = /* @__PURE__ */ new Map();
493
- function transformImageUrl(url, imageProxy) {
494
- if (url)
495
- return imageProxy ? `${imageProxy}${encodeURIComponent(url)}` : url;
1004
+ function throwIfAborted(signal) {
1005
+ if (signal?.aborted)
1006
+ throw new DOMException("Query aborted", "AbortError");
496
1007
  }
497
- async function fetchTokenMetadata(mints) {
498
- let results = /* @__PURE__ */ new Map();
499
- if (mints.length === 0) return results;
500
- let uncachedMints = [];
501
- for (let mint of mints) {
502
- let cached = tokenMetadataCache.get(mint);
503
- cached ? results.set(mint, cached) : uncachedMints.push(mint);
504
- }
505
- if (uncachedMints.length === 0) return results;
506
- try {
507
- let response = await fetch("https://token-list-api.solana.cloud/v1/mints?chainId=101", {
508
- method: "POST",
509
- headers: { "Content-Type": "application/json" },
510
- body: JSON.stringify({ addresses: uncachedMints }),
511
- signal: AbortSignal.timeout(5e3)
512
- });
513
- if (!response.ok) return results;
514
- let data = await response.json();
515
- for (let item of data.content) {
516
- let metadata = { symbol: item.symbol, icon: item.logoURI };
517
- results.set(item.address, metadata), tokenMetadataCache.set(item.address, metadata);
1008
+ function clampInt(value, min, max) {
1009
+ return Number.isFinite(value) ? Math.min(max, Math.max(min, Math.floor(value))) : min;
1010
+ }
1011
+ async function mapWithConcurrency(inputs, worker, options) {
1012
+ let concurrency = clampInt(options.concurrency, 1, 32), results = new Array(inputs.length), nextIndex = 0;
1013
+ async function run() {
1014
+ for (; ; ) {
1015
+ throwIfAborted(options.signal);
1016
+ let index = nextIndex;
1017
+ if (nextIndex += 1, index >= inputs.length) return;
1018
+ results[index] = await worker(inputs[index], index);
518
1019
  }
519
- } catch (error) {
520
- console.warn("[useTransactions] Failed to fetch token metadata:", error);
521
1020
  }
1021
+ let runners = Array.from({ length: Math.min(concurrency, inputs.length) }, () => run());
1022
+ return await Promise.all(runners), results;
1023
+ }
1024
+ async function fetchTransactionTokenMetadata(mints, options = {}) {
1025
+ let results = /* @__PURE__ */ new Map();
1026
+ if (!mints.length) return results;
1027
+ let tokenList = await fetchSolanaTokenListMetadata(mints, {
1028
+ timeoutMs: 5e3,
1029
+ signal: options.signal,
1030
+ cluster: options.cluster
1031
+ });
1032
+ for (let [mint, meta] of tokenList)
1033
+ results.set(mint, { symbol: meta.symbol, icon: meta.logoURI });
522
1034
  return results;
523
1035
  }
524
1036
  function useTransactions(options = {}) {
525
- let { limit = 10, autoRefresh = false, refreshInterval = 6e4, fetchDetails = true } = options, { address: address$1, connected } = useAccount(), { cluster } = useCluster(), client = useSolanaClient(), connectorClient = useConnectorClient(), [transactions, setTransactions] = useState([]), [isLoading, setIsLoading] = useState(false), [error, setError] = useState(null), [hasMore, setHasMore] = useState(true), [lastUpdated, setLastUpdated] = useState(null), beforeSignatureRef = useRef(void 0), prevDepsRef = useRef(
526
- null
527
- ), rpcClient = client?.client ?? null, imageProxy = connectorClient?.getConfig().imageProxy, parseTransaction = useCallback(
1037
+ let {
1038
+ enabled = true,
1039
+ limit = 10,
1040
+ autoRefresh = false,
1041
+ refreshInterval = 6e4,
1042
+ fetchDetails = true,
1043
+ detailsConcurrency = 6,
1044
+ staleTimeMs = 0,
1045
+ cacheTimeMs = 300 * 1e3,
1046
+ // 5 minutes
1047
+ refetchOnMount = "stale",
1048
+ client: clientOverride
1049
+ } = options, { address: address$1, connected } = useAccount(), { cluster } = useCluster(), { client: providerClient } = useSolanaClient(), connectorClient = useConnectorClient(), [paginatedTransactions, setPaginatedTransactions] = useState([]), [isPaginationLoading, setIsPaginationLoading] = useState(false), [hasMore, setHasMore] = useState(true), beforeSignatureRef = useRef(void 0), rpcClient = clientOverride ?? providerClient, connectorConfig = connectorClient?.getConfig(), imageProxy = connectorConfig?.imageProxy, programLabels = connectorConfig?.programLabels, parseTransaction = useCallback(
528
1050
  (tx, walletAddress, sig, blockTime, slot, err, explorerUrl) => {
529
1051
  let { date, time } = formatDate(blockTime), baseInfo = {
530
1052
  signature: sig,
531
1053
  blockTime,
532
1054
  slot,
533
1055
  status: err ? "failed" : "success",
534
- error: err ? JSON.stringify(
535
- err,
536
- (_key, value) => typeof value == "bigint" ? value.toString() : value
537
- ) : void 0,
1056
+ error: err ? JSON.stringify(err, (_key, value) => typeof value == "bigint" ? value.toString() : value) : void 0,
538
1057
  type: "unknown",
539
1058
  formattedDate: date,
540
1059
  formattedTime: time,
@@ -549,24 +1068,13 @@ function useTransactions(options = {}) {
549
1068
  let { message } = transaction;
550
1069
  if (!isTransactionMessage(message))
551
1070
  return baseInfo;
552
- let accountKeys = getAccountKeys(message), walletIndex = accountKeys.findIndex((key) => key.trim() === walletAddress.trim());
1071
+ let accountKeys = getAccountKeys(message), walletIndex = accountKeys.findIndex((key2) => key2.trim() === walletAddress.trim());
553
1072
  if (walletIndex === -1)
554
1073
  return baseInfo;
555
- let { balanceChange, solChange } = parseSolChange(meta, walletIndex), programIds = detectProgramIds(message, accountKeys), hasJupiter = programIds.has("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"), hasOrca = programIds.has("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"), hasRaydium = programIds.has("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"), hasStake = programIds.has("Stake11111111111111111111111111111111111111"), hasMetaplex = programIds.has("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"), hasSystemProgram = programIds.has("11111111111111111111111111111111"), hasTokenProgram = programIds.has("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), type = "unknown", direction, counterparty, tokenMint, tokenAmount, tokenDecimals;
556
- if (hasJupiter || hasOrca || hasRaydium)
557
- type = "swap";
558
- else if (hasStake)
559
- type = "stake";
560
- else if (hasMetaplex)
561
- type = "nft";
562
- else if (hasSystemProgram && Math.abs(balanceChange) > 0)
563
- type = balanceChange > 0 ? "received" : "sent", direction = balanceChange > 0 ? "in" : "out", tokenMint = "So11111111111111111111111111111111111111112", accountKeys.length >= 2 && (counterparty = accountKeys.find(
564
- (key, idx) => idx !== walletIndex && key !== "11111111111111111111111111111111"
565
- ));
566
- else if (hasTokenProgram) {
567
- let tokenTransfer = parseTokenTransfers(meta, accountKeys, walletAddress);
568
- tokenTransfer && (type = tokenTransfer.type, direction = tokenTransfer.direction, tokenMint = tokenTransfer.tokenMint, tokenAmount = tokenTransfer.tokenAmount, tokenDecimals = tokenTransfer.tokenDecimals);
569
- } else programIds.size > 0 && (type = "program");
1074
+ let { balanceChange, solChange } = parseSolChange(meta, walletIndex), programIds = detectProgramIds(message, accountKeys), hasJupiter = programIds.has("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"), hasOrca = programIds.has("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"), hasRaydium = programIds.has("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"), hasStake = programIds.has("Stake11111111111111111111111111111111111111"), hasMetaplex = programIds.has("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"), hasSystemProgram = programIds.has("11111111111111111111111111111111"), hasTokenProgram = programIds.has("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), tokenTransfer = hasTokenProgram ? parseTokenTransfers(meta, accountKeys, walletAddress) : null, tokenAccountClosure = hasTokenProgram ? parseTokenAccountClosure(meta, accountKeys, walletAddress) : null, inferredSwapTokens = parseSwapTokens(meta, accountKeys, walletAddress, solChange), inferredSwapFromMint = inferredSwapTokens.fromToken?.mint, inferredSwapToMint = inferredSwapTokens.toToken?.mint, hasNonTrivialProgram = [...programIds].some((id) => !DEFAULT_IGNORED_PROGRAM_IDS.has(id)), hasInferredSwap = !!(inferredSwapFromMint && inferredSwapToMint) && inferredSwapFromMint !== inferredSwapToMint && hasNonTrivialProgram && !tokenAccountClosure, programId = pickPrimaryProgramId(programIds), programName = programId ? resolveProgramName(programId, programLabels) : void 0, programIdsArray = [...programIds], instructionTypes = getParsedInstructionTypes(message), instructionCount = Array.isArray(message.instructions) ? message.instructions.length : void 0, type = "unknown", direction, counterparty, tokenMint, tokenAmount, tokenDecimals, swapFromToken, swapToToken;
1075
+ hasJupiter || hasOrca || hasRaydium ? (type = "swap", inferredSwapTokens.fromToken && (swapFromToken = { mint: inferredSwapTokens.fromToken.mint }), inferredSwapTokens.toToken && (swapToToken = { mint: inferredSwapTokens.toToken.mint })) : hasStake ? type = "stake" : hasMetaplex ? type = "nft" : hasInferredSwap ? (type = "swap", swapFromToken = { mint: inferredSwapFromMint }, swapToToken = { mint: inferredSwapToMint }) : tokenTransfer ? (type = tokenTransfer.type, direction = tokenTransfer.direction, tokenMint = tokenTransfer.tokenMint, tokenAmount = tokenTransfer.tokenAmount, tokenDecimals = tokenTransfer.tokenDecimals) : tokenAccountClosure ? (type = "tokenAccountClosed", tokenMint = tokenAccountClosure.tokenMint, direction = solChange > 0 ? "in" : void 0) : hasSystemProgram && Math.abs(balanceChange) > 0 ? (type = balanceChange > 0 ? "received" : "sent", direction = balanceChange > 0 ? "in" : "out", tokenMint = "So11111111111111111111111111111111111111112", accountKeys.length >= 2 && (counterparty = accountKeys.find(
1076
+ (key2, idx) => idx !== walletIndex && key2 !== "11111111111111111111111111111111"
1077
+ ))) : programIds.size > 0 && (type = "program");
570
1078
  let formattedAmount = formatAmount(tokenAmount, tokenDecimals, direction, solChange);
571
1079
  return {
572
1080
  ...baseInfo,
@@ -575,131 +1083,189 @@ function useTransactions(options = {}) {
575
1083
  amount: tokenAmount ?? Math.abs(solChange),
576
1084
  formattedAmount,
577
1085
  tokenMint,
578
- counterparty: counterparty ? `${counterparty.slice(0, 4)}...${counterparty.slice(-4)}` : void 0
1086
+ counterparty: counterparty ? `${counterparty.slice(0, 4)}...${counterparty.slice(-4)}` : void 0,
1087
+ swapFromToken,
1088
+ swapToToken,
1089
+ programId,
1090
+ programName,
1091
+ programIds: programIdsArray.length ? programIdsArray : void 0,
1092
+ instructionTypes,
1093
+ instructionCount
579
1094
  };
580
1095
  } catch (parseError) {
581
1096
  return console.warn("Failed to parse transaction:", parseError), baseInfo;
582
1097
  }
583
1098
  },
584
- []
585
- ), fetchTransactions = useCallback(
586
- async (loadMore = false) => {
587
- if (!connected || !address$1 || !rpcClient || !cluster) {
588
- setTransactions([]);
589
- return;
590
- }
591
- setIsLoading(true), setError(null);
592
- try {
593
- let rpc = rpcClient.rpc, walletAddress = address(address$1), signaturesResult = await rpc.getSignaturesForAddress(walletAddress, {
594
- limit,
595
- ...loadMore && beforeSignatureRef.current ? { before: signature(beforeSignatureRef.current) } : {}
596
- }).send(), newTransactions;
597
- if (fetchDetails && signaturesResult.length > 0) {
598
- let txPromises = signaturesResult.map(
599
- (s) => rpc.getTransaction(signature(String(s.signature)), {
600
- encoding: "jsonParsed",
601
- maxSupportedTransactionVersion: 0
602
- }).send().catch(() => null)
603
- ), txDetails = await Promise.all(txPromises);
604
- newTransactions = signaturesResult.map((sig, idx) => {
605
- let blockTimeNum = sig.blockTime ? Number(sig.blockTime) : null, tx = txDetails[idx];
606
- return parseTransaction(
607
- tx,
608
- address$1,
609
- String(sig.signature),
610
- blockTimeNum,
611
- Number(sig.slot),
612
- sig.err,
613
- getTransactionUrl(String(sig.signature), cluster)
614
- );
615
- });
616
- } else
617
- newTransactions = signaturesResult.map((sig) => {
618
- let blockTimeNum = sig.blockTime ? Number(sig.blockTime) : null, { date, time } = formatDate(blockTimeNum);
619
- return {
620
- signature: String(sig.signature),
621
- blockTime: blockTimeNum,
622
- slot: Number(sig.slot),
623
- status: sig.err ? "failed" : "success",
624
- error: sig.err ? JSON.stringify(sig.err) : void 0,
625
- type: "unknown",
626
- formattedDate: date,
627
- formattedTime: time,
628
- explorerUrl: getTransactionUrl(String(sig.signature), cluster)
1099
+ [programLabels]
1100
+ ), key = useMemo(() => {
1101
+ if (!enabled || !connected || !address$1 || !rpcClient || !cluster) return null;
1102
+ let rpcUrl = rpcClient.urlOrMoniker instanceof URL ? rpcClient.urlOrMoniker.toString() : String(rpcClient.urlOrMoniker);
1103
+ return JSON.stringify(["wallet-transactions", rpcUrl, address$1, cluster.id, limit, fetchDetails]);
1104
+ }, [enabled, connected, address$1, rpcClient, cluster, limit, fetchDetails]);
1105
+ useEffect(() => {
1106
+ beforeSignatureRef.current = void 0, setPaginatedTransactions([]), setIsPaginationLoading(false), setHasMore(true);
1107
+ }, [key]);
1108
+ let fetchAndEnrichTransactions = useCallback(
1109
+ async (beforeSignature, currentCluster, signal) => {
1110
+ if (!rpcClient || !address$1)
1111
+ return { transactions: [], hasMore: false };
1112
+ throwIfAborted(signal);
1113
+ let rpc = rpcClient.rpc, walletAddress = address(address$1), signaturesResult = await rpc.getSignaturesForAddress(walletAddress, {
1114
+ limit,
1115
+ ...beforeSignature ? { before: signature(beforeSignature) } : {}
1116
+ }).send();
1117
+ throwIfAborted(signal);
1118
+ let newTransactions;
1119
+ if (fetchDetails && signaturesResult.length > 0) {
1120
+ let txDetails = await mapWithConcurrency(
1121
+ signaturesResult,
1122
+ async (sig) => rpc.getTransaction(signature(String(sig.signature)), {
1123
+ encoding: "jsonParsed",
1124
+ maxSupportedTransactionVersion: 0
1125
+ }).send().catch(() => null),
1126
+ { concurrency: detailsConcurrency, signal }
1127
+ );
1128
+ throwIfAborted(signal), newTransactions = signaturesResult.map((sig, idx) => {
1129
+ let blockTimeNum = sig.blockTime ? Number(sig.blockTime) : null, tx = txDetails[idx];
1130
+ return parseTransaction(
1131
+ tx,
1132
+ address$1,
1133
+ String(sig.signature),
1134
+ blockTimeNum,
1135
+ Number(sig.slot),
1136
+ sig.err,
1137
+ getTransactionUrl(String(sig.signature), currentCluster)
1138
+ );
1139
+ });
1140
+ } else
1141
+ newTransactions = signaturesResult.map((sig) => {
1142
+ let blockTimeNum = sig.blockTime ? Number(sig.blockTime) : null, { date, time } = formatDate(blockTimeNum);
1143
+ return {
1144
+ signature: String(sig.signature),
1145
+ blockTime: blockTimeNum,
1146
+ slot: Number(sig.slot),
1147
+ status: sig.err ? "failed" : "success",
1148
+ error: sig.err ? JSON.stringify(sig.err) : void 0,
1149
+ type: "unknown",
1150
+ formattedDate: date,
1151
+ formattedTime: time,
1152
+ explorerUrl: getTransactionUrl(String(sig.signature), currentCluster)
1153
+ };
1154
+ });
1155
+ let mintsToFetch = [
1156
+ .../* @__PURE__ */ new Set([
1157
+ ...newTransactions.filter((tx) => tx.tokenMint).map((tx) => tx.tokenMint),
1158
+ ...newTransactions.filter((tx) => tx.swapFromToken?.mint).map((tx) => tx.swapFromToken.mint),
1159
+ ...newTransactions.filter((tx) => tx.swapToToken?.mint).map((tx) => tx.swapToToken.mint)
1160
+ ])
1161
+ ];
1162
+ if (mintsToFetch.length > 0) {
1163
+ throwIfAborted(signal);
1164
+ let tokenMetadata = await fetchTransactionTokenMetadata(mintsToFetch, {
1165
+ signal,
1166
+ cluster: getClusterType(currentCluster)
1167
+ });
1168
+ tokenMetadata.size > 0 && (newTransactions = newTransactions.map((tx) => {
1169
+ let enrichedTx = { ...tx };
1170
+ if (tx.tokenMint && tokenMetadata.has(tx.tokenMint)) {
1171
+ let meta = tokenMetadata.get(tx.tokenMint);
1172
+ enrichedTx = {
1173
+ ...enrichedTx,
1174
+ tokenSymbol: meta.symbol,
1175
+ tokenIcon: transformImageUrl(meta.icon, imageProxy),
1176
+ formattedAmount: tx.formattedAmount ? `${tx.formattedAmount} ${meta.symbol}` : tx.formattedAmount
629
1177
  };
630
- });
631
- setTransactions(loadMore ? (prev) => [...prev, ...newTransactions] : newTransactions);
632
- let mintsToFetch = [...new Set(newTransactions.filter((tx) => tx.tokenMint).map((tx) => tx.tokenMint))];
633
- if (mintsToFetch.length > 0) {
634
- let tokenMetadata = await fetchTokenMetadata(mintsToFetch);
635
- if (tokenMetadata.size > 0) {
636
- let enrichedTransactions = newTransactions.map((tx) => {
637
- if (tx.tokenMint && tokenMetadata.has(tx.tokenMint)) {
638
- let meta = tokenMetadata.get(tx.tokenMint);
639
- return {
640
- ...tx,
641
- tokenSymbol: meta.symbol,
642
- tokenIcon: transformImageUrl(meta.icon, imageProxy),
643
- // Update formatted amount with symbol
644
- formattedAmount: tx.formattedAmount ? `${tx.formattedAmount} ${meta.symbol}` : tx.formattedAmount
645
- };
1178
+ }
1179
+ if (tx.swapFromToken?.mint && tokenMetadata.has(tx.swapFromToken.mint)) {
1180
+ let meta = tokenMetadata.get(tx.swapFromToken.mint);
1181
+ enrichedTx = {
1182
+ ...enrichedTx,
1183
+ swapFromToken: {
1184
+ ...tx.swapFromToken,
1185
+ symbol: meta.symbol,
1186
+ icon: transformImageUrl(meta.icon, imageProxy)
646
1187
  }
647
- return tx;
648
- });
649
- setTransactions(loadMore ? (prev) => [...prev.slice(0, -newTransactions.length), ...enrichedTransactions] : enrichedTransactions);
1188
+ };
650
1189
  }
651
- }
652
- if (typeof newTransactions < "u" && Array.isArray(newTransactions)) {
653
- if (newTransactions.length > 0) {
654
- let newBeforeSignature = newTransactions[newTransactions.length - 1].signature;
655
- beforeSignatureRef.current = newBeforeSignature;
1190
+ if (tx.swapToToken?.mint && tokenMetadata.has(tx.swapToToken.mint)) {
1191
+ let meta = tokenMetadata.get(tx.swapToToken.mint);
1192
+ enrichedTx = {
1193
+ ...enrichedTx,
1194
+ swapToToken: {
1195
+ ...tx.swapToToken,
1196
+ symbol: meta.symbol,
1197
+ icon: transformImageUrl(meta.icon, imageProxy)
1198
+ }
1199
+ };
656
1200
  }
657
- setHasMore(newTransactions.length === limit);
658
- }
659
- setLastUpdated(/* @__PURE__ */ new Date());
1201
+ return enrichedTx;
1202
+ }));
1203
+ }
1204
+ return {
1205
+ transactions: newTransactions,
1206
+ hasMore: newTransactions.length === limit
1207
+ };
1208
+ },
1209
+ [rpcClient, address$1, limit, fetchDetails, detailsConcurrency, parseTransaction, imageProxy]
1210
+ ), queryFn = useCallback(
1211
+ async (signal) => {
1212
+ if (!connected || !address$1 || !rpcClient || !cluster)
1213
+ return [];
1214
+ throwIfAborted(signal);
1215
+ let result = await fetchAndEnrichTransactions(void 0, cluster, signal);
1216
+ return throwIfAborted(signal), result.transactions;
1217
+ },
1218
+ [connected, address$1, rpcClient, cluster, fetchAndEnrichTransactions]
1219
+ ), {
1220
+ data: initialTransactions,
1221
+ error,
1222
+ isFetching: isInitialLoading,
1223
+ updatedAt,
1224
+ refetch: sharedRefetch,
1225
+ abort
1226
+ } = useSharedQuery(key, queryFn, {
1227
+ enabled,
1228
+ staleTimeMs,
1229
+ cacheTimeMs,
1230
+ refetchOnMount,
1231
+ refetchIntervalMs: autoRefresh ? refreshInterval : false
1232
+ });
1233
+ useEffect(() => {
1234
+ initialTransactions && (beforeSignatureRef.current = initialTransactions.length ? initialTransactions[initialTransactions.length - 1].signature : void 0, setHasMore(initialTransactions.length === limit), setPaginatedTransactions((prev) => prev.length ? [] : prev));
1235
+ }, [initialTransactions, limit]);
1236
+ let loadMoreFn = useCallback(async () => {
1237
+ if (!(!hasMore || isPaginationLoading || !cluster)) {
1238
+ setIsPaginationLoading(true);
1239
+ try {
1240
+ let result = await fetchAndEnrichTransactions(beforeSignatureRef.current, cluster);
1241
+ result.transactions.length > 0 && (beforeSignatureRef.current = result.transactions[result.transactions.length - 1].signature, setPaginatedTransactions((prev) => [...prev, ...result.transactions])), setHasMore(result.hasMore);
660
1242
  } catch (err) {
661
- setError(err), console.error("Failed to fetch transactions:", err);
1243
+ console.error("Failed to load more transactions:", err);
662
1244
  } finally {
663
- setIsLoading(false);
1245
+ setIsPaginationLoading(false);
664
1246
  }
1247
+ }
1248
+ }, [hasMore, isPaginationLoading, cluster, fetchAndEnrichTransactions]), refetch = useCallback(
1249
+ async (opts) => {
1250
+ beforeSignatureRef.current = void 0, setPaginatedTransactions([]), setHasMore(true), await sharedRefetch(opts);
665
1251
  },
666
- [connected, address$1, rpcClient, cluster, limit, fetchDetails, parseTransaction, imageProxy]
667
- ), refetch = useCallback(async () => {
668
- beforeSignatureRef.current = void 0, await fetchTransactions(false);
669
- }, [fetchTransactions]), loadMoreFn = useCallback(async () => {
670
- hasMore && !isLoading && await fetchTransactions(true);
671
- }, [hasMore, isLoading, fetchTransactions]);
672
- return useEffect(() => {
673
- let prevDeps = prevDepsRef.current, currentDeps = { connected, address: address$1, cluster };
674
- (!prevDeps || prevDeps.connected !== connected || prevDeps.address !== address$1 || prevDeps.cluster !== cluster) && (prevDepsRef.current = currentDeps, beforeSignatureRef.current = void 0, fetchTransactions(false));
675
- }, [connected, address$1, cluster, fetchTransactions]), useEffect(() => {
676
- if (!connected || !autoRefresh) return;
677
- let interval = setInterval(refetch, refreshInterval);
678
- return () => clearInterval(interval);
679
- }, [connected, autoRefresh, refreshInterval, refetch]), useMemo(
1252
+ [sharedRefetch]
1253
+ ), transactions = useMemo(() => [...initialTransactions ?? [], ...paginatedTransactions], [initialTransactions, paginatedTransactions]), isLoading = isInitialLoading || isPaginationLoading, visibleError = updatedAt ? null : error;
1254
+ return useMemo(
680
1255
  () => ({
681
1256
  transactions,
682
1257
  isLoading,
683
- error,
1258
+ error: visibleError,
684
1259
  hasMore,
685
1260
  loadMore: loadMoreFn,
686
1261
  refetch,
687
- lastUpdated
1262
+ abort,
1263
+ lastUpdated: updatedAt ? new Date(updatedAt) : null
688
1264
  }),
689
- [transactions, isLoading, error, hasMore, loadMoreFn, refetch, lastUpdated]
1265
+ [transactions, isLoading, visibleError, hasMore, loadMoreFn, refetch, abort, updatedAt]
690
1266
  );
691
1267
  }
692
- function createTimeoutSignal(ms) {
693
- if (typeof AbortSignal.timeout == "function")
694
- return { signal: AbortSignal.timeout(ms), cleanup: () => {
695
- } };
696
- let controller = new AbortController(), timeoutId = setTimeout(() => controller.abort(), ms);
697
- return {
698
- signal: controller.signal,
699
- cleanup: () => clearTimeout(timeoutId)
700
- };
701
- }
702
- var NATIVE_MINT = "So11111111111111111111111111111111111111112", CACHE_MAX_SIZE = 500, PRICE_CACHE_TTL = 6e4, STALE_CLEANUP_INTERVAL = 12e4, COINGECKO_DEFAULT_MAX_RETRIES = 3, COINGECKO_DEFAULT_BASE_DELAY = 1e3, COINGECKO_DEFAULT_MAX_TIMEOUT = 3e4, COINGECKO_API_BASE_URL = "https://api.coingecko.com/api/v3", LRUCache = class {
1268
+ var CACHE_MAX_SIZE = 500, PRICE_CACHE_TTL = 6e4, STALE_CLEANUP_INTERVAL = 12e4, COINGECKO_DEFAULT_MAX_RETRIES = 3, COINGECKO_DEFAULT_BASE_DELAY = 1e3, COINGECKO_DEFAULT_MAX_TIMEOUT = 3e4, COINGECKO_API_BASE_URL = "https://api.coingecko.com/api/v3", LRUCache = class {
703
1269
  constructor(maxSize, options) {
704
1270
  __publicField(this, "cache", /* @__PURE__ */ new Map());
705
1271
  __publicField(this, "maxSize");
@@ -739,10 +1305,6 @@ var NATIVE_MINT = "So11111111111111111111111111111111111111112", CACHE_MAX_SIZE
739
1305
  get size() {
740
1306
  return this.cache.size;
741
1307
  }
742
- /**
743
- * Prune stale entries based on TTL.
744
- * Only works if getTtl and getTimestamp are provided.
745
- */
746
1308
  pruneStale() {
747
1309
  if (!this.getTtl || !this.getTimestamp) return 0;
748
1310
  let now = Date.now(), pruned = 0;
@@ -767,27 +1329,6 @@ function stopCacheCleanup() {
767
1329
  function clearTokenCaches() {
768
1330
  metadataCache.clear(), priceCache.clear();
769
1331
  }
770
- async function fetchSolanaTokenMetadata(mints) {
771
- let results = /* @__PURE__ */ new Map();
772
- if (mints.length === 0) return results;
773
- let { signal, cleanup } = createTimeoutSignal(1e4);
774
- try {
775
- let response = await fetch("https://token-list-api.solana.cloud/v1/mints?chainId=101", {
776
- method: "POST",
777
- headers: { "Content-Type": "application/json" },
778
- body: JSON.stringify({ addresses: mints }),
779
- signal
780
- });
781
- if (cleanup(), !response.ok)
782
- throw new Error(`Solana Token List API error: ${response.status}`);
783
- let data = await response.json();
784
- for (let item of data.content)
785
- results.set(item.address, item);
786
- } catch (error) {
787
- cleanup(), console.warn("[useTokens] Solana Token List API failed:", error);
788
- }
789
- return results;
790
- }
791
1332
  function calculateBackoffDelay(attempt, baseDelay, retryAfter) {
792
1333
  if (retryAfter !== void 0 && retryAfter > 0) {
793
1334
  let jitter2 = Math.random() * 500;
@@ -823,26 +1364,17 @@ async function fetchCoinGeckoPrices(coingeckoIds, config) {
823
1364
  let elapsedTime = Date.now() - startTime;
824
1365
  if (elapsedTime >= maxTimeout) {
825
1366
  console.warn(
826
- `[useTokens] CoinGecko API: Total timeout (${maxTimeout}ms) exceeded after ${attempt} attempts. Returning cached/partial results.`
1367
+ `[useTokens] CoinGecko API: Total timeout (${maxTimeout}ms) exceeded after ${attempt} attempts.`
827
1368
  );
828
1369
  break;
829
1370
  }
830
1371
  let remainingTimeout = maxTimeout - elapsedTime, requestTimeout = Math.min(1e4, remainingTimeout), { signal, cleanup } = createTimeoutSignal(requestTimeout);
831
1372
  try {
832
- let response = await fetch(url, {
833
- headers,
834
- signal
835
- });
1373
+ let response = await fetch(url, { headers, signal });
836
1374
  if (cleanup(), response.status === 429) {
837
1375
  let retryAfter = parseRetryAfter(response.headers.get("Retry-After")), delay = calculateBackoffDelay(attempt, baseDelay, retryAfter);
838
- if (console.warn(
839
- `[useTokens] CoinGecko API rate limited (429). Attempt ${attempt + 1}/${maxRetries + 1}. Retry-After: ${retryAfter ?? "not specified"}s. Waiting ${Math.round(delay)}ms before retry. Consider adding an API key for higher limits: https://www.coingecko.com/en/api/pricing`
840
- ), Date.now() - startTime + delay >= maxTimeout) {
841
- console.warn(
842
- `[useTokens] CoinGecko API: Skipping retry - would exceed total timeout (${maxTimeout}ms). Returning cached/partial results.`
843
- );
1376
+ if (console.warn(`[useTokens] CoinGecko API rate limited (429). Waiting ${Math.round(delay)}ms.`), Date.now() - startTime + delay >= maxTimeout)
844
1377
  break;
845
- }
846
1378
  await new Promise((resolve) => setTimeout(resolve, delay)), attempt++;
847
1379
  continue;
848
1380
  }
@@ -853,174 +1385,195 @@ async function fetchCoinGeckoPrices(coingeckoIds, config) {
853
1385
  priceData?.usd !== void 0 && (results.set(id, priceData.usd), priceCache.set(id, { price: priceData.usd, timestamp: fetchTime }));
854
1386
  return results;
855
1387
  } catch (error) {
856
- if (cleanup(), lastError = error, error instanceof DOMException && error.name === "AbortError" ? console.warn(
857
- `[useTokens] CoinGecko API request timed out. Attempt ${attempt + 1}/${maxRetries + 1}.`
858
- ) : console.warn(
859
- `[useTokens] CoinGecko API request failed. Attempt ${attempt + 1}/${maxRetries + 1}:`,
860
- error
861
- ), attempt < maxRetries) {
1388
+ if (cleanup(), lastError = error, attempt < maxRetries) {
862
1389
  let delay = calculateBackoffDelay(attempt, baseDelay);
863
1390
  Date.now() - startTime + delay < maxTimeout && await new Promise((resolve) => setTimeout(resolve, delay));
864
1391
  }
865
1392
  attempt++;
866
1393
  }
867
1394
  }
868
- return attempt > maxRetries && console.warn(
869
- `[useTokens] CoinGecko API: All ${maxRetries + 1} attempts failed. Returning cached/partial results. Last error: ${lastError?.message ?? "Unknown error"}. If you are frequently rate limited, consider adding an API key: https://www.coingecko.com/en/api/pricing`
870
- ), results;
1395
+ return attempt > maxRetries && lastError && console.warn(`[useTokens] CoinGecko API: All attempts failed. Last error: ${lastError.message}`), results;
871
1396
  }
872
- async function fetchTokenMetadataHybrid(mints, coingeckoConfig) {
873
- let results = /* @__PURE__ */ new Map();
874
- if (mints.length === 0) return results;
875
- let uncachedMints = [], now = Date.now();
876
- for (let mint of mints) {
877
- let cached = metadataCache.get(mint);
878
- cached ? (cached.coingeckoId && (!priceCache.get(cached.coingeckoId) || now - (priceCache.get(cached.coingeckoId)?.timestamp ?? 0) >= PRICE_CACHE_TTL) && cached.coingeckoId && uncachedMints.push(mint), results.set(mint, cached)) : uncachedMints.push(mint);
879
- }
880
- if (uncachedMints.length === 0) return results;
881
- let solanaMetadata = await fetchSolanaTokenMetadata(uncachedMints), coingeckoIdToMint = /* @__PURE__ */ new Map();
882
- for (let [mint, meta] of solanaMetadata)
883
- meta.extensions?.coingeckoId && coingeckoIdToMint.set(meta.extensions.coingeckoId, mint);
1397
+ async function fetchTokenMetadataHybrid(mints, coingeckoConfig, options) {
1398
+ if (mints.length === 0) return false;
1399
+ let now = Date.now(), mintsNeedingTokenList = [], staleCoingeckoIdToMint = /* @__PURE__ */ new Map();
884
1400
  for (let mint of mints) {
885
1401
  let cached = metadataCache.get(mint);
886
- cached?.coingeckoId && !coingeckoIdToMint.has(cached.coingeckoId) && coingeckoIdToMint.set(cached.coingeckoId, mint);
1402
+ if (!cached) {
1403
+ mintsNeedingTokenList.push(mint);
1404
+ continue;
1405
+ }
1406
+ if (!cached.coingeckoId) continue;
1407
+ let priceEntry = priceCache.get(cached.coingeckoId);
1408
+ !!(priceEntry && now - priceEntry.timestamp < PRICE_CACHE_TTL) || staleCoingeckoIdToMint.set(cached.coingeckoId, mint);
887
1409
  }
888
- let prices = await fetchCoinGeckoPrices([...coingeckoIdToMint.keys()], coingeckoConfig);
889
- for (let [mint, meta] of solanaMetadata) {
890
- let coingeckoId = meta.extensions?.coingeckoId, usdPrice = coingeckoId ? prices.get(coingeckoId) : void 0, combined = {
1410
+ let didUpdate = false, tokenListMetadata = await fetchSolanaTokenListMetadata(mintsNeedingTokenList, {
1411
+ timeoutMs: 1e4,
1412
+ cluster: options?.cluster
1413
+ });
1414
+ for (let [mint, meta] of tokenListMetadata) {
1415
+ let coingeckoId = meta.extensions?.coingeckoId, usdPrice = (coingeckoId ? priceCache.get(coingeckoId) : void 0)?.price, combined = {
891
1416
  address: meta.address,
892
- name: meta.address === NATIVE_MINT ? "Solana" : meta.name,
1417
+ name: meta.address === NATIVE_SOL_MINT ? "Solana" : meta.name,
893
1418
  symbol: meta.symbol,
894
1419
  decimals: meta.decimals,
895
1420
  logoURI: meta.logoURI,
896
1421
  coingeckoId,
897
1422
  usdPrice
898
- };
899
- results.set(mint, combined), metadataCache.set(mint, combined);
1423
+ }, existing = metadataCache.get(mint);
1424
+ (!existing || existing.name !== combined.name || existing.symbol !== combined.symbol || existing.decimals !== combined.decimals || existing.logoURI !== combined.logoURI || existing.coingeckoId !== combined.coingeckoId || existing.usdPrice !== combined.usdPrice) && (didUpdate = true, metadataCache.set(mint, combined));
900
1425
  }
1426
+ didUpdate && options?.onUpdate?.();
1427
+ let coingeckoIdToMint = new Map(staleCoingeckoIdToMint);
1428
+ for (let [mint, meta] of tokenListMetadata)
1429
+ meta.extensions?.coingeckoId && coingeckoIdToMint.set(meta.extensions.coingeckoId, mint);
1430
+ if (coingeckoIdToMint.size === 0)
1431
+ return didUpdate;
1432
+ let prices = await fetchCoinGeckoPrices([...coingeckoIdToMint.keys()], coingeckoConfig), didUpdatePrices = false;
901
1433
  for (let [coingeckoId, mint] of coingeckoIdToMint) {
902
- let cached = results.get(mint) ?? metadataCache.get(mint);
1434
+ let cached = metadataCache.get(mint);
903
1435
  if (cached) {
904
1436
  let usdPrice = prices.get(coingeckoId);
905
- usdPrice !== void 0 && (cached.usdPrice = usdPrice, results.set(mint, cached), metadataCache.set(mint, cached));
1437
+ usdPrice !== void 0 && cached.usdPrice !== usdPrice && (didUpdate = true, didUpdatePrices = true, cached.usdPrice = usdPrice, metadataCache.set(mint, cached));
906
1438
  }
907
1439
  }
908
- return results;
1440
+ return didUpdatePrices && options?.onUpdate?.(), didUpdate;
909
1441
  }
910
1442
  function formatBalance(amount, decimals) {
911
- return (Number(amount) / Math.pow(10, decimals)).toLocaleString(void 0, {
912
- minimumFractionDigits: 0,
913
- maximumFractionDigits: Math.min(decimals, 6)
1443
+ return formatBigIntBalance(amount, decimals, {
1444
+ maxDecimals: Math.min(decimals, 6)
914
1445
  });
915
1446
  }
916
1447
  function formatUsd(amount, decimals, usdPrice) {
917
- return (Number(amount) / Math.pow(10, decimals) * usdPrice).toLocaleString(void 0, {
918
- style: "currency",
919
- currency: "USD",
920
- minimumFractionDigits: 2,
921
- maximumFractionDigits: 2
922
- });
1448
+ return formatBigIntUsd(amount, decimals, usdPrice);
1449
+ }
1450
+ function selectTokens(assets, address) {
1451
+ return {
1452
+ lamports: assets?.lamports ?? 0n,
1453
+ tokenAccounts: assets?.tokenAccounts ?? [],
1454
+ address
1455
+ };
923
1456
  }
924
- function transformImageUrl2(url, imageProxy) {
925
- if (!url) return;
926
- if (!imageProxy) return url;
927
- let encodedUrl = encodeURIComponent(url);
928
- return imageProxy.endsWith("/") ? imageProxy + encodedUrl : imageProxy + "/" + encodedUrl;
1457
+ function sortByValueDesc(a, b) {
1458
+ let metadataSort = (b.logo ? 1 : 0) - (a.logo ? 1 : 0);
1459
+ if (metadataSort !== 0) return metadataSort;
1460
+ let aValue = Number(a.amount) / Math.pow(10, a.decimals) * (a.usdPrice ?? 0);
1461
+ return Number(b.amount) / Math.pow(10, b.decimals) * (b.usdPrice ?? 0) - aValue;
929
1462
  }
930
1463
  function useTokens(options = {}) {
931
1464
  let {
1465
+ enabled = true,
932
1466
  includeZeroBalance = false,
933
1467
  autoRefresh = false,
934
1468
  refreshInterval = 6e4,
935
1469
  fetchMetadata = true,
936
- includeNativeSol = true
937
- } = options, { address: address$1, connected } = useAccount(), client = useSolanaClient(), connectorClient = useConnectorClient(), [tokens, setTokens] = useState([]), [isLoading, setIsLoading] = useState(false), [error, setError] = useState(null), [lastUpdated, setLastUpdated] = useState(null), [totalAccounts, setTotalAccounts] = useState(0), rpcClient = client?.client ?? null, connectorConfig = connectorClient?.getConfig(), imageProxy = connectorConfig?.imageProxy, coingeckoConfig = connectorConfig?.coingecko, fetchTokens = useCallback(async () => {
938
- if (!connected || !address$1 || !rpcClient) {
939
- setTokens([]), setTotalAccounts(0);
940
- return;
941
- }
942
- setIsLoading(true), setError(null);
943
- try {
944
- let rpc = rpcClient.rpc, walletAddress = address(address$1), tokenProgramId = address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), [balanceResult, tokenAccountsResult] = await Promise.all([
945
- includeNativeSol ? rpc.getBalance(walletAddress).send() : Promise.resolve(null),
946
- rpc.getTokenAccountsByOwner(walletAddress, { programId: tokenProgramId }, { encoding: "jsonParsed" }).send()
947
- ]), tokenList = [], mints = [];
948
- if (includeNativeSol && balanceResult !== null) {
949
- let solBalance = balanceResult.value;
950
- (includeZeroBalance || solBalance > 0n) && (tokenList.push({
951
- mint: NATIVE_MINT,
952
- tokenAccount: address$1,
953
- // SOL uses wallet address
954
- amount: solBalance,
955
- decimals: 9,
956
- formatted: formatBalance(solBalance, 9),
957
- isFrozen: false,
958
- owner: address$1
959
- }), mints.push(NATIVE_MINT));
960
- }
961
- for (let account of tokenAccountsResult.value) {
962
- let parsed = account.account.data;
963
- if (parsed?.parsed?.info) {
964
- let info = parsed.parsed.info, amount = BigInt(info.tokenAmount?.amount || "0"), decimals = info.tokenAmount?.decimals || 0;
965
- if (!includeZeroBalance && amount === 0n)
966
- continue;
967
- tokenList.push({
968
- mint: info.mint,
969
- tokenAccount: account.pubkey,
970
- amount,
971
- decimals,
972
- formatted: formatBalance(amount, decimals),
973
- isFrozen: info.state === "frozen",
974
- owner: info.owner
975
- }), mints.push(info.mint);
976
- }
977
- }
978
- if (setTokens([...tokenList]), setTotalAccounts(tokenAccountsResult.value.length + (includeNativeSol ? 1 : 0)), setLastUpdated(/* @__PURE__ */ new Date()), fetchMetadata && mints.length > 0) {
979
- let metadata = await fetchTokenMetadataHybrid(mints, coingeckoConfig);
980
- for (let i = 0; i < tokenList.length; i++) {
981
- let meta = metadata.get(tokenList[i].mint);
982
- meta && (tokenList[i] = {
983
- ...tokenList[i],
984
- name: meta.name,
985
- symbol: meta.symbol,
986
- logo: transformImageUrl2(meta.logoURI, imageProxy),
987
- usdPrice: meta.usdPrice,
988
- formattedUsd: meta.usdPrice ? formatUsd(tokenList[i].amount, tokenList[i].decimals, meta.usdPrice) : void 0
989
- });
990
- }
991
- tokenList.sort((a, b) => {
992
- let metadataSort = (b.logo ? 1 : 0) - (a.logo ? 1 : 0);
993
- if (metadataSort !== 0) return metadataSort;
994
- let aValue = Number(a.amount) / Math.pow(10, a.decimals) * (a.usdPrice ?? 0);
995
- return Number(b.amount) / Math.pow(10, b.decimals) * (b.usdPrice ?? 0) - aValue;
996
- }), setTokens([...tokenList]);
997
- }
998
- } catch (err) {
999
- setError(err), console.error("[useTokens] Failed to fetch tokens:", err);
1000
- } finally {
1001
- setIsLoading(false);
1002
- }
1003
- }, [connected, address$1, rpcClient, includeZeroBalance, fetchMetadata, includeNativeSol, imageProxy, coingeckoConfig]);
1470
+ includeNativeSol = true,
1471
+ staleTimeMs = 0,
1472
+ cacheTimeMs = 300 * 1e3,
1473
+ // 5 minutes
1474
+ refetchOnMount = "stale",
1475
+ client: clientOverride
1476
+ } = options, { address, connected } = useAccount(), { type: clusterType } = useCluster(), connectorConfig = useConnectorClient()?.getConfig(), imageProxy = connectorConfig?.imageProxy, coingeckoConfig = connectorConfig?.coingecko, selectFn = useCallback(
1477
+ (assets) => selectTokens(assets, address ?? ""),
1478
+ [address]
1479
+ ), {
1480
+ data: selection,
1481
+ error,
1482
+ isFetching,
1483
+ updatedAt,
1484
+ refetch: sharedRefetch,
1485
+ abort
1486
+ } = useWalletAssets({
1487
+ enabled,
1488
+ staleTimeMs,
1489
+ cacheTimeMs,
1490
+ refetchOnMount,
1491
+ refetchIntervalMs: autoRefresh ? refreshInterval : false,
1492
+ client: clientOverride,
1493
+ select: selectFn
1494
+ }), lamports = selection?.lamports ?? 0n, tokenAccounts = selection?.tokenAccounts ?? [], walletAddress = selection?.address ?? "", baseTokens = useMemo(() => {
1495
+ let result = [];
1496
+ includeNativeSol && walletAddress && (includeZeroBalance || lamports > 0n) && result.push({
1497
+ mint: NATIVE_SOL_MINT,
1498
+ tokenAccount: walletAddress,
1499
+ amount: lamports,
1500
+ decimals: 9,
1501
+ formatted: formatBalance(lamports, 9),
1502
+ isFrozen: false,
1503
+ owner: walletAddress
1504
+ });
1505
+ for (let account of tokenAccounts)
1506
+ !includeZeroBalance && account.amount === 0n || result.push({
1507
+ mint: account.mint,
1508
+ tokenAccount: account.pubkey,
1509
+ amount: account.amount,
1510
+ decimals: account.decimals,
1511
+ formatted: formatBalance(account.amount, account.decimals),
1512
+ isFrozen: account.isFrozen,
1513
+ owner: account.owner,
1514
+ programId: account.programId
1515
+ });
1516
+ return result;
1517
+ }, [lamports, tokenAccounts, walletAddress, includeNativeSol, includeZeroBalance]), mints = useMemo(() => {
1518
+ let unique = /* @__PURE__ */ new Set();
1519
+ for (let token of baseTokens)
1520
+ unique.add(token.mint);
1521
+ return [...unique].sort();
1522
+ }, [baseTokens]), mintsKey = useMemo(() => mints.join(","), [mints]), [metadataVersion, setMetadataVersion] = useState(0);
1004
1523
  useEffect(() => {
1005
- fetchTokens();
1006
- }, [fetchTokens]), useEffect(() => {
1007
- if (!connected || !autoRefresh) return;
1008
- let interval = setInterval(fetchTokens, refreshInterval);
1009
- return () => clearInterval(interval);
1010
- }, [connected, autoRefresh, refreshInterval, fetchTokens]), useEffect(() => (startCacheCleanup(), () => stopCacheCleanup()), []);
1524
+ if (!fetchMetadata || !mintsKey) return;
1525
+ let isMounted = true;
1526
+ return (async () => {
1527
+ try {
1528
+ let mintList = mintsKey.split(",");
1529
+ await fetchTokenMetadataHybrid(mintList, coingeckoConfig, {
1530
+ onUpdate: () => {
1531
+ isMounted && setMetadataVersion((v) => v + 1);
1532
+ },
1533
+ cluster: clusterType ?? void 0
1534
+ }), isMounted && setMetadataVersion((v) => v + 1);
1535
+ } catch (err) {
1536
+ console.error("[useTokens] Failed to fetch metadata:", err);
1537
+ }
1538
+ })(), () => {
1539
+ isMounted = false;
1540
+ };
1541
+ }, [mintsKey, fetchMetadata, coingeckoConfig, clusterType]);
1542
+ let tokens = useMemo(() => fetchMetadata ? baseTokens.map((token) => {
1543
+ let meta = metadataCache.get(token.mint);
1544
+ if (!meta) return token;
1545
+ let usdPrice = (meta.coingeckoId ? priceCache.get(meta.coingeckoId) : void 0)?.price ?? meta.usdPrice;
1546
+ return {
1547
+ ...token,
1548
+ name: meta.name,
1549
+ symbol: meta.symbol,
1550
+ logo: transformImageUrl(meta.logoURI, imageProxy),
1551
+ usdPrice,
1552
+ formattedUsd: usdPrice ? formatUsd(token.amount, token.decimals, usdPrice) : void 0
1553
+ };
1554
+ }).sort(sortByValueDesc) : baseTokens.slice().sort(sortByValueDesc), [baseTokens, fetchMetadata, imageProxy, metadataVersion]), totalAccounts = tokenAccounts.length + (includeNativeSol ? 1 : 0);
1555
+ useEffect(() => (startCacheCleanup(), () => stopCacheCleanup()), []);
1011
1556
  let wasConnectedRef = useRef(connected);
1012
- return useEffect(() => {
1557
+ useEffect(() => {
1013
1558
  wasConnectedRef.current && !connected && clearTokenCaches(), wasConnectedRef.current = connected;
1014
- }, [connected]), useMemo(
1559
+ }, [connected]);
1560
+ let visibleError = updatedAt ? null : error, refetch = useCallback(
1561
+ async (opts) => {
1562
+ await sharedRefetch(opts);
1563
+ },
1564
+ [sharedRefetch]
1565
+ );
1566
+ return useMemo(
1015
1567
  () => ({
1016
1568
  tokens,
1017
- isLoading,
1018
- error,
1019
- refetch: fetchTokens,
1020
- lastUpdated,
1569
+ isLoading: isFetching,
1570
+ error: visibleError,
1571
+ refetch,
1572
+ abort,
1573
+ lastUpdated: updatedAt ? new Date(updatedAt) : null,
1021
1574
  totalAccounts
1022
1575
  }),
1023
- [tokens, isLoading, error, fetchTokens, lastUpdated, totalAccounts]
1576
+ [tokens, isFetching, visibleError, refetch, abort, updatedAt, totalAccounts]
1024
1577
  );
1025
1578
  }
1026
1579
  function DisconnectElement({
@@ -1864,6 +2417,8 @@ function BalanceElement({
1864
2417
  BalanceElement.displayName = "BalanceElement";
1865
2418
  function TransactionHistoryElement({
1866
2419
  limit = 5,
2420
+ fetchDetails = true,
2421
+ detailsConcurrency,
1867
2422
  showStatus = true,
1868
2423
  showTime = true,
1869
2424
  className,
@@ -1873,7 +2428,11 @@ function TransactionHistoryElement({
1873
2428
  render,
1874
2429
  renderItem
1875
2430
  }) {
1876
- let { transactions, isLoading, error, hasMore, loadMore, refetch } = useTransactions({ limit });
2431
+ let { transactions, isLoading, error, hasMore, loadMore, refetch } = useTransactions({
2432
+ limit,
2433
+ fetchDetails,
2434
+ detailsConcurrency
2435
+ });
1877
2436
  if (render)
1878
2437
  return /* @__PURE__ */ jsx(Fragment, { children: render({ transactions, isLoading, error, hasMore, loadMore, refetch }) });
1879
2438
  let statusIcon = (status) => /* @__PURE__ */ jsx(
@@ -2180,6 +2739,6 @@ function TokenListElement({
2180
2739
  }
2181
2740
  TokenListElement.displayName = "TokenListElement";
2182
2741
 
2183
- export { AccountElement, BalanceElement, ClusterElement, ConnectorProvider, DisconnectElement, TokenListElement, TransactionHistoryElement, UnifiedProvider, WalletListElement, useAccount, useBalance, useCluster, useConnector, useConnectorClient, useGillSolanaClient, useGillTransactionSigner, useKitTransactionSigner, useSolanaClient, useTokens, useTransactionPreparer, useTransactionSigner, useTransactions, useWalletInfo };
2184
- //# sourceMappingURL=chunk-3STZXVXD.mjs.map
2185
- //# sourceMappingURL=chunk-3STZXVXD.mjs.map
2742
+ export { AccountElement, BalanceElement, ClusterElement, ConnectorProvider, DisconnectElement, TokenListElement, TransactionHistoryElement, UnifiedProvider, WalletListElement, clearSharedQueryCache, invalidateSharedQuery, useAccount, useBalance, useCluster, useConnector, useConnectorClient, useGillSolanaClient, useGillTransactionSigner, useKitTransactionSigner, useSolanaClient, useTokens, useTransactionPreparer, useTransactionSigner, useTransactions, useWalletInfo };
2743
+ //# sourceMappingURL=chunk-JK47EFJT.mjs.map
2744
+ //# sourceMappingURL=chunk-JK47EFJT.mjs.map