- 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
235 lines
No EOL
5.5 KiB
TypeScript
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())
|
|
};
|
|
}
|