@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.
Files changed (42) hide show
  1. package/README.md +301 -0
  2. package/dist/{chunk-U64YZRJL.mjs → chunk-3STZXVXD.mjs} +254 -53
  3. package/dist/chunk-3STZXVXD.mjs.map +1 -0
  4. package/dist/{chunk-RIQH5W7D.js → chunk-I64FD2EH.js} +4 -3
  5. package/dist/chunk-I64FD2EH.js.map +1 -0
  6. package/dist/{chunk-CLXM6UEE.js → chunk-JUZVCBAI.js} +91 -85
  7. package/dist/chunk-JUZVCBAI.js.map +1 -0
  8. package/dist/{chunk-D6PZY5G6.js → chunk-NQXK7PGX.js} +30 -26
  9. package/dist/chunk-NQXK7PGX.js.map +1 -0
  10. package/dist/{chunk-N3Q2J2FG.mjs → chunk-QKVL45F6.mjs} +10 -6
  11. package/dist/chunk-QKVL45F6.mjs.map +1 -0
  12. package/dist/{chunk-P5MWBDFG.mjs → chunk-QL3IT3TS.mjs} +4 -3
  13. package/dist/chunk-QL3IT3TS.mjs.map +1 -0
  14. package/dist/{chunk-LUZWUZ5N.js → chunk-ULUYX23Q.js} +268 -67
  15. package/dist/chunk-ULUYX23Q.js.map +1 -0
  16. package/dist/{chunk-YTCSTE3Q.mjs → chunk-VMSZJPR5.mjs} +10 -4
  17. package/dist/chunk-VMSZJPR5.mjs.map +1 -0
  18. package/dist/compat.js +3 -3
  19. package/dist/compat.mjs +1 -1
  20. package/dist/headless.d.mts +2 -2
  21. package/dist/headless.d.ts +2 -2
  22. package/dist/headless.js +120 -120
  23. package/dist/headless.mjs +3 -3
  24. package/dist/index.d.mts +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +147 -147
  27. package/dist/index.mjs +4 -4
  28. package/dist/react.d.mts +4 -4
  29. package/dist/react.d.ts +4 -4
  30. package/dist/react.js +28 -28
  31. package/dist/react.mjs +2 -2
  32. package/dist/{wallet-standard-shim-DC_Z7DS-.d.ts → wallet-standard-shim--YcrQNRt.d.ts} +83 -0
  33. package/dist/{wallet-standard-shim-Cp4vF4oo.d.mts → wallet-standard-shim-Dx7H8Ctf.d.mts} +83 -0
  34. package/package.json +1 -1
  35. package/dist/chunk-CLXM6UEE.js.map +0 -1
  36. package/dist/chunk-D6PZY5G6.js.map +0 -1
  37. package/dist/chunk-LUZWUZ5N.js.map +0 -1
  38. package/dist/chunk-N3Q2J2FG.mjs.map +0 -1
  39. package/dist/chunk-P5MWBDFG.mjs.map +0 -1
  40. package/dist/chunk-RIQH5W7D.js.map +0 -1
  41. package/dist/chunk-U64YZRJL.mjs.map +0 -1
  42. 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)