From 2a32e1a77bfc8a73b9c1620cffb1c1be895fe18b Mon Sep 17 00:00:00 2001 From: Jacob Homanics Date: Mon, 16 Dec 2024 05:50:24 -0600 Subject: [PATCH] Optional chainId config in Scaffold hooks (#931) --- .../debug/_components/contract/ContractUI.tsx | 2 +- packages/nextjs/hooks/scaffold-eth/index.ts | 1 + .../scaffold-eth/useDeployedContractInfo.ts | 22 +++++++--- .../hooks/scaffold-eth/useNetworkColor.ts | 9 ++-- .../hooks/scaffold-eth/useScaffoldContract.ts | 16 ++++++-- .../scaffold-eth/useScaffoldEventHistory.ts | 19 ++++++--- .../scaffold-eth/useScaffoldReadContract.ts | 17 +++++--- .../useScaffoldWatchContractEvent.ts | 14 +++++-- .../scaffold-eth/useScaffoldWriteContract.ts | 41 +++++++++++-------- .../hooks/scaffold-eth/useSelectedNetwork.ts | 8 ++++ .../nextjs/utils/scaffold-eth/contract.ts | 17 +++++++- .../nextjs/utils/scaffold-eth/networks.ts | 1 + 12 files changed, 120 insertions(+), 47 deletions(-) create mode 100644 packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts diff --git a/packages/nextjs/app/debug/_components/contract/ContractUI.tsx b/packages/nextjs/app/debug/_components/contract/ContractUI.tsx index 8fcec9d38..4d770ec28 100644 --- a/packages/nextjs/app/debug/_components/contract/ContractUI.tsx +++ b/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/packages/nextjs/hooks/scaffold-eth/index.ts b/packages/nextjs/hooks/scaffold-eth/index.ts index 084ccc852..05450b260 100644 --- a/packages/nextjs/hooks/scaffold-eth/index.ts +++ b/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/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts b/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts index 8f649c38a..0973af21d 100644 --- a/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts +++ b/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts @@ -1,19 +1,29 @@ 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"; /** * 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 const useDeployedContractInfo = ({ + contractName, + chainId, +}: UseDeployedContractConfig) => { const isMounted = useIsMounted(); - const { targetNetwork } = useTargetNetwork(); - const deployedContract = contracts?.[targetNetwork.id]?.[contractName as ContractName] as Contract; + + 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 () => { diff --git a/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts b/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts index ec9c77060..96d1958a1 100644 --- a/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts +++ b/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/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts index 1add8cba8..d814b230c 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts +++ b/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/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts index 73c8e98e5..80914f5dc 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts +++ b/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), }, ], diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts index 9d9e8f031..82dfe3dc9 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts +++ b/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/packages/nextjs/hooks/scaffold-eth/useScaffoldWatchContractEvent.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldWatchContractEvent.ts index 844b4a08b..26878a5ce 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldWatchContractEvent.ts +++ b/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/packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts index 4bd903f7f..f8454394b 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts @@ -1,37 +1,44 @@ import { useState } from "react"; -import { useTargetNetwork } from "./useTargetNetwork"; import { MutateOptions } from "@tanstack/react-query"; import { Abi, ExtractAbiFunctionNames } from "abitype"; -import { Config, UseWriteContractParameters, useAccount, useWriteContract } from "wagmi"; +import { Config, 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"; /** * 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, - writeContractParams?: UseWriteContractParameters, -) => { - const { chain } = useAccount(); +export const useScaffoldWriteContract = ({ + contractName, + chainId, + writeContractParams, +}: UseScaffoldWriteConfig) => { + const { chain: accountChain } = useAccount(); const writeTx = useTransactor(); const [isMining, setIsMining] = useState(false); - const { targetNetwork } = useTargetNetwork(); const wagmiContractWrite = useWriteContract(writeContractParams); - const { data: deployedContractData } = useDeployedContractInfo(contractName); + const selectedNetwork = useSelectedNetwork(chainId); + + const { data: deployedContractData } = useDeployedContractInfo({ + contractName, + chainId: selectedNetwork.id as AllowedChainIds, + }); const sendContractWriteAsyncTx = async < TFunctionName extends ExtractAbiFunctionNames, "nonpayable" | "payable">, @@ -44,12 +51,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 +101,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; } diff --git a/packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts b/packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts new file mode 100644 index 000000000..93832ef4a --- /dev/null +++ b/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/packages/nextjs/utils/scaffold-eth/contract.ts b/packages/nextjs/utils/scaffold-eth/contract.ts index e405cf480..70d1d4c32 100644 --- a/packages/nextjs/utils/scaffold-eth/contract.ts +++ b/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; diff --git a/packages/nextjs/utils/scaffold-eth/networks.ts b/packages/nextjs/utils/scaffold-eth/networks.ts index 96b779866..2a5802050 100644 --- a/packages/nextjs/utils/scaffold-eth/networks.ts +++ b/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 = {