create-eth
Version:
Create a Scaffold-ETH-2 app
182 lines (163 loc) • 6.36 kB
text/typescript
import { useEffect, useState } from "react";
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,
ContractName,
UseScaffoldEventHistoryConfig,
UseScaffoldEventHistoryData,
} from "~~/utils/scaffold-eth/contract";
const getEvents = async (
getLogsParams: GetLogsParameters<AbiEvent | undefined, AbiEvent[] | undefined, boolean, BlockNumber, BlockNumber>,
publicClient?: UsePublicClientReturnType<Config, number>,
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
* @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)
* @param config.receiptData - if set to true it will return the receipt data for each event (default: false)
* @param config.watch - if set to true, the events will be updated every pollingInterval milliseconds set at scaffoldConfig (default: false)
* @param config.enabled - set this to false to disable the hook from running (default: true)
*/
export const useScaffoldEventHistory = <
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
TBlockData extends boolean = false,
TTransactionData extends boolean = false,
TReceiptData extends boolean = false,
>({
contractName,
eventName,
fromBlock,
chainId,
filters,
blockData,
transactionData,
receiptData,
watch,
enabled = true,
}: UseScaffoldEventHistoryConfig<TContractName, TEventName, TBlockData, TTransactionData, TReceiptData>) => {
const selectedNetwork = useSelectedNetwork(chainId);
const publicClient = usePublicClient({
chainId: selectedNetwork.id,
});
const [isFirstRender, setIsFirstRender] = useState(true);
const { data: blockNumber } = useBlockNumber({ watch: watch, chainId: selectedNetwork.id });
const { data: deployedContractData } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork.id as AllowedChainIds,
});
const event =
deployedContractData &&
((deployedContractData.abi as Abi).find(part => part.type === "event" && part.name === eventName) as AbiEvent);
const isContractAddressAndClientReady = Boolean(deployedContractData?.address) && Boolean(publicClient);
const query = useInfiniteQuery({
queryKey: [
"eventHistory",
{
contractName,
address: deployedContractData?.address,
eventName,
fromBlock: fromBlock.toString(),
chainId: selectedNetwork.id,
filters: JSON.stringify(filters, replacer),
},
],
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;
},
enabled: enabled && isContractAddressAndClientReady,
initialPageParam: fromBlock,
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() as unknown as UseScaffoldEventHistoryData<
TContractName,
TEventName,
TBlockData,
TTransactionData,
TReceiptData
>;
return {
pages: events?.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: query.data?.pages,
status: query.status,
error: query.error,
isLoading: query.isLoading,
isFetchingNewEvent: query.isFetchingNextPage,
refetch: query.refetch,
};
};