Ziya/app/utils/ipfs.ts
rizary 67fb3a203e
feat: implement CEX analysis cards and real-time token monitoring
- Add TokenCard and CexAnalysisCard components for displaying token data
- Implement real-time Redis event streaming for token updates
- Add environment-based configuration system for dev/prod Redis servers
- Create comprehensive hunting ground dashboard with card management
- Add individual and bulk card removal functionality
- Implement browser integration for token details viewing
- Add timestamp utilities and proper type handling for Redis events
- Create production-ready configuration with 154.38.185.112 Redis server
- Add comprehensive documentation in README.md and CONTRIBUTORS.md
- Restructure project architecture with proper Electron-Vue integration

BREAKING CHANGE: Redis configuration now uses environment-based settings
2025-06-23 09:03:39 +07:00

235 lines
No EOL
5.5 KiB
TypeScript

// 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<string, TokenMetadata>();
// 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<TokenMetadata | null> {
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())
};
}