Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.5
This commit is contained in:
292
packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts
Normal file
292
packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { Abi, AbiEvent, ExtractAbiEventNames } from "abitype";
|
||||
import { BlockNumber, GetLogsParameters } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
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,
|
||||
toBlock: getLogsParams.toBlock,
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated **Recommended only for local (hardhat/anvil) chains and development.**
|
||||
* It uses getLogs which can overload RPC endpoints (especially on L2s with short block times).
|
||||
* For production, use an indexer such as ponder.sh or similar to query contract events efficiently.
|
||||
*
|
||||
* 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 - optional block number to start reading events from (defaults to `deployedOnBlock` in deployedContracts.ts if set for contract, otherwise defaults to 0)
|
||||
* @param config.toBlock - optional block number to stop reading events at (if not provided, reads until current block)
|
||||
* @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)
|
||||
* @param config.blocksBatchSize - optional batch size for fetching events. If specified, each batch will contain at most this many blocks (default: 500)
|
||||
*/
|
||||
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,
|
||||
toBlock,
|
||||
chainId,
|
||||
filters,
|
||||
blockData,
|
||||
transactionData,
|
||||
receiptData,
|
||||
watch,
|
||||
enabled = true,
|
||||
blocksBatchSize = 500,
|
||||
}: UseScaffoldEventHistoryConfig<TContractName, TEventName, TBlockData, TTransactionData, TReceiptData>) => {
|
||||
const selectedNetwork = useSelectedNetwork(chainId);
|
||||
|
||||
// Runtime warning for non-local chains
|
||||
useEffect(() => {
|
||||
if (selectedNetwork.id !== hardhat.id) {
|
||||
console.log(
|
||||
"⚠️ useScaffoldEventHistory is not optimized for production use. It can overload RPC endpoints (especially on L2s)",
|
||||
);
|
||||
}
|
||||
}, [selectedNetwork.id]);
|
||||
|
||||
const publicClient = usePublicClient({
|
||||
chainId: selectedNetwork.id,
|
||||
});
|
||||
const [liveEvents, setLiveEvents] = useState<any[]>([]);
|
||||
const [lastFetchedBlock, setLastFetchedBlock] = useState<bigint | null>(null);
|
||||
const [isPollingActive, setIsPollingActive] = useState(false);
|
||||
|
||||
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 fromBlockValue =
|
||||
fromBlock !== undefined
|
||||
? fromBlock
|
||||
: BigInt(
|
||||
deployedContractData && "deployedOnBlock" in deployedContractData
|
||||
? deployedContractData.deployedOnBlock || 0
|
||||
: 0,
|
||||
);
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: [
|
||||
"eventHistory",
|
||||
{
|
||||
contractName,
|
||||
address: deployedContractData?.address,
|
||||
eventName,
|
||||
fromBlock: fromBlockValue?.toString(),
|
||||
toBlock: toBlock?.toString(),
|
||||
chainId: selectedNetwork.id,
|
||||
filters: JSON.stringify(filters, replacer),
|
||||
blocksBatchSize: blocksBatchSize.toString(),
|
||||
},
|
||||
],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!isContractAddressAndClientReady) return undefined;
|
||||
|
||||
// Calculate the toBlock for this batch
|
||||
let batchToBlock = toBlock;
|
||||
const batchEndBlock = pageParam + BigInt(blocksBatchSize) - 1n;
|
||||
const maxBlock = toBlock || (blockNumber ? BigInt(blockNumber) : undefined);
|
||||
if (maxBlock) {
|
||||
batchToBlock = batchEndBlock < maxBlock ? batchEndBlock : maxBlock;
|
||||
}
|
||||
|
||||
const data = await getEvents(
|
||||
{
|
||||
address: deployedContractData?.address,
|
||||
event,
|
||||
fromBlock: pageParam,
|
||||
toBlock: batchToBlock,
|
||||
args: filters,
|
||||
},
|
||||
publicClient,
|
||||
{ blockData, transactionData, receiptData },
|
||||
);
|
||||
|
||||
setLastFetchedBlock(batchToBlock || blockNumber || 0n);
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: enabled && isContractAddressAndClientReady && !isPollingActive, // Disable when polling starts
|
||||
initialPageParam: fromBlockValue,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam) => {
|
||||
if (!blockNumber || fromBlockValue >= blockNumber) return undefined;
|
||||
|
||||
const nextBlock = lastPageParam + BigInt(blocksBatchSize);
|
||||
|
||||
// Don't go beyond the specified toBlock or current block
|
||||
const maxBlock = toBlock && toBlock < blockNumber ? toBlock : blockNumber;
|
||||
|
||||
if (nextBlock > maxBlock) 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Check if we're caught up and should start polling
|
||||
const shouldStartPolling = () => {
|
||||
if (!watch || !blockNumber || isPollingActive) return false;
|
||||
|
||||
return !query.hasNextPage && query.status === "success";
|
||||
};
|
||||
|
||||
// Poll for new events when watch mode is enabled
|
||||
useQuery({
|
||||
queryKey: ["liveEvents", contractName, eventName, blockNumber?.toString(), lastFetchedBlock?.toString()],
|
||||
enabled: Boolean(
|
||||
watch && enabled && isContractAddressAndClientReady && blockNumber && (shouldStartPolling() || isPollingActive),
|
||||
),
|
||||
queryFn: async () => {
|
||||
if (!isContractAddressAndClientReady || !blockNumber) return null;
|
||||
|
||||
if (!isPollingActive && shouldStartPolling()) {
|
||||
setIsPollingActive(true);
|
||||
}
|
||||
|
||||
const maxBlock = toBlock && toBlock < blockNumber ? toBlock : blockNumber;
|
||||
const startBlock = lastFetchedBlock || maxBlock;
|
||||
|
||||
// Only fetch if there are new blocks to check
|
||||
if (startBlock >= maxBlock) return null;
|
||||
|
||||
const newEvents = await getEvents(
|
||||
{
|
||||
address: deployedContractData?.address,
|
||||
event,
|
||||
fromBlock: startBlock + 1n,
|
||||
toBlock: maxBlock,
|
||||
args: filters,
|
||||
},
|
||||
publicClient,
|
||||
{ blockData, transactionData, receiptData },
|
||||
);
|
||||
|
||||
if (newEvents && newEvents.length > 0) {
|
||||
setLiveEvents(prev => [...newEvents, ...prev]);
|
||||
}
|
||||
|
||||
setLastFetchedBlock(maxBlock);
|
||||
return newEvents;
|
||||
},
|
||||
refetchInterval: false,
|
||||
});
|
||||
|
||||
// Manual trigger to fetch next page when previous page completes (only when not polling)
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isPollingActive &&
|
||||
query.status === "success" &&
|
||||
query.hasNextPage &&
|
||||
!query.isFetchingNextPage &&
|
||||
!query.error
|
||||
) {
|
||||
query.fetchNextPage();
|
||||
}
|
||||
}, [query, isPollingActive]);
|
||||
|
||||
// Combine historical data from infinite query with live events from watch hook
|
||||
const historicalEvents = query.data?.pages || [];
|
||||
const allEvents = [...liveEvents, ...historicalEvents] as typeof historicalEvents;
|
||||
|
||||
// remove duplicates
|
||||
const seenEvents = new Set<string>();
|
||||
const combinedEvents = allEvents.filter(event => {
|
||||
const eventKey = `${event?.transactionHash}-${event?.logIndex}-${event?.blockHash}`;
|
||||
if (seenEvents.has(eventKey)) {
|
||||
return false;
|
||||
}
|
||||
seenEvents.add(eventKey);
|
||||
return true;
|
||||
}) as typeof historicalEvents;
|
||||
|
||||
return {
|
||||
data: combinedEvents,
|
||||
status: query.status,
|
||||
error: query.error,
|
||||
isLoading: query.isLoading,
|
||||
isFetchingNewEvent: query.isFetchingNextPage,
|
||||
refetch: query.refetch,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user