Skip to content

Commit 37f10ef

Browse files
use useQuery and timeout anyabi if it takes 3sec
1 parent dd901af commit 37f10ef

File tree

2 files changed

+90
-67
lines changed

2 files changed

+90
-67
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useState } from "react";
2+
import { useQuery } from "@tanstack/react-query";
3+
import { fetchContractABIFromAnyABI, fetchContractABIFromEtherscan } from "~~/utils/abi";
4+
import { detectProxyTarget } from "~~/utils/abi-ninja/proxyContracts";
5+
6+
const ANYABI_TIMEOUT = 3000;
7+
8+
const useFetchContractAbi = (contractAddress: string, parsedNetworkId: number, publicClient: any) => {
9+
const [implementationAddress, setImplementationAddress] = useState<string | null>(null);
10+
11+
const fetchAbi = async () => {
12+
try {
13+
const implAddress = await detectProxyTarget(contractAddress, publicClient);
14+
if (implAddress) {
15+
setImplementationAddress(implAddress);
16+
}
17+
18+
const addressToUse = implAddress || contractAddress;
19+
20+
// Create a promise that resolves after ANYABI_TIMEOUT
21+
const timeoutPromise = new Promise((_, reject) => {
22+
setTimeout(() => reject(new Error("AnyABI request timed out")), ANYABI_TIMEOUT);
23+
});
24+
25+
// Race between the AnyABI fetch and the timeout
26+
const abi = await Promise.race([fetchContractABIFromAnyABI(addressToUse, parsedNetworkId), timeoutPromise]);
27+
28+
if (!abi) throw new Error("Got empty or undefined ABI from AnyABI");
29+
30+
return { abi, address: contractAddress };
31+
} catch (error) {
32+
console.error("Error or timeout fetching ABI from AnyABI: ", error);
33+
console.log("Falling back to Etherscan...");
34+
35+
const abiString = await fetchContractABIFromEtherscan(contractAddress, parsedNetworkId);
36+
const parsedAbi = JSON.parse(abiString);
37+
return { abi: parsedAbi, address: contractAddress };
38+
}
39+
};
40+
41+
const { data, error, isLoading } = useQuery({
42+
queryKey: ["contractAbi", { contractAddress, chainId: parsedNetworkId }],
43+
queryFn: fetchAbi,
44+
});
45+
46+
return {
47+
contractData: data,
48+
error,
49+
isLoading,
50+
implementationAddress,
51+
};
52+
};
53+
54+
export default useFetchContractAbi;

packages/nextjs/pages/[contractAddress]/[network].tsx

Lines changed: 36 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { MiniHeader } from "~~/components/MiniHeader";
1111
import { formDataToChain, storeChainInLocalStorage } from "~~/components/NetworksDropdown/utils";
1212
import { SwitchTheme } from "~~/components/SwitchTheme";
1313
import { ContractUI } from "~~/components/scaffold-eth";
14+
import useFetchContractAbi from "~~/hooks/useFetchContractAbi";
1415
import { useAbiNinjaState, useGlobalState } from "~~/services/store/store";
15-
import { fetchContractABIFromAnyABI, fetchContractABIFromEtherscan, parseAndCorrectJSON } from "~~/utils/abi";
16-
import { detectProxyTarget } from "~~/utils/abi-ninja/proxyContracts";
16+
import { parseAndCorrectJSON } from "~~/utils/abi";
1717
import { notification } from "~~/utils/scaffold-eth";
1818

1919
interface ParsedQueryContractDetailsPage extends ParsedUrlQuery {
@@ -32,7 +32,6 @@ type ServerSideProps = {
3232
};
3333

3434
export const getServerSideProps: GetServerSideProps = async context => {
35-
// Assume that 'contractAddress' and 'network' cannot be arrays.
3635
const contractAddress = context.params?.contractAddress as Address | undefined;
3736
const network = context.params?.network as string | undefined;
3837

@@ -58,16 +57,14 @@ const toCamelCase = (str: string) => {
5857
const ContractDetailPage = ({ addressFromUrl, chainIdFromUrl }: ServerSideProps) => {
5958
const router = useRouter();
6059
const { contractAddress, network } = router.query as ParsedQueryContractDetailsPage;
61-
const [contractData, setContractData] = useState<ContractData>({ abi: [], address: contractAddress });
6260
const [localContractAbi, setLocalContractAbi] = useState<string>("");
63-
const [isLoading, setIsLoading] = useState(true);
64-
const [error, setError] = useState<string | null>(null);
65-
const contractName = contractData.address;
66-
const { setMainChainId, chainId, setImplementationAddress, contractAbi } = useAbiNinjaState(state => ({
61+
const [isUseLocalAbi, setIsUseLocalAbi] = useState(false);
62+
const [contractData, setContractData] = useState<ContractData | null>(null);
63+
const contractName = contractAddress;
64+
const { setMainChainId, chainId, setImplementationAddress } = useAbiNinjaState(state => ({
6765
setMainChainId: state.setMainChainId,
6866
chainId: state.mainChainId,
6967
setImplementationAddress: state.setImplementationAddress,
70-
contractAbi: state.contractAbi,
7168
}));
7269

7370
const { addChain, chains } = useGlobalState(state => ({
@@ -84,75 +81,47 @@ const ContractDetailPage = ({ addressFromUrl, chainIdFromUrl }: ServerSideProps)
8481
return chain ? chain.name : "Unknown Network";
8582
};
8683

87-
useEffect(() => {
88-
if (contractAbi.length > 0) {
89-
setContractData({ abi: contractAbi, address: contractAddress });
90-
}
84+
const {
85+
contractData: fetchedContractData,
86+
error: fetchError,
87+
isLoading,
88+
implementationAddress,
89+
} = useFetchContractAbi(contractAddress, parseInt(network), publicClient);
90+
91+
const effectiveContractData = isUseLocalAbi && contractData ? contractData : fetchedContractData;
92+
93+
const error = isUseLocalAbi ? null : fetchError;
9194

95+
useEffect(() => {
9296
if (network) {
9397
let normalizedNetwork = network.toLowerCase();
9498
if (normalizedNetwork === "ethereum" || normalizedNetwork === "mainnet") {
95-
normalizedNetwork = "ethereum"; // chain.network for mainnet in viem/chains
99+
normalizedNetwork = "ethereum";
96100
}
97101

98102
const chain = Object.values(chains).find(chain => toCamelCase(chain.name) === normalizedNetwork);
99-
100-
let parsedNetworkId = 1;
101-
if (chain) {
102-
parsedNetworkId = chain.id;
103-
} else {
104-
parsedNetworkId = parseInt(network);
105-
}
106-
103+
const parsedNetworkId = chain ? chain.id : parseInt(network);
107104
setMainChainId(parsedNetworkId);
105+
}
108106

109-
const fetchContractAbi = async () => {
110-
setIsLoading(true);
111-
112-
try {
113-
const implementationAddress = await detectProxyTarget(contractAddress, publicClient);
114-
115-
if (implementationAddress) {
116-
setImplementationAddress(implementationAddress);
117-
}
118-
const abi = await fetchContractABIFromAnyABI(implementationAddress || contractAddress, parsedNetworkId);
119-
if (!abi) throw new Error("Got empty or undefined ABI from AnyABI");
120-
setContractData({ abi, address: contractAddress });
121-
setError(null);
122-
} catch (error: any) {
123-
console.error("Error fetching ABI from AnyABI: ", error);
124-
console.log("Trying to fetch ABI from Etherscan...");
125-
try {
126-
const abiString = await fetchContractABIFromEtherscan(contractAddress, parsedNetworkId);
127-
const parsedAbi = JSON.parse(abiString);
128-
setContractData({ abi: parsedAbi, address: contractAddress });
129-
setError(null);
130-
} catch (etherscanError: any) {
131-
console.error("Error fetching ABI from Etherscan: ", etherscanError);
132-
setError(etherscanError.message || "Error occurred while fetching ABI");
133-
}
134-
} finally {
135-
setIsLoading(false);
136-
}
137-
};
138-
139-
if (contractAddress && network) {
140-
if (isAddress(contractAddress)) {
141-
fetchContractAbi();
142-
} else {
143-
setIsLoading(false);
144-
setError("Please enter a valid address");
145-
}
146-
}
107+
if (implementationAddress) {
108+
setImplementationAddress(implementationAddress);
147109
}
148-
}, [contractAddress, network, setMainChainId, setImplementationAddress, publicClient, chains, contractAbi]);
110+
}, [network, implementationAddress, chains, setMainChainId, setImplementationAddress]);
149111

150112
const handleUserProvidedAbi = () => {
151113
try {
152114
const parsedAbi = parseAndCorrectJSON(localContractAbi);
153-
setContractData({ abi: parsedAbi, address: contractAddress });
154-
notification.success("ABI successfully loaded.");
115+
if (parsedAbi) {
116+
setIsUseLocalAbi(true);
117+
setContractData({ abi: parsedAbi, address: contractAddress });
118+
notification.success("ABI successfully loaded.");
119+
} else {
120+
throw new Error("Parsed ABI is null or undefined");
121+
}
155122
} catch (error) {
123+
console.error("Error parsing ABI:", error);
124+
setIsUseLocalAbi(false);
156125
notification.error("Invalid ABI format. Please ensure it is a valid JSON.");
157126
}
158127
};
@@ -176,18 +145,18 @@ const ContractDetailPage = ({ addressFromUrl, chainIdFromUrl }: ServerSideProps)
176145
<div className="bg-base-100 h-screen flex flex-col">
177146
<MiniHeader />
178147
<div className="flex flex-col gap-y-6 lg:gap-y-8 flex-grow h-full overflow-hidden">
179-
{isLoading ? (
148+
{isLoading && !isUseLocalAbi ? (
180149
<div className="flex justify-center h-full mt-14">
181150
<span className="loading loading-spinner text-primary h-14 w-14"></span>
182151
</div>
183-
) : contractData.abi?.length > 0 ? (
184-
<ContractUI key={contractName} initialContractData={contractData} />
152+
) : effectiveContractData && effectiveContractData?.abi?.length > 0 ? (
153+
<ContractUI key={contractName} initialContractData={effectiveContractData} />
185154
) : (
186155
<div className="bg-base-200 flex flex-col border shadow-xl rounded-2xl px-6 lg:px-8 m-4 overflow-auto">
187156
<div className="flex items-center">
188157
<ExclamationTriangleIcon className="text-red-500 mt-4 h-20 w-20 pr-4" />
189158
<div>
190-
<h2 className="text-2xl pt-2 flex items-end">{error}</h2>
159+
<h2 className="text-2xl pt-2 flex items-end">{error?.message}</h2>
191160
<p className="break-all">
192161
There was an error loading the contract <strong>{contractAddress}</strong> on{" "}
193162
<strong>{getNetworkName(chainId)}</strong>.

0 commit comments

Comments
 (0)