Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.2
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
12
packages/nextjs/app/blockexplorer/_components/BackButton.tsx
Normal file
12
packages/nextjs/app/blockexplorer/_components/BackButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
102
packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx
Normal file
102
packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx
Normal 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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
49
packages/nextjs/app/blockexplorer/_components/SearchBar.tsx
Normal file
49
packages/nextjs/app/blockexplorer/_components/SearchBar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
7
packages/nextjs/app/blockexplorer/_components/index.tsx
Normal file
7
packages/nextjs/app/blockexplorer/_components/index.tsx
Normal 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";
|
||||
101
packages/nextjs/app/blockexplorer/address/[address]/page.tsx
Normal file
101
packages/nextjs/app/blockexplorer/address/[address]/page.tsx
Normal 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;
|
||||
12
packages/nextjs/app/blockexplorer/layout.tsx
Normal file
12
packages/nextjs/app/blockexplorer/layout.tsx
Normal 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;
|
||||
83
packages/nextjs/app/blockexplorer/page.tsx
Normal file
83
packages/nextjs/app/blockexplorer/page.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
73
packages/nextjs/app/debug/_components/DebugContracts.tsx
Normal file
73
packages/nextjs/app/debug/_components/DebugContracts.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
104
packages/nextjs/app/debug/_components/contract/ContractUI.tsx
Normal file
104
packages/nextjs/app/debug/_components/contract/ContractUI.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
44
packages/nextjs/app/debug/_components/contract/Tuple.tsx
Normal file
44
packages/nextjs/app/debug/_components/contract/Tuple.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
142
packages/nextjs/app/debug/_components/contract/TupleArray.tsx
Normal file
142
packages/nextjs/app/debug/_components/contract/TupleArray.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
42
packages/nextjs/app/debug/_components/contract/TxReceipt.tsx
Normal file
42
packages/nextjs/app/debug/_components/contract/TxReceipt.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
8
packages/nextjs/app/debug/_components/contract/index.tsx
Normal file
8
packages/nextjs/app/debug/_components/contract/index.tsx
Normal 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";
|
||||
166
packages/nextjs/app/debug/_components/contract/utilsContract.tsx
Normal file
166
packages/nextjs/app/debug/_components/contract/utilsContract.tsx
Normal 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,
|
||||
};
|
||||
114
packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx
Normal file
114
packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
28
packages/nextjs/app/debug/page.tsx
Normal file
28
packages/nextjs/app/debug/page.tsx
Normal 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;
|
||||
198
packages/nextjs/app/dex/_components/Curve.tsx
Normal file
198
packages/nextjs/app/dex/_components/Curve.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
packages/nextjs/app/dex/_components/index.tsx
Normal file
1
packages/nextjs/app/dex/_components/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Curve";
|
||||
298
packages/nextjs/app/dex/page.tsx
Normal file
298
packages/nextjs/app/dex/page.tsx
Normal 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;
|
||||
216
packages/nextjs/app/events/page.tsx
Normal file
216
packages/nextjs/app/events/page.tsx
Normal 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;
|
||||
30
packages/nextjs/app/layout.tsx
Normal file
30
packages/nextjs/app/layout.tsx
Normal 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;
|
||||
16
packages/nextjs/app/not-found.tsx
Normal file
16
packages/nextjs/app/not-found.tsx
Normal 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're looking for doesn't exist.</p>
|
||||
<Link href="/" className="btn btn-primary">
|
||||
Go Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
packages/nextjs/app/page.tsx
Normal file
99
packages/nextjs/app/page.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user