diff --git a/templates/base/packages/nextjs/components/SwitchTheme.tsx b/templates/base/packages/nextjs/components/SwitchTheme.tsx
index 6e49638e8..431e1847f 100644
--- a/templates/base/packages/nextjs/components/SwitchTheme.tsx
+++ b/templates/base/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 */}
diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts
index 27775d5be..60dd837dd 100644
--- a/templates/base/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts
+++ b/templates/base/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;
diff --git a/templates/base/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts b/templates/base/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts
index dd7c8a200..3420550da 100644
--- a/templates/base/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts
+++ b/templates/base/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,
};