@solana/connector 0.1.4 → 0.1.6
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 +301 -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,307 @@ const mobile = getDefaultMobileConfig({
|
|
|
608
608
|
|
|
609
609
|
---
|
|
610
610
|
|
|
611
|
+
## Security Considerations
|
|
612
|
+
|
|
613
|
+
### RPC API Key Protection
|
|
614
|
+
|
|
615
|
+
If you're using a paid RPC provider (Helius, QuickNode, etc.), avoid exposing your API key client-side. Anyone can grab it from the browser's network tab.
|
|
616
|
+
|
|
617
|
+
**Solution: RPC Proxy Route**
|
|
618
|
+
|
|
619
|
+
Create an API route that proxies RPC requests, keeping the API key server-side:
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
// app/api/rpc/route.ts
|
|
623
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
624
|
+
|
|
625
|
+
// Server-side only - not exposed to client
|
|
626
|
+
const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
|
|
627
|
+
|
|
628
|
+
export async function POST(request: NextRequest) {
|
|
629
|
+
try {
|
|
630
|
+
const body = await request.json();
|
|
631
|
+
|
|
632
|
+
const response = await fetch(RPC_URL, {
|
|
633
|
+
method: 'POST',
|
|
634
|
+
headers: { 'Content-Type': 'application/json' },
|
|
635
|
+
body: JSON.stringify(body),
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const data = await response.json();
|
|
639
|
+
return NextResponse.json(data);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
return NextResponse.json({ error: 'RPC request failed' }, { status: 500 });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Then configure the connector to use the proxy:
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
'use client';
|
|
650
|
+
|
|
651
|
+
import { getDefaultConfig } from '@solana/connector/headless';
|
|
652
|
+
|
|
653
|
+
// Get origin for absolute URL (Kit requires full URLs)
|
|
654
|
+
const getOrigin = () => {
|
|
655
|
+
if (typeof window !== 'undefined') return window.location.origin;
|
|
656
|
+
return 'http://localhost:3000';
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const config = getDefaultConfig({
|
|
660
|
+
appName: 'My App',
|
|
661
|
+
clusters: [
|
|
662
|
+
{
|
|
663
|
+
id: 'solana:mainnet' as const,
|
|
664
|
+
label: 'Mainnet',
|
|
665
|
+
name: 'mainnet-beta' as const,
|
|
666
|
+
url: `${getOrigin()}/api/rpc`, // Proxy URL
|
|
667
|
+
},
|
|
668
|
+
// ... other clusters
|
|
669
|
+
],
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Your `.env` file (no `NEXT_PUBLIC_` prefix):
|
|
674
|
+
```
|
|
675
|
+
SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=your-key
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Token Image Privacy
|
|
679
|
+
|
|
680
|
+
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:
|
|
681
|
+
|
|
682
|
+
- User IP addresses
|
|
683
|
+
- Request timing (when users viewed their tokens)
|
|
684
|
+
- User agent and browser information
|
|
685
|
+
|
|
686
|
+
This could potentially be exploited by malicious token creators who set tracking URLs in their token metadata.
|
|
687
|
+
|
|
688
|
+
### Image Proxy Configuration
|
|
689
|
+
|
|
690
|
+
To protect user privacy, you can configure an image proxy that fetches images on behalf of your users:
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
const config = getDefaultConfig({
|
|
694
|
+
appName: 'My App',
|
|
695
|
+
imageProxy: '/_next/image?w=64&q=75&url=', // Next.js Image Optimization
|
|
696
|
+
});
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
When `imageProxy` is set, all token image URLs returned by `useTokens()` and `useTransactions()` will be automatically transformed:
|
|
700
|
+
|
|
701
|
+
```
|
|
702
|
+
// Original URL from token metadata
|
|
703
|
+
https://raw.githubusercontent.com/.../token-logo.png
|
|
704
|
+
|
|
705
|
+
// Transformed URL (when imageProxy is set)
|
|
706
|
+
/_next/image?w=64&q=75&url=https%3A%2F%2Fraw.githubusercontent.com%2F...%2Ftoken-logo.png
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Common Proxy Options
|
|
710
|
+
|
|
711
|
+
| Service | Configuration |
|
|
712
|
+
|---------|---------------|
|
|
713
|
+
| **Next.js Image** | `imageProxy: '/_next/image?w=64&q=75&url='` |
|
|
714
|
+
| **Cloudflare** | `imageProxy: '/cdn-cgi/image/width=64,quality=75/'` |
|
|
715
|
+
| **imgproxy** | `imageProxy: 'https://imgproxy.example.com/insecure/fill/64/64/'` |
|
|
716
|
+
| **Custom API** | `imageProxy: '/api/image-proxy?url='` |
|
|
717
|
+
|
|
718
|
+
### Custom Proxy API Route (Next.js Example)
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
// app/api/image-proxy/route.ts
|
|
722
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
723
|
+
import dns from 'dns/promises';
|
|
724
|
+
|
|
725
|
+
// Allowlist of permitted domains for image fetching
|
|
726
|
+
const ALLOWED_DOMAINS = [
|
|
727
|
+
'raw.githubusercontent.com',
|
|
728
|
+
'arweave.net',
|
|
729
|
+
'ipfs.io',
|
|
730
|
+
'cloudflare-ipfs.com',
|
|
731
|
+
'nftstorage.link',
|
|
732
|
+
// Add other trusted image domains as needed
|
|
733
|
+
];
|
|
734
|
+
|
|
735
|
+
// Check if an IP address falls within private/reserved ranges
|
|
736
|
+
function isPrivateOrReservedIP(ip: string): boolean {
|
|
737
|
+
// IPv4 private/reserved ranges
|
|
738
|
+
const ipv4PrivateRanges = [
|
|
739
|
+
/^127\./, // 127.0.0.0/8 (loopback)
|
|
740
|
+
/^10\./, // 10.0.0.0/8 (private)
|
|
741
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12 (private)
|
|
742
|
+
/^192\.168\./, // 192.168.0.0/16 (private)
|
|
743
|
+
/^169\.254\./, // 169.254.0.0/16 (link-local/metadata)
|
|
744
|
+
/^0\./, // 0.0.0.0/8 (current network)
|
|
745
|
+
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./, // 100.64.0.0/10 (CGNAT)
|
|
746
|
+
/^192\.0\.0\./, // 192.0.0.0/24 (IETF protocol assignments)
|
|
747
|
+
/^192\.0\.2\./, // 192.0.2.0/24 (TEST-NET-1)
|
|
748
|
+
/^198\.51\.100\./, // 198.51.100.0/24 (TEST-NET-2)
|
|
749
|
+
/^203\.0\.113\./, // 203.0.113.0/24 (TEST-NET-3)
|
|
750
|
+
/^224\./, // 224.0.0.0/4 (multicast)
|
|
751
|
+
/^240\./, // 240.0.0.0/4 (reserved)
|
|
752
|
+
/^255\.255\.255\.255$/, // broadcast
|
|
753
|
+
];
|
|
754
|
+
|
|
755
|
+
// IPv6 private/reserved ranges
|
|
756
|
+
const ipv6PrivatePatterns = [
|
|
757
|
+
/^::1$/, // loopback
|
|
758
|
+
/^fe80:/i, // link-local
|
|
759
|
+
/^fc00:/i, // unique local (fc00::/7)
|
|
760
|
+
/^fd/i, // unique local (fd00::/8)
|
|
761
|
+
/^::ffff:(127\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.|169\.254\.)/i, // IPv4-mapped
|
|
762
|
+
];
|
|
763
|
+
|
|
764
|
+
// Check IPv4
|
|
765
|
+
for (const range of ipv4PrivateRanges) {
|
|
766
|
+
if (range.test(ip)) return true;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Check IPv6
|
|
770
|
+
for (const pattern of ipv6PrivatePatterns) {
|
|
771
|
+
if (pattern.test(ip)) return true;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Validate and parse the URL
|
|
778
|
+
function validateUrl(urlString: string): URL | null {
|
|
779
|
+
try {
|
|
780
|
+
const parsed = new URL(urlString);
|
|
781
|
+
// Only allow http and https protocols
|
|
782
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
return parsed;
|
|
786
|
+
} catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Check if hostname is in the allowlist
|
|
792
|
+
function isAllowedDomain(hostname: string): boolean {
|
|
793
|
+
return ALLOWED_DOMAINS.some(
|
|
794
|
+
(domain) => hostname === domain || hostname.endsWith(`.${domain}`)
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export async function GET(request: NextRequest) {
|
|
799
|
+
const urlParam = request.nextUrl.searchParams.get('url');
|
|
800
|
+
|
|
801
|
+
// (1) Ensure URL exists and parses correctly with http/https
|
|
802
|
+
if (!urlParam) {
|
|
803
|
+
return new NextResponse('Missing URL parameter', { status: 400 });
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const parsedUrl = validateUrl(urlParam);
|
|
807
|
+
if (!parsedUrl) {
|
|
808
|
+
return new NextResponse('Invalid URL or protocol', { status: 400 });
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// (2) Enforce allowlist of permitted domains
|
|
812
|
+
if (!isAllowedDomain(parsedUrl.hostname)) {
|
|
813
|
+
return new NextResponse('Domain not allowed', { status: 403 });
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// (3) Resolve hostname and check for private/reserved IPs
|
|
817
|
+
try {
|
|
818
|
+
const addresses = await dns.resolve(parsedUrl.hostname);
|
|
819
|
+
for (const ip of addresses) {
|
|
820
|
+
if (isPrivateOrReservedIP(ip)) {
|
|
821
|
+
return new NextResponse('Resolved IP is not allowed', { status: 403 });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
} catch {
|
|
825
|
+
return new NextResponse('Failed to resolve hostname', { status: 400 });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// (4) All checks passed - perform the fetch
|
|
829
|
+
try {
|
|
830
|
+
const response = await fetch(parsedUrl.toString());
|
|
831
|
+
const buffer = await response.arrayBuffer();
|
|
832
|
+
|
|
833
|
+
return new NextResponse(buffer, {
|
|
834
|
+
headers: {
|
|
835
|
+
'Content-Type': response.headers.get('Content-Type') || 'image/png',
|
|
836
|
+
'Cache-Control': 'public, max-age=86400',
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
} catch {
|
|
840
|
+
return new NextResponse('Failed to fetch image', { status: 500 });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
## CoinGecko API & Rate Limits
|
|
848
|
+
|
|
849
|
+
The `useTokens()` hook fetches token prices from CoinGecko. CoinGecko has rate limits that may affect your application:
|
|
850
|
+
|
|
851
|
+
### Rate Limits (as of 2024)
|
|
852
|
+
|
|
853
|
+
| Tier | Rate Limit | API Key Required |
|
|
854
|
+
|------|------------|------------------|
|
|
855
|
+
| **Free (Public)** | 10-30 requests/minute | No |
|
|
856
|
+
| **Demo** | 30 requests/minute | Yes (free) |
|
|
857
|
+
| **Analyst** | 500 requests/minute | Yes (paid) |
|
|
858
|
+
| **Pro** | 1000+ requests/minute | Yes (paid) |
|
|
859
|
+
|
|
860
|
+
### Handling Rate Limits
|
|
861
|
+
|
|
862
|
+
ConnectorKit automatically handles rate limits with:
|
|
863
|
+
- **Exponential backoff**: Retries with increasing delays
|
|
864
|
+
- **Jitter**: Random delay added to prevent thundering herd
|
|
865
|
+
- **Retry-After header**: Honors server-specified wait times
|
|
866
|
+
- **Bounded timeout**: Won't block forever (default 30s max)
|
|
867
|
+
|
|
868
|
+
### Adding a CoinGecko API Key
|
|
869
|
+
|
|
870
|
+
For higher rate limits, add a free Demo API key from [CoinGecko](https://www.coingecko.com/en/api/pricing):
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
const config = getDefaultConfig({
|
|
874
|
+
appName: 'My App',
|
|
875
|
+
coingecko: {
|
|
876
|
+
apiKey: process.env.COINGECKO_API_KEY, // Demo or Pro API key
|
|
877
|
+
isPro: false, // Set to true for Pro API keys
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### Advanced Configuration
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
const config = getDefaultConfig({
|
|
886
|
+
appName: 'My App',
|
|
887
|
+
coingecko: {
|
|
888
|
+
// API key for higher rate limits (optional)
|
|
889
|
+
apiKey: process.env.COINGECKO_API_KEY,
|
|
890
|
+
|
|
891
|
+
// Set to true if using a Pro API key (default: false for Demo keys)
|
|
892
|
+
isPro: false,
|
|
893
|
+
|
|
894
|
+
// Maximum retry attempts on 429 (default: 3)
|
|
895
|
+
maxRetries: 3,
|
|
896
|
+
|
|
897
|
+
// Base delay for exponential backoff in ms (default: 1000)
|
|
898
|
+
baseDelay: 1000,
|
|
899
|
+
|
|
900
|
+
// Maximum total timeout in ms (default: 30000)
|
|
901
|
+
maxTimeout: 30000,
|
|
902
|
+
},
|
|
903
|
+
});
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
### Caching
|
|
907
|
+
|
|
908
|
+
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.
|
|
909
|
+
|
|
910
|
+
---
|
|
911
|
+
|
|
611
912
|
## Advanced Usage
|
|
612
913
|
|
|
613
914
|
### Headless Client (Vue, Svelte, Vanilla JS)
|