diff --git a/packages/nextjs/.env.example b/packages/nextjs/.env.example index 1d5ae40f..a81d3b0c 100644 --- a/packages/nextjs/.env.example +++ b/packages/nextjs/.env.example @@ -16,6 +16,7 @@ NEXT_PUBLIC_POLYGON_ETHERSCAN_API_KEY= NEXT_PUBLIC_ARBITRUM_ETHERSCAN_API_KEY= NEXT_PUBLIC_SCROLL_ETHERSCAN_API_KEY= NEXT_PUBLIC_BASE_ETHERSCAN_API_KEY= +NEXT_PUBLIC_HEIMDALL_URL= NEXT_PUBLIC_ALCHEMY_API_KEY= NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID= diff --git a/packages/nextjs/components/MiniFooter.tsx b/packages/nextjs/components/MiniFooter.tsx index b1aff937..ad757317 100644 --- a/packages/nextjs/components/MiniFooter.tsx +++ b/packages/nextjs/components/MiniFooter.tsx @@ -3,7 +3,7 @@ import { BuidlGuidlLogo } from "~~/components/assets/BuidlGuidlLogo"; export const MiniFooter = () => { return ( -
+
Fork me diff --git a/packages/nextjs/components/NetworksDropdown.tsx b/packages/nextjs/components/NetworksDropdown.tsx index 69570271..903128dd 100644 --- a/packages/nextjs/components/NetworksDropdown.tsx +++ b/packages/nextjs/components/NetworksDropdown.tsx @@ -78,7 +78,7 @@ export const NetworksDropdown = ({ onChange }: { onChange: (options: any) => any onChange={onChange} components={{ Option: IconOption }} isSearchable={!isMobile} - className="text-sm w-44 max-w-xs bg-white relative" + className="max-w-xs bg-white relative text-sm w-44" theme={theme => ({ ...theme, colors: { @@ -88,6 +88,9 @@ export const NetworksDropdown = ({ onChange }: { onChange: (options: any) => any primary: "#551d98", }, })} + styles={{ + menuList: provided => ({ ...provided, maxHeight: 280, overflow: "auto" }), + }} /> ); }; diff --git a/packages/nextjs/pages/index.tsx b/packages/nextjs/pages/index.tsx index c6fac2d4..3bc43ac9 100644 --- a/packages/nextjs/pages/index.tsx +++ b/packages/nextjs/pages/index.tsx @@ -5,10 +5,11 @@ import { useRouter } from "next/router"; import type { NextPage } from "next"; import { Address, isAddress } from "viem"; import { usePublicClient } from "wagmi"; +import { ChevronLeftIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { MetaHeader } from "~~/components/MetaHeader"; import { MiniFooter } from "~~/components/MiniFooter"; import { NetworksDropdown } from "~~/components/NetworksDropdown"; -import { AddressInput, InputBase } from "~~/components/scaffold-eth"; +import { AddressInput } from "~~/components/scaffold-eth"; import { useAbiNinjaState } from "~~/services/store/store"; import { fetchContractABIFromAnyABI, fetchContractABIFromEtherscan, parseAndCorrectJSON } from "~~/utils/abi"; import { detectProxyTarget } from "~~/utils/abi-ninja/proxyContracts"; @@ -25,13 +26,11 @@ const networks = getTargetNetworks(); const Home: NextPage = () => { const [activeTab, setActiveTab] = useState(TabName.verifiedContract); - const [network, setNetwork] = useState(networks[1].id.toString()); + const [network, setNetwork] = useState(networks[0].id.toString()); const [verifiedContractAddress, setVerifiedContractAddress] = useState
(""); const [localAbiContractAddress, setLocalAbiContractAddress] = useState(""); const [localContractAbi, setLocalContractAbi] = useState(""); const [isFetchingAbi, setIsFetchingAbi] = useState(false); - const [isCheckingContractAddress, setIsCheckingContractAddress] = useState(false); - const [isContract, setIsContract] = useState(false); const publicClient = usePublicClient({ chainId: parseInt(network), @@ -74,7 +73,18 @@ const Home: NextPage = () => { } catch (etherscanError: any) { setIsAbiAvailable(false); console.error("Error fetching ABI from Etherscan: ", etherscanError); - notification.error(etherscanError.message || "Error occurred while fetching ABI"); + + const bytecode = await publicClient.getBytecode({ + address: verifiedContractAddress, + }); + const isContract = Boolean(bytecode) && bytecode !== "0x"; + + if (isContract) { + setLocalAbiContractAddress(verifiedContractAddress); + setActiveTab(TabName.addressAbi); + } else { + notification.error("Address is not a contract, are you sure you are on the correct chain?"); + } } } finally { setIsFetchingAbi(false); @@ -83,44 +93,14 @@ const Home: NextPage = () => { if (isAddress(verifiedContractAddress)) { if (network === "31337") { - notification.error("To interact with Localhost contracts, please use Address + ABI tab"); + setActiveTab(TabName.addressAbi); return; } fetchContractAbi(); } else { setIsAbiAvailable(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [verifiedContractAddress, network, setContractAbi]); - - useEffect(() => { - const checkContract = async () => { - if (!isAddress(localAbiContractAddress)) { - setIsContract(false); - return; - } - - setIsCheckingContractAddress(true); - try { - const bytecode = await publicClient.getBytecode({ - address: localAbiContractAddress, - }); - const isContract = Boolean(bytecode) && bytecode !== "0x"; - setIsContract(isContract); - - if (!isContract) { - notification.error("Address is not a contract"); - } - } catch (e) { - notification.error("Error while checking for contract address"); - setIsContract(false); - } finally { - setIsCheckingContractAddress(false); - } - }; - - checkContract(); - }, [localAbiContractAddress, publicClient]); + }, [verifiedContractAddress, network, setContractAbi, publicClient, setImplementationAddress]); useEffect(() => { if (router.pathname === "/") { @@ -130,17 +110,46 @@ const Home: NextPage = () => { }, [router.pathname, setContractAbi, setImplementationAddress]); const handleLoadContract = () => { - if (activeTab === TabName.verifiedContract) { + if (isAbiAvailable) { router.push(`/${verifiedContractAddress}/${network}`); - } else if (activeTab === TabName.addressAbi) { - try { - setContractAbi(parseAndCorrectJSON(localContractAbi)); - } catch (error) { - notification.error("Invalid ABI format. Please ensure it is a valid JSON."); + } + }; + + const handleUserProvidedAbi = () => { + if (!localContractAbi) { + notification.error("Please provide an ABI."); + return; + } + try { + const parsedAbi = parseAndCorrectJSON(localContractAbi); + setContractAbi(parsedAbi); + router.push(`/${localAbiContractAddress}/${network}`); + notification.success("ABI successfully loaded."); + } catch (error) { + notification.error("Invalid ABI format. Please ensure it is a valid JSON."); + } + }; + + const fetchAbiFromHeimdall = async (contractAddress: string) => { + setIsFetchingAbi(true); + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_HEIMDALL_URL}/${network}/${contractAddress}`); + const abi = await response.json(); + if (abi.length === 0) { + notification.error("Failed to fetch ABI from Heimdall. Please try again or enter ABI manually."); + setIsFetchingAbi(false); return; } - setAbiContractAddress(localAbiContractAddress); - router.push(`/${localAbiContractAddress}/${network}`); + setContractAbi(abi); + setIsAbiAvailable(true); + setAbiContractAddress(contractAddress); + router.push(`/${contractAddress}/${network}`); + } catch (error) { + console.error("Error fetching ABI from Heimdall: ", error); + notification.error("Failed to fetch ABI from Heimdall. Please try again or enter ABI manually."); + setIsAbiAvailable(false); + } finally { + setIsFetchingAbi(false); } }; @@ -148,121 +157,123 @@ const Home: NextPage = () => { <>
-
-
- logo -

ABI Ninja

-

Interact with any contract on Ethereum

-
- setNetwork(option ? option.value.toString() : "")} /> -
- -
- setActiveTab(TabName.verifiedContract)} - > - Verified Contract - - +
+ {tabValues.map(tabValue => ( + + {tabValue === TabName.verifiedContract ? ( +
+ logo +

ABI Ninja

+

Interact with any contract on Ethereum

+
+ setNetwork(option ? option.value.toString() : "")} /> +
-
-
- {tabValues.map(tabValue => ( -
- {tabValue === TabName.verifiedContract ? ( -
- -
-
Quick Access
-
- - DAI - - - Gitcoin - - - Opensea - -
-
+
+ +
+ + +
+
Quick Access
+
+ + DAI + + + Gitcoin + + + Opensea +
- ) : ( -
- - +
+
+ ) : ( +
+
+ + logo +
+ +
+
+ +

Contract not verified

- )} +

+ {localAbiContractAddress} +

+

+ You can decompile the contract (beta) or import the ABI manually below. +

+ +
+
+

Manually import ABI

+ + +
- ))} + )}
-
- - + ))}
diff --git a/packages/nextjs/scaffold.config.ts b/packages/nextjs/scaffold.config.ts index 9ed80318..bcc92962 100644 --- a/packages/nextjs/scaffold.config.ts +++ b/packages/nextjs/scaffold.config.ts @@ -14,9 +14,7 @@ const scaffoldConfig = { targetNetworks: [ chains.mainnet, chains.sepolia, - chains.goerli, chains.optimism, - chains.optimismGoerli, chains.base, chains.baseSepolia, chains.polygon, diff --git a/packages/nextjs/utils/scaffold-eth/networks.ts b/packages/nextjs/utils/scaffold-eth/networks.ts index ebec9643..0af1d5cf 100644 --- a/packages/nextjs/utils/scaffold-eth/networks.ts +++ b/packages/nextjs/utils/scaffold-eth/networks.ts @@ -40,12 +40,6 @@ export const NETWORKS_EXTRA_DATA: Record = { etherscanApiKey: MAINNET_ETHERSCAN_API_KEY, icon: "/mainnet.svg", }, - [chains.goerli.id]: { - color: "#0975F6", - etherscanEndpoint: "https://api-goerli.etherscan.io", - etherscanApiKey: MAINNET_ETHERSCAN_API_KEY, - icon: "/mainnet.svg", - }, [chains.gnosis.id]: { color: "#48a9a6", etherscanEndpoint: "https://api.gnosisscan.io", @@ -66,22 +60,12 @@ export const NETWORKS_EXTRA_DATA: Record = { etherscanApiKey: POLYGON_ETHERSCAN_API_KEY, icon: "/polygon.svg", }, - [chains.optimismGoerli.id]: { - color: "#f01a37", - etherscanEndpoint: "https://api-goerli-optimistic.etherscan.io", - etherscanApiKey: OPTIMISM_ETHERSCAN_API_KEY, - icon: "/optimism.svg", - }, [chains.optimism.id]: { color: "#f01a37", etherscanEndpoint: "https://api-optimistic.etherscan.io", etherscanApiKey: OPTIMISM_ETHERSCAN_API_KEY, icon: "/optimism.svg", }, - [chains.arbitrumGoerli.id]: { - color: "#28a0f0", - icon: "/arbitrum.jpg", - }, [chains.arbitrum.id]: { color: "#28a0f0", etherscanEndpoint: "https://api.arbiscan.io",