diff --git a/README.md b/README.md index d0afc2cae..542c0f48b 100644 --- a/README.md +++ b/README.md @@ -213,8 +213,8 @@ const { } = useScaffoldEventHistory({ contractName: "YourContract", eventName: "GreetingChange", - // Specify the starting block number from which to read events. - fromBlock: 31231, + // Specify the starting block number from which to read events, this is a bigint. + fromBlock: 31231n, blockData: true, // Apply filters to the event based on parameter names and values { [parameterName]: value }, filters: { premium: true } @@ -251,13 +251,12 @@ const { data: yourContract } = useScaffoldContract({ await yourContract?.greeting(); // Used to write to a contract and can be called in any function -import { Signer } from "ethers"; -import { useSigner } from "wagmi"; +import { useWalletClient } from "wagmi"; -const { data: signer, isError, isLoading } = useSigner(); +const { data: walletClient } = useWalletClient(); const { data: yourContract } = useScaffoldContract({ contractName: "YourContract", - signerOrProvider: signer as Signer, + walletClient, }); const setGreeting = async () => { // Call the method in any function diff --git a/packages/nextjs/components/blockexplorer/AddressLogsTab.tsx b/packages/nextjs/components/blockexplorer/AddressLogsTab.tsx index 2a548a7b7..9d2ab0e8f 100644 --- a/packages/nextjs/components/blockexplorer/AddressLogsTab.tsx +++ b/packages/nextjs/components/blockexplorer/AddressLogsTab.tsx @@ -1,6 +1,8 @@ +import { Address } from "viem"; import { useContractLogs } from "~~/hooks/scaffold-eth"; +import { replacer } from "~~/utils/scaffold-eth/common"; -export const AddressLogsTab = ({ address }: { address: string }) => { +export const AddressLogsTab = ({ address }: { address: Address }) => { const contractLogs = useContractLogs(address); return ( @@ -9,7 +11,7 @@ export const AddressLogsTab = ({ address }: { address: string }) => {
           {contractLogs.map((log, i) => (
             
- Log: {JSON.stringify(log, null, 2)} + Log: {JSON.stringify(log, replacer, 2)}
))}
diff --git a/packages/nextjs/components/blockexplorer/AddressStorageTab.tsx b/packages/nextjs/components/blockexplorer/AddressStorageTab.tsx index 85f869c66..046dd908e 100644 --- a/packages/nextjs/components/blockexplorer/AddressStorageTab.tsx +++ b/packages/nextjs/components/blockexplorer/AddressStorageTab.tsx @@ -1,8 +1,11 @@ import { useEffect, useState } from "react"; -import { localhost } from "wagmi/chains"; -import { getLocalProvider } from "~~/utils/scaffold-eth"; +import { createPublicClient, http, toHex } from "viem"; +import { hardhat } from "wagmi/chains"; -const provider = getLocalProvider(localhost); +const publicClient = createPublicClient({ + chain: hardhat, + transport: http(), +}); export const AddressStorageTab = ({ address }: { address: string }) => { const [storage, setStorage] = useState([]); @@ -14,7 +17,10 @@ export const AddressStorageTab = ({ address }: { address: string }) => { let idx = 0; while (true) { - const storageAtPosition = await provider?.getStorageAt(address, idx); + const storageAtPosition = await publicClient.getStorageAt({ + address: address, + slot: toHex(idx), + }); if (storageAtPosition === "0x" + "0".repeat(64)) break; diff --git a/packages/nextjs/components/blockexplorer/SearchBar.tsx b/packages/nextjs/components/blockexplorer/SearchBar.tsx index 73817b91d..e9877c712 100644 --- a/packages/nextjs/components/blockexplorer/SearchBar.tsx +++ b/packages/nextjs/components/blockexplorer/SearchBar.tsx @@ -1,19 +1,20 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { ethers } from "ethers"; -import { localhost } from "wagmi/chains"; -import { getLocalProvider } from "~~/utils/scaffold-eth"; +import { isAddress, isHex } from "viem"; +import { usePublicClient } from "wagmi"; +import { hardhat } from "wagmi/chains"; -const provider = getLocalProvider(localhost); 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 (ethers.utils.isHexString(searchInput)) { + if (isHex(searchInput)) { try { - const tx = await provider?.getTransaction(searchInput); + const tx = await client.getTransaction({ hash: searchInput }); if (tx) { router.push(`/blockexplorer/transaction/${searchInput}`); return; @@ -23,7 +24,7 @@ export const SearchBar = () => { } } - if (ethers.utils.isAddress(searchInput)) { + if (isAddress(searchInput)) { router.push(`/blockexplorer/address/${searchInput}`); return; } diff --git a/packages/nextjs/components/blockexplorer/TransactionsTable.tsx b/packages/nextjs/components/blockexplorer/TransactionsTable.tsx index e94135c86..469548f0f 100644 --- a/packages/nextjs/components/blockexplorer/TransactionsTable.tsx +++ b/packages/nextjs/components/blockexplorer/TransactionsTable.tsx @@ -1,7 +1,7 @@ -import { ethers } from "ethers"; +import { formatEther } from "viem"; import { TransactionHash } from "~~/components/blockexplorer/TransactionHash"; import { Address } from "~~/components/scaffold-eth"; -import { getTargetNetwork } from "~~/utils/scaffold-eth"; +import { TransactionWithFunction, getTargetNetwork } from "~~/utils/scaffold-eth"; import { TransactionsTableProps } from "~~/utils/scaffold-eth/"; export const TransactionsTable = ({ blocks, transactionReceipts, isLoading }: TransactionsTableProps) => { @@ -36,10 +36,10 @@ export const TransactionsTable = ({ blocks, transactionReceipts, isLoading }: Tr ) : ( {blocks.map(block => - block.transactions.map(tx => { + (block.transactions as TransactionWithFunction[]).map(tx => { const receipt = transactionReceipts[tx.hash]; - const timeMined = new Date(block.timestamp * 1000).toLocaleString(); - const functionCalled = tx.data.substring(0, 10); + const timeMined = new Date(Number(block.timestamp) * 1000).toLocaleString(); + const functionCalled = tx.input.substring(0, 10); return ( @@ -52,7 +52,7 @@ export const TransactionsTable = ({ blocks, transactionReceipts, isLoading }: Tr {functionCalled} )} - {block.number} + {block.number?.toString()} {timeMined}
@@ -68,7 +68,7 @@ export const TransactionsTable = ({ blocks, transactionReceipts, isLoading }: Tr )} - {ethers.utils.formatEther(tx.value)} {targetNetwork.nativeCurrency.symbol} + {formatEther(tx.value)} {targetNetwork.nativeCurrency.symbol} ); diff --git a/packages/nextjs/components/example-ui/ContractData.tsx b/packages/nextjs/components/example-ui/ContractData.tsx index 317aa6b3a..98786141c 100644 --- a/packages/nextjs/components/example-ui/ContractData.tsx +++ b/packages/nextjs/components/example-ui/ContractData.tsx @@ -33,8 +33,11 @@ export const ContractData = () => { useScaffoldEventSubscriber({ contractName: "YourContract", eventName: "GreetingChange", - listener: (greetingSetter, newGreeting, premium, value) => { - console.log(greetingSetter, newGreeting, premium, value); + listener: logs => { + logs.map(log => { + const { greetingSetter, value, premium, newGreeting } = log.args; + console.log("📡 GreetingChange event", greetingSetter, value, premium, newGreeting); + }); }, }); @@ -45,7 +48,7 @@ export const ContractData = () => { } = useScaffoldEventHistory({ contractName: "YourContract", eventName: "GreetingChange", - fromBlock: Number(process.env.NEXT_PUBLIC_DEPLOY_BLOCK) || 0, + fromBlock: process.env.NEXT_PUBLIC_DEPLOY_BLOCK ? BigInt(process.env.NEXT_PUBLIC_DEPLOY_BLOCK) : 0n, filters: { greetingSetter: address }, blockData: true, }); diff --git a/packages/nextjs/components/scaffold-eth/Address.tsx b/packages/nextjs/components/scaffold-eth/Address.tsx index 903ee9d26..3b0c9172d 100644 --- a/packages/nextjs/components/scaffold-eth/Address.tsx +++ b/packages/nextjs/components/scaffold-eth/Address.tsx @@ -1,9 +1,8 @@ import { useEffect, useState } from "react"; import Link from "next/link"; -import { ethers } from "ethers"; -import { isAddress } from "ethers/lib/utils"; import Blockies from "react-blockies"; import { CopyToClipboard } from "react-copy-to-clipboard"; +import { isAddress } from "viem"; import { useEnsAvatar, useEnsName } from "wagmi"; import { hardhat } from "wagmi/chains"; import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; @@ -36,8 +35,8 @@ export const Address = ({ address, disableAddressLink, format, size = "base" }: const { data: fetchedEns } = useEnsName({ address, enabled: isAddress(address ?? ""), chainId: 1 }); const { data: fetchedEnsAvatar } = useEnsAvatar({ - address, - enabled: isAddress(address ?? ""), + name: fetchedEns, + enabled: Boolean(fetchedEns), chainId: 1, cacheTime: 30_000, }); @@ -63,7 +62,7 @@ export const Address = ({ address, disableAddressLink, format, size = "base" }: ); } - if (!ethers.utils.isAddress(address)) { + if (!isAddress(address)) { return Wrong address; } diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx index e5119bffc..396f4a9e3 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from "react"; -import { utils } from "ethers"; +import { AbiParameter } from "abitype"; import { AddressInput, Bytes32Input, @@ -11,9 +11,9 @@ import { type ContractInputProps = { setForm: Dispatch>>; - form: Record; + form: Record | undefined; stateObjectKey: string; - paramType: utils.ParamType; + paramType: AbiParameter; }; /** @@ -22,7 +22,7 @@ type ContractInputProps = { export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: ContractInputProps) => { const inputProps = { name: stateObjectKey, - value: form[stateObjectKey], + value: form?.[stateObjectKey], placeholder: paramType.name ? `${paramType.type} ${paramType.name}` : paramType.type, onChange: (value: any) => { setForm(form => ({ ...form, [stateObjectKey]: value })); diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractReadMethods.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractReadMethods.tsx new file mode 100644 index 000000000..eaf11a8fd --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractReadMethods.tsx @@ -0,0 +1,29 @@ +import { ReadOnlyFunctionForm } from "./ReadOnlyFunctionForm"; +import { Abi, AbiFunction } from "abitype"; +import { Contract, ContractName } from "~~/utils/scaffold-eth/contract"; + +export const ContractReadMethods = ({ deployedContractData }: { deployedContractData: Contract }) => { + 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; + }); + + if (!functionsToDisplay.length) { + return <>No read methods; + } + + return ( + <> + {functionsToDisplay.map(fn => ( + + ))} + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx index f8be03df1..202ad19e0 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx @@ -1,15 +1,9 @@ -import { useMemo, useState } from "react"; -import { Abi } from "abitype"; -import { useContract, useProvider } from "wagmi"; +import { useReducer } from "react"; +import { ContractReadMethods } from "./ContractReadMethods"; +import { ContractVariables } from "./ContractVariables"; +import { ContractWriteMethods } from "./ContractWriteMethods"; import { Spinner } from "~~/components/Spinner"; -import { - Address, - Balance, - getAllContractFunctions, - getContractReadOnlyMethodsWithParams, - getContractVariablesAndNoParamsReadMethods, - getContractWriteMethods, -} from "~~/components/scaffold-eth"; +import { Address, Balance } from "~~/components/scaffold-eth"; import { useDeployedContractInfo, useNetworkColor } from "~~/hooks/scaffold-eth"; import { getTargetNetwork } from "~~/utils/scaffold-eth"; import { ContractName } from "~~/utils/scaffold-eth/contract"; @@ -23,34 +17,12 @@ type ContractUIProps = { * UI component to interface with deployed contracts. **/ export const ContractUI = ({ contractName, className = "" }: ContractUIProps) => { - const provider = useProvider(); - const [refreshDisplayVariables, setRefreshDisplayVariables] = useState(false); + const [refreshDisplayVariables, triggerRefreshDisplayVariables] = useReducer(value => !value, false); const configuredNetwork = getTargetNetwork(); const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); const networkColor = useNetworkColor(); - const contract = useContract({ - address: deployedContractData?.address, - abi: deployedContractData?.abi as Abi, - signerOrProvider: provider, - }); - - const displayedContractFunctions = useMemo(() => getAllContractFunctions(contract), [contract]); - - const contractVariablesDisplay = useMemo(() => { - return getContractVariablesAndNoParamsReadMethods(contract, displayedContractFunctions, refreshDisplayVariables); - }, [contract, displayedContractFunctions, refreshDisplayVariables]); - - const contractMethodsDisplay = useMemo( - () => getContractReadOnlyMethodsWithParams(contract, displayedContractFunctions), - [contract, displayedContractFunctions], - ); - const contractWriteMethods = useMemo( - () => getContractWriteMethods(contract, displayedContractFunctions, setRefreshDisplayVariables), - [contract, displayedContractFunctions], - ); - if (deployedContractLoading) { return (
@@ -90,7 +62,10 @@ export const ContractUI = ({ contractName, className = "" }: ContractUIProps) => )}
- {contractVariablesDisplay.methods.length > 0 ? contractVariablesDisplay.methods : "No contract variables"} +
@@ -102,7 +77,7 @@ export const ContractUI = ({ contractName, className = "" }: ContractUIProps) =>
- {contractMethodsDisplay.methods.length > 0 ? contractMethodsDisplay.methods : "No read methods"} +
@@ -114,7 +89,10 @@ export const ContractUI = ({ contractName, className = "" }: ContractUIProps) =>
- {contractWriteMethods.methods.length > 0 ? contractWriteMethods.methods : "No write methods"} +
diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractVariables.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractVariables.tsx new file mode 100644 index 000000000..804569927 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractVariables.tsx @@ -0,0 +1,40 @@ +import { DisplayVariable } from "./DisplayVariable"; +import { Abi, AbiFunction } from "abitype"; +import { Contract, ContractName } from "~~/utils/scaffold-eth/contract"; + +export const ContractVariables = ({ + refreshDisplayVariables, + deployedContractData, +}: { + refreshDisplayVariables: boolean; + deployedContractData: Contract; +}) => { + 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; + }); + + if (!functionsToDisplay.length) { + return <>No contract variables; + } + + return ( + <> + {functionsToDisplay.map(fn => ( + + ))} + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractWriteMethods.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractWriteMethods.tsx new file mode 100644 index 000000000..c13fe8c22 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractWriteMethods.tsx @@ -0,0 +1,39 @@ +import { WriteOnlyFunctionForm } from "./WriteOnlyFunctionForm"; +import { Abi, AbiFunction } from "abitype"; +import { Contract, ContractName } from "~~/utils/scaffold-eth/contract"; + +export const ContractWriteMethods = ({ + onChange, + deployedContractData, +}: { + onChange: () => void; + deployedContractData: Contract; +}) => { + 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; + }); + + if (!functionsToDisplay.length) { + return <>No write methods; + } + + return ( + <> + {functionsToDisplay.map((fn, idx) => ( + + ))} + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/DisplayVariable.tsx b/packages/nextjs/components/scaffold-eth/Contract/DisplayVariable.tsx index 13deccd0d..f0f05f61a 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/DisplayVariable.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/DisplayVariable.tsx @@ -1,32 +1,27 @@ import { useEffect } from "react"; -import { FunctionFragment } from "ethers/lib/utils"; +import { Abi, AbiFunction } from "abitype"; +import { Address } from "viem"; import { useContractRead } from "wagmi"; import { ArrowPathIcon } from "@heroicons/react/24/outline"; import { displayTxResult } from "~~/components/scaffold-eth"; import { useAnimationConfig } from "~~/hooks/scaffold-eth"; -import { getTargetNetwork, notification } from "~~/utils/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; -type TDisplayVariableProps = { - functionFragment: FunctionFragment; - contractAddress: string; +type DisplayVariableProps = { + contractAddress: Address; + abiFunction: AbiFunction; refreshDisplayVariables: boolean; }; -export const DisplayVariable = ({ - contractAddress, - functionFragment, - refreshDisplayVariables, -}: TDisplayVariableProps) => { +export const DisplayVariable = ({ contractAddress, abiFunction, refreshDisplayVariables }: DisplayVariableProps) => { const { data: result, isFetching, refetch, } = useContractRead({ - chainId: getTargetNetwork().id, address: contractAddress, - abi: [functionFragment], - functionName: functionFragment.name, - args: [], + functionName: abiFunction.name, + abi: [abiFunction] as Abi, onError: error => { notification.error(error.message); }, @@ -41,7 +36,7 @@ export const DisplayVariable = ({ return (
-

{functionFragment.name}

+

{abiFunction.name}

diff --git a/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx b/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx index d2d461d77..92a115fd4 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx @@ -1,46 +1,38 @@ import { useState } from "react"; -import { FunctionFragment } from "ethers/lib/utils"; +import { Abi, AbiFunction } from "abitype"; +import { Address } from "viem"; import { useContractRead } from "wagmi"; import { ContractInput, displayTxResult, getFunctionInputKey, + getInitialFormState, getParsedContractFunctionArgs, } from "~~/components/scaffold-eth"; -import { getTargetNetwork, notification } from "~~/utils/scaffold-eth"; - -const getInitialFormState = (functionFragment: FunctionFragment) => { - const initialForm: Record = {}; - functionFragment.inputs.forEach((input, inputIndex) => { - const key = getFunctionInputKey(functionFragment, input, inputIndex); - initialForm[key] = ""; - }); - return initialForm; -}; +import { notification } from "~~/utils/scaffold-eth"; type TReadOnlyFunctionFormProps = { - functionFragment: FunctionFragment; - contractAddress: string; + contractAddress: Address; + abiFunction: AbiFunction; }; -export const ReadOnlyFunctionForm = ({ functionFragment, contractAddress }: TReadOnlyFunctionFormProps) => { - const [form, setForm] = useState>(() => getInitialFormState(functionFragment)); +export const ReadOnlyFunctionForm = ({ contractAddress, abiFunction }: TReadOnlyFunctionFormProps) => { + const [form, setForm] = useState>(() => getInitialFormState(abiFunction)); const [result, setResult] = useState(); const { isFetching, refetch } = useContractRead({ - chainId: getTargetNetwork().id, address: contractAddress, - abi: [functionFragment], - functionName: functionFragment.name, + functionName: abiFunction.name, + abi: [abiFunction] as Abi, args: getParsedContractFunctionArgs(form), enabled: false, - onError: error => { + onError: (error: any) => { notification.error(error.message); }, }); - const inputs = functionFragment.inputs.map((input, inputIndex) => { - const key = getFunctionInputKey(functionFragment, input, inputIndex); + const inputElements = abiFunction.inputs.map((input, inputIndex) => { + const key = getFunctionInputKey(abiFunction.name, input, inputIndex); return ( -

{functionFragment.name}

- {inputs} +

{abiFunction.name}

+ {inputElements}
{result !== null && result !== undefined && ( diff --git a/packages/nextjs/components/scaffold-eth/Contract/TxReceipt.tsx b/packages/nextjs/components/scaffold-eth/Contract/TxReceipt.tsx index aab26add6..12570b464 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/TxReceipt.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/TxReceipt.tsx @@ -1,9 +1,8 @@ -import { TransactionReceipt } from "@ethersproject/abstract-provider"; -import { BigNumber } from "ethers"; +import { TransactionReceipt } from "viem"; import { displayTxResult } from "~~/components/scaffold-eth"; export const TxReceipt = ( - txResult: string | number | BigNumber | Record | TransactionReceipt | undefined, + txResult: string | number | bigint | Record | TransactionReceipt | undefined, ) => { return (
diff --git a/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx index bc349cc2d..21c73c2a4 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx @@ -1,72 +1,52 @@ -import { Dispatch, SetStateAction, useEffect, useState } from "react"; -import { TransactionReceipt } from "@ethersproject/abstract-provider"; -import { BigNumber } from "ethers"; -import { FunctionFragment } from "ethers/lib/utils"; +import { useEffect, useState } from "react"; +import { Abi, AbiFunction } from "abitype"; +import { Address, TransactionReceipt } from "viem"; import { useContractWrite, useNetwork, useWaitForTransaction } from "wagmi"; import { ContractInput, IntegerInput, TxReceipt, getFunctionInputKey, + getInitialFormState, getParsedContractFunctionArgs, - getParsedEthersError, + getParsedError, } from "~~/components/scaffold-eth"; import { useTransactor } from "~~/hooks/scaffold-eth"; -import { getTargetNetwork, notification, parseTxnValue } from "~~/utils/scaffold-eth"; +import { getTargetNetwork, notification } from "~~/utils/scaffold-eth"; -// TODO set sensible initial state values to avoid error on first render, also put it in utilsContract -const getInitialFormState = (functionFragment: FunctionFragment) => { - const initialForm: Record = {}; - functionFragment.inputs.forEach((input, inputIndex) => { - const key = getFunctionInputKey(functionFragment, input, inputIndex); - initialForm[key] = ""; - }); - return initialForm; -}; - -type TWriteOnlyFunctionFormProps = { - functionFragment: FunctionFragment; - contractAddress: string; - setRefreshDisplayVariables: Dispatch>; +type WriteOnlyFunctionFormProps = { + abiFunction: AbiFunction; + onChange: () => void; + contractAddress: Address; }; -export const WriteOnlyFunctionForm = ({ - functionFragment, - contractAddress, - setRefreshDisplayVariables, -}: TWriteOnlyFunctionFormProps) => { - const [form, setForm] = useState>(() => getInitialFormState(functionFragment)); - const [txValue, setTxValue] = useState(""); +export const WriteOnlyFunctionForm = ({ abiFunction, onChange, contractAddress }: WriteOnlyFunctionFormProps) => { + const [form, setForm] = useState>(() => getInitialFormState(abiFunction)); + const [txValue, setTxValue] = useState(""); const { chain } = useNetwork(); const writeTxn = useTransactor(); const writeDisabled = !chain || chain?.id !== getTargetNetwork().id; - // We are omitting usePrepareContractWrite here to avoid unnecessary RPC calls and wrong gas estimations. - // See: - // - https://github.com/scaffold-eth/se-2/issues/59 - // - https://github.com/scaffold-eth/se-2/pull/86#issuecomment-1374902738 const { data: result, isLoading, writeAsync, } = useContractWrite({ + chainId: getTargetNetwork().id, address: contractAddress, - functionName: functionFragment.name, - abi: [functionFragment], + functionName: abiFunction.name, + abi: [abiFunction] as Abi, args: getParsedContractFunctionArgs(form), - mode: "recklesslyUnprepared", - overrides: { - value: typeof txValue === "string" ? parseTxnValue(txValue) : txValue, - }, }); const handleWrite = async () => { if (writeAsync) { try { - await writeTxn(writeAsync()); - setRefreshDisplayVariables(prevState => !prevState); + const makeWriteWithParams = () => writeAsync({ value: BigInt(txValue) }); + await writeTxn(makeWriteWithParams); + onChange(); } catch (e: any) { - const message = getParsedEthersError(e); + const message = getParsedError(e); notification.error(message); } } @@ -81,8 +61,8 @@ export const WriteOnlyFunctionForm = ({ }, [txResult]); // TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm - const inputs = functionFragment.inputs.map((input, inputIndex) => { - const key = getFunctionInputKey(functionFragment, input, inputIndex); + const inputs = abiFunction.inputs.map((input, inputIndex) => { + const key = getFunctionInputKey(abiFunction.name, input, inputIndex); return ( ); }); - const zeroInputs = inputs.length === 0 && !functionFragment.payable; + const zeroInputs = inputs.length === 0 && abiFunction.stateMutability !== "payable"; return (
-

{functionFragment.name}

+

{abiFunction.name}

{inputs} - {functionFragment.payable ? ( + {abiFunction.stateMutability === "payable" ? ( { diff --git a/packages/nextjs/components/scaffold-eth/Contract/utilsContract.tsx b/packages/nextjs/components/scaffold-eth/Contract/utilsContract.tsx index 3cbbf0d25..286fa3b27 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/utilsContract.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/utilsContract.tsx @@ -1,166 +1,43 @@ -import { Dispatch, SetStateAction } from "react"; -import { Contract, utils } from "ethers"; -import { FunctionFragment } from "ethers/lib/utils"; -import { DisplayVariable, ReadOnlyFunctionForm, WriteOnlyFunctionForm } from "~~/components/scaffold-eth"; - -/** - * @param {Contract} contract - * @returns {FunctionFragment[]} array of function fragments - */ -const getAllContractFunctions = (contract: Contract | null): FunctionFragment[] => { - return contract ? Object.values(contract.interface.functions).filter(fn => fn.type === "function") : []; -}; - -/** - * @dev used to filter all readOnly functions with zero params - * @param {Contract} contract - * @param {FunctionFragment[]} contractMethodsAndVariables - array of all functions in the contract - * @param {boolean} refreshDisplayVariables refetch values - * @returns { methods: (JSX.Element | null)[] } array of DisplayVariable component - * which has corresponding input field for param type and button to read - */ -const getContractVariablesAndNoParamsReadMethods = ( - contract: Contract | null, - contractMethodsAndVariables: FunctionFragment[], - refreshDisplayVariables: boolean, -): { methods: (JSX.Element | null)[] } => { - return { - methods: contract - ? contractMethodsAndVariables - .map(fn => { - const isQueryableWithNoParams = - (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0; - if (isQueryableWithNoParams) { - return ( - - ); - } - return null; - }) - .filter(n => n) - : [], - }; -}; - -/** - * @dev used to filter all readOnly functions with greater than or equal to 1 params - * @param {Contract} contract - * @param {FunctionFragment[]} contractMethodsAndVariables - array of all functions in the contract - * @returns { methods: (JSX.Element | null)[] } array of ReadOnlyFunctionForm component - * which has corresponding input field for param type and button to read - */ -const getContractReadOnlyMethodsWithParams = ( - contract: Contract | null, - contractMethodsAndVariables: FunctionFragment[], -): { methods: (JSX.Element | null)[] } => { - return { - methods: contract - ? contractMethodsAndVariables - .map((fn, idx) => { - const isQueryableWithParams = - (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length > 0; - if (isQueryableWithParams) { - return ( - - ); - } - return null; - }) - .filter(n => n) - : [], - }; -}; - -/** - * @dev used to filter all write functions - * @param {Contract} contract - * @param {FunctionFragment[]} contractMethodsAndVariables - array of all functions in the contract - * @param {Dispatch>} setRefreshDisplayVariables - trigger variable refresh - * @returns { methods: (JSX.Element | null)[] } array of WriteOnlyFunctionForm component - * which has corresponding input field for param type, txnValue input if required and button to send transaction - */ -const getContractWriteMethods = ( - contract: Contract | null, - contractMethodsAndVariables: FunctionFragment[], - setRefreshDisplayVariables: Dispatch>, -): { methods: (JSX.Element | null)[] } => { - return { - methods: contract - ? contractMethodsAndVariables - .map((fn, idx) => { - const isWriteableFunction = fn.stateMutability !== "view" && fn.stateMutability !== "pure"; - if (isWriteableFunction) { - return ( - - ); - } - return null; - }) - .filter(n => n) - : [], - }; -}; +import { AbiFunction, AbiParameter } from "abitype"; +import { BaseError as BaseViemError } from "viem"; /** * @dev utility function to generate key corresponding to function metaData - * @param {FunctionFragment} functionInfo + * @param {AbiFunction} functionName * @param {utils.ParamType} input - object containing function name and input type corresponding to index * @param {number} inputIndex * @returns {string} key */ -const getFunctionInputKey = (functionInfo: FunctionFragment, input: utils.ParamType, inputIndex: number): string => { +const getFunctionInputKey = (functionName: string, input: AbiParameter, inputIndex: number): string => { const name = input?.name || `input_${inputIndex}_`; - return functionInfo.name + "_" + name + "_" + input.type + "_" + input.baseType; + return functionName + "_" + name + "_" + input.internalType + "_" + input.type; }; /** - * @dev utility function to parse error thrown by ethers - * @param e - ethers error object + * @dev utility function to parse error + * @param e - error object * @returns {string} parsed error string */ -const getParsedEthersError = (e: any): string => { - let message = - e.data && e.data.message - ? e.data.message - : e.error && JSON.parse(JSON.stringify(e.error)).body - ? JSON.parse(JSON.parse(JSON.stringify(e.error)).body).error.message - : e.data - ? e.data - : JSON.stringify(e); - if (!e.error && e.message) { - message = e.message; - } +const getParsedError = (e: any | BaseViemError): string => { + let message = e.message ?? "An unknown error occurred"; - console.log("Attempt to clean up:", message); - try { - const obj = JSON.parse(message); - if (obj && obj.body) { - const errorObj = JSON.parse(obj.body); - if (errorObj && errorObj.error && errorObj.error.message) { - message = errorObj.error.message; - } + if (e instanceof BaseViemError) { + if (e.details) { + message = e.details; + } else if (e.shortMessage) { + message = e.shortMessage; + } else if (e.message) { + message = e.message; + } else if (e.name) { + message = e.name; } - } catch (e) { - //ignore } return message; }; +// This regex is used to identify array types in the form of `type[size]` +const ARRAY_TYPE_REGEX = /\[.*\]$/; /** * @dev Parse form input with array support * @param {Record} form - form object containing key value pairs @@ -174,7 +51,7 @@ const getParsedContractFunctionArgs = (form: Record) => { const baseTypeOfArg = keySplitArray[keySplitArray.length - 1]; let valueOfArg = form[key]; - if (["array", "tuple"].includes(baseTypeOfArg)) { + if (ARRAY_TYPE_REGEX.test(baseTypeOfArg) || baseTypeOfArg === "tuple") { valueOfArg = JSON.parse(valueOfArg); } else if (baseTypeOfArg === "bool") { if (["true", "1", "0x1", "0x01", "0x0001"].includes(valueOfArg)) { @@ -191,12 +68,14 @@ const getParsedContractFunctionArgs = (form: Record) => { return parsedArguments; }; -export { - getAllContractFunctions, - getContractReadOnlyMethodsWithParams, - getContractVariablesAndNoParamsReadMethods, - getContractWriteMethods, - getFunctionInputKey, - getParsedContractFunctionArgs, - getParsedEthersError, +const getInitialFormState = (abiFunction: AbiFunction) => { + const initialForm: Record = {}; + if (!abiFunction.inputs) return initialForm; + abiFunction.inputs.forEach((input, inputIndex) => { + const key = getFunctionInputKey(abiFunction.name, input, inputIndex); + initialForm[key] = ""; + }); + return initialForm; }; + +export { getFunctionInputKey, getInitialFormState, getParsedContractFunctionArgs, getParsedError }; diff --git a/packages/nextjs/components/scaffold-eth/Contract/utilsDisplay.tsx b/packages/nextjs/components/scaffold-eth/Contract/utilsDisplay.tsx index 807645920..5fcf3103a 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/utilsDisplay.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/utilsDisplay.tsx @@ -1,10 +1,17 @@ import { ReactElement } from "react"; -import { TransactionResponse } from "@ethersproject/providers"; -import { formatUnits } from "@ethersproject/units"; -import { BigNumber } from "ethers"; +import { TransactionBase, TransactionReceipt, formatEther } from "viem"; import { Address } from "~~/components/scaffold-eth"; - -type DisplayContent = string | number | BigNumber | Record | TransactionResponse | undefined | unknown; +import { replacer } from "~~/utils/scaffold-eth/common"; + +type DisplayContent = + | string + | number + | bigint + | Record + | TransactionBase + | TransactionReceipt + | undefined + | unknown; export const displayTxResult = ( displayContent: DisplayContent | DisplayContent[], @@ -14,11 +21,16 @@ export const displayTxResult = ( return ""; } - if (displayContent && BigNumber.isBigNumber(displayContent)) { + if (typeof displayContent === "bigint") { try { - return displayContent.toNumber(); + const asNumber = Number(displayContent); + if (asNumber <= Number.MAX_SAFE_INTEGER && asNumber >= Number.MIN_SAFE_INTEGER) { + return asNumber; + } else { + return "Ξ" + formatEther(displayContent); + } } catch (e) { - return "Ξ" + formatUnits(displayContent, "ether"); + return "Ξ" + formatEther(displayContent); } } @@ -26,10 +38,10 @@ export const displayTxResult = ( return asText ? displayContent :
; } - if (displayContent && Array.isArray(displayContent)) { + if (Array.isArray(displayContent)) { const mostReadable = (v: DisplayContent) => ["number", "boolean"].includes(typeof v) ? v : displayTxResultAsText(v); - const displayable = JSON.stringify(displayContent.map(mostReadable)); + const displayable = JSON.stringify(displayContent.map(mostReadable), replacer); return asText ? ( displayable @@ -38,7 +50,7 @@ export const displayTxResult = ( ); } - return JSON.stringify(displayContent, null, 2); + return JSON.stringify(displayContent, replacer, 2); }; const displayTxResultAsText = (displayContent: DisplayContent) => displayTxResult(displayContent, true); diff --git a/packages/nextjs/components/scaffold-eth/Faucet.tsx b/packages/nextjs/components/scaffold-eth/Faucet.tsx index bf8a61dbb..251d4ee14 100644 --- a/packages/nextjs/components/scaffold-eth/Faucet.tsx +++ b/packages/nextjs/components/scaffold-eth/Faucet.tsx @@ -1,37 +1,36 @@ import { useEffect, useState } from "react"; -import { ethers } from "ethers"; +import { Address as AddressType, createWalletClient, http, parseEther } from "viem"; import { useNetwork } from "wagmi"; -import { hardhat, localhost } from "wagmi/chains"; +import { hardhat } from "wagmi/chains"; import { BanknotesIcon } from "@heroicons/react/24/outline"; -import { Address, AddressInput, Balance, EtherInput, getParsedEthersError } from "~~/components/scaffold-eth"; +import { Address, AddressInput, Balance, EtherInput, getParsedError } from "~~/components/scaffold-eth"; import { useTransactor } from "~~/hooks/scaffold-eth"; -import { getLocalProvider, notification } from "~~/utils/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; // Account index to use from generated hardhat accounts. const FAUCET_ACCOUNT_INDEX = 0; -const provider = getLocalProvider(localhost); - /** * Faucet modal which lets you send ETH to any address. */ export const Faucet = () => { const [loading, setLoading] = useState(false); - const [inputAddress, setInputAddress] = useState(""); - const [faucetAddress, setFaucetAddress] = useState(""); + const [inputAddress, setInputAddress] = useState(); + const [faucetAddress, setFaucetAddress] = useState(); const [sendValue, setSendValue] = useState(""); const { chain: ConnectedChain } = useNetwork(); - const signer = provider?.getSigner(FAUCET_ACCOUNT_INDEX); - const faucetTxn = useTransactor(signer); + const localWalletClient = createWalletClient({ + chain: hardhat, + transport: http(), + }); + const faucetTxn = useTransactor(localWalletClient); useEffect(() => { const getFaucetAddress = async () => { try { - if (provider) { - const accounts = await provider.listAccounts(); - setFaucetAddress(accounts[FAUCET_ACCOUNT_INDEX]); - } + const accounts = await localWalletClient.getAddresses(); + setFaucetAddress(accounts[FAUCET_ACCOUNT_INDEX]); } catch (error) { notification.error( <> @@ -49,17 +48,25 @@ export const Faucet = () => { } }; getFaucetAddress(); - }, []); + }, [localWalletClient]); const sendETH = async () => { + if (!faucetAddress) { + return; + } try { setLoading(true); - await faucetTxn({ to: inputAddress, value: ethers.utils.parseEther(sendValue) }); + await faucetTxn({ + to: inputAddress, + value: parseEther(sendValue as `${number}`), + account: faucetAddress, + chain: hardhat, + }); setLoading(false); - setInputAddress(""); + setInputAddress(undefined); setSendValue(""); } catch (error) { - const parsedError = getParsedEthersError(error); + const parsedError = getParsedError(error); console.error("⚡️ ~ file: Faucet.tsx:sendETH ~ error", error); notification.error(parsedError); setLoading(false); @@ -103,7 +110,7 @@ export const Faucet = () => {
setInputAddress(value)} /> setSendValue(value)} /> diff --git a/packages/nextjs/components/scaffold-eth/FaucetButton.tsx b/packages/nextjs/components/scaffold-eth/FaucetButton.tsx index dd630f822..6ce26c4d0 100644 --- a/packages/nextjs/components/scaffold-eth/FaucetButton.tsx +++ b/packages/nextjs/components/scaffold-eth/FaucetButton.tsx @@ -1,13 +1,13 @@ import { useState } from "react"; -import { ethers } from "ethers"; +import { createWalletClient, http, parseEther } from "viem"; import { useAccount, useNetwork } from "wagmi"; -import { hardhat, localhost } from "wagmi/chains"; +import { hardhat } from "wagmi/chains"; import { BanknotesIcon } from "@heroicons/react/24/outline"; import { useAccountBalance, useTransactor } from "~~/hooks/scaffold-eth"; -import { getLocalProvider } from "~~/utils/scaffold-eth"; // Number of ETH faucet sends to an address const NUM_OF_ETH = "1"; +const FAUCET_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; /** * FaucetButton button which lets you grab eth. @@ -17,14 +17,21 @@ export const FaucetButton = () => { const { balance } = useAccountBalance(address); const { chain: ConnectedChain } = useNetwork(); const [loading, setLoading] = useState(false); - const provider = getLocalProvider(localhost); - const signer = provider?.getSigner(); - const faucetTxn = useTransactor(signer); + const localWalletClient = createWalletClient({ + chain: hardhat, + transport: http(), + }); + const faucetTxn = useTransactor(localWalletClient); const sendETH = async () => { try { setLoading(true); - await faucetTxn({ to: address, value: ethers.utils.parseEther(NUM_OF_ETH) }); + await faucetTxn({ + chain: hardhat, + account: FAUCET_ADDRESS, + to: address, + value: parseEther(NUM_OF_ETH), + }); setLoading(false); } catch (error) { console.error("⚡️ ~ file: FaucetButton.tsx:sendETH ~ error", error); diff --git a/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx b/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx index d4f38ebd8..cf44ba616 100644 --- a/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx +++ b/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from "react"; -import { isAddress } from "ethers/lib/utils"; import Blockies from "react-blockies"; +import { isAddress } from "viem"; +import { Address } from "viem"; import { useEnsAddress, useEnsAvatar, useEnsName } from "wagmi"; import { CommonInputProps, InputBase } from "~~/components/scaffold-eth"; @@ -10,7 +11,7 @@ const isENS = (address = "") => address.endsWith(".eth") || address.endsWith(".x /** * Address input with ENS name resolution */ -export const AddressInput = ({ value, name, placeholder, onChange }: CommonInputProps) => { +export const AddressInput = ({ value, name, placeholder, onChange }: CommonInputProps
) => { const { data: ensAddress, isLoading: isEnsAddressLoading } = useEnsAddress({ name: value, enabled: isENS(value), @@ -27,8 +28,8 @@ export const AddressInput = ({ value, name, placeholder, onChange }: CommonInput }); const { data: ensAvatar } = useEnsAvatar({ - address: value, - enabled: isAddress(value), + name: ensName, + enabled: Boolean(ensName), chainId: 1, cacheTime: 30_000, }); @@ -43,7 +44,7 @@ export const AddressInput = ({ value, name, placeholder, onChange }: CommonInput }, [ensAddress, onChange, value]); const handleChange = useCallback( - (newValue: string) => { + (newValue: Address) => { setEnteredEnsName(undefined); onChange(newValue); }, @@ -51,7 +52,7 @@ export const AddressInput = ({ value, name, placeholder, onChange }: CommonInput ); return ( - name={name} placeholder={placeholder} error={ensAddress === null} diff --git a/packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx b/packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx index f2e16b112..31b4514eb 100644 --- a/packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx +++ b/packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { ethers } from "ethers"; +import { hexToString, isHex, stringToHex } from "viem"; import { CommonInputProps, InputBase } from "~~/components/scaffold-eth"; export const Bytes32Input = ({ value, onChange, name, placeholder }: CommonInputProps) => { @@ -7,11 +7,7 @@ export const Bytes32Input = ({ value, onChange, name, placeholder }: CommonInput if (!value) { return; } - onChange( - ethers.utils.isHexString(value) - ? ethers.utils.parseBytes32String(value) - : ethers.utils.formatBytes32String(value), - ); + onChange(isHex(value) ? hexToString(value, { size: 32 }) : stringToHex(value, { size: 32 })); }, [onChange, value]); return ( diff --git a/packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx b/packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx index 3493aeaa6..f61ff5352 100644 --- a/packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx +++ b/packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx @@ -1,14 +1,10 @@ import { useCallback } from "react"; -import { ethers } from "ethers"; +import { bytesToString, isHex, toBytes, toHex } from "viem"; import { CommonInputProps, InputBase } from "~~/components/scaffold-eth"; export const BytesInput = ({ value, onChange, name, placeholder }: CommonInputProps) => { const convertStringToBytes = useCallback(() => { - onChange( - ethers.utils.isHexString(value) - ? ethers.utils.toUtf8String(value) - : ethers.utils.hexlify(ethers.utils.toUtf8Bytes(value)), - ); + onChange(isHex(value) ? bytesToString(toBytes(value)) : toHex(toBytes(value))); }, [onChange, value]); return ( diff --git a/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx b/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx index edd1391a3..b79b2d7d8 100644 --- a/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx +++ b/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx @@ -8,7 +8,7 @@ type InputBaseProps = CommonInputProps & { suffix?: ReactNode; }; -export const InputBase = string } = string>({ +export const InputBase = string } | undefined = string>({ name, value, onChange, diff --git a/packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx b/packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx index 84e72352f..a59cba682 100644 --- a/packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx +++ b/packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx @@ -1,8 +1,7 @@ import { useCallback, useEffect, useState } from "react"; -import { BigNumber, ethers } from "ethers"; import { CommonInputProps, InputBase, IntegerVariant, isValidInteger } from "~~/components/scaffold-eth"; -type IntegerInputProps = CommonInputProps & { +type IntegerInputProps = CommonInputProps & { variant?: IntegerVariant; }; @@ -18,7 +17,10 @@ export const IntegerInput = ({ if (!value) { return; } - onChange(ethers.utils.parseEther(value.toString())); + if (typeof value === "bigint") { + return onChange(value * 10n ** 18n); + } + return onChange(BigInt(Math.round(Number(value) * 10 ** 18))); }, [onChange, value]); useEffect(() => { diff --git a/packages/nextjs/components/scaffold-eth/Input/utils.ts b/packages/nextjs/components/scaffold-eth/Input/utils.ts index 724ad7a65..8d6405ca5 100644 --- a/packages/nextjs/components/scaffold-eth/Input/utils.ts +++ b/packages/nextjs/components/scaffold-eth/Input/utils.ts @@ -1,5 +1,3 @@ -import { BigNumber, BigNumberish } from "ethers"; - export interface CommonInputProps { value: T; onChange: (newValue: T) => void; @@ -77,15 +75,15 @@ export enum IntegerVariant { export const SIGNED_NUMBER_REGEX = /^-?\d+\.?\d*$/; export const UNSIGNED_NUMBER_REGEX = /^\.?\d+\.?\d*$/; -export const isValidInteger = (dataType: IntegerVariant, value: BigNumberish, strict = true) => { +export const isValidInteger = (dataType: IntegerVariant, value: bigint | string, strict = true) => { const isSigned = dataType.startsWith("i"); const bitcount = Number(dataType.substring(isSigned ? 3 : 4)); - let valueAsBigNumber; + let valueAsBigInt; try { - valueAsBigNumber = BigNumber.from(value); + valueAsBigInt = BigInt(value); } catch (e) {} - if (!BigNumber.isBigNumber(valueAsBigNumber)) { + if (typeof valueAsBigInt !== "bigint") { if (strict) { return false; } @@ -93,10 +91,10 @@ export const isValidInteger = (dataType: IntegerVariant, value: BigNumberish, st return true; } return isSigned ? SIGNED_NUMBER_REGEX.test(value) || value === "-" : UNSIGNED_NUMBER_REGEX.test(value); - } else if (!isSigned && valueAsBigNumber.isNegative()) { + } else if (!isSigned && valueAsBigInt < 0) { return false; } - const hexString = valueAsBigNumber.toHexString(); + const hexString = valueAsBigInt.toString(16); const significantHexDigits = hexString.match(/.*x0*(.*)$/)?.[1] ?? ""; if ( significantHexDigits.length * 4 > bitcount || diff --git a/packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts b/packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts index 2b8486337..3d1755bbb 100644 --- a/packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts +++ b/packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts @@ -16,7 +16,7 @@ const walletIdStorageKey = "scaffoldEth2.wallet"; */ const getInitialConnector = ( previousWalletId: string, - connectors: Connector[], + connectors: Connector[], ): { connector: Connector | undefined; chainId?: number } | undefined => { const burnerConfig = scaffoldConfig.burnerWallet; const targetNetwork = getTargetNetwork(); diff --git a/packages/nextjs/hooks/scaffold-eth/useBurnerWallet.ts b/packages/nextjs/hooks/scaffold-eth/useBurnerWallet.ts index 18b34441b..47490ed40 100644 --- a/packages/nextjs/hooks/scaffold-eth/useBurnerWallet.ts +++ b/packages/nextjs/hooks/scaffold-eth/useBurnerWallet.ts @@ -1,8 +1,8 @@ -import { useCallback, useEffect, useRef } from "react"; -import { BytesLike, Signer, Wallet, ethers } from "ethers"; -import { useDebounce } from "use-debounce"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useLocalStorage } from "usehooks-ts"; -import { useProvider } from "wagmi"; +import { Hex, HttpTransport, PrivateKeyAccount, createWalletClient, http } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { Chain, WalletClient, usePublicClient } from "wagmi"; const burnerStorageKey = "scaffoldEth2.burnerWallet.sk"; @@ -12,14 +12,14 @@ const burnerStorageKey = "scaffoldEth2.burnerWallet.sk"; * @param pk * @returns */ -const isValidSk = (pk: BytesLike | undefined | null): boolean => { +const isValidSk = (pk: Hex | string | undefined | null): boolean => { return pk?.length === 64 || pk?.length === 66; }; /** - * If no burner is found in localstorage, we will use a new default wallet + * If no burner is found in localstorage, we will generate a random private key */ -const newDefaultWallet = ethers.Wallet.createRandom(); +const newDefaultPriaveKey = generatePrivateKey(); /** * Save the current burner private key from storage @@ -27,9 +27,9 @@ const newDefaultWallet = ethers.Wallet.createRandom(); * @internal * @returns */ -export const saveBurnerSK = (wallet: Wallet): void => { +export const saveBurnerSK = (privateKey: Hex): void => { if (typeof window != "undefined" && window != null) { - window?.localStorage?.setItem(burnerStorageKey, wallet.privateKey); + window?.localStorage?.setItem(burnerStorageKey, privateKey); } }; @@ -39,17 +39,17 @@ export const saveBurnerSK = (wallet: Wallet): void => { * @internal * @returns */ -export const loadBurnerSK = (): string => { - let currentSk = ""; +export const loadBurnerSK = (): Hex => { + let currentSk: Hex = "0x"; if (typeof window != "undefined" && window != null) { - currentSk = window?.localStorage?.getItem?.(burnerStorageKey)?.replaceAll('"', "") ?? ""; + currentSk = (window?.localStorage?.getItem?.(burnerStorageKey)?.replaceAll('"', "") ?? "0x") as Hex; } if (!!currentSk && isValidSk(currentSk)) { return currentSk; } else { - saveBurnerSK(newDefaultWallet); - return newDefaultWallet.privateKey; + saveBurnerSK(newDefaultPriaveKey); + return newDefaultPriaveKey; } }; @@ -65,8 +65,8 @@ export const loadBurnerSK = (): string => { * @category Hooks */ export type TBurnerSigner = { - signer: Signer | undefined; - account: string | undefined; + walletClient: WalletClient | undefined; + account: PrivateKeyAccount | undefined; /** * create a new burner signer */ @@ -88,71 +88,89 @@ export type TBurnerSigner = { * @returns IBurnerSigner */ export const useBurnerWallet = (): TBurnerSigner => { - const [burnerSk, setBurnerSk] = useLocalStorage(burnerStorageKey, newDefaultWallet.privateKey); + const [burnerSk, setBurnerSk] = useLocalStorage(burnerStorageKey, newDefaultPriaveKey); - const provider = useProvider(); - const walletRef = useRef(); + const publicClient = usePublicClient(); + const [walletClient, setWalletClient] = useState>(); + const [generatedPrivateKey, setGeneratedPrivateKey] = useState("0x"); + const [account, setAccount] = useState(); const isCreatingNewBurnerRef = useRef(false); - const [signer] = useDebounce(walletRef.current, 200, { - trailing: true, - equalityFn: (a, b) => a?.address === b?.address && a != null && b != null, - }); - const account = walletRef.current?.address; - /** * callback to save current wallet sk */ const saveBurner = useCallback(() => { - setBurnerSk(walletRef.current?.privateKey ?? ""); - }, [setBurnerSk]); + setBurnerSk(generatedPrivateKey); + }, [setBurnerSk, generatedPrivateKey]); /** * create a new burnerkey */ const generateNewBurner = useCallback(() => { - if (provider && !isCreatingNewBurnerRef.current) { + if (publicClient && !isCreatingNewBurnerRef.current) { console.log("🔑 Create new burner wallet..."); isCreatingNewBurnerRef.current = true; - const wallet = Wallet.createRandom().connect(provider); + const randomPrivateKey = generatePrivateKey(); + const randomAccount = privateKeyToAccount(randomPrivateKey); + + const client = createWalletClient({ + chain: publicClient.chain, + account: randomAccount, + transport: http(), + }); + + setWalletClient(client); + setGeneratedPrivateKey(randomPrivateKey); + setAccount(randomAccount); + setBurnerSk(() => { console.log("🔥 ...Save new burner wallet"); isCreatingNewBurnerRef.current = false; - return wallet.privateKey; + return randomPrivateKey; }); - return wallet; + return client; } else { console.log("⚠ Could not create burner wallet"); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [provider?.network?.chainId]); + }, [publicClient.chain.id]); /** * Load wallet with burnerSk * connect and set wallet, once we have burnerSk and valid provider */ useEffect(() => { - if (burnerSk && provider.network.chainId) { - let wallet: Wallet | undefined = undefined; + if (burnerSk && publicClient.chain.id) { + let wallet: WalletClient | undefined = undefined; if (isValidSk(burnerSk)) { - wallet = new ethers.Wallet(burnerSk, provider); + const randomAccount = privateKeyToAccount(burnerSk); + + wallet = createWalletClient({ + chain: publicClient.chain, + account: randomAccount, + transport: http(), + }); + + setGeneratedPrivateKey(burnerSk); + setAccount(randomAccount); } else { - wallet = generateNewBurner?.(); + wallet = generateNewBurner(); } if (wallet == null) { throw "Error: Could not create burner wallet"; } - walletRef.current = wallet; - saveBurner?.(); + + setWalletClient(wallet); + saveBurner(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [burnerSk, provider?.network?.chainId]); + }, [burnerSk, publicClient.chain.id]); return { - signer, + walletClient, account, generateNewBurner, saveBurner, diff --git a/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts b/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts index d969f6bdc..a5cf7d0a6 100644 --- a/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts +++ b/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts @@ -1,38 +1,37 @@ import { useEffect, useState } from "react"; -import { ethers } from "ethers"; -import { useProvider } from "wagmi"; +import { Address, Log } from "viem"; +import { usePublicClient } from "wagmi"; -export const useContractLogs = (address: string) => { - const [logs, setLogs] = useState([]); - const provider = useProvider(); +export const useContractLogs = (address: Address) => { + const [logs, setLogs] = useState([]); + const client = usePublicClient(); useEffect(() => { const fetchLogs = async () => { try { - const filter = { + const existingLogs = await client.getLogs({ address: address, - fromBlock: 0, + fromBlock: 0n, toBlock: "latest", - }; - const existingLogs = await provider.getLogs(filter); + }); setLogs(existingLogs); } catch (error) { console.error("Failed to fetch logs:", error); } }; - - const handleLog = (log: ethers.providers.Log) => { - setLogs(prevLogs => [...prevLogs, log]); - }; - - const filter = { address: address }; - fetchLogs(); - provider.on(filter, handleLog); - return () => { - provider.off(filter, handleLog); - }; - }, [address, provider]); + + return client.watchBlockNumber({ + onBlockNumber: async (blockNumber, prevBlockNumber) => { + const newLogs = await client.getLogs({ + address: address, + fromBlock: prevBlockNumber, + toBlock: "latest", + }); + setLogs(prevLogs => [...prevLogs, ...newLogs]); + }, + }); + }, [address, client]); return logs; }; diff --git a/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts b/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts index f305082ea..869748051 100644 --- a/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts +++ b/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useIsMounted } from "usehooks-ts"; -import { useProvider } from "wagmi"; +import { usePublicClient } from "wagmi"; import scaffoldConfig from "~~/scaffold.config"; import { Contract, ContractCodeStatus, ContractName, contracts } from "~~/utils/scaffold-eth/contract"; @@ -14,7 +14,7 @@ export const useDeployedContractInfo = (cont contractName as ContractName ] as Contract; const [status, setStatus] = useState(ContractCodeStatus.LOADING); - const provider = useProvider({ chainId: scaffoldConfig.targetNetwork.id }); + const publicClient = usePublicClient({ chainId: scaffoldConfig.targetNetwork.id }); useEffect(() => { const checkContractDeployment = async () => { @@ -22,7 +22,9 @@ export const useDeployedContractInfo = (cont setStatus(ContractCodeStatus.NOT_FOUND); return; } - const code = await provider.getCode((deployedContract as Contract).address); + const code = await publicClient.getBytecode({ + address: deployedContract.address, + }); if (!isMounted()) { return; @@ -36,7 +38,7 @@ export const useDeployedContractInfo = (cont }; checkContractDeployment(); - }, [isMounted, contractName, deployedContract, provider]); + }, [isMounted, contractName, deployedContract, publicClient]); return { data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined, diff --git a/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts b/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts index 5ef13fac8..b61c7c8c8 100644 --- a/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts +++ b/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts @@ -1,21 +1,19 @@ import { useCallback, useEffect, useState } from "react"; -import { ethers } from "ethers"; -import { localhost } from "wagmi/chains"; -import { decodeTransactionData } from "~~/utils/scaffold-eth"; -import { getLocalProvider } from "~~/utils/scaffold-eth"; -import { Block } from "~~/utils/scaffold-eth/block"; +import { Block, Transaction, TransactionReceipt } from "viem"; +import { usePublicClient } from "wagmi"; +import { hardhat } from "wagmi/chains"; const BLOCKS_PER_PAGE = 20; -const provider = getLocalProvider(localhost) || new ethers.providers.JsonRpcProvider("http://localhost:8545"); - export const useFetchBlocks = () => { + const client = usePublicClient({ chainId: hardhat.id }); + const [blocks, setBlocks] = useState([]); const [transactionReceipts, setTransactionReceipts] = useState<{ - [key: string]: ethers.providers.TransactionReceipt; + [key: string]: TransactionReceipt; }>({}); const [currentPage, setCurrentPage] = useState(0); - const [totalBlocks, setTotalBlocks] = useState(0); + const [totalBlocks, setTotalBlocks] = useState(0n); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -24,18 +22,18 @@ export const useFetchBlocks = () => { setError(null); try { - const blockNumber = await provider.getBlockNumber(); + const blockNumber = await client.getBlockNumber(); setTotalBlocks(blockNumber); - const startingBlock = blockNumber - currentPage * BLOCKS_PER_PAGE; + const startingBlock = blockNumber - BigInt(currentPage * BLOCKS_PER_PAGE); const blockNumbersToFetch = Array.from( - { length: Math.min(BLOCKS_PER_PAGE, startingBlock + 1) }, - (_, i) => startingBlock - i, + { length: Number(BLOCKS_PER_PAGE < startingBlock + 1n ? BLOCKS_PER_PAGE : startingBlock + 1n) }, + (_, i) => startingBlock - BigInt(i), ); const blocksWithTransactions = blockNumbersToFetch.map(async blockNumber => { try { - return provider.getBlockWithTransactions(blockNumber); + return client.getBlock({ blockNumber, includeTransactions: true }); } catch (err) { setError(err instanceof Error ? err : new Error("An error occurred.")); throw err; @@ -43,16 +41,12 @@ export const useFetchBlocks = () => { }); const fetchedBlocks = await Promise.all(blocksWithTransactions); - fetchedBlocks.forEach(block => { - block.transactions.forEach(tx => decodeTransactionData(tx)); - }); - const txReceipts = await Promise.all( fetchedBlocks.flatMap(block => block.transactions.map(async tx => { try { - const receipt = await provider.getTransactionReceipt(tx.hash); - return { [tx.hash]: receipt }; + const receipt = await client.getTransactionReceipt({ hash: (tx as Transaction).hash }); + return { [(tx as Transaction).hash]: receipt }; } catch (err) { setError(err instanceof Error ? err : new Error("An error occurred.")); throw err; @@ -67,27 +61,24 @@ export const useFetchBlocks = () => { setError(err instanceof Error ? err : new Error("An error occurred.")); } setIsLoading(false); - }, [currentPage]); + }, [client, currentPage]); useEffect(() => { fetchBlocks(); }, [fetchBlocks]); useEffect(() => { - const handleNewBlock = async (blockNumber: number) => { + const handleNewBlock = async (newBlock: Block) => { try { - const newBlock = await provider.getBlockWithTransactions(blockNumber); if (!blocks.some(block => block.number === newBlock.number)) { if (currentPage === 0) { setBlocks(prevBlocks => [newBlock, ...prevBlocks.slice(0, BLOCKS_PER_PAGE - 1)]); - newBlock.transactions.forEach(tx => decodeTransactionData(tx)); - const receipts = await Promise.all( newBlock.transactions.map(async tx => { try { - const receipt = await provider.getTransactionReceipt(tx.hash); - return { [tx.hash]: receipt }; + const receipt = await client.getTransactionReceipt({ hash: (tx as Transaction).hash }); + return { [(tx as Transaction).hash]: receipt }; } catch (err) { setError(err instanceof Error ? err : new Error("An error occurred.")); throw err; @@ -97,19 +88,17 @@ export const useFetchBlocks = () => { setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...receipts) })); } - setTotalBlocks(blockNumber + 1); + if (newBlock.number) { + setTotalBlocks(newBlock.number); + } } } catch (err) { setError(err instanceof Error ? err : new Error("An error occurred.")); } }; - provider.on("block", handleNewBlock); - - return () => { - provider.off("block", handleNewBlock); - }; - }, [blocks, currentPage]); + return client.watchBlocks({ onBlock: handleNewBlock, includeTransactions: true }); + }, [blocks, client, currentPage]); return { blocks, diff --git a/packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts b/packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts index ea27785d3..6af4895fb 100644 --- a/packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts +++ b/packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; import { useInterval } from "usehooks-ts"; -import { useProvider } from "wagmi"; import scaffoldConfig from "~~/scaffold.config"; import { fetchPriceFromUniswap } from "~~/utils/scaffold-eth"; @@ -11,21 +10,20 @@ const enablePolling = false; * @returns nativeCurrencyPrice: number */ export const useNativeCurrencyPrice = () => { - const provider = useProvider({ chainId: 1 }); const [nativeCurrencyPrice, setNativeCurrencyPrice] = useState(0); // Get the price of ETH from Uniswap on mount useEffect(() => { (async () => { - const price = await fetchPriceFromUniswap(provider); + const price = await fetchPriceFromUniswap(); setNativeCurrencyPrice(price); })(); - }, [provider]); + }, []); // Get the price of ETH from Uniswap at a given interval useInterval( async () => { - const price = await fetchPriceFromUniswap(provider); + const price = await fetchPriceFromUniswap(); setNativeCurrencyPrice(price); }, enablePolling ? scaffoldConfig.pollingInterval : null, diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts index 4c3edfddb..a634f49a5 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts @@ -1,6 +1,6 @@ import { Abi } from "abitype"; -import { ethers } from "ethers"; -import { useContract, useProvider } from "wagmi"; +import { getContract } from "viem"; +import { GetWalletClientResult } from "wagmi/actions"; import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; import { ContractName } from "~~/utils/scaffold-eth/contract"; @@ -8,23 +8,28 @@ import { ContractName } from "~~/utils/scaffold-eth/contract"; * Gets a deployed contract by contract name and returns a contract instance * @param config - The config settings * @param config.contractName - Deployed contract name - * @param config.signerOrProvider - An ethers Provider or Signer (optional) + * @param config.walletClient - An viem wallet client instance (optional) */ export const useScaffoldContract = ({ contractName, - signerOrProvider, + walletClient, }: { contractName: TContractName; - signerOrProvider?: ethers.Signer | ethers.providers.Provider; + walletClient?: GetWalletClientResult; }) => { const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); - const provider = useProvider(); - const contract = useContract({ - address: deployedContractData?.address, - abi: deployedContractData?.abi as Abi, - signerOrProvider: signerOrProvider === undefined ? provider : signerOrProvider, - }); + // type GetWalletClientResult = WalletClient | null, hence narrowing it to undefined so that it can be passed to getContract + const walletClientInstance = walletClient != null ? walletClient : undefined; + + let contract = undefined; + if (deployedContractData) { + contract = getContract({ + address: deployedContractData.address, + abi: deployedContractData.abi as Abi, + walletClient: walletClientInstance, + }); + } return { data: contract, diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldContractWrite.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldContractWrite.ts index 53f7b7cd2..b1b51c290 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldContractWrite.ts +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldContractWrite.ts @@ -1,12 +1,14 @@ import { useState } from "react"; import { Abi, ExtractAbiFunctionNames } from "abitype"; -import { utils } from "ethers"; +import { parseEther } from "viem"; import { useContractWrite, useNetwork } from "wagmi"; -import { getParsedEthersError } from "~~/components/scaffold-eth"; +import { getParsedError } from "~~/components/scaffold-eth"; import { useDeployedContractInfo, useTransactor } from "~~/hooks/scaffold-eth"; import { getTargetNetwork, notification } from "~~/utils/scaffold-eth"; import { ContractAbi, ContractName, UseScaffoldWriteConfig } from "~~/utils/scaffold-eth/contract"; +type UpdatedArgs = Parameters>["writeAsync"]>[0]; + /** * @dev wrapper for wagmi's useContractWrite hook(with config prepared by usePrepareContractWrite hook) which loads in deployed contract abi and address automatically * @param config - The config settings, including extra wagmi configuration @@ -33,31 +35,24 @@ export const useScaffoldContractWrite = < const [isMining, setIsMining] = useState(false); const configuredNetwork = getTargetNetwork(); - const { overrides, ...restConfig } = writeConfig; - const wagmiContractWrite = useContractWrite({ - mode: "recklesslyUnprepared", chainId: configuredNetwork.id, address: deployedContractData?.address, abi: deployedContractData?.abi as Abi, - args: args as unknown[], functionName: functionName as any, - overrides: { - value: value ? utils.parseEther(value) : undefined, - ...overrides, - }, - ...restConfig, + args: args as unknown[], + value: value ? parseEther(value) : undefined, + ...writeConfig, }); const sendContractWriteTx = async ({ - args, - value, - overrides, + args: newArgs, + value: newValue, + ...otherConfig }: { args?: UseScaffoldWriteConfig["args"]; value?: UseScaffoldWriteConfig["value"]; - overrides?: UseScaffoldWriteConfig["overrides"]; - } = {}) => { + } & UpdatedArgs = {}) => { if (!deployedContractData) { notification.error("Target Contract is not deployed, did you forgot to run `yarn deploy`?"); return; @@ -75,21 +70,16 @@ export const useScaffoldContractWrite = < try { setIsMining(true); await writeTx( - wagmiContractWrite.writeAsync({ - recklesslySetUnpreparedArgs: args as unknown[], - recklesslySetUnpreparedOverrides: - value && overrides - ? { value: utils.parseEther(value), ...overrides } - : value - ? { value: utils.parseEther(value) } - : overrides - ? overrides - : undefined, - }), + () => + wagmiContractWrite.writeAsync({ + args: newArgs ?? args, + value: newValue ? parseEther(newValue) : value && parseEther(value), + ...otherConfig, + }), { onBlockConfirmation, blockConfirmations }, ); } catch (e: any) { - const message = getParsedEthersError(e); + const message = getParsedError(e); notification.error(message); } finally { setIsMining(false); diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts index 39e86231a..a3c61cc62 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts @@ -1,8 +1,9 @@ import { useEffect, useState } from "react"; -import { Abi, ExtractAbiEventNames } from "abitype"; -import { ethers } from "ethers"; -import { useContract, useProvider } from "wagmi"; +import { AbiEvent, ExtractAbiEventNames } from "abitype"; +import { Hash } from "viem"; +import { usePublicClient } from "wagmi"; import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import { replacer } from "~~/utils/scaffold-eth/common"; import { ContractAbi, ContractName, UseScaffoldEventHistoryConfig } from "~~/utils/scaffold-eth/contract"; /** @@ -32,69 +33,43 @@ export const useScaffoldEventHistory = < const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(); const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); - const provider = useProvider(); - - const contract = useContract({ - address: deployedContractData?.address, - abi: deployedContractData?.abi as Abi, - signerOrProvider: provider, - }); + const publicClient = usePublicClient(); useEffect(() => { async function readEvents() { try { - if (!deployedContractData || !contract) { + if (!deployedContractData) { throw new Error("Contract not found"); } - const fragment = contract.interface.getEvent(eventName); - const emptyIface = new ethers.utils.Interface([]); - const topicHash = emptyIface.getEventTopic(fragment); - const topics = [topicHash] as any[]; - - const indexedParameters = fragment.inputs.filter(input => input.indexed); - - if (indexedParameters.length > 0 && filters) { - const indexedTopics = indexedParameters.map(input => { - const value = (filters as any)[input.name]; - if (value === undefined) { - return null; - } - if (Array.isArray(value)) { - return value.map(v => ethers.utils.hexZeroPad(ethers.utils.hexlify(v), 32)); - } - return ethers.utils.hexZeroPad(ethers.utils.hexlify(value), 32); - }); - topics.push(...indexedTopics); - } + const event = deployedContractData.abi.find( + part => part.type === "event" && part.name === eventName, + ) as AbiEvent; - const logs = await provider.getLogs({ + const logs = await publicClient.getLogs({ address: deployedContractData?.address, - topics: topics, - fromBlock: fromBlock, + event, + args: filters as any, // TODO: check if it works and fix type + fromBlock, }); const newEvents = []; for (let i = logs.length - 1; i >= 0; i--) { - let block; - if (blockData) { - block = await provider.getBlock(logs[i].blockHash); - } - let transaction; - if (transactionData) { - transaction = await provider.getTransaction(logs[i].transactionHash); - } - let receipt; - if (receiptData) { - receipt = await provider.getTransactionReceipt(logs[i].transactionHash); - } - const log = { + newEvents.push({ log: logs[i], - args: contract.interface.parseLog(logs[i]).args, - block: block, - transaction: transaction, - receipt: receipt, - }; - newEvents.push(log); + args: logs[i].args, + block: + blockData && logs[i].blockHash === null + ? null + : await publicClient.getBlock({ blockHash: logs[i].blockHash as Hash }), + transaction: + transactionData && logs[i].transactionHash !== null + ? await publicClient.getTransaction({ hash: logs[i].transactionHash as Hash }) + : null, + receipt: + receiptData && logs[i].transactionHash !== null + ? await publicClient.getTransactionReceipt({ hash: logs[i].transactionHash as Hash }) + : null, + }); } setEvents(newEvents); setError(undefined); @@ -111,16 +86,15 @@ export const useScaffoldEventHistory = < } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - provider, + publicClient, fromBlock, contractName, eventName, deployedContractLoading, deployedContractData?.address, - contract, deployedContractData, // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify(filters), + JSON.stringify(filters, replacer), blockData, transactionData, receiptData, diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldEventSubscriber.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventSubscriber.ts index b27a7d3cb..928ffda67 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldEventSubscriber.ts +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventSubscriber.ts @@ -1,35 +1,32 @@ -import { Abi, ExtractAbiEventNames } from "abitype"; +import { ExtractAbiEventNames } from "abitype"; +import { Log } from "viem"; import { useContractEvent } from "wagmi"; import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; import { getTargetNetwork } from "~~/utils/scaffold-eth"; -import { AbiEventArgs, ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaffold-eth/contract"; +import { ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaffold-eth/contract"; /** * @dev wrapper for wagmi's useContractEvent * @param config - The config settings * @param config.contractName - deployed contract name * @param config.eventName - name of the event to listen for - * @param config.listener - the callback that receives event - * @param config.once - if set to true it will receive only a single event, then stop listening for the event. Defaults to false + * @param config.listener - the callback that receives events. If only interested in 1 event, call `unwatch` inside of the listener */ export const useScaffoldEventSubscriber = < TContractName extends ContractName, TEventName extends ExtractAbiEventNames>, - TEventInputs extends AbiEventArgs, TEventName> & any[], >({ contractName, eventName, listener, - once, -}: UseScaffoldEventConfig) => { +}: UseScaffoldEventConfig) => { const { data: deployedContractData } = useDeployedContractInfo(contractName); return useContractEvent({ address: deployedContractData?.address, - abi: deployedContractData?.abi as Abi, + abi: deployedContractData?.abi, chainId: getTargetNetwork().id, - listener: listener as (...args: unknown[]) => void, - eventName: eventName as string, - once: once ?? false, + listener: listener as (logs: Log[]) => void, + eventName, }); }; diff --git a/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx b/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx index b1ad8a1a3..7832661c3 100644 --- a/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx +++ b/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx @@ -1,18 +1,16 @@ -import { TransactionReceipt, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider"; -import { SendTransactionResult } from "@wagmi/core"; -import { Signer } from "ethers"; -import { Deferrable } from "ethers/lib/utils"; -import { useSigner } from "wagmi"; -import { getParsedEthersError } from "~~/components/scaffold-eth"; +import { WriteContractResult, getPublicClient } from "@wagmi/core"; +import { Hash, SendTransactionParameters, TransactionReceipt, WalletClient } from "viem"; +import { useWalletClient } from "wagmi"; +import { getParsedError } from "~~/components/scaffold-eth"; import { getBlockExplorerTxLink, notification } from "~~/utils/scaffold-eth"; -type TTransactionFunc = ( - tx: Promise | Deferrable | undefined, +type TransactionFunc = ( + tx: (() => Promise) | SendTransactionParameters, options?: { onBlockConfirmation?: (txnReceipt: TransactionReceipt) => void; blockConfirmations?: number; }, -) => Promise | undefined>; +) => Promise; /** * Custom notification content for TXs. @@ -31,48 +29,52 @@ const TxnNotification = ({ message, blockExplorerLink }: { message: string; bloc }; /** - * Runs TXs showing UI feedback. - * @param _signer - * @dev If signer is provided => dev wants to send a raw tx. + * @description Runs Transaction passed in to returned funtion showing UI feedback. + * @param _walletClient + * @returns function that takes a transaction and returns a promise of the transaction hash */ -export const useTransactor = (_signer?: Signer): TTransactionFunc => { - let signer = _signer; - const { data } = useSigner(); - if (signer === undefined && data) { - signer = data; +export const useTransactor = (_walletClient?: WalletClient): TransactionFunc => { + let walletClient = _walletClient; + const { data } = useWalletClient(); + if (walletClient === undefined && data) { + walletClient = data; } - const result: TTransactionFunc = async (tx, options) => { - if (!signer) { - notification.error("Wallet/Signer not connected"); + const result: TransactionFunc = async (tx, options) => { + if (!walletClient) { + notification.error("Cannot access account"); console.error("⚡️ ~ file: useTransactor.tsx ~ error"); return; } let notificationId = null; - let transactionResponse: SendTransactionResult | TransactionResponse | undefined; + let transactionHash: Awaited["hash"] | undefined = undefined; try { - const provider = signer.provider; - const network = await provider?.getNetwork(); + const network = await walletClient.getChainId(); + // Get full transaction from public client + const publicClient = getPublicClient(); notificationId = notification.loading(); - if (tx instanceof Promise) { + if (typeof tx === "function") { // Tx is already prepared by the caller - transactionResponse = await tx; + transactionHash = (await tx()).hash; } else if (tx != null) { - transactionResponse = await signer.sendTransaction(tx); + transactionHash = await walletClient.sendTransaction(tx); } else { throw new Error("Incorrect transaction passed to transactor"); } notification.remove(notificationId); - const blockExplorerTxURL = network ? getBlockExplorerTxLink(network, transactionResponse.hash) : ""; + const blockExplorerTxURL = network ? getBlockExplorerTxLink(network, transactionHash) : ""; notificationId = notification.loading( , ); - const transactionReceipt = await transactionResponse.wait(options?.blockConfirmations); + const transactionReceipt = await publicClient.waitForTransactionReceipt({ + hash: transactionHash, + confirmations: options?.blockConfirmations, + }); notification.remove(notificationId); notification.success( @@ -87,13 +89,12 @@ export const useTransactor = (_signer?: Signer): TTransactionFunc => { if (notificationId) { notification.remove(notificationId); } - // TODO handle error properly console.error("⚡️ ~ file: useTransactor.ts ~ error", error); - const message = getParsedEthersError(error); + const message = getParsedError(error); notification.error(message); } - return transactionResponse; + return transactionHash; }; return result; diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index d3ebc76a6..5b50ede20 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -14,13 +14,12 @@ "vercel:yolo": "vercel --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true" }, "dependencies": { - "@ethersproject/networks": "^5.7.1", - "@ethersproject/web": "^5.7.1", + "@ethersproject/providers": "^5.7.2", "@heroicons/react": "^2.0.11", - "@rainbow-me/rainbowkit": "^0.12.15", - "@uniswap/sdk": "^3.0.3", + "@rainbow-me/rainbowkit": "^1.0.4", + "@uniswap/sdk-core": "^4.0.1", + "@uniswap/v2-sdk": "^3.0.1", "daisyui": "^2.31.0", - "ethers": "^5.0.0", "next": "^13.1.6", "nextjs-progressbar": "^0.0.16", "react": "^18.2.0", @@ -31,7 +30,8 @@ "react-hot-toast": "^2.4.0", "use-debounce": "^8.0.4", "usehooks-ts": "^2.7.2", - "wagmi": "^0.12.15", + "viem": "^1.2.1", + "wagmi": "^1.3.2", "zustand": "^4.1.2" }, "devDependencies": { diff --git a/packages/nextjs/pages/_app.tsx b/packages/nextjs/pages/_app.tsx index c56b69210..70b88958b 100644 --- a/packages/nextjs/pages/_app.tsx +++ b/packages/nextjs/pages/_app.tsx @@ -11,7 +11,7 @@ import { Header } from "~~/components/Header"; import { BlockieAvatar } from "~~/components/scaffold-eth"; import { useNativeCurrencyPrice } from "~~/hooks/scaffold-eth"; import { useGlobalState } from "~~/services/store/store"; -import { wagmiClient } from "~~/services/web3/wagmiClient"; +import { wagmiConfig } from "~~/services/web3/wagmiConfig"; import { appChains } from "~~/services/web3/wagmiConnectors"; import "~~/styles/globals.css"; @@ -33,7 +33,7 @@ const ScaffoldEthApp = ({ Component, pageProps }: AppProps) => { }, [isDarkMode]); return ( - + { const router = useRouter(); @@ -37,17 +40,20 @@ const AddressPage = ({ address, contractData }: PageProps) => { useEffect(() => { const checkIsContract = async () => { - const contractCode = await provider?.getCode(address); - setIsContract(contractCode !== "0x"); + const contractCode = await publicClient.getBytecode({ address: address }); + setIsContract(contractCode !== undefined && contractCode !== "0x"); }; checkIsContract(); }, [address]); const filteredBlocks = blocks.filter(block => - block.transactions.some( - tx => tx.from.toLowerCase() === address.toLowerCase() || tx.to?.toLowerCase() === address.toLowerCase(), - ), + block.transactions.some(tx => { + if (typeof tx === "string") { + return false; + } + return tx.from.toLowerCase() === address.toLowerCase() || tx.to?.toLowerCase() === address.toLowerCase(); + }), ); return ( @@ -103,7 +109,11 @@ const AddressPage = ({ address, contractData }: PageProps) => { {activeTab === "transactions" && (
- +
)} {activeTab === "code" && contractData && ( diff --git a/packages/nextjs/pages/blockexplorer/index.tsx b/packages/nextjs/pages/blockexplorer/index.tsx index bbff977dc..2144a181b 100644 --- a/packages/nextjs/pages/blockexplorer/index.tsx +++ b/packages/nextjs/pages/blockexplorer/index.tsx @@ -52,7 +52,7 @@ const Blockexplorer: NextPage = () => {
- +
); }; diff --git a/packages/nextjs/pages/blockexplorer/transaction/[txHash].tsx b/packages/nextjs/pages/blockexplorer/transaction/[txHash].tsx index 2a21e6250..168af64c0 100644 --- a/packages/nextjs/pages/blockexplorer/transaction/[txHash].tsx +++ b/packages/nextjs/pages/blockexplorer/transaction/[txHash].tsx @@ -1,45 +1,40 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import { ethers } from "ethers"; import type { NextPage } from "next"; -import { localhost } from "wagmi/chains"; +import { Transaction, TransactionReceipt, formatEther, formatUnits } from "viem"; +import { usePublicClient } from "wagmi"; +import { hardhat } from "wagmi/chains"; import { Address } from "~~/components/scaffold-eth"; -import { - TransactionWithFunction, - decodeTransactionData, - getFunctionDetails, - getTargetNetwork, -} from "~~/utils/scaffold-eth"; -import { getLocalProvider } from "~~/utils/scaffold-eth"; - -const provider = getLocalProvider(localhost) || new ethers.providers.JsonRpcProvider("http://localhost:8545"); +import { decodeTransactionData, getFunctionDetails, getTargetNetwork } from "~~/utils/scaffold-eth"; const TransactionPage: NextPage = () => { + const client = usePublicClient({ chainId: hardhat.id }); + const router = useRouter(); - const { txHash } = router.query; - const [transaction, setTransaction] = useState(null); - const [receipt, setReceipt] = useState(null); - const [functionCalled, setFunctionCalled] = useState(null); + const { txHash } = router.query as { txHash?: `0x${string}` }; + const [transaction, setTransaction] = useState(); + const [receipt, setReceipt] = useState(); + const [functionCalled, setFunctionCalled] = useState(); const configuredNetwork = getTargetNetwork(); useEffect(() => { if (txHash) { const fetchTransaction = async () => { - const tx = await provider.getTransaction(txHash as string); - const receipt = await provider.getTransactionReceipt(txHash as string); + 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.data.substring(0, 10); + const functionCalled = transactionWithDecodedData.input.substring(0, 10); setFunctionCalled(functionCalled); }; fetchTransaction(); } - }, [txHash]); + }, [client, txHash]); return (
@@ -61,7 +56,7 @@ const TransactionPage: NextPage = () => { Block Number: - {transaction.blockNumber} + {Number(transaction.blockNumber)} @@ -91,7 +86,7 @@ const TransactionPage: NextPage = () => { Value: - {ethers.utils.formatEther(transaction.value)} {configuredNetwork.nativeCurrency.symbol} + {formatEther(transaction.value)} {configuredNetwork.nativeCurrency.symbol} @@ -113,14 +108,14 @@ const TransactionPage: NextPage = () => { Gas Price: - {ethers.utils.formatUnits(transaction.gasPrice || ethers.constants.Zero, "gwei")} Gwei + {formatUnits(transaction.gasPrice || 0n, 9)} Gwei Data: -