A complete blockchain implementation featuring: - synord: Full node with GHOSTDAG consensus - explorer-web: Modern React blockchain explorer with 3D DAG visualization - CLI wallet and tools - Smart contract SDK and example contracts (DEX, NFT, token) - WASM crypto library for browser/mobile
224 lines
8.1 KiB
TypeScript
224 lines
8.1 KiB
TypeScript
import { useParams, Link } from 'react-router-dom';
|
|
import { Box, Clock, ArrowUpRight, Layers, Activity, Zap } from 'lucide-react';
|
|
import { useBlock } from '../hooks/useApi';
|
|
import TransactionList from '../components/TransactionList';
|
|
import CopyButton from '../components/CopyButton';
|
|
import BlockRelationshipDiagram from '../components/BlockRelationshipDiagram';
|
|
import { formatDateTime, truncateHash } from '../lib/utils';
|
|
|
|
export default function Block() {
|
|
const { hash } = useParams<{ hash: string }>();
|
|
const { data: block, isLoading, error } = useBlock(hash || '');
|
|
|
|
if (!hash) {
|
|
return <div className="card p-6 text-red-400">Block hash is required</div>;
|
|
}
|
|
|
|
if (isLoading) {
|
|
return <BlockSkeleton />;
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="card p-6 text-red-400">
|
|
Error loading block: {error.message}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!block) {
|
|
return <div className="card p-6 text-gray-400">Block not found</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Modern Header */}
|
|
<div className="relative">
|
|
{/* Background glow */}
|
|
<div className="absolute -top-10 left-0 w-[300px] h-[150px] bg-synor-500/20 rounded-full blur-[80px] pointer-events-none" />
|
|
|
|
<div className="relative flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-3 rounded-xl bg-gradient-to-br from-synor-500/20 to-violet-500/20 border border-synor-500/30">
|
|
<Box size={28} className="text-synor-400" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl md:text-3xl font-bold bg-gradient-to-r from-white to-gray-300 bg-clip-text text-transparent">
|
|
Block Details
|
|
</h1>
|
|
<div className="flex items-center gap-3 mt-1">
|
|
<span className="text-sm text-gray-400 flex items-center gap-1.5">
|
|
<Activity size={14} className="text-synor-400" />
|
|
Blue Score: {block.blueScore.toLocaleString()}
|
|
</span>
|
|
{block.isChainBlock && (
|
|
<span className="px-2 py-0.5 text-xs font-medium bg-synor-500/20 text-synor-400 rounded-full border border-synor-500/30">
|
|
Chain Block
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick stats */}
|
|
<div className="flex items-center gap-3">
|
|
<div className="px-4 py-2 rounded-xl bg-gray-800/50 border border-gray-700/50">
|
|
<div className="text-xs text-gray-500">Transactions</div>
|
|
<div className="text-lg font-bold text-white flex items-center gap-1.5">
|
|
<Zap size={14} className="text-amber-400" />
|
|
{block.transactionCount}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Block Relationship Diagram */}
|
|
<BlockRelationshipDiagram
|
|
currentHash={block.hash}
|
|
parentHashes={block.parentHashes}
|
|
childrenHashes={block.childrenHashes}
|
|
isChainBlock={block.isChainBlock}
|
|
mergeSetBlues={block.mergeSetBlues}
|
|
mergeSetReds={block.mergeSetReds}
|
|
/>
|
|
|
|
{/* Block Info Card */}
|
|
<div className="card">
|
|
<div className="card-header">
|
|
<h2 className="font-semibold">Block Information</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-800">
|
|
<InfoRow label="Hash">
|
|
<div className="flex items-center gap-2">
|
|
<span className="hash text-synor-400">{block.hash}</span>
|
|
<CopyButton text={block.hash} />
|
|
</div>
|
|
</InfoRow>
|
|
<InfoRow label="Timestamp">
|
|
<div className="flex items-center gap-2">
|
|
<Clock size={16} className="text-gray-500" />
|
|
{formatDateTime(block.timestamp)}
|
|
</div>
|
|
</InfoRow>
|
|
<InfoRow label="Blue Score">
|
|
{block.blueScore.toLocaleString()}
|
|
</InfoRow>
|
|
<InfoRow label="DAA Score">
|
|
{block.daaScore.toLocaleString()}
|
|
</InfoRow>
|
|
<InfoRow label="Difficulty">
|
|
{block.difficulty.toLocaleString()}
|
|
</InfoRow>
|
|
<InfoRow label="Transactions">
|
|
{block.transactionCount}
|
|
</InfoRow>
|
|
<InfoRow label="Version">{block.version}</InfoRow>
|
|
<InfoRow label="Nonce">{block.nonce.toLocaleString()}</InfoRow>
|
|
<InfoRow label="Blue Work">
|
|
<span className="hash text-xs">{block.blueWork}</span>
|
|
</InfoRow>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Parent Blocks */}
|
|
{block.parentHashes.length > 0 && (
|
|
<div className="card">
|
|
<div className="card-header flex items-center gap-2">
|
|
<Layers size={18} className="text-synor-400" />
|
|
<h2 className="font-semibold">
|
|
Parent Blocks ({block.parentHashes.length})
|
|
</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-800">
|
|
{block.parentHashes.map((parentHash, i) => (
|
|
<div key={parentHash} className="px-4 py-3 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{i === 0 && (
|
|
<span className="badge badge-info">Selected</span>
|
|
)}
|
|
<Link
|
|
to={`/block/${parentHash}`}
|
|
className="hash text-synor-400 hover:text-synor-300"
|
|
>
|
|
{truncateHash(parentHash, 16, 16)}
|
|
</Link>
|
|
</div>
|
|
<ArrowUpRight size={16} className="text-gray-500" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Child Blocks */}
|
|
{block.childrenHashes.length > 0 && (
|
|
<div className="card">
|
|
<div className="card-header flex items-center gap-2">
|
|
<Layers size={18} className="text-synor-400" />
|
|
<h2 className="font-semibold">
|
|
Child Blocks ({block.childrenHashes.length})
|
|
</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-800">
|
|
{block.childrenHashes.map((childHash) => (
|
|
<div key={childHash} className="px-4 py-3 flex items-center justify-between">
|
|
<Link
|
|
to={`/block/${childHash}`}
|
|
className="hash text-synor-400 hover:text-synor-300"
|
|
>
|
|
{truncateHash(childHash, 16, 16)}
|
|
</Link>
|
|
<ArrowUpRight size={16} className="text-gray-500" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Transactions */}
|
|
{block.transactions && block.transactions.length > 0 && (
|
|
<TransactionList
|
|
transactions={block.transactions}
|
|
title={`Block Transactions (${block.transactions.length})`}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InfoRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
return (
|
|
<div className="px-4 py-3 flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4">
|
|
<span className="text-sm text-gray-400 sm:w-32 flex-shrink-0">{label}</span>
|
|
<span className="text-gray-100 break-all">{children}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BlockSkeleton() {
|
|
return (
|
|
<div className="space-y-6 animate-pulse">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-12 h-12 rounded-lg bg-gray-800" />
|
|
<div>
|
|
<div className="h-7 w-40 bg-gray-800 rounded mb-2" />
|
|
<div className="h-4 w-32 bg-gray-800 rounded" />
|
|
</div>
|
|
</div>
|
|
<div className="card">
|
|
<div className="card-header">
|
|
<div className="h-5 w-36 bg-gray-800 rounded" />
|
|
</div>
|
|
<div className="divide-y divide-gray-800">
|
|
{Array.from({ length: 8 }).map((_, i) => (
|
|
<div key={i} className="px-4 py-3 flex items-center gap-4">
|
|
<div className="h-4 w-24 bg-gray-800 rounded" />
|
|
<div className="h-4 w-48 bg-gray-800 rounded" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|