From a78be8761e1d039218cccf57d60a4de56261d859 Mon Sep 17 00:00:00 2001 From: "Shiv Bhonde | shivbhonde.eth" Date: Tue, 25 Jun 2024 20:03:35 +0530 Subject: [PATCH] rewrite useScaffoldEventHistory hook (#869) --- .../scaffold-eth/useScaffoldEventHistory.ts | 220 ++++++++---------- 1 file changed, 102 insertions(+), 118 deletions(-) diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts index 869bb5c11..48f8ed671 100644 --- a/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts @@ -1,13 +1,10 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { useTargetNetwork } from "./useTargetNetwork"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { Abi, AbiEvent, ExtractAbiEventNames } from "abitype"; -import { useInterval } from "usehooks-ts"; -import { Hash } from "viem"; -import * as chains from "viem/chains"; -import { usePublicClient } from "wagmi"; +import { BlockNumber, GetLogsParameters } from "viem"; +import { Config, UsePublicClientReturnType, useBlockNumber, usePublicClient } from "wagmi"; import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; -import scaffoldConfig from "~~/scaffold.config"; -import { replacer } from "~~/utils/scaffold-eth/common"; import { ContractAbi, ContractName, @@ -15,6 +12,44 @@ import { UseScaffoldEventHistoryData, } from "~~/utils/scaffold-eth/contract"; +const getEvents = async ( + getLogsParams: GetLogsParameters, + publicClient?: UsePublicClientReturnType, + Options?: { + blockData?: boolean; + transactionData?: boolean; + receiptData?: boolean; + }, +) => { + const logs = await publicClient?.getLogs({ + address: getLogsParams.address, + fromBlock: getLogsParams.fromBlock, + args: getLogsParams.args, + event: getLogsParams.event, + }); + if (!logs) return undefined; + + const finalEvents = await Promise.all( + logs.map(async log => { + return { + ...log, + blockData: + Options?.blockData && log.blockHash ? await publicClient?.getBlock({ blockHash: log.blockHash }) : null, + transactionData: + Options?.transactionData && log.transactionHash + ? await publicClient?.getTransaction({ hash: log.transactionHash }) + : null, + receiptData: + Options?.receiptData && log.transactionHash + ? await publicClient?.getTransactionReceipt({ hash: log.transactionHash }) + : null, + }; + }), + ); + + return finalEvents; +}; + /** * Reads events from a deployed contract * @param config - The config settings @@ -45,135 +80,84 @@ export const useScaffoldEventHistory = < watch, enabled = true, }: UseScaffoldEventHistoryConfig) => { - const [events, setEvents] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(); - const [fromBlockUpdated, setFromBlockUpdated] = useState(fromBlock); - - const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); const { targetNetwork } = useTargetNetwork(); const publicClient = usePublicClient({ chainId: targetNetwork.id, }); + const [isFirstRender, setIsFirstRender] = useState(true); - const readEvents = useCallback( - async () => { - setIsLoading(true); - try { - if (!deployedContractData) { - throw new Error("Contract not found"); - } - - if (!enabled) { - throw new Error("Hook disabled"); - } + const { data: blockNumber } = useBlockNumber({ watch: watch, chainId: targetNetwork.id }); - if (!publicClient) { - throw new Error("Public client not found"); - } + const { data: deployedContractData } = useDeployedContractInfo(contractName); - const event = (deployedContractData.abi as Abi).find( - part => part.type === "event" && part.name === eventName, - ) as AbiEvent; + const event = + deployedContractData && + ((deployedContractData.abi as Abi).find(part => part.type === "event" && part.name === eventName) as AbiEvent); - const blockNumber = await publicClient.getBlockNumber({ cacheTime: 0 }); + const isContractAddressAndClientReady = Boolean(deployedContractData?.address) && Boolean(publicClient); - if (blockNumber >= fromBlockUpdated) { - const logs = await publicClient.getLogs({ - address: deployedContractData?.address, - event, - args: filters as any, - fromBlock: fromBlockUpdated, - toBlock: blockNumber, - }); - setFromBlockUpdated(blockNumber + 1n); - - const newEvents = []; - for (let i = logs.length - 1; i >= 0; i--) { - newEvents.push({ - log: logs[i], - 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, ...events]); - setError(undefined); - } - } catch (e: any) { - if (events.length > 0) { - setEvents([]); - } - setError(e); - console.error(e); - } finally { - setIsLoading(false); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - blockData, - deployedContractData, - enabled, - eventName, - events, - // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify(filters, replacer), - fromBlockUpdated, - publicClient, - receiptData, - transactionData, + const query = useInfiniteQuery({ + queryKey: [ + "eventHistory", + { + contractName, + address: deployedContractData?.address, + eventName, + fromBlock: fromBlock.toString(), + chainId: targetNetwork.id, + }, ], - ); - - useEffect(() => { - if (!deployedContractLoading) { - readEvents(); - } - }, [readEvents, deployedContractLoading]); - - useEffect(() => { - // Reset the internal state when target network or fromBlock changed - setEvents([]); - setFromBlockUpdated(fromBlock); - setError(undefined); - }, [fromBlock, targetNetwork.id]); - - useInterval( - async () => { - if (!deployedContractLoading) { - readEvents(); - } + queryFn: async ({ pageParam }) => { + if (!isContractAddressAndClientReady) return undefined; + const data = await getEvents( + { address: deployedContractData?.address, event, fromBlock: pageParam, args: filters }, + publicClient, + { blockData, transactionData, receiptData }, + ); + + return data; }, - watch && enabled ? (targetNetwork.id !== chains.hardhat.id ? scaffoldConfig.pollingInterval : 4_000) : null, - ); - - const eventHistoryData = useMemo( - () => - events?.map(addIndexedArgsToEvent) as UseScaffoldEventHistoryData< + enabled: enabled && isContractAddressAndClientReady, + initialPageParam: fromBlock, + getNextPageParam: () => { + return blockNumber; + }, + select: data => { + const events = data.pages.flat(); + const eventHistoryData = events?.map(addIndexedArgsToEvent) as UseScaffoldEventHistoryData< TContractName, TEventName, TBlockData, TTransactionData, TReceiptData - >, - [events], - ); + >; + return { + pages: eventHistoryData?.reverse(), + pageParams: data.pageParams, + }; + }, + }); + + useEffect(() => { + const shouldSkipEffect = !blockNumber || !watch || isFirstRender; + if (shouldSkipEffect) { + // skipping on first render, since on first render we should call queryFn with + // fromBlock value, not blockNumber + if (isFirstRender) setIsFirstRender(false); + return; + } + + query.fetchNextPage(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [blockNumber, watch]); return { - data: eventHistoryData, - isLoading: isLoading, - error: error, + data: query.data?.pages, + status: query.status, + error: query.error, + isLoading: query.isLoading, + isFetchingNewEvent: query.isFetchingNextPage, + refetch: query.refetch, }; };