@solana/connector 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -0
- package/dist/{chunk-U64YZRJL.mjs → chunk-3STZXVXD.mjs} +254 -53
- package/dist/chunk-3STZXVXD.mjs.map +1 -0
- package/dist/{chunk-RIQH5W7D.js → chunk-I64FD2EH.js} +4 -3
- package/dist/chunk-I64FD2EH.js.map +1 -0
- package/dist/{chunk-CLXM6UEE.js → chunk-JUZVCBAI.js} +91 -85
- package/dist/chunk-JUZVCBAI.js.map +1 -0
- package/dist/{chunk-D6PZY5G6.js → chunk-NQXK7PGX.js} +30 -26
- package/dist/chunk-NQXK7PGX.js.map +1 -0
- package/dist/{chunk-N3Q2J2FG.mjs → chunk-QKVL45F6.mjs} +10 -6
- package/dist/chunk-QKVL45F6.mjs.map +1 -0
- package/dist/{chunk-P5MWBDFG.mjs → chunk-QL3IT3TS.mjs} +4 -3
- package/dist/chunk-QL3IT3TS.mjs.map +1 -0
- package/dist/{chunk-LUZWUZ5N.js → chunk-ULUYX23Q.js} +268 -67
- package/dist/chunk-ULUYX23Q.js.map +1 -0
- package/dist/{chunk-YTCSTE3Q.mjs → chunk-VMSZJPR5.mjs} +10 -4
- package/dist/chunk-VMSZJPR5.mjs.map +1 -0
- package/dist/compat.js +3 -3
- package/dist/compat.mjs +1 -1
- package/dist/headless.d.mts +2 -2
- package/dist/headless.d.ts +2 -2
- package/dist/headless.js +120 -120
- package/dist/headless.mjs +3 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +147 -147
- package/dist/index.mjs +4 -4
- package/dist/react.d.mts +4 -4
- package/dist/react.d.ts +4 -4
- package/dist/react.js +28 -28
- package/dist/react.mjs +2 -2
- package/dist/{wallet-standard-shim-DC_Z7DS-.d.ts → wallet-standard-shim--YcrQNRt.d.ts} +83 -0
- package/dist/{wallet-standard-shim-Cp4vF4oo.d.mts → wallet-standard-shim-Dx7H8Ctf.d.mts} +83 -0
- package/package.json +1 -1
- package/dist/chunk-CLXM6UEE.js.map +0 -1
- package/dist/chunk-D6PZY5G6.js.map +0 -1
- package/dist/chunk-LUZWUZ5N.js.map +0 -1
- package/dist/chunk-N3Q2J2FG.mjs.map +0 -1
- package/dist/chunk-P5MWBDFG.mjs.map +0 -1
- package/dist/chunk-RIQH5W7D.js.map +0 -1
- package/dist/chunk-U64YZRJL.mjs.map +0 -1
- package/dist/chunk-YTCSTE3Q.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -608,6 +608,242 @@ const mobile = getDefaultMobileConfig({
|
|
|
608
608
|
|
|
609
609
|
---
|
|
610
610
|
|
|
611
|
+
## Security Considerations
|
|
612
|
+
|
|
613
|
+
### Token Image Privacy
|
|
614
|
+
|
|
615
|
+
When using `useTokens()` or `useTransactions()`, token metadata (including logo URLs) is fetched from external APIs. By default, these image URLs are returned directly, which means when your users' browsers fetch these images, the image host can see:
|
|
616
|
+
|
|
617
|
+
- User IP addresses
|
|
618
|
+
- Request timing (when users viewed their tokens)
|
|
619
|
+
- User agent and browser information
|
|
620
|
+
|
|
621
|
+
This could potentially be exploited by malicious token creators who set tracking URLs in their token metadata.
|
|
622
|
+
|
|
623
|
+
### Image Proxy Configuration
|
|
624
|
+
|
|
625
|
+
To protect user privacy, you can configure an image proxy that fetches images on behalf of your users:
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
const config = getDefaultConfig({
|
|
629
|
+
appName: 'My App',
|
|
630
|
+
imageProxy: '/_next/image?w=64&q=75&url=', // Next.js Image Optimization
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
When `imageProxy` is set, all token image URLs returned by `useTokens()` and `useTransactions()` will be automatically transformed:
|
|
635
|
+
|
|
636
|
+
```
|
|
637
|
+
// Original URL from token metadata
|
|
638
|
+
https://raw.githubusercontent.com/.../token-logo.png
|
|
639
|
+
|
|
640
|
+
// Transformed URL (when imageProxy is set)
|
|
641
|
+
/_next/image?w=64&q=75&url=https%3A%2F%2Fraw.githubusercontent.com%2F...%2Ftoken-logo.png
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Common Proxy Options
|
|
645
|
+
|
|
646
|
+
| Service | Configuration |
|
|
647
|
+
|---------|---------------|
|
|
648
|
+
| **Next.js Image** | `imageProxy: '/_next/image?w=64&q=75&url='` |
|
|
649
|
+
| **Cloudflare** | `imageProxy: '/cdn-cgi/image/width=64,quality=75/'` |
|
|
650
|
+
| **imgproxy** | `imageProxy: 'https://imgproxy.example.com/insecure/fill/64/64/'` |
|
|
651
|
+
| **Custom API** | `imageProxy: '/api/image-proxy?url='` |
|
|
652
|
+
|
|
653
|
+
### Custom Proxy API Route (Next.js Example)
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
// app/api/image-proxy/route.ts
|
|
657
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
658
|
+
import dns from 'dns/promises';
|
|
659
|
+
|
|
660
|
+
// Allowlist of permitted domains for image fetching
|
|
661
|
+
const ALLOWED_DOMAINS = [
|
|
662
|
+
'raw.githubusercontent.com',
|
|
663
|
+
'arweave.net',
|
|
664
|
+
'ipfs.io',
|
|
665
|
+
'cloudflare-ipfs.com',
|
|
666
|
+
'nftstorage.link',
|
|
667
|
+
// Add other trusted image domains as needed
|
|
668
|
+
];
|
|
669
|
+
|
|
670
|
+
// Check if an IP address falls within private/reserved ranges
|
|
671
|
+
function isPrivateOrReservedIP(ip: string): boolean {
|
|
672
|
+
// IPv4 private/reserved ranges
|
|
673
|
+
const ipv4PrivateRanges = [
|
|
674
|
+
/^127\./, // 127.0.0.0/8 (loopback)
|
|
675
|
+
/^10\./, // 10.0.0.0/8 (private)
|
|
676
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12 (private)
|
|
677
|
+
/^192\.168\./, // 192.168.0.0/16 (private)
|
|
678
|
+
/^169\.254\./, // 169.254.0.0/16 (link-local/metadata)
|
|
679
|
+
/^0\./, // 0.0.0.0/8 (current network)
|
|
680
|
+
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./, // 100.64.0.0/10 (CGNAT)
|
|
681
|
+
/^192\.0\.0\./, // 192.0.0.0/24 (IETF protocol assignments)
|
|
682
|
+
/^192\.0\.2\./, // 192.0.2.0/24 (TEST-NET-1)
|
|
683
|
+
/^198\.51\.100\./, // 198.51.100.0/24 (TEST-NET-2)
|
|
684
|
+
/^203\.0\.113\./, // 203.0.113.0/24 (TEST-NET-3)
|
|
685
|
+
/^224\./, // 224.0.0.0/4 (multicast)
|
|
686
|
+
/^240\./, // 240.0.0.0/4 (reserved)
|
|
687
|
+
/^255\.255\.255\.255$/, // broadcast
|
|
688
|
+
];
|
|
689
|
+
|
|
690
|
+
// IPv6 private/reserved ranges
|
|
691
|
+
const ipv6PrivatePatterns = [
|
|
692
|
+
/^::1$/, // loopback
|
|
693
|
+
/^fe80:/i, // link-local
|
|
694
|
+
/^fc00:/i, // unique local (fc00::/7)
|
|
695
|
+
/^fd/i, // unique local (fd00::/8)
|
|
696
|
+
/^::ffff:(127\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.|169\.254\.)/i, // IPv4-mapped
|
|
697
|
+
];
|
|
698
|
+
|
|
699
|
+
// Check IPv4
|
|
700
|
+
for (const range of ipv4PrivateRanges) {
|
|
701
|
+
if (range.test(ip)) return true;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Check IPv6
|
|
705
|
+
for (const pattern of ipv6PrivatePatterns) {
|
|
706
|
+
if (pattern.test(ip)) return true;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Validate and parse the URL
|
|
713
|
+
function validateUrl(urlString: string): URL | null {
|
|
714
|
+
try {
|
|
715
|
+
const parsed = new URL(urlString);
|
|
716
|
+
// Only allow http and https protocols
|
|
717
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
return parsed;
|
|
721
|
+
} catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Check if hostname is in the allowlist
|
|
727
|
+
function isAllowedDomain(hostname: string): boolean {
|
|
728
|
+
return ALLOWED_DOMAINS.some(
|
|
729
|
+
(domain) => hostname === domain || hostname.endsWith(`.${domain}`)
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export async function GET(request: NextRequest) {
|
|
734
|
+
const urlParam = request.nextUrl.searchParams.get('url');
|
|
735
|
+
|
|
736
|
+
// (1) Ensure URL exists and parses correctly with http/https
|
|
737
|
+
if (!urlParam) {
|
|
738
|
+
return new NextResponse('Missing URL parameter', { status: 400 });
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const parsedUrl = validateUrl(urlParam);
|
|
742
|
+
if (!parsedUrl) {
|
|
743
|
+
return new NextResponse('Invalid URL or protocol', { status: 400 });
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// (2) Enforce allowlist of permitted domains
|
|
747
|
+
if (!isAllowedDomain(parsedUrl.hostname)) {
|
|
748
|
+
return new NextResponse('Domain not allowed', { status: 403 });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// (3) Resolve hostname and check for private/reserved IPs
|
|
752
|
+
try {
|
|
753
|
+
const addresses = await dns.resolve(parsedUrl.hostname);
|
|
754
|
+
for (const ip of addresses) {
|
|
755
|
+
if (isPrivateOrReservedIP(ip)) {
|
|
756
|
+
return new NextResponse('Resolved IP is not allowed', { status: 403 });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
} catch {
|
|
760
|
+
return new NextResponse('Failed to resolve hostname', { status: 400 });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// (4) All checks passed - perform the fetch
|
|
764
|
+
try {
|
|
765
|
+
const response = await fetch(parsedUrl.toString());
|
|
766
|
+
const buffer = await response.arrayBuffer();
|
|
767
|
+
|
|
768
|
+
return new NextResponse(buffer, {
|
|
769
|
+
headers: {
|
|
770
|
+
'Content-Type': response.headers.get('Content-Type') || 'image/png',
|
|
771
|
+
'Cache-Control': 'public, max-age=86400',
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
} catch {
|
|
775
|
+
return new NextResponse('Failed to fetch image', { status: 500 });
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## CoinGecko API & Rate Limits
|
|
783
|
+
|
|
784
|
+
The `useTokens()` hook fetches token prices from CoinGecko. CoinGecko has rate limits that may affect your application:
|
|
785
|
+
|
|
786
|
+
### Rate Limits (as of 2024)
|
|
787
|
+
|
|
788
|
+
| Tier | Rate Limit | API Key Required |
|
|
789
|
+
|------|------------|------------------|
|
|
790
|
+
| **Free (Public)** | 10-30 requests/minute | No |
|
|
791
|
+
| **Demo** | 30 requests/minute | Yes (free) |
|
|
792
|
+
| **Analyst** | 500 requests/minute | Yes (paid) |
|
|
793
|
+
| **Pro** | 1000+ requests/minute | Yes (paid) |
|
|
794
|
+
|
|
795
|
+
### Handling Rate Limits
|
|
796
|
+
|
|
797
|
+
ConnectorKit automatically handles rate limits with:
|
|
798
|
+
- **Exponential backoff**: Retries with increasing delays
|
|
799
|
+
- **Jitter**: Random delay added to prevent thundering herd
|
|
800
|
+
- **Retry-After header**: Honors server-specified wait times
|
|
801
|
+
- **Bounded timeout**: Won't block forever (default 30s max)
|
|
802
|
+
|
|
803
|
+
### Adding a CoinGecko API Key
|
|
804
|
+
|
|
805
|
+
For higher rate limits, add a free Demo API key from [CoinGecko](https://www.coingecko.com/en/api/pricing):
|
|
806
|
+
|
|
807
|
+
```typescript
|
|
808
|
+
const config = getDefaultConfig({
|
|
809
|
+
appName: 'My App',
|
|
810
|
+
coingecko: {
|
|
811
|
+
apiKey: process.env.COINGECKO_API_KEY, // Demo or Pro API key
|
|
812
|
+
isPro: false, // Set to true for Pro API keys
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Advanced Configuration
|
|
818
|
+
|
|
819
|
+
```typescript
|
|
820
|
+
const config = getDefaultConfig({
|
|
821
|
+
appName: 'My App',
|
|
822
|
+
coingecko: {
|
|
823
|
+
// API key for higher rate limits (optional)
|
|
824
|
+
apiKey: process.env.COINGECKO_API_KEY,
|
|
825
|
+
|
|
826
|
+
// Set to true if using a Pro API key (default: false for Demo keys)
|
|
827
|
+
isPro: false,
|
|
828
|
+
|
|
829
|
+
// Maximum retry attempts on 429 (default: 3)
|
|
830
|
+
maxRetries: 3,
|
|
831
|
+
|
|
832
|
+
// Base delay for exponential backoff in ms (default: 1000)
|
|
833
|
+
baseDelay: 1000,
|
|
834
|
+
|
|
835
|
+
// Maximum total timeout in ms (default: 30000)
|
|
836
|
+
maxTimeout: 30000,
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### Caching
|
|
842
|
+
|
|
843
|
+
Token prices are cached for 60 seconds to minimize API calls. The retry logic only applies to uncached token IDs, so frequently-viewed tokens won't trigger additional API calls.
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
611
847
|
## Advanced Usage
|
|
612
848
|
|
|
613
849
|
### Headless Client (Vue, Svelte, Vanilla JS)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { installPolyfills, ConnectorErrorBoundary, isMainnetCluster, isDevnetCluster, isTestnetCluster, isLocalCluster, getClusterExplorerUrl, getClusterType, formatAddress, copyAddressToClipboard, createTransactionSigner, createKitTransactionSigner, NetworkError, getTransactionUrl, ConnectorClient } from './chunk-
|
|
2
|
-
import { createLogger, createSolanaClient, prepareTransaction } from './chunk-
|
|
3
|
-
import React, { createContext, useContext, useSyncExternalStore, useMemo, useState, useCallback,
|
|
1
|
+
import { installPolyfills, ConnectorErrorBoundary, isMainnetCluster, isDevnetCluster, isTestnetCluster, isLocalCluster, getClusterExplorerUrl, getClusterType, formatAddress, copyAddressToClipboard, createTransactionSigner, createKitTransactionSigner, NetworkError, getTransactionUrl, ConnectorClient } from './chunk-VMSZJPR5.mjs';
|
|
2
|
+
import { createLogger, __publicField, createSolanaClient, prepareTransaction } from './chunk-QL3IT3TS.mjs';
|
|
3
|
+
import React, { createContext, useContext, useSyncExternalStore, useMemo, useState, useCallback, useRef, useEffect } from 'react';
|
|
4
4
|
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
import { address } from '@solana/addresses';
|
|
6
6
|
import { signature } from '@solana/keys';
|
|
@@ -307,9 +307,9 @@ function formatSol(lamports, decimals = 4) {
|
|
|
307
307
|
}) + " SOL";
|
|
308
308
|
}
|
|
309
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), rpcClient = client?.client ?? null, fetchBalance = useCallback(async () => {
|
|
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
311
|
if (!connected || !address$1 || !rpcClient) {
|
|
312
|
-
setLamports(0n), setTokens([]);
|
|
312
|
+
setLamports(0n), setTokens([]), hasDataRef.current = false;
|
|
313
313
|
return;
|
|
314
314
|
}
|
|
315
315
|
setIsLoading(true), setError(null);
|
|
@@ -337,9 +337,9 @@ function useBalance() {
|
|
|
337
337
|
} catch (tokenError) {
|
|
338
338
|
console.warn("Failed to fetch token balances:", tokenError), setTokens([]);
|
|
339
339
|
}
|
|
340
|
-
setLastUpdated(/* @__PURE__ */ new Date());
|
|
340
|
+
hasDataRef.current = true, setLastUpdated(/* @__PURE__ */ new Date());
|
|
341
341
|
} catch (err) {
|
|
342
|
-
setError(err), console.error("Failed to fetch balance:", err);
|
|
342
|
+
hasDataRef.current || (setError(err), console.error("Failed to fetch balance:", err));
|
|
343
343
|
} finally {
|
|
344
344
|
setIsLoading(false);
|
|
345
345
|
}
|
|
@@ -490,6 +490,10 @@ function formatAmount(tokenAmount, tokenDecimals, direction, solChange) {
|
|
|
490
490
|
return `${solChange > 0 ? "+" : ""}${solChange.toFixed(4)} SOL`;
|
|
491
491
|
}
|
|
492
492
|
var tokenMetadataCache = /* @__PURE__ */ new Map();
|
|
493
|
+
function transformImageUrl(url, imageProxy) {
|
|
494
|
+
if (url)
|
|
495
|
+
return imageProxy ? `${imageProxy}${encodeURIComponent(url)}` : url;
|
|
496
|
+
}
|
|
493
497
|
async function fetchTokenMetadata(mints) {
|
|
494
498
|
let results = /* @__PURE__ */ new Map();
|
|
495
499
|
if (mints.length === 0) return results;
|
|
@@ -500,16 +504,17 @@ async function fetchTokenMetadata(mints) {
|
|
|
500
504
|
}
|
|
501
505
|
if (uncachedMints.length === 0) return results;
|
|
502
506
|
try {
|
|
503
|
-
let
|
|
504
|
-
|
|
505
|
-
|
|
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 }),
|
|
506
511
|
signal: AbortSignal.timeout(5e3)
|
|
507
512
|
});
|
|
508
513
|
if (!response.ok) return results;
|
|
509
|
-
let
|
|
510
|
-
for (let item of
|
|
511
|
-
let metadata = { symbol: item.symbol, icon: item.
|
|
512
|
-
results.set(item.
|
|
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);
|
|
513
518
|
}
|
|
514
519
|
} catch (error) {
|
|
515
520
|
console.warn("[useTransactions] Failed to fetch token metadata:", error);
|
|
@@ -517,16 +522,19 @@ async function fetchTokenMetadata(mints) {
|
|
|
517
522
|
return results;
|
|
518
523
|
}
|
|
519
524
|
function useTransactions(options = {}) {
|
|
520
|
-
let { limit = 10, autoRefresh = false, refreshInterval = 6e4, fetchDetails = true } = options, { address: address$1, connected } = useAccount(), { cluster } = useCluster(), client = useSolanaClient(), [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(
|
|
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(
|
|
521
526
|
null
|
|
522
|
-
), rpcClient = client?.client ?? null, parseTransaction = useCallback(
|
|
527
|
+
), rpcClient = client?.client ?? null, imageProxy = connectorClient?.getConfig().imageProxy, parseTransaction = useCallback(
|
|
523
528
|
(tx, walletAddress, sig, blockTime, slot, err, explorerUrl) => {
|
|
524
529
|
let { date, time } = formatDate(blockTime), baseInfo = {
|
|
525
530
|
signature: sig,
|
|
526
531
|
blockTime,
|
|
527
532
|
slot,
|
|
528
533
|
status: err ? "failed" : "success",
|
|
529
|
-
error: err ? JSON.stringify(
|
|
534
|
+
error: err ? JSON.stringify(
|
|
535
|
+
err,
|
|
536
|
+
(_key, value) => typeof value == "bigint" ? value.toString() : value
|
|
537
|
+
) : void 0,
|
|
530
538
|
type: "unknown",
|
|
531
539
|
formattedDate: date,
|
|
532
540
|
formattedTime: time,
|
|
@@ -631,7 +639,7 @@ function useTransactions(options = {}) {
|
|
|
631
639
|
return {
|
|
632
640
|
...tx,
|
|
633
641
|
tokenSymbol: meta.symbol,
|
|
634
|
-
tokenIcon: meta.icon,
|
|
642
|
+
tokenIcon: transformImageUrl(meta.icon, imageProxy),
|
|
635
643
|
// Update formatted amount with symbol
|
|
636
644
|
formattedAmount: tx.formattedAmount ? `${tx.formattedAmount} ${meta.symbol}` : tx.formattedAmount
|
|
637
645
|
};
|
|
@@ -655,7 +663,7 @@ function useTransactions(options = {}) {
|
|
|
655
663
|
setIsLoading(false);
|
|
656
664
|
}
|
|
657
665
|
},
|
|
658
|
-
[connected, address$1, rpcClient, cluster, limit, fetchDetails, parseTransaction]
|
|
666
|
+
[connected, address$1, rpcClient, cluster, limit, fetchDetails, parseTransaction, imageProxy]
|
|
659
667
|
), refetch = useCallback(async () => {
|
|
660
668
|
beforeSignatureRef.current = void 0, await fetchTransactions(false);
|
|
661
669
|
}, [fetchTransactions]), loadMoreFn = useCallback(async () => {
|
|
@@ -681,38 +689,221 @@ function useTransactions(options = {}) {
|
|
|
681
689
|
[transactions, isLoading, error, hasMore, loadMoreFn, refetch, lastUpdated]
|
|
682
690
|
);
|
|
683
691
|
}
|
|
684
|
-
|
|
685
|
-
|
|
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 {
|
|
703
|
+
constructor(maxSize, options) {
|
|
704
|
+
__publicField(this, "cache", /* @__PURE__ */ new Map());
|
|
705
|
+
__publicField(this, "maxSize");
|
|
706
|
+
__publicField(this, "getTtl");
|
|
707
|
+
__publicField(this, "getTimestamp");
|
|
708
|
+
this.maxSize = maxSize, this.getTtl = options?.getTtl, this.getTimestamp = options?.getTimestamp;
|
|
709
|
+
}
|
|
710
|
+
get(key) {
|
|
711
|
+
let value = this.cache.get(key);
|
|
712
|
+
if (value !== void 0) {
|
|
713
|
+
if (this.getTtl && this.getTimestamp) {
|
|
714
|
+
let ttl = this.getTtl(value), timestamp = this.getTimestamp(value);
|
|
715
|
+
if (ttl !== void 0 && timestamp !== void 0 && Date.now() - timestamp >= ttl) {
|
|
716
|
+
this.cache.delete(key);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return this.cache.delete(key), this.cache.set(key, value), value;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
set(key, value) {
|
|
724
|
+
if (this.cache.has(key) && this.cache.delete(key), this.cache.size >= this.maxSize) {
|
|
725
|
+
let oldestKey = this.cache.keys().next().value;
|
|
726
|
+
oldestKey !== void 0 && this.cache.delete(oldestKey);
|
|
727
|
+
}
|
|
728
|
+
this.cache.set(key, value);
|
|
729
|
+
}
|
|
730
|
+
has(key) {
|
|
731
|
+
return this.cache.has(key);
|
|
732
|
+
}
|
|
733
|
+
delete(key) {
|
|
734
|
+
return this.cache.delete(key);
|
|
735
|
+
}
|
|
736
|
+
clear() {
|
|
737
|
+
this.cache.clear();
|
|
738
|
+
}
|
|
739
|
+
get size() {
|
|
740
|
+
return this.cache.size;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Prune stale entries based on TTL.
|
|
744
|
+
* Only works if getTtl and getTimestamp are provided.
|
|
745
|
+
*/
|
|
746
|
+
pruneStale() {
|
|
747
|
+
if (!this.getTtl || !this.getTimestamp) return 0;
|
|
748
|
+
let now = Date.now(), pruned = 0;
|
|
749
|
+
for (let [key, value] of this.cache) {
|
|
750
|
+
let ttl = this.getTtl(value), timestamp = this.getTimestamp(value);
|
|
751
|
+
ttl !== void 0 && timestamp !== void 0 && now - timestamp >= ttl && (this.cache.delete(key), pruned++);
|
|
752
|
+
}
|
|
753
|
+
return pruned;
|
|
754
|
+
}
|
|
755
|
+
}, metadataCache = new LRUCache(CACHE_MAX_SIZE), priceCache = new LRUCache(CACHE_MAX_SIZE, {
|
|
756
|
+
getTtl: () => PRICE_CACHE_TTL,
|
|
757
|
+
getTimestamp: (entry) => entry.timestamp
|
|
758
|
+
}), cleanupIntervalId = null, cleanupRefCount = 0;
|
|
759
|
+
function startCacheCleanup() {
|
|
760
|
+
cleanupRefCount++, cleanupIntervalId === null && (cleanupIntervalId = setInterval(() => {
|
|
761
|
+
priceCache.pruneStale();
|
|
762
|
+
}, STALE_CLEANUP_INTERVAL));
|
|
763
|
+
}
|
|
764
|
+
function stopCacheCleanup() {
|
|
765
|
+
cleanupRefCount = Math.max(0, cleanupRefCount - 1), cleanupRefCount === 0 && cleanupIntervalId !== null && (clearInterval(cleanupIntervalId), cleanupIntervalId = null);
|
|
766
|
+
}
|
|
767
|
+
function clearTokenCaches() {
|
|
768
|
+
metadataCache.clear(), priceCache.clear();
|
|
769
|
+
}
|
|
770
|
+
async function fetchSolanaTokenMetadata(mints) {
|
|
686
771
|
let results = /* @__PURE__ */ new Map();
|
|
687
772
|
if (mints.length === 0) return results;
|
|
688
|
-
let
|
|
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
|
+
function calculateBackoffDelay(attempt, baseDelay, retryAfter) {
|
|
792
|
+
if (retryAfter !== void 0 && retryAfter > 0) {
|
|
793
|
+
let jitter2 = Math.random() * 500;
|
|
794
|
+
return retryAfter * 1e3 + jitter2;
|
|
795
|
+
}
|
|
796
|
+
let exponentialDelay = baseDelay * Math.pow(2, attempt), jitter = Math.random() * 500;
|
|
797
|
+
return exponentialDelay + jitter;
|
|
798
|
+
}
|
|
799
|
+
function parseRetryAfter(retryAfterHeader) {
|
|
800
|
+
if (!retryAfterHeader) return;
|
|
801
|
+
let seconds = parseInt(retryAfterHeader, 10);
|
|
802
|
+
if (!isNaN(seconds) && seconds >= 0)
|
|
803
|
+
return seconds;
|
|
804
|
+
let date = Date.parse(retryAfterHeader);
|
|
805
|
+
if (!isNaN(date)) {
|
|
806
|
+
let waitMs = date - Date.now();
|
|
807
|
+
return waitMs > 0 ? Math.ceil(waitMs / 1e3) : 0;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function fetchCoinGeckoPrices(coingeckoIds, config) {
|
|
811
|
+
let results = /* @__PURE__ */ new Map();
|
|
812
|
+
if (coingeckoIds.length === 0) return results;
|
|
813
|
+
let now = Date.now(), uncachedIds = [];
|
|
814
|
+
for (let id of coingeckoIds) {
|
|
815
|
+
let cached = priceCache.get(id);
|
|
816
|
+
cached && now - cached.timestamp < PRICE_CACHE_TTL ? results.set(id, cached.price) : uncachedIds.push(id);
|
|
817
|
+
}
|
|
818
|
+
if (uncachedIds.length === 0) return results;
|
|
819
|
+
let maxRetries = config?.maxRetries ?? COINGECKO_DEFAULT_MAX_RETRIES, baseDelay = config?.baseDelay ?? COINGECKO_DEFAULT_BASE_DELAY, maxTimeout = config?.maxTimeout ?? COINGECKO_DEFAULT_MAX_TIMEOUT, apiKey = config?.apiKey, isPro = config?.isPro ?? false, url = `${COINGECKO_API_BASE_URL}/simple/price?ids=${uncachedIds.join(",")}&vs_currencies=usd`, headers = {};
|
|
820
|
+
apiKey && (headers[isPro ? "x-cg-pro-api-key" : "x-cg-demo-api-key"] = apiKey);
|
|
821
|
+
let startTime = Date.now(), attempt = 0, lastError = null;
|
|
822
|
+
for (; attempt <= maxRetries; ) {
|
|
823
|
+
let elapsedTime = Date.now() - startTime;
|
|
824
|
+
if (elapsedTime >= maxTimeout) {
|
|
825
|
+
console.warn(
|
|
826
|
+
`[useTokens] CoinGecko API: Total timeout (${maxTimeout}ms) exceeded after ${attempt} attempts. Returning cached/partial results.`
|
|
827
|
+
);
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
let remainingTimeout = maxTimeout - elapsedTime, requestTimeout = Math.min(1e4, remainingTimeout), { signal, cleanup } = createTimeoutSignal(requestTimeout);
|
|
831
|
+
try {
|
|
832
|
+
let response = await fetch(url, {
|
|
833
|
+
headers,
|
|
834
|
+
signal
|
|
835
|
+
});
|
|
836
|
+
if (cleanup(), response.status === 429) {
|
|
837
|
+
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
|
+
);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
await new Promise((resolve) => setTimeout(resolve, delay)), attempt++;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (!response.ok)
|
|
850
|
+
throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
|
|
851
|
+
let data = await response.json(), fetchTime = Date.now();
|
|
852
|
+
for (let [id, priceData] of Object.entries(data))
|
|
853
|
+
priceData?.usd !== void 0 && (results.set(id, priceData.usd), priceCache.set(id, { price: priceData.usd, timestamp: fetchTime }));
|
|
854
|
+
return results;
|
|
855
|
+
} 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) {
|
|
862
|
+
let delay = calculateBackoffDelay(attempt, baseDelay);
|
|
863
|
+
Date.now() - startTime + delay < maxTimeout && await new Promise((resolve) => setTimeout(resolve, delay));
|
|
864
|
+
}
|
|
865
|
+
attempt++;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
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;
|
|
871
|
+
}
|
|
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();
|
|
689
876
|
for (let mint of mints) {
|
|
690
877
|
let cached = metadataCache.get(mint);
|
|
691
|
-
cached ? results.set(mint, cached) : uncachedMints.push(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);
|
|
692
879
|
}
|
|
693
880
|
if (uncachedMints.length === 0) return results;
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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);
|
|
884
|
+
for (let mint of mints) {
|
|
885
|
+
let cached = metadataCache.get(mint);
|
|
886
|
+
cached?.coingeckoId && !coingeckoIdToMint.has(cached.coingeckoId) && coingeckoIdToMint.set(cached.coingeckoId, mint);
|
|
887
|
+
}
|
|
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 = {
|
|
891
|
+
address: meta.address,
|
|
892
|
+
name: meta.address === NATIVE_MINT ? "Solana" : meta.name,
|
|
893
|
+
symbol: meta.symbol,
|
|
894
|
+
decimals: meta.decimals,
|
|
895
|
+
logoURI: meta.logoURI,
|
|
896
|
+
coingeckoId,
|
|
897
|
+
usdPrice
|
|
898
|
+
};
|
|
899
|
+
results.set(mint, combined), metadataCache.set(mint, combined);
|
|
900
|
+
}
|
|
901
|
+
for (let [coingeckoId, mint] of coingeckoIdToMint) {
|
|
902
|
+
let cached = results.get(mint) ?? metadataCache.get(mint);
|
|
903
|
+
if (cached) {
|
|
904
|
+
let usdPrice = prices.get(coingeckoId);
|
|
905
|
+
usdPrice !== void 0 && (cached.usdPrice = usdPrice, results.set(mint, cached), metadataCache.set(mint, cached));
|
|
713
906
|
}
|
|
714
|
-
} catch (error) {
|
|
715
|
-
console.warn("[useTokens] Jupiter API failed:", error);
|
|
716
907
|
}
|
|
717
908
|
return results;
|
|
718
909
|
}
|
|
@@ -730,6 +921,12 @@ function formatUsd(amount, decimals, usdPrice) {
|
|
|
730
921
|
maximumFractionDigits: 2
|
|
731
922
|
});
|
|
732
923
|
}
|
|
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;
|
|
929
|
+
}
|
|
733
930
|
function useTokens(options = {}) {
|
|
734
931
|
let {
|
|
735
932
|
includeZeroBalance = false,
|
|
@@ -737,7 +934,7 @@ function useTokens(options = {}) {
|
|
|
737
934
|
refreshInterval = 6e4,
|
|
738
935
|
fetchMetadata = true,
|
|
739
936
|
includeNativeSol = true
|
|
740
|
-
} = options, { address: address$1, connected } = useAccount(), client = useSolanaClient(), [tokens, setTokens] = useState([]), [isLoading, setIsLoading] = useState(false), [error, setError] = useState(null), [lastUpdated, setLastUpdated] = useState(null), [totalAccounts, setTotalAccounts] = useState(0), rpcClient = client?.client ?? null, fetchTokens = useCallback(async () => {
|
|
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 () => {
|
|
741
938
|
if (!connected || !address$1 || !rpcClient) {
|
|
742
939
|
setTokens([]), setTotalAccounts(0);
|
|
743
940
|
return;
|
|
@@ -779,16 +976,16 @@ function useTokens(options = {}) {
|
|
|
779
976
|
}
|
|
780
977
|
}
|
|
781
978
|
if (setTokens([...tokenList]), setTotalAccounts(tokenAccountsResult.value.length + (includeNativeSol ? 1 : 0)), setLastUpdated(/* @__PURE__ */ new Date()), fetchMetadata && mints.length > 0) {
|
|
782
|
-
let metadata = await
|
|
979
|
+
let metadata = await fetchTokenMetadataHybrid(mints, coingeckoConfig);
|
|
783
980
|
for (let i = 0; i < tokenList.length; i++) {
|
|
784
981
|
let meta = metadata.get(tokenList[i].mint);
|
|
785
982
|
meta && (tokenList[i] = {
|
|
786
983
|
...tokenList[i],
|
|
787
984
|
name: meta.name,
|
|
788
985
|
symbol: meta.symbol,
|
|
789
|
-
logo: meta.
|
|
986
|
+
logo: transformImageUrl2(meta.logoURI, imageProxy),
|
|
790
987
|
usdPrice: meta.usdPrice,
|
|
791
|
-
formattedUsd: formatUsd(tokenList[i].amount, tokenList[i].decimals, meta.usdPrice)
|
|
988
|
+
formattedUsd: meta.usdPrice ? formatUsd(tokenList[i].amount, tokenList[i].decimals, meta.usdPrice) : void 0
|
|
792
989
|
});
|
|
793
990
|
}
|
|
794
991
|
tokenList.sort((a, b) => {
|
|
@@ -803,14 +1000,18 @@ function useTokens(options = {}) {
|
|
|
803
1000
|
} finally {
|
|
804
1001
|
setIsLoading(false);
|
|
805
1002
|
}
|
|
806
|
-
}, [connected, address$1, rpcClient, includeZeroBalance, fetchMetadata, includeNativeSol]);
|
|
807
|
-
|
|
1003
|
+
}, [connected, address$1, rpcClient, includeZeroBalance, fetchMetadata, includeNativeSol, imageProxy, coingeckoConfig]);
|
|
1004
|
+
useEffect(() => {
|
|
808
1005
|
fetchTokens();
|
|
809
1006
|
}, [fetchTokens]), useEffect(() => {
|
|
810
1007
|
if (!connected || !autoRefresh) return;
|
|
811
1008
|
let interval = setInterval(fetchTokens, refreshInterval);
|
|
812
1009
|
return () => clearInterval(interval);
|
|
813
|
-
}, [connected, autoRefresh, refreshInterval, fetchTokens]),
|
|
1010
|
+
}, [connected, autoRefresh, refreshInterval, fetchTokens]), useEffect(() => (startCacheCleanup(), () => stopCacheCleanup()), []);
|
|
1011
|
+
let wasConnectedRef = useRef(connected);
|
|
1012
|
+
return useEffect(() => {
|
|
1013
|
+
wasConnectedRef.current && !connected && clearTokenCaches(), wasConnectedRef.current = connected;
|
|
1014
|
+
}, [connected]), useMemo(
|
|
814
1015
|
() => ({
|
|
815
1016
|
tokens,
|
|
816
1017
|
isLoading,
|
|
@@ -1980,5 +2181,5 @@ function TokenListElement({
|
|
|
1980
2181
|
TokenListElement.displayName = "TokenListElement";
|
|
1981
2182
|
|
|
1982
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 };
|
|
1983
|
-
//# sourceMappingURL=chunk-
|
|
1984
|
-
//# sourceMappingURL=chunk-
|
|
2184
|
+
//# sourceMappingURL=chunk-3STZXVXD.mjs.map
|
|
2185
|
+
//# sourceMappingURL=chunk-3STZXVXD.mjs.map
|