Skip to content
Merged
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
118 changes: 88 additions & 30 deletions src/app/api/block/[hash]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,61 @@ async function fetchBlockFromRpc(
}
}

function isBlockNumber(identifier: string): boolean {
return /^\d+$/.test(identifier);
}

async function fetchBlockFromRpcByNumber(
blockNumber: string,
): Promise<Block<bigint, true> | null> {
try {
const block = await client.getBlock({
blockNumber: BigInt(blockNumber),
includeTransactions: true,
});
return block;
} catch (error) {
console.error("Failed to fetch block from RPC by number:", error);
return null;
}
}

async function buildAndCacheBlockData(
rpcBlock: Block<bigint, true>,
hash: Hash,
number: bigint,
): Promise<BlockData> {
const transactions: BlockTransaction[] = await Promise.all(
rpcBlock.transactions.map(async (tx, index) => {
const { bundleId, executionTimeUs } =
await enrichTransactionWithBundleData(tx.hash);
return {
hash: tx.hash,
from: tx.from,
to: tx.to,
gasUsed: tx.gas,
executionTimeUs,
bundleId,
index,
};
}),
);

const blockData: BlockData = {
hash,
number,
timestamp: rpcBlock.timestamp,
transactions,
gasUsed: rpcBlock.gasUsed,
gasLimit: rpcBlock.gasLimit,
cachedAt: Date.now(),
};

await cacheBlockData(blockData);

return blockData;
}

// On OP Stack, the first transaction (index 0) is the L1 attributes deposit transaction.
// This is not a perfect check (ideally we'd check tx.type === 'deposit' or type 0x7e),
// but sufficient for filtering out system transactions that don't need simulation data.
Expand Down Expand Up @@ -133,9 +188,35 @@ export async function GET(
{ params }: { params: Promise<{ hash: string }> },
) {
try {
const { hash } = await params;
const { hash: identifier } = await params;

const cachedBlock = await getBlockFromCache(hash);
// If the identifier is a block number, resolve it to a hash first
if (isBlockNumber(identifier)) {
const rpcBlock = await fetchBlockFromRpcByNumber(identifier);
if (!rpcBlock || !rpcBlock.hash || !rpcBlock.number) {
return NextResponse.json({ error: "Block not found" }, { status: 404 });
}

// Check cache by resolved hash
const cachedBlock = await getBlockFromCache(rpcBlock.hash);
if (cachedBlock) {
const { updatedBlock, hasUpdates } =
await refetchMissingTransactionSimulations(cachedBlock);
if (hasUpdates) {
await cacheBlockData(updatedBlock);
}
return NextResponse.json(serializeBlockData(updatedBlock));
}

const blockData = await buildAndCacheBlockData(
rpcBlock,
rpcBlock.hash,
rpcBlock.number,
);
return NextResponse.json(serializeBlockData(blockData));
}

const cachedBlock = await getBlockFromCache(identifier);
if (cachedBlock) {
const { updatedBlock, hasUpdates } =
await refetchMissingTransactionSimulations(cachedBlock);
Expand All @@ -147,39 +228,16 @@ export async function GET(
return NextResponse.json(serializeBlockData(updatedBlock));
}

const rpcBlock = await fetchBlockFromRpc(hash);
const rpcBlock = await fetchBlockFromRpc(identifier);
if (!rpcBlock || !rpcBlock.hash || !rpcBlock.number) {
return NextResponse.json({ error: "Block not found" }, { status: 404 });
}

const transactions: BlockTransaction[] = await Promise.all(
rpcBlock.transactions.map(async (tx, index) => {
const { bundleId, executionTimeUs } =
await enrichTransactionWithBundleData(tx.hash);
return {
hash: tx.hash,
from: tx.from,
to: tx.to,
gasUsed: tx.gas,
executionTimeUs,
bundleId,
index,
};
}),
const blockData = await buildAndCacheBlockData(
rpcBlock,
rpcBlock.hash,
rpcBlock.number,
);

const blockData: BlockData = {
hash: rpcBlock.hash,
number: rpcBlock.number,
timestamp: rpcBlock.timestamp,
transactions,
gasUsed: rpcBlock.gasUsed,
gasLimit: rpcBlock.gasLimit,
cachedAt: Date.now(),
};

await cacheBlockData(blockData);

return NextResponse.json(serializeBlockData(blockData));
} catch (error) {
console.error("Error fetching block data:", error);
Expand Down
77 changes: 74 additions & 3 deletions src/app/block/[hash]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ export default function BlockPage({ params }: PageProps) {
useEffect(() => {
if (!hash) return;

setData(null);
setLoading(true);
setError(null);

const fetchData = async () => {
try {
const response = await fetch(`/api/block/${hash}`);
Expand Down Expand Up @@ -321,15 +325,82 @@ export default function BlockPage({ params }: PageProps) {
>
TIPS
</Link>
{data && (
<>
<div className="h-6 w-px bg-gray-200" />
<div className="flex items-center gap-1">
{Number(data.number) > 0 ? (
<Link
href={`/block/${Number(data.number) - 1}`}
className="inline-flex items-center justify-center w-8 h-8 rounded-lg hover:bg-gray-100 transition-colors text-gray-500 hover:text-gray-900"
title="Previous block"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<title>Previous block</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</Link>
) : (
<span className="inline-flex items-center justify-center w-8 h-8 rounded-lg text-gray-300 cursor-not-allowed">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<title>Previous block</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</span>
)}
<Link
href={`/block/${Number(data.number) + 1}`}
className="inline-flex items-center justify-center w-8 h-8 rounded-lg hover:bg-gray-100 transition-colors text-gray-500 hover:text-gray-900"
title="Next block"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<title>Next block</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</Link>
</div>
</>
)}
</div>
<div className="flex items-center gap-2 text-sm">
<code className="font-mono text-gray-600 bg-gray-100 px-2 py-1 rounded text-xs">
{hash.slice(0, 10)}...{hash.slice(-8)}
{(data?.hash ?? hash).slice(0, 10)}...
{(data?.hash ?? hash).slice(-8)}
</code>
<CopyButton text={hash} />
<CopyButton text={data?.hash ?? hash} />
{BLOCK_EXPLORER_URL && (
<a
href={`${BLOCK_EXPLORER_URL}/block/${hash}`}
href={`${BLOCK_EXPLORER_URL}/block/${data?.hash ?? hash}`}
target="_blank"
rel="noopener noreferrer"
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors text-gray-400 hover:text-gray-600"
Expand Down
Loading