Skip to content

Commit

Permalink
Use getsourcecode endpoint from etherscan api instead of anyabi+ prox…
Browse files Browse the repository at this point in the history
…y detection logic (#157)
  • Loading branch information
portdeveloper authored Oct 3, 2024
1 parent 4783145 commit b7a03b0
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 202 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/test-app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Create .env.local in nextjs
run: |
touch packages/nextjs/.env.local
echo NEXT_PUBLIC_GNOSIS_ETHERSCAN_API_KEY=${{ secrets.NEXT_PUBLIC_GNOSIS_ETHERSCAN_API_KEY }} >> packages/nextjs/.env.local
echo NEXT_PUBLIC_BSC_ETHERSCAN_API_KEY=${{ secrets.NEXT_PUBLIC_BSC_ETHERSCAN_API_KEY }} >> packages/nextjs/.env.local
echo NEXT_PUBLIC_HEIMDALL_URL=${{ secrets.NEXT_PUBLIC_HEIMDALL_URL }} >> packages/nextjs/.env.local
echo NEXT_PUBLIC_BASE_ETHERSCAN_API_KEY=${{ secrets.NEXT_PUBLIC_BASE_ETHERSCAN_API_KEY }} >> packages/nextjs/.env.local
echo NEXT_PUBLIC_SCROLL_ETHERSCAN_API_KEY=${{ secrets.NEXT_PUBLIC_SCROLL_ETHERSCAN_API_KEY }} >> packages/nextjs/.env.local
echo NEXT_PUBLIC_OPTIMISM_ETHERSCAN_API_KEY=${{ secrets.NEXT_PUBLIC_OPTIMISM_ETHERSCAN_API_KEY }} >> packages/nextjs/.env.local
echo NEXT_PUBLIC_MAINNET_ETHERSCAN_API_KEY=${{ secrets.NEXT_PUBLIC_MAINNET_ETHERSCAN_API_KEY }} >> packages/nextjs/.env.local
echo NEXT_PUBLIC_POLYGON_ETHERSCAN_API_KEY=${{ secrets.NEXT_PUBLIC_POLYGON_ETHERSCAN_API_KEY }} >> packages/nextjs/.env.local
echo NEXT_PUBLIC_ARBITRUM_ETHERSCAN_API_KEY=${{ secrets.NEXT_PUBLIC_ARBITRUM_ETHERSCAN_API_KEY }} >> packages/nextjs/.env.local
- name: Cypress run
uses: cypress-io/github-action@v6
with:
Expand Down
8 changes: 6 additions & 2 deletions packages/nextjs/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@

NEXT_PUBLIC_MAINNET_ETHERSCAN_API_KEY=
NEXT_PUBLIC_OPTIMISM_ETHERSCAN_API_KEY=
NEXT_PUBLIC_BASE_ETHERSCAN_API_KEY=
NEXT_PUBLIC_GNOSIS_ETHERSCAN_API_KEY=
NEXT_PUBLIC_ZKSYNC_ETHERSCAN_API_KEY=
NEXT_PUBLIC_POLYGON_ETHERSCAN_API_KEY=
NEXT_PUBLIC_ARBITRUM_ETHERSCAN_API_KEY=
NEXT_PUBLIC_ZKSYNC_ETHERSCAN_API_KEY=
NEXT_PUBLIC_SCROLL_ETHERSCAN_API_KEY=
NEXT_PUBLIC_BASE_ETHERSCAN_API_KEY=
NEXT_PUBLIC_BSC_ETHERSCAN_API_KEY=

NEXT_PUBLIC_HEIMDALL_URL=

NEXT_PUBLIC_ALCHEMY_API_KEY=
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
43 changes: 12 additions & 31 deletions packages/nextjs/hooks/useFetchContractAbi.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,36 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { isAddress } from "viem";
import { UsePublicClientReturnType } from "wagmi";
import { fetchContractABIFromAnyABI, fetchContractABIFromEtherscan } from "~~/utils/abi";
import { detectProxyTarget } from "~~/utils/abi-ninja/proxyContracts";

const ANYABI_TIMEOUT = 3000;
import { Address, isAddress } from "viem";
import { fetchContractABIFromEtherscan } from "~~/utils/abi";

type FetchContractAbiParams = {
contractAddress: string;
chainId: number;
publicClient: UsePublicClientReturnType;
disabled?: boolean;
};

const useFetchContractAbi = ({ contractAddress, chainId, publicClient, disabled = false }: FetchContractAbiParams) => {
const [implementationAddress, setImplementationAddress] = useState<string | null>(null);
const useFetchContractAbi = ({ contractAddress, chainId, disabled = false }: FetchContractAbiParams) => {
const [implementationAddress, setImplementationAddress] = useState<Address | null>(null);

const fetchAbi = async () => {
if (!isAddress(contractAddress)) {
throw new Error("Invalid contract address");
}

let addressToUse: string = contractAddress;
const addressToUse: Address = contractAddress;
try {
const implAddress = await detectProxyTarget(contractAddress, publicClient);
if (implAddress) {
setImplementationAddress(implAddress);
}

addressToUse = implAddress || contractAddress;

// Create a promise that resolves after ANYABI_TIMEOUT
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("AnyABI request timed out")), ANYABI_TIMEOUT);
});
const { abi, implementation } = await fetchContractABIFromEtherscan(addressToUse, chainId);

// Race between the AnyABI fetch and the timeout
const abi = await Promise.race([fetchContractABIFromAnyABI(addressToUse, chainId), timeoutPromise]);
if (!abi) throw new Error("Got empty or undefined ABI from Etherscan");

if (!abi) throw new Error("Got empty or undefined ABI from AnyABI");
if (implementation && implementation !== "0x0000000000000000000000000000000000000000") {
setImplementationAddress(implementation);
}

return { abi, address: addressToUse };
} catch (error) {
console.error("Error or timeout fetching ABI from AnyABI: ", error);
console.log("Falling back to Etherscan...");

const abiString = await fetchContractABIFromEtherscan(addressToUse, chainId);
const parsedAbi = JSON.parse(abiString);

return { abi: parsedAbi, address: contractAddress };
console.error("Error fetching ABI from Etherscan: ", error);
throw error;
}
};

Expand Down
6 changes: 0 additions & 6 deletions packages/nextjs/pages/[contractAddress]/[network].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useRouter } from "next/router";
import { GetServerSideProps } from "next";
import { ParsedUrlQuery } from "querystring";
import { Abi, Address, isAddress } from "viem";
import { usePublicClient } from "wagmi";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import { MetaHeader } from "~~/components/MetaHeader";
import { MiniHeader } from "~~/components/MiniHeader";
Expand Down Expand Up @@ -72,10 +71,6 @@ const ContractDetailPage = ({ addressFromUrl, chainIdFromUrl }: ServerSideProps)
chains: state.chains,
}));

const publicClient = usePublicClient({
chainId: parseInt(network),
});

const getNetworkName = (chainId: number) => {
const chain = Object.values(chains).find(chain => chain.id === chainId);
return chain ? chain.name : "Unknown Network";
Expand All @@ -89,7 +84,6 @@ const ContractDetailPage = ({ addressFromUrl, chainIdFromUrl }: ServerSideProps)
} = useFetchContractAbi({
contractAddress,
chainId: parseInt(network),
publicClient,
disabled: contractAbi.length > 0,
});

Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const Home: NextPage = () => {
error,
isLoading: isFetchingAbi,
implementationAddress,
} = useFetchContractAbi({ contractAddress: verifiedContractAddress, chainId: parseInt(network), publicClient });
} = useFetchContractAbi({ contractAddress: verifiedContractAddress, chainId: parseInt(network) });

const isAbiAvailable = contractData?.abi && contractData.abi.length > 0;

Expand Down
135 changes: 0 additions & 135 deletions packages/nextjs/utils/abi-ninja/proxyContracts.ts

This file was deleted.

90 changes: 63 additions & 27 deletions packages/nextjs/utils/abi.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,77 @@
import { NETWORKS_EXTRA_DATA, getTargetNetworks } from "./scaffold-eth";
import { NETWORKS_EXTRA_DATA } from "./scaffold-eth";
import { isZeroAddress } from "./scaffold-eth/common";
import { Address } from "viem";
import * as chains from "viem/chains";

export const fetchContractABIFromAnyABI = async (verifiedContractAddress: string, chainId: number) => {
const chain = getTargetNetworks().find(network => network.id === chainId);
const findChainById = (chainId: number): chains.Chain => {
const chainEntries = Object.entries(chains as Record<string, chains.Chain>);

if (!chain) throw new Error(`ChainId ${chainId} not found in supported networks`);
for (const [, chain] of chainEntries) {
if (chain.id === chainId) {
return chain;
}
}

throw new Error(`No chain found with ID ${chainId}`);
};

const url = `https://anyabi.xyz/api/get-abi/${chainId}/${verifiedContractAddress}`;
const getEtherscanApiKey = (chainId: number): string => {
const networkData = NETWORKS_EXTRA_DATA[chainId];

const response = await fetch(url);
const data = await response.json();
if (data.abi) {
return data.abi;
} else {
console.error("Could not fetch ABI from AnyABI:", data.error);
return;
if (!networkData || !networkData.etherscanApiKey) {
console.warn(`No API key found for chain ID ${chainId}`);
return "";
}

return networkData.etherscanApiKey;
};

export const fetchContractABIFromEtherscan = async (verifiedContractAddress: string, chainId: number) => {
const chain = NETWORKS_EXTRA_DATA[chainId];
export const fetchContractABIFromEtherscan = async (verifiedContractAddress: Address, chainId: number) => {
const chain = findChainById(chainId);

if (!chain || !chain.etherscanEndpoint)
throw new Error(`ChainId ${chainId} not found in supported etherscan networks`);
if (!chain || !chain.blockExplorers?.default?.apiUrl) {
throw new Error(`ChainId ${chainId} not found in supported networks or missing block explorer API URL`);
}

const apiKey = chain.etherscanApiKey ?? "";
const apiKey = getEtherscanApiKey(chainId);
const apiKeyUrlParam = apiKey.trim().length > 0 ? `&apikey=${apiKey}` : "";
const url = `${chain.etherscanEndpoint}/api?module=contract&action=getabi&address=${verifiedContractAddress}${apiKeyUrlParam}`;

const response = await fetch(url);
const data = await response.json();
if (data.status === "1") {
return data.result;
} else {
console.error("Got non-1 status from Etherscan API", data);
if (data.result) throw new Error(data.result);
throw new Error("Got non-1 status from Etherscan API");

// First call to get source code and check for implementation
const sourceCodeUrl = `${chain.blockExplorers.default.apiUrl}?module=contract&action=getsourcecode&address=${verifiedContractAddress}${apiKeyUrlParam}`;

const sourceCodeResponse = await fetch(sourceCodeUrl);
const sourceCodeData = await sourceCodeResponse.json();

if (sourceCodeData.status !== "1" || !sourceCodeData.result || sourceCodeData.result.length === 0) {
console.error("Error fetching source code from Etherscan:", sourceCodeData);
throw new Error("Failed to fetch source code from Etherscan");
}

const contractData = sourceCodeData.result[0];
const implementation = contractData.Implementation || null;

// If there's an implementation address, make a second call to get its ABI
if (implementation && !isZeroAddress(implementation)) {
const abiUrl = `${chain.blockExplorers.default.apiUrl}?module=contract&action=getabi&address=${implementation}${apiKeyUrlParam}`;
const abiResponse = await fetch(abiUrl);
const abiData = await abiResponse.json();

if (abiData.status === "1" && abiData.result) {
return {
abi: JSON.parse(abiData.result),
implementation,
};
} else {
console.error("Error fetching ABI for implementation from Etherscan:", abiData);
throw new Error("Failed to fetch ABI for implementation from Etherscan");
}
}

// If no implementation or failed to get implementation ABI, return original contract ABI
return {
abi: JSON.parse(contractData.ABI),
implementation,
};
};

export function parseAndCorrectJSON(input: string): any {
Expand Down
4 changes: 4 additions & 0 deletions packages/nextjs/utils/scaffold-eth/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// To be used in JSON.stringify when a field might be bigint
// https://wagmi.sh/react/faq#bigint-serialization
export const replacer = (_key: string, value: unknown) => (typeof value === "bigint" ? value.toString() : value);

export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

export const isZeroAddress = (address: string) => address === ZERO_ADDRESS;
Loading

0 comments on commit b7a03b0

Please sign in to comment.