import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import Layout from '@/components/Layout'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Search, Blocks, ArrowRightLeft, Users, Zap, CheckCircle, XCircle, RefreshCw, Loader2, ExternalLink, Copy, Activity, Database, Timer, Hash, Wallet, ChevronRight, AlertCircle, } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; interface BlockInfo { number: number; hash: string; parentHash: string; stateRoot: string; extrinsicsRoot: string; extrinsicsCount: number; timestamp: number; author?: string; } interface ExtrinsicInfo { hash: string; blockNumber: number; index: number; section: string; method: string; signer?: string; success: boolean; timestamp: number; args?: string; } interface NetworkStats { bestBlock: number; finalizedBlock: number; totalExtrinsics: number; activeValidators: number; avgBlockTime: number; tps: number; totalAccounts: number; era: number; } type ExplorerView = 'overview' | 'accounts' | 'assets' | 'account' | 'block' | 'tx'; const Explorer: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); const { api, isApiReady } = usePezkuwi(); // Parse URL to determine view const getViewFromPath = (): { view: ExplorerView; param?: string } => { const path = location.pathname.replace('/explorer', '').replace(/^\//, ''); if (!path) return { view: 'overview' }; const parts = path.split('/'); const viewType = parts[0]; const param = parts[1]; switch (viewType) { case 'accounts': return param ? { view: 'account', param } : { view: 'accounts' }; case 'account': return { view: 'account', param }; case 'assets': return { view: 'assets' }; case 'block': return { view: 'block', param }; case 'tx': return { view: 'tx', param }; default: return { view: 'overview' }; } }; const { view: currentView, param: viewParam } = getViewFromPath(); const [stats, setStats] = useState({ bestBlock: 0, finalizedBlock: 0, totalExtrinsics: 0, activeValidators: 0, avgBlockTime: 6, tps: 0, totalAccounts: 0, era: 0, }); const [recentBlocks, setRecentBlocks] = useState([]); const [recentExtrinsics, setRecentExtrinsics] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [searchError, setSearchError] = useState(''); const [isSearching, setIsSearching] = useState(false); const [lastUpdate, setLastUpdate] = useState(new Date()); // Format address for display const formatAddress = (address: string) => { if (!address) return ''; return `${address.slice(0, 6)}...${address.slice(-6)}`; }; // Copy to clipboard const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); }; // Fetch network stats const fetchStats = useCallback(async () => { if (!api || !isApiReady) return; try { const [header, finalizedHash, validators, currentEra] = await Promise.all([ api.rpc.chain.getHeader(), api.rpc.chain.getFinalizedHead(), api.query.session?.validators?.() || Promise.resolve([]), api.query.staking?.currentEra?.() || Promise.resolve(null), ]); const finalizedHeader = await api.rpc.chain.getHeader(finalizedHash); setStats(prev => ({ ...prev, bestBlock: header.number.toNumber(), finalizedBlock: finalizedHeader.number.toNumber(), activeValidators: Array.isArray(validators) ? validators.length : 0, era: currentEra ? currentEra.unwrapOr(0).toNumber() : 0, })); } catch (error) { console.error('Error fetching stats:', error); } }, [api, isApiReady]); // Fetch recent blocks const fetchRecentBlocks = useCallback(async () => { if (!api || !isApiReady) return; try { const header = await api.rpc.chain.getHeader(); const currentBlock = header.number.toNumber(); const blocks: BlockInfo[] = []; // Fetch last 10 blocks for (let i = 0; i < 10 && currentBlock - i > 0; i++) { const blockNumber = currentBlock - i; const blockHash = await api.rpc.chain.getBlockHash(blockNumber); const signedBlock = await api.rpc.chain.getBlock(blockHash); // Try to get timestamp from block let timestamp = Date.now() - i * 6000; // Fallback: estimate based on 6s blocks const timestampExtrinsic = signedBlock.block.extrinsics.find( ext => ext.method.section === 'timestamp' && ext.method.method === 'set' ); if (timestampExtrinsic) { timestamp = Number(timestampExtrinsic.method.args[0].toString()); } blocks.push({ number: blockNumber, hash: blockHash.toString(), parentHash: signedBlock.block.header.parentHash.toString(), stateRoot: signedBlock.block.header.stateRoot.toString(), extrinsicsRoot: signedBlock.block.header.extrinsicsRoot.toString(), extrinsicsCount: signedBlock.block.extrinsics.length, timestamp, }); } setRecentBlocks(blocks); // Calculate TPS from recent blocks if (blocks.length >= 2) { const timeDiff = (blocks[0].timestamp - blocks[blocks.length - 1].timestamp) / 1000; const totalExts = blocks.reduce((sum, b) => sum + b.extrinsicsCount, 0); const tps = timeDiff > 0 ? totalExts / timeDiff : 0; setStats(prev => ({ ...prev, tps: Math.round(tps * 100) / 100 })); } } catch (error) { console.error('Error fetching blocks:', error); } }, [api, isApiReady]); // Fetch recent extrinsics const fetchRecentExtrinsics = useCallback(async () => { if (!api || !isApiReady) return; try { const header = await api.rpc.chain.getHeader(); const currentBlock = header.number.toNumber(); const extrinsics: ExtrinsicInfo[] = []; // Fetch extrinsics from last 5 blocks for (let i = 0; i < 5 && currentBlock - i > 0 && extrinsics.length < 15; i++) { const blockNumber = currentBlock - i; const blockHash = await api.rpc.chain.getBlockHash(blockNumber); const signedBlock = await api.rpc.chain.getBlock(blockHash); const apiAt = await api.at(blockHash); const allRecords = await apiAt.query.system.events(); // Get timestamp let timestamp = Date.now() - i * 6000; const timestampExtrinsic = signedBlock.block.extrinsics.find( ext => ext.method.section === 'timestamp' && ext.method.method === 'set' ); if (timestampExtrinsic) { timestamp = Number(timestampExtrinsic.method.args[0].toString()); } signedBlock.block.extrinsics.forEach((ext, index) => { // Skip timestamp and inherent extrinsics for cleaner display if (ext.method.section === 'timestamp' || ext.method.section === 'parachainSystem') { return; } // Check if extrinsic succeeded const events = (allRecords as unknown as Array<{ phase: { isApplyExtrinsic: boolean; asApplyExtrinsic: { eq: (n: number) => boolean } }; event: { section: string; method: string } }>) .filter(({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index) ); const success = events.some(({ event }) => event.section === 'system' && event.method === 'ExtrinsicSuccess' ); const failed = events.some(({ event }) => event.section === 'system' && event.method === 'ExtrinsicFailed' ); extrinsics.push({ hash: ext.hash.toString(), blockNumber, index, section: ext.method.section, method: ext.method.method, signer: ext.isSigned ? ext.signer.toString() : undefined, success: success || !failed, timestamp, }); }); } setRecentExtrinsics(extrinsics.slice(0, 15)); setStats(prev => ({ ...prev, totalExtrinsics: extrinsics.length })); } catch (error) { console.error('Error fetching extrinsics:', error); } }, [api, isApiReady]); // Search handler const handleSearch = async () => { if (!searchQuery.trim() || !api || !isApiReady) return; setIsSearching(true); setSearchError(''); try { const query = searchQuery.trim(); // Check if it's a block number if (/^\d+$/.test(query)) { const blockNumber = parseInt(query); const header = await api.rpc.chain.getHeader(); if (blockNumber <= header.number.toNumber()) { navigate(`/explorer/block/${blockNumber}`); return; } else { setSearchError('Block number does not exist yet'); } } // Check if it's a hash (block or extrinsic) else if (/^0x[a-fA-F0-9]{64}$/.test(query)) { // Try as block hash first try { const block = await api.rpc.chain.getBlock(query); if (block) { navigate(`/explorer/block/${block.block.header.number.toNumber()}`); return; } } catch { // Not a block hash, might be extrinsic hash navigate(`/explorer/tx/${query}`); return; } } // Check if it's an address else if (query.length >= 47 && query.length <= 48) { navigate(`/explorer/account/${query}`); return; } else { setSearchError('Invalid search query. Enter a block number, hash, or address.'); } } catch (error) { console.error('Search error:', error); setSearchError('Search failed. Please try again.'); } finally { setIsSearching(false); } }; // Subscribe to new blocks useEffect(() => { if (!api || !isApiReady) return; let unsubscribe: (() => void) | undefined; const subscribe = async () => { unsubscribe = await api.rpc.chain.subscribeNewHeads(() => { fetchStats(); fetchRecentBlocks(); fetchRecentExtrinsics(); setLastUpdate(new Date()); }); }; subscribe(); return () => { if (unsubscribe) unsubscribe(); }; }, [api, isApiReady, fetchStats, fetchRecentBlocks, fetchRecentExtrinsics]); // Initial load useEffect(() => { const loadData = async () => { setIsLoading(true); await Promise.all([ fetchStats(), fetchRecentBlocks(), fetchRecentExtrinsics(), ]); setIsLoading(false); }; if (isApiReady) { loadData(); } }, [isApiReady, fetchStats, fetchRecentBlocks, fetchRecentExtrinsics]); // Refresh handler const handleRefresh = () => { fetchStats(); fetchRecentBlocks(); fetchRecentExtrinsics(); setLastUpdate(new Date()); }; if (!isApiReady) { return (

Connecting to Blockchain

Please wait while we establish connection...

); } return (
{/* Header */}

Block Explorer

Live Last updated {formatDistanceToNow(lastUpdate, { addSuffix: true })}
{/* View Navigation */}
{/* Search Bar */}
{ setSearchQuery(e.target.value); setSearchError(''); }} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} className="pl-10 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500" />
{searchError && (
{searchError}
)}
{/* View-specific content */} {currentView === 'accounts' && ( Accounts

Search for an account address to view details, balances, and transaction history.

Use the search bar above to find an account by address

)} {currentView === 'assets' && ( Assets
HEZ Native

Native token of PezkuwiChain

PEZ Asset ID: 1

Pezkuwi governance token

wHEZ Asset ID: 0

Wrapped HEZ token

wUSDT Asset ID: 2

Wrapped USDT stablecoin (6 decimals)

)} {currentView === 'account' && viewParam && ( Account Details
Address
{viewParam}

Account balance and transaction history loading...

)} {/* Stats Grid - Only show for overview */} {currentView === 'overview' && ( <> {isLoading ? (
{[...Array(8)].map((_, i) => (
))}
) : (
Best Block

#{stats.bestBlock.toLocaleString()}

Finalized

#{stats.finalizedBlock.toLocaleString()}

Validators

{stats.activeValidators}

Era

{stats.era}

Block Time

~{stats.avgBlockTime}s

TPS

{stats.tps.toFixed(2)}

Recent Extrinsics

{recentExtrinsics.length} in last {recentBlocks.length} blocks

)} {/* Blocks and Extrinsics Grid */}
{/* Recent Blocks */} Recent Blocks {isLoading ? ( [...Array(5)].map((_, i) => (
)) ) : recentBlocks.length === 0 ? (
No blocks found
) : ( recentBlocks.slice(0, 8).map((block) => (
navigate(`/explorer/block/${block.number}`)} >
#{block.number.toLocaleString()} {formatDistanceToNow(new Date(block.timestamp), { addSuffix: true })}
{formatAddress(block.hash)}
Extrinsics:{' '} {block.extrinsicsCount}
)) )}
{/* Recent Extrinsics */} Recent Extrinsics {isLoading ? ( [...Array(5)].map((_, i) => (
)) ) : recentExtrinsics.length === 0 ? (
No extrinsics found
) : ( recentExtrinsics.slice(0, 8).map((ext, idx) => (
navigate(`/explorer/tx/${ext.hash}`)} >
{ext.success ? ( ) : ( )} {ext.section}.{ext.method}
Block #{ext.blockNumber.toLocaleString()}
{formatAddress(ext.hash)}
{ext.signer && (
{formatAddress(ext.signer)}
)}
)) )}
{/* Quick Links */}

Quick Links

)}
); }; export default Explorer;