Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.5

This commit is contained in:
han
2026-01-10 18:17:37 +07:00
commit 98751c5b87
165 changed files with 29073 additions and 0 deletions

View 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,
};
};