Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.2

This commit is contained in:
han
2026-01-21 20:45:23 +07:00
commit e7b2a69f2a
156 changed files with 29196 additions and 0 deletions

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,36 @@
import { BackButton } from "./BackButton";
import { ContractTabs } from "./ContractTabs";
import { Address as AddressType } from "viem";
import { Address, Balance } from "~~/components/scaffold-eth";
export const AddressComponent = ({
address,
contractData,
}: {
address: AddressType;
contractData: { bytecode: string; assembly: string } | null;
}) => {
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 />
<div className="flex gap-1 items-center">
<span className="font-bold text-sm">Balance:</span>
<Balance address={address} className="text" />
</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,71 @@
import { TransactionHash } from "./TransactionHash";
import { formatEther } from "viem";
import { Address } from "~~/components/scaffold-eth";
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 />
</td>
<td className="w-2/12 md:py-4">
{!receipt?.contractAddress ? (
tx.to && <Address address={tx.to} size="sm" onlyEnsOrAddress />
) : (
<div className="relative">
<Address address={receipt.contractAddress} size="sm" onlyEnsOrAddress />
<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,152 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Hash, Transaction, TransactionReceipt, formatEther, formatUnits } from "viem";
import { hardhat } from "viem/chains";
import { usePublicClient } from "wagmi";
import { Address } from "~~/components/scaffold-eth";
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 />
</td>
</tr>
<tr>
<td>
<strong>To:</strong>
</td>
<td>
{!receipt?.contractAddress ? (
transaction.to && <Address address={transaction.to} format="long" onlyEnsOrAddress />
) : (
<span>
Contract Creation:
<Address address={receipt.contractAddress} format="long" onlyEnsOrAddress />
</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,73 @@
"use client";
import { useEffect, useMemo } from "react";
import { useSessionStorage } from "usehooks-ts";
import { BarsArrowUpIcon } from "@heroicons/react/20/solid";
import { ContractUI } from "~~/app/debug/_components/contract";
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 => (
<ContractUI
key={contractName}
contractName={contractName}
className={contractName === selectedContract ? "" : "hidden"}
/>
))}
</>
)}
</div>
);
}

View File

@@ -0,0 +1,84 @@
"use client";
import { Dispatch, SetStateAction } from "react";
import { Tuple } from "./Tuple";
import { TupleArray } from "./TupleArray";
import { AbiParameter } from "abitype";
import {
AddressInput,
Bytes32Input,
BytesInput,
InputBase,
IntegerInput,
IntegerVariant,
} from "~~/components/scaffold-eth";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
type ContractInputProps = {
setForm: Dispatch<SetStateAction<Record<string, any>>>;
form: Record<string, any> | undefined;
stateObjectKey: string;
paramType: AbiParameter;
};
/**
* Generic Input component to handle input's based on their function param type
*/
export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: ContractInputProps) => {
const inputProps = {
name: stateObjectKey,
value: form?.[stateObjectKey],
placeholder: paramType.name ? `${paramType.type} ${paramType.name}` : paramType.type,
onChange: (value: any) => {
setForm(form => ({ ...form, [stateObjectKey]: value }));
},
};
const renderInput = () => {
switch (paramType.type) {
case "address":
return <AddressInput {...inputProps} />;
case "bytes32":
return <Bytes32Input {...inputProps} />;
case "bytes":
return <BytesInput {...inputProps} />;
case "string":
return <InputBase {...inputProps} />;
case "tuple":
return (
<Tuple
setParentForm={setForm}
parentForm={form}
abiTupleParameter={paramType as AbiParameterTuple}
parentStateObjectKey={stateObjectKey}
/>
);
default:
// Handling 'int' types and 'tuple[]' types
if (paramType.type.includes("int") && !paramType.type.includes("[")) {
return <IntegerInput {...inputProps} variant={paramType.type as IntegerVariant} />;
} else if (paramType.type.startsWith("tuple[")) {
return (
<TupleArray
setParentForm={setForm}
parentForm={form}
abiTupleParameter={paramType as AbiParameterTuple}
parentStateObjectKey={stateObjectKey}
/>
);
} else {
return <InputBase {...inputProps} />;
}
}
};
return (
<div className="flex flex-col gap-1.5 w-full">
<div className="flex items-center ml-2">
{paramType.name && <span className="text-xs font-medium mr-2 leading-none">{paramType.name}</span>}
<span className="block text-xs font-extralight leading-none">{paramType.type}</span>
</div>
{renderInput()}
</div>
);
};

View File

@@ -0,0 +1,43 @@
import { Abi, AbiFunction } from "abitype";
import { ReadOnlyFunctionForm } from "~~/app/debug/_components/contract";
import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
export const ContractReadMethods = ({ deployedContractData }: { deployedContractData: Contract<ContractName> }) => {
if (!deployedContractData) {
return null;
}
const functionsToDisplay = (
((deployedContractData.abi || []) as Abi).filter(part => part.type === "function") as AbiFunction[]
)
.filter(fn => {
const isQueryableWithParams =
(fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length > 0;
return isQueryableWithParams;
})
.map(fn => {
return {
fn,
inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
};
})
.sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
if (!functionsToDisplay.length) {
return <>No read methods</>;
}
return (
<>
{functionsToDisplay.map(({ fn, inheritedFrom }) => (
<ReadOnlyFunctionForm
abi={deployedContractData.abi as Abi}
contractAddress={deployedContractData.address}
abiFunction={fn}
key={fn.name}
inheritedFrom={inheritedFrom}
/>
))}
</>
);
};

View File

@@ -0,0 +1,104 @@
"use client";
// @refresh reset
import { useReducer } from "react";
import { ContractReadMethods } from "./ContractReadMethods";
import { ContractVariables } from "./ContractVariables";
import { ContractWriteMethods } from "./ContractWriteMethods";
import { Address, Balance } from "~~/components/scaffold-eth";
import { useDeployedContractInfo, useNetworkColor } 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, className = "" }: ContractUIProps) => {
const [refreshDisplayVariables, triggerRefreshDisplayVariables] = useReducer(value => !value, false);
const { targetNetwork } = useTargetNetwork();
const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({ contractName });
const networkColor = useNetworkColor();
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 (
<div className={`grid grid-cols-1 lg:grid-cols-6 px-6 lg:px-10 lg:gap-12 w-full max-w-7xl my-0 ${className}`}>
<div className="col-span-5 grid grid-cols-1 lg:grid-cols-3 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">
<div className="flex">
<div className="flex flex-col gap-1">
<span className="font-bold">{contractName}</span>
<Address address={deployedContractData.address} onlyEnsOrAddress />
<div className="flex gap-1 items-center">
<span className="font-bold text-sm">Balance:</span>
<Balance address={deployedContractData.address} className="px-0 h-1.5 min-h-[0.375rem]" />
</div>
</div>
</div>
{targetNetwork && (
<p className="my-0 text-sm">
<span className="font-bold">Network</span>:{" "}
<span style={{ color: networkColor }}>{targetNetwork.name}</span>
</p>
)}
</div>
<div className="bg-base-300 rounded-3xl px-6 lg:px-8 py-4 shadow-lg shadow-base-300">
<ContractVariables
refreshDisplayVariables={refreshDisplayVariables}
deployedContractData={deployedContractData}
/>
</div>
</div>
<div className="col-span-1 lg:col-span-2 flex flex-col gap-6">
<div className="z-10">
<div className="bg-base-100 rounded-3xl shadow-md shadow-secondary border border-base-300 flex flex-col mt-10 relative">
<div className="h-[5rem] w-[5.5rem] bg-base-300 absolute self-start rounded-[22px] -top-[38px] -left-[1px] -z-10 py-[0.65rem] shadow-lg shadow-base-300">
<div className="flex items-center justify-center space-x-2">
<p className="my-0 text-sm">Read</p>
</div>
</div>
<div className="p-5 divide-y divide-base-300">
<ContractReadMethods deployedContractData={deployedContractData} />
</div>
</div>
</div>
<div className="z-10">
<div className="bg-base-100 rounded-3xl shadow-md shadow-secondary border border-base-300 flex flex-col mt-10 relative">
<div className="h-[5rem] w-[5.5rem] bg-base-300 absolute self-start rounded-[22px] -top-[38px] -left-[1px] -z-10 py-[0.65rem] shadow-lg shadow-base-300">
<div className="flex items-center justify-center space-x-2">
<p className="my-0 text-sm">Write</p>
</div>
</div>
<div className="p-5 divide-y divide-base-300">
<ContractWriteMethods
deployedContractData={deployedContractData}
onChange={triggerRefreshDisplayVariables}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,50 @@
import { DisplayVariable } from "./DisplayVariable";
import { Abi, AbiFunction } from "abitype";
import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
export const ContractVariables = ({
refreshDisplayVariables,
deployedContractData,
}: {
refreshDisplayVariables: boolean;
deployedContractData: Contract<ContractName>;
}) => {
if (!deployedContractData) {
return null;
}
const functionsToDisplay = (
(deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]
)
.filter(fn => {
const isQueryableWithNoParams =
(fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0;
return isQueryableWithNoParams;
})
.map(fn => {
return {
fn,
inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
};
})
.sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
if (!functionsToDisplay.length) {
return <>No contract variables</>;
}
return (
<>
{functionsToDisplay.map(({ fn, inheritedFrom }) => (
<DisplayVariable
abi={deployedContractData.abi as Abi}
abiFunction={fn}
contractAddress={deployedContractData.address}
key={fn.name}
refreshDisplayVariables={refreshDisplayVariables}
inheritedFrom={inheritedFrom}
/>
))}
</>
);
};

View File

@@ -0,0 +1,49 @@
import { Abi, AbiFunction } from "abitype";
import { WriteOnlyFunctionForm } from "~~/app/debug/_components/contract";
import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
export const ContractWriteMethods = ({
onChange,
deployedContractData,
}: {
onChange: () => void;
deployedContractData: Contract<ContractName>;
}) => {
if (!deployedContractData) {
return null;
}
const functionsToDisplay = (
(deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]
)
.filter(fn => {
const isWriteableFunction = fn.stateMutability !== "view" && fn.stateMutability !== "pure";
return isWriteableFunction;
})
.map(fn => {
return {
fn,
inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
};
})
.sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
if (!functionsToDisplay.length) {
return <>No write methods</>;
}
return (
<>
{functionsToDisplay.map(({ fn, inheritedFrom }, idx) => (
<WriteOnlyFunctionForm
abi={deployedContractData.abi as Abi}
key={`${fn.name}-${idx}}`}
abiFunction={fn}
onChange={onChange}
contractAddress={deployedContractData.address}
inheritedFrom={inheritedFrom}
/>
))}
</>
);
};

View File

@@ -0,0 +1,85 @@
"use client";
import { useEffect } from "react";
import { InheritanceTooltip } from "./InheritanceTooltip";
import { displayTxResult } from "./utilsDisplay";
import { Abi, AbiFunction } from "abitype";
import { Address } from "viem";
import { useReadContract } from "wagmi";
import { ArrowPathIcon } from "@heroicons/react/24/outline";
import { useAnimationConfig } from "~~/hooks/scaffold-eth";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { getParsedError, notification } from "~~/utils/scaffold-eth";
type DisplayVariableProps = {
contractAddress: Address;
abiFunction: AbiFunction;
refreshDisplayVariables: boolean;
inheritedFrom?: string;
abi: Abi;
};
export const DisplayVariable = ({
contractAddress,
abiFunction,
refreshDisplayVariables,
abi,
inheritedFrom,
}: DisplayVariableProps) => {
const { targetNetwork } = useTargetNetwork();
const {
data: result,
isFetching,
refetch,
error,
} = useReadContract({
address: contractAddress,
functionName: abiFunction.name,
abi: abi,
chainId: targetNetwork.id,
query: {
retry: false,
},
});
const { showAnimation } = useAnimationConfig(result);
useEffect(() => {
refetch();
}, [refetch, refreshDisplayVariables]);
useEffect(() => {
if (error) {
const parsedError = getParsedError(error);
notification.error(parsedError);
}
}, [error]);
return (
<div className="space-y-1 pb-2">
<div className="flex items-center">
<h3 className="font-medium text-lg mb-0 break-all">{abiFunction.name}</h3>
<button className="btn btn-ghost btn-xs" onClick={async () => await refetch()}>
{isFetching ? (
<span className="loading loading-spinner loading-xs"></span>
) : (
<ArrowPathIcon className="h-3 w-3 cursor-pointer" aria-hidden="true" />
)}
</button>
<InheritanceTooltip inheritedFrom={inheritedFrom} />
</div>
<div className="text-base-content/80 flex flex-col items-start">
<div>
<div
className={`break-all block transition bg-transparent ${
showAnimation ? "bg-warning rounded-xs animate-pulse-fast" : ""
}`}
>
{displayTxResult(result)}
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,14 @@
import { InformationCircleIcon } from "@heroicons/react/20/solid";
export const InheritanceTooltip = ({ inheritedFrom }: { inheritedFrom?: string }) => (
<>
{inheritedFrom && (
<span
className="tooltip tooltip-top tooltip-accent px-2 md:break-normal"
data-tip={`Inherited from: ${inheritedFrom}`}
>
<InformationCircleIcon className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>
);

View File

@@ -0,0 +1,102 @@
"use client";
import { useEffect, useState } from "react";
import { InheritanceTooltip } from "./InheritanceTooltip";
import { Abi, AbiFunction } from "abitype";
import { Address } from "viem";
import { useReadContract } from "wagmi";
import {
ContractInput,
displayTxResult,
getFunctionInputKey,
getInitialFormState,
getParsedContractFunctionArgs,
transformAbiFunction,
} from "~~/app/debug/_components/contract";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { getParsedError, notification } from "~~/utils/scaffold-eth";
type ReadOnlyFunctionFormProps = {
contractAddress: Address;
abiFunction: AbiFunction;
inheritedFrom?: string;
abi: Abi;
};
export const ReadOnlyFunctionForm = ({
contractAddress,
abiFunction,
inheritedFrom,
abi,
}: ReadOnlyFunctionFormProps) => {
const [form, setForm] = useState<Record<string, any>>(() => getInitialFormState(abiFunction));
const [result, setResult] = useState<unknown>();
const { targetNetwork } = useTargetNetwork();
const { isFetching, refetch, error } = useReadContract({
address: contractAddress,
functionName: abiFunction.name,
abi: abi,
args: getParsedContractFunctionArgs(form),
chainId: targetNetwork.id,
query: {
enabled: false,
retry: false,
},
});
useEffect(() => {
if (error) {
const parsedError = getParsedError(error);
notification.error(parsedError);
}
}, [error]);
const transformedFunction = transformAbiFunction(abiFunction);
const inputElements = transformedFunction.inputs.map((input, inputIndex) => {
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
return (
<ContractInput
key={key}
setForm={updatedFormValue => {
setResult(undefined);
setForm(updatedFormValue);
}}
form={form}
stateObjectKey={key}
paramType={input}
/>
);
});
return (
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-1">
<p className="font-medium my-0 break-words">
{abiFunction.name}
<InheritanceTooltip inheritedFrom={inheritedFrom} />
</p>
{inputElements}
<div className="flex flex-col md:flex-row justify-between gap-2 flex-wrap">
<div className="grow w-full md:max-w-[80%]">
{result !== null && result !== undefined && (
<div className="bg-secondary rounded-3xl text-sm px-4 py-1.5 break-words overflow-auto">
<p className="font-bold m-0 mb-1">Result:</p>
<pre className="whitespace-pre-wrap break-words">{displayTxResult(result, "sm")}</pre>
</div>
)}
</div>
<button
className="btn btn-secondary btn-sm self-end md:self-start"
onClick={async () => {
const { data } = await refetch();
setResult(data);
}}
disabled={isFetching}
>
{isFetching && <span className="loading loading-spinner loading-xs"></span>}
Read 📡
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,44 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { ContractInput } from "./ContractInput";
import { getFunctionInputKey, getInitialTupleFormState } from "./utilsContract";
import { replacer } from "~~/utils/scaffold-eth/common";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
type TupleProps = {
abiTupleParameter: AbiParameterTuple;
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
parentStateObjectKey: string;
parentForm: Record<string, any> | undefined;
};
export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleProps) => {
const [form, setForm] = useState<Record<string, any>>(() => getInitialTupleFormState(abiTupleParameter));
useEffect(() => {
const values = Object.values(form);
const argsStruct: Record<string, any> = {};
abiTupleParameter.components.forEach((component, componentIndex) => {
argsStruct[component.name || `input_${componentIndex}_`] = values[componentIndex];
});
setParentForm(parentForm => ({ ...parentForm, [parentStateObjectKey]: JSON.stringify(argsStruct, replacer) }));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(form, replacer)]);
return (
<div>
<div tabIndex={0} className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
<input type="checkbox" className="min-h-fit! peer" />
<div className="collapse-title after:top-3.5! p-0 min-h-fit! peer-checked:mb-2 text-primary-content/50">
<p className="m-0 p-0 text-[1rem]">{abiTupleParameter.internalType}</p>
</div>
<div className="ml-3 flex-col space-y-4 border-secondary/80 border-l-2 pl-4 collapse-content">
{abiTupleParameter?.components?.map((param, index) => {
const key = getFunctionInputKey(abiTupleParameter.name || "tuple", param, index);
return <ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />;
})}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,142 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { ContractInput } from "./ContractInput";
import { getFunctionInputKey, getInitialTupleArrayFormState } from "./utilsContract";
import { replacer } from "~~/utils/scaffold-eth/common";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
type TupleArrayProps = {
abiTupleParameter: AbiParameterTuple & { isVirtual?: true };
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
parentStateObjectKey: string;
parentForm: Record<string, any> | undefined;
};
export const TupleArray = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleArrayProps) => {
const [form, setForm] = useState<Record<string, any>>(() => getInitialTupleArrayFormState(abiTupleParameter));
const [additionalInputs, setAdditionalInputs] = useState<Array<typeof abiTupleParameter.components>>([
abiTupleParameter.components,
]);
const depth = (abiTupleParameter.type.match(/\[\]/g) || []).length;
useEffect(() => {
// Extract and group fields based on index prefix
const groupedFields = Object.keys(form).reduce(
(acc, key) => {
const [indexPrefix, ...restArray] = key.split("_");
const componentName = restArray.join("_");
if (!acc[indexPrefix]) {
acc[indexPrefix] = {};
}
acc[indexPrefix][componentName] = form[key];
return acc;
},
{} as Record<string, Record<string, any>>,
);
let argsArray: Array<Record<string, any>> = [];
Object.keys(groupedFields).forEach(key => {
const currentKeyValues = Object.values(groupedFields[key]);
const argsStruct: Record<string, any> = {};
abiTupleParameter.components.forEach((component, componentIndex) => {
argsStruct[component.name || `input_${componentIndex}_`] = currentKeyValues[componentIndex];
});
argsArray.push(argsStruct);
});
if (depth > 1) {
argsArray = argsArray.map(args => {
return args[abiTupleParameter.components[0].name || "tuple"];
});
}
setParentForm(parentForm => {
return { ...parentForm, [parentStateObjectKey]: JSON.stringify(argsArray, replacer) };
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(form, replacer)]);
const addInput = () => {
setAdditionalInputs(previousValue => {
const newAdditionalInputs = [...previousValue, abiTupleParameter.components];
// Add the new inputs to the form
setForm(form => {
const newForm = { ...form };
abiTupleParameter.components.forEach((component, componentIndex) => {
const key = getFunctionInputKey(
`${newAdditionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
component,
componentIndex,
);
newForm[key] = "";
});
return newForm;
});
return newAdditionalInputs;
});
};
const removeInput = () => {
// Remove the last inputs from the form
setForm(form => {
const newForm = { ...form };
abiTupleParameter.components.forEach((component, componentIndex) => {
const key = getFunctionInputKey(
`${additionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
component,
componentIndex,
);
delete newForm[key];
});
return newForm;
});
setAdditionalInputs(inputs => inputs.slice(0, -1));
};
return (
<div>
<div className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
<input type="checkbox" className="min-h-fit! peer" />
<div className="collapse-title after:top-3.5! p-0 min-h-fit! peer-checked:mb-1 text-primary-content/50">
<p className="m-0 text-[1rem]">{abiTupleParameter.internalType}</p>
</div>
<div className="ml-3 flex-col space-y-2 border-secondary/70 border-l-2 pl-4 collapse-content">
{additionalInputs.map((additionalInput, additionalIndex) => (
<div key={additionalIndex} className="space-y-1">
<span className="badge bg-base-300 badge-sm">
{depth > 1 ? `${additionalIndex}` : `tuple[${additionalIndex}]`}
</span>
<div className="space-y-4">
{additionalInput.map((param, index) => {
const key = getFunctionInputKey(
`${additionalIndex}_${abiTupleParameter.name || "tuple"}`,
param,
index,
);
return (
<ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />
);
})}
</div>
</div>
))}
<div className="flex space-x-2">
<button className="btn btn-sm btn-secondary" onClick={addInput}>
+
</button>
{additionalInputs.length > 0 && (
<button className="btn btn-sm btn-secondary" onClick={removeInput}>
-
</button>
)}
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,42 @@
import { TransactionReceipt } from "viem";
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { ObjectFieldDisplay } from "~~/app/debug/_components/contract";
import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
import { replacer } from "~~/utils/scaffold-eth/common";
export const TxReceipt = ({ txResult }: { txResult: TransactionReceipt }) => {
const { copyToClipboard: copyTxResultToClipboard, isCopiedToClipboard: isTxResultCopiedToClipboard } =
useCopyToClipboard();
return (
<div className="flex text-sm rounded-3xl peer-checked:rounded-b-none min-h-0 bg-secondary py-0">
<div className="mt-1 pl-2">
{isTxResultCopiedToClipboard ? (
<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={() => copyTxResultToClipboard(JSON.stringify(txResult, replacer, 2))}
/>
)}
</div>
<div tabIndex={0} className="flex-wrap collapse collapse-arrow">
<input type="checkbox" className="min-h-0! peer" />
<div className="collapse-title text-sm min-h-0! py-1.5 pl-1 after:top-4!">
<strong>Transaction Receipt</strong>
</div>
<div className="collapse-content overflow-auto bg-secondary rounded-t-none rounded-3xl pl-0!">
<pre className="text-xs">
{Object.entries(txResult).map(([k, v]) => (
<ObjectFieldDisplay name={k} value={v} size="xs" leftPad={false} key={k} />
))}
</pre>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,144 @@
"use client";
import { useEffect, useState } from "react";
import { InheritanceTooltip } from "./InheritanceTooltip";
import { Abi, AbiFunction } from "abitype";
import { Address, TransactionReceipt } from "viem";
import { useAccount, useConfig, useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import {
ContractInput,
TxReceipt,
getFunctionInputKey,
getInitialFormState,
getParsedContractFunctionArgs,
transformAbiFunction,
} from "~~/app/debug/_components/contract";
import { IntegerInput } from "~~/components/scaffold-eth";
import { useTransactor } from "~~/hooks/scaffold-eth";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { simulateContractWriteAndNotifyError } from "~~/utils/scaffold-eth/contract";
type WriteOnlyFunctionFormProps = {
abi: Abi;
abiFunction: AbiFunction;
onChange: () => void;
contractAddress: Address;
inheritedFrom?: string;
};
export const WriteOnlyFunctionForm = ({
abi,
abiFunction,
onChange,
contractAddress,
inheritedFrom,
}: WriteOnlyFunctionFormProps) => {
const [form, setForm] = useState<Record<string, any>>(() => getInitialFormState(abiFunction));
const [txValue, setTxValue] = useState<string>("");
const { chain } = useAccount();
const writeTxn = useTransactor();
const { targetNetwork } = useTargetNetwork();
const writeDisabled = !chain || chain?.id !== targetNetwork.id;
const { data: result, isPending, writeContractAsync } = useWriteContract();
const wagmiConfig = useConfig();
const handleWrite = async () => {
if (writeContractAsync) {
try {
const writeContractObj = {
address: contractAddress,
functionName: abiFunction.name,
abi: abi,
args: getParsedContractFunctionArgs(form),
value: BigInt(txValue),
};
await simulateContractWriteAndNotifyError({ wagmiConfig, writeContractParams: writeContractObj });
const makeWriteWithParams = () => writeContractAsync(writeContractObj);
await writeTxn(makeWriteWithParams);
onChange();
} catch (e: any) {
console.error("⚡️ ~ file: WriteOnlyFunctionForm.tsx:handleWrite ~ error", e);
}
}
};
const [displayedTxResult, setDisplayedTxResult] = useState<TransactionReceipt>();
const { data: txResult } = useWaitForTransactionReceipt({
hash: result,
});
useEffect(() => {
setDisplayedTxResult(txResult);
}, [txResult]);
// TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm
const transformedFunction = transformAbiFunction(abiFunction);
const inputs = transformedFunction.inputs.map((input, inputIndex) => {
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
return (
<ContractInput
key={key}
setForm={updatedFormValue => {
setDisplayedTxResult(undefined);
setForm(updatedFormValue);
}}
form={form}
stateObjectKey={key}
paramType={input}
/>
);
});
const zeroInputs = inputs.length === 0 && abiFunction.stateMutability !== "payable";
return (
<div className="py-5 space-y-3 first:pt-0 last:pb-1">
<div className={`flex gap-3 ${zeroInputs ? "flex-row justify-between items-center" : "flex-col"}`}>
<p className="font-medium my-0 break-words">
{abiFunction.name}
<InheritanceTooltip inheritedFrom={inheritedFrom} />
</p>
{inputs}
{abiFunction.stateMutability === "payable" ? (
<div className="flex flex-col gap-1.5 w-full">
<div className="flex items-center ml-2">
<span className="text-xs font-medium mr-2 leading-none">payable value</span>
<span className="block text-xs font-extralight leading-none">wei</span>
</div>
<IntegerInput
value={txValue}
onChange={updatedTxValue => {
setDisplayedTxResult(undefined);
setTxValue(updatedTxValue);
}}
placeholder="value (wei)"
/>
</div>
) : null}
<div className="flex justify-between gap-2">
{!zeroInputs && (
<div className="grow basis-0">{displayedTxResult ? <TxReceipt txResult={displayedTxResult} /> : null}</div>
)}
<div
className={`flex ${
writeDisabled &&
"tooltip tooltip-bottom tooltip-secondary before:content-[attr(data-tip)] before:-translate-x-1/3 before:left-auto before:transform-none"
}`}
data-tip={`${writeDisabled && "Wallet not connected or in the wrong network"}`}
>
<button className="btn btn-secondary btn-sm" disabled={writeDisabled || isPending} onClick={handleWrite}>
{isPending && <span className="loading loading-spinner loading-xs"></span>}
Send 💸
</button>
</div>
</div>
</div>
{zeroInputs && txResult ? (
<div className="grow basis-0">
<TxReceipt txResult={txResult} />
</div>
) : null}
</div>
);
};

View File

@@ -0,0 +1,8 @@
export * from "./ContractInput";
export * from "./ContractUI";
export * from "./DisplayVariable";
export * from "./ReadOnlyFunctionForm";
export * from "./TxReceipt";
export * from "./utilsContract";
export * from "./utilsDisplay";
export * from "./WriteOnlyFunctionForm";

View File

@@ -0,0 +1,166 @@
import { AbiFunction, AbiParameter } from "abitype";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
/**
* Generates a key based on function metadata
*/
const getFunctionInputKey = (functionName: string, input: AbiParameter, inputIndex: number): string => {
const name = input?.name || `input_${inputIndex}_`;
return functionName + "_" + name + "_" + input.internalType + "_" + input.type;
};
const isJsonString = (str: string) => {
try {
JSON.parse(str);
return true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return false;
}
};
const isBigInt = (str: string) => {
if (str.trim().length === 0 || str.startsWith("0")) return false;
try {
BigInt(str);
return true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return false;
}
};
// Recursive function to deeply parse JSON strings, correctly handling nested arrays and encoded JSON strings
const deepParseValues = (value: any): any => {
if (typeof value === "string") {
// first try with bigInt because we losse precision with JSON.parse
if (isBigInt(value)) {
return BigInt(value);
}
if (isJsonString(value)) {
const parsed = JSON.parse(value);
return deepParseValues(parsed);
}
// It's a string but not a JSON string, return as is
return value;
} else if (Array.isArray(value)) {
// If it's an array, recursively parse each element
return value.map(element => deepParseValues(element));
} else if (typeof value === "object" && value !== null) {
// If it's an object, recursively parse each value
return Object.entries(value).reduce((acc: any, [key, val]) => {
acc[key] = deepParseValues(val);
return acc;
}, {});
}
// Handle boolean values represented as strings
if (value === "true" || value === "1" || value === "0x1" || value === "0x01" || value === "0x0001") {
return true;
} else if (value === "false" || value === "0" || value === "0x0" || value === "0x00" || value === "0x0000") {
return false;
}
return value;
};
/**
* parses form input with array support
*/
const getParsedContractFunctionArgs = (form: Record<string, any>) => {
return Object.keys(form).map(key => {
const valueOfArg = form[key];
// Attempt to deeply parse JSON strings
return deepParseValues(valueOfArg);
});
};
const getInitialFormState = (abiFunction: AbiFunction) => {
const initialForm: Record<string, any> = {};
if (!abiFunction.inputs) return initialForm;
abiFunction.inputs.forEach((input, inputIndex) => {
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
initialForm[key] = "";
});
return initialForm;
};
const getInitialTupleFormState = (abiTupleParameter: AbiParameterTuple) => {
const initialForm: Record<string, any> = {};
if (abiTupleParameter.components.length === 0) return initialForm;
abiTupleParameter.components.forEach((component, componentIndex) => {
const key = getFunctionInputKey(abiTupleParameter.name || "tuple", component, componentIndex);
initialForm[key] = "";
});
return initialForm;
};
const getInitialTupleArrayFormState = (abiTupleParameter: AbiParameterTuple) => {
const initialForm: Record<string, any> = {};
if (abiTupleParameter.components.length === 0) return initialForm;
abiTupleParameter.components.forEach((component, componentIndex) => {
const key = getFunctionInputKey("0_" + abiTupleParameter.name || "tuple", component, componentIndex);
initialForm[key] = "";
});
return initialForm;
};
const adjustInput = (input: AbiParameterTuple): AbiParameter => {
if (input.type.startsWith("tuple[")) {
const depth = (input.type.match(/\[\]/g) || []).length;
return {
...input,
components: transformComponents(input.components, depth, {
internalType: input.internalType || "struct",
name: input.name,
}),
};
} else if (input.components) {
return {
...input,
components: input.components.map(value => adjustInput(value as AbiParameterTuple)),
};
}
return input;
};
const transformComponents = (
components: readonly AbiParameter[],
depth: number,
parentComponentData: { internalType?: string; name?: string },
): AbiParameter[] => {
// Base case: if depth is 1 or no components, return the original components
if (depth === 1 || !components) {
return [...components];
}
// Recursive case: wrap components in an additional tuple layer
const wrappedComponents: AbiParameter = {
internalType: `${parentComponentData.internalType || "struct"}`.replace(/\[\]/g, "") + "[]".repeat(depth - 1),
name: `${parentComponentData.name || "tuple"}`,
type: `tuple${"[]".repeat(depth - 1)}`,
components: transformComponents(components, depth - 1, parentComponentData),
};
return [wrappedComponents];
};
const transformAbiFunction = (abiFunction: AbiFunction): AbiFunction => {
return {
...abiFunction,
inputs: abiFunction.inputs.map(value => adjustInput(value as AbiParameterTuple)),
};
};
export {
getFunctionInputKey,
getInitialFormState,
getParsedContractFunctionArgs,
getInitialTupleFormState,
getInitialTupleArrayFormState,
transformAbiFunction,
};

View File

@@ -0,0 +1,114 @@
import { ReactElement, useState } from "react";
import { TransactionBase, TransactionReceipt, formatEther, isAddress, isHex } from "viem";
import { ArrowsRightLeftIcon } from "@heroicons/react/24/solid";
import { Address } from "~~/components/scaffold-eth";
import { replacer } from "~~/utils/scaffold-eth/common";
type DisplayContent =
| string
| number
| bigint
| Record<string, any>
| TransactionBase
| TransactionReceipt
| undefined
| unknown;
type ResultFontSize = "sm" | "base" | "xs" | "lg" | "xl" | "2xl" | "3xl";
export const displayTxResult = (
displayContent: DisplayContent | DisplayContent[],
fontSize: ResultFontSize = "base",
): string | ReactElement | number => {
if (displayContent == null) {
return "";
}
if (typeof displayContent === "bigint") {
return <NumberDisplay value={displayContent} />;
}
if (typeof displayContent === "string") {
if (isAddress(displayContent)) {
return <Address address={displayContent} size={fontSize} onlyEnsOrAddress />;
}
if (isHex(displayContent)) {
return displayContent; // don't add quotes
}
}
if (Array.isArray(displayContent)) {
return <ArrayDisplay values={displayContent} size={fontSize} />;
}
if (typeof displayContent === "object") {
return <StructDisplay struct={displayContent} size={fontSize} />;
}
return JSON.stringify(displayContent, replacer, 2);
};
const NumberDisplay = ({ value }: { value: bigint }) => {
const [isEther, setIsEther] = useState(false);
const asNumber = Number(value);
if (asNumber <= Number.MAX_SAFE_INTEGER && asNumber >= Number.MIN_SAFE_INTEGER) {
return String(value);
}
return (
<div className="flex items-baseline">
{isEther ? "Ξ" + formatEther(value) : String(value)}
<span
className="tooltip tooltip-secondary font-sans ml-2"
data-tip={isEther ? "Multiply by 1e18" : "Divide by 1e18"}
>
<button className="btn btn-ghost btn-circle btn-xs" onClick={() => setIsEther(!isEther)}>
<ArrowsRightLeftIcon className="h-3 w-3 opacity-65" />
</button>
</span>
</div>
);
};
export const ObjectFieldDisplay = ({
name,
value,
size,
leftPad = true,
}: {
name: string;
value: DisplayContent;
size: ResultFontSize;
leftPad?: boolean;
}) => {
return (
<div className={`flex flex-row items-baseline ${leftPad ? "ml-4" : ""}`}>
<span className="text-base-content/60 mr-2">{name}:</span>
<span className="text-base-content">{displayTxResult(value, size)}</span>
</div>
);
};
const ArrayDisplay = ({ values, size }: { values: DisplayContent[]; size: ResultFontSize }) => {
return (
<div className="flex flex-col gap-y-1">
{values.length ? "array" : "[]"}
{values.map((v, i) => (
<ObjectFieldDisplay key={i} name={`[${i}]`} value={v} size={size} />
))}
</div>
);
};
const StructDisplay = ({ struct, size }: { struct: Record<string, any>; size: ResultFontSize }) => {
return (
<div className="flex flex-col gap-y-1">
struct
{Object.entries(struct).map(([k, v]) => (
<ObjectFieldDisplay key={k} name={k} value={v} size={size} />
))}
</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,198 @@
import { FC, useEffect, useRef } from "react";
const drawArrow = (ctx: CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number) => {
const [dx, dy] = [x1 - x2, y1 - y2];
const norm = Math.sqrt(dx * dx + dy * dy);
const [udx, udy] = [dx / norm, dy / norm];
const size = norm / 7;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 + udx * size - udy * size, y2 + udx * size + udy * size);
ctx.moveTo(x2, y2);
ctx.lineTo(x2 + udx * size + udy * size, y2 - udx * size + udy * size);
ctx.stroke();
};
export interface ICurveProps {
ethReserve: number;
tokenReserve: number;
addingEth: number;
addingToken: number;
width: number;
height: number;
}
export const Curve: FC<ICurveProps> = (props: ICurveProps) => {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = ref.current;
if (!canvas) {
return;
}
const textSize = 12;
const width = canvas.width;
const height = canvas.height;
if (props.ethReserve && props.tokenReserve) {
const k = props.ethReserve * props.tokenReserve;
const ctx = canvas.getContext("2d");
if (ctx == null) {
return;
}
ctx.clearRect(0, 0, width, height);
let maxX = k / (props.ethReserve / 4);
let minX = 0;
if (props.addingEth || props.addingToken) {
maxX = k / (props.ethReserve * 0.4);
//maxX = k/(props.ethReserve*0.8)
minX = k / Math.max(0, 500 - props.ethReserve);
}
const maxY = (maxX * height) / width;
const minY = (minX * height) / width;
const plotX = (x: number) => {
return ((x - minX) / (maxX - minX)) * width;
};
const plotY = (y: number) => {
return height - ((y - minY) / (maxY - minY)) * height;
};
ctx.strokeStyle = "#000000";
ctx.fillStyle = "#000000";
ctx.font = textSize + "px Arial";
// +Y axis
ctx.beginPath();
ctx.moveTo(plotX(minX), plotY(0));
ctx.lineTo(plotX(minX), plotY(maxY));
ctx.stroke();
// +X axis
ctx.beginPath();
ctx.moveTo(plotX(0), plotY(minY));
ctx.lineTo(plotX(maxX), plotY(minY));
ctx.stroke();
ctx.lineWidth = 2;
ctx.beginPath();
let first = true;
for (let x = minX; x <= maxX; x += maxX / width) {
/////
const y = k / x;
/////
if (first) {
ctx.moveTo(plotX(x), plotY(y));
first = false;
} else {
ctx.lineTo(plotX(x), plotY(y));
}
}
ctx.stroke();
ctx.lineWidth = 1;
if (props.addingEth) {
const newEthReserve = props.ethReserve + parseFloat(props.addingEth.toString());
ctx.fillStyle = "#bbbbbb";
ctx.beginPath();
ctx.arc(plotX(newEthReserve), plotY(k / newEthReserve), 5, 0, 2 * Math.PI);
ctx.fill();
ctx.strokeStyle = "#009900";
drawArrow(
ctx,
plotX(props.ethReserve),
plotY(props.tokenReserve),
plotX(newEthReserve),
plotY(props.tokenReserve),
);
ctx.fillStyle = "#000000";
ctx.fillText(
"" + props.addingEth + " ETH input",
plotX(props.ethReserve) + textSize,
plotY(props.tokenReserve) - textSize,
);
ctx.strokeStyle = "#990000";
drawArrow(ctx, plotX(newEthReserve), plotY(props.tokenReserve), plotX(newEthReserve), plotY(k / newEthReserve));
const amountGained = Math.round((10000 * (props.addingEth * props.tokenReserve)) / newEthReserve) / 10000;
ctx.fillStyle = "#000000";
ctx.fillText(
"" + amountGained + " 🎈 output (-0.3% fee)",
plotX(newEthReserve) + textSize,
plotY(k / newEthReserve),
);
} else if (props.addingToken) {
const newTokenReserve = props.tokenReserve + parseFloat(props.addingToken.toString());
ctx.fillStyle = "#bbbbbb";
ctx.beginPath();
ctx.arc(plotX(k / newTokenReserve), plotY(newTokenReserve), 5, 0, 2 * Math.PI);
ctx.fill();
//console.log("newTokenReserve",newTokenReserve)
ctx.strokeStyle = "#990000";
drawArrow(
ctx,
plotX(props.ethReserve),
plotY(props.tokenReserve),
plotX(props.ethReserve),
plotY(newTokenReserve),
);
ctx.fillStyle = "#000000";
ctx.fillText(
"" + props.addingToken + " 🎈 input",
plotX(props.ethReserve) + textSize,
plotY(props.tokenReserve),
);
ctx.strokeStyle = "#009900";
drawArrow(
ctx,
plotX(props.ethReserve),
plotY(newTokenReserve),
plotX(k / newTokenReserve),
plotY(newTokenReserve),
);
const amountGained = Math.round((10000 * (props.addingToken * props.ethReserve)) / newTokenReserve) / 10000;
//console.log("amountGained",amountGained)
ctx.fillStyle = "#000000";
ctx.fillText(
"" + amountGained + " ETH output (-0.3% fee)",
plotX(k / newTokenReserve) + textSize,
plotY(newTokenReserve) - textSize,
);
}
ctx.fillStyle = "#0000FF";
ctx.beginPath();
ctx.arc(plotX(props.ethReserve), plotY(props.tokenReserve), 5, 0, 2 * Math.PI);
ctx.fill();
}
}, [props]);
return (
<div style={{ position: "relative", width: props.width, height: props.height }}>
<canvas style={{ position: "absolute", left: 0, top: 0 }} ref={ref} width={props.width} height={props.height} />
<div style={{ position: "absolute", left: "20%", bottom: -20 }}>-- ETH Reserve --{">"}</div>
<div
style={{ position: "absolute", left: -20, bottom: "20%", transform: "rotate(-90deg)", transformOrigin: "0 0" }}
>
-- Token Reserve --{">"}
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export * from "./Curve";

View File

@@ -0,0 +1,298 @@
"use client";
import { useEffect, useState } from "react";
import { Curve } from "./_components";
import type { NextPage } from "next";
import { Address as AddressType, formatEther, isAddress, parseEther } from "viem";
import { useAccount } from "wagmi";
import { Address, AddressInput, Balance, EtherInput, IntegerInput } from "~~/components/scaffold-eth";
import { useDeployedContractInfo, useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
import { useWatchBalance } from "~~/hooks/scaffold-eth/useWatchBalance";
// REGEX for number inputs (only allow numbers and a single decimal point)
const NUMBER_REGEX = /^\.?\d+\.?\d*$/;
const Dex: NextPage = () => {
const [isLoading, setIsLoading] = useState(true);
const [ethToTokenAmount, setEthToTokenAmount] = useState("");
const [tokenToETHAmount, setTokenToETHAmount] = useState("");
const [depositAmount, setDepositAmount] = useState("");
const [withdrawAmount, setWithdrawAmount] = useState("");
const [approveSpender, setApproveSpender] = useState("");
const [approveAmount, setApproveAmount] = useState("");
const [accountBalanceOf, setAccountBalanceOf] = useState("");
const { data: DEXInfo } = useDeployedContractInfo({ contractName: "DEX" });
const { data: BalloonsInfo } = useDeployedContractInfo({ contractName: "Balloons" });
const { address: connectedAccount } = useAccount();
const { data: DEXBalloonBalance } = useScaffoldReadContract({
contractName: "Balloons",
functionName: "balanceOf",
args: [DEXInfo?.address?.toString()],
});
useEffect(() => {
if (DEXBalloonBalance !== undefined) {
setIsLoading(false);
}
}, [DEXBalloonBalance]);
const { data: DEXtotalLiquidity } = useScaffoldReadContract({
contractName: "DEX",
functionName: "totalLiquidity",
});
const { writeContractAsync: writeDexContractAsync } = useScaffoldWriteContract({ contractName: "DEX" });
const { writeContractAsync: writeBalloonsContractAsync } = useScaffoldWriteContract({ contractName: "Balloons" });
const { data: balanceOfWrite } = useScaffoldReadContract({
contractName: "Balloons",
functionName: "balanceOf",
args: [accountBalanceOf as AddressType],
query: {
enabled: isAddress(accountBalanceOf),
},
});
const { data: contractBalance } = useScaffoldReadContract({
contractName: "Balloons",
functionName: "balanceOf",
args: [DEXInfo?.address],
});
const { data: userBalloons } = useScaffoldReadContract({
contractName: "Balloons",
functionName: "balanceOf",
args: [connectedAccount],
});
const { data: userLiquidity } = useScaffoldReadContract({
contractName: "DEX",
functionName: "getLiquidity",
args: [connectedAccount],
});
const { data: contractETHBalance } = useWatchBalance({ address: DEXInfo?.address });
return (
<>
<h1 className="text-center mb-4 mt-5">
<span className="block text-xl text-right mr-7">
🎈: {parseFloat(formatEther(userBalloons || 0n)).toFixed(4)}
</span>
<span className="block text-xl text-right mr-7">
💦💦: {parseFloat(formatEther(userLiquidity || 0n)).toFixed(4)}
</span>
<span className="block text-2xl mb-2">SpeedRunEthereum</span>
<span className="block text-4xl font-bold">Challenge: Build a DEX </span>
</h1>
<div className="items-start pt-10 grid grid-cols-1 md:grid-cols-2 content-start">
<div className="px-5 py-5">
<div className="bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl p-8 m-8">
<div className="flex flex-col text-center">
<span className="text-3xl font-semibold mb-2">DEX Contract</span>
<span className="block text-2xl mb-2 mx-auto">
<Address size="xl" address={DEXInfo?.address} />
</span>
<span className="flex flex-row mx-auto mt-5">
{" "}
<Balance className="text-xl" address={DEXInfo?.address} />
{isLoading ? (
<span>Loading...</span>
) : (
<span className="pl-8 text-xl">🎈 {parseFloat(formatEther(DEXBalloonBalance || 0n)).toFixed(4)}</span>
)}
</span>
</div>
<div className="py-3 px-4">
<div className="flex mb-4 justify-center items-center">
<span className="w-1/2">
ethToToken{" "}
<EtherInput
value={ethToTokenAmount}
onChange={value => {
setTokenToETHAmount("");
setEthToTokenAmount(value);
}}
name="ethToToken"
/>
</span>
<button
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
onClick={async () => {
try {
await writeDexContractAsync({
functionName: "ethToToken",
value: NUMBER_REGEX.test(ethToTokenAmount) ? parseEther(ethToTokenAmount) : 0n,
});
} catch (err) {
console.error("Error calling ethToToken function", err);
}
}}
>
Send
</button>
</div>
<div className="flex justify-center items-center">
<span className="w-1/2">
tokenToETH{" "}
<IntegerInput
value={tokenToETHAmount}
onChange={value => {
setEthToTokenAmount("");
setTokenToETHAmount(value.toString());
}}
name="tokenToETH"
disableMultiplyBy1e18
/>
</span>
<button
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
onClick={async () => {
try {
await writeDexContractAsync({
functionName: "tokenToEth",
// @ts-expect-error - Show error on frontend while sending, if user types invalid number
args: [NUMBER_REGEX.test(tokenToETHAmount) ? parseEther(tokenToETHAmount) : tokenToETHAmount],
});
} catch (err) {
console.error("Error calling tokenToEth function", err);
}
}}
>
Send
</button>
</div>
</div>
<p className="text-center text-primary-content text-xl mt-8 -ml-8">
Liquidity ({DEXtotalLiquidity ? parseFloat(formatEther(DEXtotalLiquidity || 0n)).toFixed(4) : "None"})
</p>
<div className="px-4 py-3">
<div className="flex mb-4 justify-center items-center">
<span className="w-1/2">
Deposit <EtherInput value={depositAmount} onChange={value => setDepositAmount(value)} />
</span>
<button
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
onClick={async () => {
try {
await writeDexContractAsync({
functionName: "deposit",
value: NUMBER_REGEX.test(depositAmount) ? parseEther(depositAmount) : 0n,
});
} catch (err) {
console.error("Error calling deposit function", err);
}
}}
>
Send
</button>
</div>
<div className="flex justify-center items-center">
<span className="w-1/2">
Withdraw <EtherInput value={withdrawAmount} onChange={value => setWithdrawAmount(value)} />
</span>
<button
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-6 mx-5"
onClick={async () => {
try {
await writeDexContractAsync({
functionName: "withdraw",
// @ts-expect-error - Show error on frontend while sending, if user types invalid number
args: [NUMBER_REGEX.test(withdrawAmount) ? parseEther(withdrawAmount) : withdrawAmount],
});
} catch (err) {
console.error("Error calling withdraw function", err);
}
}}
>
Send
</button>
</div>
</div>
</div>
<div className="space-y-4 bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl py-5 p-8 m-8">
<div className="flex flex-col text-center mt-2 mb-4 px-4">
<span className="block text-3xl font-semibold mb-2">Balloons</span>
<span className="mx-auto">
<Address size="xl" address={BalloonsInfo?.address} />
</span>
</div>
<div className=" px-4 py-3">
<div className="flex flex-col gap-4 mb-4 justify-center items-center">
<span className="w-1/2">
Approve{" "}
<AddressInput
value={approveSpender ?? ""}
onChange={value => setApproveSpender(value)}
placeholder="Address Spender"
/>
</span>
<span className="w-1/2">
<IntegerInput
value={approveAmount}
onChange={value => setApproveAmount(value.toString())}
placeholder="Amount"
disableMultiplyBy1e18
/>
</span>
<button
className="btn btn-primary h-[2.2rem] min-h-[2.2rem] mt-auto"
onClick={async () => {
try {
await writeBalloonsContractAsync({
functionName: "approve",
args: [
approveSpender as AddressType,
// @ts-expect-error - Show error on frontend while sending, if user types invalid number
NUMBER_REGEX.test(approveAmount) ? parseEther(approveAmount) : approveAmount,
],
});
} catch (err) {
console.error("Error calling approve function", err);
}
}}
>
Send
</button>
<span className="w-1/2">
balanceOf{" "}
<AddressInput
value={accountBalanceOf}
onChange={value => setAccountBalanceOf(value)}
placeholder="address Account"
/>
</span>
{balanceOfWrite === undefined ? (
<h1></h1>
) : (
<span className="font-bold bg-primary px-3 rounded-2xl">
BAL Balance: {parseFloat(formatEther(balanceOfWrite || 0n)).toFixed(4)}
</span>
)}
</div>
</div>
</div>
</div>
<div className="mx-auto p-8 m-8 md:sticky md:top-0">
<Curve
addingEth={ethToTokenAmount !== "" ? parseFloat(ethToTokenAmount.toString()) : 0}
addingToken={tokenToETHAmount !== "" ? parseFloat(tokenToETHAmount.toString()) : 0}
ethReserve={parseFloat(formatEther(contractETHBalance?.value || 0n))}
tokenReserve={parseFloat(formatEther(contractBalance || 0n))}
width={500}
height={500}
/>
</div>
</div>
</>
);
};
export default Dex;

View File

@@ -0,0 +1,216 @@
"use client";
import type { NextPage } from "next";
import { formatEther } from "viem";
import { Address } from "~~/components/scaffold-eth";
import { useScaffoldEventHistory } from "~~/hooks/scaffold-eth";
const Events: NextPage = () => {
const { data: EthToTokenEvents, isLoading: isEthToTokenEventsLoading } = useScaffoldEventHistory({
contractName: "DEX",
eventName: "EthToTokenSwap",
});
const { data: tokenToEthEvents, isLoading: isTokenToEthEventsLoading } = useScaffoldEventHistory({
contractName: "DEX",
eventName: "TokenToEthSwap",
});
const { data: liquidityProvidedEvents, isLoading: isLiquidityProvidedEventsLoading } = useScaffoldEventHistory({
contractName: "DEX",
eventName: "LiquidityProvided",
});
const { data: liquidityRemovedEvents, isLoading: isLiquidityRemovedEventsLoading } = useScaffoldEventHistory({
contractName: "DEX",
eventName: "LiquidityRemoved",
});
return (
<>
<div className="flex items-center flex-col flex-grow pt-10">
{isEthToTokenEventsLoading ? (
<div className="flex justify-center items-center mt-10">
<span className="loading loading-spinner loading-lg"></span>
</div>
) : (
<div>
<div className="text-center mb-4">
<span className="block text-2xl font-bold">ETH To Balloons Events</span>
</div>
<div className="overflow-x-auto shadow-lg">
<table className="table table-zebra w-full">
<thead>
<tr>
<th className="bg-primary">Address</th>
<th className="bg-primary">Amount of ETH in</th>
<th className="bg-primary">Amount of Balloons out</th>
</tr>
</thead>
<tbody>
{!EthToTokenEvents || EthToTokenEvents.length === 0 ? (
<tr>
<td colSpan={3} className="text-center">
No events found
</td>
</tr>
) : (
EthToTokenEvents?.map((event, index) => {
return (
<tr key={index}>
<td className="text-center">
<Address address={event.args.swapper} />
</td>
<td>{parseFloat(formatEther(event.args.ethInput || 0n)).toFixed(4)}</td>
<td>{parseFloat(formatEther(event.args.tokenOutput || 0n)).toFixed(4)}</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
)}
{isTokenToEthEventsLoading ? (
<div className="flex justify-center items-center mt-10">
<span className="loading loading-spinner loading-lg"></span>
</div>
) : (
<div className="mt-8">
<div className="text-center mb-4">
<span className="block text-2xl font-bold">Balloons To ETH Events</span>
</div>
<div className="overflow-x-auto shadow-lg">
<table className="table table-zebra w-full">
<thead>
<tr>
<th className="bg-primary">Address</th>
<th className="bg-primary">Amount of Balloons In</th>
<th className="bg-primary">Amount of ETH Out</th>
</tr>
</thead>
<tbody>
{!tokenToEthEvents || tokenToEthEvents.length === 0 ? (
<tr>
<td colSpan={3} className="text-center">
No events found
</td>
</tr>
) : (
tokenToEthEvents?.map((event, index) => {
return (
<tr key={index}>
<td className="text-center">
<Address address={event.args.swapper} />
</td>
<td>{parseFloat(formatEther(event.args.tokensInput || 0n)).toFixed(4)}</td>
<td>{parseFloat(formatEther(event.args.ethOutput || 0n)).toFixed(4)}</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
)}
{isLiquidityProvidedEventsLoading ? (
<div className="flex justify-center items-center mt-10">
<span className="loading loading-spinner loading-lg"></span>
</div>
) : (
<div className="mt-8">
<div className="text-center mb-4">
<span className="block text-2xl font-bold">Liquidity Provided Events</span>
</div>
<div className="overflow-x-auto shadow-lg">
<table className="table table-zebra w-full">
<thead>
<tr>
<th className="bg-primary">Address</th>
<th className="bg-primary">Amount of ETH In</th>
<th className="bg-primary">Amount of Balloons In</th>
<th className="bg-primary">Lİquidity Minted</th>
</tr>
</thead>
<tbody>
{!liquidityProvidedEvents || liquidityProvidedEvents.length === 0 ? (
<tr>
<td colSpan={4} className="text-center">
No events found
</td>
</tr>
) : (
liquidityProvidedEvents?.map((event, index) => {
return (
<tr key={index}>
<td className="text-center">
<Address address={event.args.liquidityProvider} />
</td>
<td>{parseFloat(formatEther(event.args.ethInput || 0n)).toFixed(4)}</td>
<td>{parseFloat(formatEther(event.args.tokensInput || 0n)).toFixed(4)}</td>
<td>{parseFloat(formatEther(event.args.liquidityMinted || 0n)).toFixed(4)}</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
)}
{isLiquidityRemovedEventsLoading ? (
<div className="flex justify-center items-center mt-10">
<span className="loading loading-spinner loading-lg"></span>
</div>
) : (
<div className="mt-8 mb-8">
<div className="text-center mb-4">
<span className="block text-2xl font-bold">Liquidity Removed Events</span>
</div>
<div className="overflow-x-auto shadow-lg mb-5">
<table className="table table-zebra w-full">
<thead>
<tr>
<th className="bg-primary">Address</th>
<th className="bg-primary">Amount of ETH Out</th>
<th className="bg-primary">Amount of Balloons Out</th>
<th className="bg-primary">Liquidity Withdrawn</th>
</tr>
</thead>
<tbody>
{!liquidityRemovedEvents || liquidityRemovedEvents.length === 0 ? (
<tr>
<td colSpan={4} className="text-center">
No events found
</td>
</tr>
) : (
liquidityRemovedEvents?.map((event, index) => {
return (
<tr key={index}>
<td className="text-center">
<Address address={event.args.liquidityRemover} />
</td>
<td>{parseFloat(formatEther(event.args.ethOutput || 0n)).toFixed(4)}</td>
<td>{parseFloat(formatEther(event.args.tokensOutput || 0n)).toFixed(4)}</td>
<td>{parseFloat(formatEther(event.args.liquidityWithdrawn || 0n)).toFixed(4)}</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
)}
</div>
</>
);
};
export default Events;

View File

@@ -0,0 +1,30 @@
import { Space_Grotesk } from "next/font/google";
import "@rainbow-me/rainbowkit/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: "Dex | SpeedRunEthereum",
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,99 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import type { NextPage } from "next";
import { useAccount } from "wagmi";
import { BugAntIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Address } from "~~/components/scaffold-eth";
const Home: NextPage = () => {
const { address: connectedAddress } = useAccount();
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: Dex 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} />
</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">SpeedRunEthereum</span>
<span className="block text-4xl font-bold">Challenge: Build a DEX</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">
This challenge will help you build/understand a simple decentralized exchange, with one token-pair
(ERC20 BALLOONS ($BAL) and ETH). This repo is an updated version of the{" "}
<a
href="https://medium.com/@austin_48503/%EF%B8%8F-minimum-viable-exchange-d84f30bd0c90"
target="_blank"
rel="noreferrer"
className="underline"
>
original tutorial
</a>{" "}
and challenge repos before it. Please read the intro for a background on what we are building first!
</p>
<p className="text-center text-lg">
🌟 The final deliverable is an app that allows users to seamlessly trade ERC20 BALLOONS ($BAL) with
ETH in a decentralized manner. Users will be able to connect their wallets, view their token
balances, and buy or sell their tokens according to a price formula! 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;