Initial commit with 🏗️ create-eth @ 2.0.4

This commit is contained in:
han
2026-01-11 17:24:19 +07:00
commit 64378512ba
128 changed files with 27844 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
# Template for NextJS environment variables.
# For local development, copy this file, rename it to .env.local, and fill in the values.
# When deploying live, you'll need to store the vars in Vercel/System config.
# If not set, we provide default values (check `scaffold.config.ts`) so developers can start prototyping out of the box,
# but we recommend getting your own API Keys for Production Apps.
# To access the values stored in this env file you can use: process.env.VARIABLENAME
# You'll need to prefix the variables names with NEXT_PUBLIC_ if you want to access them on the client side.
# More info: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables
NEXT_PUBLIC_ALCHEMY_API_KEY=
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=

38
packages/nextjs/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
.vercel
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# typescript
*.tsbuildinfo
ipfs-upload.config.json

View File

@@ -0,0 +1,9 @@
module.exports = {
arrowParens: "avoid",
printWidth: 120,
tabWidth: 2,
trailingComma: "all",
importOrder: ["^react$", "^next/(.*)$", "<THIRD_PARTY_MODULES>", "^@heroicons/(.*)$", "^~~/(.*)$"],
importOrderSortSpecifiers: true,
plugins: [require.resolve("@trivago/prettier-plugin-sort-imports")],
};

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,63 @@
"use client";
import { Address } from "@scaffold-ui/components";
import type { NextPage } from "next";
import { formatEther } from "viem";
import { useScaffoldEventHistory } from "~~/hooks/scaffold-eth";
const ContributionsPage: NextPage = () => {
const { data: contributionEvents, isLoading } = useScaffoldEventHistory({
contractName: "CrowdFund",
eventName: "Contribution",
});
if (isLoading)
return (
<div className="flex justify-center items-center mt-10">
<span className="loading loading-spinner loading-lg"></span>
</div>
);
return (
<div className="flex items-center flex-col flex-grow pt-10">
<div className="px-5">
<h1 className="text-center mb-3">
<span className="block text-2xl font-bold">All Contributions</span>
</h1>
</div>
<div className="overflow-x-auto shadow-lg">
<table className="table table-zebra w-full">
<thead>
<tr>
<th className="bg-primary">From</th>
<th className="bg-primary">Value</th>
</tr>
</thead>
<tbody>
{!contributionEvents || contributionEvents.length === 0 ? (
<tr>
<td colSpan={3} className="text-center">
No events found
</td>
</tr>
) : (
contributionEvents.map((event, index) => {
return (
<tr key={index}>
<td>
<Address address={event.args?.[0]} />
</td>
<td>{formatEther(event.args?.[1] || 0n)} ETH</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
);
};
export default ContributionsPage;

View File

@@ -0,0 +1,143 @@
"use client";
import { ETHToPrice } from "./EthToPrice";
import { Address } from "@scaffold-ui/components";
import { useWatchBalance } from "@scaffold-ui/hooks";
import humanizeDuration from "humanize-duration";
import { formatEther, parseEther } from "viem";
import { useAccount } from "wagmi";
import { useDeployedContractInfo, useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
export const ContributeContractInteraction = ({ address }: { address?: string }) => {
const { address: connectedAddress } = useAccount();
const { data: crowdFundContract } = useDeployedContractInfo({ contractName: "CrowdFund" });
const { data: fundingRecipientContract } = useDeployedContractInfo({ contractName: "FundingRecipient" });
const { data: crowdFundContractBalance } = useWatchBalance({ address: crowdFundContract?.address });
const { data: fundingRecipientBalance } = useWatchBalance({ address: fundingRecipientContract?.address });
const { targetNetwork } = useTargetNetwork();
const { data: threshold } = useScaffoldReadContract({
contractName: "CrowdFund",
functionName: "threshold",
watch: true,
});
const { data: timeLeft } = useScaffoldReadContract({
contractName: "CrowdFund",
functionName: "timeLeft",
watch: true,
});
const { data: myContribution } = useScaffoldReadContract({
contractName: "CrowdFund",
functionName: "balances",
args: [connectedAddress],
watch: true,
});
const { data: isFundingCompleted } = useScaffoldReadContract({
contractName: "FundingRecipient",
functionName: "completed",
watch: true,
});
const { writeContractAsync } = useScaffoldWriteContract({ contractName: "CrowdFund" });
return (
<div className="flex items-center flex-col flex-grow w-full px-4 gap-12">
{isFundingCompleted && (
<div className="flex flex-col items-center gap-2 bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl p-6 mt-12 w-full max-w-lg">
<p className="block m-0 font-semibold">🎉 Crowdfunding contract triggered FundingRecipient 🎉</p>
<div className="flex items-center">
<ETHToPrice
value={fundingRecipientBalance ? formatEther(fundingRecipientBalance.value) : undefined}
className="text-[1rem]"
/>
<p className="block m-0 text-lg -ml-1">received</p>
</div>
</div>
)}
<div
className={`flex flex-col items-center space-y-8 bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl p-6 w-full max-w-lg ${
!isFundingCompleted ? "mt-24" : ""
}`}
>
<div className="flex flex-col w-full items-center">
<p className="block text-2xl mt-0 mb-2 font-semibold">CrowdFund Contract</p>
<Address address={address} size="xl" />
</div>
<div className="flex items-start justify-around w-full">
<div className="flex flex-col items-center justify-center w-1/2">
<p className="block text-xl mt-0 mb-1 font-semibold">Time Left</p>
<p className="m-0 p-0">{timeLeft ? `${humanizeDuration(Number(timeLeft) * 1000)}` : "DONE"}</p>
</div>
<div className="flex flex-col items-center w-1/2">
<p className="block text-xl mt-0 mb-1 font-semibold">You Contributed</p>
<span>
{myContribution ? formatEther(myContribution) : 0} {targetNetwork.nativeCurrency.symbol}
</span>
</div>
</div>
<div className="flex flex-col items-center shrink-0 w-full">
<p className="block text-xl mt-0 mb-1 font-semibold">Total Contributed</p>
<div className="flex space-x-2">
<ETHToPrice value={crowdFundContractBalance ? formatEther(crowdFundContractBalance.value) : undefined} />
<span>/</span>
<ETHToPrice value={threshold ? formatEther(threshold) : undefined} />
</div>
</div>
<div className="flex flex-col space-y-5">
<div className="flex space-x-7">
<button
className="btn btn-primary uppercase"
onClick={async () => {
try {
await writeContractAsync({ functionName: "execute" });
} catch (err) {
console.error("Error calling execute function", err);
}
}}
>
Execute
</button>
<button
className="btn btn-primary uppercase"
onClick={async () => {
try {
await writeContractAsync({ functionName: "withdraw" });
} catch (err) {
console.error("Error calling withdraw function", err);
}
}}
>
Withdraw
</button>
</div>
<button
className="btn btn-primary uppercase"
onClick={async () => {
try {
await writeContractAsync({ functionName: "contribute", value: parseEther("0.5") });
} catch (err) {
console.error("Error calling contribute function", err);
}
}}
>
🤝 Contribute 0.5 ether!
</button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,53 @@
import { useCallback, useState } from "react";
import { useFetchNativeCurrencyPrice } from "@scaffold-ui/hooks";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
type TBalanceProps = {
value?: string;
className?: string;
};
/**
* Display (ETH & USD) value for the input value provided.
*/
export const ETHToPrice = ({ value, className = "" }: TBalanceProps) => {
const [isEthBalance, setIsEthBalance] = useState(true);
const { targetNetwork } = useTargetNetwork();
const { price } = useFetchNativeCurrencyPrice();
const onToggleBalance = useCallback(() => {
if (price > 0) {
setIsEthBalance(!isEthBalance);
}
}, [isEthBalance, price]);
if (!value) {
return (
<div className="animate-pulse flex space-x-4">
<div className="flex items-center space-y-6">
<div className="h-5 w-12 bg-slate-300 rounded"></div>
</div>
</div>
);
}
return (
<button
className={`btn btn-sm btn-ghost flex flex-col font-normal items-center hover:bg-transparent ${className}`}
onClick={onToggleBalance}
>
<div className="w-full flex items-center justify-center">
{isEthBalance ? (
<>
<span>{parseFloat(value).toFixed(4)}</span>
<span className="text-xs font-bold ml-1">{targetNetwork.nativeCurrency.symbol}</span>
</>
) : (
<>
<span className="text-xs font-bold mr-1">$</span>
<span>{(parseFloat(value) * price).toFixed(2)}</span>
</>
)}
</div>
</button>
);
};

View File

@@ -0,0 +1,2 @@
export * from "./EthToPrice";
export * from "./ContributeContractInteraction";

View File

@@ -0,0 +1,12 @@
"use client";
import { ContributeContractInteraction } from "./_components";
import type { NextPage } from "next";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
const CrowdFundPage: NextPage = () => {
const { data: crowdFundContract } = useDeployedContractInfo({ contractName: "CrowdFund" });
return <ContributeContractInteraction key={crowdFundContract?.address} address={crowdFundContract?.address} />;
};
export default CrowdFundPage;

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: "Crowdfunding | 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,101 @@
"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">(SpeedrunEthereum Challenge: Crowdfunding App 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 pt-10">
<div className="px-5">
<h1 className="text-center mb-6">
<span className="block text-2xl mb-2">Speedrun Ethereum</span>
<span className="block text-4xl font-bold">Challenge: 📣 Crowdfunding App</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">
🦸 A superpower of Ethereum is allowing you, the builder, to create a simple set of rules that an
adversarial group of players can use to work together. In this challenge, you create a decentralized
application where users can coordinate a group funding effort. If the users cooperate, the money is
collected in a second smart contract. If they defect, the worst that can happen is everyone gets
their money back. The users only have to trust the code, not each other.
</p>
<p className="text-center text-lg">
🌟 The final deliverable is deploying a Dapp that lets users send ether to a contract and then fund
the cause if the conditions are met, then deploy your app to a public webserver. 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,80 @@
import React from "react";
import Link from "next/link";
import { useFetchNativeCurrencyPrice } from "@scaffold-ui/hooks";
import { hardhat } from "viem/chains";
import { CurrencyDollarIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { HeartIcon } from "@heroicons/react/24/outline";
import { SwitchTheme } from "~~/components/SwitchTheme";
import { BuidlGuidlLogo } from "~~/components/assets/BuidlGuidlLogo";
import { Faucet } from "~~/components/scaffold-eth";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
/**
* Site footer
*/
export const Footer = () => {
const { targetNetwork } = useTargetNetwork();
const isLocalNetwork = targetNetwork.id === hardhat.id;
const { price: nativeCurrencyPrice } = useFetchNativeCurrencyPrice();
return (
<div className="min-h-0 py-5 px-1 mb-11 lg:mb-0">
<div>
<div className="fixed flex justify-between items-center w-full z-10 p-4 bottom-0 left-0 pointer-events-none">
<div className="flex flex-col md:flex-row gap-2 pointer-events-auto">
{nativeCurrencyPrice > 0 && (
<div>
<div className="btn btn-primary btn-sm font-normal gap-1 cursor-auto">
<CurrencyDollarIcon className="h-4 w-4" />
<span>{nativeCurrencyPrice.toFixed(2)}</span>
</div>
</div>
)}
{isLocalNetwork && (
<>
<Faucet />
<Link href="/blockexplorer" passHref className="btn btn-primary btn-sm font-normal gap-1">
<MagnifyingGlassIcon className="h-4 w-4" />
<span>Block Explorer</span>
</Link>
</>
)}
</div>
<SwitchTheme className={`pointer-events-auto ${isLocalNetwork ? "self-end md:self-auto" : ""}`} />
</div>
</div>
<div className="w-full">
<ul className="menu menu-horizontal w-full">
<div className="flex justify-center items-center gap-2 text-sm w-full">
<div className="text-center">
<a href="https://github.com/scaffold-eth/se-2" target="_blank" rel="noreferrer" className="link">
Fork me
</a>
</div>
<span>·</span>
<div className="flex justify-center items-center gap-2">
<p className="m-0 text-center">
Built with <HeartIcon className="inline-block h-4 w-4" /> at
</p>
<a
className="flex justify-center items-center gap-1"
href="https://buidlguidl.com/"
target="_blank"
rel="noreferrer"
>
<BuidlGuidlLogo className="w-3 h-5 pb-1" />
<span className="link">BuidlGuidl</span>
</a>
</div>
<span>·</span>
<div className="text-center">
<a href="https://t.me/joinchat/KByvmRe5wkR-8F_zz6AjpA" target="_blank" rel="noreferrer" className="link">
Support
</a>
</div>
</div>
</ul>
</div>
</div>
);
};

View File

@@ -0,0 +1,114 @@
"use client";
import React, { useRef } from "react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { hardhat } from "viem/chains";
import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline";
import { CircleStackIcon, InboxStackIcon } from "@heroicons/react/24/outline";
import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
import { useOutsideClick, useTargetNetwork } from "~~/hooks/scaffold-eth";
type HeaderMenuLink = {
label: string;
href: string;
icon?: React.ReactNode;
};
export const menuLinks: HeaderMenuLink[] = [
{
label: "Home",
href: "/",
},
{
label: "Crowdfund",
href: "/crowdfund",
icon: <CircleStackIcon className="h-4 w-4" />,
},
{
label: "Contributions",
href: "/contributions",
icon: <InboxStackIcon className="h-4 w-4" />,
},
{
label: "Debug Contracts",
href: "/debug",
icon: <BugAntIcon className="h-4 w-4" />,
},
];
export const HeaderMenuLinks = () => {
const pathname = usePathname();
return (
<>
{menuLinks.map(({ label, href, icon }) => {
const isActive = pathname === href;
return (
<li key={href}>
<Link
href={href}
passHref
className={`${
isActive ? "bg-secondary shadow-md" : ""
} hover:bg-secondary hover:shadow-md focus:!bg-secondary active:!text-neutral py-1.5 px-3 text-sm rounded-full gap-2 grid grid-flow-col`}
>
{icon}
<span>{label}</span>
</Link>
</li>
);
})}
</>
);
};
/**
* Site header
*/
export const Header = () => {
const { targetNetwork } = useTargetNetwork();
const isLocalNetwork = targetNetwork.id === hardhat.id;
const burgerMenuRef = useRef<HTMLDetailsElement>(null);
useOutsideClick(burgerMenuRef, () => {
burgerMenuRef?.current?.removeAttribute("open");
});
return (
<div className="sticky lg:static top-0 navbar bg-base-100 min-h-0 shrink-0 justify-between z-20 shadow-md shadow-secondary px-0 sm:px-2">
<div className="navbar-start w-auto lg:w-1/2">
<details className="dropdown" ref={burgerMenuRef}>
<summary className="ml-1 btn btn-ghost lg:hidden hover:bg-transparent">
<Bars3Icon className="h-1/2" />
</summary>
<ul
className="menu menu-compact dropdown-content mt-3 p-2 shadow-sm bg-base-100 rounded-box w-52"
onClick={() => {
burgerMenuRef?.current?.removeAttribute("open");
}}
>
<HeaderMenuLinks />
</ul>
</details>
<Link href="/" passHref className="hidden lg:flex items-center gap-2 ml-4 mr-6 shrink-0">
<div className="flex relative w-10 h-10">
<Image alt="SE2 logo" className="cursor-pointer" fill src="/logo.svg" />
</div>
<div className="flex flex-col">
<span className="font-bold leading-tight">SRE Challenges</span>
<span className="text-xs">Crowdfunding App</span>
</div>
</Link>
<ul className="hidden lg:flex lg:flex-nowrap menu menu-horizontal px-1 gap-2">
<HeaderMenuLinks />
</ul>
</div>
<div className="navbar-end grow mr-4">
<RainbowKitCustomConnectButton />
{isLocalNetwork && <FaucetButton />}
</div>
</div>
);
};

View File

@@ -0,0 +1,58 @@
"use client";
import { useEffect, useState } from "react";
import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AppProgressBar as ProgressBar } from "next-nprogress-bar";
import { useTheme } from "next-themes";
import { Toaster } from "react-hot-toast";
import { WagmiProvider } from "wagmi";
import { Footer } from "~~/components/Footer";
import { Header } from "~~/components/Header";
import { BlockieAvatar } from "~~/components/scaffold-eth";
import { wagmiConfig } from "~~/services/web3/wagmiConfig";
const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
return (
<>
<div className={`flex flex-col min-h-screen `}>
<Header />
<main className="relative flex flex-col flex-1">{children}</main>
<Footer />
</div>
<Toaster />
</>
);
};
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
export const ScaffoldEthAppWithProviders = ({ children }: { children: React.ReactNode }) => {
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark";
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
avatar={BlockieAvatar}
theme={mounted ? (isDarkMode ? darkTheme() : lightTheme()) : lightTheme()}
>
<ProgressBar height="3px" color="#2299dd" />
<ScaffoldEthApp>{children}</ScaffoldEthApp>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
};

View File

@@ -0,0 +1,42 @@
"use client";
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { MoonIcon, SunIcon } from "@heroicons/react/24/outline";
export const SwitchTheme = ({ className }: { className?: string }) => {
const { setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const isDarkMode = resolvedTheme === "dark";
const handleToggle = () => {
if (isDarkMode) {
setTheme("light");
return;
}
setTheme("dark");
};
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<div className={`flex space-x-2 h-8 items-center justify-center text-sm ${className}`}>
<input
id="theme-toggle"
type="checkbox"
className="toggle bg-secondary toggle-primary hover:bg-accent transition-all"
onChange={handleToggle}
checked={isDarkMode}
/>
<label htmlFor="theme-toggle" className={`swap swap-rotate ${!isDarkMode ? "swap-active" : ""}`}>
<SunIcon className="swap-on h-5 w-5" />
<MoonIcon className="swap-off h-5 w-5" />
</label>
</div>
);
};

View File

@@ -0,0 +1,9 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
};

View File

@@ -0,0 +1,18 @@
export const BuidlGuidlLogo = ({ className }: { className: string }) => {
return (
<svg
className={className}
width="53"
height="72"
viewBox="0 0 53 72"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M25.9 17.434v15.638h3.927v9.04h9.718v-9.04h6.745v18.08l-10.607 19.88-12.11-.182-12.11.183L.856 51.152v-18.08h6.713v9.04h9.75v-9.04h4.329V2.46a2.126 2.126 0 0 1 4.047-.914c1.074.412 2.157 1.5 3.276 2.626 1.33 1.337 2.711 2.726 4.193 3.095 1.496.373 2.605-.026 3.855-.475 1.31-.47 2.776-.997 5.005-.747 1.67.197 2.557 1.289 3.548 2.509 1.317 1.623 2.82 3.473 6.599 3.752l-.024.017c-2.42 1.709-5.726 4.043-10.86 3.587-1.605-.139-2.736-.656-3.82-1.153-1.546-.707-2.997-1.37-5.59-.832-2.809.563-4.227 1.892-5.306 2.903-.236.221-.456.427-.67.606Z"
clipRule="evenodd"
/>
</svg>
);
};

View File

@@ -0,0 +1,17 @@
"use client";
import { AvatarComponent } from "@rainbow-me/rainbowkit";
import { blo } from "blo";
// Custom Avatar for RainbowKit
export const BlockieAvatar: AvatarComponent = ({ address, ensImage, size }) => (
// Don't want to use nextJS Image here (and adding remote patterns for the URL)
// eslint-disable-next-line @next/next/no-img-element
<img
className="rounded-full"
src={ensImage || blo(address as `0x${string}`)}
width={size}
height={size}
alt={`${address} avatar`}
/>
);

View File

@@ -0,0 +1,140 @@
"use client";
import { useEffect, useState } from "react";
import { Address, AddressInput, Balance, EtherInput } from "@scaffold-ui/components";
import { Address as AddressType, createWalletClient, http, parseEther } from "viem";
import { hardhat } from "viem/chains";
import { useAccount } from "wagmi";
import { BanknotesIcon } from "@heroicons/react/24/outline";
import { useTargetNetwork, useTransactor } from "~~/hooks/scaffold-eth";
import { notification } from "~~/utils/scaffold-eth";
// Account index to use from generated hardhat accounts.
const FAUCET_ACCOUNT_INDEX = 0;
const localWalletClient = createWalletClient({
chain: hardhat,
transport: http(),
});
/**
* Faucet modal which lets you send ETH to any address.
*/
export const Faucet = () => {
const [loading, setLoading] = useState(false);
const [inputAddress, setInputAddress] = useState<AddressType>();
const [faucetAddress, setFaucetAddress] = useState<AddressType>();
const [sendValue, setSendValue] = useState("");
const { targetNetwork } = useTargetNetwork();
const { chain: ConnectedChain } = useAccount();
const faucetTxn = useTransactor(localWalletClient);
useEffect(() => {
const getFaucetAddress = async () => {
try {
const accounts = await localWalletClient.getAddresses();
setFaucetAddress(accounts[FAUCET_ACCOUNT_INDEX]);
} catch (error) {
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>
</>,
);
console.error("⚡️ ~ file: Faucet.tsx:getFaucetAddress ~ error", error);
}
};
getFaucetAddress();
}, []);
const sendETH = async () => {
if (!faucetAddress || !inputAddress) {
return;
}
try {
setLoading(true);
await faucetTxn({
to: inputAddress,
value: parseEther(sendValue as `${number}`),
account: faucetAddress,
});
setLoading(false);
setInputAddress(undefined);
setSendValue("");
} catch (error) {
console.error("⚡️ ~ file: Faucet.tsx:sendETH ~ error", error);
setLoading(false);
}
};
// Render only on local chain
if (ConnectedChain?.id !== hardhat.id) {
return null;
}
return (
<div>
<label htmlFor="faucet-modal" className="btn btn-primary btn-sm font-normal gap-1">
<BanknotesIcon className="h-4 w-4" />
<span>Faucet</span>
</label>
<input type="checkbox" id="faucet-modal" className="modal-toggle" />
<label htmlFor="faucet-modal" className="modal cursor-pointer">
<label className="modal-box relative">
{/* dummy input to capture event onclick on modal box */}
<input className="h-0 w-0 absolute top-0 left-0" />
<h3 className="text-xl font-bold mb-3">Local Faucet</h3>
<label htmlFor="faucet-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
</label>
<div className="space-y-3">
<div className="flex space-x-4">
<div>
<span className="text-sm font-bold">From:</span>
<Address
address={faucetAddress}
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${faucetAddress}` : undefined
}
/>
</div>
<div>
<span className="text-sm font-bold pl-3">Available:</span>
<Balance address={faucetAddress} />
</div>
</div>
<div className="flex flex-col space-y-3">
<AddressInput
placeholder="Destination Address"
value={inputAddress ?? ""}
onChange={value => setInputAddress(value as AddressType)}
/>
<EtherInput
placeholder="Amount to send"
onValueChange={({ valueInEth }) => setSendValue(valueInEth)}
style={{ width: "100%" }}
/>
<button className="h-10 btn btn-primary btn-sm px-2 rounded-full" onClick={sendETH} disabled={loading}>
{!loading ? (
<BanknotesIcon className="h-6 w-6" />
) : (
<span className="loading loading-spinner loading-sm"></span>
)}
<span>Send</span>
</button>
</div>
</div>
</label>
</label>
</div>
);
};

View File

@@ -0,0 +1,73 @@
"use client";
import { useState } from "react";
import { useWatchBalance } from "@scaffold-ui/hooks";
import { createWalletClient, http, parseEther } from "viem";
import { hardhat } from "viem/chains";
import { useAccount } from "wagmi";
import { BanknotesIcon } from "@heroicons/react/24/outline";
import { useTransactor } from "~~/hooks/scaffold-eth";
// Number of ETH faucet sends to an address
const NUM_OF_ETH = "1";
const FAUCET_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
const localWalletClient = createWalletClient({
chain: hardhat,
transport: http(),
});
/**
* FaucetButton button which lets you grab eth.
*/
export const FaucetButton = () => {
const { address, chain: ConnectedChain } = useAccount();
const { data: balance } = useWatchBalance({ address, chain: hardhat });
const [loading, setLoading] = useState(false);
const faucetTxn = useTransactor(localWalletClient);
const sendETH = async () => {
if (!address) return;
try {
setLoading(true);
await faucetTxn({
account: FAUCET_ADDRESS,
to: address,
value: parseEther(NUM_OF_ETH),
});
setLoading(false);
} catch (error) {
console.error("⚡️ ~ file: FaucetButton.tsx:sendETH ~ error", error);
setLoading(false);
}
};
// Render only on local chain
if (ConnectedChain?.id !== hardhat.id) {
return null;
}
const isBalanceZero = balance && balance.value === 0n;
return (
<div
className={
!isBalanceZero
? "ml-1"
: "ml-1 tooltip tooltip-bottom tooltip-primary tooltip-open font-bold before:left-auto before:transform-none before:content-[attr(data-tip)] before:-translate-x-2/5"
}
data-tip="Grab funds from faucet"
>
<button className="btn btn-secondary btn-sm px-2 rounded-full" onClick={sendETH} disabled={loading}>
{!loading ? (
<BanknotesIcon className="h-4 w-4" />
) : (
<span className="loading loading-spinner loading-xs"></span>
)}
</button>
</div>
);
};

View File

@@ -0,0 +1,137 @@
import { useRef, useState } from "react";
import { NetworkOptions } from "./NetworkOptions";
import { getAddress } from "viem";
import { Address } from "viem";
import { useAccount, useDisconnect } from "wagmi";
import {
ArrowLeftOnRectangleIcon,
ArrowTopRightOnSquareIcon,
ArrowsRightLeftIcon,
CheckCircleIcon,
ChevronDownIcon,
DocumentDuplicateIcon,
EyeIcon,
QrCodeIcon,
} from "@heroicons/react/24/outline";
import { BlockieAvatar } from "~~/components/scaffold-eth";
import { useCopyToClipboard, useOutsideClick } from "~~/hooks/scaffold-eth";
import { getTargetNetworks } from "~~/utils/scaffold-eth";
import { isENS } from "~~/utils/scaffold-eth/common";
const BURNER_WALLET_ID = "burnerWallet";
const allowedNetworks = getTargetNetworks();
type AddressInfoDropdownProps = {
address: Address;
blockExplorerAddressLink: string | undefined;
displayName: string;
ensAvatar?: string;
};
export const AddressInfoDropdown = ({
address,
ensAvatar,
displayName,
blockExplorerAddressLink,
}: AddressInfoDropdownProps) => {
const { disconnect } = useDisconnect();
const { connector } = useAccount();
const checkSumAddress = getAddress(address);
const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
useCopyToClipboard();
const [selectingNetwork, setSelectingNetwork] = useState(false);
const dropdownRef = useRef<HTMLDetailsElement>(null);
const closeDropdown = () => {
setSelectingNetwork(false);
dropdownRef.current?.removeAttribute("open");
};
useOutsideClick(dropdownRef, closeDropdown);
return (
<>
<details ref={dropdownRef} className="dropdown dropdown-end leading-3">
<summary className="btn btn-secondary btn-sm pl-0 pr-2 shadow-md dropdown-toggle gap-0 h-auto!">
<BlockieAvatar address={checkSumAddress} size={30} ensImage={ensAvatar} />
<span className="ml-2 mr-1">
{isENS(displayName) ? displayName : checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4)}
</span>
<ChevronDownIcon className="h-6 w-4 ml-2 sm:ml-0" />
</summary>
<ul className="dropdown-content menu z-2 p-2 mt-2 shadow-center shadow-accent bg-base-200 rounded-box gap-1">
<NetworkOptions hidden={!selectingNetwork} />
<li className={selectingNetwork ? "hidden" : ""}>
<div
className="h-8 btn-sm rounded-xl! flex gap-3 py-3 cursor-pointer"
onClick={() => copyAddressToClipboard(checkSumAddress)}
>
{isAddressCopiedToClipboard ? (
<>
<CheckCircleIcon className="text-xl font-normal h-6 w-4 ml-2 sm:ml-0" aria-hidden="true" />
<span className="whitespace-nowrap">Copied!</span>
</>
) : (
<>
<DocumentDuplicateIcon className="text-xl font-normal h-6 w-4 ml-2 sm:ml-0" aria-hidden="true" />
<span className="whitespace-nowrap">Copy address</span>
</>
)}
</div>
</li>
<li className={selectingNetwork ? "hidden" : ""}>
<label htmlFor="qrcode-modal" className="h-8 btn-sm rounded-xl! flex gap-3 py-3">
<QrCodeIcon className="h-6 w-4 ml-2 sm:ml-0" />
<span className="whitespace-nowrap">View QR Code</span>
</label>
</li>
<li className={selectingNetwork ? "hidden" : ""}>
<button className="h-8 btn-sm rounded-xl! flex gap-3 py-3" type="button">
<ArrowTopRightOnSquareIcon className="h-6 w-4 ml-2 sm:ml-0" />
<a
target="_blank"
href={blockExplorerAddressLink}
rel="noopener noreferrer"
className="whitespace-nowrap"
>
View on Block Explorer
</a>
</button>
</li>
{allowedNetworks.length > 1 ? (
<li className={selectingNetwork ? "hidden" : ""}>
<button
className="h-8 btn-sm rounded-xl! flex gap-3 py-3"
type="button"
onClick={() => {
setSelectingNetwork(true);
}}
>
<ArrowsRightLeftIcon className="h-6 w-4 ml-2 sm:ml-0" /> <span>Switch Network</span>
</button>
</li>
) : null}
{connector?.id === BURNER_WALLET_ID ? (
<li>
<label htmlFor="reveal-burner-pk-modal" className="h-8 btn-sm rounded-xl! flex gap-3 py-3 text-error">
<EyeIcon className="h-6 w-4 ml-2 sm:ml-0" />
<span>Reveal Private Key</span>
</label>
</li>
) : null}
<li className={selectingNetwork ? "hidden" : ""}>
<button
className="menu-item text-error h-8 btn-sm rounded-xl! flex gap-3 py-3"
type="button"
onClick={() => disconnect()}
>
<ArrowLeftOnRectangleIcon className="h-6 w-4 ml-2 sm:ml-0" /> <span>Disconnect</span>
</button>
</li>
</ul>
</details>
</>
);
};

View File

@@ -0,0 +1,44 @@
import { Address } from "@scaffold-ui/components";
import { QRCodeSVG } from "qrcode.react";
import { Address as AddressType } from "viem";
import { hardhat } from "viem/chains";
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
type AddressQRCodeModalProps = {
address: AddressType;
modalId: string;
};
export const AddressQRCodeModal = ({ address, modalId }: AddressQRCodeModalProps) => {
const { targetNetwork } = useTargetNetwork();
return (
<>
<div>
<input type="checkbox" id={`${modalId}`} className="modal-toggle" />
<label htmlFor={`${modalId}`} className="modal cursor-pointer">
<label className="modal-box relative">
{/* dummy input to capture event onclick on modal box */}
<input className="h-0 w-0 absolute top-0 left-0" />
<label htmlFor={`${modalId}`} className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
</label>
<div className="space-y-3 py-6">
<div className="flex flex-col items-center gap-6">
<QRCodeSVG value={address} size={256} />
<Address
address={address}
format="long"
disableAddressLink
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${address}` : undefined
}
/>
</div>
</div>
</label>
</label>
</div>
</>
);
};

View File

@@ -0,0 +1,48 @@
import { useTheme } from "next-themes";
import { useAccount, useSwitchChain } from "wagmi";
import { ArrowsRightLeftIcon } from "@heroicons/react/24/solid";
import { getNetworkColor } from "~~/hooks/scaffold-eth";
import { getTargetNetworks } from "~~/utils/scaffold-eth";
const allowedNetworks = getTargetNetworks();
type NetworkOptionsProps = {
hidden?: boolean;
};
export const NetworkOptions = ({ hidden = false }: NetworkOptionsProps) => {
const { switchChain } = useSwitchChain();
const { chain } = useAccount();
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark";
return (
<>
{allowedNetworks
.filter(allowedNetwork => allowedNetwork.id !== chain?.id)
.map(allowedNetwork => (
<li key={allowedNetwork.id} className={hidden ? "hidden" : ""}>
<button
className="menu-item btn-sm rounded-xl! flex gap-3 py-3 whitespace-nowrap"
type="button"
onClick={() => {
switchChain?.({ chainId: allowedNetwork.id });
}}
>
<ArrowsRightLeftIcon className="h-6 w-4 ml-2 sm:ml-0" />
<span>
Switch to{" "}
<span
style={{
color: getNetworkColor(allowedNetwork, isDarkMode),
}}
>
{allowedNetwork.name}
</span>
</span>
</button>
</li>
))}
</>
);
};

View File

@@ -0,0 +1,59 @@
import { useRef } from "react";
import { rainbowkitBurnerWallet } from "burner-connector";
import { ShieldExclamationIcon } from "@heroicons/react/24/outline";
import { useCopyToClipboard } from "~~/hooks/scaffold-eth";
import { getParsedError, notification } from "~~/utils/scaffold-eth";
const BURNER_WALLET_PK_KEY = "burnerWallet.pk";
export const RevealBurnerPKModal = () => {
const { copyToClipboard, isCopiedToClipboard } = useCopyToClipboard();
const modalCheckboxRef = useRef<HTMLInputElement>(null);
const handleCopyPK = async () => {
try {
const storage = rainbowkitBurnerWallet.useSessionStorage ? sessionStorage : localStorage;
const burnerPK = storage?.getItem(BURNER_WALLET_PK_KEY);
if (!burnerPK) throw new Error("Burner wallet private key not found");
await copyToClipboard(burnerPK);
notification.success("Burner wallet private key copied to clipboard");
} catch (e) {
const parsedError = getParsedError(e);
notification.error(parsedError);
if (modalCheckboxRef.current) modalCheckboxRef.current.checked = false;
}
};
return (
<>
<div>
<input type="checkbox" id="reveal-burner-pk-modal" className="modal-toggle" ref={modalCheckboxRef} />
<label htmlFor="reveal-burner-pk-modal" className="modal cursor-pointer">
<label className="modal-box relative">
{/* dummy input to capture event onclick on modal box */}
<input className="h-0 w-0 absolute top-0 left-0" />
<label htmlFor="reveal-burner-pk-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
</label>
<div>
<p className="text-lg font-semibold m-0 p-0">Copy Burner Wallet Private Key</p>
<div role="alert" className="alert alert-warning mt-4">
<ShieldExclamationIcon className="h-6 w-6" />
<span className="font-semibold">
Burner wallets are intended for local development only and are not safe for storing real funds.
</span>
</div>
<p>
Your Private Key provides <strong>full access</strong> to your entire wallet and funds. This is
currently stored <strong>temporarily</strong> in your browser.
</p>
<button className="btn btn-outline btn-error" onClick={handleCopyPK} disabled={isCopiedToClipboard}>
Copy Private Key To Clipboard
</button>
</div>
</label>
</label>
</div>
</>
);
};

View File

@@ -0,0 +1,32 @@
import { NetworkOptions } from "./NetworkOptions";
import { useDisconnect } from "wagmi";
import { ArrowLeftOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
export const WrongNetworkDropdown = () => {
const { disconnect } = useDisconnect();
return (
<div className="dropdown dropdown-end mr-2">
<label tabIndex={0} className="btn btn-error btn-sm dropdown-toggle gap-1">
<span>Wrong network</span>
<ChevronDownIcon className="h-6 w-4 ml-2 sm:ml-0" />
</label>
<ul
tabIndex={0}
className="dropdown-content menu p-2 mt-1 shadow-center shadow-accent bg-base-200 rounded-box gap-1"
>
<NetworkOptions />
<li>
<button
className="menu-item text-error btn-sm rounded-xl! flex gap-3 py-3"
type="button"
onClick={() => disconnect()}
>
<ArrowLeftOnRectangleIcon className="h-6 w-4 ml-2 sm:ml-0" />
<span>Disconnect</span>
</button>
</li>
</ul>
</div>
);
};

View File

@@ -0,0 +1,76 @@
"use client";
// @refresh reset
import { AddressInfoDropdown } from "./AddressInfoDropdown";
import { AddressQRCodeModal } from "./AddressQRCodeModal";
import { RevealBurnerPKModal } from "./RevealBurnerPKModal";
import { WrongNetworkDropdown } from "./WrongNetworkDropdown";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { Balance } from "@scaffold-ui/components";
import { Address } from "viem";
import { useNetworkColor } from "~~/hooks/scaffold-eth";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";
/**
* Custom Wagmi Connect Button (watch balance + custom design)
*/
export const RainbowKitCustomConnectButton = () => {
const networkColor = useNetworkColor();
const { targetNetwork } = useTargetNetwork();
return (
<ConnectButton.Custom>
{({ account, chain, openConnectModal, mounted }) => {
const connected = mounted && account && chain;
const blockExplorerAddressLink = account
? getBlockExplorerAddressLink(targetNetwork, account.address)
: undefined;
return (
<>
{(() => {
if (!connected) {
return (
<button className="btn btn-primary btn-sm" onClick={openConnectModal} type="button">
Connect Wallet
</button>
);
}
if (chain.unsupported || chain.id !== targetNetwork.id) {
return <WrongNetworkDropdown />;
}
return (
<>
<div className="flex flex-col items-center mr-2">
<Balance
address={account.address as Address}
style={{
minHeight: "0",
height: "auto",
fontSize: "0.8em",
}}
/>
<span className="text-xs" style={{ color: networkColor }}>
{chain.name}
</span>
</div>
<AddressInfoDropdown
address={account.address as Address}
displayName={account.displayName}
ensAvatar={account.ensAvatar}
blockExplorerAddressLink={blockExplorerAddressLink}
/>
<AddressQRCodeModal address={account.address as Address} modalId="qrcode-modal" />
<RevealBurnerPKModal />
</>
);
})()}
</>
);
}}
</ConnectButton.Custom>
);
};

View File

@@ -0,0 +1,4 @@
export * from "./BlockieAvatar";
export * from "./Faucet";
export * from "./FaucetButton";
export * from "./RainbowKitCustomConnectButton";

View File

@@ -0,0 +1,9 @@
/**
* This file is autogenerated by Scaffold-ETH.
* You should not edit it manually or your changes might be overwritten.
*/
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
const deployedContracts = {} as const;
export default deployedContracts satisfies GenericContractsDeclaration;

View File

@@ -0,0 +1,16 @@
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
/**
* @example
* const externalContracts = {
* 1: {
* DAI: {
* address: "0x...",
* abi: [...],
* },
* },
* } as const;
*/
const externalContracts = {} as const;
export default externalContracts satisfies GenericContractsDeclaration;

View File

@@ -0,0 +1,32 @@
import { FlatCompat } from "@eslint/eslintrc";
import prettierPlugin from "eslint-plugin-prettier";
import { defineConfig } from "eslint/config";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
export default defineConfig([
{
plugins: {
prettier: prettierPlugin,
},
extends: compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"prettier/prettier": [
"warn",
{
endOfLine: "auto",
},
],
},
},
]);

View File

@@ -0,0 +1,14 @@
export * from "./useContractLogs";
export * from "./useCopyToClipboard";
export * from "./useDeployedContractInfo";
export * from "./useFetchBlocks";
export * from "./useNetworkColor";
export * from "./useOutsideClick";
export * from "./useScaffoldContract";
export * from "./useScaffoldEventHistory";
export * from "./useScaffoldReadContract";
export * from "./useScaffoldWatchContractEvent";
export * from "./useScaffoldWriteContract";
export * from "./useTargetNetwork";
export * from "./useTransactor";
export * from "./useSelectedNetwork";

View File

@@ -0,0 +1,40 @@
import { useEffect, useState } from "react";
import { useTargetNetwork } from "./useTargetNetwork";
import { Address, Log } from "viem";
import { usePublicClient } from "wagmi";
export const useContractLogs = (address: Address) => {
const [logs, setLogs] = useState<Log[]>([]);
const { targetNetwork } = useTargetNetwork();
const client = usePublicClient({ chainId: targetNetwork.id });
useEffect(() => {
const fetchLogs = async () => {
if (!client) return console.error("Client not found");
try {
const existingLogs = await client.getLogs({
address: address,
fromBlock: 0n,
toBlock: "latest",
});
setLogs(existingLogs);
} catch (error) {
console.error("Failed to fetch logs:", error);
}
};
fetchLogs();
return client?.watchBlockNumber({
onBlockNumber: async (_blockNumber, prevBlockNumber) => {
const newLogs = await client.getLogs({
address: address,
fromBlock: prevBlockNumber,
toBlock: "latest",
});
setLogs(prevLogs => [...prevLogs, ...newLogs]);
},
});
}, [address, client]);
return logs;
};

View File

@@ -0,0 +1,19 @@
import { useState } from "react";
export const useCopyToClipboard = () => {
const [isCopiedToClipboard, setIsCopiedToClipboard] = useState(false);
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setIsCopiedToClipboard(true);
setTimeout(() => {
setIsCopiedToClipboard(false);
}, 800);
} catch (err) {
console.error("Failed to copy text:", err);
}
};
return { copyToClipboard, isCopiedToClipboard };
};

View File

@@ -0,0 +1,86 @@
import { useEffect, useState } from "react";
import { useIsMounted } from "usehooks-ts";
import { usePublicClient } from "wagmi";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import {
Contract,
ContractCodeStatus,
ContractName,
UseDeployedContractConfig,
contracts,
} from "~~/utils/scaffold-eth/contract";
type DeployedContractData<TContractName extends ContractName> = {
data: Contract<TContractName> | undefined;
isLoading: boolean;
};
/**
* Gets the matching contract info for the provided contract name from the contracts present in deployedContracts.ts
* and externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
*/
export function useDeployedContractInfo<TContractName extends ContractName>(
config: UseDeployedContractConfig<TContractName>,
): DeployedContractData<TContractName>;
/**
* @deprecated Use object parameter version instead: useDeployedContractInfo({ contractName: "YourContract" })
*/
export function useDeployedContractInfo<TContractName extends ContractName>(
contractName: TContractName,
): DeployedContractData<TContractName>;
export function useDeployedContractInfo<TContractName extends ContractName>(
configOrName: UseDeployedContractConfig<TContractName> | TContractName,
): DeployedContractData<TContractName> {
const isMounted = useIsMounted();
const finalConfig: UseDeployedContractConfig<TContractName> =
typeof configOrName === "string" ? { contractName: configOrName } : (configOrName as any);
useEffect(() => {
if (typeof configOrName === "string") {
console.warn(
"Using `useDeployedContractInfo` with a string parameter is deprecated. Please use the object parameter version instead.",
);
}
}, [configOrName]);
const { contractName, chainId } = finalConfig;
const selectedNetwork = useSelectedNetwork(chainId);
const deployedContract = contracts?.[selectedNetwork.id]?.[contractName as ContractName] as Contract<TContractName>;
const [status, setStatus] = useState<ContractCodeStatus>(ContractCodeStatus.LOADING);
const publicClient = usePublicClient({ chainId: selectedNetwork.id });
useEffect(() => {
const checkContractDeployment = async () => {
try {
if (!isMounted() || !publicClient) return;
if (!deployedContract) {
setStatus(ContractCodeStatus.NOT_FOUND);
return;
}
const code = await publicClient.getBytecode({
address: deployedContract.address,
});
// If contract code is `0x` => no contract deployed on that address
if (code === "0x") {
setStatus(ContractCodeStatus.NOT_FOUND);
return;
}
setStatus(ContractCodeStatus.DEPLOYED);
} catch (e) {
console.error(e);
setStatus(ContractCodeStatus.NOT_FOUND);
}
};
checkContractDeployment();
}, [isMounted, contractName, deployedContract, publicClient]);
return {
data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined,
isLoading: status === ContractCodeStatus.LOADING,
};
}

View File

@@ -0,0 +1,133 @@
import { useCallback, useEffect, useState } from "react";
import {
Block,
Hash,
Transaction,
TransactionReceipt,
createTestClient,
publicActions,
walletActions,
webSocket,
} from "viem";
import { hardhat } from "viem/chains";
import { decodeTransactionData } from "~~/utils/scaffold-eth";
const BLOCKS_PER_PAGE = 20;
export const testClient = createTestClient({
chain: hardhat,
mode: "hardhat",
transport: webSocket("ws://127.0.0.1:8545"),
})
.extend(publicActions)
.extend(walletActions);
export const useFetchBlocks = () => {
const [blocks, setBlocks] = useState<Block[]>([]);
const [transactionReceipts, setTransactionReceipts] = useState<{
[key: string]: TransactionReceipt;
}>({});
const [currentPage, setCurrentPage] = useState(0);
const [totalBlocks, setTotalBlocks] = useState(0n);
const [error, setError] = useState<Error | null>(null);
const fetchBlocks = useCallback(async () => {
setError(null);
try {
const blockNumber = await testClient.getBlockNumber();
setTotalBlocks(blockNumber);
const startingBlock = blockNumber - BigInt(currentPage * BLOCKS_PER_PAGE);
const blockNumbersToFetch = Array.from(
{ length: Number(BLOCKS_PER_PAGE < startingBlock + 1n ? BLOCKS_PER_PAGE : startingBlock + 1n) },
(_, i) => startingBlock - BigInt(i),
);
const blocksWithTransactions = blockNumbersToFetch.map(async blockNumber => {
try {
return testClient.getBlock({ blockNumber, includeTransactions: true });
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred."));
throw err;
}
});
const fetchedBlocks = await Promise.all(blocksWithTransactions);
fetchedBlocks.forEach(block => {
block.transactions.forEach(tx => decodeTransactionData(tx as Transaction));
});
const txReceipts = await Promise.all(
fetchedBlocks.flatMap(block =>
block.transactions.map(async tx => {
try {
const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
return { [(tx as Transaction).hash]: receipt };
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred."));
throw err;
}
}),
),
);
setBlocks(fetchedBlocks);
setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...txReceipts) }));
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred."));
}
}, [currentPage]);
useEffect(() => {
fetchBlocks();
}, [fetchBlocks]);
useEffect(() => {
const handleNewBlock = async (newBlock: any) => {
try {
if (currentPage === 0) {
if (newBlock.transactions.length > 0) {
const transactionsDetails = await Promise.all(
newBlock.transactions.map((txHash: string) => testClient.getTransaction({ hash: txHash as Hash })),
);
newBlock.transactions = transactionsDetails;
}
newBlock.transactions.forEach((tx: Transaction) => decodeTransactionData(tx as Transaction));
const receipts = await Promise.all(
newBlock.transactions.map(async (tx: Transaction) => {
try {
const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
return { [(tx as Transaction).hash]: receipt };
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred fetching receipt."));
throw err;
}
}),
);
setBlocks(prevBlocks => [newBlock, ...prevBlocks.slice(0, BLOCKS_PER_PAGE - 1)]);
setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...receipts) }));
}
if (newBlock.number) {
setTotalBlocks(newBlock.number);
}
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred."));
}
};
return testClient.watchBlocks({ onBlock: handleNewBlock, includeTransactions: true });
}, [currentPage]);
return {
blocks,
transactionReceipts,
currentPage,
totalBlocks,
setCurrentPage,
error,
};
};

View File

@@ -0,0 +1,22 @@
import { useTheme } from "next-themes";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { AllowedChainIds, ChainWithAttributes } from "~~/utils/scaffold-eth";
export const DEFAULT_NETWORK_COLOR: [string, string] = ["#666666", "#bbbbbb"];
export function getNetworkColor(network: ChainWithAttributes, isDarkMode: boolean) {
const colorConfig = network.color ?? DEFAULT_NETWORK_COLOR;
return Array.isArray(colorConfig) ? (isDarkMode ? colorConfig[1] : colorConfig[0]) : colorConfig;
}
/**
* Gets the color of the target network
*/
export const useNetworkColor = (chainId?: AllowedChainIds) => {
const { resolvedTheme } = useTheme();
const chain = useSelectedNetwork(chainId);
const isDarkMode = resolvedTheme === "dark";
return getNetworkColor(chain, isDarkMode);
};

View File

@@ -0,0 +1,23 @@
import React, { useEffect } from "react";
/**
* Handles clicks outside of passed ref element
* @param ref - react ref of the element
* @param callback - callback function to call when clicked outside
*/
export const useOutsideClick = (ref: React.RefObject<HTMLElement | null>, callback: { (): void }) => {
useEffect(() => {
function handleOutsideClick(event: MouseEvent) {
if (!(event.target instanceof Element)) {
return;
}
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
}
document.addEventListener("click", handleOutsideClick);
return () => document.removeEventListener("click", handleOutsideClick);
}, [ref, callback]);
};

View File

@@ -0,0 +1,65 @@
import { Account, Address, Chain, Client, Transport, getContract } from "viem";
import { usePublicClient } from "wagmi";
import { GetWalletClientReturnType } from "wagmi/actions";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import { Contract, ContractName } from "~~/utils/scaffold-eth/contract";
/**
* Gets a viem instance of the contract present in deployedContracts.ts or externalContracts.ts corresponding to
* targetNetworks configured in scaffold.config.ts. Optional walletClient can be passed for doing write transactions.
* @param config - The config settings for the hook
* @param config.contractName - deployed contract name
* @param config.walletClient - optional walletClient from wagmi useWalletClient hook can be passed for doing write transactions
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
*/
export const useScaffoldContract = <
TContractName extends ContractName,
TWalletClient extends Exclude<GetWalletClientReturnType, null> | undefined,
>({
contractName,
walletClient,
chainId,
}: {
contractName: TContractName;
walletClient?: TWalletClient | null;
chainId?: AllowedChainIds;
}) => {
const selectedNetwork = useSelectedNetwork(chainId);
const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork?.id as AllowedChainIds,
});
const publicClient = usePublicClient({ chainId: selectedNetwork?.id });
let contract = undefined;
if (deployedContractData && publicClient) {
contract = getContract<
Transport,
Address,
Contract<TContractName>["abi"],
TWalletClient extends Exclude<GetWalletClientReturnType, null>
? {
public: Client<Transport, Chain>;
wallet: TWalletClient;
}
: { public: Client<Transport, Chain> },
Chain,
Account
>({
address: deployedContractData.address,
abi: deployedContractData.abi as Contract<TContractName>["abi"],
client: {
public: publicClient,
wallet: walletClient ? walletClient : undefined,
} as any,
});
}
return {
data: contract,
isLoading: deployedContractLoading,
};
};

View File

@@ -0,0 +1,292 @@
import { useEffect, useState } from "react";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { Abi, AbiEvent, ExtractAbiEventNames } from "abitype";
import { BlockNumber, GetLogsParameters } from "viem";
import { hardhat } from "viem/chains";
import { Config, UsePublicClientReturnType, useBlockNumber, usePublicClient } from "wagmi";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import { replacer } from "~~/utils/scaffold-eth/common";
import {
ContractAbi,
ContractName,
UseScaffoldEventHistoryConfig,
UseScaffoldEventHistoryData,
} from "~~/utils/scaffold-eth/contract";
const getEvents = async (
getLogsParams: GetLogsParameters<AbiEvent | undefined, AbiEvent[] | undefined, boolean, BlockNumber, BlockNumber>,
publicClient?: UsePublicClientReturnType<Config, number>,
Options?: {
blockData?: boolean;
transactionData?: boolean;
receiptData?: boolean;
},
) => {
const logs = await publicClient?.getLogs({
address: getLogsParams.address,
fromBlock: getLogsParams.fromBlock,
toBlock: getLogsParams.toBlock,
args: getLogsParams.args,
event: getLogsParams.event,
});
if (!logs) return undefined;
const finalEvents = await Promise.all(
logs.map(async log => {
return {
...log,
blockData:
Options?.blockData && log.blockHash ? await publicClient?.getBlock({ blockHash: log.blockHash }) : null,
transactionData:
Options?.transactionData && log.transactionHash
? await publicClient?.getTransaction({ hash: log.transactionHash })
: null,
receiptData:
Options?.receiptData && log.transactionHash
? await publicClient?.getTransactionReceipt({ hash: log.transactionHash })
: null,
};
}),
);
return finalEvents;
};
/**
* @deprecated **Recommended only for local (hardhat/anvil) chains and development.**
* It uses getLogs which can overload RPC endpoints (especially on L2s with short block times).
* For production, use an indexer such as ponder.sh or similar to query contract events efficiently.
*
* Reads events from a deployed contract.
* @param config - The config settings
* @param config.contractName - deployed contract name
* @param config.eventName - name of the event to listen for
* @param config.fromBlock - optional block number to start reading events from (defaults to `deployedOnBlock` in deployedContracts.ts if set for contract, otherwise defaults to 0)
* @param config.toBlock - optional block number to stop reading events at (if not provided, reads until current block)
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
* @param config.filters - filters to be applied to the event (parameterName: value)
* @param config.blockData - if set to true it will return the block data for each event (default: false)
* @param config.transactionData - if set to true it will return the transaction data for each event (default: false)
* @param config.receiptData - if set to true it will return the receipt data for each event (default: false)
* @param config.watch - if set to true, the events will be updated every pollingInterval milliseconds set at scaffoldConfig (default: false)
* @param config.enabled - set this to false to disable the hook from running (default: true)
* @param config.blocksBatchSize - optional batch size for fetching events. If specified, each batch will contain at most this many blocks (default: 500)
*/
export const useScaffoldEventHistory = <
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
TBlockData extends boolean = false,
TTransactionData extends boolean = false,
TReceiptData extends boolean = false,
>({
contractName,
eventName,
fromBlock,
toBlock,
chainId,
filters,
blockData,
transactionData,
receiptData,
watch,
enabled = true,
blocksBatchSize = 500,
}: UseScaffoldEventHistoryConfig<TContractName, TEventName, TBlockData, TTransactionData, TReceiptData>) => {
const selectedNetwork = useSelectedNetwork(chainId);
// Runtime warning for non-local chains
useEffect(() => {
if (selectedNetwork.id !== hardhat.id) {
console.log(
"⚠️ useScaffoldEventHistory is not optimized for production use. It can overload RPC endpoints (especially on L2s)",
);
}
}, [selectedNetwork.id]);
const publicClient = usePublicClient({
chainId: selectedNetwork.id,
});
const [liveEvents, setLiveEvents] = useState<any[]>([]);
const [lastFetchedBlock, setLastFetchedBlock] = useState<bigint | null>(null);
const [isPollingActive, setIsPollingActive] = useState(false);
const { data: blockNumber } = useBlockNumber({ watch: watch, chainId: selectedNetwork.id });
const { data: deployedContractData } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork.id as AllowedChainIds,
});
const event =
deployedContractData &&
((deployedContractData.abi as Abi).find(part => part.type === "event" && part.name === eventName) as AbiEvent);
const isContractAddressAndClientReady = Boolean(deployedContractData?.address) && Boolean(publicClient);
const fromBlockValue =
fromBlock !== undefined
? fromBlock
: BigInt(
deployedContractData && "deployedOnBlock" in deployedContractData
? deployedContractData.deployedOnBlock || 0
: 0,
);
const query = useInfiniteQuery({
queryKey: [
"eventHistory",
{
contractName,
address: deployedContractData?.address,
eventName,
fromBlock: fromBlockValue?.toString(),
toBlock: toBlock?.toString(),
chainId: selectedNetwork.id,
filters: JSON.stringify(filters, replacer),
blocksBatchSize: blocksBatchSize.toString(),
},
],
queryFn: async ({ pageParam }) => {
if (!isContractAddressAndClientReady) return undefined;
// Calculate the toBlock for this batch
let batchToBlock = toBlock;
const batchEndBlock = pageParam + BigInt(blocksBatchSize) - 1n;
const maxBlock = toBlock || (blockNumber ? BigInt(blockNumber) : undefined);
if (maxBlock) {
batchToBlock = batchEndBlock < maxBlock ? batchEndBlock : maxBlock;
}
const data = await getEvents(
{
address: deployedContractData?.address,
event,
fromBlock: pageParam,
toBlock: batchToBlock,
args: filters,
},
publicClient,
{ blockData, transactionData, receiptData },
);
setLastFetchedBlock(batchToBlock || blockNumber || 0n);
return data;
},
enabled: enabled && isContractAddressAndClientReady && !isPollingActive, // Disable when polling starts
initialPageParam: fromBlockValue,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (!blockNumber || fromBlockValue >= blockNumber) return undefined;
const nextBlock = lastPageParam + BigInt(blocksBatchSize);
// Don't go beyond the specified toBlock or current block
const maxBlock = toBlock && toBlock < blockNumber ? toBlock : blockNumber;
if (nextBlock > maxBlock) return undefined;
return nextBlock;
},
select: data => {
const events = data.pages.flat() as unknown as UseScaffoldEventHistoryData<
TContractName,
TEventName,
TBlockData,
TTransactionData,
TReceiptData
>;
return {
pages: events?.reverse(),
pageParams: data.pageParams,
};
},
});
// Check if we're caught up and should start polling
const shouldStartPolling = () => {
if (!watch || !blockNumber || isPollingActive) return false;
return !query.hasNextPage && query.status === "success";
};
// Poll for new events when watch mode is enabled
useQuery({
queryKey: ["liveEvents", contractName, eventName, blockNumber?.toString(), lastFetchedBlock?.toString()],
enabled: Boolean(
watch && enabled && isContractAddressAndClientReady && blockNumber && (shouldStartPolling() || isPollingActive),
),
queryFn: async () => {
if (!isContractAddressAndClientReady || !blockNumber) return null;
if (!isPollingActive && shouldStartPolling()) {
setIsPollingActive(true);
}
const maxBlock = toBlock && toBlock < blockNumber ? toBlock : blockNumber;
const startBlock = lastFetchedBlock || maxBlock;
// Only fetch if there are new blocks to check
if (startBlock >= maxBlock) return null;
const newEvents = await getEvents(
{
address: deployedContractData?.address,
event,
fromBlock: startBlock + 1n,
toBlock: maxBlock,
args: filters,
},
publicClient,
{ blockData, transactionData, receiptData },
);
if (newEvents && newEvents.length > 0) {
setLiveEvents(prev => [...newEvents, ...prev]);
}
setLastFetchedBlock(maxBlock);
return newEvents;
},
refetchInterval: false,
});
// Manual trigger to fetch next page when previous page completes (only when not polling)
useEffect(() => {
if (
!isPollingActive &&
query.status === "success" &&
query.hasNextPage &&
!query.isFetchingNextPage &&
!query.error
) {
query.fetchNextPage();
}
}, [query, isPollingActive]);
// Combine historical data from infinite query with live events from watch hook
const historicalEvents = query.data?.pages || [];
const allEvents = [...liveEvents, ...historicalEvents] as typeof historicalEvents;
// remove duplicates
const seenEvents = new Set<string>();
const combinedEvents = allEvents.filter(event => {
const eventKey = `${event?.transactionHash}-${event?.logIndex}-${event?.blockHash}`;
if (seenEvents.has(eventKey)) {
return false;
}
seenEvents.add(eventKey);
return true;
}) as typeof historicalEvents;
return {
data: combinedEvents,
status: query.status,
error: query.error,
isLoading: query.isLoading,
isFetchingNewEvent: query.isFetchingNextPage,
refetch: query.refetch,
};
};

View File

@@ -0,0 +1,80 @@
import { useEffect } from "react";
import { QueryObserverResult, RefetchOptions, useQueryClient } from "@tanstack/react-query";
import type { ExtractAbiFunctionNames } from "abitype";
import { ReadContractErrorType } from "viem";
import { useBlockNumber, useReadContract } from "wagmi";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import {
AbiFunctionReturnType,
ContractAbi,
ContractName,
UseScaffoldReadConfig,
} from "~~/utils/scaffold-eth/contract";
/**
* Wrapper around wagmi's useContractRead hook which automatically loads (by name) the contract ABI and address from
* the contracts present in deployedContracts.ts & externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
* @param config - The config settings, including extra wagmi configuration
* @param config.contractName - deployed contract name
* @param config.functionName - name of the function to be called
* @param config.args - args to be passed to the function call
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
*/
export const useScaffoldReadContract = <
TContractName extends ContractName,
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "pure" | "view">,
>({
contractName,
functionName,
args,
chainId,
...readConfig
}: UseScaffoldReadConfig<TContractName, TFunctionName>) => {
const selectedNetwork = useSelectedNetwork(chainId);
const { data: deployedContract } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork.id as AllowedChainIds,
});
const { query: queryOptions, watch, ...readContractConfig } = readConfig;
// set watch to true by default
const defaultWatch = watch ?? true;
const readContractHookRes = useReadContract({
chainId: selectedNetwork.id,
functionName,
address: deployedContract?.address,
abi: deployedContract?.abi,
args,
...(readContractConfig as any),
query: {
enabled: !Array.isArray(args) || !args.some(arg => arg === undefined),
...queryOptions,
},
}) as Omit<ReturnType<typeof useReadContract>, "data" | "refetch"> & {
data: AbiFunctionReturnType<ContractAbi, TFunctionName> | undefined;
refetch: (
options?: RefetchOptions | undefined,
) => Promise<QueryObserverResult<AbiFunctionReturnType<ContractAbi, TFunctionName>, ReadContractErrorType>>;
};
const queryClient = useQueryClient();
const { data: blockNumber } = useBlockNumber({
watch: defaultWatch,
chainId: selectedNetwork.id,
query: {
enabled: defaultWatch,
},
});
useEffect(() => {
if (defaultWatch) {
queryClient.invalidateQueries({ queryKey: readContractHookRes.queryKey });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [blockNumber]);
return readContractHookRes;
};

View File

@@ -0,0 +1,40 @@
import { Abi, ExtractAbiEventNames } from "abitype";
import { Log } from "viem";
import { useWatchContractEvent } from "wagmi";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import { ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaffold-eth/contract";
/**
* Wrapper around wagmi's useEventSubscriber hook which automatically loads (by name) the contract ABI and
* address from the contracts present in deployedContracts.ts & externalContracts.ts
* @param config - The config settings
* @param config.contractName - deployed contract name
* @param config.eventName - name of the event to listen for
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
* @param config.onLogs - the callback that receives events.
*/
export const useScaffoldWatchContractEvent = <
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
>({
contractName,
eventName,
chainId,
onLogs,
}: UseScaffoldEventConfig<TContractName, TEventName>) => {
const selectedNetwork = useSelectedNetwork(chainId);
const { data: deployedContractData } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork.id as AllowedChainIds,
});
return useWatchContractEvent({
address: deployedContractData?.address,
abi: deployedContractData?.abi as Abi,
chainId: selectedNetwork.id,
onLogs: (logs: Log[]) => onLogs(logs as Parameters<typeof onLogs>[0]),
eventName,
});
};

View File

@@ -0,0 +1,194 @@
import { useEffect, useState } from "react";
import { MutateOptions } from "@tanstack/react-query";
import { Abi, ExtractAbiFunctionNames } from "abitype";
import { Config, UseWriteContractParameters, useAccount, useConfig, useWriteContract } from "wagmi";
import { WriteContractErrorType, WriteContractReturnType } from "wagmi/actions";
import { WriteContractVariables } from "wagmi/query";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo, useTransactor } from "~~/hooks/scaffold-eth";
import { AllowedChainIds, notification } from "~~/utils/scaffold-eth";
import {
ContractAbi,
ContractName,
ScaffoldWriteContractOptions,
ScaffoldWriteContractVariables,
UseScaffoldWriteConfig,
simulateContractWriteAndNotifyError,
} from "~~/utils/scaffold-eth/contract";
type ScaffoldWriteContractReturnType<TContractName extends ContractName> = Omit<
ReturnType<typeof useWriteContract>,
"writeContract" | "writeContractAsync"
> & {
isMining: boolean;
writeContractAsync: <
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">,
>(
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
options?: ScaffoldWriteContractOptions,
) => Promise<WriteContractReturnType | undefined>;
writeContract: <TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">>(
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
options?: Omit<ScaffoldWriteContractOptions, "onBlockConfirmation" | "blockConfirmations">,
) => void;
};
export function useScaffoldWriteContract<TContractName extends ContractName>(
config: UseScaffoldWriteConfig<TContractName>,
): ScaffoldWriteContractReturnType<TContractName>;
/**
* @deprecated Use object parameter version instead: useScaffoldWriteContract({ contractName: "YourContract" })
*/
export function useScaffoldWriteContract<TContractName extends ContractName>(
contractName: TContractName,
writeContractParams?: UseWriteContractParameters,
): ScaffoldWriteContractReturnType<TContractName>;
/**
* Wrapper around wagmi's useWriteContract hook which automatically loads (by name) the contract ABI and address from
* the contracts present in deployedContracts.ts & externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
* @param contractName - name of the contract to be written to
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
* @param writeContractParams - wagmi's useWriteContract parameters
*/
export function useScaffoldWriteContract<TContractName extends ContractName>(
configOrName: UseScaffoldWriteConfig<TContractName> | TContractName,
writeContractParams?: UseWriteContractParameters,
): ScaffoldWriteContractReturnType<TContractName> {
const finalConfig =
typeof configOrName === "string"
? { contractName: configOrName, writeContractParams, chainId: undefined }
: (configOrName as UseScaffoldWriteConfig<TContractName>);
const { contractName, chainId, writeContractParams: finalWriteContractParams } = finalConfig;
const wagmiConfig = useConfig();
useEffect(() => {
if (typeof configOrName === "string") {
console.warn(
"Using `useScaffoldWriteContract` with a string parameter is deprecated. Please use the object parameter version instead.",
);
}
}, [configOrName]);
const { chain: accountChain } = useAccount();
const writeTx = useTransactor();
const [isMining, setIsMining] = useState(false);
const wagmiContractWrite = useWriteContract(finalWriteContractParams);
const selectedNetwork = useSelectedNetwork(chainId);
const { data: deployedContractData } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork.id as AllowedChainIds,
});
const sendContractWriteAsyncTx = async <
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">,
>(
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
options?: ScaffoldWriteContractOptions,
) => {
if (!deployedContractData) {
notification.error("Target Contract is not deployed, did you forget to run `yarn deploy`?");
return;
}
if (!accountChain?.id) {
notification.error("Please connect your wallet");
return;
}
if (accountChain?.id !== selectedNetwork.id) {
notification.error(`Wallet is connected to the wrong network. Please switch to ${selectedNetwork.name}`);
return;
}
try {
setIsMining(true);
const { blockConfirmations, onBlockConfirmation, ...mutateOptions } = options || {};
const writeContractObject = {
abi: deployedContractData.abi as Abi,
address: deployedContractData.address,
...variables,
} as WriteContractVariables<Abi, string, any[], Config, number>;
if (!finalConfig?.disableSimulate) {
await simulateContractWriteAndNotifyError({
wagmiConfig,
writeContractParams: writeContractObject,
chainId: selectedNetwork.id as AllowedChainIds,
});
}
const makeWriteWithParams = () =>
wagmiContractWrite.writeContractAsync(
writeContractObject,
mutateOptions as
| MutateOptions<
WriteContractReturnType,
WriteContractErrorType,
WriteContractVariables<Abi, string, any[], Config, number>,
unknown
>
| undefined,
);
const writeTxResult = await writeTx(makeWriteWithParams, { blockConfirmations, onBlockConfirmation });
return writeTxResult;
} catch (e: any) {
throw e;
} finally {
setIsMining(false);
}
};
const sendContractWriteTx = <
TContractName extends ContractName,
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">,
>(
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
options?: Omit<ScaffoldWriteContractOptions, "onBlockConfirmation" | "blockConfirmations">,
) => {
if (!deployedContractData) {
notification.error("Target Contract is not deployed, did you forget to run `yarn deploy`?");
return;
}
if (!accountChain?.id) {
notification.error("Please connect your wallet");
return;
}
if (accountChain?.id !== selectedNetwork.id) {
notification.error(`Wallet is connected to the wrong network. Please switch to ${selectedNetwork.name}`);
return;
}
wagmiContractWrite.writeContract(
{
abi: deployedContractData.abi as Abi,
address: deployedContractData.address,
...variables,
} as WriteContractVariables<Abi, string, any[], Config, number>,
options as
| MutateOptions<
WriteContractReturnType,
WriteContractErrorType,
WriteContractVariables<Abi, string, any[], Config, number>,
unknown
>
| undefined,
);
};
return {
...wagmiContractWrite,
isMining,
// Overwrite wagmi's writeContactAsync
writeContractAsync: sendContractWriteAsyncTx,
// Overwrite wagmi's writeContract
writeContract: sendContractWriteTx,
};
}

View File

@@ -0,0 +1,19 @@
import scaffoldConfig from "~~/scaffold.config";
import { useGlobalState } from "~~/services/store/store";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import { ChainWithAttributes, NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth/networks";
/**
* Given a chainId, retrives the network object from `scaffold.config`,
* if not found default to network set by `useTargetNetwork` hook
*/
export function useSelectedNetwork(chainId?: AllowedChainIds): ChainWithAttributes {
const globalTargetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork);
const targetNetwork = scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chainId);
if (targetNetwork) {
return { ...targetNetwork, ...NETWORKS_EXTRA_DATA[targetNetwork.id] };
}
return globalTargetNetwork;
}

View File

@@ -0,0 +1,24 @@
import { useEffect, useMemo } from "react";
import { useAccount } from "wagmi";
import scaffoldConfig from "~~/scaffold.config";
import { useGlobalState } from "~~/services/store/store";
import { ChainWithAttributes } from "~~/utils/scaffold-eth";
import { NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth";
/**
* Retrieves the connected wallet's network from scaffold.config or defaults to the 0th network in the list if the wallet is not connected.
*/
export function useTargetNetwork(): { targetNetwork: ChainWithAttributes } {
const { chain } = useAccount();
const targetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork);
const setTargetNetwork = useGlobalState(({ setTargetNetwork }) => setTargetNetwork);
useEffect(() => {
const newSelectedNetwork = scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chain?.id);
if (newSelectedNetwork && newSelectedNetwork.id !== targetNetwork.id) {
setTargetNetwork({ ...newSelectedNetwork, ...NETWORKS_EXTRA_DATA[newSelectedNetwork.id] });
}
}, [chain?.id, setTargetNetwork, targetNetwork.id]);
return useMemo(() => ({ targetNetwork }), [targetNetwork]);
}

View File

@@ -0,0 +1,115 @@
import { Hash, SendTransactionParameters, TransactionReceipt, WalletClient } from "viem";
import { Config, useWalletClient } from "wagmi";
import { getPublicClient } from "wagmi/actions";
import { SendTransactionMutate } from "wagmi/query";
import scaffoldConfig from "~~/scaffold.config";
import { wagmiConfig } from "~~/services/web3/wagmiConfig";
import { AllowedChainIds, getBlockExplorerTxLink, notification } from "~~/utils/scaffold-eth";
import { TransactorFuncOptions, getParsedErrorWithAllAbis } from "~~/utils/scaffold-eth/contract";
type TransactionFunc = (
tx: (() => Promise<Hash>) | Parameters<SendTransactionMutate<Config, undefined>>[0],
options?: TransactorFuncOptions,
) => Promise<Hash | undefined>;
/**
* Custom notification content for TXs.
*/
const TxnNotification = ({ message, blockExplorerLink }: { message: string; blockExplorerLink?: string }) => {
return (
<div className={`flex flex-col ml-1 cursor-default`}>
<p className="my-0">{message}</p>
{blockExplorerLink && blockExplorerLink.length > 0 ? (
<a href={blockExplorerLink} target="_blank" rel="noreferrer" className="block link">
check out transaction
</a>
) : null}
</div>
);
};
/**
* Runs Transaction passed in to returned function showing UI feedback.
* @param _walletClient - Optional wallet client to use. If not provided, will use the one from useWalletClient.
* @returns function that takes in transaction function as callback, shows UI feedback for transaction and returns a promise of the transaction hash
*/
export const useTransactor = (_walletClient?: WalletClient): TransactionFunc => {
let walletClient = _walletClient;
const { data } = useWalletClient();
if (walletClient === undefined && data) {
walletClient = data;
}
const result: TransactionFunc = async (tx, options) => {
if (!walletClient) {
notification.error("Cannot access account");
console.error("⚡️ ~ file: useTransactor.tsx ~ error");
return;
}
let notificationId = null;
let transactionHash: Hash | undefined = undefined;
let transactionReceipt: TransactionReceipt | undefined;
let blockExplorerTxURL = "";
let chainId: number = scaffoldConfig.targetNetworks[0].id;
try {
chainId = await walletClient.getChainId();
// Get full transaction from public client
const publicClient = getPublicClient(wagmiConfig);
notificationId = notification.loading(<TxnNotification message="Awaiting for user confirmation" />);
if (typeof tx === "function") {
// Tx is already prepared by the caller
const result = await tx();
transactionHash = result;
} else if (tx != null) {
transactionHash = await walletClient.sendTransaction(tx as SendTransactionParameters);
} else {
throw new Error("Incorrect transaction passed to transactor");
}
notification.remove(notificationId);
blockExplorerTxURL = chainId ? getBlockExplorerTxLink(chainId, transactionHash) : "";
notificationId = notification.loading(
<TxnNotification message="Waiting for transaction to complete." blockExplorerLink={blockExplorerTxURL} />,
);
transactionReceipt = await publicClient.waitForTransactionReceipt({
hash: transactionHash,
confirmations: options?.blockConfirmations,
});
notification.remove(notificationId);
if (transactionReceipt.status === "reverted") throw new Error("Transaction reverted");
notification.success(
<TxnNotification message="Transaction completed successfully!" blockExplorerLink={blockExplorerTxURL} />,
{
icon: "🎉",
},
);
if (options?.onBlockConfirmation) options.onBlockConfirmation(transactionReceipt);
} catch (error: any) {
if (notificationId) {
notification.remove(notificationId);
}
console.error("⚡️ ~ file: useTransactor.ts ~ error", error);
const message = getParsedErrorWithAllAbis(error, chainId as AllowedChainIds);
// if receipt was reverted, show notification with block explorer link and return error
if (transactionReceipt?.status === "reverted") {
notification.error(<TxnNotification message={message} blockExplorerLink={blockExplorerTxURL} />);
throw error;
}
notification.error(message);
throw error;
}
return transactionHash;
};
return result;
};

6
packages/nextjs/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -0,0 +1,29 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
devIndicators: false,
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
webpack: config => {
config.resolve.fallback = { fs: false, net: false, tls: false };
config.externals.push("pino-pretty", "lokijs", "encoding");
return config;
},
};
const isIpfs = process.env.NEXT_PUBLIC_IPFS_BUILD === "true";
if (isIpfs) {
nextConfig.output = "export";
nextConfig.trailingSlash = true;
nextConfig.images = {
unoptimized: true,
};
}
module.exports = nextConfig;

View File

@@ -0,0 +1,64 @@
{
"name": "@se-2/nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"check-types": "tsc --noEmit --incremental",
"dev": "next dev",
"format": "prettier --write . '!(node_modules|.next|contracts)/**/*'",
"ipfs": "NEXT_PUBLIC_IPFS_BUILD=true yarn build && yarn bgipfs upload config init -u https://upload.bgipfs.com && CID=$(yarn bgipfs upload out | grep -o 'CID: [^ ]*' | cut -d' ' -f2) && [ ! -z \"$CID\" ] && echo '🚀 Upload complete! Your site is now available at: https://community.bgipfs.com/ipfs/'$CID || echo '❌ Upload failed'",
"lint": "next lint",
"serve": "next start",
"start": "next dev",
"vercel": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env VERCEL_TELEMETRY_DISABLED=1",
"vercel:login": "vercel login",
"vercel:yolo": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true --build-env VERCEL_TELEMETRY_DISABLED=1"
},
"dependencies": {
"@heroicons/react": "~2.1.5",
"@rainbow-me/rainbowkit": "2.2.9",
"@react-native-async-storage/async-storage": "~2.2.0",
"@scaffold-ui/components": "^0.1.7",
"@scaffold-ui/debug-contracts": "^0.1.6",
"@scaffold-ui/hooks": "^0.1.5",
"@tanstack/react-query": "~5.59.15",
"blo": "~1.2.0",
"burner-connector": "0.0.20",
"daisyui": "5.0.9",
"humanize-duration": "^3.28.0",
"kubo-rpc-client": "~5.0.2",
"next": "~15.2.8",
"next-nprogress-bar": "~2.3.13",
"next-themes": "~0.3.0",
"qrcode.react": "~4.0.1",
"react": "~19.2.3",
"react-dom": "~19.2.3",
"react-hot-toast": "~2.4.0",
"usehooks-ts": "~3.1.0",
"viem": "2.39.0",
"wagmi": "2.19.5",
"zustand": "~5.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "4.0.15",
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
"@types/humanize-duration": "^3",
"@types/node": "~18.19.50",
"@types/react": "~19.0.7",
"abitype": "1.0.6",
"autoprefixer": "~10.4.20",
"bgipfs": "~0.0.12",
"eslint": "~9.23.0",
"eslint-config-next": "~15.2.3",
"eslint-config-prettier": "~10.1.1",
"eslint-plugin-prettier": "~5.2.4",
"postcss": "~8.4.45",
"prettier": "~3.5.3",
"tailwindcss": "4.1.3",
"type-fest": "~4.26.1",
"typescript": "~5.8.2",
"vercel": "~39.1.3"
},
"packageManager": "yarn@3.2.3"
}

View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,10 @@
<svg width="103" height="102" viewBox="0 0 103 102" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.72229" y="0.99707" width="101.901" height="100.925" rx="19.6606" fill="black"/>
<path d="M69.4149 42.5046L70.9118 39.812L72.5438 42.5046L83.0241 59.1906L70.9533 65.9361L59.1193 59.1906L69.4149 42.5046Z" fill="white"/>
<path d="M70.9533 69.2496L60.6577 63.6876L70.9533 79.0719L81.184 63.4985L70.9533 69.2496Z" fill="white"/>
<path d="M70.953 65.9259V41.8533V39.8499L83.063 59.1785L70.953 65.9259Z" fill="#DFDFDF"/>
<path d="M70.9617 79.0499L71.0409 69.2969L81.2062 63.4629L70.9617 79.0499Z" fill="#DFDFDF"/>
<path d="M34.409 21.6931V24.6125H34.4132L34.4124 27.747H26.8093L26.8093 27.7566H25.5383L21.3839 36.4914H34.4135V39.9723H34.4091L34.4135 69.4549C34.4135 73.5268 31.1126 76.8277 27.0408 76.8277H24.7346L19.8064 84.0772H62.4172L57.3539 76.8277H51.7795C47.7076 76.8277 44.4067 73.5268 44.4067 69.4549L44.4024 43.665C44.5071 39.7076 47.7304 36.5273 51.7046 36.4914H79.5481L74.6584 27.7566H53.6021L53.6021 27.747L44.3987 27.7469L44.3994 24.6125H44.4022V18.3245L34.409 21.6931Z" fill="white"/>
<path d="M39.882 19.8517V76.5496C39.9731 74.9642 41.0475 70.8554 44.3648 69.1027V50.8665L44.4703 50.8998V43.8309C44.4703 39.7591 47.7712 36.4582 51.843 36.4582H79.5812L76.9508 31.8656H49.9083C46.1286 31.8656 44.3648 34.556 44.3648 34.556L44.4435 18.3066L39.882 19.8517Z" fill="#DFDFDF"/>
<path d="M23.622 31.7927L21.3295 36.5083H34.4247V31.7927H23.622Z" fill="#DFDFDF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,5 @@
{
"name": "Scaffold-ETH 2 DApp",
"description": "A DApp built with Scaffold-ETH",
"iconPath": "logo.svg"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,40 @@
import * as chains from "viem/chains";
export type BaseConfig = {
targetNetworks: readonly chains.Chain[];
pollingInterval: number;
alchemyApiKey: string;
rpcOverrides?: Record<number, string>;
walletConnectProjectId: string;
onlyLocalBurnerWallet: boolean;
};
export type ScaffoldConfig = BaseConfig;
export const DEFAULT_ALCHEMY_API_KEY = "cR4WnXePioePZ5fFrnSiR";
const scaffoldConfig = {
// The networks on which your DApp is live
targetNetworks: [chains.hardhat],
// The interval at which your front-end polls the RPC servers for new data (it has no effect if you only target the local network (default is 4000))
pollingInterval: 30000,
// This is ours Alchemy's default API key.
// You can get your own at https://dashboard.alchemyapi.io
// It's recommended to store it in an env variable:
// .env.local for local testing, and in the Vercel/system env config for live apps.
alchemyApiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || DEFAULT_ALCHEMY_API_KEY,
// If you want to use a different RPC for a specific network, you can add it here.
// The key is the chain ID, and the value is the HTTP RPC URL
rpcOverrides: {
// Example:
// [chains.mainnet.id]: "https://mainnet.rpc.buidlguidl.com",
},
// This is ours WalletConnect's default project ID.
// You can get your own at https://cloud.walletconnect.com
// It's recommended to store it in an env variable:
// .env.local for local testing, and in the Vercel/system env config for live apps.
walletConnectProjectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || "3a8170812b534d0ff9d794f19a901d64",
onlyLocalBurnerWallet: true,
} as const satisfies ScaffoldConfig;
export default scaffoldConfig;

View File

@@ -0,0 +1,25 @@
import { create } from "zustand";
import scaffoldConfig from "~~/scaffold.config";
import { ChainWithAttributes, NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth";
/**
* Zustand Store
*
* You can add global state to the app using this useGlobalState, to get & set
* values from anywhere in the app.
*
* Think about it as a global useState.
*/
type GlobalState = {
targetNetwork: ChainWithAttributes;
setTargetNetwork: (newTargetNetwork: ChainWithAttributes) => void;
};
export const useGlobalState = create<GlobalState>(set => ({
targetNetwork: {
...scaffoldConfig.targetNetworks[0],
...NETWORKS_EXTRA_DATA[scaffoldConfig.targetNetworks[0].id],
},
setTargetNetwork: (newTargetNetwork: ChainWithAttributes) => set(() => ({ targetNetwork: newTargetNetwork })),
}));

View File

@@ -0,0 +1,40 @@
import { wagmiConnectors } from "./wagmiConnectors";
import { Chain, createClient, fallback, http } from "viem";
import { hardhat, mainnet } from "viem/chains";
import { createConfig } from "wagmi";
import scaffoldConfig, { DEFAULT_ALCHEMY_API_KEY, ScaffoldConfig } from "~~/scaffold.config";
import { getAlchemyHttpUrl } from "~~/utils/scaffold-eth";
const { targetNetworks } = scaffoldConfig;
// We always want to have mainnet enabled (ENS resolution, ETH price, etc). But only once.
export const enabledChains = targetNetworks.find((network: Chain) => network.id === 1)
? targetNetworks
: ([...targetNetworks, mainnet] as const);
export const wagmiConfig = createConfig({
chains: enabledChains,
connectors: wagmiConnectors(),
ssr: true,
client: ({ chain }) => {
const mainnetFallbackWithDefaultRPC = [http("https://mainnet.rpc.buidlguidl.com")];
let rpcFallbacks = [...(chain.id === mainnet.id ? mainnetFallbackWithDefaultRPC : []), http()];
const rpcOverrideUrl = (scaffoldConfig.rpcOverrides as ScaffoldConfig["rpcOverrides"])?.[chain.id];
if (rpcOverrideUrl) {
rpcFallbacks = [http(rpcOverrideUrl), ...rpcFallbacks];
} else {
const alchemyHttpUrl = getAlchemyHttpUrl(chain.id);
if (alchemyHttpUrl) {
const isUsingDefaultKey = scaffoldConfig.alchemyApiKey === DEFAULT_ALCHEMY_API_KEY;
rpcFallbacks = isUsingDefaultKey
? [...rpcFallbacks, http(alchemyHttpUrl)]
: [http(alchemyHttpUrl), ...rpcFallbacks];
}
}
return createClient({
chain,
transport: fallback(rpcFallbacks),
...(chain.id !== (hardhat as Chain).id ? { pollingInterval: scaffoldConfig.pollingInterval } : {}),
});
},
});

View File

@@ -0,0 +1,51 @@
import { connectorsForWallets } from "@rainbow-me/rainbowkit";
import {
baseAccount,
ledgerWallet,
metaMaskWallet,
rainbowWallet,
safeWallet,
walletConnectWallet,
} from "@rainbow-me/rainbowkit/wallets";
import { rainbowkitBurnerWallet } from "burner-connector";
import * as chains from "viem/chains";
import scaffoldConfig from "~~/scaffold.config";
const { onlyLocalBurnerWallet, targetNetworks } = scaffoldConfig;
const wallets = [
metaMaskWallet,
walletConnectWallet,
ledgerWallet,
baseAccount,
rainbowWallet,
safeWallet,
...(!targetNetworks.some(network => network.id !== (chains.hardhat as chains.Chain).id) || !onlyLocalBurnerWallet
? [rainbowkitBurnerWallet]
: []),
];
/**
* wagmi connectors for the wagmi context
*/
export const wagmiConnectors = () => {
// Only create connectors on client-side to avoid SSR issues
// TODO: update when https://github.com/rainbow-me/rainbowkit/issues/2476 is resolved
if (typeof window === "undefined") {
return [];
}
return connectorsForWallets(
[
{
groupName: "Supported Wallets",
wallets,
},
],
{
appName: "scaffold-eth-2",
projectId: scaffoldConfig.walletConnectProjectId,
},
);
};

View File

@@ -0,0 +1,200 @@
@import "tailwindcss";
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
@theme {
--shadow-center: 0 0 12px -2px rgb(0 0 0 / 0.05);
--animate-pulse-fast: pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@plugin "daisyui" {
themes:
light,
dark --prefersdark;
}
@plugin "daisyui/theme" {
name: "light";
--color-primary: #93bbfb;
--color-primary-content: #212638;
--color-secondary: #dae8ff;
--color-secondary-content: #212638;
--color-accent: #93bbfb;
--color-accent-content: #212638;
--color-neutral: #212638;
--color-neutral-content: #ffffff;
--color-base-100: #ffffff;
--color-base-200: #f4f8ff;
--color-base-300: #dae8ff;
--color-base-content: #212638;
--color-info: #93bbfb;
--color-success: #34eeb6;
--color-warning: #ffcf72;
--color-error: #ff8863;
--radius-field: 9999rem;
--radius-box: 1rem;
--tt-tailw: 6px;
}
@plugin "daisyui/theme" {
name: "dark";
--color-primary: #212638;
--color-primary-content: #f9fbff;
--color-secondary: #323f61;
--color-secondary-content: #f9fbff;
--color-accent: #4969a6;
--color-accent-content: #f9fbff;
--color-neutral: #f9fbff;
--color-neutral-content: #385183;
--color-base-100: #385183;
--color-base-200: #2a3655;
--color-base-300: #212638;
--color-base-content: #f9fbff;
--color-info: #385183;
--color-success: #34eeb6;
--color-warning: #ffcf72;
--color-error: #ff8863;
--radius-field: 9999rem;
--radius-box: 1rem;
--tt-tailw: 6px;
--tt-bg: var(--color-primary);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
p {
margin: 1rem 0;
}
body {
min-height: 100vh;
}
h1,
h2,
h3,
h4 {
margin-bottom: 0.5rem;
line-height: 1;
}
}
:root,
[data-theme] {
background: var(--color-base-200);
}
.btn {
@apply shadow-md;
}
.btn.btn-ghost {
@apply shadow-none;
}
.link {
text-underline-offset: 2px;
}
.link:hover {
opacity: 80%;
}
/* -- EXTENSION OVERRIDES -- */
@plugin "daisyui/theme" {
name: "light";
--color-primary: #c8f5ff;
--color-primary-content: #026262;
--color-secondary: #89d7e9;
--color-secondary-content: #088484;
--color-accent: #026262;
--color-accent-content: #e9fbff;
--color-neutral: #088484;
--color-neutral-content: #f0fcff;
--color-base-100: #f0fcff;
--color-base-200: #e1faff;
--color-base-300: #c8f5ff;
--color-base-content: #088484;
--color-info: #026262;
--color-success: #34eeb6;
--color-warning: #ffcf72;
--color-error: #ff8863;
/* radius / button rounding */
--radius-field: 9999rem;
--radius-box: 1rem;
/* tooltip tail width */
--tt-tailw: 6px;
}
/* —— DARK THEME —— */
@plugin "daisyui/theme" {
name: "dark";
--color-primary: #026262;
--color-primary-content: #c8f5ff;
--color-secondary: #107575;
--color-secondary-content: #e9fbff;
--color-accent: #c8f5ff;
--color-accent-content: #088484;
--color-neutral: #e9fbff;
--color-neutral-content: #11acac;
--color-base-100: #11acac;
--color-base-200: #088484;
--color-base-300: #026262;
--color-base-content: #e9fbff;
--color-info: #c8f5ff;
--color-success: #34eeb6;
--color-warning: #ffcf72;
--color-error: #ff8863;
--radius-field: 9999rem;
--radius-box: 1rem;
--tt-tailw: 6px;
--tt-bg: var(--color-primary); /* if you need a tooltip bg override */
}
@theme inline {
--font-space-grotesk: var(--font-space-grotesk);
}
/* Override Scaffold UI theme colors */
:root {
--color-sui-primary: var(--color-primary);
--color-sui-primary-content: var(--color-primary-content);
--color-sui-base-content: var(--color-base-content);
--color-sui-input-border: var(--color-secondary);
--color-sui-input-background: var(--color-base-200);
--color-sui-accent: var(--color-accent);
--color-sui-input-text: color-mix(in oklab, var(--color-sui-base-content) 70%, transparent);
--color-sui-primary-subtle: var(--color-secondary);
--color-sui-primary-neutral: var(--color-base-200);
--color-sui-skeleton-base: var(--color-sui-primary-subtle);
--color-sui-skeleton-highlight: var(--color-sui-primary-neutral);
}

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"~~/*": ["./*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

16
packages/nextjs/types/abitype/abi.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
import "abitype";
import "~~/node_modules/viem/node_modules/abitype";
type AddressType = string;
declare module "abitype" {
export interface Register {
AddressType: AddressType;
}
}
declare module "~~/node_modules/viem/node_modules/abitype" {
export interface Register {
AddressType: AddressType;
}
}

View File

@@ -0,0 +1,17 @@
import { Block, Transaction, TransactionReceipt } from "viem";
export type TransactionWithFunction = Transaction & {
functionName?: string;
functionArgs?: any[];
functionArgNames?: string[];
functionArgTypes?: string[];
};
type TransactionReceipts = {
[key: string]: TransactionReceipt;
};
export type TransactionsTableProps = {
blocks: Block[];
transactionReceipts: TransactionReceipts;
};

View File

@@ -0,0 +1,12 @@
// To be used in JSON.stringify when a field might be bigint
// https://wagmi.sh/react/faq#bigint-serialization
export const replacer = (_key: string, value: unknown) => (typeof value === "bigint" ? value.toString() : value);
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
export const isZeroAddress = (address: string) => address === ZERO_ADDRESS;
// Treat any dot-separated string as a potential ENS name
const ensRegex = /.+\..+/;
export const isENS = (address = "") => ensRegex.test(address);

View File

@@ -0,0 +1,424 @@
import { getParsedError } from "./getParsedError";
import { AllowedChainIds } from "./networks";
import { notification } from "./notification";
import { MutateOptions } from "@tanstack/react-query";
import {
Abi,
AbiParameter,
AbiParameterToPrimitiveType,
AbiParametersToPrimitiveTypes,
ExtractAbiEvent,
ExtractAbiEventNames,
ExtractAbiFunction,
} from "abitype";
import type { ExtractAbiFunctionNames } from "abitype";
import type { Simplify } from "type-fest";
import type { MergeDeepRecord } from "type-fest/source/merge-deep";
import {
Address,
Block,
GetEventArgs,
GetTransactionReceiptReturnType,
GetTransactionReturnType,
Log,
TransactionReceipt,
WriteContractErrorType,
keccak256,
toHex,
} from "viem";
import { Config, UseReadContractParameters, UseWatchContractEventParameters, UseWriteContractParameters } from "wagmi";
import { WriteContractParameters, WriteContractReturnType, simulateContract } from "wagmi/actions";
import { WriteContractVariables } from "wagmi/query";
import deployedContractsData from "~~/contracts/deployedContracts";
import externalContractsData from "~~/contracts/externalContracts";
import scaffoldConfig from "~~/scaffold.config";
type AddExternalFlag<T> = {
[ChainId in keyof T]: {
[ContractName in keyof T[ChainId]]: T[ChainId][ContractName] & { external?: true };
};
};
const deepMergeContracts = <L extends Record<PropertyKey, any>, E extends Record<PropertyKey, any>>(
local: L,
external: E,
) => {
const result: Record<PropertyKey, any> = {};
const allKeys = Array.from(new Set([...Object.keys(external), ...Object.keys(local)]));
for (const key of allKeys) {
if (!external[key]) {
result[key] = local[key];
continue;
}
const amendedExternal = Object.fromEntries(
Object.entries(external[key] as Record<string, Record<string, unknown>>).map(([contractName, declaration]) => [
contractName,
{ ...declaration, external: true },
]),
);
result[key] = { ...local[key], ...amendedExternal };
}
return result as MergeDeepRecord<AddExternalFlag<L>, AddExternalFlag<E>, { arrayMergeMode: "replace" }>;
};
const contractsData = deepMergeContracts(deployedContractsData, externalContractsData);
export type InheritedFunctions = { readonly [key: string]: string };
export type GenericContract = {
address: Address;
abi: Abi;
inheritedFunctions?: InheritedFunctions;
external?: true;
deployedOnBlock?: number;
};
export type GenericContractsDeclaration = {
[chainId: number]: {
[contractName: string]: GenericContract;
};
};
export const contracts = contractsData as GenericContractsDeclaration | null;
type ConfiguredChainId = (typeof scaffoldConfig)["targetNetworks"][0]["id"];
type IsContractDeclarationMissing<TYes, TNo> = typeof contractsData extends { [key in ConfiguredChainId]: any }
? TNo
: TYes;
type ContractsDeclaration = IsContractDeclarationMissing<GenericContractsDeclaration, typeof contractsData>;
type Contracts = ContractsDeclaration[ConfiguredChainId];
export type ContractName = keyof Contracts;
export type Contract<TContractName extends ContractName> = Contracts[TContractName];
type InferContractAbi<TContract> = TContract extends { abi: infer TAbi } ? TAbi : never;
export type ContractAbi<TContractName extends ContractName = ContractName> = InferContractAbi<Contract<TContractName>>;
export type AbiFunctionInputs<TAbi extends Abi, TFunctionName extends string> = ExtractAbiFunction<
TAbi,
TFunctionName
>["inputs"];
export type AbiFunctionArguments<TAbi extends Abi, TFunctionName extends string> = AbiParametersToPrimitiveTypes<
AbiFunctionInputs<TAbi, TFunctionName>
>;
export type AbiFunctionOutputs<TAbi extends Abi, TFunctionName extends string> = ExtractAbiFunction<
TAbi,
TFunctionName
>["outputs"];
export type AbiFunctionReturnType<TAbi extends Abi, TFunctionName extends string> = IsContractDeclarationMissing<
any,
AbiParametersToPrimitiveTypes<AbiFunctionOutputs<TAbi, TFunctionName>> extends readonly [any]
? AbiParametersToPrimitiveTypes<AbiFunctionOutputs<TAbi, TFunctionName>>[0]
: AbiParametersToPrimitiveTypes<AbiFunctionOutputs<TAbi, TFunctionName>>
>;
export type AbiEventInputs<TAbi extends Abi, TEventName extends ExtractAbiEventNames<TAbi>> = ExtractAbiEvent<
TAbi,
TEventName
>["inputs"];
export enum ContractCodeStatus {
"LOADING",
"DEPLOYED",
"NOT_FOUND",
}
type AbiStateMutability = "pure" | "view" | "nonpayable" | "payable";
export type ReadAbiStateMutability = "view" | "pure";
export type WriteAbiStateMutability = "nonpayable" | "payable";
export type FunctionNamesWithInputs<
TContractName extends ContractName,
TAbiStateMutability extends AbiStateMutability = AbiStateMutability,
> = Exclude<
Extract<
ContractAbi<TContractName>[number],
{
type: "function";
stateMutability: TAbiStateMutability;
}
>,
{
inputs: readonly [];
}
>["name"];
type Expand<T> = T extends object ? (T extends infer O ? { [K in keyof O]: O[K] } : never) : T;
type UnionToIntersection<U> = Expand<(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never>;
type OptionalTuple<T> = T extends readonly [infer H, ...infer R] ? readonly [H | undefined, ...OptionalTuple<R>] : T;
type UseScaffoldArgsParam<
TContractName extends ContractName,
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>>,
> =
TFunctionName extends FunctionNamesWithInputs<TContractName>
? {
args: OptionalTuple<UnionToIntersection<AbiFunctionArguments<ContractAbi<TContractName>, TFunctionName>>>;
value?: ExtractAbiFunction<ContractAbi<TContractName>, TFunctionName>["stateMutability"] extends "payable"
? bigint | undefined
: undefined;
}
: {
args?: never;
};
export type UseDeployedContractConfig<TContractName extends ContractName> = {
contractName: TContractName;
chainId?: AllowedChainIds;
};
export type UseScaffoldWriteConfig<TContractName extends ContractName> = {
contractName: TContractName;
chainId?: AllowedChainIds;
disableSimulate?: boolean;
writeContractParams?: UseWriteContractParameters;
};
export type UseScaffoldReadConfig<
TContractName extends ContractName,
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, ReadAbiStateMutability>,
> = {
contractName: TContractName;
chainId?: AllowedChainIds;
watch?: boolean;
} & IsContractDeclarationMissing<
Partial<UseReadContractParameters>,
{
functionName: TFunctionName;
} & UseScaffoldArgsParam<TContractName, TFunctionName> &
Omit<UseReadContractParameters, "chainId" | "abi" | "address" | "functionName" | "args">
>;
export type ScaffoldWriteContractVariables<
TContractName extends ContractName,
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, WriteAbiStateMutability>,
> = IsContractDeclarationMissing<
Partial<WriteContractParameters>,
{
functionName: TFunctionName;
} & UseScaffoldArgsParam<TContractName, TFunctionName> &
Omit<WriteContractParameters, "chainId" | "abi" | "address" | "functionName" | "args">
>;
type WriteVariables = WriteContractVariables<Abi, string, any[], Config, number>;
export type TransactorFuncOptions = {
onBlockConfirmation?: (txnReceipt: TransactionReceipt) => void;
blockConfirmations?: number;
};
export type ScaffoldWriteContractOptions = MutateOptions<
WriteContractReturnType,
WriteContractErrorType,
WriteVariables,
unknown
> &
TransactorFuncOptions;
export type UseScaffoldEventConfig<
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
TEvent extends ExtractAbiEvent<ContractAbi<TContractName>, TEventName> = ExtractAbiEvent<
ContractAbi<TContractName>,
TEventName
>,
> = {
contractName: TContractName;
eventName: TEventName;
chainId?: AllowedChainIds;
} & IsContractDeclarationMissing<
Omit<UseWatchContractEventParameters, "onLogs" | "address" | "abi" | "eventName"> & {
onLogs: (
logs: Simplify<
Omit<Log<bigint, number, any>, "args" | "eventName"> & {
args: Record<string, unknown>;
eventName: string;
}
>[],
) => void;
},
Omit<UseWatchContractEventParameters<ContractAbi<TContractName>>, "onLogs" | "address" | "abi" | "eventName"> & {
onLogs: (
logs: Simplify<
Omit<Log<bigint, number, false, TEvent, false, [TEvent], TEventName>, "args"> & {
args: AbiParametersToPrimitiveTypes<TEvent["inputs"]> &
GetEventArgs<
ContractAbi<TContractName>,
TEventName,
{
IndexedOnly: false;
}
>;
}
>[],
) => void;
}
>;
type IndexedEventInputs<
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
> = Extract<AbiEventInputs<ContractAbi<TContractName>, TEventName>[number], { indexed: true }>;
export type EventFilters<
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
> = IsContractDeclarationMissing<
any,
IndexedEventInputs<TContractName, TEventName> extends never
? never
: {
[Key in IsContractDeclarationMissing<
any,
IndexedEventInputs<TContractName, TEventName>["name"]
>]?: AbiParameterToPrimitiveType<Extract<IndexedEventInputs<TContractName, TEventName>, { name: Key }>>;
}
>;
export type UseScaffoldEventHistoryConfig<
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
TBlockData extends boolean = false,
TTransactionData extends boolean = false,
TReceiptData extends boolean = false,
> = {
contractName: TContractName;
eventName: IsContractDeclarationMissing<string, TEventName>;
fromBlock?: bigint;
toBlock?: bigint;
chainId?: AllowedChainIds;
filters?: EventFilters<TContractName, TEventName>;
blockData?: TBlockData;
transactionData?: TTransactionData;
receiptData?: TReceiptData;
watch?: boolean;
enabled?: boolean;
blocksBatchSize?: number;
};
export type UseScaffoldEventHistoryData<
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
TBlockData extends boolean = false,
TTransactionData extends boolean = false,
TReceiptData extends boolean = false,
TEvent extends ExtractAbiEvent<ContractAbi<TContractName>, TEventName> = ExtractAbiEvent<
ContractAbi<TContractName>,
TEventName
>,
> =
| IsContractDeclarationMissing<
any[],
{
args: AbiParametersToPrimitiveTypes<TEvent["inputs"]> &
GetEventArgs<
ContractAbi<TContractName>,
TEventName,
{
IndexedOnly: false;
}
>;
blockData: TBlockData extends true ? Block<bigint, true> : null;
receiptData: TReceiptData extends true ? GetTransactionReturnType : null;
transactionData: TTransactionData extends true ? GetTransactionReceiptReturnType : null;
} & Log<bigint, number, false, TEvent, false, [TEvent], TEventName>[]
>
| undefined;
export type AbiParameterTuple = Extract<AbiParameter, { type: "tuple" | `tuple[${string}]` }>;
/**
* Enhanced error parsing that creates a lookup table from all deployed contracts
* to decode error signatures from any contract in the system
*/
export const getParsedErrorWithAllAbis = (error: any, chainId: AllowedChainIds): string => {
const originalParsedError = getParsedError(error);
// Check if this is an unrecognized error signature
if (/Encoded error signature.*not found on ABI/i.test(originalParsedError)) {
const signatureMatch = originalParsedError.match(/0x[a-fA-F0-9]{8}/);
const signature = signatureMatch ? signatureMatch[0] : "";
if (!signature) {
return originalParsedError;
}
try {
// Get all deployed contracts for the current chain
const chainContracts = deployedContractsData[chainId as keyof typeof deployedContractsData];
if (!chainContracts) {
return originalParsedError;
}
// Build a lookup table of error signatures to error names
const errorLookup: Record<string, { name: string; contract: string; signature: string }> = {};
Object.entries(chainContracts).forEach(([contractName, contract]: [string, any]) => {
if (contract.abi) {
contract.abi.forEach((item: any) => {
if (item.type === "error") {
// Create the proper error signature like Solidity does
const errorName = item.name;
const inputs = item.inputs || [];
const inputTypes = inputs.map((input: any) => input.type).join(",");
const errorSignature = `${errorName}(${inputTypes})`;
// Hash the signature and take the first 4 bytes (8 hex chars)
const hash = keccak256(toHex(errorSignature));
const errorSelector = hash.slice(0, 10); // 0x + 8 chars = 10 total
errorLookup[errorSelector] = {
name: errorName,
contract: contractName,
signature: errorSignature,
};
}
});
}
});
// Check if we can find the error in our lookup
const errorInfo = errorLookup[signature];
if (errorInfo) {
return `Contract function execution reverted with the following reason:\n${errorInfo.signature} from ${errorInfo.contract} contract`;
}
// If not found in simple lookup, provide a helpful message with context
return `${originalParsedError}\n\nThis error occurred when calling a function that internally calls another contract. Check the contract that your function calls internally for more details.`;
} catch (lookupError) {
console.log("Failed to create error lookup table:", lookupError);
}
}
return originalParsedError;
};
export const simulateContractWriteAndNotifyError = async ({
wagmiConfig,
writeContractParams: params,
chainId,
}: {
wagmiConfig: Config;
writeContractParams: WriteContractVariables<Abi, string, any[], Config, number>;
chainId: AllowedChainIds;
}) => {
try {
await simulateContract(wagmiConfig, params);
} catch (error) {
const parsedError = getParsedErrorWithAllAbis(error, chainId);
notification.error(parsedError);
throw error;
}
};

View File

@@ -0,0 +1,11 @@
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
import { GenericContractsDeclaration, contracts } from "~~/utils/scaffold-eth/contract";
const DEFAULT_ALL_CONTRACTS: GenericContractsDeclaration[number] = {};
export function useAllContracts() {
const { targetNetwork } = useTargetNetwork();
const contractsData = contracts?.[targetNetwork.id];
// using constant to avoid creating a new object on every call
return contractsData || DEFAULT_ALL_CONTRACTS;
}

View File

@@ -0,0 +1,65 @@
import { TransactionWithFunction } from "./block";
import { GenericContractsDeclaration } from "./contract";
import { Abi, AbiFunction, decodeFunctionData, getAbiItem } from "viem";
import { hardhat } from "viem/chains";
import contractData from "~~/contracts/deployedContracts";
type ContractsInterfaces = Record<string, Abi>;
type TransactionType = TransactionWithFunction | null;
const deployedContracts = contractData as GenericContractsDeclaration | null;
const chainMetaData = deployedContracts?.[hardhat.id];
const interfaces = chainMetaData
? Object.entries(chainMetaData).reduce((finalInterfacesObj, [contractName, contract]) => {
finalInterfacesObj[contractName] = contract.abi;
return finalInterfacesObj;
}, {} as ContractsInterfaces)
: {};
export const decodeTransactionData = (tx: TransactionWithFunction) => {
if (tx.input.length >= 10 && !tx.input.startsWith("0x60e06040")) {
let foundInterface = false;
for (const [, contractAbi] of Object.entries(interfaces)) {
try {
const { functionName, args } = decodeFunctionData({
abi: contractAbi,
data: tx.input,
});
tx.functionName = functionName;
tx.functionArgs = args as any[];
tx.functionArgNames = getAbiItem<AbiFunction[], string>({
abi: contractAbi as AbiFunction[],
name: functionName,
})?.inputs?.map((input: any) => input.name);
tx.functionArgTypes = getAbiItem<AbiFunction[], string>({
abi: contractAbi as AbiFunction[],
name: functionName,
})?.inputs.map((input: any) => input.type);
foundInterface = true;
break;
} catch {
// do nothing
}
}
if (!foundInterface) {
tx.functionName = "⚠️ Unknown";
}
}
return tx;
};
export const getFunctionDetails = (transaction: TransactionType) => {
if (
transaction &&
transaction.functionName &&
transaction.functionArgNames &&
transaction.functionArgTypes &&
transaction.functionArgs
) {
const details = transaction.functionArgNames.map(
(name, i) => `${transaction.functionArgTypes?.[i] || ""} ${name} = ${transaction.functionArgs?.[i] ?? ""}`,
);
return `${transaction.functionName}(${details.join(", ")})`;
}
return "";
};

View File

@@ -0,0 +1,72 @@
import { ChainWithAttributes, getAlchemyHttpUrl } from "./networks";
import { CurrencyAmount, Token } from "@uniswap/sdk-core";
import { Pair, Route } from "@uniswap/v2-sdk";
import { Address, createPublicClient, fallback, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
const alchemyHttpUrl = getAlchemyHttpUrl(mainnet.id);
const rpcFallbacks = alchemyHttpUrl ? [http(alchemyHttpUrl), http()] : [http()];
const publicClient = createPublicClient({
chain: mainnet,
transport: fallback(rpcFallbacks),
});
const ABI = parseAbi([
"function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)",
"function token0() external view returns (address)",
"function token1() external view returns (address)",
]);
export const fetchPriceFromUniswap = async (targetNetwork: ChainWithAttributes): Promise<number> => {
if (
targetNetwork.nativeCurrency.symbol !== "ETH" &&
targetNetwork.nativeCurrency.symbol !== "SEP" &&
!targetNetwork.nativeCurrencyTokenAddress
) {
return 0;
}
try {
const DAI = new Token(1, "0x6B175474E89094C44Da98b954EedeAC495271d0F", 18);
const TOKEN = new Token(
1,
targetNetwork.nativeCurrencyTokenAddress || "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
18,
);
const pairAddress = Pair.getAddress(TOKEN, DAI) as Address;
const wagmiConfig = {
address: pairAddress,
abi: ABI,
};
const reserves = await publicClient.readContract({
...wagmiConfig,
functionName: "getReserves",
});
const token0Address = await publicClient.readContract({
...wagmiConfig,
functionName: "token0",
});
const token1Address = await publicClient.readContract({
...wagmiConfig,
functionName: "token1",
});
const token0 = [TOKEN, DAI].find(token => token.address === token0Address) as Token;
const token1 = [TOKEN, DAI].find(token => token.address === token1Address) as Token;
const pair = new Pair(
CurrencyAmount.fromRawAmount(token0, reserves[0].toString()),
CurrencyAmount.fromRawAmount(token1, reserves[1].toString()),
);
const route = new Route([pair], TOKEN, DAI);
const price = parseFloat(route.midPrice.toSignificant(6));
return price;
} catch (error) {
console.error(
`useNativeCurrencyPrice - Error fetching ${targetNetwork.nativeCurrency.symbol} price from Uniswap: `,
error,
);
return 0;
}
};

View File

@@ -0,0 +1,56 @@
import type { Metadata } from "next";
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: `http://localhost:${process.env.PORT || 3000}`;
const titleTemplate = "%s | SpeedrunEthereum";
export const getMetadata = ({
title,
description,
imageRelativePath = "/thumbnail-challenge.png",
}: {
title: string;
description: string;
imageRelativePath?: string;
}): Metadata => {
const imageUrl = `${baseUrl}${imageRelativePath}`;
return {
metadataBase: new URL(baseUrl),
title: {
default: title,
template: titleTemplate,
},
description: description,
openGraph: {
title: {
default: title,
template: titleTemplate,
},
description: description,
images: [
{
url: imageUrl,
},
],
},
twitter: {
title: {
default: title,
template: titleTemplate,
},
description: description,
images: [imageUrl],
},
icons: {
icon: [
{
url: "/favicon.png",
sizes: "32x32",
type: "image/png",
},
],
},
};
};

View File

@@ -0,0 +1,35 @@
import { BaseError as BaseViemError, ContractFunctionRevertedError } from "viem";
/**
* Parses an viem/wagmi error to get a displayable string
* @param e - error object
* @returns parsed error string
*/
export const getParsedError = (error: any): string => {
const parsedError = error?.walk ? error.walk() : error;
if (parsedError instanceof BaseViemError) {
if (parsedError.details) {
return parsedError.details;
}
if (parsedError.shortMessage) {
if (
parsedError instanceof ContractFunctionRevertedError &&
parsedError.data &&
parsedError.data.errorName !== "Error"
) {
const customErrorArgs = parsedError.data.args?.toString() ?? "";
return `${parsedError.shortMessage.replace(/reverted\.$/, "reverted with the following reason:")}\n${
parsedError.data.errorName
}(${customErrorArgs})`;
}
return parsedError.shortMessage;
}
return parsedError.message ?? parsedError.name ?? "An unknown error occurred";
}
return parsedError?.message ?? "An unknown error occurred";
};

View File

@@ -0,0 +1,5 @@
export * from "./networks";
export * from "./notification";
export * from "./block";
export * from "./decodeTxData";
export * from "./getParsedError";

View File

@@ -0,0 +1,145 @@
import * as chains from "viem/chains";
import scaffoldConfig from "~~/scaffold.config";
type ChainAttributes = {
// color | [lightThemeColor, darkThemeColor]
color: string | [string, string];
// Used to fetch price by providing mainnet token address
// for networks having native currency other than ETH
nativeCurrencyTokenAddress?: string;
};
export type ChainWithAttributes = chains.Chain & Partial<ChainAttributes>;
export type AllowedChainIds = (typeof scaffoldConfig.targetNetworks)[number]["id"];
// Mapping of chainId to RPC chain name an format followed by alchemy and infura
export const RPC_CHAIN_NAMES: Record<number, string> = {
[chains.mainnet.id]: "eth-mainnet",
[chains.goerli.id]: "eth-goerli",
[chains.sepolia.id]: "eth-sepolia",
[chains.optimism.id]: "opt-mainnet",
[chains.optimismGoerli.id]: "opt-goerli",
[chains.optimismSepolia.id]: "opt-sepolia",
[chains.arbitrum.id]: "arb-mainnet",
[chains.arbitrumGoerli.id]: "arb-goerli",
[chains.arbitrumSepolia.id]: "arb-sepolia",
[chains.polygon.id]: "polygon-mainnet",
[chains.polygonMumbai.id]: "polygon-mumbai",
[chains.polygonAmoy.id]: "polygon-amoy",
[chains.astar.id]: "astar-mainnet",
[chains.polygonZkEvm.id]: "polygonzkevm-mainnet",
[chains.polygonZkEvmTestnet.id]: "polygonzkevm-testnet",
[chains.base.id]: "base-mainnet",
[chains.baseGoerli.id]: "base-goerli",
[chains.baseSepolia.id]: "base-sepolia",
[chains.celo.id]: "celo-mainnet",
[chains.celoSepolia.id]: "celo-sepolia",
};
export const getAlchemyHttpUrl = (chainId: number) => {
return scaffoldConfig.alchemyApiKey && RPC_CHAIN_NAMES[chainId]
? `https://${RPC_CHAIN_NAMES[chainId]}.g.alchemy.com/v2/${scaffoldConfig.alchemyApiKey}`
: undefined;
};
export const NETWORKS_EXTRA_DATA: Record<string, ChainAttributes> = {
[chains.hardhat.id]: {
color: "#b8af0c",
},
[chains.mainnet.id]: {
color: "#ff8b9e",
},
[chains.sepolia.id]: {
color: ["#5f4bb6", "#87ff65"],
},
[chains.gnosis.id]: {
color: "#48a9a6",
},
[chains.polygon.id]: {
color: "#2bbdf7",
nativeCurrencyTokenAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
},
[chains.polygonMumbai.id]: {
color: "#92D9FA",
nativeCurrencyTokenAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
},
[chains.optimismSepolia.id]: {
color: "#f01a37",
},
[chains.optimism.id]: {
color: "#f01a37",
},
[chains.arbitrumSepolia.id]: {
color: "#28a0f0",
},
[chains.arbitrum.id]: {
color: "#28a0f0",
},
[chains.fantom.id]: {
color: "#1969ff",
},
[chains.fantomTestnet.id]: {
color: "#1969ff",
},
[chains.scrollSepolia.id]: {
color: "#fbebd4",
},
[chains.celo.id]: {
color: "#FCFF52",
},
[chains.celoSepolia.id]: {
color: "#476520",
},
};
/**
* Gives the block explorer transaction URL, returns empty string if the network is a local chain
*/
export function getBlockExplorerTxLink(chainId: number, txnHash: string) {
const chainNames = Object.keys(chains);
const targetChainArr = chainNames.filter(chainName => {
const wagmiChain = chains[chainName as keyof typeof chains];
return wagmiChain.id === chainId;
});
if (targetChainArr.length === 0) {
return "";
}
const targetChain = targetChainArr[0] as keyof typeof chains;
const blockExplorerTxURL = chains[targetChain]?.blockExplorers?.default?.url;
if (!blockExplorerTxURL) {
return "";
}
return `${blockExplorerTxURL}/tx/${txnHash}`;
}
/**
* Gives the block explorer URL for a given address.
* Defaults to Etherscan if no (wagmi) block explorer is configured for the network.
*/
export function getBlockExplorerAddressLink(network: chains.Chain, address: string) {
const blockExplorerBaseURL = network.blockExplorers?.default?.url;
if (network.id === chains.hardhat.id) {
return `/blockexplorer/address/${address}`;
}
if (!blockExplorerBaseURL) {
return `https://etherscan.io/address/${address}`;
}
return `${blockExplorerBaseURL}/address/${address}`;
}
/**
* @returns targetNetworks array containing networks configured in scaffold.config including extra network metadata
*/
export function getTargetNetworks(): ChainWithAttributes[] {
return scaffoldConfig.targetNetworks.map(targetNetwork => ({
...targetNetwork,
...NETWORKS_EXTRA_DATA[targetNetwork.id],
}));
}

View File

@@ -0,0 +1,90 @@
import React from "react";
import { Toast, ToastPosition, toast } from "react-hot-toast";
import { XMarkIcon } from "@heroicons/react/20/solid";
import {
CheckCircleIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
} from "@heroicons/react/24/solid";
type NotificationProps = {
content: React.ReactNode;
status: "success" | "info" | "loading" | "error" | "warning";
duration?: number;
icon?: string;
position?: ToastPosition;
};
type NotificationOptions = {
duration?: number;
icon?: string;
position?: ToastPosition;
};
const ENUM_STATUSES = {
success: <CheckCircleIcon className="w-7 text-success" />,
loading: <span className="w-6 loading loading-spinner"></span>,
error: <ExclamationCircleIcon className="w-7 text-error" />,
info: <InformationCircleIcon className="w-7 text-info" />,
warning: <ExclamationTriangleIcon className="w-7 text-warning" />,
};
const DEFAULT_DURATION = 3000;
const DEFAULT_POSITION: ToastPosition = "top-center";
/**
* Custom Notification
*/
const Notification = ({
content,
status,
duration = DEFAULT_DURATION,
icon,
position = DEFAULT_POSITION,
}: NotificationProps) => {
return toast.custom(
(t: Toast) => (
<div
className={`flex flex-row items-start justify-between max-w-sm rounded-xl shadow-center shadow-accent bg-base-200 p-4 transform-gpu relative transition-all duration-500 ease-in-out space-x-2
${
position.substring(0, 3) == "top"
? `hover:translate-y-1 ${t.visible ? "top-0" : "-top-96"}`
: `hover:-translate-y-1 ${t.visible ? "bottom-0" : "-bottom-96"}`
}`}
>
<div className="leading-[0] self-center">{icon ? icon : ENUM_STATUSES[status]}</div>
<div className={`overflow-x-hidden break-words whitespace-pre-line ${icon ? "mt-1" : ""}`}>{content}</div>
<div className={`cursor-pointer text-lg ${icon ? "mt-1" : ""}`} onClick={() => toast.dismiss(t.id)}>
<XMarkIcon className="w-6 cursor-pointer" onClick={() => toast.remove(t.id)} />
</div>
</div>
),
{
duration: status === "loading" ? Infinity : duration,
position,
},
);
};
export const notification = {
success: (content: React.ReactNode, options?: NotificationOptions) => {
return Notification({ content, status: "success", ...options });
},
info: (content: React.ReactNode, options?: NotificationOptions) => {
return Notification({ content, status: "info", ...options });
},
warning: (content: React.ReactNode, options?: NotificationOptions) => {
return Notification({ content, status: "warning", ...options });
},
error: (content: React.ReactNode, options?: NotificationOptions) => {
return Notification({ content, status: "error", ...options });
},
loading: (content: React.ReactNode, options?: NotificationOptions) => {
return Notification({ content, status: "loading", ...options });
},
remove: (toastId: string) => {
toast.remove(toastId);
},
};

View File

@@ -0,0 +1,3 @@
{
"installCommand": "yarn install"
}