| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { useState, useRef } from 'react'; |
| import { useAppStore } from '../store/appStore'; |
| import { |
| useNetworkGraph, |
| useBundleDetection, |
| useFreshWalletAnalysis, |
| useSniperDetection, |
| useCopyTradingDetection, |
| } from '../hooks/useAnalytics'; |
| import { |
| Search, |
| Network, |
| Users, |
| Clock, |
| Zap, |
| Target, |
| Share2, |
| Copy, |
| Download, |
| AlertTriangle, |
| RotateCcw, |
| Filter, |
| Activity, |
| Brain, |
| Maximize2, |
| Minimize2, |
| } from 'lucide-react'; |
|
|
| const CHAINS = [ |
| { id: 'ethereum', name: 'Ethereum', icon: '⧫' }, |
| { id: 'bsc', name: 'BSC', icon: 'B' }, |
| { id: 'polygon', name: 'Polygon', icon: 'P' }, |
| { id: 'arbitrum', name: 'Arbitrum', icon: 'A' }, |
| { id: 'optimism', name: 'Optimism', icon: 'O' }, |
| { id: 'base', name: 'Base', icon: 'B' }, |
| { id: 'solana', name: 'Solana', icon: 'S' }, |
| ]; |
|
|
| const ANALYSIS_TYPES = [ |
| { id: 'network', label: 'Muncher Map', icon: Network, desc: 'Visual wallet relationships' }, |
| { id: 'bundle', label: 'Bundle Detection', icon: Users, desc: 'Find coordinated groups' }, |
| { id: 'fresh', label: 'Fresh Wallet', icon: Clock, desc: 'New wallet analysis' }, |
| { id: 'sniper', label: 'Sniper Track', icon: Target, desc: 'First-block buyers' }, |
| { id: 'copy', label: 'Copy Trading', icon: Share2, desc: 'Mirror patterns' }, |
| { id: 'bot', label: 'Bot Farm', icon: Zap, desc: 'Automation detection' }, |
| ]; |
|
|
| |
| const MOCK_NETWORK_NODES = [ |
| { id: '0x1234...5678', type: 'target', x: 400, y: 300, risk: 85, label: 'Target Wallet' }, |
| { id: '0xabcd...ef01', type: 'connected', x: 250, y: 200, risk: 92, label: 'Sniper 1' }, |
| { id: '0x2345...6789', type: 'connected', x: 550, y: 200, risk: 88, label: 'Sniper 2' }, |
| { id: '0xbcde...f012', type: 'connected', x: 200, y: 350, risk: 45, label: 'CEX Deposit' }, |
| { id: '0x3456...7890', type: 'connected', x: 600, y: 350, risk: 67, label: 'Fresh Wallet' }, |
| { id: '0xcdef...0123', type: 'connected', x: 300, y: 450, risk: 73, label: 'Related' }, |
| { id: '0x4567...8901', type: 'connected', x: 500, y: 450, risk: 56, label: 'Related' }, |
| { id: '0xdef0...1234', type: 'cex', x: 150, y: 300, risk: 30, label: 'Binance Hot' }, |
| { id: '0x5678...9012', type: 'bridge', x: 650, y: 300, risk: 40, label: 'Bridge' }, |
| ]; |
|
|
| const MOCK_NETWORK_EDGES = [ |
| { from: '0x1234...5678', to: '0xabcd...ef01', type: 'fund', value: 5.2 }, |
| { from: '0x1234...5678', to: '0x2345...6789', type: 'fund', value: 3.8 }, |
| { from: '0xabcd...ef01', to: '0xbcde...f012', type: 'cex', value: 2.1 }, |
| { from: '0x2345...6789', to: '0x3456...7890', type: 'transfer', value: 1.5 }, |
| { from: '0x1234...5678', to: '0xcdef...0123', type: 'interaction', value: 0.8 }, |
| { from: '0x1234...5678', to: '0x4567...8901', type: 'interaction', value: 0.3 }, |
| { from: '0xbcde...f012', to: '0xdef0...1234', type: 'cex', value: 5.5 }, |
| { from: '0x3456...7890', to: '0x5678...9012', type: 'bridge', value: 2.0 }, |
| { from: '0xabcd...ef01', to: '0x2345...6789', type: 'same_origin', value: 0 }, |
| ]; |
|
|
| export default function Analytics() { |
| const [address, setAddress] = useState(''); |
| const [chain, setChain] = useState('ethereum'); |
| const [activeTab, setActiveTab] = useState('network'); |
| const [hops, setHops] = useState(2); |
| const [selectedNode, setSelectedNode] = useState<string | null>(null); |
| const [zoom, setZoom] = useState(1); |
| const [isFullscreen, setIsFullscreen] = useState(false); |
| const [showLabels, setShowLabels] = useState(true); |
| const [riskFilter, setRiskFilter] = useState<'all' | 'high' | 'critical'>('all'); |
| const graphRef = useRef<HTMLDivElement>(null); |
|
|
| const user = useAppStore((state) => state.user); |
| const setError = useAppStore((state) => state.setError); |
|
|
| const tier = user?.tier || 'FREE'; |
| const isPro = tier === 'PRO' || tier === 'ELITE' || tier === 'ENTERPRISE'; |
| const isElite = tier === 'ELITE' || tier === 'ENTERPRISE'; |
|
|
| |
| const networkQuery = useNetworkGraph(address, chain, { enabled: address.length >= 10 && activeTab === 'network' }); |
| const bundleQuery = useBundleDetection(address, chain, { enabled: address.length >= 10 && activeTab === 'bundle' }); |
| const freshQuery = useFreshWalletAnalysis(address, chain, { enabled: address.length >= 10 && activeTab === 'fresh' }); |
| const sniperQuery = useSniperDetection(address, chain, { enabled: address.length >= 10 && activeTab === 'sniper' }); |
| const copyQuery = useCopyTradingDetection(address, chain, { enabled: address.length >= 10 && activeTab === 'copy' }); |
|
|
| const handleAnalyze = () => { |
| if (!address || address.length < 10) { |
| setError('Please enter a valid wallet address'); |
| return; |
| } |
| |
| switch (activeTab) { |
| case 'network': |
| networkQuery.refetch(); |
| break; |
| case 'bundle': |
| bundleQuery.refetch(); |
| break; |
| case 'fresh': |
| freshQuery.refetch(); |
| break; |
| case 'sniper': |
| sniperQuery.refetch(); |
| break; |
| case 'copy': |
| copyQuery.refetch(); |
| break; |
| } |
| }; |
|
|
| const getNodeColor = (risk: number, type: string) => { |
| if (type === 'target') return '#8b5cf6'; |
| if (type === 'cex') return '#3b82f6'; |
| if (type === 'bridge') return '#f59e0b'; |
| if (risk >= 80) return '#ef4444'; |
| if (risk >= 60) return '#f97316'; |
| if (risk >= 40) return '#eab308'; |
| return '#22c55e'; |
| }; |
|
|
| const getEdgeStyle = (type: string) => { |
| switch (type) { |
| case 'fund': |
| return { stroke: '#8b5cf6', width: 2, dash: '0' }; |
| case 'cex': |
| return { stroke: '#3b82f6', width: 1.5, dash: '5,5' }; |
| case 'bridge': |
| return { stroke: '#f59e0b', width: 1.5, dash: '10,5' }; |
| case 'same_origin': |
| return { stroke: '#ef4444', width: 2, dash: '0' }; |
| default: |
| return { stroke: '#6b7280', width: 1, dash: '0' }; |
| } |
| }; |
|
|
| const renderNetworkGraph = () => { |
| const nodes = networkQuery.data?.nodes || MOCK_NETWORK_NODES; |
| const edges = networkQuery.data?.edges || MOCK_NETWORK_EDGES; |
|
|
| const filteredNodes = riskFilter === 'all' |
| ? nodes |
| : riskFilter === 'critical' |
| ? nodes.filter((n: any) => n.risk >= 80) |
| : nodes.filter((n: any) => n.risk >= 60); |
|
|
| return ( |
| <div |
| ref={graphRef} |
| className={`relative bg-[#0a0a0f] rounded-xl border border-purple-500/20 overflow-hidden ${ |
| isFullscreen ? 'fixed inset-4 z-50' : 'h-[500px]' |
| }`} |
| > |
| {/* Graph Controls */} |
| <div className="absolute top-4 left-4 right-4 flex items-center justify-between z-10"> |
| <div className="flex items-center gap-2"> |
| <span className="text-xs text-gray-400 bg-black/50 px-2 py-1 rounded"> |
| {filteredNodes.length} nodes · {edges.length} connections |
| </span> |
| {isElite && ( |
| <span className="text-xs text-green-400 bg-green-500/10 px-2 py-1 rounded flex items-center gap-1"> |
| <Brain size={12} /> |
| ML-Powered |
| </span> |
| )} |
| </div> |
| <div className="flex items-center gap-2"> |
| <button |
| onClick={() => setZoom(z => Math.max(0.5, z - 0.1))} |
| className="p-2 bg-black/50 rounded-lg text-gray-400 hover:text-white" |
| > |
| - |
| </button> |
| <span className="text-xs text-gray-400 w-12 text-center">{Math.round(zoom * 100)}%</span> |
| <button |
| onClick={() => setZoom(z => Math.min(2, z + 0.1))} |
| className="p-2 bg-black/50 rounded-lg text-gray-400 hover:text-white" |
| > |
| + |
| </button> |
| <button |
| onClick={() => setZoom(1)} |
| className="p-2 bg-black/50 rounded-lg text-gray-400 hover:text-white" |
| > |
| <RotateCcw size={16} /> |
| </button> |
| <button |
| onClick={() => setIsFullscreen(!isFullscreen)} |
| className="p-2 bg-black/50 rounded-lg text-gray-400 hover:text-white" |
| > |
| {isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />} |
| </button> |
| </div> |
| </div> |
| |
| {/* Legend */} |
| <div className="absolute bottom-4 left-4 z-10 bg-black/70 rounded-lg p-3 space-y-2"> |
| <p className="text-xs text-gray-400 font-medium mb-2">Connection Types</p> |
| <div className="flex items-center gap-2"> |
| <div className="w-4 h-0.5 bg-purple-500"></div> |
| <span className="text-xs text-gray-300">Fund Flow</span> |
| </div> |
| <div className="flex items-center gap-2"> |
| <div className="w-4 h-0.5 bg-blue-500 border-dashed border-t border-blue-500"></div> |
| <span className="text-xs text-gray-300">CEX</span> |
| </div> |
| <div className="flex items-center gap-2"> |
| <div className="w-4 h-0.5 bg-amber-500"></div> |
| <span className="text-xs text-gray-300">Bridge</span> |
| </div> |
| <div className="flex items-center gap-2"> |
| <div className="w-4 h-0.5 bg-red-500"></div> |
| <span className="text-xs text-gray-300">Same Origin</span> |
| </div> |
| </div> |
| |
| {/* SVG Graph */} |
| <svg |
| viewBox="0 0 800 600" |
| className="w-full h-full" |
| style={{ transform: `scale(${zoom})`, transformOrigin: 'center' }} |
| > |
| {/* Grid background */} |
| <defs> |
| <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse"> |
| <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1f2937" strokeWidth="0.5"/> |
| </pattern> |
| </defs> |
| <rect width="800" height="600" fill="url(#grid)" /> |
| |
| {/* Edges */} |
| {edges.map((edge: any, idx: number) => { |
| const fromNode = nodes.find((n: any) => n.id === edge.from); |
| const toNode = nodes.find((n: any) => n.id === edge.to); |
| if (!fromNode || !toNode) return null; |
| const style = getEdgeStyle(edge.type); |
| return ( |
| <g key={idx}> |
| <line |
| x1={fromNode.x} |
| y1={fromNode.y} |
| x2={toNode.x} |
| y2={toNode.y} |
| stroke={style.stroke} |
| strokeWidth={style.width} |
| strokeDasharray={style.dash} |
| opacity={0.6} |
| /> |
| {edge.value > 0 && ( |
| <text |
| x={(fromNode.x + toNode.x) / 2} |
| y={(fromNode.y + toNode.y) / 2 - 5} |
| fill="#9ca3af" |
| fontSize="10" |
| textAnchor="middle" |
| > |
| {edge.value} ETH |
| </text> |
| )} |
| </g> |
| ); |
| })} |
| |
| {/* Nodes */} |
| {filteredNodes.map((node: any) => ( |
| <g |
| key={node.id} |
| className="cursor-pointer" |
| onClick={() => setSelectedNode(selectedNode === node.id ? null : node.id)} |
| > |
| <circle |
| cx={node.x} |
| cy={node.y} |
| r={node.type === 'target' ? 25 : 18} |
| fill={getNodeColor(node.risk, node.type)} |
| stroke={selectedNode === node.id ? '#fff' : 'transparent'} |
| strokeWidth={3} |
| className="transition-all hover:opacity-80" |
| /> |
| {node.type === 'target' && ( |
| <circle |
| cx={node.x} |
| cy={node.y} |
| r={30} |
| fill="none" |
| stroke="#8b5cf6" |
| strokeWidth="2" |
| strokeDasharray="4,4" |
| className="animate-spin" |
| style={{ transformOrigin: `${node.x}px ${node.y}px`, animationDuration: '8s' }} |
| /> |
| )} |
| {showLabels && ( |
| <text |
| x={node.x} |
| y={node.y + (node.type === 'target' ? 40 : 32)} |
| fill="#fff" |
| fontSize="11" |
| textAnchor="middle" |
| className="pointer-events-none" |
| > |
| {node.label} |
| </text> |
| )} |
| <text |
| x={node.x} |
| y={node.y + 5} |
| fill="#fff" |
| fontSize="10" |
| textAnchor="middle" |
| className="pointer-events-none font-mono" |
| > |
| {node.risk} |
| </text> |
| </g> |
| ))} |
| </svg> |
| |
| {/* Selected Node Panel */} |
| {selectedNode && ( |
| <div className="absolute top-16 right-4 w-64 bg-[#12121a] rounded-lg border border-purple-500/20 p-4 z-10"> |
| <div className="flex items-center justify-between mb-3"> |
| <h4 className="text-sm font-medium text-white">Node Details</h4> |
| <button |
| onClick={() => setSelectedNode(null)} |
| className="text-gray-400 hover:text-white" |
| > |
| ✕ |
| </button> |
| </div> |
| {(() => { |
| const node = nodes.find((n: any) => n.id === selectedNode); |
| if (!node) return null; |
| return ( |
| <div className="space-y-2"> |
| <p className="text-xs text-gray-400">Address</p> |
| <p className="text-sm font-mono text-neon-green">{node.id}</p> |
| <p className="text-xs text-gray-400">Risk Score</p> |
| <p className={`text-lg font-bold ${node.risk >= 80 ? 'text-red-400' : node.risk >= 60 ? 'text-orange-400' : 'text-green-400'}`}> |
| {node.risk}/100 |
| </p> |
| <p className="text-xs text-gray-400">Type</p> |
| <p className="text-sm text-white capitalize">{node.type.replace('_', ' ')}</p> |
| <div className="flex gap-2 pt-2"> |
| <button className="flex-1 px-2 py-1 bg-purple-600/20 text-purple-300 rounded text-xs"> |
| Expand |
| </button> |
| <button className="flex-1 px-2 py-1 bg-blue-600/20 text-blue-300 rounded text-xs flex items-center justify-center gap-1"> |
| <Copy size={12} /> |
| Copy |
| </button> |
| </div> |
| </div> |
| ); |
| })()} |
| </div> |
| )} |
| </div> |
| ); |
| }; |
|
|
| const renderBundleAnalysis = () => { |
| const bundles = bundleQuery.data?.bundles || [ |
| { |
| id: 1, |
| size: 12, |
| wallets: ['0xabc...123', '0xdef...456', '0xghi...789'], |
| coordination_score: 94, |
| first_seen: '2 hours ago', |
| total_volume: 45.8, |
| risk_level: 'CRITICAL', |
| pattern: 'Same-block buys + synchronized dumps', |
| }, |
| { |
| id: 2, |
| size: 8, |
| wallets: ['0xjkl...012', '0xmno...345'], |
| coordination_score: 78, |
| first_seen: '5 hours ago', |
| total_volume: 23.2, |
| risk_level: 'HIGH', |
| pattern: 'Funding from same CEX batch', |
| }, |
| ]; |
|
|
| return ( |
| <div className="space-y-4"> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center gap-2"> |
| <AlertTriangle className="text-red-400" size={20} /> |
| <span className="text-white font-medium">{bundles.length} Coordinated Groups Detected</span> |
| </div> |
| <button className="px-3 py-1.5 bg-purple-600/20 text-purple-300 rounded-lg text-sm flex items-center gap-1"> |
| <Download size={14} /> |
| Export Report |
| </button> |
| </div> |
| |
| {bundles.map((bundle: any) => ( |
| <div key={bundle.id} className="bg-[#12121a] rounded-xl border border-red-500/20 p-4"> |
| <div className="flex items-start justify-between mb-4"> |
| <div> |
| <h4 className="text-white font-medium flex items-center gap-2"> |
| Bundle #{bundle.id} |
| <span className={`px-2 py-0.5 rounded text-xs ${ |
| bundle.risk_level === 'CRITICAL' ? 'bg-red-500/20 text-red-400' : |
| bundle.risk_level === 'HIGH' ? 'bg-orange-500/20 text-orange-400' : |
| 'bg-yellow-500/20 text-yellow-400' |
| }`}> |
| {bundle.risk_level} |
| </span> |
| </h4> |
| <p className="text-sm text-gray-400 mt-1">{bundle.pattern}</p> |
| </div> |
| <div className="text-right"> |
| <p className="text-2xl font-bold text-white">{bundle.size}</p> |
| <p className="text-xs text-gray-400">wallets</p> |
| </div> |
| </div> |
| |
| <div className="grid grid-cols-3 gap-4 mb-4"> |
| <div className="bg-black/30 rounded-lg p-3"> |
| <p className="text-xs text-gray-400">Coordination</p> |
| <p className="text-lg font-bold text-purple-400">{bundle.coordination_score}%</p> |
| </div> |
| <div className="bg-black/30 rounded-lg p-3"> |
| <p className="text-xs text-gray-400">Total Volume</p> |
| <p className="text-lg font-bold text-white">{bundle.total_volume} ETH</p> |
| </div> |
| <div className="bg-black/30 rounded-lg p-3"> |
| <p className="text-xs text-gray-400">First Seen</p> |
| <p className="text-lg font-bold text-white">{bundle.first_seen}</p> |
| </div> |
| </div> |
| |
| <div className="space-y-2"> |
| <p className="text-xs text-gray-400">Connected Wallets ({bundle.wallets.length} shown)</p> |
| <div className="flex flex-wrap gap-2"> |
| {bundle.wallets.map((wallet: string, idx: number) => ( |
| <span key={idx} className="px-2 py-1 bg-black/50 rounded text-xs font-mono text-gray-300"> |
| {wallet} |
| </span> |
| ))} |
| {bundle.size > bundle.wallets.length && ( |
| <span className="px-2 py-1 bg-purple-600/20 rounded text-xs text-purple-300"> |
| +{bundle.size - bundle.wallets.length} more |
| </span> |
| )} |
| </div> |
| </div> |
| |
| <div className="flex gap-2 mt-4"> |
| <button className="flex-1 px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm transition-colors"> |
| View in Muncher Map |
| </button> |
| <button className="px-3 py-2 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg text-sm transition-colors flex items-center gap-1"> |
| <AlertTriangle size={14} /> |
| Report |
| </button> |
| </div> |
| </div> |
| ))} |
| </div> |
| ); |
| }; |
|
|
| const renderFreshWalletAnalysis = () => { |
| const data = freshQuery.data || { |
| total_holders: 1247, |
| fresh_wallets: 892, |
| fresh_percentage: 71.5, |
| avg_wallet_age_hours: 18.3, |
| funding_sources: [ |
| { source: 'Binance', count: 423, percentage: 47.4 }, |
| { source: 'OKX', count: 198, percentage: 22.2 }, |
| { source: 'Faucet', count: 156, percentage: 17.5 }, |
| { source: 'Other CEX', count: 115, percentage: 12.9 }, |
| ], |
| age_distribution: [ |
| { range: '< 1 hour', count: 234, percentage: 26.2 }, |
| { range: '1-6 hours', count: 356, percentage: 39.9 }, |
| { range: '6-24 hours', count: 198, percentage: 22.2 }, |
| { range: '1-7 days', count: 104, percentage: 11.7 }, |
| ], |
| risk_assessment: { |
| score: 87, |
| level: 'CRITICAL', |
| factors: [ |
| '71.5% wallets created within 24 hours', |
| '47% funded from same exchange batch', |
| 'Batch creation detected (156 wallets in same block)', |
| 'No organic transaction history', |
| ], |
| }, |
| }; |
|
|
| return ( |
| <div className="space-y-6"> |
| {/* Risk Overview */} |
| <div className="bg-gradient-to-r from-red-500/20 to-orange-500/20 rounded-xl border border-red-500/30 p-6"> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center gap-4"> |
| <div className="w-16 h-16 rounded-full bg-red-500/30 flex items-center justify-center"> |
| <Clock size={32} className="text-red-400" /> |
| </div> |
| <div> |
| <h3 className="text-xl font-bold text-white">Fresh Wallet Risk</h3> |
| <p className="text-red-300">{data.risk_assessment.factors[0]}</p> |
| </div> |
| </div> |
| <div className="text-right"> |
| <p className="text-5xl font-bold text-red-400">{data.risk_assessment.score}</p> |
| <p className="text-sm text-red-300">/100 Risk Score</p> |
| </div> |
| </div> |
| </div> |
| |
| {/* Stats Grid */} |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> |
| <div className="bg-[#12121a] rounded-xl p-4 border border-purple-500/20"> |
| <p className="text-sm text-gray-400">Total Holders</p> |
| <p className="text-2xl font-bold text-white">{data.total_holders.toLocaleString()}</p> |
| </div> |
| <div className="bg-[#12121a] rounded-xl p-4 border border-red-500/20"> |
| <p className="text-sm text-gray-400">Fresh Wallets (<24h)</p> |
| <p className="text-2xl font-bold text-red-400">{data.fresh_wallets.toLocaleString()}</p> |
| <p className="text-sm text-red-300">{data.fresh_percentage}%</p> |
| </div> |
| <div className="bg-[#12121a] rounded-xl p-4 border border-orange-500/20"> |
| <p className="text-sm text-gray-400">Avg Wallet Age</p> |
| <p className="text-2xl font-bold text-orange-400">{data.avg_wallet_age_hours}h</p> |
| </div> |
| <div className="bg-[#12121a] rounded-xl p-4 border border-purple-500/20"> |
| <p className="text-sm text-gray-400">Prediction</p> |
| <p className="text-lg font-bold text-purple-400">87% Rug Probability</p> |
| </div> |
| </div> |
| |
| {/* Funding Sources */} |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
| <div className="bg-[#12121a] rounded-xl border border-purple-500/20 p-4"> |
| <h4 className="text-white font-medium mb-4">Funding Sources</h4> |
| <div className="space-y-3"> |
| {data.funding_sources.map((source: any, idx: number) => ( |
| <div key={idx}> |
| <div className="flex justify-between text-sm mb-1"> |
| <span className="text-gray-300">{source.source}</span> |
| <span className="text-gray-400">{source.count} ({source.percentage}%)</span> |
| </div> |
| <div className="h-2 bg-black/50 rounded-full overflow-hidden"> |
| <div |
| className="h-full bg-purple-500 rounded-full" |
| style={{ width: `${source.percentage}%` }} |
| /> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| <div className="bg-[#12121a] rounded-xl border border-purple-500/20 p-4"> |
| <h4 className="text-white font-medium mb-4">Age Distribution</h4> |
| <div className="space-y-3"> |
| {data.age_distribution.map((age: any, idx: number) => ( |
| <div key={idx}> |
| <div className="flex justify-between text-sm mb-1"> |
| <span className="text-gray-300">{age.range}</span> |
| <span className="text-gray-400">{age.count} ({age.percentage}%)</span> |
| </div> |
| <div className="h-2 bg-black/50 rounded-full overflow-hidden"> |
| <div |
| className={`h-full rounded-full ${ |
| idx === 0 ? 'bg-red-500' : |
| idx === 1 ? 'bg-orange-500' : |
| idx === 2 ? 'bg-yellow-500' : 'bg-green-500' |
| }`} |
| style={{ width: `${age.percentage}%` }} |
| /> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| |
| {/* Risk Factors */} |
| <div className="bg-[#12121a] rounded-xl border border-red-500/20 p-4"> |
| <h4 className="text-white font-medium mb-3 flex items-center gap-2"> |
| <AlertTriangle size={18} className="text-red-400" /> |
| Critical Risk Factors |
| </h4> |
| <ul className="space-y-2"> |
| {data.risk_assessment.factors.map((factor: string, idx: number) => ( |
| <li key={idx} className="flex items-start gap-2 text-sm text-gray-300"> |
| <span className="text-red-400 mt-0.5">•</span> |
| {factor} |
| </li> |
| ))} |
| </ul> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| const renderSniperTracking = () => { |
| const snipers = sniperQuery.data?.snipers || [ |
| { |
| address: '0x1234...5678', |
| entry_block: 18472931, |
| exit_block: 18472945, |
| hold_time: '14 blocks (~3 min)', |
| profit: '+342%', |
| gas_paid: 0.42, |
| entry_price: 0.00000123, |
| exit_price: 0.00000544, |
| dump_percentage: 94, |
| is_sniper: true, |
| }, |
| { |
| address: '0xabcd...ef01', |
| entry_block: 18472932, |
| exit_block: null, |
| hold_time: 'Still holding', |
| profit: '+156%', |
| gas_paid: 0.38, |
| entry_price: 0.00000125, |
| exit_price: null, |
| dump_percentage: 0, |
| is_sniper: true, |
| }, |
| ]; |
|
|
| return ( |
| <div className="space-y-4"> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center gap-2"> |
| <Target className="text-purple-400" size={20} /> |
| <span className="text-white font-medium">{snipers.length} Sniper Wallets Detected</span> |
| </div> |
| <div className="flex gap-2"> |
| <span className="px-3 py-1 bg-red-500/20 text-red-400 rounded-full text-sm"> |
| ⚠️ High Dump Risk: 67% |
| </span> |
| </div> |
| </div> |
| |
| <div className="overflow-x-auto"> |
| <table className="w-full"> |
| <thead> |
| <tr className="border-b border-purple-500/20"> |
| <th className="text-left p-3 text-gray-400 font-medium text-sm">Wallet</th> |
| <th className="text-left p-3 text-gray-400 font-medium text-sm">Entry Block</th> |
| <th className="text-left p-3 text-gray-400 font-medium text-sm">Hold Time</th> |
| <th className="text-left p-3 text-gray-400 font-medium text-sm">Profit</th> |
| <th className="text-left p-3 text-gray-400 font-medium text-sm">Dump %</th> |
| <th className="text-left p-3 text-gray-400 font-medium text-sm">Risk</th> |
| </tr> |
| </thead> |
| <tbody> |
| {snipers.map((sniper: any, idx: number) => ( |
| <tr key={idx} className="border-b border-purple-500/10 hover:bg-white/5"> |
| <td className="p-3"> |
| <code className="text-sm font-mono text-neon-green">{sniper.address}</code> |
| </td> |
| <td className="p-3 text-sm text-gray-300">{sniper.entry_block}</td> |
| <td className="p-3 text-sm text-gray-300">{sniper.hold_time}</td> |
| <td className="p-3"> |
| <span className={`text-sm font-medium ${sniper.profit.startsWith('+') ? 'text-green-400' : 'text-red-400'}`}> |
| {sniper.profit} |
| </span> |
| </td> |
| <td className="p-3"> |
| <div className="flex items-center gap-2"> |
| <div className="w-16 h-2 bg-black/50 rounded-full overflow-hidden"> |
| <div |
| className={`h-full rounded-full ${sniper.dump_percentage > 50 ? 'bg-red-500' : 'bg-yellow-500'}`} |
| style={{ width: `${sniper.dump_percentage}%` }} |
| /> |
| </div> |
| <span className="text-sm text-gray-300">{sniper.dump_percentage}%</span> |
| </div> |
| </td> |
| <td className="p-3"> |
| {sniper.dump_percentage > 90 ? ( |
| <span className="px-2 py-1 bg-red-500/20 text-red-400 rounded text-xs">DUMPED</span> |
| ) : sniper.dump_percentage > 50 ? ( |
| <span className="px-2 py-1 bg-orange-500/20 text-orange-400 rounded text-xs">EXITING</span> |
| ) : ( |
| <span className="px-2 py-1 bg-yellow-500/20 text-yellow-400 rounded text-xs">HOLDING</span> |
| )} |
| </td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| |
| {/* Sniper Summary */} |
| <div className="grid grid-cols-3 gap-4"> |
| <div className="bg-[#12121a] rounded-xl p-4 border border-purple-500/20"> |
| <p className="text-sm text-gray-400">Avg Hold Time</p> |
| <p className="text-xl font-bold text-white">4.2 minutes</p> |
| <p className="text-xs text-red-400">⚠️ Very short - typical sniper behavior</p> |
| </div> |
| <div className="bg-[#12121a] rounded-xl p-4 border border-purple-500/20"> |
| <p className="text-sm text-gray-400">Total Sniper Profit</p> |
| <p className="text-xl font-bold text-green-400">+1,247 ETH</p> |
| <p className="text-xs text-gray-400">Extracted from regular buyers</p> |
| </div> |
| <div className="bg-[#12121a] rounded-xl p-4 border border-purple-500/20"> |
| <p className="text-sm text-gray-400">Dump Warning</p> |
| <p className="text-xl font-bold text-red-400">ACTIVE</p> |
| <p className="text-xs text-red-400">3 snipers still holding</p> |
| </div> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| const renderCopyTrading = () => { |
| const copies = copyQuery.data?.copies || [ |
| { |
| leader: '0x7890...1234', |
| followers: 23, |
| total_copy_volume: 156.7, |
| avg_delay_blocks: 3.2, |
| success_rate: 68, |
| pattern: 'Mirror exact trades with 2-4 block delay', |
| }, |
| ]; |
|
|
| return ( |
| <div className="space-y-4"> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center gap-2"> |
| <Share2 className="text-blue-400" size={20} /> |
| <span className="text-white font-medium">{copies.length} Copy Trading Pattern Detected</span> |
| </div> |
| </div> |
| |
| {copies.map((copy: any, idx: number) => ( |
| <div key={idx} className="bg-[#12121a] rounded-xl border border-blue-500/20 p-4"> |
| <div className="flex items-start justify-between mb-4"> |
| <div> |
| <p className="text-sm text-gray-400">Leader Wallet</p> |
| <code className="text-lg font-mono text-blue-400">{copy.leader}</code> |
| </div> |
| <div className="text-right"> |
| <p className="text-3xl font-bold text-white">{copy.followers}</p> |
| <p className="text-sm text-gray-400">followers</p> |
| </div> |
| </div> |
| |
| <div className="grid grid-cols-3 gap-4 mb-4"> |
| <div className="bg-black/30 rounded-lg p-3"> |
| <p className="text-xs text-gray-400">Copy Volume</p> |
| <p className="text-lg font-bold text-blue-400">{copy.total_copy_volume} ETH</p> |
| </div> |
| <div className="bg-black/30 rounded-lg p-3"> |
| <p className="text-xs text-gray-400">Avg Delay</p> |
| <p className="text-lg font-bold text-white">{copy.avg_delay_blocks} blocks</p> |
| </div> |
| <div className="bg-black/30 rounded-lg p-3"> |
| <p className="text-xs text-gray-400">Success Rate</p> |
| <p className="text-lg font-bold text-green-400">{copy.success_rate}%</p> |
| </div> |
| </div> |
| |
| <p className="text-sm text-gray-300 bg-black/30 rounded-lg p-3"> |
| <span className="text-gray-400">Pattern:</span> {copy.pattern} |
| </p> |
| </div> |
| ))} |
| </div> |
| ); |
| }; |
|
|
| const renderBotFarmDetection = () => { |
| return ( |
| <div className="space-y-6"> |
| <div className="bg-gradient-to-r from-purple-500/20 to-blue-500/20 rounded-xl border border-purple-500/30 p-6"> |
| <div className="flex items-center gap-4"> |
| <div className="w-16 h-16 rounded-full bg-purple-500/30 flex items-center justify-center"> |
| <Zap size={32} className="text-purple-400" /> |
| </div> |
| <div> |
| <h3 className="text-xl font-bold text-white">Bot Farm Detection</h3> |
| <p className="text-purple-300">Advanced automation pattern analysis</p> |
| </div> |
| </div> |
| </div> |
| |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div className="bg-[#12121a] rounded-xl border border-purple-500/20 p-4"> |
| <h4 className="text-white font-medium mb-3 flex items-center gap-2"> |
| <Activity size={16} className="text-purple-400" /> |
| Gas Pattern Analysis |
| </h4> |
| <div className="space-y-2"> |
| <div className="flex justify-between text-sm"> |
| <span className="text-gray-400">Pattern Consistency</span> |
| <span className="text-red-400">94% (High Bot Probability)</span> |
| </div> |
| <div className="flex justify-between text-sm"> |
| <span className="text-gray-400">Gas Price Variance</span> |
| <span className="text-orange-400">Low (0.2% deviation)</span> |
| </div> |
| <div className="flex justify-between text-sm"> |
| <span className="text-gray-400">Priority Fee Pattern</span> |
| <span className="text-red-400">Identical across 23 wallets</span> |
| </div> |
| </div> |
| </div> |
| |
| <div className="bg-[#12121a] rounded-xl border border-purple-500/20 p-4"> |
| <h4 className="text-white font-medium mb-3 flex items-center gap-2"> |
| <Clock size={16} className="text-purple-400" /> |
| Timing Analysis |
| </h4> |
| <div className="space-y-2"> |
| <div className="flex justify-between text-sm"> |
| <span className="text-gray-400">Inter-TX Time</span> |
| <span className="text-red-400">12.4s avg (Suspiciously regular)</span> |
| </div> |
| <div className="flex justify-between text-sm"> |
| <span className="text-gray-400">Block Position</span> |
| <span className="text-orange-400">0-3 (Flashbots/MEV)</span> |
| </div> |
| <div className="flex justify-between text-sm"> |
| <span className="text-gray-400">Time Clustering</span> |
| <span className="text-red-400">47 wallets, 60s window</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div className="bg-[#12121a] rounded-xl border border-red-500/20 p-4"> |
| <h4 className="text-white font-medium mb-3 flex items-center gap-2"> |
| <AlertTriangle size={18} className="text-red-400" /> |
| Bot Farm Detected: 847 Wallets |
| </h4> |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> |
| <div className="text-center p-3 bg-black/30 rounded-lg"> |
| <p className="text-2xl font-bold text-red-400">847</p> |
| <p className="text-xs text-gray-400">Total Wallets</p> |
| </div> |
| <div className="text-center p-3 bg-black/30 rounded-lg"> |
| <p className="text-2xl font-bold text-orange-400">98.2%</p> |
| <p className="text-xs text-gray-400">Bot Probability</p> |
| </div> |
| <div className="text-center p-3 bg-black/30 rounded-lg"> |
| <p className="text-2xl font-bold text-purple-400">$2.4M</p> |
| <p className="text-xs text-gray-400">Total Volume</p> |
| </div> |
| <div className="text-center p-3 bg-black/30 rounded-lg"> |
| <p className="text-2xl font-bold text-blue-400">OKX</p> |
| <p className="text-xs text-gray-400">Funding Source</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| return ( |
| <div className="space-y-6"> |
| {/* Header */} |
| <div className="flex items-start justify-between"> |
| <div> |
| <h1 className="text-2xl font-bold text-white flex items-center gap-2"> |
| <Network className="text-purple-400" /> |
| Advanced Analytics |
| </h1> |
| <p className="text-gray-400 mt-1"> |
| Muncher Maps, bundle detection, fresh wallet analysis & more |
| </p> |
| </div> |
| <div className="flex items-center gap-2"> |
| <span className="px-3 py-1 bg-purple-500/20 text-purple-300 rounded-lg text-sm"> |
| {tier} Tier |
| </span> |
| {!isPro && ( |
| <button className="px-3 py-1 bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-lg text-sm hover:opacity-90 transition-opacity"> |
| Upgrade to PRO |
| </button> |
| )} |
| </div> |
| </div> |
| |
| {/* Search */} |
| <div className="bg-[#12121a] rounded-xl border border-purple-500/20 p-4"> |
| <div className="flex flex-col md:flex-row gap-4"> |
| <div className="md:w-48"> |
| <label className="block text-sm text-gray-400 mb-2">Blockchain</label> |
| <select |
| value={chain} |
| onChange={(e) => setChain(e.target.value)} |
| className="w-full bg-[#0a0a0f] border border-purple-500/20 rounded-lg px-3 py-2.5 text-white focus:outline-none focus:border-purple-500/50" |
| > |
| {CHAINS.map((c) => ( |
| <option key={c.id} value={c.id}> |
| {c.icon} {c.name} |
| </option> |
| ))} |
| </select> |
| </div> |
| <div className="flex-1"> |
| <label className="block text-sm text-gray-400 mb-2"> |
| Wallet Address or Contract |
| {activeTab === 'network' && isElite && ( |
| <span className="ml-2 text-xs text-green-400">(6 hops enabled)</span> |
| )} |
| </label> |
| <div className="relative"> |
| <input |
| type="text" |
| value={address} |
| onChange={(e) => setAddress(e.target.value)} |
| placeholder="0x... or contract address" |
| className="w-full bg-[#0a0a0f] border border-purple-500/20 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-purple-500/50 font-mono" |
| /> |
| </div> |
| </div> |
| <div className="md:w-auto"> |
| <label className="block text-sm text-transparent mb-2">Action</label> |
| <button |
| onClick={handleAnalyze} |
| disabled={!address || address.length < 10} |
| className="w-full md:w-auto px-6 py-2.5 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-600/50 text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2" |
| > |
| <Search size={18} /> |
| Analyze |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| {/* Analysis Type Tabs */} |
| <div className="flex flex-wrap gap-2"> |
| {ANALYSIS_TYPES.map((type) => { |
| const Icon = type.icon; |
| const isLocked = |
| (type.id === 'network' && !isPro) || |
| (type.id === 'bundle' && tier === 'FREE') || |
| (type.id === 'fresh' && tier === 'FREE') || |
| (type.id === 'sniper' && !isPro) || |
| (type.id === 'copy' && !isPro) || |
| (type.id === 'bot' && !isPro); |
| |
| return ( |
| <button |
| key={type.id} |
| onClick={() => !isLocked && setActiveTab(type.id)} |
| disabled={isLocked} |
| className={`flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium transition-all ${ |
| activeTab === type.id |
| ? 'bg-purple-600 text-white' |
| : isLocked |
| ? 'bg-gray-800/50 text-gray-500 cursor-not-allowed' |
| : 'bg-[#12121a] text-gray-300 hover:bg-purple-600/20 hover:text-purple-300' |
| }`} |
| > |
| <Icon size={16} /> |
| {type.label} |
| {isLocked && <span className="text-xs ml-1">🔒</span>} |
| </button> |
| ); |
| })} |
| </div> |
| |
| {/* Filters (for network graph) */} |
| {activeTab === 'network' && isPro && ( |
| <div className="flex flex-wrap items-center gap-4 bg-[#12121a] rounded-lg p-3 border border-purple-500/20"> |
| <span className="text-sm text-gray-400 flex items-center gap-1"> |
| <Filter size={14} /> |
| Filters: |
| </span> |
| <select |
| value={riskFilter} |
| onChange={(e) => setRiskFilter(e.target.value as any)} |
| className="bg-[#0a0a0f] border border-purple-500/20 rounded px-2 py-1 text-sm text-white" |
| > |
| <option value="all">All Risk Levels</option> |
| <option value="high">High Risk (60+)</option> |
| <option value="critical">Critical (80+)</option> |
| </select> |
| <label className="flex items-center gap-2 text-sm text-gray-300"> |
| <input |
| type="checkbox" |
| checked={showLabels} |
| onChange={(e) => setShowLabels(e.target.checked)} |
| className="rounded border-purple-500/20" |
| /> |
| Show Labels |
| </label> |
| {isElite && ( |
| <select |
| value={hops} |
| onChange={(e) => setHops(Number(e.target.value))} |
| className="bg-[#0a0a0f] border border-purple-500/20 rounded px-2 py-1 text-sm text-white" |
| > |
| <option value={2}>2 Hops</option> |
| <option value={3}>3 Hops (PRO)</option> |
| <option value={4}>4 Hops (ELITE)</option> |
| <option value={6}>6 Hops (ELITE+)</option> |
| </select> |
| )} |
| </div> |
| )} |
| |
| {/* Content */} |
| <div className="min-h-[400px]"> |
| {activeTab === 'network' && ( |
| isPro ? renderNetworkGraph() : ( |
| <div className="flex flex-col items-center justify-center h-[400px] bg-[#12121a] rounded-xl border border-purple-500/20"> |
| <Network size={48} className="text-gray-600 mb-4" /> |
| <p className="text-gray-400 mb-2">Muncher Maps requires PRO tier</p> |
| <p className="text-sm text-gray-500 mb-4">Visualize wallet relationships and track funds</p> |
| <button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"> |
| Upgrade to PRO |
| </button> |
| </div> |
| ) |
| )} |
| {activeTab === 'bundle' && ( |
| tier === 'FREE' ? ( |
| <div className="flex flex-col items-center justify-center h-[400px] bg-[#12121a] rounded-xl border border-purple-500/20"> |
| <Users size={48} className="text-gray-600 mb-4" /> |
| <p className="text-gray-400 mb-2">Bundle Detection requires BASIC+ tier</p> |
| <p className="text-sm text-gray-500 mb-4">Detect coordinated wallet groups and sybil attacks</p> |
| <button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"> |
| Upgrade Now |
| </button> |
| </div> |
| ) : renderBundleAnalysis() |
| )} |
| {activeTab === 'fresh' && ( |
| tier === 'FREE' ? ( |
| <div className="flex flex-col items-center justify-center h-[400px] bg-[#12121a] rounded-xl border border-purple-500/20"> |
| <Clock size={48} className="text-gray-600 mb-4" /> |
| <p className="text-gray-400 mb-2">Fresh Wallet Analysis requires BASIC+ tier</p> |
| <p className="text-sm text-gray-500 mb-4">Predict rugs by analyzing new wallet patterns</p> |
| <button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"> |
| Upgrade Now |
| </button> |
| </div> |
| ) : renderFreshWalletAnalysis() |
| )} |
| {activeTab === 'sniper' && ( |
| !isPro ? ( |
| <div className="flex flex-col items-center justify-center h-[400px] bg-[#12121a] rounded-xl border border-purple-500/20"> |
| <Target size={48} className="text-gray-600 mb-4" /> |
| <p className="text-gray-400 mb-2">Sniper Tracking requires PRO tier</p> |
| <p className="text-sm text-gray-500 mb-4">Track first-block buyers and instant dumpers</p> |
| <button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"> |
| Upgrade to PRO |
| </button> |
| </div> |
| ) : renderSniperTracking() |
| )} |
| {activeTab === 'copy' && ( |
| !isPro ? ( |
| <div className="flex flex-col items-center justify-center h-[400px] bg-[#12121a] rounded-xl border border-purple-500/20"> |
| <Share2 size={48} className="text-gray-600 mb-4" /> |
| <p className="text-gray-400 mb-2">Copy Trading Detection requires PRO tier</p> |
| <p className="text-sm text-gray-500 mb-4">Find wallets copying successful traders</p> |
| <button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"> |
| Upgrade to PRO |
| </button> |
| </div> |
| ) : renderCopyTrading() |
| )} |
| {activeTab === 'bot' && ( |
| !isPro ? ( |
| <div className="flex flex-col items-center justify-center h-[400px] bg-[#12121a] rounded-xl border border-purple-500/20"> |
| <Zap size={48} className="text-gray-600 mb-4" /> |
| <p className="text-gray-400 mb-2">Bot Farm Detection requires PRO tier</p> |
| <p className="text-sm text-gray-500 mb-4">Identify automation patterns and MEV bots</p> |
| <button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"> |
| Upgrade to PRO |
| </button> |
| </div> |
| ) : renderBotFarmDetection() |
| )} |
| </div> |
| </div> |
| ); |
| } |
|
|