From ca1de5b4b31b0df6774ceaf7d52bec0ccd3de3ad Mon Sep 17 00:00:00 2001 From: Damian Martinelli Date: Wed, 17 Jun 2026 11:27:33 -0300 Subject: [PATCH 1/4] Fix duplicated logs (#1300) --- .../hooks/scaffold-eth/useContractLogs.ts | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts b/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts index 27775d5be3..60dd837dd6 100644 --- a/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts +++ b/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts @@ -9,31 +9,67 @@ export const useContractLogs = (address: Address) => { const client = usePublicClient({ chainId: targetNetwork.id }); useEffect(() => { + setLogs([]); + let unwatch: (() => void) | undefined; + let lastFetchedBlock = 0n; + // Set to true on effect cleanup. Guards against in-flight fetchLogs completing after + // unmount or re-run (e.g. React Strict Mode, address/client change) and registering + // a duplicate watcher or calling setLogs with stale data. + let ignore = false; + const fetchLogs = async () => { if (!client) return console.error("Client not found"); try { + const latestBlock = await client.getBlockNumber(); + if (ignore) return; + const existingLogs = await client.getLogs({ address: address, fromBlock: 0n, - toBlock: "latest", + toBlock: latestBlock, }); + if (ignore) return; + + lastFetchedBlock = latestBlock; setLogs(existingLogs); + + unwatch = client.watchBlockNumber({ + onBlockNumber: async blockNumber => { + const fromBlock = lastFetchedBlock + 1n; + if (fromBlock > blockNumber) return; + + // Advance cursor before await so concurrent block callbacks don't overlap + lastFetchedBlock = blockNumber; + + try { + const newLogs = await client.getLogs({ + address: address, + fromBlock: fromBlock, + toBlock: blockNumber, + }); + if (ignore) return; + + if (newLogs.length > 0) { + setLogs(prevLogs => [...prevLogs, ...newLogs]); + } + } catch (error) { + // Roll back the cursor so the next block callback retries this range + lastFetchedBlock = fromBlock - 1n; + console.error("Failed to fetch logs:", error); + } + }, + }); } catch (error) { console.error("Failed to fetch logs:", error); } }; + fetchLogs(); - return client?.watchBlockNumber({ - onBlockNumber: async (_blockNumber, prevBlockNumber) => { - const newLogs = await client.getLogs({ - address: address, - fromBlock: prevBlockNumber, - toBlock: "latest", - }); - setLogs(prevLogs => [...prevLogs, ...newLogs]); - }, - }); + return () => { + ignore = true; + unwatch?.(); + }; }, [address, client]); return logs; From 491178ce395a46ecc9d6187363653e6b1d98b8c0 Mon Sep 17 00:00:00 2001 From: Rinat Date: Tue, 30 Jun 2026 14:41:10 +0400 Subject: [PATCH 2/4] New ui tweaks (#1304) --- packages/nextjs/components/Header.tsx | 4 ++-- packages/nextjs/components/SwitchTheme.tsx | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 0a3bd7baa2..2b2889fbbf 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -41,7 +41,7 @@ export const HeaderMenuLinks = () => { passHref className={`${ isActive ? "bg-base-300" : "" - } hover:bg-base-300 focus:!bg-base-300 h-full px-4 text-sm gap-2 flex items-center`} + } hover:bg-base-300 focus:!bg-base-300 h-full px-4 text-sm gap-2 flex items-center whitespace-nowrap`} > {icon} {label} @@ -67,7 +67,7 @@ export const Header = () => { return (
-
+
diff --git a/packages/nextjs/components/SwitchTheme.tsx b/packages/nextjs/components/SwitchTheme.tsx index 6e49638e85..431e1847fd 100644 --- a/packages/nextjs/components/SwitchTheme.tsx +++ b/packages/nextjs/components/SwitchTheme.tsx @@ -26,10 +26,11 @@ export const SwitchTheme = ({ className }: { className?: string }) => { return (
+ {/* [--depth:0] removes DaisyUI's glossy top highlight so the indicator is a single flat color */} From 78ed3e8f19d2b64373bc1b9a9f28b0a9de3a65f2 Mon Sep 17 00:00:00 2001 From: Damian Martinelli Date: Wed, 1 Jul 2026 02:07:02 -0300 Subject: [PATCH 3/4] Cache block explorer transaction total to avoid full-chain rescans on pagination (#1301) Co-authored-by: Rinat --- .../_components/ContractTabs.tsx | 15 +---- .../_components/PaginationButton.tsx | 8 +-- packages/nextjs/app/blockexplorer/page.tsx | 4 +- .../hooks/scaffold-eth/useFetchBlocks.ts | 64 ++++++++++++------- 4 files changed, 50 insertions(+), 41 deletions(-) diff --git a/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx b/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx index 20630fb51a..760c7ae83d 100644 --- a/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx +++ b/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx @@ -26,7 +26,7 @@ const publicClient = createPublicClient({ }); export const ContractTabs = ({ address, contractData }: PageProps) => { - const { blocks, transactionReceipts, currentPage, totalTransactions, setCurrentPage } = useFetchBlocks(); + const { blocks, transactionReceipts, currentPage, hasNextPage, setCurrentPage } = useFetchBlocks(address); const [activeTab, setActiveTab] = useState("transactions"); const [isContract, setIsContract] = useState(false); @@ -39,15 +39,6 @@ export const ContractTabs = ({ address, contractData }: PageProps) => { checkIsContract(); }, [address]); - const filteredBlocks = blocks.filter(block => - block.transactions.some(tx => { - if (typeof tx === "string") { - return false; - } - return tx.from.toLowerCase() === address.toLowerCase() || tx.to?.toLowerCase() === address.toLowerCase(); - }), - ); - return ( <> {isContract && ( @@ -84,8 +75,8 @@ export const ContractTabs = ({ address, contractData }: PageProps) => { )} {activeTab === "transactions" && (
- - + +
)} {activeTab === "code" && contractData && ( diff --git a/packages/nextjs/app/blockexplorer/_components/PaginationButton.tsx b/packages/nextjs/app/blockexplorer/_components/PaginationButton.tsx index 15a4f62a65..871c7d988a 100644 --- a/packages/nextjs/app/blockexplorer/_components/PaginationButton.tsx +++ b/packages/nextjs/app/blockexplorer/_components/PaginationButton.tsx @@ -2,15 +2,13 @@ import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline"; type PaginationButtonProps = { currentPage: number; - totalItems: number; + hasNextPage: boolean; setCurrentPage: (page: number) => void; }; -const ITEMS_PER_PAGE = 20; - -export const PaginationButton = ({ currentPage, totalItems, setCurrentPage }: PaginationButtonProps) => { +export const PaginationButton = ({ currentPage, hasNextPage, setCurrentPage }: PaginationButtonProps) => { const isPrevButtonDisabled = currentPage === 0; - const isNextButtonDisabled = currentPage + 1 >= Math.ceil(totalItems / ITEMS_PER_PAGE); + const isNextButtonDisabled = !hasNextPage; const prevButtonClass = isPrevButtonDisabled ? "btn-disabled cursor-default" : "btn-primary"; const nextButtonClass = isNextButtonDisabled ? "btn-disabled cursor-default" : "btn-primary"; diff --git a/packages/nextjs/app/blockexplorer/page.tsx b/packages/nextjs/app/blockexplorer/page.tsx index 71d1561a92..daa0701462 100644 --- a/packages/nextjs/app/blockexplorer/page.tsx +++ b/packages/nextjs/app/blockexplorer/page.tsx @@ -9,7 +9,7 @@ import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; import { notification } from "~~/utils/scaffold-eth"; const BlockExplorer: NextPage = () => { - const { blocks, transactionReceipts, currentPage, totalTransactions, setCurrentPage, error } = useFetchBlocks(); + const { blocks, transactionReceipts, currentPage, hasNextPage, setCurrentPage, error } = useFetchBlocks(); const { targetNetwork } = useTargetNetwork(); const [isLocalNetwork, setIsLocalNetwork] = useState(true); const [hasError, setHasError] = useState(false); @@ -75,7 +75,7 @@ const BlockExplorer: NextPage = () => {
- +
); }; diff --git a/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts b/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts index dd7c8a200d..3420550da2 100644 --- a/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts +++ b/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { + Address, Block, Hash, Transaction, @@ -48,14 +49,17 @@ const groupTransactionsByBlock = (items: { block: Block; tx: Transaction }[]): B return groupedBlocks; }; -const fetchTransactionsPage = async ( +// Collects the requested page plus one extra transaction (the cheap "is there a next page?" +// signal), counting only transactions that pass matchesFilter — so address views paginate over +// the address's own transactions instead of slicing a global page. +const fetchPageItems = async ( latestBlock: bigint, page: number, -): Promise<{ items: { block: Block; tx: Transaction }[]; totalTransactions: number }> => { + matchesFilter: (tx: Transaction) => boolean, +): Promise<{ block: Block; tx: Transaction }[]> => { const skipCount = page * TRANSACTIONS_PER_PAGE; const pageItems: { block: Block; tx: Transaction }[] = []; let skipped = 0; - let totalTransactions = 0; for (let blockNum = latestBlock; blockNum >= 0n; ) { const batchEnd = blockNum - BigInt(BLOCK_BATCH_SIZE - 1); @@ -72,15 +76,18 @@ const fetchTransactionsPage = async ( for (const block of fetchedBlocks) { for (const tx of block.transactions as Transaction[]) { - totalTransactions++; + if (!matchesFilter(tx)) { + continue; + } if (skipped < skipCount) { skipped++; continue; } - if (pageItems.length < TRANSACTIONS_PER_PAGE) { - pageItems.push({ block, tx }); + pageItems.push({ block, tx }); + if (pageItems.length > TRANSACTIONS_PER_PAGE) { + return pageItems; } } } @@ -88,24 +95,34 @@ const fetchTransactionsPage = async ( blockNum = batchStart - 1n; } - return { items: pageItems, totalTransactions }; + return pageItems; }; -export const useFetchBlocks = () => { +export const useFetchBlocks = (addressFilter?: Address) => { const [blocks, setBlocks] = useState([]); const [transactionReceipts, setTransactionReceipts] = useState<{ [key: string]: TransactionReceipt; }>({}); const [currentPage, setCurrentPage] = useState(0); - const [totalTransactions, setTotalTransactions] = useState(0); + const [hasNextPage, setHasNextPage] = useState(false); const [error, setError] = useState(null); + const matchesAddressFilter = useCallback( + (tx: Transaction) => { + if (!addressFilter) return true; + const filter = addressFilter.toLowerCase(); + return tx.from.toLowerCase() === filter || tx.to?.toLowerCase() === filter; + }, + [addressFilter], + ); + const fetchBlocks = useCallback(async () => { setError(null); try { const blockNumber = await testClient.getBlockNumber(); - const { items, totalTransactions: totalTxCount } = await fetchTransactionsPage(blockNumber, currentPage); + const fetchedItems = await fetchPageItems(blockNumber, currentPage, matchesAddressFilter); + const items = fetchedItems.slice(0, TRANSACTIONS_PER_PAGE); items.forEach(({ tx }) => decodeTransactionData(tx)); @@ -122,12 +139,12 @@ export const useFetchBlocks = () => { ); setBlocks(groupTransactionsByBlock(items)); - setTotalTransactions(totalTxCount); + setHasNextPage(fetchedItems.length > TRANSACTIONS_PER_PAGE); setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...txReceipts) })); } catch (err) { setError(err instanceof Error ? err : new Error("An error occurred.")); } - }, [currentPage]); + }, [currentPage, matchesAddressFilter]); useEffect(() => { fetchBlocks(); @@ -146,6 +163,12 @@ export const useFetchBlocks = () => { blockWithTxDetails = { ...newBlock, transactions: transactionsDetails }; } + const matchingTransactions = (blockWithTxDetails.transactions as Transaction[]).filter(matchesAddressFilter); + if (matchingTransactions.length === 0) { + return; + } + blockWithTxDetails = { ...blockWithTxDetails, transactions: matchingTransactions }; + (blockWithTxDetails.transactions as Transaction[]).forEach(tx => decodeTransactionData(tx)); const receipts = await Promise.all( @@ -163,26 +186,23 @@ export const useFetchBlocks = () => { setBlocks(prevBlocks => { const latestBlockNumber = blockWithTxDetails.number!; const existingBlockIndex = prevBlocks.findIndex(block => block.number === latestBlockNumber); - const existingBlockTransactionsCount = - existingBlockIndex >= 0 ? prevBlocks[existingBlockIndex].transactions.length : 0; - const latestBlockTransactionsCount = blockWithTxDetails.transactions.length; const nextBlocks = existingBlockIndex >= 0 ? prevBlocks.map((block, index) => (index === existingBlockIndex ? blockWithTxDetails : block)) : [blockWithTxDetails, ...prevBlocks]; - const transactionsDelta = latestBlockTransactionsCount - existingBlockTransactionsCount; - if (transactionsDelta !== 0) { - setTotalTransactions(prevTotal => Math.max(0, prevTotal + transactionsDelta)); - } - const trimmedBlocks = [...nextBlocks]; let transactionsInTrimmedBlocks = trimmedBlocks.reduce( (count, block) => count + block.transactions.length, 0, ); + // More than one page of transactions are now in view, so a next page exists. + if (transactionsInTrimmedBlocks > TRANSACTIONS_PER_PAGE) { + setHasNextPage(true); + } + while (transactionsInTrimmedBlocks > TRANSACTIONS_PER_PAGE && trimmedBlocks.length > 0) { const removedBlock = trimmedBlocks.pop(); if (removedBlock) { @@ -201,13 +221,13 @@ export const useFetchBlocks = () => { }; return testClient.watchBlocks({ onBlock: handleNewBlock, includeTransactions: true }); - }, [currentPage]); + }, [currentPage, matchesAddressFilter]); return { blocks, transactionReceipts, currentPage, - totalTransactions, + hasNextPage, setCurrentPage, error, }; From 8ae10565054c3082228b484c696d838810b0878b Mon Sep 17 00:00:00 2001 From: Rinat Date: Thu, 2 Jul 2026 12:33:47 +0400 Subject: [PATCH 4/4] chore: add changeset for backmerge --- .changeset/backmerge-02-07-26.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/backmerge-02-07-26.md diff --git a/.changeset/backmerge-02-07-26.md b/.changeset/backmerge-02-07-26.md new file mode 100644 index 0000000000..ef6bf1ce47 --- /dev/null +++ b/.changeset/backmerge-02-07-26.md @@ -0,0 +1,7 @@ +--- +"create-eth": patch +--- + +- fix: duplicated logs (https://github.com/scaffold-eth/scaffold-eth-2/pull/1300) +- cache block explorer transaction total to avoid full-chain rescans on pagination (https://github.com/scaffold-eth/scaffold-eth-2/pull/1301) +- UI tweaks (https://github.com/scaffold-eth/scaffold-eth-2/pull/1304)