// Simple IPFS metadata fetcher with direct gateway access export interface TokenMetadata { name?: string; symbol?: string; description?: string; image?: string; showName?: boolean; createdOn?: string; twitter?: string; website?: string; telegram?: string; } // Cache for metadata to avoid duplicate requests const metadataCache = new Map(); // IPFS gateways that support CORS const IPFS_GATEWAYS = [ 'https://dweb.link/ipfs/', 'https://nftstorage.link/ipfs/', 'https://cloudflare-ipfs.com/ipfs/', 'https://gateway.pinata.cloud/ipfs/', 'https://ipfs.io/ipfs/' ]; // Extract IPFS hash from various URI formats function extractIpfsHash(uri: string): string | null { if (!uri) return null; // Handle different IPFS URI formats: // - ipfs://bafkreixxx // - https://ipfs.io/ipfs/bafkreixxx // - bafkreixxx (direct hash) if (uri.startsWith('ipfs://')) { return uri.replace('ipfs://', ''); } if (uri.includes('/ipfs/')) { const parts = uri.split('/ipfs/'); return parts[1]?.split('/')[0] || null; } // Assume it's a direct hash if it looks like one if (uri.match(/^[a-zA-Z0-9]{46,}$/)) { return uri; } return null; } export async function fetchTokenMetadata(uri: string): Promise { if (!uri || typeof uri !== 'string') { return null; } // Check cache first if (metadataCache.has(uri)) { return metadataCache.get(uri)!; } try { // Extract IPFS hash from URI const hash = extractIpfsHash(uri); if (!hash) { return null; } // Try each gateway until one works for (const gateway of IPFS_GATEWAYS) { try { const url = `${gateway}${hash}`; const response = await fetch(url, { method: 'GET', mode: 'cors', headers: { 'Accept': 'application/json', }, signal: AbortSignal.timeout(8000) // 8 second timeout }); if (!response.ok) { continue; // Try next gateway } // Check if response is JSON const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { continue; // Try next gateway } const metadata: TokenMetadata = await response.json(); // Cache the result metadataCache.set(uri, metadata); return metadata; } catch { // Continue to next gateway continue; } } // If all gateways fail, return null return null; } catch { return null; } } // Helper function to get token image URL export function getTokenImage(metadata: TokenMetadata): string | null { if (!metadata.image) return null; // If the image is already a full URL, return it if (metadata.image.startsWith('http')) { return metadata.image; } // If it's an IPFS URI, extract the hash and use a reliable gateway const hash = extractIpfsHash(metadata.image); if (hash) { return `https://dweb.link/ipfs/${hash}`; } return metadata.image; } // Helper function to check if a URL is a social media link export function getSocialIcon(url: string): string | null { if (!url) return null; if (url.includes('twitter.com') || url.includes('x.com')) { return 'twitter'; } if (url.includes('telegram.org') || url.includes('t.me')) { return 'telegram'; } if (url.includes('discord.gg') || url.includes('discord.com')) { return 'discord'; } return 'website'; } // Helper function to check if metadata has social links export function getSocialLinks(metadata: TokenMetadata) { return { twitter: metadata.twitter || null, website: metadata.website || null, telegram: metadata.telegram || null }; } // Helper function to validate and clean social URLs export function cleanSocialUrl(url: string): string | null { if (!url || typeof url !== 'string') return null; // Basic URL validation try { const urlObj = new URL(url); return urlObj.href; } catch { // If not a valid URL, try to make it one if (!url.startsWith('http')) { try { const urlObj = new URL(`https://${url}`); return urlObj.href; } catch { return null; } } return null; } } // Helper function to extract social links from metadata export function extractSocialLinks(metadata: TokenMetadata) { const links: Array<{ type: string; url: string; icon: string }> = []; if (metadata.twitter) { const cleanUrl = cleanSocialUrl(metadata.twitter); const icon = getSocialIcon('twitter'); if (cleanUrl && icon) { links.push({ type: 'twitter', url: cleanUrl, icon }); } } if (metadata.website) { const cleanUrl = cleanSocialUrl(metadata.website); const icon = getSocialIcon('website'); if (cleanUrl && icon) { links.push({ type: 'website', url: cleanUrl, icon }); } } if (metadata.telegram) { const cleanUrl = cleanSocialUrl(metadata.telegram); const icon = getSocialIcon('telegram'); if (cleanUrl && icon) { links.push({ type: 'telegram', url: cleanUrl, icon }); } } return links; } // Clear cache utility export function clearMetadataCache(): void { metadataCache.clear(); } // Get cache statistics export function getCacheStats() { return { size: metadataCache.size, keys: Array.from(metadataCache.keys()) }; }