diff --git a/.changeset/silly-hounds-stare.md b/.changeset/silly-hounds-stare.md new file mode 100644 index 000000000..d121e436d --- /dev/null +++ b/.changeset/silly-hounds-stare.md @@ -0,0 +1,11 @@ +--- +"create-eth": patch +--- + +- Follow root level naming convention (https://github.com/scaffold-eth/scaffold-eth-2/pull/1006) +- Fix useScaffoldEventHistory duplicated events (https://github.com/scaffold-eth/scaffold-eth-2/pull/1014) +- feat: disable vercel telemetry (https://github.com/scaffold-eth/scaffold-eth-2/pull/1012) +- Optional chainId config in Scaffold hooks (https://github.com/scaffold-eth/scaffold-eth-2/pull/931) +- Foundry improvements (https://github.com/scaffold-eth/scaffold-eth-2/pull/1011) +- make `useScaffoldWriteContract` & `useDeployedContractInfo` backward compatible (https://github.com/scaffold-eth/scaffold-eth-2/pull/1015) +- fix: move warnings to useEffect (https://github.com/scaffold-eth/scaffold-eth-2/pull/1016) diff --git a/templates/base/packages/nextjs/app/debug/_components/contract/ContractUI.tsx b/templates/base/packages/nextjs/app/debug/_components/contract/ContractUI.tsx index 8fcec9d38..4d770ec28 100644 --- a/templates/base/packages/nextjs/app/debug/_components/contract/ContractUI.tsx +++ b/templates/base/packages/nextjs/app/debug/_components/contract/ContractUI.tsx @@ -21,7 +21,7 @@ type ContractUIProps = { export const ContractUI = ({ contractName, className = "" }: ContractUIProps) => { const [refreshDisplayVariables, triggerRefreshDisplayVariables] = useReducer(value => !value, false); const { targetNetwork } = useTargetNetwork(); - const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); + const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({ contractName }); const networkColor = useNetworkColor(); if (deployedContractLoading) { diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/index.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/index.ts index 084ccc852..05450b260 100644 --- a/templates/base/packages/nextjs/hooks/scaffold-eth/index.ts +++ b/templates/base/packages/nextjs/hooks/scaffold-eth/index.ts @@ -13,3 +13,4 @@ export * from "./useScaffoldWriteContract"; export * from "./useTargetNetwork"; export * from "./useTransactor"; export * from "./useWatchBalance"; +export * from "./useSelectedNetwork"; diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts index 8f649c38a..3d58526cd 100644 --- a/templates/base/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts +++ b/templates/base/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts @@ -1,19 +1,54 @@ import { useEffect, useState } from "react"; -import { useTargetNetwork } from "./useTargetNetwork"; import { useIsMounted } from "usehooks-ts"; import { usePublicClient } from "wagmi"; -import { Contract, ContractCodeStatus, ContractName, contracts } from "~~/utils/scaffold-eth/contract"; +import { useSelectedNetwork } from "~~/hooks/scaffold-eth"; +import { + Contract, + ContractCodeStatus, + ContractName, + UseDeployedContractConfig, + contracts, +} from "~~/utils/scaffold-eth/contract"; + +type DeployedContractData = { + data: Contract | undefined; + isLoading: boolean; +}; /** * Gets the matching contract info for the provided contract name from the contracts present in deployedContracts.ts * and externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts */ -export const useDeployedContractInfo = (contractName: TContractName) => { +export function useDeployedContractInfo( + config: UseDeployedContractConfig, +): DeployedContractData; +/** + * @deprecated Use object parameter version instead: useDeployedContractInfo({ contractName: "YourContract" }) + */ +export function useDeployedContractInfo( + contractName: TContractName, +): DeployedContractData; + +export function useDeployedContractInfo( + configOrName: UseDeployedContractConfig | TContractName, +): DeployedContractData { const isMounted = useIsMounted(); - const { targetNetwork } = useTargetNetwork(); - const deployedContract = contracts?.[targetNetwork.id]?.[contractName as ContractName] as Contract; + + const finalConfig: UseDeployedContractConfig = + typeof configOrName === "string" ? { contractName: configOrName } : (configOrName as any); + + useEffect(() => { + if (typeof configOrName === "string") { + console.warn( + "Using `useDeployedContractInfo` with a string parameter is deprecated. Please use the object parameter version instead.", + ); + } + }, [configOrName]); + const { contractName, chainId } = finalConfig; + const selectedNetwork = useSelectedNetwork(chainId); + const deployedContract = contracts?.[selectedNetwork.id]?.[contractName as ContractName] as Contract; const [status, setStatus] = useState(ContractCodeStatus.LOADING); - const publicClient = usePublicClient({ chainId: targetNetwork.id }); + const publicClient = usePublicClient({ chainId: selectedNetwork.id }); useEffect(() => { const checkContractDeployment = async () => { @@ -48,4 +83,4 @@ export const useDeployedContractInfo = (cont data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined, isLoading: status === ContractCodeStatus.LOADING, }; -}; +} diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts index ec9c77060..96d1958a1 100644 --- a/templates/base/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts +++ b/templates/base/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts @@ -1,6 +1,7 @@ import { useTargetNetwork } from "./useTargetNetwork"; import { useTheme } from "next-themes"; -import { ChainWithAttributes } from "~~/utils/scaffold-eth"; +import { useSelectedNetwork } from "~~/hooks/scaffold-eth"; +import { AllowedChainIds, ChainWithAttributes } from "~~/utils/scaffold-eth"; export const DEFAULT_NETWORK_COLOR: [string, string] = ["#666666", "#bbbbbb"]; @@ -12,11 +13,11 @@ export function getNetworkColor(network: ChainWithAttributes, isDarkMode: boolea /** * Gets the color of the target network */ -export const useNetworkColor = () => { +export const useNetworkColor = (chainId?: AllowedChainIds) => { const { resolvedTheme } = useTheme(); - const { targetNetwork } = useTargetNetwork(); + const chain = useSelectedNetwork(chainId); const isDarkMode = resolvedTheme === "dark"; - return getNetworkColor(targetNetwork, isDarkMode); + return getNetworkColor(chain, isDarkMode); }; diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts index 1add8cba8..d814b230c 100644 --- a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts +++ b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts @@ -1,8 +1,9 @@ -import { useTargetNetwork } from "./useTargetNetwork"; import { Account, Address, Chain, Client, Transport, getContract } from "viem"; import { usePublicClient } from "wagmi"; import { GetWalletClientReturnType } from "wagmi/actions"; +import { useSelectedNetwork } from "~~/hooks/scaffold-eth"; import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import { AllowedChainIds } from "~~/utils/scaffold-eth"; import { Contract, ContractName } from "~~/utils/scaffold-eth/contract"; /** @@ -11,6 +12,7 @@ import { Contract, ContractName } from "~~/utils/scaffold-eth/contract"; * @param config - The config settings for the hook * @param config.contractName - deployed contract name * @param config.walletClient - optional walletClient from wagmi useWalletClient hook can be passed for doing write transactions + * @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions. */ export const useScaffoldContract = < TContractName extends ContractName, @@ -18,13 +20,19 @@ export const useScaffoldContract = < >({ contractName, walletClient, + chainId, }: { contractName: TContractName; walletClient?: TWalletClient | null; + chainId?: AllowedChainIds; }) => { - const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); - const { targetNetwork } = useTargetNetwork(); - const publicClient = usePublicClient({ chainId: targetNetwork.id }); + const selectedNetwork = useSelectedNetwork(chainId); + const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({ + contractName, + chainId: selectedNetwork?.id as AllowedChainIds, + }); + + const publicClient = usePublicClient({ chainId: selectedNetwork?.id }); let contract = undefined; if (deployedContractData && publicClient) { diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts index 2477a9b20..80914f5dc 100644 --- a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts +++ b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts @@ -1,10 +1,11 @@ import { useEffect, useState } from "react"; -import { useTargetNetwork } from "./useTargetNetwork"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Abi, AbiEvent, ExtractAbiEventNames } from "abitype"; import { BlockNumber, GetLogsParameters } from "viem"; import { Config, UsePublicClientReturnType, useBlockNumber, usePublicClient } from "wagmi"; +import { useSelectedNetwork } from "~~/hooks/scaffold-eth"; import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import { AllowedChainIds } from "~~/utils/scaffold-eth"; import { replacer } from "~~/utils/scaffold-eth/common"; import { ContractAbi, @@ -57,6 +58,7 @@ const getEvents = async ( * @param config.contractName - deployed contract name * @param config.eventName - name of the event to listen for * @param config.fromBlock - the block number to start reading events from + * @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions. * @param config.filters - filters to be applied to the event (parameterName: value) * @param config.blockData - if set to true it will return the block data for each event (default: false) * @param config.transactionData - if set to true it will return the transaction data for each event (default: false) @@ -74,6 +76,7 @@ export const useScaffoldEventHistory = < contractName, eventName, fromBlock, + chainId, filters, blockData, transactionData, @@ -81,15 +84,19 @@ export const useScaffoldEventHistory = < watch, enabled = true, }: UseScaffoldEventHistoryConfig) => { - const { targetNetwork } = useTargetNetwork(); + const selectedNetwork = useSelectedNetwork(chainId); + const publicClient = usePublicClient({ - chainId: targetNetwork.id, + chainId: selectedNetwork.id, }); const [isFirstRender, setIsFirstRender] = useState(true); - const { data: blockNumber } = useBlockNumber({ watch: watch, chainId: targetNetwork.id }); + const { data: blockNumber } = useBlockNumber({ watch: watch, chainId: selectedNetwork.id }); - const { data: deployedContractData } = useDeployedContractInfo(contractName); + const { data: deployedContractData } = useDeployedContractInfo({ + contractName, + chainId: selectedNetwork.id as AllowedChainIds, + }); const event = deployedContractData && @@ -105,7 +112,7 @@ export const useScaffoldEventHistory = < address: deployedContractData?.address, eventName, fromBlock: fromBlock.toString(), - chainId: targetNetwork.id, + chainId: selectedNetwork.id, filters: JSON.stringify(filters, replacer), }, ], @@ -121,8 +128,18 @@ export const useScaffoldEventHistory = < }, enabled: enabled && isContractAddressAndClientReady, initialPageParam: fromBlock, - getNextPageParam: () => { - return blockNumber; + getNextPageParam: (lastPage, allPages, lastPageParam) => { + if (!blockNumber || fromBlock >= blockNumber) return undefined; + + const lastPageHighestBlock = Math.max( + Number(fromBlock), + ...(lastPage || []).map(event => Number(event.blockNumber || 0)), + ); + const nextBlock = BigInt(Math.max(Number(lastPageParam), lastPageHighestBlock) + 1); + + if (nextBlock > blockNumber) return undefined; + + return nextBlock; }, select: data => { const events = data.pages.flat(); @@ -133,6 +150,7 @@ export const useScaffoldEventHistory = < TTransactionData, TReceiptData >; + return { pages: eventHistoryData?.reverse(), pageParams: data.pageParams, diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts index 9d9e8f031..82dfe3dc9 100644 --- a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts +++ b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts @@ -1,10 +1,11 @@ import { useEffect } from "react"; -import { useTargetNetwork } from "./useTargetNetwork"; import { QueryObserverResult, RefetchOptions, useQueryClient } from "@tanstack/react-query"; import type { ExtractAbiFunctionNames } from "abitype"; import { ReadContractErrorType } from "viem"; import { useBlockNumber, useReadContract } from "wagmi"; +import { useSelectedNetwork } from "~~/hooks/scaffold-eth"; import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import { AllowedChainIds } from "~~/utils/scaffold-eth"; import { AbiFunctionReturnType, ContractAbi, @@ -19,6 +20,7 @@ import { * @param config.contractName - deployed contract name * @param config.functionName - name of the function to be called * @param config.args - args to be passed to the function call + * @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions. */ export const useScaffoldReadContract = < TContractName extends ContractName, @@ -27,16 +29,21 @@ export const useScaffoldReadContract = < contractName, functionName, args, + chainId, ...readConfig }: UseScaffoldReadConfig) => { - const { data: deployedContract } = useDeployedContractInfo(contractName); - const { targetNetwork } = useTargetNetwork(); + const selectedNetwork = useSelectedNetwork(chainId); + const { data: deployedContract } = useDeployedContractInfo({ + contractName, + chainId: selectedNetwork.id as AllowedChainIds, + }); + const { query: queryOptions, watch, ...readContractConfig } = readConfig; // set watch to true by default const defaultWatch = watch ?? true; const readContractHookRes = useReadContract({ - chainId: targetNetwork.id, + chainId: selectedNetwork.id, functionName, address: deployedContract?.address, abi: deployedContract?.abi, @@ -56,7 +63,7 @@ export const useScaffoldReadContract = < const queryClient = useQueryClient(); const { data: blockNumber } = useBlockNumber({ watch: defaultWatch, - chainId: targetNetwork.id, + chainId: selectedNetwork.id, query: { enabled: defaultWatch, }, diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldWatchContractEvent.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldWatchContractEvent.ts index 844b4a08b..26878a5ce 100644 --- a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldWatchContractEvent.ts +++ b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldWatchContractEvent.ts @@ -1,8 +1,9 @@ -import { useTargetNetwork } from "./useTargetNetwork"; import { Abi, ExtractAbiEventNames } from "abitype"; import { Log } from "viem"; import { useWatchContractEvent } from "wagmi"; +import { useSelectedNetwork } from "~~/hooks/scaffold-eth"; import { addIndexedArgsToEvent, useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import { AllowedChainIds } from "~~/utils/scaffold-eth"; import { ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaffold-eth/contract"; /** @@ -11,6 +12,7 @@ import { ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaf * @param config - The config settings * @param config.contractName - deployed contract name * @param config.eventName - name of the event to listen for + * @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions. * @param config.onLogs - the callback that receives events. */ export const useScaffoldWatchContractEvent = < @@ -19,10 +21,14 @@ export const useScaffoldWatchContractEvent = < >({ contractName, eventName, + chainId, onLogs, }: UseScaffoldEventConfig) => { - const { data: deployedContractData } = useDeployedContractInfo(contractName); - const { targetNetwork } = useTargetNetwork(); + const selectedNetwork = useSelectedNetwork(chainId); + const { data: deployedContractData } = useDeployedContractInfo({ + contractName, + chainId: selectedNetwork.id as AllowedChainIds, + }); const addIndexedArgsToLogs = (logs: Log[]) => logs.map(addIndexedArgsToEvent); const listenerWithIndexedArgs = (logs: Log[]) => onLogs(addIndexedArgsToLogs(logs) as Parameters[0]); @@ -30,7 +36,7 @@ export const useScaffoldWatchContractEvent = < return useWatchContractEvent({ address: deployedContractData?.address, abi: deployedContractData?.abi as Abi, - chainId: targetNetwork.id, + chainId: selectedNetwork.id, onLogs: listenerWithIndexedArgs, eventName, }); diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts index 4bd903f7f..c6707c709 100644 --- a/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts +++ b/templates/base/packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts @@ -1,37 +1,85 @@ -import { useState } from "react"; -import { useTargetNetwork } from "./useTargetNetwork"; +import { useEffect, useState } from "react"; import { MutateOptions } from "@tanstack/react-query"; import { Abi, ExtractAbiFunctionNames } from "abitype"; import { Config, UseWriteContractParameters, useAccount, useWriteContract } from "wagmi"; import { WriteContractErrorType, WriteContractReturnType } from "wagmi/actions"; import { WriteContractVariables } from "wagmi/query"; +import { useSelectedNetwork } from "~~/hooks/scaffold-eth"; import { useDeployedContractInfo, useTransactor } from "~~/hooks/scaffold-eth"; -import { notification } from "~~/utils/scaffold-eth"; +import { AllowedChainIds, notification } from "~~/utils/scaffold-eth"; import { ContractAbi, ContractName, ScaffoldWriteContractOptions, ScaffoldWriteContractVariables, + UseScaffoldWriteConfig, } from "~~/utils/scaffold-eth/contract"; +type ScaffoldWriteContractReturnType = Omit< + ReturnType, + "writeContract" | "writeContractAsync" +> & { + isMining: boolean; + writeContractAsync: < + TFunctionName extends ExtractAbiFunctionNames, "nonpayable" | "payable">, + >( + variables: ScaffoldWriteContractVariables, + options?: ScaffoldWriteContractOptions, + ) => Promise; + writeContract: , "nonpayable" | "payable">>( + variables: ScaffoldWriteContractVariables, + options?: Omit, + ) => void; +}; + +export function useScaffoldWriteContract( + config: UseScaffoldWriteConfig, +): ScaffoldWriteContractReturnType; +/** + * @deprecated Use object parameter version instead: useScaffoldWriteContract({ contractName: "YourContract" }) + */ +export function useScaffoldWriteContract( + contractName: TContractName, + writeContractParams?: UseWriteContractParameters, +): ScaffoldWriteContractReturnType; + /** * Wrapper around wagmi's useWriteContract hook which automatically loads (by name) the contract ABI and address from * the contracts present in deployedContracts.ts & externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts * @param contractName - name of the contract to be written to + * @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions. * @param writeContractParams - wagmi's useWriteContract parameters */ -export const useScaffoldWriteContract = ( - contractName: TContractName, +export function useScaffoldWriteContract( + configOrName: UseScaffoldWriteConfig | TContractName, writeContractParams?: UseWriteContractParameters, -) => { - const { chain } = useAccount(); +): ScaffoldWriteContractReturnType { + const finalConfig = + typeof configOrName === "string" + ? { contractName: configOrName, writeContractParams, chainId: undefined } + : (configOrName as UseScaffoldWriteConfig); + const { contractName, chainId, writeContractParams: finalWriteContractParams } = finalConfig; + + useEffect(() => { + if (typeof configOrName === "string") { + console.warn( + "Using `useScaffoldWriteContract` with a string parameter is deprecated. Please use the object parameter version instead.", + ); + } + }, [configOrName]); + + const { chain: accountChain } = useAccount(); const writeTx = useTransactor(); const [isMining, setIsMining] = useState(false); - const { targetNetwork } = useTargetNetwork(); - const wagmiContractWrite = useWriteContract(writeContractParams); + const wagmiContractWrite = useWriteContract(finalWriteContractParams); + + const selectedNetwork = useSelectedNetwork(chainId); - const { data: deployedContractData } = useDeployedContractInfo(contractName); + const { data: deployedContractData } = useDeployedContractInfo({ + contractName, + chainId: selectedNetwork.id as AllowedChainIds, + }); const sendContractWriteAsyncTx = async < TFunctionName extends ExtractAbiFunctionNames, "nonpayable" | "payable">, @@ -44,12 +92,13 @@ export const useScaffoldWriteContract = ( return; } - if (!chain?.id) { + if (!accountChain?.id) { notification.error("Please connect your wallet"); return; } - if (chain?.id !== targetNetwork.id) { - notification.error("You are on the wrong network"); + + if (accountChain?.id !== selectedNetwork.id) { + notification.error(`Wallet is connected to the wrong network. Please switch to ${selectedNetwork.name}`); return; } @@ -93,12 +142,13 @@ export const useScaffoldWriteContract = ( notification.error("Target Contract is not deployed, did you forget to run `yarn deploy`?"); return; } - if (!chain?.id) { + if (!accountChain?.id) { notification.error("Please connect your wallet"); return; } - if (chain?.id !== targetNetwork.id) { - notification.error("You are on the wrong network"); + + if (accountChain?.id !== selectedNetwork.id) { + notification.error(`Wallet is connected to the wrong network. Please switch to ${selectedNetwork.name}`); return; } @@ -127,4 +177,4 @@ export const useScaffoldWriteContract = ( // Overwrite wagmi's writeContract writeContract: sendContractWriteTx, }; -}; +} diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts new file mode 100644 index 000000000..93832ef4a --- /dev/null +++ b/templates/base/packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts @@ -0,0 +1,8 @@ +import scaffoldConfig from "~~/scaffold.config"; +import { useGlobalState } from "~~/services/store/store"; +import { AllowedChainIds } from "~~/utils/scaffold-eth"; + +export function useSelectedNetwork(chainId?: AllowedChainIds) { + const targetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork); + return scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chainId) ?? targetNetwork; +} diff --git a/templates/base/packages/nextjs/package.json b/templates/base/packages/nextjs/package.json index ad08902ef..bd878e05f 100644 --- a/templates/base/packages/nextjs/package.json +++ b/templates/base/packages/nextjs/package.json @@ -3,15 +3,15 @@ "private": true, "version": "0.1.0", "scripts": { - "dev": "next dev", - "start": "next dev", "build": "next build", - "serve": "next start", - "lint": "next lint", - "format": "prettier --write . '!(node_modules|.next|contracts)/**/*'", "check-types": "tsc --noEmit --incremental", - "vercel": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1", - "vercel:yolo": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true" + "dev": "next dev", + "format": "prettier --write . '!(node_modules|.next|contracts)/**/*'", + "lint": "next lint", + "serve": "next start", + "start": "next dev", + "vercel": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env VERCEL_TELEMETRY_DISABLED=1", + "vercel:yolo": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true --build-env VERCEL_TELEMETRY_DISABLED=1" }, "dependencies": { "@heroicons/react": "~2.1.5", diff --git a/templates/base/packages/nextjs/utils/scaffold-eth/contract.ts b/templates/base/packages/nextjs/utils/scaffold-eth/contract.ts index b66f42a9b..70d1d4c32 100644 --- a/templates/base/packages/nextjs/utils/scaffold-eth/contract.ts +++ b/templates/base/packages/nextjs/utils/scaffold-eth/contract.ts @@ -1,3 +1,4 @@ +import { AllowedChainIds } from "./networks"; import { MutateOptions } from "@tanstack/react-query"; import { Abi, @@ -21,7 +22,7 @@ import { TransactionReceipt, WriteContractErrorType, } from "viem"; -import { Config, UseReadContractParameters, UseWatchContractEventParameters } from "wagmi"; +import { Config, UseReadContractParameters, UseWatchContractEventParameters, UseWriteContractParameters } from "wagmi"; import { WriteContractParameters, WriteContractReturnType } from "wagmi/actions"; import { WriteContractVariables } from "wagmi/query"; import deployedContractsData from "~~/contracts/deployedContracts"; @@ -166,11 +167,23 @@ type UseScaffoldArgsParam< args?: never; }; +export type UseDeployedContractConfig = { + contractName: TContractName; + chainId?: AllowedChainIds; +}; + +export type UseScaffoldWriteConfig = { + contractName: TContractName; + chainId?: AllowedChainIds; + writeContractParams?: UseWriteContractParameters; +}; + export type UseScaffoldReadConfig< TContractName extends ContractName, TFunctionName extends ExtractAbiFunctionNames, ReadAbiStateMutability>, > = { contractName: TContractName; + chainId?: AllowedChainIds; watch?: boolean; } & IsContractDeclarationMissing< Partial, @@ -216,6 +229,7 @@ export type UseScaffoldEventConfig< > = { contractName: TContractName; eventName: TEventName; + chainId?: AllowedChainIds; } & IsContractDeclarationMissing< Omit & { onLogs: ( @@ -275,6 +289,7 @@ export type UseScaffoldEventHistoryConfig< contractName: TContractName; eventName: IsContractDeclarationMissing; fromBlock: bigint; + chainId?: AllowedChainIds; filters?: EventFilters; blockData?: TBlockData; transactionData?: TTransactionData; @@ -297,7 +312,6 @@ export type UseScaffoldEventHistoryData< | IsContractDeclarationMissing< any[], { - log: Log; args: AbiParametersToPrimitiveTypes & GetEventArgs< ContractAbi, @@ -309,7 +323,7 @@ export type UseScaffoldEventHistoryData< blockData: TBlockData extends true ? Block : null; receiptData: TReceiptData extends true ? GetTransactionReturnType : null; transactionData: TTransactionData extends true ? GetTransactionReceiptReturnType : null; - }[] + } & Log[] > | undefined; diff --git a/templates/base/packages/nextjs/utils/scaffold-eth/networks.ts b/templates/base/packages/nextjs/utils/scaffold-eth/networks.ts index 96b779866..2a5802050 100644 --- a/templates/base/packages/nextjs/utils/scaffold-eth/networks.ts +++ b/templates/base/packages/nextjs/utils/scaffold-eth/networks.ts @@ -10,6 +10,7 @@ type ChainAttributes = { }; export type ChainWithAttributes = chains.Chain & Partial; +export type AllowedChainIds = (typeof scaffoldConfig.targetNetworks)[number]["id"]; // Mapping of chainId to RPC chain name an format followed by alchemy and infura export const RPC_CHAIN_NAMES: Record = { diff --git a/templates/example-contracts/foundry/packages/foundry/script/DeployYourContract.s.sol b/templates/example-contracts/foundry/packages/foundry/script/DeployYourContract.s.sol index 166e2df57..7f82e5afa 100644 --- a/templates/example-contracts/foundry/packages/foundry/script/DeployYourContract.s.sol +++ b/templates/example-contracts/foundry/packages/foundry/script/DeployYourContract.s.sol @@ -1,13 +1,30 @@ -//SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "../contracts/YourContract.sol"; import "./DeployHelpers.s.sol"; +import "../contracts/YourContract.sol"; +/** + * @notice Deploy script for YourContract contract + * @dev Inherits ScaffoldETHDeploy which: + * - Includes forge-std/Script.sol for deployment + * - Includes ScaffoldEthDeployerRunner modifier + * - Provides `deployer` variable + * Example: + * yarn deploy --file DeployYourContract.s.sol # local anvil chain + * yarn deploy --file DeployYourContract.s.sol --network optimism # live network (requires keystore) + */ contract DeployYourContract is ScaffoldETHDeploy { - // use `deployer` from `ScaffoldETHDeploy` - function run() external ScaffoldEthDeployerRunner { - YourContract yourContract = new YourContract(deployer); - console.logString(string.concat("YourContract deployed at: ", vm.toString(address(yourContract)))); - } + /** + * @dev Deployer setup based on `ETH_KEYSTORE_ACCOUNT` in `.env`: + * - "scaffold-eth-default": Uses Anvil's account #9 (0xa0Ee7A142d267C1f36714E4a8F75612F20a79720), no password prompt + * - "scaffold-eth-custom": requires password used while creating keystore + * + * Note: Must use ScaffoldEthDeployerRunner modifier to: + * - Setup correct `deployer` account and fund it + * - Export contract addresses & ABIs to `nextjs` packages + */ + function run() external ScaffoldEthDeployerRunner { + new YourContract(deployer); + } } diff --git a/templates/solidity-frameworks/foundry/package.json b/templates/solidity-frameworks/foundry/package.json index cb202038f..ab69202ee 100644 --- a/templates/solidity-frameworks/foundry/package.json +++ b/templates/solidity-frameworks/foundry/package.json @@ -1,20 +1,30 @@ { "scripts": { - "account": "yarn workspace @se-2/foundry account", - "fork": "yarn workspace @se-2/foundry fork", - "compile": "yarn workspace @se-2/foundry compile", - "chain": "yarn workspace @se-2/foundry chain", - "deploy": "yarn workspace @se-2/foundry deploy", - "deploy:verify": "yarn workspace @se-2/foundry deploy:verify", - "account:generate": "yarn workspace @se-2/foundry account:generate", - "account:import": "yarn workspace @se-2/foundry account:import", + "account": "yarn foundry:account", + "account:generate": "yarn foundry:generate", + "account:import": "yarn foundry:account-import", + "chain": "yarn foundry:chain", + "compile": "yarn foundry:compile", + "deploy": "yarn foundry:deploy", + "deploy:verify": "yarn foundry:deploy-verify", + "fork": "yarn foundry:fork", + "format": "yarn next:format && yarn foundry:format", + "foundry:account": "yarn workspace @se-2/foundry account", + "foundry:account-import": "yarn workspace @se-2/foundry account:import", + "foundry:chain": "yarn workspace @se-2/foundry chain", + "foundry:compile": "yarn workspace @se-2/foundry compile", + "foundry:deploy": "yarn workspace @se-2/foundry deploy", + "foundry:deploy-verify": "yarn workspace @se-2/foundry deploy:verify", + "foundry:flatten": "yarn workspace @se-2/foundry flatten", + "foundry:fork": "yarn workspace @se-2/foundry fork", + "foundry:format": "yarn workspace @se-2/foundry format", + "foundry:generate": "yarn workspace @se-2/foundry account:generate", "foundry:lint": "yarn workspace @se-2/foundry lint", "foundry:test": "yarn workspace @se-2/foundry test", - "foundry:format": "yarn workspace @se-2/foundry format", + "foundry:verify": "yarn workspace @se-2/foundry verify", + "generate": "yarn foundry:generate", + "lint": "yarn nextjs:lint && yarn foundry:lint", "test": "yarn foundry:test", - "verify": "yarn workspace @se-2/foundry verify", - "generate": "yarn account:generate", - "flatten": "yarn workspace @se-2/foundry flatten", - "format": "yarn next:format && yarn foundry:format" + "verify": "yarn foundry:verify" } } diff --git a/templates/solidity-frameworks/foundry/packages/foundry/Makefile b/templates/solidity-frameworks/foundry/packages/foundry/Makefile index 00144c4d3..c8817e64c 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/Makefile +++ b/templates/solidity-frameworks/foundry/packages/foundry/Makefile @@ -1,8 +1,11 @@ .PHONY: build deploy generate-abis verify-keystore account chain compile deploy-verify flatten fork format lint test verify +DEPLOY_SCRIPT ?= script/Deploy.s.sol + # setup wallet for anvil setup-anvil-wallet: shx rm ~/.foundry/keystores/scaffold-eth-default 2>/dev/null; \ + shx rm -rf broadcast/Deploy.s.sol/31337 cast wallet import --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 --unsafe-password 'localhost' scaffold-eth-default # Start local chain @@ -15,14 +18,22 @@ fork: setup-anvil-wallet # Build the project build: - forge build --build-info --build-info-path out/build-info/ + forge build --via-ir --build-info --build-info-path out/build-info/ -# Deploy the project +# Deploy the contracts deploy: + @if [ ! -f "$(DEPLOY_SCRIPT)" ]; then \ + echo "Error: Deploy script '$(DEPLOY_SCRIPT)' not found"; \ + exit 1; \ + fi @if [ "$(RPC_URL)" = "localhost" ]; then \ - forge script script/Deploy.s.sol --rpc-url localhost --password localhost --broadcast --legacy --ffi; \ + if [ "$(ETH_KEYSTORE_ACCOUNT)" = "scaffold-eth-default" ]; then \ + forge script $(DEPLOY_SCRIPT) --rpc-url localhost --password localhost --broadcast --legacy --ffi; \ + else \ + forge script $(DEPLOY_SCRIPT) --rpc-url localhost --broadcast --legacy --ffi; \ + fi \ else \ - forge script script/Deploy.s.sol --rpc-url $(RPC_URL) --broadcast --legacy --ffi; \ + forge script $(DEPLOY_SCRIPT) --rpc-url $(RPC_URL) --broadcast --legacy --ffi; \ fi # Build and deploy target @@ -35,9 +46,9 @@ generate-abis: verify-keystore: if grep -q "scaffold-eth-default" .env; then \ cast wallet address --password localhost; \ - else \ + else \ cast wallet address; \ - fi + fi # List account account: @@ -58,10 +69,18 @@ compile: # Deploy and verify deploy-verify: + @if [ ! -f "$(DEPLOY_SCRIPT)" ]; then \ + echo "Error: Deploy script '$(DEPLOY_SCRIPT)' not found"; \ + exit 1; \ + fi @if [ "$(RPC_URL)" = "localhost" ]; then \ - forge script script/Deploy.s.sol --rpc-url localhost --password localhost --broadcast --legacy --ffi --verify; \ + if [ "$(ETH_KEYSTORE_ACCOUNT)" = "scaffold-eth-default" ]; then \ + forge script $(DEPLOY_SCRIPT) --rpc-url localhost --password localhost --broadcast --legacy --ffi --verify; \ + else \ + forge script $(DEPLOY_SCRIPT) --rpc-url localhost --broadcast --legacy --ffi --verify; \ + fi \ else \ - forge script script/Deploy.s.sol --rpc-url $(RPC_URL) --broadcast --legacy --ffi --verify; \ + forge script $(DEPLOY_SCRIPT) --rpc-url $(RPC_URL) --broadcast --legacy --ffi --verify; \ fi node scripts-js/generateTsAbis.js @@ -77,10 +96,6 @@ format: lint: forge fmt --check && prettier --check ./script/**/*.js -# Run tests -test: - forge test - # Verify contracts verify: forge script script/VerifyAll.s.sol --ffi --rpc-url $(RPC_URL) diff --git a/templates/solidity-frameworks/foundry/packages/foundry/foundry.toml b/templates/solidity-frameworks/foundry/packages/foundry/foundry.toml index bd4de0621..d3537f6a6 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/foundry.toml +++ b/templates/solidity-frameworks/foundry/packages/foundry/foundry.toml @@ -33,9 +33,8 @@ sepolia = { key = "${ETHERSCAN_API_KEY}" } [fmt] -multiline_func_header = "params_first" -line_length = 80 -tab_width = 2 +line_length = 120 +tab_width = 4 quote_style = "double" bracket_spacing = true int_types = "long" diff --git a/templates/solidity-frameworks/foundry/packages/foundry/package.json b/templates/solidity-frameworks/foundry/packages/foundry/package.json index 960366dc0..35389f931 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/package.json +++ b/templates/solidity-frameworks/foundry/packages/foundry/package.json @@ -3,21 +3,21 @@ "version": "0.0.1", "type": "module", "scripts": { - "verify-keystore": "make verify-keystore", - "account": "make account", - "account:generate": "make account-generate ACCOUNT_NAME=${1:-scaffold-eth-custom}", - "account:import": "make account-import ACCOUNT_NAME=${1:-scaffold-eth-custom}", - "chain": "make chain", - "compile": "make compile", - "deploy": "make build-and-deploy RPC_URL=${1:-localhost}", - "deploy:verify": "make deploy-verify RPC_URL=${1:-localhost}", - "flatten": "make flatten", - "fork": "make fork FORK_URL=${1:-mainnet}", - "format": "make format", - "lint": "make lint", - "test": "make test", - "verify": "make build-and-verify RPC_URL=${1:-localhost}", - "postinstall": "shx cp -n .env.example .env" + "account": "make account", + "account:generate": "make account-generate ACCOUNT_NAME=${1:-scaffold-eth-custom}", + "account:import": "make account-import ACCOUNT_NAME=${1:-scaffold-eth-custom}", + "chain": "make chain", + "compile": "make compile", + "deploy": "node scripts-js/parseArgs.js", + "deploy:verify": "node scripts/parseArgs.js --verify", + "flatten": "make flatten", + "fork": "make fork FORK_URL=${1:-mainnet}", + "format": "make format", + "lint": "make lint", + "postinstall": "shx cp -n .env.example .env", + "test": "forge test", + "verify": "make build-and-verify RPC_URL=${1:-localhost}", + "verify-keystore": "make verify-keystore" }, "dependencies": { "dotenv": "~16.3.1", diff --git a/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs b/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs index 018e7ce21..17324b23a 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs +++ b/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs @@ -6,13 +6,22 @@ pragma solidity ^0.8.19; import "./DeployHelpers.s.sol"; ${deploymentsScriptsImports.filter(Boolean).join("\n")} +/** + * @notice Main deployment script for all contracts + * @dev Run this when you want to deploy multiple contracts at once + * + * Example: yarn deploy # runs this script(without\`--file\` flag) + */ contract DeployScript is ScaffoldETHDeploy { function run() external { + // Deploys all your contracts sequentially + // Add new deployments here when needed + ${deploymentsLogic.filter(Boolean).join("\n")} - // deploy more contracts here - // DeployMyContract deployMyContract = new DeployMyContract(); - // deployMyContract.run(); + // Deploy another contract + // DeployMyContract myContract = new DeployMyContract(); + // myContract.run(); } }`; diff --git a/templates/solidity-frameworks/foundry/packages/foundry/script/DeployHelpers.s.sol b/templates/solidity-frameworks/foundry/packages/foundry/script/DeployHelpers.s.sol index d8bacf668..2d77229c7 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/script/DeployHelpers.s.sol +++ b/templates/solidity-frameworks/foundry/packages/foundry/script/DeployHelpers.s.sol @@ -5,123 +5,117 @@ import { Script, console } from "forge-std/Script.sol"; import { Vm } from "forge-std/Vm.sol"; contract ScaffoldETHDeploy is Script { - error InvalidChain(); - error DeployerHasNoBalance(); - error InvalidPrivateKey(string); - - event AnvilSetBalance(address account, uint256 amount); - event FailedAnvilRequest(); - - struct Deployment { - string name; - address addr; - } - - string root; - string path; - Deployment[] public deployments; - uint256 constant ANVIL_BASE_BALANCE = 10000 ether; - - /// @notice The deployer address for every run - address deployer; - - /// @notice Use this modifier on your run() function on your deploy scripts - modifier ScaffoldEthDeployerRunner() { - deployer = _startBroadcast(); - if (deployer == address(0)) { - revert InvalidPrivateKey("Invalid private key"); + error InvalidChain(); + error DeployerHasNoBalance(); + error InvalidPrivateKey(string); + + event AnvilSetBalance(address account, uint256 amount); + event FailedAnvilRequest(); + + struct Deployment { + string name; + address addr; } - _; - _stopBroadcast(); - exportDeployments(); - } - - function _startBroadcast() internal returns (address) { - vm.startBroadcast(); - (, address _deployer,) = vm.readCallers(); - - if (block.chainid == 31337 && _deployer.balance == 0) { - try this.anvil_setBalance(_deployer, ANVIL_BASE_BALANCE) { - emit AnvilSetBalance(_deployer, ANVIL_BASE_BALANCE); - } catch { - emit FailedAnvilRequest(); - } + + string root; + string path; + Deployment[] public deployments; + uint256 constant ANVIL_BASE_BALANCE = 10000 ether; + + /// @notice The deployer address for every run + address deployer; + + /// @notice Use this modifier on your run() function on your deploy scripts + modifier ScaffoldEthDeployerRunner() { + deployer = _startBroadcast(); + if (deployer == address(0)) { + revert InvalidPrivateKey("Invalid private key"); + } + _; + _stopBroadcast(); + exportDeployments(); } - return _deployer; - } - function _stopBroadcast() internal { - vm.stopBroadcast(); - } + function _startBroadcast() internal returns (address) { + vm.startBroadcast(); + (, address _deployer,) = vm.readCallers(); - function exportDeployments() internal { - // fetch already existing contracts - root = vm.projectRoot(); - path = string.concat(root, "/deployments/"); - string memory chainIdStr = vm.toString(block.chainid); - path = string.concat(path, string.concat(chainIdStr, ".json")); + if (block.chainid == 31337 && _deployer.balance == 0) { + try this.anvil_setBalance(_deployer, ANVIL_BASE_BALANCE) { + emit AnvilSetBalance(_deployer, ANVIL_BASE_BALANCE); + } catch { + emit FailedAnvilRequest(); + } + } + return _deployer; + } - string memory jsonWrite; + function _stopBroadcast() internal { + vm.stopBroadcast(); + } - uint256 len = deployments.length; + function exportDeployments() internal { + // fetch already existing contracts + root = vm.projectRoot(); + path = string.concat(root, "/deployments/"); + string memory chainIdStr = vm.toString(block.chainid); + path = string.concat(path, string.concat(chainIdStr, ".json")); - for (uint256 i = 0; i < len; i++) { - vm.serializeString( - jsonWrite, vm.toString(deployments[i].addr), deployments[i].name - ); + string memory jsonWrite; + + uint256 len = deployments.length; + + for (uint256 i = 0; i < len; i++) { + vm.serializeString(jsonWrite, vm.toString(deployments[i].addr), deployments[i].name); + } + + string memory chainName; + + try this.getChain() returns (Chain memory chain) { + chainName = chain.name; + } catch { + chainName = findChainName(); + } + jsonWrite = vm.serializeString(jsonWrite, "networkName", chainName); + vm.writeJson(jsonWrite, path); } - string memory chainName; + function getChain() public returns (Chain memory) { + return getChain(block.chainid); + } - try this.getChain() returns (Chain memory chain) { - chainName = chain.name; - } catch { - chainName = findChainName(); + function anvil_setBalance(address addr, uint256 amount) public { + string memory addressString = vm.toString(addr); + string memory amountString = vm.toString(amount); + string memory requestPayload = string.concat( + '{"method":"anvil_setBalance","params":["', addressString, '","', amountString, '"],"id":1,"jsonrpc":"2.0"}' + ); + + string[] memory inputs = new string[](8); + inputs[0] = "curl"; + inputs[1] = "-X"; + inputs[2] = "POST"; + inputs[3] = "http://localhost:8545"; + inputs[4] = "-H"; + inputs[5] = "Content-Type: application/json"; + inputs[6] = "--data"; + inputs[7] = requestPayload; + + vm.ffi(inputs); } - jsonWrite = vm.serializeString(jsonWrite, "networkName", chainName); - vm.writeJson(jsonWrite, path); - } - - function getChain() public returns (Chain memory) { - return getChain(block.chainid); - } - - function anvil_setBalance(address addr, uint256 amount) public { - string memory addressString = vm.toString(addr); - string memory amountString = vm.toString(amount); - string memory requestPayload = string.concat( - '{"method":"anvil_setBalance","params":["', - addressString, - '","', - amountString, - '"],"id":1,"jsonrpc":"2.0"}' - ); - - string[] memory inputs = new string[](8); - inputs[0] = "curl"; - inputs[1] = "-X"; - inputs[2] = "POST"; - inputs[3] = "http://localhost:8545"; - inputs[4] = "-H"; - inputs[5] = "Content-Type: application/json"; - inputs[6] = "--data"; - inputs[7] = requestPayload; - - vm.ffi(inputs); - } - - function findChainName() public returns (string memory) { - uint256 thisChainId = block.chainid; - string[2][] memory allRpcUrls = vm.rpcUrls(); - for (uint256 i = 0; i < allRpcUrls.length; i++) { - try vm.createSelectFork(allRpcUrls[i][1]) { - if (block.chainid == thisChainId) { - return allRpcUrls[i][0]; + + function findChainName() public returns (string memory) { + uint256 thisChainId = block.chainid; + string[2][] memory allRpcUrls = vm.rpcUrls(); + for (uint256 i = 0; i < allRpcUrls.length; i++) { + try vm.createSelectFork(allRpcUrls[i][1]) { + if (block.chainid == thisChainId) { + return allRpcUrls[i][0]; + } + } catch { + continue; + } } - } catch { - continue; - } + revert InvalidChain(); } - revert InvalidChain(); - } } diff --git a/templates/solidity-frameworks/foundry/packages/foundry/script/VerifyAll.s.sol b/templates/solidity-frameworks/foundry/packages/foundry/script/VerifyAll.s.sol index 2b307a04d..c6e58669d 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/script/VerifyAll.s.sol +++ b/templates/solidity-frameworks/foundry/packages/foundry/script/VerifyAll.s.sol @@ -11,121 +11,91 @@ import "solidity-bytes-utils/BytesLib.sol"; * @notice will be deleted once the forge/std is updated */ struct FfiResult { - int32 exit_code; - bytes stdout; - bytes stderr; + int32 exit_code; + bytes stdout; + bytes stderr; } interface tempVm { - function tryFfi(string[] calldata) external returns (FfiResult memory); + function tryFfi(string[] calldata) external returns (FfiResult memory); } contract VerifyAll is Script { - uint96 currTransactionIdx; + uint96 currTransactionIdx; - function run() external { - string memory root = vm.projectRoot(); - string memory path = string.concat( - root, - "/broadcast/Deploy.s.sol/", - vm.toString(block.chainid), - "/run-latest.json" - ); - string memory content = vm.readFile(path); + function run() external { + string memory root = vm.projectRoot(); + string memory path = + string.concat(root, "/broadcast/Deploy.s.sol/", vm.toString(block.chainid), "/run-latest.json"); + string memory content = vm.readFile(path); - while (this.nextTransaction(content)) { - _verifyIfContractDeployment(content); - currTransactionIdx++; + while (this.nextTransaction(content)) { + _verifyIfContractDeployment(content); + currTransactionIdx++; + } } - } - function _verifyIfContractDeployment(string memory content) internal { - string memory txType = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "transactionType")), - (string) - ); - if (keccak256(bytes(txType)) == keccak256(bytes("CREATE"))) { - _verifyContract(content); + function _verifyIfContractDeployment(string memory content) internal { + string memory txType = + abi.decode(vm.parseJson(content, searchStr(currTransactionIdx, "transactionType")), (string)); + if (keccak256(bytes(txType)) == keccak256(bytes("CREATE"))) { + _verifyContract(content); + } } - } - function _verifyContract(string memory content) internal { - string memory contractName = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "contractName")), - (string) - ); - address contractAddr = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "contractAddress")), - (address) - ); - bytes memory deployedBytecode = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "transaction.input")), - (bytes) - ); - bytes memory compiledBytecode = abi.decode( - vm.parseJson(_getCompiledBytecode(contractName), ".bytecode.object"), - (bytes) - ); - bytes memory constructorArgs = BytesLib.slice( - deployedBytecode, - compiledBytecode.length, - deployedBytecode.length - compiledBytecode.length - ); + function _verifyContract(string memory content) internal { + string memory contractName = + abi.decode(vm.parseJson(content, searchStr(currTransactionIdx, "contractName")), (string)); + address contractAddr = + abi.decode(vm.parseJson(content, searchStr(currTransactionIdx, "contractAddress")), (address)); + bytes memory deployedBytecode = + abi.decode(vm.parseJson(content, searchStr(currTransactionIdx, "transaction.input")), (bytes)); + bytes memory compiledBytecode = + abi.decode(vm.parseJson(_getCompiledBytecode(contractName), ".bytecode.object"), (bytes)); + bytes memory constructorArgs = + BytesLib.slice(deployedBytecode, compiledBytecode.length, deployedBytecode.length - compiledBytecode.length); - string[] memory inputs = new string[](9); - inputs[0] = "forge"; - inputs[1] = "verify-contract"; - inputs[2] = vm.toString(contractAddr); - inputs[3] = contractName; - inputs[4] = "--chain"; - inputs[5] = vm.toString(block.chainid); - inputs[6] = "--constructor-args"; - inputs[7] = vm.toString(constructorArgs); - inputs[8] = "--watch"; + string[] memory inputs = new string[](9); + inputs[0] = "forge"; + inputs[1] = "verify-contract"; + inputs[2] = vm.toString(contractAddr); + inputs[3] = contractName; + inputs[4] = "--chain"; + inputs[5] = vm.toString(block.chainid); + inputs[6] = "--constructor-args"; + inputs[7] = vm.toString(constructorArgs); + inputs[8] = "--watch"; - FfiResult memory f = tempVm(address(vm)).tryFfi(inputs); + FfiResult memory f = tempVm(address(vm)).tryFfi(inputs); - if (f.stderr.length != 0) { - console.logString( - string.concat( - "Submitting verification for contract: ", vm.toString(contractAddr) - ) - ); - console.logString(string(f.stderr)); - } else { - console.logString(string(f.stdout)); + if (f.stderr.length != 0) { + console.logString(string.concat("Submitting verification for contract: ", vm.toString(contractAddr))); + console.logString(string(f.stderr)); + } else { + console.logString(string(f.stdout)); + } + return; } - return; - } - function nextTransaction(string memory content) external view returns (bool) { - try this.getTransactionFromRaw(content, currTransactionIdx) { - return true; - } catch { - return false; + function nextTransaction(string memory content) external view returns (bool) { + try this.getTransactionFromRaw(content, currTransactionIdx) { + return true; + } catch { + return false; + } } - } - function _getCompiledBytecode( - string memory contractName - ) internal view returns (string memory compiledBytecode) { - string memory root = vm.projectRoot(); - string memory path = - string.concat(root, "/out/", contractName, ".sol/", contractName, ".json"); - compiledBytecode = vm.readFile(path); - } + function _getCompiledBytecode(string memory contractName) internal view returns (string memory compiledBytecode) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/out/", contractName, ".sol/", contractName, ".json"); + compiledBytecode = vm.readFile(path); + } - function getTransactionFromRaw( - string memory content, - uint96 idx - ) external pure { - abi.decode(vm.parseJson(content, searchStr(idx, "hash")), (bytes32)); - } + function getTransactionFromRaw(string memory content, uint96 idx) external pure { + abi.decode(vm.parseJson(content, searchStr(idx, "hash")), (bytes32)); + } - function searchStr( - uint96 idx, - string memory searchKey - ) internal pure returns (string memory) { - return string.concat(".transactions[", vm.toString(idx), "].", searchKey); - } + function searchStr(uint96 idx, string memory searchKey) internal pure returns (string memory) { + return string.concat(".transactions[", vm.toString(idx), "].", searchKey); + } } diff --git a/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/generateTsAbis.js b/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/generateTsAbis.js index 274f12cb3..0862e875e 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/generateTsAbis.js +++ b/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/generateTsAbis.js @@ -16,25 +16,78 @@ const generatedContractComment = ` /** * This file is autogenerated by Scaffold-ETH. * You should not edit it manually or your changes might be overwritten. - */ -`; + */`; function getDirectories(path) { return readdirSync(path).filter(function (file) { - return statSync(path + "/" + file).isDirectory(); + return statSync(join(path, file)).isDirectory(); }); } + function getFiles(path) { return readdirSync(path).filter(function (file) { - return statSync(path + "/" + file).isFile(); + return statSync(join(path, file)).isFile(); }); } + +function parseTransactionRun(filePath) { + try { + const content = readFileSync(filePath, "utf8"); + const broadcastData = JSON.parse(content); + return broadcastData.transactions || []; + } catch (error) { + console.warn(`Warning: Could not parse ${filePath}:`, error.message); + return []; + } +} + +function getDeploymentHistory(broadcastPath) { + const files = getFiles(broadcastPath); + const deploymentHistory = new Map(); + + // Sort files to process them in chronological order + const runFiles = files + .filter( + (file) => + file.startsWith("run-") && + file.endsWith(".json") && + !file.includes("run-latest") + ) + .sort((a, b) => { + // Extract run numbers and compare them + const runA = parseInt(a.match(/run-(\d+)/)?.[1] || "0"); + const runB = parseInt(b.match(/run-(\d+)/)?.[1] || "0"); + return runA - runB; + }); + + for (const file of runFiles) { + const transactions = parseTransactionRun(join(broadcastPath, file)); + + for (const tx of transactions) { + if (tx.transactionType === "CREATE") { + // Store or update contract deployment info + deploymentHistory.set(tx.contractAddress, { + contractName: tx.contractName, + address: tx.contractAddress, + deploymentFile: file, + transaction: tx, + }); + } + } + } + + return Array.from(deploymentHistory.values()); +} + function getArtifactOfContract(contractName) { const current_path_to_artifacts = join( __dirname, "..", `out/${contractName}.sol` ); + + if (!existsSync(current_path_to_artifacts)) return null; + const artifactJson = JSON.parse( readFileSync(`${current_path_to_artifacts}/${contractName}.json`) ); @@ -62,32 +115,88 @@ function getInheritedFunctions(mainArtifact) { const inheritedFromContracts = getInheritedFromContracts(mainArtifact); const inheritedFunctions = {}; for (const inheritanceContractName of inheritedFromContracts) { - const { - abi, - ast: { absolutePath }, - } = getArtifactOfContract(inheritanceContractName); - for (const abiEntry of abi) { - if (abiEntry.type == "function") { - inheritedFunctions[abiEntry.name] = absolutePath; + const artifact = getArtifactOfContract(inheritanceContractName); + if (artifact) { + const { + abi, + ast: { absolutePath }, + } = artifact; + for (const abiEntry of abi) { + if (abiEntry.type == "function") { + inheritedFunctions[abiEntry.name] = absolutePath; + } } } } return inheritedFunctions; } +function processAllDeployments(broadcastPath) { + const scriptFolders = getDirectories(broadcastPath); + const allDeployments = new Map(); + + scriptFolders.forEach((scriptFolder) => { + const scriptPath = join(broadcastPath, scriptFolder); + const chainFolders = getDirectories(scriptPath); + + chainFolders.forEach((chainId) => { + const chainPath = join(scriptPath, chainId); + const deploymentHistory = getDeploymentHistory(chainPath); + + deploymentHistory.forEach((deployment) => { + const timestamp = parseInt( + deployment.deploymentFile.match(/run-(\d+)/)?.[1] || "0" + ); + const key = `${chainId}-${deployment.contractName}`; + + // Only update if this deployment is newer + if ( + !allDeployments.has(key) || + timestamp > allDeployments.get(key).timestamp + ) { + allDeployments.set(key, { + ...deployment, + timestamp, + chainId, + deploymentScript: scriptFolder, + }); + } + }); + }); + }); + + const allContracts = {}; + + allDeployments.forEach((deployment) => { + const { chainId, contractName } = deployment; + const artifact = getArtifactOfContract(contractName); + + if (artifact) { + if (!allContracts[chainId]) { + allContracts[chainId] = {}; + } + + allContracts[chainId][contractName] = { + address: deployment.address, + abi: artifact.abi, + inheritedFunctions: getInheritedFunctions(artifact), + deploymentFile: deployment.deploymentFile, + deploymentScript: deployment.deploymentScript, + }; + } + }); + + return allContracts; +} + function main() { - const current_path_to_broadcast = join( - __dirname, - "..", - "broadcast/Deploy.s.sol" - ); + const current_path_to_broadcast = join(__dirname, "..", "broadcast"); const current_path_to_deployments = join(__dirname, "..", "deployments"); - const chains = getDirectories(current_path_to_broadcast); const Deploymentchains = getFiles(current_path_to_deployments); - const deployments = {}; + // Load existing deployments from deployments directory Deploymentchains.forEach((chain) => { if (!chain.endsWith(".json")) return; chain = chain.slice(0, -5); @@ -97,31 +206,31 @@ function main() { deployments[chain] = deploymentObject; }); - const allGeneratedContracts = {}; + // Process all deployments from all script folders + const allGeneratedContracts = processAllDeployments( + current_path_to_broadcast + ); - chains.forEach((chain) => { - allGeneratedContracts[chain] = {}; - const broadCastObject = JSON.parse( - readFileSync(`${current_path_to_broadcast}/${chain}/run-latest.json`) - ); - const transactionsCreate = broadCastObject.transactions.filter( - (transaction) => transaction.transactionType == "CREATE" - ); - transactionsCreate.forEach((transaction) => { - const artifact = getArtifactOfContract(transaction.contractName); - allGeneratedContracts[chain][ - deployments[chain][transaction.contractAddress] || - transaction.contractName - ] = { - address: transaction.contractAddress, - abi: artifact.abi, - inheritedFunctions: getInheritedFunctions(artifact), - }; + // Update contract keys based on deployments if they exist + Object.entries(allGeneratedContracts).forEach(([chainId, contracts]) => { + Object.entries(contracts).forEach(([contractName, contractData]) => { + const deployedName = deployments[chainId]?.[contractData.address]; + if (deployedName) { + // If we have a deployment name, use it instead of the contract name + allGeneratedContracts[chainId][deployedName] = contractData; + delete allGeneratedContracts[chainId][contractName]; + } }); }); - const TARGET_DIR = "../nextjs/contracts/"; + const NEXTJS_TARGET_DIR = "../nextjs/contracts/"; + + // Ensure target directories exist + if (!existsSync(NEXTJS_TARGET_DIR)) { + mkdirSync(NEXTJS_TARGET_DIR, { recursive: true }); + } + // Generate the deployedContracts content const fileContent = Object.entries(allGeneratedContracts).reduce( (content, [chainId, chainConfig]) => { return `${content}${parseInt(chainId).toFixed(0)}:${JSON.stringify( @@ -133,24 +242,31 @@ function main() { "" ); - if (!existsSync(TARGET_DIR)) { - mkdirSync(TARGET_DIR); - } + // Write the files + const fileTemplate = (importPath) => ` + ${generatedContractComment} + import { GenericContractsDeclaration } from "${importPath}"; + + const deployedContracts = {${fileContent}} as const; + + export default deployedContracts satisfies GenericContractsDeclaration; + `; + writeFileSync( - `${TARGET_DIR}deployedContracts.ts`, - format( - `${generatedContractComment} import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; \n\n - const deployedContracts = {${fileContent}} as const; \n\n export default deployedContracts satisfies GenericContractsDeclaration`, - { - parser: "typescript", - } - ) + `${NEXTJS_TARGET_DIR}deployedContracts.ts`, + format(fileTemplate("~~/utils/scaffold-eth/contract"), { + parser: "typescript", + }) + ); + + console.log( + `šŸ“ Updated TypeScript contract definition file on ${NEXTJS_TARGET_DIR}deployedContracts.ts` ); } try { main(); } catch (error) { - console.error(error); + console.error("Error:", error); process.exitCode = 1; } diff --git a/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/parseArgs.js b/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/parseArgs.js new file mode 100644 index 000000000..a9f2fea78 --- /dev/null +++ b/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/parseArgs.js @@ -0,0 +1,115 @@ +import { spawnSync } from "child_process"; +import { config } from "dotenv"; +import { join, dirname } from "path"; +import { readFileSync } from "fs"; +import { parse } from "toml"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +config(); + +// Get all arguments after the script name +const args = process.argv.slice(2); +let fileName = "Deploy.s.sol"; +let network = "localhost"; + +// Show help message if --help is provided +if (args.includes("--help") || args.includes("-h")) { + console.log(` +Usage: yarn deploy [options] +Options: + --file Specify the deployment script file (default: Deploy.s.sol) + --network Specify the network (default: localhost) + --help, -h Show this help message +Examples: + yarn deploy --file DeployYourContract.s.sol --network sepolia + yarn deploy --network sepolia + yarn deploy --file DeployYourContract.s.sol + yarn deploy + `); + process.exit(0); +} + +// Parse arguments +for (let i = 0; i < args.length; i++) { + if (args[i] === "--network" && args[i + 1]) { + network = args[i + 1]; + i++; // Skip next arg since we used it + } else if (args[i] === "--file" && args[i + 1]) { + fileName = args[i + 1]; + i++; // Skip next arg since we used it + } +} + +// Check if the network exists in rpc_endpoints +try { + const foundryTomlPath = join(__dirname, "..", "foundry.toml"); + const tomlString = readFileSync(foundryTomlPath, "utf-8"); + const parsedToml = parse(tomlString); + + if (!parsedToml.rpc_endpoints[network]) { + console.log( + `\nāŒ Error: Network '${network}' not found in foundry.toml!`, + "\nPlease check `foundry.toml` for available networks in the [rpc_endpoints] section or add a new network." + ); + process.exit(1); + } +} catch (error) { + console.error("\nāŒ Error reading or parsing foundry.toml:", error); + process.exit(1); +} + +// Check for default account on live network +if ( + process.env.ETH_KEYSTORE_ACCOUNT === "scaffold-eth-default" && + network !== "localhost" +) { + console.log(` +āŒ Error: Cannot deploy to live network using default keystore account! + +To deploy to ${network}, please follow these steps: + +1. If you haven't generated a keystore account yet: + $ yarn generate + +2. Update your .env file: + ETH_KEYSTORE_ACCOUNT='scaffold-eth-custom' + +The default account (scaffold-eth-default) can only be used for localhost deployments. +`); + process.exit(0); +} + +if ( + process.env.ETH_KEYSTORE_ACCOUNT !== "scaffold-eth-default" && + network === "localhost" +) { + console.log(` +āš ļø Warning: Using ${process.env.ETH_KEYSTORE_ACCOUNT} keystore account on localhost. + +You can either: +1. Enter the password for ${process.env.ETH_KEYSTORE_ACCOUNT} account + OR +2. Set the default keystore account in your .env and re-run the command to skip password prompt: + ETH_KEYSTORE_ACCOUNT='scaffold-eth-default' +`); +} + +// Set environment variables for the make command +process.env.DEPLOY_SCRIPT = `script/${fileName}`; +process.env.RPC_URL = network; + +const result = spawnSync( + "make", + [ + "build-and-deploy", + `DEPLOY_SCRIPT=${process.env.DEPLOY_SCRIPT}`, + `RPC_URL=${process.env.RPC_URL}`, + ], + { + stdio: "inherit", + shell: true, + } +); + +process.exit(result.status); diff --git a/templates/solidity-frameworks/hardhat/package.json b/templates/solidity-frameworks/hardhat/package.json index 6008de639..1c06bbb22 100644 --- a/templates/solidity-frameworks/hardhat/package.json +++ b/templates/solidity-frameworks/hardhat/package.json @@ -1,21 +1,30 @@ { "scripts": { - "account": "yarn workspace @se-2/hardhat account", + "account": "yarn hardhat:account", "account:import": "yarn workspace @se-2/hardhat account:import", "account:generate": "yarn workspace @se-2/hardhat account:generate", - "chain": "yarn workspace @se-2/hardhat chain", - "fork": "yarn workspace @se-2/hardhat fork", - "deploy": "yarn workspace @se-2/hardhat deploy", - "verify": "yarn workspace @se-2/hardhat verify", - "hardhat-verify": "yarn workspace @se-2/hardhat hardhat-verify", - "compile": "yarn workspace @se-2/hardhat compile", + "chain": "yarn hardhat:chain", + "compile": "yarn hardhat:compile", + "deploy": "yarn hardhat:deploy", + "fork": "yarn hardhat:fork", + "format": "yarn next:format && yarn hardhat:format", "generate": "yarn account:generate", + "hardhat:account": "yarn workspace @se-2/hardhat account", + "hardhat:chain": "yarn workspace @se-2/hardhat chain", + "hardhat:check-types": "yarn workspace @se-2/hardhat check-types", + "hardhat:compile": "yarn workspace @se-2/hardhat compile", + "hardhat:deploy": "yarn workspace @se-2/hardhat deploy", + "hardhat:flatten": "yarn workspace @se-2/hardhat flatten", + "hardhat:fork": "yarn workspace @se-2/hardhat fork", + "hardhat:format": "yarn workspace @se-2/hardhat format", + "hardhat:generate": "yarn workspace @se-2/hardhat generate", + "hardhat:hardhat-verify": "yarn workspace @se-2/hardhat hardhat-verify", "hardhat:lint": "yarn workspace @se-2/hardhat lint", "hardhat:lint-staged": "yarn workspace @se-2/hardhat lint-staged", - "hardhat:format": "yarn workspace @se-2/hardhat format", "hardhat:test": "yarn workspace @se-2/hardhat test", + "hardhat:verify": "yarn workspace @se-2/hardhat verify", + "lint": "yarn nextjs:lint && yarn hardhat:lint", "test": "yarn hardhat:test", - "flatten": "yarn workspace @se-2/hardhat flatten", - "format": "yarn next:format && yarn hardhat:format" + "verify": "yarn hardhat:verify" } } diff --git a/templates/solidity-frameworks/hardhat/packages/hardhat/package.json b/templates/solidity-frameworks/hardhat/packages/hardhat/package.json index 7d4faf652..59cf1d736 100644 --- a/templates/solidity-frameworks/hardhat/packages/hardhat/package.json +++ b/templates/solidity-frameworks/hardhat/packages/hardhat/package.json @@ -3,21 +3,21 @@ "version": "0.0.1", "scripts": { "account": "hardhat run scripts/listAccount.ts", - "account:import": "hardhat run scripts/importAccount.ts", "account:generate": "hardhat run scripts/generateAccount.ts", + "account:import": "hardhat run scripts/importAccount.ts", "chain": "hardhat node --network hardhat --no-deploy", "check-types": "tsc --noEmit --incremental", "compile": "hardhat compile", "deploy": "ts-node scripts/runHardhatDeployWithPK.ts", + "flatten": "hardhat flatten", "fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy", + "format": "prettier --write './**/*.(ts|sol)'", "generate": "yarn account:generate", - "flatten": "hardhat flatten", + "hardhat-verify": "hardhat verify", "lint": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts", "lint-staged": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore", - "format": "prettier --write './**/*.(ts|sol)'", "test": "REPORT_GAS=true hardhat test --network hardhat", - "verify": "hardhat etherscan-verify", - "hardhat-verify": "hardhat verify" + "verify": "hardhat etherscan-verify" }, "devDependencies": { "@ethersproject/abi": "~5.7.0",