Initial commit with 🏗️ create-eth @ 2.0.4

This commit is contained in:
han
2026-01-23 20:20:58 +07:00
commit b330aba2b4
185 changed files with 36981 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const CONFIG_PATH = path.join(process.cwd(), "..", "hardhat", "scripts", "oracle-bot", "config.json");
export async function POST(request: Request) {
try {
const body = await request.json();
const { value, nodeAddress } = body;
if (typeof value !== "number" || value < 0) {
return NextResponse.json({ error: "Value must be a non-negative number" }, { status: 400 });
}
// Read current config
const configContent = await fs.promises.readFile(CONFIG_PATH, "utf-8");
const config = JSON.parse(configContent);
// Update node-specific config
if (!config.NODE_CONFIGS[nodeAddress]) {
config.NODE_CONFIGS[nodeAddress] = { ...config.NODE_CONFIGS.default };
}
config.NODE_CONFIGS[nodeAddress].PRICE_VARIANCE = value;
// Write back to file
await fs.promises.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
return NextResponse.json({ success: true, value });
} catch (error) {
console.error("Error updating price variance:", error);
return NextResponse.json({ error: "Failed to update configuration" }, { status: 500 });
}
}
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const nodeAddress = searchParams.get("nodeAddress");
if (!nodeAddress) {
return NextResponse.json({ error: "nodeAddress parameter is required" }, { status: 400 });
}
const configContent = await fs.promises.readFile(CONFIG_PATH, "utf-8");
const config = JSON.parse(configContent);
const nodeConfig = config.NODE_CONFIGS[nodeAddress] || config.NODE_CONFIGS.default;
return NextResponse.json({
value: nodeConfig.PRICE_VARIANCE,
});
} catch (error) {
console.error("Error reading price variance:", error);
return NextResponse.json({ error: "Failed to read configuration" }, { status: 500 });
}
}

View File

@@ -0,0 +1,56 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const CONFIG_PATH = path.join(process.cwd(), "..", "hardhat", "scripts", "oracle-bot", "config.json");
export async function POST(request: Request) {
try {
const body = await request.json();
const { value, nodeAddress } = body;
if (typeof value !== "number" || value < 0 || value > 1) {
return NextResponse.json({ error: "Value must be a number between 0 and 1" }, { status: 400 });
}
// Read current config
const configContent = await fs.promises.readFile(CONFIG_PATH, "utf-8");
const config = JSON.parse(configContent);
// Update node-specific config
if (!config.NODE_CONFIGS[nodeAddress]) {
config.NODE_CONFIGS[nodeAddress] = { ...config.NODE_CONFIGS.default };
}
config.NODE_CONFIGS[nodeAddress].PROBABILITY_OF_SKIPPING_REPORT = value;
// Write back to file
await fs.promises.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
return NextResponse.json({ success: true, value });
} catch (error) {
console.error("Error updating skip probability:", error);
return NextResponse.json({ error: "Failed to update configuration" }, { status: 500 });
}
}
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const nodeAddress = searchParams.get("nodeAddress");
if (!nodeAddress) {
return NextResponse.json({ error: "nodeAddress parameter is required" }, { status: 400 });
}
const configContent = await fs.promises.readFile(CONFIG_PATH, "utf-8");
const config = JSON.parse(configContent);
const nodeConfig = config.NODE_CONFIGS[nodeAddress] || config.NODE_CONFIGS.default;
return NextResponse.json({
value: nodeConfig.PROBABILITY_OF_SKIPPING_REPORT,
});
} catch (error) {
console.error("Error reading skip probability:", error);
return NextResponse.json({ error: "Failed to read configuration" }, { status: 500 });
}
}

View File

@@ -0,0 +1,88 @@
import { NextResponse } from "next/server";
import { createPublicClient, createWalletClient, http, parseEther } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { hardhat } from "viem/chains";
import deployedContracts from "~~/contracts/deployedContracts";
const oraTokenAbi = [
{
type: "function",
name: "transfer",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
] as const;
const stakingOracleAbi = [
{
type: "function",
name: "oracleToken",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "address" }],
},
] as const;
const DEPLOYER_PRIVATE_KEY =
(process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY as `0x${string}` | undefined) ??
// Hardhat default account #0 private key (localhost only).
("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as const);
function isAddress(value: unknown): value is `0x${string}` {
return typeof value === "string" && /^0x[a-fA-F0-9]{40}$/.test(value);
}
export async function POST(request: Request) {
try {
const body = await request.json();
const to = body?.to;
const amount = body?.amount ?? "2000";
if (!isAddress(to)) {
return NextResponse.json({ error: "Invalid `to` address" }, { status: 400 });
}
if (typeof amount !== "string" || !/^\d+(\.\d+)?$/.test(amount)) {
return NextResponse.json({ error: "Invalid `amount`" }, { status: 400 });
}
// Safety: this faucet is intended for local Hardhat usage only.
if (process.env.NODE_ENV === "production") {
return NextResponse.json({ error: "ORA faucet disabled in production" }, { status: 403 });
}
const publicClient = createPublicClient({ chain: hardhat, transport: http() });
const account = privateKeyToAccount(DEPLOYER_PRIVATE_KEY);
const walletClient = createWalletClient({ chain: hardhat, transport: http(), account });
const stakingOracleAddress = (deployedContracts as any)?.[hardhat.id]?.StakingOracle?.address as
| `0x${string}`
| undefined;
if (!stakingOracleAddress) {
return NextResponse.json({ error: "StakingOracle not deployed on this network" }, { status: 500 });
}
const oraTokenAddress = (await publicClient.readContract({
address: stakingOracleAddress,
abi: stakingOracleAbi,
functionName: "oracleToken",
})) as `0x${string}`;
const hash = await walletClient.writeContract({
address: oraTokenAddress,
abi: oraTokenAbi,
functionName: "transfer",
args: [to, parseEther(amount)],
});
await publicClient.waitForTransactionReceipt({ hash });
return NextResponse.json({ success: true, hash });
} catch (error) {
console.error("Error funding ORA:", error);
return NextResponse.json({ error: "Failed to fund ORA" }, { status: 500 });
}
}

View File

@@ -0,0 +1,27 @@
type AddressCodeTabProps = {
bytecode: string;
assembly: string;
};
export const AddressCodeTab = ({ bytecode, assembly }: AddressCodeTabProps) => {
const formattedAssembly = Array.from(assembly.matchAll(/\w+( 0x[a-fA-F0-9]+)?/g))
.map(it => it[0])
.join("\n");
return (
<div className="flex flex-col gap-3 p-4">
Bytecode
<div className="mockup-code -indent-5 overflow-y-auto max-h-[500px]">
<pre className="px-5">
<code className="whitespace-pre-wrap overflow-auto break-words">{bytecode}</code>
</pre>
</div>
Opcodes
<div className="mockup-code -indent-5 overflow-y-auto max-h-[500px]">
<pre className="px-5">
<code>{formattedAssembly}</code>
</pre>
</div>
</div>
);
};

View File

@@ -0,0 +1,48 @@
"use client";
import { BackButton } from "./BackButton";
import { ContractTabs } from "./ContractTabs";
import { Address, Balance } from "@scaffold-ui/components";
import { Address as AddressType } from "viem";
import { hardhat } from "viem/chains";
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
export const AddressComponent = ({
address,
contractData,
}: {
address: AddressType;
contractData: { bytecode: string; assembly: string } | null;
}) => {
const { targetNetwork } = useTargetNetwork();
return (
<div className="m-10 mb-20">
<div className="flex justify-start mb-5">
<BackButton />
</div>
<div className="col-span-5 grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-10">
<div className="col-span-1 flex flex-col">
<div className="bg-base-100 border-base-300 border shadow-md shadow-secondary rounded-3xl px-6 lg:px-8 mb-6 space-y-1 py-4 overflow-x-auto">
<div className="flex">
<div className="flex flex-col gap-1">
<Address
address={address}
format="long"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${address}` : undefined
}
/>
<div className="flex gap-1 items-center">
<span className="font-bold text-sm">Balance:</span>
<Balance address={address} />
</div>
</div>
</div>
</div>
</div>
</div>
<ContractTabs address={address} contractData={contractData} />
</div>
);
};

View File

@@ -0,0 +1,21 @@
import { Address } from "viem";
import { useContractLogs } from "~~/hooks/scaffold-eth";
import { replacer } from "~~/utils/scaffold-eth/common";
export const AddressLogsTab = ({ address }: { address: Address }) => {
const contractLogs = useContractLogs(address);
return (
<div className="flex flex-col gap-3 p-4">
<div className="mockup-code overflow-auto max-h-[500px]">
<pre className="px-5 whitespace-pre-wrap break-words">
{contractLogs.map((log, i) => (
<div key={i}>
<strong>Log:</strong> {JSON.stringify(log, replacer, 2)}
</div>
))}
</pre>
</div>
</div>
);
};

View File

@@ -0,0 +1,61 @@
"use client";
import { useEffect, useState } from "react";
import { Address, createPublicClient, http, toHex } from "viem";
import { hardhat } from "viem/chains";
const publicClient = createPublicClient({
chain: hardhat,
transport: http(),
});
export const AddressStorageTab = ({ address }: { address: Address }) => {
const [storage, setStorage] = useState<string[]>([]);
useEffect(() => {
const fetchStorage = async () => {
try {
const storageData = [];
let idx = 0;
while (true) {
const storageAtPosition = await publicClient.getStorageAt({
address: address,
slot: toHex(idx),
});
if (storageAtPosition === "0x" + "0".repeat(64)) break;
if (storageAtPosition) {
storageData.push(storageAtPosition);
}
idx++;
}
setStorage(storageData);
} catch (error) {
console.error("Failed to fetch storage:", error);
}
};
fetchStorage();
}, [address]);
return (
<div className="flex flex-col gap-3 p-4">
{storage.length > 0 ? (
<div className="mockup-code overflow-auto max-h-[500px]">
<pre className="px-5 whitespace-pre-wrap break-words">
{storage.map((data, i) => (
<div key={i}>
<strong>Storage Slot {i}:</strong> {data}
</div>
))}
</pre>
</div>
) : (
<div className="text-lg">This contract does not have any variables.</div>
)}
</div>
);
};

View File

@@ -0,0 +1,12 @@
"use client";
import { useRouter } from "next/navigation";
export const BackButton = () => {
const router = useRouter();
return (
<button className="btn btn-sm btn-primary" onClick={() => router.back()}>
Back
</button>
);
};

View File

@@ -0,0 +1,102 @@
"use client";
import { useEffect, useState } from "react";
import { AddressCodeTab } from "./AddressCodeTab";
import { AddressLogsTab } from "./AddressLogsTab";
import { AddressStorageTab } from "./AddressStorageTab";
import { PaginationButton } from "./PaginationButton";
import { TransactionsTable } from "./TransactionsTable";
import { Address, createPublicClient, http } from "viem";
import { hardhat } from "viem/chains";
import { useFetchBlocks } from "~~/hooks/scaffold-eth";
type AddressCodeTabProps = {
bytecode: string;
assembly: string;
};
type PageProps = {
address: Address;
contractData: AddressCodeTabProps | null;
};
const publicClient = createPublicClient({
chain: hardhat,
transport: http(),
});
export const ContractTabs = ({ address, contractData }: PageProps) => {
const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage } = useFetchBlocks();
const [activeTab, setActiveTab] = useState("transactions");
const [isContract, setIsContract] = useState(false);
useEffect(() => {
const checkIsContract = async () => {
const contractCode = await publicClient.getBytecode({ address: address });
setIsContract(contractCode !== undefined && contractCode !== "0x");
};
checkIsContract();
}, [address]);
const filteredBlocks = blocks.filter(block =>
block.transactions.some(tx => {
if (typeof tx === "string") {
return false;
}
return tx.from.toLowerCase() === address.toLowerCase() || tx.to?.toLowerCase() === address.toLowerCase();
}),
);
return (
<>
{isContract && (
<div role="tablist" className="tabs tabs-lift">
<button
role="tab"
className={`tab ${activeTab === "transactions" ? "tab-active" : ""}`}
onClick={() => setActiveTab("transactions")}
>
Transactions
</button>
<button
role="tab"
className={`tab ${activeTab === "code" ? "tab-active" : ""}`}
onClick={() => setActiveTab("code")}
>
Code
</button>
<button
role="tab"
className={`tab ${activeTab === "storage" ? "tab-active" : ""}`}
onClick={() => setActiveTab("storage")}
>
Storage
</button>
<button
role="tab"
className={`tab ${activeTab === "logs" ? "tab-active" : ""}`}
onClick={() => setActiveTab("logs")}
>
Logs
</button>
</div>
)}
{activeTab === "transactions" && (
<div className="pt-4">
<TransactionsTable blocks={filteredBlocks} transactionReceipts={transactionReceipts} />
<PaginationButton
currentPage={currentPage}
totalItems={Number(totalBlocks)}
setCurrentPage={setCurrentPage}
/>
</div>
)}
{activeTab === "code" && contractData && (
<AddressCodeTab bytecode={contractData.bytecode} assembly={contractData.assembly} />
)}
{activeTab === "storage" && <AddressStorageTab address={address} />}
{activeTab === "logs" && <AddressLogsTab address={address} />}
</>
);
};

View File

@@ -0,0 +1,39 @@
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
type PaginationButtonProps = {
currentPage: number;
totalItems: number;
setCurrentPage: (page: number) => void;
};
const ITEMS_PER_PAGE = 20;
export const PaginationButton = ({ currentPage, totalItems, setCurrentPage }: PaginationButtonProps) => {
const isPrevButtonDisabled = currentPage === 0;
const isNextButtonDisabled = currentPage + 1 >= Math.ceil(totalItems / ITEMS_PER_PAGE);
const prevButtonClass = isPrevButtonDisabled ? "btn-disabled cursor-default" : "btn-primary";
const nextButtonClass = isNextButtonDisabled ? "btn-disabled cursor-default" : "btn-primary";
if (isNextButtonDisabled && isPrevButtonDisabled) return null;
return (
<div className="mt-5 justify-end flex gap-3 mx-5">
<button
className={`btn btn-sm ${prevButtonClass}`}
disabled={isPrevButtonDisabled}
onClick={() => setCurrentPage(currentPage - 1)}
>
<ArrowLeftIcon className="h-4 w-4" />
</button>
<span className="self-center text-primary-content font-medium">Page {currentPage + 1}</span>
<button
className={`btn btn-sm ${nextButtonClass}`}
disabled={isNextButtonDisabled}
onClick={() => setCurrentPage(currentPage + 1)}
>
<ArrowRightIcon className="h-4 w-4" />
</button>
</div>
);
};

View File

@@ -0,0 +1,49 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { isAddress, isHex } from "viem";
import { hardhat } from "viem/chains";
import { usePublicClient } from "wagmi";
export const SearchBar = () => {
const [searchInput, setSearchInput] = useState("");
const router = useRouter();
const client = usePublicClient({ chainId: hardhat.id });
const handleSearch = async (event: React.FormEvent) => {
event.preventDefault();
if (isHex(searchInput)) {
try {
const tx = await client?.getTransaction({ hash: searchInput });
if (tx) {
router.push(`/blockexplorer/transaction/${searchInput}`);
return;
}
} catch (error) {
console.error("Failed to fetch transaction:", error);
}
}
if (isAddress(searchInput)) {
router.push(`/blockexplorer/address/${searchInput}`);
return;
}
};
return (
<form onSubmit={handleSearch} className="flex items-center justify-end mb-5 space-x-3 mx-5">
<input
className="border-primary bg-base-100 text-base-content placeholder:text-base-content/50 p-2 mr-2 w-full md:w-1/2 lg:w-1/3 rounded-md shadow-md focus:outline-hidden focus:ring-2 focus:ring-accent"
type="text"
value={searchInput}
placeholder="Search by hash or address"
onChange={e => setSearchInput(e.target.value)}
/>
<button className="btn btn-sm btn-primary" type="submit">
Search
</button>
</form>
);
};

View File

@@ -0,0 +1,28 @@
import Link from "next/link";
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
export const TransactionHash = ({ hash }: { hash: string }) => {
const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
useCopyToClipboard();
return (
<div className="flex items-center">
<Link href={`/blockexplorer/transaction/${hash}`}>
{hash?.substring(0, 6)}...{hash?.substring(hash.length - 4)}
</Link>
{isAddressCopiedToClipboard ? (
<CheckCircleIcon
className="ml-1.5 text-xl font-normal text-base-content h-5 w-5 cursor-pointer"
aria-hidden="true"
/>
) : (
<DocumentDuplicateIcon
className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer"
aria-hidden="true"
onClick={() => copyAddressToClipboard(hash)}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,97 @@
import { TransactionHash } from "./TransactionHash";
import { Address } from "@scaffold-ui/components";
import { formatEther } from "viem";
import { hardhat } from "viem/chains";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { TransactionWithFunction } from "~~/utils/scaffold-eth";
import { TransactionsTableProps } from "~~/utils/scaffold-eth/";
export const TransactionsTable = ({ blocks, transactionReceipts }: TransactionsTableProps) => {
const { targetNetwork } = useTargetNetwork();
return (
<div className="flex justify-center px-4 md:px-0">
<div className="overflow-x-auto w-full shadow-2xl rounded-xl">
<table className="table text-xl bg-base-100 table-zebra w-full md:table-md table-sm">
<thead>
<tr className="rounded-xl text-sm text-base-content">
<th className="bg-primary">Transaction Hash</th>
<th className="bg-primary">Function Called</th>
<th className="bg-primary">Block Number</th>
<th className="bg-primary">Time Mined</th>
<th className="bg-primary">From</th>
<th className="bg-primary">To</th>
<th className="bg-primary text-end">Value ({targetNetwork.nativeCurrency.symbol})</th>
</tr>
</thead>
<tbody>
{blocks.map(block =>
(block.transactions as TransactionWithFunction[]).map(tx => {
const receipt = transactionReceipts[tx.hash];
const timeMined = new Date(Number(block.timestamp) * 1000).toLocaleString();
const functionCalled = tx.input.substring(0, 10);
return (
<tr key={tx.hash} className="hover text-sm">
<td className="w-1/12 md:py-4">
<TransactionHash hash={tx.hash} />
</td>
<td className="w-2/12 md:py-4">
{tx.functionName === "0x" ? "" : <span className="mr-1">{tx.functionName}</span>}
{functionCalled !== "0x" && (
<span className="badge badge-primary font-bold text-xs">{functionCalled}</span>
)}
</td>
<td className="w-1/12 md:py-4">{block.number?.toString()}</td>
<td className="w-2/12 md:py-4">{timeMined}</td>
<td className="w-2/12 md:py-4">
<Address
address={tx.from}
size="sm"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${tx.from}` : undefined
}
/>
</td>
<td className="w-2/12 md:py-4">
{!receipt?.contractAddress ? (
tx.to && (
<Address
address={tx.to}
size="sm"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${tx.to}` : undefined
}
/>
)
) : (
<div className="relative">
<Address
address={receipt.contractAddress}
size="sm"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id
? `/blockexplorer/address/${receipt.contractAddress}`
: undefined
}
/>
<small className="absolute top-4 left-4">(Contract Creation)</small>
</div>
)}
</td>
<td className="text-right md:py-4">
{formatEther(tx.value)} {targetNetwork.nativeCurrency.symbol}
</td>
</tr>
);
}),
)}
</tbody>
</table>
</div>
</div>
);
};

View File

@@ -0,0 +1,7 @@
export * from "./SearchBar";
export * from "./BackButton";
export * from "./AddressCodeTab";
export * from "./TransactionHash";
export * from "./ContractTabs";
export * from "./PaginationButton";
export * from "./TransactionsTable";

View File

@@ -0,0 +1,101 @@
import fs from "fs";
import path from "path";
import { Address } from "viem";
import { hardhat } from "viem/chains";
import { AddressComponent } from "~~/app/blockexplorer/_components/AddressComponent";
import deployedContracts from "~~/contracts/deployedContracts";
import { isZeroAddress } from "~~/utils/scaffold-eth/common";
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
type PageProps = {
params: Promise<{ address: Address }>;
};
async function fetchByteCodeAndAssembly(buildInfoDirectory: string, contractPath: string) {
const buildInfoFiles = fs.readdirSync(buildInfoDirectory);
let bytecode = "";
let assembly = "";
for (let i = 0; i < buildInfoFiles.length; i++) {
const filePath = path.join(buildInfoDirectory, buildInfoFiles[i]);
const buildInfo = JSON.parse(fs.readFileSync(filePath, "utf8"));
if (buildInfo.output.contracts[contractPath]) {
for (const contract in buildInfo.output.contracts[contractPath]) {
bytecode = buildInfo.output.contracts[contractPath][contract].evm.bytecode.object;
assembly = buildInfo.output.contracts[contractPath][contract].evm.bytecode.opcodes;
break;
}
}
if (bytecode && assembly) {
break;
}
}
return { bytecode, assembly };
}
const getContractData = async (address: Address) => {
const contracts = deployedContracts as GenericContractsDeclaration | null;
const chainId = hardhat.id;
if (!contracts || !contracts[chainId] || Object.keys(contracts[chainId]).length === 0) {
return null;
}
let contractPath = "";
const buildInfoDirectory = path.join(
__dirname,
"..",
"..",
"..",
"..",
"..",
"..",
"..",
"hardhat",
"artifacts",
"build-info",
);
if (!fs.existsSync(buildInfoDirectory)) {
throw new Error(`Directory ${buildInfoDirectory} not found.`);
}
const deployedContractsOnChain = contracts[chainId];
for (const [contractName, contractInfo] of Object.entries(deployedContractsOnChain)) {
if (contractInfo.address.toLowerCase() === address.toLowerCase()) {
contractPath = `contracts/${contractName}.sol`;
break;
}
}
if (!contractPath) {
// No contract found at this address
return null;
}
const { bytecode, assembly } = await fetchByteCodeAndAssembly(buildInfoDirectory, contractPath);
return { bytecode, assembly };
};
export function generateStaticParams() {
// An workaround to enable static exports in Next.js, generating single dummy page.
return [{ address: "0x0000000000000000000000000000000000000000" }];
}
const AddressPage = async (props: PageProps) => {
const params = await props.params;
const address = params?.address as Address;
if (isZeroAddress(address)) return null;
const contractData: { bytecode: string; assembly: string } | null = await getContractData(address);
return <AddressComponent address={address} contractData={contractData} />;
};
export default AddressPage;

View File

@@ -0,0 +1,12 @@
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
export const metadata = getMetadata({
title: "Block Explorer",
description: "Block Explorer created with 🏗 Scaffold-ETH 2",
});
const BlockExplorerLayout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default BlockExplorerLayout;

View File

@@ -0,0 +1,83 @@
"use client";
import { useEffect, useState } from "react";
import { PaginationButton, SearchBar, TransactionsTable } from "./_components";
import type { NextPage } from "next";
import { hardhat } from "viem/chains";
import { useFetchBlocks } from "~~/hooks/scaffold-eth";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { notification } from "~~/utils/scaffold-eth";
const BlockExplorer: NextPage = () => {
const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage, error } = useFetchBlocks();
const { targetNetwork } = useTargetNetwork();
const [isLocalNetwork, setIsLocalNetwork] = useState(true);
const [hasError, setHasError] = useState(false);
useEffect(() => {
if (targetNetwork.id !== hardhat.id) {
setIsLocalNetwork(false);
}
}, [targetNetwork.id]);
useEffect(() => {
if (targetNetwork.id === hardhat.id && error) {
setHasError(true);
}
}, [targetNetwork.id, error]);
useEffect(() => {
if (!isLocalNetwork) {
notification.error(
<>
<p className="font-bold mt-0 mb-1">
<code className="italic bg-base-300 text-base font-bold"> targetNetwork </code> is not localhost
</p>
<p className="m-0">
- You are on <code className="italic bg-base-300 text-base font-bold">{targetNetwork.name}</code> .This
block explorer is only for <code className="italic bg-base-300 text-base font-bold">localhost</code>.
</p>
<p className="mt-1 break-normal">
- You can use{" "}
<a className="text-accent" href={targetNetwork.blockExplorers?.default.url}>
{targetNetwork.blockExplorers?.default.name}
</a>{" "}
instead
</p>
</>,
);
}
}, [
isLocalNetwork,
targetNetwork.blockExplorers?.default.name,
targetNetwork.blockExplorers?.default.url,
targetNetwork.name,
]);
useEffect(() => {
if (hasError) {
notification.error(
<>
<p className="font-bold mt-0 mb-1">Cannot connect to local provider</p>
<p className="m-0">
- Did you forget to run <code className="italic bg-base-300 text-base font-bold">yarn chain</code> ?
</p>
<p className="mt-1 break-normal">
- Or you can change <code className="italic bg-base-300 text-base font-bold">targetNetwork</code> in{" "}
<code className="italic bg-base-300 text-base font-bold">scaffold.config.ts</code>
</p>
</>,
);
}
}, [hasError]);
return (
<div className="container mx-auto my-10">
<SearchBar />
<TransactionsTable blocks={blocks} transactionReceipts={transactionReceipts} />
<PaginationButton currentPage={currentPage} totalItems={Number(totalBlocks)} setCurrentPage={setCurrentPage} />
</div>
);
};
export default BlockExplorer;

View File

@@ -0,0 +1,23 @@
import TransactionComp from "../_components/TransactionComp";
import type { NextPage } from "next";
import { Hash } from "viem";
import { isZeroAddress } from "~~/utils/scaffold-eth/common";
type PageProps = {
params: Promise<{ txHash?: Hash }>;
};
export function generateStaticParams() {
// An workaround to enable static exports in Next.js, generating single dummy page.
return [{ txHash: "0x0000000000000000000000000000000000000000" }];
}
const TransactionPage: NextPage<PageProps> = async (props: PageProps) => {
const params = await props.params;
const txHash = params?.txHash as Hash;
if (isZeroAddress(txHash)) return null;
return <TransactionComp txHash={txHash} />;
};
export default TransactionPage;

View File

@@ -0,0 +1,177 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Address } from "@scaffold-ui/components";
import { Hash, Transaction, TransactionReceipt, formatEther, formatUnits } from "viem";
import { hardhat } from "viem/chains";
import { usePublicClient } from "wagmi";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { decodeTransactionData, getFunctionDetails } from "~~/utils/scaffold-eth";
import { replacer } from "~~/utils/scaffold-eth/common";
const TransactionComp = ({ txHash }: { txHash: Hash }) => {
const client = usePublicClient({ chainId: hardhat.id });
const router = useRouter();
const [transaction, setTransaction] = useState<Transaction>();
const [receipt, setReceipt] = useState<TransactionReceipt>();
const [functionCalled, setFunctionCalled] = useState<string>();
const { targetNetwork } = useTargetNetwork();
useEffect(() => {
if (txHash && client) {
const fetchTransaction = async () => {
const tx = await client.getTransaction({ hash: txHash });
const receipt = await client.getTransactionReceipt({ hash: txHash });
const transactionWithDecodedData = decodeTransactionData(tx);
setTransaction(transactionWithDecodedData);
setReceipt(receipt);
const functionCalled = transactionWithDecodedData.input.substring(0, 10);
setFunctionCalled(functionCalled);
};
fetchTransaction();
}
}, [client, txHash]);
return (
<div className="container mx-auto mt-10 mb-20 px-10 md:px-0">
<button className="btn btn-sm btn-primary" onClick={() => router.back()}>
Back
</button>
{transaction ? (
<div className="overflow-x-auto">
<h2 className="text-3xl font-bold mb-4 text-center text-primary-content">Transaction Details</h2>{" "}
<table className="table rounded-lg bg-base-100 w-full shadow-lg md:table-lg table-md">
<tbody>
<tr>
<td>
<strong>Transaction Hash:</strong>
</td>
<td>{transaction.hash}</td>
</tr>
<tr>
<td>
<strong>Block Number:</strong>
</td>
<td>{Number(transaction.blockNumber)}</td>
</tr>
<tr>
<td>
<strong>From:</strong>
</td>
<td>
<Address
address={transaction.from}
format="long"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${transaction.from}` : undefined
}
/>
</td>
</tr>
<tr>
<td>
<strong>To:</strong>
</td>
<td>
{!receipt?.contractAddress ? (
transaction.to && (
<Address
address={transaction.to}
format="long"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${transaction.to}` : undefined
}
/>
)
) : (
<span>
Contract Creation:
<Address
address={receipt.contractAddress}
format="long"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id
? `/blockexplorer/address/${receipt.contractAddress}`
: undefined
}
/>
</span>
)}
</td>
</tr>
<tr>
<td>
<strong>Value:</strong>
</td>
<td>
{formatEther(transaction.value)} {targetNetwork.nativeCurrency.symbol}
</td>
</tr>
<tr>
<td>
<strong>Function called:</strong>
</td>
<td>
<div className="w-full md:max-w-[600px] lg:max-w-[800px] overflow-x-auto whitespace-nowrap">
{functionCalled === "0x" ? (
"This transaction did not call any function."
) : (
<>
<span className="mr-2">{getFunctionDetails(transaction)}</span>
<span className="badge badge-primary font-bold">{functionCalled}</span>
</>
)}
</div>
</td>
</tr>
<tr>
<td>
<strong>Gas Price:</strong>
</td>
<td>{formatUnits(transaction.gasPrice || 0n, 9)} Gwei</td>
</tr>
<tr>
<td>
<strong>Data:</strong>
</td>
<td className="form-control">
<textarea
readOnly
value={transaction.input}
className="p-0 w-full textarea-primary bg-inherit h-[150px]"
/>
</td>
</tr>
<tr>
<td>
<strong>Logs:</strong>
</td>
<td>
<ul>
{receipt?.logs?.map((log, i) => (
<li key={i}>
<strong>Log {i} topics:</strong> {JSON.stringify(log.topics, replacer, 2)}
</li>
))}
</ul>
</td>
</tr>
</tbody>
</table>
</div>
) : (
<p className="text-2xl text-base-content">Loading...</p>
)}
</div>
);
};
export default TransactionComp;

View File

@@ -0,0 +1,38 @@
"use client";
// @refresh reset
import { Contract } from "@scaffold-ui/debug-contracts";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { ContractName } from "~~/utils/scaffold-eth/contract";
type ContractUIProps = {
contractName: ContractName;
className?: string;
};
/**
* UI component to interface with deployed contracts.
**/
export const ContractUI = ({ contractName }: ContractUIProps) => {
const { targetNetwork } = useTargetNetwork();
const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({ contractName });
if (deployedContractLoading) {
return (
<div className="mt-14">
<span className="loading loading-spinner loading-lg"></span>
</div>
);
}
if (!deployedContractData) {
return (
<p className="text-3xl mt-14">
No contract found by the name of {contractName} on chain {targetNetwork.name}!
</p>
);
}
return <Contract contractName={contractName as string} contract={deployedContractData} chainId={targetNetwork.id} />;
};

View File

@@ -0,0 +1,71 @@
"use client";
import { useEffect, useMemo } from "react";
import { ContractUI } from "./ContractUI";
import "@scaffold-ui/debug-contracts/styles.css";
import { useSessionStorage } from "usehooks-ts";
import { BarsArrowUpIcon } from "@heroicons/react/20/solid";
import { ContractName, GenericContract } from "~~/utils/scaffold-eth/contract";
import { useAllContracts } from "~~/utils/scaffold-eth/contractsData";
const selectedContractStorageKey = "scaffoldEth2.selectedContract";
export function DebugContracts() {
const contractsData = useAllContracts();
const contractNames = useMemo(
() =>
Object.keys(contractsData).sort((a, b) => {
return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
}) as ContractName[],
[contractsData],
);
const [selectedContract, setSelectedContract] = useSessionStorage<ContractName>(
selectedContractStorageKey,
contractNames[0],
{ initializeWithValue: false },
);
useEffect(() => {
if (!contractNames.includes(selectedContract)) {
setSelectedContract(contractNames[0]);
}
}, [contractNames, selectedContract, setSelectedContract]);
return (
<div className="flex flex-col gap-y-6 lg:gap-y-8 py-8 lg:py-12 justify-center items-center">
{contractNames.length === 0 ? (
<p className="text-3xl mt-14">No contracts found!</p>
) : (
<>
{contractNames.length > 1 && (
<div className="flex flex-row gap-2 w-full max-w-7xl pb-1 px-6 lg:px-10 flex-wrap">
{contractNames.map(contractName => (
<button
className={`btn btn-secondary btn-sm font-light hover:border-transparent ${
contractName === selectedContract
? "bg-base-300 hover:bg-base-300 no-animation"
: "bg-base-100 hover:bg-secondary"
}`}
key={contractName}
onClick={() => setSelectedContract(contractName)}
>
{contractName}
{(contractsData[contractName] as GenericContract)?.external && (
<span className="tooltip tooltip-top tooltip-accent" data-tip="External contract">
<BarsArrowUpIcon className="h-4 w-4 cursor-pointer" />
</span>
)}
</button>
))}
</div>
)}
{contractNames.map(
contractName =>
contractName === selectedContract && <ContractUI key={contractName} contractName={contractName} />,
)}
</>
)}
</div>
);
}

View File

@@ -0,0 +1,28 @@
import { DebugContracts } from "./_components/DebugContracts";
import type { NextPage } from "next";
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
export const metadata = getMetadata({
title: "Debug Contracts",
description: "Debug your deployed 🏗 Scaffold-ETH 2 contracts in an easy way",
});
const Debug: NextPage = () => {
return (
<>
<DebugContracts />
<div className="text-center mt-8 bg-secondary p-10">
<h1 className="text-4xl my-0">Debug Contracts</h1>
<p className="text-neutral">
You can debug & interact with your deployed contracts here.
<br /> Check{" "}
<code className="italic bg-base-300 text-base font-bold [word-spacing:-0.5rem] px-1">
packages / nextjs / app / debug / page.tsx
</code>{" "}
</p>
</div>
</>
);
};
export default Debug;

View File

@@ -0,0 +1,31 @@
import { Space_Grotesk } from "next/font/google";
import "@rainbow-me/rainbowkit/styles.css";
import "@scaffold-ui/components/styles.css";
import { ScaffoldEthAppWithProviders } from "~~/components/ScaffoldEthAppWithProviders";
import { ThemeProvider } from "~~/components/ThemeProvider";
import "~~/styles/globals.css";
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
variable: "--font-space-grotesk",
});
export const metadata = getMetadata({
title: "Oracles | Speedrun Ethereum",
description: "Built with 🏗 Scaffold-ETH 2",
});
const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
return (
<html suppressHydrationWarning className={`${spaceGrotesk.variable} font-space-grotesk`}>
<body>
<ThemeProvider enableSystem>
<ScaffoldEthAppWithProviders>{children}</ScaffoldEthAppWithProviders>
</ThemeProvider>
</body>
</html>
);
};
export default ScaffoldEthApp;

View File

@@ -0,0 +1,16 @@
import Link from "next/link";
export default function NotFound() {
return (
<div className="flex items-center h-full flex-1 justify-center bg-base-200">
<div className="text-center">
<h1 className="text-6xl font-bold m-0 mb-1">404</h1>
<h2 className="text-2xl font-semibold m-0">Page Not Found</h2>
<p className="text-base-content/70 m-0 mb-4">The page you&apos;re looking for doesn&apos;t exist.</p>
<Link href="/" className="btn btn-primary">
Go Home
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,117 @@
"use client";
import { useEffect, useState } from "react";
import type { NextPage } from "next";
import { useReadContracts } from "wagmi";
import { AssertedTable } from "~~/components/oracle/optimistic/AssertedTable";
import { AssertionModal } from "~~/components/oracle/optimistic/AssertionModal";
import { DisputedTable } from "~~/components/oracle/optimistic/DisputedTable";
import { ExpiredTable } from "~~/components/oracle/optimistic/ExpiredTable";
import { ProposedTable } from "~~/components/oracle/optimistic/ProposedTable";
import { SettledTable } from "~~/components/oracle/optimistic/SettledTable";
import { SubmitAssertionButton } from "~~/components/oracle/optimistic/SubmitAssertionButton";
import { useDeployedContractInfo, useScaffoldReadContract } from "~~/hooks/scaffold-eth";
import { useChallengeState } from "~~/services/store/challengeStore";
// Loading spinner component
const LoadingSpinner = () => (
<div className="flex justify-center items-center min-h-[400px]">
<div className="animate-spin rounded-full h-16 w-16 border-b-4 border-blue-500"></div>
</div>
);
const Home: NextPage = () => {
const setRefetchAssertionStates = useChallengeState(state => state.setRefetchAssertionStates);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const { data: nextAssertionId, isLoading: isLoadingNextAssertionId } = useScaffoldReadContract({
contractName: "OptimisticOracle",
functionName: "nextAssertionId",
query: {
placeholderData: (previousData: any) => previousData,
},
});
// get deployed contract address
const { data: deployedContractAddress, isLoading: isLoadingDeployedContract } = useDeployedContractInfo({
contractName: "OptimisticOracle",
});
// Create contracts array to get state for all assertions from 1 to nextAssertionId-1
const assertionContracts = nextAssertionId
? Array.from({ length: Number(nextAssertionId) - 1 }, (_, i) => ({
address: deployedContractAddress?.address as `0x${string}`,
abi: deployedContractAddress?.abi,
functionName: "getState",
args: [BigInt(i + 1)],
})).filter(contract => contract.address && contract.abi)
: [];
const {
data: assertionStates,
refetch: refetchAssertionStates,
isLoading: isLoadingAssertionStates,
} = useReadContracts({
contracts: assertionContracts,
query: {
placeholderData: (previousData: any) => previousData,
},
});
// Set the refetch function in the global store
useEffect(() => {
if (refetchAssertionStates) {
setRefetchAssertionStates(refetchAssertionStates);
}
}, [refetchAssertionStates, setRefetchAssertionStates]);
// Map assertion IDs to their states and filter out expired ones (state 5)
const assertionStateMap =
nextAssertionId && assertionStates
? Array.from({ length: Number(nextAssertionId) - 1 }, (_, i) => ({
assertionId: i + 1,
state: (assertionStates[i]?.result as number) || 0, // Default to 0 (Invalid) if no result
}))
: [];
// Track when initial loading is complete
const isFirstLoading =
isInitialLoading && (isLoadingNextAssertionId || isLoadingAssertionStates || isLoadingDeployedContract);
// Mark as initially loaded when all data is available
useEffect(() => {
if (isInitialLoading && !isLoadingNextAssertionId && !isLoadingDeployedContract && !isLoadingAssertionStates) {
setIsInitialLoading(false);
}
}, [isInitialLoading, isLoadingNextAssertionId, isLoadingDeployedContract, isLoadingAssertionStates]);
return (
<div className="container mx-auto px-8 py-8 max-w-screen-lg xl:max-w-screen-xl">
{/* Show loading spinner only during initial load */}
{isFirstLoading ? (
<LoadingSpinner />
) : (
<>
{/* Submit Assertion Button with Modal */}
<SubmitAssertionButton />
{/* Tables */}
<h2 className="text-2xl font-bold my-4">Asserted</h2>
<AssertedTable assertions={assertionStateMap.filter(assertion => assertion.state === 1)} />
<h2 className="text-2xl font-bold mt-12 mb-4">Proposed</h2>
<ProposedTable assertions={assertionStateMap.filter(assertion => assertion.state === 2)} />
<h2 className="text-2xl font-bold mt-12 mb-4">Disputed</h2>
<DisputedTable assertions={assertionStateMap.filter(assertion => assertion.state === 3)} />
<h2 className="text-2xl font-bold mt-12 mb-4">Settled</h2>
<SettledTable assertions={assertionStateMap.filter(assertion => assertion.state === 4)} />
<h2 className="text-2xl font-bold mt-12 mb-4">Expired</h2>
<ExpiredTable assertions={assertionStateMap.filter(assertion => assertion.state === 5)} />
</>
)}
<AssertionModal />
</div>
);
};
export default Home;

View File

@@ -0,0 +1,102 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { Address } from "@scaffold-ui/components";
import type { NextPage } from "next";
import { hardhat } from "viem/chains";
import { useAccount } from "wagmi";
import { BugAntIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
const Home: NextPage = () => {
const { address: connectedAddress } = useAccount();
const { targetNetwork } = useTargetNetwork();
return (
<>
<div className="flex items-center flex-col grow pt-10">
<div className="px-5">
<h1 className="text-center">
<span className="block text-2xl mb-2">Welcome to</span>
<span className="block text-4xl font-bold">Scaffold-ETH 2</span>
<span className="block text-xl font-bold">(Speedrun Ethereum Oracles extension)</span>
</h1>
<div className="flex justify-center items-center space-x-2 flex-col">
<p className="my-2 font-medium">Connected Address:</p>
<Address
address={connectedAddress}
chain={targetNetwork}
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${connectedAddress}` : undefined
}
/>
</div>
<div className="flex items-center flex-col flex-grow mt-4">
<div className="px-5 w-[90%]">
<h1 className="text-center mb-6">
<span className="block text-4xl font-bold">Oracles</span>
</h1>
<div className="flex flex-col items-center justify-center">
<Image
src="/hero.png"
width="727"
height="231"
alt="challenge banner"
className="rounded-xl border-4 border-primary"
/>
<div className="max-w-3xl">
<p className="text-center text-lg mt-8">
🔮 Build your own decentralized oracle network! In this challenge, you&apos;ll explore different
oracle architectures and implementations. You&apos;ll dive deep into concepts like staking
mechanisms, consensus algorithms, slashing conditions, and dispute resolution all crucial
components of a robust oracle system.
</p>
<p className="text-center text-lg">
🌟 The final deliverable is a comprehensive understanding of oracle architectures through hands-on
implementation. You&apos;ll explore two existing oracle systems (Whitelist and Staking) to
understand their mechanics, then implement the Optimistic Oracle from scratch. Deploy your
optimistic oracle to a testnet and demonstrate how it handles assertions, proposals, disputes, and
settlements. Then build and upload your app to a public web server. Submit the url on{" "}
<a href="https://speedrunethereum.com/" target="_blank" rel="noreferrer" className="underline">
SpeedrunEthereum.com
</a>{" "}
!
</p>
</div>
</div>
</div>
</div>
</div>
<div className="grow bg-base-300 w-full mt-16 px-8 py-12">
<div className="flex justify-center items-center gap-12 flex-col md:flex-row">
<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center max-w-xs rounded-3xl">
<BugAntIcon className="h-8 w-8 fill-secondary" />
<p>
Tinker with your smart contract using the{" "}
<Link href="/debug" passHref className="link">
Debug Contracts
</Link>{" "}
tab.
</p>
</div>
<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center max-w-xs rounded-3xl">
<MagnifyingGlassIcon className="h-8 w-8 fill-secondary" />
<p>
Explore your local transactions with the{" "}
<Link href="/blockexplorer" passHref className="link">
Block Explorer
</Link>{" "}
tab.
</p>
</div>
</div>
</div>
</div>
</>
);
};
export default Home;

View File

@@ -0,0 +1,42 @@
"use client";
import { useState } from "react";
import type { NextPage } from "next";
import { BucketCountdown } from "~~/components/oracle/BucketCountdown";
import { BuyOraWidget } from "~~/components/oracle/BuyOraWidget";
import { NodesTable } from "~~/components/oracle/NodesTable";
import { PriceWidget } from "~~/components/oracle/PriceWidget";
import { TotalSlashedWidget } from "~~/components/oracle/TotalSlashedWidget";
const Home: NextPage = () => {
const [selectedBucket, setSelectedBucket] = useState<bigint | "current">("current");
return (
<>
<div className="flex items-center flex-col flex-grow pt-2">
<div className="w-full px-0 sm:px-2">
<div className="flex justify-end mr-4 pt-2">
<BuyOraWidget />
</div>
</div>
<div className="px-5 w-full max-w-5xl mx-auto">
<div className="flex flex-col gap-8">
<div className="w-full">
<div className="grid w-full items-stretch grid-cols-1 md:grid-cols-3 gap-4">
<PriceWidget contractName="StakingOracle" />
<BucketCountdown />
<TotalSlashedWidget />
</div>
</div>
<div className="w-full">
<NodesTable selectedBucket={selectedBucket} onBucketChange={setSelectedBucket} />
</div>
</div>
</div>
</div>
</>
);
};
export default Home;

View File

@@ -0,0 +1,26 @@
"use client";
import type { NextPage } from "next";
import { PriceWidget } from "~~/components/oracle/PriceWidget";
import { WhitelistTable } from "~~/components/oracle/whitelist/WhitelistTable";
const Home: NextPage = () => {
return (
<>
<div className="flex items-center flex-col flex-grow pt-10">
<div className="px-5 w-full max-w-5xl mx-auto">
<div className="flex flex-col gap-8">
<div className="w-full">
<PriceWidget contractName="WhitelistOracle" />
</div>
<div className="w-full">
<WhitelistTable />
</div>
</div>
</div>
</div>
</>
);
};
export default Home;