Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.5
This commit is contained in:
17
packages/nextjs/hooks/scaffold-eth/index.ts
Normal file
17
packages/nextjs/hooks/scaffold-eth/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export * from "./useAnimationConfig";
|
||||
export * from "./useContractLogs";
|
||||
export * from "./useCopyToClipboard";
|
||||
export * from "./useDeployedContractInfo";
|
||||
export * from "./useFetchBlocks";
|
||||
export * from "./useInitializeNativeCurrencyPrice";
|
||||
export * from "./useNetworkColor";
|
||||
export * from "./useOutsideClick";
|
||||
export * from "./useScaffoldContract";
|
||||
export * from "./useScaffoldEventHistory";
|
||||
export * from "./useScaffoldReadContract";
|
||||
export * from "./useScaffoldWatchContractEvent";
|
||||
export * from "./useScaffoldWriteContract";
|
||||
export * from "./useTargetNetwork";
|
||||
export * from "./useTransactor";
|
||||
export * from "./useWatchBalance";
|
||||
export * from "./useSelectedNetwork";
|
||||
20
packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts
Normal file
20
packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const ANIMATION_TIME = 2000;
|
||||
|
||||
export function useAnimationConfig(data: any) {
|
||||
const [showAnimation, setShowAnimation] = useState(false);
|
||||
const [prevData, setPrevData] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
if (prevData !== undefined && prevData !== data) {
|
||||
setShowAnimation(true);
|
||||
setTimeout(() => setShowAnimation(false), ANIMATION_TIME);
|
||||
}
|
||||
setPrevData(data);
|
||||
}, [data, prevData]);
|
||||
|
||||
return {
|
||||
showAnimation,
|
||||
};
|
||||
}
|
||||
40
packages/nextjs/hooks/scaffold-eth/useContractLogs.ts
Normal file
40
packages/nextjs/hooks/scaffold-eth/useContractLogs.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTargetNetwork } from "./useTargetNetwork";
|
||||
import { Address, Log } from "viem";
|
||||
import { usePublicClient } from "wagmi";
|
||||
|
||||
export const useContractLogs = (address: Address) => {
|
||||
const [logs, setLogs] = useState<Log[]>([]);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const client = usePublicClient({ chainId: targetNetwork.id });
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLogs = async () => {
|
||||
if (!client) return console.error("Client not found");
|
||||
try {
|
||||
const existingLogs = await client.getLogs({
|
||||
address: address,
|
||||
fromBlock: 0n,
|
||||
toBlock: "latest",
|
||||
});
|
||||
setLogs(existingLogs);
|
||||
} 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]);
|
||||
},
|
||||
});
|
||||
}, [address, client]);
|
||||
|
||||
return logs;
|
||||
};
|
||||
19
packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts
Normal file
19
packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export const useCopyToClipboard = () => {
|
||||
const [isCopiedToClipboard, setIsCopiedToClipboard] = useState(false);
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setIsCopiedToClipboard(true);
|
||||
setTimeout(() => {
|
||||
setIsCopiedToClipboard(false);
|
||||
}, 800);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return { copyToClipboard, isCopiedToClipboard };
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIsMounted } from "usehooks-ts";
|
||||
import { usePublicClient } from "wagmi";
|
||||
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
|
||||
import {
|
||||
Contract,
|
||||
ContractCodeStatus,
|
||||
ContractName,
|
||||
UseDeployedContractConfig,
|
||||
contracts,
|
||||
} from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type DeployedContractData<TContractName extends ContractName> = {
|
||||
data: Contract<TContractName> | undefined;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the matching contract info for the provided contract name from the contracts present in deployedContracts.ts
|
||||
* and externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
|
||||
*/
|
||||
export function useDeployedContractInfo<TContractName extends ContractName>(
|
||||
config: UseDeployedContractConfig<TContractName>,
|
||||
): DeployedContractData<TContractName>;
|
||||
/**
|
||||
* @deprecated Use object parameter version instead: useDeployedContractInfo({ contractName: "YourContract" })
|
||||
*/
|
||||
export function useDeployedContractInfo<TContractName extends ContractName>(
|
||||
contractName: TContractName,
|
||||
): DeployedContractData<TContractName>;
|
||||
|
||||
export function useDeployedContractInfo<TContractName extends ContractName>(
|
||||
configOrName: UseDeployedContractConfig<TContractName> | TContractName,
|
||||
): DeployedContractData<TContractName> {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const finalConfig: UseDeployedContractConfig<TContractName> =
|
||||
typeof configOrName === "string" ? { contractName: configOrName } : (configOrName as any);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof configOrName === "string") {
|
||||
console.warn(
|
||||
"Using `useDeployedContractInfo` with a string parameter is deprecated. Please use the object parameter version instead.",
|
||||
);
|
||||
}
|
||||
}, [configOrName]);
|
||||
const { contractName, chainId } = finalConfig;
|
||||
const selectedNetwork = useSelectedNetwork(chainId);
|
||||
const deployedContract = contracts?.[selectedNetwork.id]?.[contractName as ContractName] as Contract<TContractName>;
|
||||
const [status, setStatus] = useState<ContractCodeStatus>(ContractCodeStatus.LOADING);
|
||||
const publicClient = usePublicClient({ chainId: selectedNetwork.id });
|
||||
|
||||
useEffect(() => {
|
||||
const checkContractDeployment = async () => {
|
||||
try {
|
||||
if (!isMounted() || !publicClient) return;
|
||||
|
||||
if (!deployedContract) {
|
||||
setStatus(ContractCodeStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
const code = await publicClient.getBytecode({
|
||||
address: deployedContract.address,
|
||||
});
|
||||
|
||||
// If contract code is `0x` => no contract deployed on that address
|
||||
if (code === "0x") {
|
||||
setStatus(ContractCodeStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
setStatus(ContractCodeStatus.DEPLOYED);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setStatus(ContractCodeStatus.NOT_FOUND);
|
||||
}
|
||||
};
|
||||
|
||||
checkContractDeployment();
|
||||
}, [isMounted, contractName, deployedContract, publicClient]);
|
||||
|
||||
return {
|
||||
data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined,
|
||||
isLoading: status === ContractCodeStatus.LOADING,
|
||||
};
|
||||
}
|
||||
21
packages/nextjs/hooks/scaffold-eth/useDisplayUsdMode.ts
Normal file
21
packages/nextjs/hooks/scaffold-eth/useDisplayUsdMode.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
|
||||
export const useDisplayUsdMode = ({ defaultUsdMode = false }: { defaultUsdMode?: boolean }) => {
|
||||
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
|
||||
const isPriceFetched = nativeCurrencyPrice > 0;
|
||||
const predefinedUsdMode = isPriceFetched ? Boolean(defaultUsdMode) : false;
|
||||
const [displayUsdMode, setDisplayUsdMode] = useState(predefinedUsdMode);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayUsdMode(predefinedUsdMode);
|
||||
}, [predefinedUsdMode]);
|
||||
|
||||
const toggleDisplayUsdMode = useCallback(() => {
|
||||
if (isPriceFetched) {
|
||||
setDisplayUsdMode(!displayUsdMode);
|
||||
}
|
||||
}, [displayUsdMode, isPriceFetched]);
|
||||
|
||||
return { displayUsdMode, toggleDisplayUsdMode };
|
||||
};
|
||||
133
packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts
Normal file
133
packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Block,
|
||||
Hash,
|
||||
Transaction,
|
||||
TransactionReceipt,
|
||||
createTestClient,
|
||||
publicActions,
|
||||
walletActions,
|
||||
webSocket,
|
||||
} from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { decodeTransactionData } from "~~/utils/scaffold-eth";
|
||||
|
||||
const BLOCKS_PER_PAGE = 20;
|
||||
|
||||
export const testClient = createTestClient({
|
||||
chain: hardhat,
|
||||
mode: "hardhat",
|
||||
transport: webSocket("ws://127.0.0.1:8545"),
|
||||
})
|
||||
.extend(publicActions)
|
||||
.extend(walletActions);
|
||||
|
||||
export const useFetchBlocks = () => {
|
||||
const [blocks, setBlocks] = useState<Block[]>([]);
|
||||
const [transactionReceipts, setTransactionReceipts] = useState<{
|
||||
[key: string]: TransactionReceipt;
|
||||
}>({});
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [totalBlocks, setTotalBlocks] = useState(0n);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchBlocks = useCallback(async () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const blockNumber = await testClient.getBlockNumber();
|
||||
setTotalBlocks(blockNumber);
|
||||
|
||||
const startingBlock = blockNumber - BigInt(currentPage * BLOCKS_PER_PAGE);
|
||||
const blockNumbersToFetch = Array.from(
|
||||
{ length: Number(BLOCKS_PER_PAGE < startingBlock + 1n ? BLOCKS_PER_PAGE : startingBlock + 1n) },
|
||||
(_, i) => startingBlock - BigInt(i),
|
||||
);
|
||||
|
||||
const blocksWithTransactions = blockNumbersToFetch.map(async blockNumber => {
|
||||
try {
|
||||
return testClient.getBlock({ blockNumber, includeTransactions: true });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred."));
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
const fetchedBlocks = await Promise.all(blocksWithTransactions);
|
||||
|
||||
fetchedBlocks.forEach(block => {
|
||||
block.transactions.forEach(tx => decodeTransactionData(tx as Transaction));
|
||||
});
|
||||
|
||||
const txReceipts = await Promise.all(
|
||||
fetchedBlocks.flatMap(block =>
|
||||
block.transactions.map(async tx => {
|
||||
try {
|
||||
const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
|
||||
return { [(tx as Transaction).hash]: receipt };
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred."));
|
||||
throw err;
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
setBlocks(fetchedBlocks);
|
||||
setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...txReceipts) }));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred."));
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBlocks();
|
||||
}, [fetchBlocks]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleNewBlock = async (newBlock: any) => {
|
||||
try {
|
||||
if (currentPage === 0) {
|
||||
if (newBlock.transactions.length > 0) {
|
||||
const transactionsDetails = await Promise.all(
|
||||
newBlock.transactions.map((txHash: string) => testClient.getTransaction({ hash: txHash as Hash })),
|
||||
);
|
||||
newBlock.transactions = transactionsDetails;
|
||||
}
|
||||
|
||||
newBlock.transactions.forEach((tx: Transaction) => decodeTransactionData(tx as Transaction));
|
||||
|
||||
const receipts = await Promise.all(
|
||||
newBlock.transactions.map(async (tx: Transaction) => {
|
||||
try {
|
||||
const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
|
||||
return { [(tx as Transaction).hash]: receipt };
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred fetching receipt."));
|
||||
throw err;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
setBlocks(prevBlocks => [newBlock, ...prevBlocks.slice(0, BLOCKS_PER_PAGE - 1)]);
|
||||
setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...receipts) }));
|
||||
}
|
||||
if (newBlock.number) {
|
||||
setTotalBlocks(newBlock.number);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("An error occurred."));
|
||||
}
|
||||
};
|
||||
|
||||
return testClient.watchBlocks({ onBlock: handleNewBlock, includeTransactions: true });
|
||||
}, [currentPage]);
|
||||
|
||||
return {
|
||||
blocks,
|
||||
transactionReceipts,
|
||||
currentPage,
|
||||
totalBlocks,
|
||||
setCurrentPage,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useTargetNetwork } from "./useTargetNetwork";
|
||||
import { useInterval } from "usehooks-ts";
|
||||
import scaffoldConfig from "~~/scaffold.config";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
import { fetchPriceFromUniswap } from "~~/utils/scaffold-eth";
|
||||
|
||||
const enablePolling = false;
|
||||
|
||||
/**
|
||||
* Get the price of Native Currency based on Native Token/DAI trading pair from Uniswap SDK
|
||||
*/
|
||||
export const useInitializeNativeCurrencyPrice = () => {
|
||||
const setNativeCurrencyPrice = useGlobalState(state => state.setNativeCurrencyPrice);
|
||||
const setIsNativeCurrencyFetching = useGlobalState(state => state.setIsNativeCurrencyFetching);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
const fetchPrice = useCallback(async () => {
|
||||
setIsNativeCurrencyFetching(true);
|
||||
const price = await fetchPriceFromUniswap(targetNetwork);
|
||||
setNativeCurrencyPrice(price);
|
||||
setIsNativeCurrencyFetching(false);
|
||||
}, [setIsNativeCurrencyFetching, setNativeCurrencyPrice, targetNetwork]);
|
||||
|
||||
// Get the price of ETH from Uniswap on mount
|
||||
useEffect(() => {
|
||||
fetchPrice();
|
||||
}, [fetchPrice]);
|
||||
|
||||
// Get the price of ETH from Uniswap at a given interval
|
||||
useInterval(fetchPrice, enablePolling ? scaffoldConfig.pollingInterval : null);
|
||||
};
|
||||
22
packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts
Normal file
22
packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
|
||||
import { AllowedChainIds, ChainWithAttributes } from "~~/utils/scaffold-eth";
|
||||
|
||||
export const DEFAULT_NETWORK_COLOR: [string, string] = ["#666666", "#bbbbbb"];
|
||||
|
||||
export function getNetworkColor(network: ChainWithAttributes, isDarkMode: boolean) {
|
||||
const colorConfig = network.color ?? DEFAULT_NETWORK_COLOR;
|
||||
return Array.isArray(colorConfig) ? (isDarkMode ? colorConfig[1] : colorConfig[0]) : colorConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the color of the target network
|
||||
*/
|
||||
export const useNetworkColor = (chainId?: AllowedChainIds) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const chain = useSelectedNetwork(chainId);
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
return getNetworkColor(chain, isDarkMode);
|
||||
};
|
||||
23
packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts
Normal file
23
packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Handles clicks outside of passed ref element
|
||||
* @param ref - react ref of the element
|
||||
* @param callback - callback function to call when clicked outside
|
||||
*/
|
||||
export const useOutsideClick = (ref: React.RefObject<HTMLElement | null>, callback: { (): void }) => {
|
||||
useEffect(() => {
|
||||
function handleOutsideClick(event: MouseEvent) {
|
||||
if (!(event.target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ref.current && !ref.current.contains(event.target)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleOutsideClick);
|
||||
return () => document.removeEventListener("click", handleOutsideClick);
|
||||
}, [ref, callback]);
|
||||
};
|
||||
65
packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts
Normal file
65
packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Account, Address, Chain, Client, Transport, getContract } from "viem";
|
||||
import { usePublicClient } from "wagmi";
|
||||
import { GetWalletClientReturnType } from "wagmi/actions";
|
||||
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
|
||||
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
|
||||
import { AllowedChainIds } from "~~/utils/scaffold-eth";
|
||||
import { Contract, ContractName } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
/**
|
||||
* Gets a viem instance of the contract present in deployedContracts.ts or externalContracts.ts corresponding to
|
||||
* targetNetworks configured in scaffold.config.ts. Optional walletClient can be passed for doing write transactions.
|
||||
* @param config - The config settings for the hook
|
||||
* @param config.contractName - deployed contract name
|
||||
* @param config.walletClient - optional walletClient from wagmi useWalletClient hook can be passed for doing write transactions
|
||||
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
|
||||
*/
|
||||
export const useScaffoldContract = <
|
||||
TContractName extends ContractName,
|
||||
TWalletClient extends Exclude<GetWalletClientReturnType, null> | undefined,
|
||||
>({
|
||||
contractName,
|
||||
walletClient,
|
||||
chainId,
|
||||
}: {
|
||||
contractName: TContractName;
|
||||
walletClient?: TWalletClient | null;
|
||||
chainId?: AllowedChainIds;
|
||||
}) => {
|
||||
const selectedNetwork = useSelectedNetwork(chainId);
|
||||
const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({
|
||||
contractName,
|
||||
chainId: selectedNetwork?.id as AllowedChainIds,
|
||||
});
|
||||
|
||||
const publicClient = usePublicClient({ chainId: selectedNetwork?.id });
|
||||
|
||||
let contract = undefined;
|
||||
if (deployedContractData && publicClient) {
|
||||
contract = getContract<
|
||||
Transport,
|
||||
Address,
|
||||
Contract<TContractName>["abi"],
|
||||
TWalletClient extends Exclude<GetWalletClientReturnType, null>
|
||||
? {
|
||||
public: Client<Transport, Chain>;
|
||||
wallet: TWalletClient;
|
||||
}
|
||||
: { public: Client<Transport, Chain> },
|
||||
Chain,
|
||||
Account
|
||||
>({
|
||||
address: deployedContractData.address,
|
||||
abi: deployedContractData.abi as Contract<TContractName>["abi"],
|
||||
client: {
|
||||
public: publicClient,
|
||||
wallet: walletClient ? walletClient : undefined,
|
||||
} as any,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: contract,
|
||||
isLoading: deployedContractLoading,
|
||||
};
|
||||
};
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useEffect } from "react";
|
||||
import { QueryObserverResult, RefetchOptions, useQueryClient } from "@tanstack/react-query";
|
||||
import type { ExtractAbiFunctionNames } from "abitype";
|
||||
import { ReadContractErrorType } from "viem";
|
||||
import { useBlockNumber, useReadContract } from "wagmi";
|
||||
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
|
||||
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
|
||||
import { AllowedChainIds } from "~~/utils/scaffold-eth";
|
||||
import {
|
||||
AbiFunctionReturnType,
|
||||
ContractAbi,
|
||||
ContractName,
|
||||
UseScaffoldReadConfig,
|
||||
} from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
/**
|
||||
* Wrapper around wagmi's useContractRead hook which automatically loads (by name) the contract ABI and address from
|
||||
* the contracts present in deployedContracts.ts & externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
|
||||
* @param config - The config settings, including extra wagmi configuration
|
||||
* @param config.contractName - deployed contract name
|
||||
* @param config.functionName - name of the function to be called
|
||||
* @param config.args - args to be passed to the function call
|
||||
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
|
||||
*/
|
||||
export const useScaffoldReadContract = <
|
||||
TContractName extends ContractName,
|
||||
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "pure" | "view">,
|
||||
>({
|
||||
contractName,
|
||||
functionName,
|
||||
args,
|
||||
chainId,
|
||||
...readConfig
|
||||
}: UseScaffoldReadConfig<TContractName, TFunctionName>) => {
|
||||
const selectedNetwork = useSelectedNetwork(chainId);
|
||||
const { data: deployedContract } = useDeployedContractInfo({
|
||||
contractName,
|
||||
chainId: selectedNetwork.id as AllowedChainIds,
|
||||
});
|
||||
|
||||
const { query: queryOptions, watch, ...readContractConfig } = readConfig;
|
||||
// set watch to true by default
|
||||
const defaultWatch = watch ?? true;
|
||||
|
||||
const readContractHookRes = useReadContract({
|
||||
chainId: selectedNetwork.id,
|
||||
functionName,
|
||||
address: deployedContract?.address,
|
||||
abi: deployedContract?.abi,
|
||||
args,
|
||||
...(readContractConfig as any),
|
||||
query: {
|
||||
enabled: !Array.isArray(args) || !args.some(arg => arg === undefined),
|
||||
...queryOptions,
|
||||
},
|
||||
}) as Omit<ReturnType<typeof useReadContract>, "data" | "refetch"> & {
|
||||
data: AbiFunctionReturnType<ContractAbi, TFunctionName> | undefined;
|
||||
refetch: (
|
||||
options?: RefetchOptions | undefined,
|
||||
) => Promise<QueryObserverResult<AbiFunctionReturnType<ContractAbi, TFunctionName>, ReadContractErrorType>>;
|
||||
};
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: blockNumber } = useBlockNumber({
|
||||
watch: defaultWatch,
|
||||
chainId: selectedNetwork.id,
|
||||
query: {
|
||||
enabled: defaultWatch,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultWatch) {
|
||||
queryClient.invalidateQueries({ queryKey: readContractHookRes.queryKey });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockNumber]);
|
||||
|
||||
return readContractHookRes;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Abi, ExtractAbiEventNames } from "abitype";
|
||||
import { Log } from "viem";
|
||||
import { useWatchContractEvent } from "wagmi";
|
||||
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
|
||||
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
|
||||
import { AllowedChainIds } from "~~/utils/scaffold-eth";
|
||||
import { ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
/**
|
||||
* Wrapper around wagmi's useEventSubscriber hook which automatically loads (by name) the contract ABI and
|
||||
* address from the contracts present in deployedContracts.ts & externalContracts.ts
|
||||
* @param config - The config settings
|
||||
* @param config.contractName - deployed contract name
|
||||
* @param config.eventName - name of the event to listen for
|
||||
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
|
||||
* @param config.onLogs - the callback that receives events.
|
||||
*/
|
||||
export const useScaffoldWatchContractEvent = <
|
||||
TContractName extends ContractName,
|
||||
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
|
||||
>({
|
||||
contractName,
|
||||
eventName,
|
||||
chainId,
|
||||
onLogs,
|
||||
}: UseScaffoldEventConfig<TContractName, TEventName>) => {
|
||||
const selectedNetwork = useSelectedNetwork(chainId);
|
||||
const { data: deployedContractData } = useDeployedContractInfo({
|
||||
contractName,
|
||||
chainId: selectedNetwork.id as AllowedChainIds,
|
||||
});
|
||||
|
||||
return useWatchContractEvent({
|
||||
address: deployedContractData?.address,
|
||||
abi: deployedContractData?.abi as Abi,
|
||||
chainId: selectedNetwork.id,
|
||||
onLogs: (logs: Log[]) => onLogs(logs as Parameters<typeof onLogs>[0]),
|
||||
eventName,
|
||||
});
|
||||
};
|
||||
194
packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts
Normal file
194
packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { MutateOptions } from "@tanstack/react-query";
|
||||
import { Abi, ExtractAbiFunctionNames } from "abitype";
|
||||
import { Config, UseWriteContractParameters, useAccount, useConfig, useWriteContract } from "wagmi";
|
||||
import { WriteContractErrorType, WriteContractReturnType } from "wagmi/actions";
|
||||
import { WriteContractVariables } from "wagmi/query";
|
||||
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
|
||||
import { useDeployedContractInfo, useTransactor } from "~~/hooks/scaffold-eth";
|
||||
import { AllowedChainIds, notification } from "~~/utils/scaffold-eth";
|
||||
import {
|
||||
ContractAbi,
|
||||
ContractName,
|
||||
ScaffoldWriteContractOptions,
|
||||
ScaffoldWriteContractVariables,
|
||||
UseScaffoldWriteConfig,
|
||||
simulateContractWriteAndNotifyError,
|
||||
} from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type ScaffoldWriteContractReturnType<TContractName extends ContractName> = Omit<
|
||||
ReturnType<typeof useWriteContract>,
|
||||
"writeContract" | "writeContractAsync"
|
||||
> & {
|
||||
isMining: boolean;
|
||||
writeContractAsync: <
|
||||
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">,
|
||||
>(
|
||||
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
|
||||
options?: ScaffoldWriteContractOptions,
|
||||
) => Promise<WriteContractReturnType | undefined>;
|
||||
writeContract: <TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">>(
|
||||
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
|
||||
options?: Omit<ScaffoldWriteContractOptions, "onBlockConfirmation" | "blockConfirmations">,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export function useScaffoldWriteContract<TContractName extends ContractName>(
|
||||
config: UseScaffoldWriteConfig<TContractName>,
|
||||
): ScaffoldWriteContractReturnType<TContractName>;
|
||||
/**
|
||||
* @deprecated Use object parameter version instead: useScaffoldWriteContract({ contractName: "YourContract" })
|
||||
*/
|
||||
export function useScaffoldWriteContract<TContractName extends ContractName>(
|
||||
contractName: TContractName,
|
||||
writeContractParams?: UseWriteContractParameters,
|
||||
): ScaffoldWriteContractReturnType<TContractName>;
|
||||
|
||||
/**
|
||||
* Wrapper around wagmi's useWriteContract hook which automatically loads (by name) the contract ABI and address from
|
||||
* the contracts present in deployedContracts.ts & externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
|
||||
* @param contractName - name of the contract to be written to
|
||||
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
|
||||
* @param writeContractParams - wagmi's useWriteContract parameters
|
||||
*/
|
||||
export function useScaffoldWriteContract<TContractName extends ContractName>(
|
||||
configOrName: UseScaffoldWriteConfig<TContractName> | TContractName,
|
||||
writeContractParams?: UseWriteContractParameters,
|
||||
): ScaffoldWriteContractReturnType<TContractName> {
|
||||
const finalConfig =
|
||||
typeof configOrName === "string"
|
||||
? { contractName: configOrName, writeContractParams, chainId: undefined }
|
||||
: (configOrName as UseScaffoldWriteConfig<TContractName>);
|
||||
const { contractName, chainId, writeContractParams: finalWriteContractParams } = finalConfig;
|
||||
|
||||
const wagmiConfig = useConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof configOrName === "string") {
|
||||
console.warn(
|
||||
"Using `useScaffoldWriteContract` with a string parameter is deprecated. Please use the object parameter version instead.",
|
||||
);
|
||||
}
|
||||
}, [configOrName]);
|
||||
|
||||
const { chain: accountChain } = useAccount();
|
||||
const writeTx = useTransactor();
|
||||
const [isMining, setIsMining] = useState(false);
|
||||
|
||||
const wagmiContractWrite = useWriteContract(finalWriteContractParams);
|
||||
|
||||
const selectedNetwork = useSelectedNetwork(chainId);
|
||||
|
||||
const { data: deployedContractData } = useDeployedContractInfo({
|
||||
contractName,
|
||||
chainId: selectedNetwork.id as AllowedChainIds,
|
||||
});
|
||||
|
||||
const sendContractWriteAsyncTx = async <
|
||||
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">,
|
||||
>(
|
||||
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
|
||||
options?: ScaffoldWriteContractOptions,
|
||||
) => {
|
||||
if (!deployedContractData) {
|
||||
notification.error("Target Contract is not deployed, did you forget to run `yarn deploy`?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accountChain?.id) {
|
||||
notification.error("Please connect your wallet");
|
||||
return;
|
||||
}
|
||||
|
||||
if (accountChain?.id !== selectedNetwork.id) {
|
||||
notification.error(`Wallet is connected to the wrong network. Please switch to ${selectedNetwork.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsMining(true);
|
||||
const { blockConfirmations, onBlockConfirmation, ...mutateOptions } = options || {};
|
||||
|
||||
const writeContractObject = {
|
||||
abi: deployedContractData.abi as Abi,
|
||||
address: deployedContractData.address,
|
||||
...variables,
|
||||
} as WriteContractVariables<Abi, string, any[], Config, number>;
|
||||
|
||||
if (!finalConfig?.disableSimulate) {
|
||||
await simulateContractWriteAndNotifyError({
|
||||
wagmiConfig,
|
||||
writeContractParams: writeContractObject,
|
||||
chainId: selectedNetwork.id as AllowedChainIds,
|
||||
});
|
||||
}
|
||||
|
||||
const makeWriteWithParams = () =>
|
||||
wagmiContractWrite.writeContractAsync(
|
||||
writeContractObject,
|
||||
mutateOptions as
|
||||
| MutateOptions<
|
||||
WriteContractReturnType,
|
||||
WriteContractErrorType,
|
||||
WriteContractVariables<Abi, string, any[], Config, number>,
|
||||
unknown
|
||||
>
|
||||
| undefined,
|
||||
);
|
||||
const writeTxResult = await writeTx(makeWriteWithParams, { blockConfirmations, onBlockConfirmation });
|
||||
|
||||
return writeTxResult;
|
||||
} catch (e: any) {
|
||||
throw e;
|
||||
} finally {
|
||||
setIsMining(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sendContractWriteTx = <
|
||||
TContractName extends ContractName,
|
||||
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">,
|
||||
>(
|
||||
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
|
||||
options?: Omit<ScaffoldWriteContractOptions, "onBlockConfirmation" | "blockConfirmations">,
|
||||
) => {
|
||||
if (!deployedContractData) {
|
||||
notification.error("Target Contract is not deployed, did you forget to run `yarn deploy`?");
|
||||
return;
|
||||
}
|
||||
if (!accountChain?.id) {
|
||||
notification.error("Please connect your wallet");
|
||||
return;
|
||||
}
|
||||
|
||||
if (accountChain?.id !== selectedNetwork.id) {
|
||||
notification.error(`Wallet is connected to the wrong network. Please switch to ${selectedNetwork.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
wagmiContractWrite.writeContract(
|
||||
{
|
||||
abi: deployedContractData.abi as Abi,
|
||||
address: deployedContractData.address,
|
||||
...variables,
|
||||
} as WriteContractVariables<Abi, string, any[], Config, number>,
|
||||
options as
|
||||
| MutateOptions<
|
||||
WriteContractReturnType,
|
||||
WriteContractErrorType,
|
||||
WriteContractVariables<Abi, string, any[], Config, number>,
|
||||
unknown
|
||||
>
|
||||
| undefined,
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
...wagmiContractWrite,
|
||||
isMining,
|
||||
// Overwrite wagmi's writeContactAsync
|
||||
writeContractAsync: sendContractWriteAsyncTx,
|
||||
// Overwrite wagmi's writeContract
|
||||
writeContract: sendContractWriteTx,
|
||||
};
|
||||
}
|
||||
19
packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts
Normal file
19
packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import scaffoldConfig from "~~/scaffold.config";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
import { AllowedChainIds } from "~~/utils/scaffold-eth";
|
||||
import { ChainWithAttributes, NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth/networks";
|
||||
|
||||
/**
|
||||
* Given a chainId, retrives the network object from `scaffold.config`,
|
||||
* if not found default to network set by `useTargetNetwork` hook
|
||||
*/
|
||||
export function useSelectedNetwork(chainId?: AllowedChainIds): ChainWithAttributes {
|
||||
const globalTargetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork);
|
||||
const targetNetwork = scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chainId);
|
||||
|
||||
if (targetNetwork) {
|
||||
return { ...targetNetwork, ...NETWORKS_EXTRA_DATA[targetNetwork.id] };
|
||||
}
|
||||
|
||||
return globalTargetNetwork;
|
||||
}
|
||||
24
packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts
Normal file
24
packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useAccount } from "wagmi";
|
||||
import scaffoldConfig from "~~/scaffold.config";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
import { ChainWithAttributes } from "~~/utils/scaffold-eth";
|
||||
import { NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth";
|
||||
|
||||
/**
|
||||
* Retrieves the connected wallet's network from scaffold.config or defaults to the 0th network in the list if the wallet is not connected.
|
||||
*/
|
||||
export function useTargetNetwork(): { targetNetwork: ChainWithAttributes } {
|
||||
const { chain } = useAccount();
|
||||
const targetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork);
|
||||
const setTargetNetwork = useGlobalState(({ setTargetNetwork }) => setTargetNetwork);
|
||||
|
||||
useEffect(() => {
|
||||
const newSelectedNetwork = scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chain?.id);
|
||||
if (newSelectedNetwork && newSelectedNetwork.id !== targetNetwork.id) {
|
||||
setTargetNetwork({ ...newSelectedNetwork, ...NETWORKS_EXTRA_DATA[newSelectedNetwork.id] });
|
||||
}
|
||||
}, [chain?.id, setTargetNetwork, targetNetwork.id]);
|
||||
|
||||
return useMemo(() => ({ targetNetwork }), [targetNetwork]);
|
||||
}
|
||||
115
packages/nextjs/hooks/scaffold-eth/useTransactor.tsx
Normal file
115
packages/nextjs/hooks/scaffold-eth/useTransactor.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Hash, SendTransactionParameters, TransactionReceipt, WalletClient } from "viem";
|
||||
import { Config, useWalletClient } from "wagmi";
|
||||
import { getPublicClient } from "wagmi/actions";
|
||||
import { SendTransactionMutate } from "wagmi/query";
|
||||
import scaffoldConfig from "~~/scaffold.config";
|
||||
import { wagmiConfig } from "~~/services/web3/wagmiConfig";
|
||||
import { AllowedChainIds, getBlockExplorerTxLink, notification } from "~~/utils/scaffold-eth";
|
||||
import { TransactorFuncOptions, getParsedErrorWithAllAbis } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type TransactionFunc = (
|
||||
tx: (() => Promise<Hash>) | Parameters<SendTransactionMutate<Config, undefined>>[0],
|
||||
options?: TransactorFuncOptions,
|
||||
) => Promise<Hash | undefined>;
|
||||
|
||||
/**
|
||||
* Custom notification content for TXs.
|
||||
*/
|
||||
const TxnNotification = ({ message, blockExplorerLink }: { message: string; blockExplorerLink?: string }) => {
|
||||
return (
|
||||
<div className={`flex flex-col ml-1 cursor-default`}>
|
||||
<p className="my-0">{message}</p>
|
||||
{blockExplorerLink && blockExplorerLink.length > 0 ? (
|
||||
<a href={blockExplorerLink} target="_blank" rel="noreferrer" className="block link">
|
||||
check out transaction
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs Transaction passed in to returned function showing UI feedback.
|
||||
* @param _walletClient - Optional wallet client to use. If not provided, will use the one from useWalletClient.
|
||||
* @returns function that takes in transaction function as callback, shows UI feedback for transaction and returns a promise of the transaction hash
|
||||
*/
|
||||
export const useTransactor = (_walletClient?: WalletClient): TransactionFunc => {
|
||||
let walletClient = _walletClient;
|
||||
const { data } = useWalletClient();
|
||||
if (walletClient === undefined && data) {
|
||||
walletClient = data;
|
||||
}
|
||||
|
||||
const result: TransactionFunc = async (tx, options) => {
|
||||
if (!walletClient) {
|
||||
notification.error("Cannot access account");
|
||||
console.error("⚡️ ~ file: useTransactor.tsx ~ error");
|
||||
return;
|
||||
}
|
||||
|
||||
let notificationId = null;
|
||||
let transactionHash: Hash | undefined = undefined;
|
||||
let transactionReceipt: TransactionReceipt | undefined;
|
||||
let blockExplorerTxURL = "";
|
||||
let chainId: number = scaffoldConfig.targetNetworks[0].id;
|
||||
try {
|
||||
chainId = await walletClient.getChainId();
|
||||
// Get full transaction from public client
|
||||
const publicClient = getPublicClient(wagmiConfig);
|
||||
|
||||
notificationId = notification.loading(<TxnNotification message="Awaiting for user confirmation" />);
|
||||
if (typeof tx === "function") {
|
||||
// Tx is already prepared by the caller
|
||||
const result = await tx();
|
||||
transactionHash = result;
|
||||
} else if (tx != null) {
|
||||
transactionHash = await walletClient.sendTransaction(tx as SendTransactionParameters);
|
||||
} else {
|
||||
throw new Error("Incorrect transaction passed to transactor");
|
||||
}
|
||||
notification.remove(notificationId);
|
||||
|
||||
blockExplorerTxURL = chainId ? getBlockExplorerTxLink(chainId, transactionHash) : "";
|
||||
|
||||
notificationId = notification.loading(
|
||||
<TxnNotification message="Waiting for transaction to complete." blockExplorerLink={blockExplorerTxURL} />,
|
||||
);
|
||||
|
||||
transactionReceipt = await publicClient.waitForTransactionReceipt({
|
||||
hash: transactionHash,
|
||||
confirmations: options?.blockConfirmations,
|
||||
});
|
||||
notification.remove(notificationId);
|
||||
|
||||
if (transactionReceipt.status === "reverted") throw new Error("Transaction reverted");
|
||||
|
||||
notification.success(
|
||||
<TxnNotification message="Transaction completed successfully!" blockExplorerLink={blockExplorerTxURL} />,
|
||||
{
|
||||
icon: "🎉",
|
||||
},
|
||||
);
|
||||
|
||||
if (options?.onBlockConfirmation) options.onBlockConfirmation(transactionReceipt);
|
||||
} catch (error: any) {
|
||||
if (notificationId) {
|
||||
notification.remove(notificationId);
|
||||
}
|
||||
console.error("⚡️ ~ file: useTransactor.ts ~ error", error);
|
||||
const message = getParsedErrorWithAllAbis(error, chainId as AllowedChainIds);
|
||||
|
||||
// if receipt was reverted, show notification with block explorer link and return error
|
||||
if (transactionReceipt?.status === "reverted") {
|
||||
notification.error(<TxnNotification message={message} blockExplorerLink={blockExplorerTxURL} />);
|
||||
throw error;
|
||||
}
|
||||
|
||||
notification.error(message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return transactionHash;
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
21
packages/nextjs/hooks/scaffold-eth/useWatchBalance.ts
Normal file
21
packages/nextjs/hooks/scaffold-eth/useWatchBalance.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTargetNetwork } from "./useTargetNetwork";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { UseBalanceParameters, useBalance, useBlockNumber } from "wagmi";
|
||||
|
||||
/**
|
||||
* Wrapper around wagmi's useBalance hook. Updates data on every block change.
|
||||
*/
|
||||
export const useWatchBalance = (useBalanceParameters: UseBalanceParameters) => {
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: blockNumber } = useBlockNumber({ watch: true, chainId: targetNetwork.id });
|
||||
const { queryKey, ...restUseBalanceReturn } = useBalance(useBalanceParameters);
|
||||
|
||||
useEffect(() => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockNumber]);
|
||||
|
||||
return restUseBalanceReturn;
|
||||
};
|
||||
Reference in New Issue
Block a user