Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,19 @@ textarea {
.lesson-viewer .token.url { color: var(--prism-operator); }

.lesson-viewer .token.punctuation { color: var(--prism-punctuation); }

/* Thin themed scrollbar shared by simulator-style panels */
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #333;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #ef4444;
}
169 changes: 169 additions & 0 deletions frontend/src/app/mempool-auction/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
'use client';

import { BlockHistory } from '@/components/mempool-auction/BlockHistory';
import { MempoolGrid } from '@/components/mempool-auction/MempoolGrid';
import { useMempoolSimulator } from '@/hooks/useMempoolSimulator';
import { MAX_FEE_BID, MIN_FEE_BID, selectForBlock, totalGas } from '@/lib/mempool';
import { useMemo } from 'react';

const GAS_LIMIT_MIN = 500_000;
const GAS_LIMIT_MAX = 5_000_000;

export default function MempoolAuctionPage() {
const {
pool,
blocks,
settings,
autoFlow,
addTransaction,
removeTransaction,
setFeeBid,
updateSettings,
mineBlock,
reset,
setAutoFlow,
} = useMempoolSimulator();

// Preview which transactions the next block would settle, so the grid can
// highlight winning bids live as parameters change.
const nextBlock = useMemo(
() => selectForBlock(pool, settings.gasLimit, settings.baseFee),
[pool, settings.gasLimit, settings.baseFee],
);
const nextBlockIds = useMemo(() => new Set(nextBlock.map((tx) => tx.id)), [nextBlock]);
const projectedFill = Math.round((totalGas(nextBlock) / settings.gasLimit) * 100);

return (
<div className="relative min-h-[calc(100vh-80px)] overflow-y-auto bg-black p-6 font-mono text-white md:p-12">
{/* Background grid accent */}
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px]" />

<div className="relative z-10 mx-auto flex max-w-7xl flex-col">
{/* Header */}
<div className="mb-10 flex flex-col items-start justify-between gap-6 md:flex-row md:items-end">
<div className="border-l-4 border-red-600 pl-6">
<h1 className="mb-2 text-4xl font-black tracking-tighter uppercase">
Gas Fee <span className="text-red-500">Auction</span>
</h1>
<p className="text-xs tracking-[0.3em] text-gray-500 uppercase">
Mempool Simulator — Highest Bidder Settles First
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<button
onClick={() => addTransaction()}
className="rounded border border-white/10 bg-zinc-900 px-4 py-2.5 text-[10px] font-black tracking-widest text-white uppercase transition-colors hover:bg-zinc-800"
>
+ Broadcast Tx
</button>
<button
onClick={() => setAutoFlow(!autoFlow)}
aria-pressed={autoFlow}
className="flex items-center gap-2 rounded border border-white/10 bg-zinc-900 px-4 py-2.5 text-[10px] font-black tracking-widest text-white uppercase transition-colors hover:bg-zinc-800"
>
<span
className={`h-2 w-2 rounded-full ${autoFlow ? 'animate-pulse bg-green-500' : 'bg-gray-600'}`}
aria-hidden="true"
/>
{autoFlow ? 'Auto Flow On' : 'Auto Flow'}
</button>
<button
onClick={mineBlock}
disabled={nextBlock.length === 0}
className="bg-white px-4 py-2.5 text-[10px] font-black tracking-widest text-black uppercase transition-colors hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-40"
>
⛏ Mine Block
</button>
<button
onClick={reset}
className="rounded border border-red-600/30 bg-red-600/10 px-4 py-2.5 text-[10px] font-black tracking-widest text-red-500 uppercase transition-colors hover:bg-red-600/20"
>
Reset
</button>
</div>
</div>

{/* Network parameters */}
<div className="mb-8 grid grid-cols-1 gap-4 rounded-2xl border border-white/10 bg-zinc-950 p-6 md:grid-cols-3">
<div>
<div className="mb-2 flex items-center justify-between text-[10px] tracking-widest text-gray-500 uppercase">
<span>Base Fee</span>
<span className="font-black text-white tabular-nums">{settings.baseFee} gwei</span>
</div>
<input
type="range"
min={MIN_FEE_BID}
max={MAX_FEE_BID}
value={settings.baseFee}
onChange={(e) => updateSettings({ baseFee: Number(e.target.value) })}
className="w-full accent-red-600"
aria-label="Network base fee in gwei"
/>
<p className="mt-1 text-[9px] text-gray-600">Bids below the base fee are excluded.</p>
</div>

<div>
<div className="mb-2 flex items-center justify-between text-[10px] tracking-widest text-gray-500 uppercase">
<span>Block Gas Limit</span>
<span className="font-black text-white tabular-nums">
{settings.gasLimit.toLocaleString()}
</span>
</div>
<input
type="range"
min={GAS_LIMIT_MIN}
max={GAS_LIMIT_MAX}
step={100_000}
value={settings.gasLimit}
onChange={(e) => updateSettings({ gasLimit: Number(e.target.value) })}
className="w-full accent-red-600"
aria-label="Block gas limit"
/>
<p className="mt-1 text-[9px] text-gray-600">Caps how many bids fit per block.</p>
</div>

<div className="flex flex-col justify-center rounded-lg border border-white/5 bg-black p-4">
<div className="mb-2 flex items-center justify-between text-[10px] tracking-widest text-gray-500 uppercase">
<span>Next Block</span>
<span className="font-black text-green-400 tabular-nums">
{nextBlock.length} tx · {projectedFill}%
</span>
</div>
<div
className="h-2 w-full overflow-hidden rounded-full bg-white/5"
role="progressbar"
aria-valuenow={projectedFill}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Projected next block gas fill"
>
<div
className="h-full rounded-full bg-green-500 transition-all"
style={{ width: `${Math.min(100, projectedFill)}%` }}
/>
</div>
<p className="mt-2 text-[9px] text-gray-600">
Highest bidders that clear the base fee and fit the gas limit.
</p>
</div>
</div>

{/* Auction view */}
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
<div className="lg:col-span-2">
<MempoolGrid
pool={pool}
baseFee={settings.baseFee}
nextBlockIds={nextBlockIds}
onFeeBid={setFeeBid}
onRemove={removeTransaction}
/>
</div>
<div className="lg:col-span-1">
<BlockHistory blocks={blocks} />
</div>
</div>
</div>
</div>
);
}
78 changes: 78 additions & 0 deletions frontend/src/components/mempool-auction/BlockHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';

import { MinedBlock } from '@/lib/mempool';

interface BlockHistoryProps {
blocks: MinedBlock[];
}

export function BlockHistory({ blocks }: BlockHistoryProps) {
return (
<section
className="flex flex-col overflow-hidden rounded-2xl border border-white/10 bg-zinc-950 p-6 shadow-2xl"
aria-label="Mined blocks"
>
<h2 className="mb-6 flex items-center justify-between border-b border-white/10 pb-4 text-sm font-bold tracking-widest uppercase">
Mined Blocks
<span className="text-[10px] font-normal text-gray-600">History [{blocks.length}]</span>
</h2>

<div className="custom-scrollbar flex-grow space-y-4 overflow-y-auto pr-1" role="feed">
{blocks.length === 0 ? (
<p className="py-10 text-center text-xs text-gray-700 italic">
No blocks yet — mine one to settle the top bids.
</p>
) : (
blocks.map((block) => {
const fill = Math.round((block.gasUsed / block.gasLimit) * 100);
return (
<article
key={block.height}
className="rounded-r border-t border-r border-b border-l-2 border-green-600 border-white/5 bg-black p-4"
aria-label={`Block ${block.height}, ${block.transactions.length} transactions, ${fill}% full`}
>
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-black text-green-500">#{block.height}</span>
<span className="text-[10px] text-gray-500">
{new Date(block.minedAt).toLocaleTimeString()}
</span>
</div>

<div className="mb-3 grid grid-cols-3 gap-2 text-[10px]">
<div>
<p className="tracking-widest text-gray-600 uppercase">Txs</p>
<p className="font-bold text-white tabular-nums">{block.transactions.length}</p>
</div>
<div>
<p className="tracking-widest text-gray-600 uppercase">Base</p>
<p className="font-bold text-white tabular-nums">{block.baseFee} gwei</p>
</div>
<div>
<p className="tracking-widest text-gray-600 uppercase">Reward</p>
<p className="font-bold text-green-400 tabular-nums">
{(block.totalFees / 1e9).toFixed(4)} Ξ
</p>
</div>
</div>

<div
className="h-2 w-full overflow-hidden rounded-full bg-white/5"
role="progressbar"
aria-valuenow={fill}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Block gas fill"
>
<div className="h-full rounded-full bg-green-500" style={{ width: `${fill}%` }} />
</div>
<p className="mt-1 text-right text-[9px] text-gray-600 tabular-nums">
{block.gasUsed.toLocaleString()} / {block.gasLimit.toLocaleString()} gas ({fill}%)
</p>
</article>
);
})
)}
</div>
</section>
);
}
Loading
Loading