From b8b825752c7b99cb06da17052e11039c014aeb3c Mon Sep 17 00:00:00 2001 From: port <108868128+portdeveloper@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:52:45 +0300 Subject: [PATCH] Enable proxy contracts (#72) --- .../scaffold-eth/Contract/ContractUI.tsx | 15 ++++++++++--- packages/nextjs/package.json | 1 + .../pages/[contractAddress]/[network].tsx | 16 ++++++++++++-- packages/nextjs/pages/index.tsx | 22 ++++++++++++++++--- packages/nextjs/services/store/store.ts | 4 ++++ yarn.lock | 8 +++++++ 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx index 8e3ba6ef..73dc28a2 100644 --- a/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx @@ -61,8 +61,11 @@ const mainNetworks = getTargetNetworks(); **/ export const ContractUI = ({ className = "", initialContractData }: ContractUIProps) => { const [refreshDisplayVariables, triggerRefreshDisplayVariables] = useReducer(value => !value, false); - const mainChainId = useAbiNinjaState(state => state.mainChainId); - const mainNetwork = mainNetworks.find(network => network.id === mainChainId); + const { implementationAddress, chainId } = useAbiNinjaState(state => ({ + chainId: state.mainChainId, + implementationAddress: state.implementationAddress, + })); + const mainNetwork = mainNetworks.find(network => network.id === chainId); const networkColor = useNetworkColor(mainNetwork); const router = useRouter(); const { network } = router.query as { network?: string }; @@ -123,7 +126,7 @@ export const ContractUI = ({ className = "", initialContractData }: ContractUIPr const { data: contractNameData, isLoading: isContractNameLoading } = useContractRead({ address: initialContractData.address, abi: initialContractData.abi, - chainId: mainChainId, + chainId: chainId, functionName: "name", }); @@ -197,6 +200,12 @@ export const ContractUI = ({ className = "", initialContractData }: ContractUIPr {displayContractName}
+ {implementationAddress && ( +
+ Implementation Address +
+
+ )}
Balance: diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 83609f16..362de183 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -21,6 +21,7 @@ "@uniswap/v2-sdk": "^3.0.1", "blo": "^1.0.1", "daisyui": "^4.4.19", + "evm-proxy-detection": "^1.2.0", "next": "13.3.4", "next-plausible": "^3.12.0", "nextjs-progressbar": "^0.0.16", diff --git a/packages/nextjs/pages/[contractAddress]/[network].tsx b/packages/nextjs/pages/[contractAddress]/[network].tsx index dd22bc4e..1aa382ed 100644 --- a/packages/nextjs/pages/[contractAddress]/[network].tsx +++ b/packages/nextjs/pages/[contractAddress]/[network].tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; +import { AlchemyProvider } from "@ethersproject/providers"; +import detectProxyTarget from "evm-proxy-detection"; import { ParsedUrlQuery } from "querystring"; import { Abi, isAddress } from "viem"; import * as chains from "viem/chains"; @@ -8,6 +10,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { MetaHeader } from "~~/components/MetaHeader"; import { MiniHeader } from "~~/components/MiniHeader"; import { ContractUI } from "~~/components/scaffold-eth"; +import scaffoldConfig from "~~/scaffold.config"; import { useAbiNinjaState } from "~~/services/store/store"; import { fetchContractABIFromAnyABI, fetchContractABIFromEtherscan } from "~~/utils/abi"; @@ -32,10 +35,12 @@ const ContractDetailPage = () => { contractAbi: storedAbi, setMainChainId, chainId, + setImplementationAddress, } = useAbiNinjaState(state => ({ contractAbi: state.contractAbi, setMainChainId: state.setMainChainId, chainId: state.mainChainId, + setImplementationAddress: state.setImplementationAddress, })); const getNetworkName = (chainId: number) => { @@ -72,7 +77,14 @@ const ContractDetailPage = () => { } try { - const abi = await fetchContractABIFromAnyABI(contractAddress, parsedNetworkId); + const alchemyProvider = new AlchemyProvider(parseInt(network), scaffoldConfig.alchemyApiKey); + const requestFunc = ({ method, params }: { method: string; params: any }) => + alchemyProvider.send(method, params); + const implementationAddress = await detectProxyTarget(contractAddress, requestFunc); + if (implementationAddress) { + setImplementationAddress(implementationAddress); + } + const abi = await fetchContractABIFromAnyABI(implementationAddress || contractAddress, parsedNetworkId); if (!abi) throw new Error("Got empty or undefined ABI from AnyABI"); setContractData({ abi, address: contractAddress }); setError(null); @@ -102,7 +114,7 @@ const ContractDetailPage = () => { } } } - }, [contractAddress, network, storedAbi, setMainChainId]); + }, [contractAddress, network, storedAbi, setMainChainId, setImplementationAddress]); return ( <> diff --git a/packages/nextjs/pages/index.tsx b/packages/nextjs/pages/index.tsx index 7e7fe631..1d5dc492 100644 --- a/packages/nextjs/pages/index.tsx +++ b/packages/nextjs/pages/index.tsx @@ -2,6 +2,8 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; +import { AlchemyProvider } from "@ethersproject/providers"; +import detectProxyTarget from "evm-proxy-detection"; import type { NextPage } from "next"; import { Address, isAddress } from "viem"; import { usePublicClient } from "wagmi"; @@ -9,6 +11,7 @@ import { MetaHeader } from "~~/components/MetaHeader"; import { MiniFooter } from "~~/components/MiniFooter"; import { NetworksDropdown } from "~~/components/NetworksDropdown"; import { AddressInput, InputBase } from "~~/components/scaffold-eth"; +import scaffoldConfig from "~~/scaffold.config"; import { useAbiNinjaState } from "~~/services/store/store"; import { fetchContractABIFromAnyABI, fetchContractABIFromEtherscan, parseAndCorrectJSON } from "~~/utils/abi"; import { getTargetNetworks, notification } from "~~/utils/scaffold-eth"; @@ -32,13 +35,17 @@ const Home: NextPage = () => { const [isCheckingContractAddress, setIsCheckingContractAddress] = useState(false); const [isContract, setIsContract] = useState(false); + const alchemyProvider = new AlchemyProvider(parseInt(network), scaffoldConfig.alchemyApiKey); + const requestFunc = ({ method, params }: { method: string; params: any }) => alchemyProvider.send(method, params); + const publicClient = usePublicClient({ chainId: parseInt(network), }); - const { setContractAbi, setAbiContractAddress } = useAbiNinjaState(state => ({ + const { setContractAbi, setAbiContractAddress, setImplementationAddress } = useAbiNinjaState(state => ({ setContractAbi: state.setContractAbi, setAbiContractAddress: state.setAbiContractAddress, + setImplementationAddress: state.setImplementationAddress, })); const [isAbiAvailable, setIsAbiAvailable] = useState(false); @@ -49,7 +56,14 @@ const Home: NextPage = () => { const fetchContractAbi = async () => { setIsFetchingAbi(true); try { - const abi = await fetchContractABIFromAnyABI(verifiedContractAddress, parseInt(network)); + const implementationAddress = await detectProxyTarget(verifiedContractAddress, requestFunc); + if (implementationAddress) { + setImplementationAddress(implementationAddress); + } + const abi = await fetchContractABIFromAnyABI( + implementationAddress || verifiedContractAddress, + parseInt(network), + ); if (!abi) throw new Error("Got empty or undefined ABI from AnyABI"); setContractAbi(abi); setIsAbiAvailable(true); @@ -80,6 +94,7 @@ const Home: NextPage = () => { } else { setIsAbiAvailable(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [verifiedContractAddress, network, setContractAbi]); useEffect(() => { @@ -114,8 +129,9 @@ const Home: NextPage = () => { useEffect(() => { if (router.pathname === "/") { setContractAbi([]); + setImplementationAddress(""); } - }, [router.pathname, setContractAbi]); + }, [router.pathname, setContractAbi, setImplementationAddress]); const handleLoadContract = () => { if (activeTab === TabName.verifiedContract) { diff --git a/packages/nextjs/services/store/store.ts b/packages/nextjs/services/store/store.ts index 55ce9b39..c0482fb2 100644 --- a/packages/nextjs/services/store/store.ts +++ b/packages/nextjs/services/store/store.ts @@ -17,6 +17,8 @@ type AbiNinjaState = { setContractAbi: (newAbi: Abi) => void; abiContractAddress: Address; setAbiContractAddress: (newAbiContractAddress: Address) => void; + implementationAddress: Address; + setImplementationAddress: (newImplementationAddress: Address) => void; }; export const useGlobalState = create(set => ({ @@ -33,4 +35,6 @@ export const useAbiNinjaState = create(set => ({ setContractAbi: (newAbi: Abi): void => set({ contractAbi: newAbi }), abiContractAddress: "", setAbiContractAddress: (newAddress: Address): void => set({ abiContractAddress: newAddress }), + implementationAddress: "", + setImplementationAddress: (newAddress: Address): void => set({ implementationAddress: newAddress }), })); diff --git a/yarn.lock b/yarn.lock index 7407a364..7c5dbd4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1370,6 +1370,7 @@ __metadata: eslint-config-next: ^13.1.6 eslint-config-prettier: ^8.5.0 eslint-plugin-prettier: ^4.2.1 + evm-proxy-detection: ^1.2.0 next: 13.3.4 next-plausible: ^3.12.0 nextjs-progressbar: ^0.0.16 @@ -4876,6 +4877,13 @@ __metadata: languageName: node linkType: hard +"evm-proxy-detection@npm:^1.2.0": + version: 1.2.0 + resolution: "evm-proxy-detection@npm:1.2.0" + checksum: d9996cbcd22eadd0b1209116d1f5c90ebf063edfe08c71c2f92372040dc004248ab280887a2f6d968d3e4963f40909f7f4cece584992e8e1f6ea136d6e18f2ee + languageName: node + linkType: hard + "execa@npm:^6.1.0": version: 6.1.0 resolution: "execa@npm:6.1.0"