diff --git a/.env.development b/.env.development index 041f210..ada8f3a 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,5 @@ REACT_APP_SERVER_URL=http://localhost:5555 -REACT_APP_REPOSITORY_URL=http://localhost:5173 +REACT_APP_REPOSITORY_URL=http://repo.staging.sourcify.dev REACT_APP_VERIFY_URL=http://verify.staging.sourcify.dev REACT_APP_TAG=development REACT_APP_OPENROUTER_API_KEY= diff --git a/package-lock.json b/package-lock.json index af6bd1f..994f8d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,10 @@ "@ethersproject/hash": "5.8.0", "@ethersproject/keccak256": "5.8.0", "@headlessui/react": "2.2.10", - "@openrouter/ai-sdk-provider": "2.5.1", + "@openrouter/ai-sdk-provider": "2.8.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "13.5.0", - "ai": "6.0.159", + "ai": "6.0.168", "bytes": "3.1.2", "framer-motion": "11.18.2", "fuse.js": "6.6.2", @@ -26,7 +26,7 @@ "react-dom": "18.3.1", "react-dropzone": "11.7.1", "react-icons": "5.6.0", - "react-router-dom": "7.14.1", + "react-router-dom": "7.14.2", "react-select-search": "3.0.10", "react-syntax-highlighter": "15.6.6", "react-tooltip": "5.30.1", @@ -41,7 +41,7 @@ "@types/react-dom": "19.2.3", "@types/react-syntax-highlighter": "13.5.2", "autoprefixer": "10.5.0", - "postcss": "8.5.9", + "postcss": "8.5.12", "react-scripts": "5.0.1", "tailwindcss": "3.4.19" }, @@ -50,14 +50,14 @@ } }, "node_modules/@ai-sdk/gateway": { - "version": "3.0.96", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.96.tgz", - "integrity": "sha512-BDiVEMUVHGpngReeigzLyJobG0TvzYbNGzdHI8JYBZHrjOX4aL6qwIls7z3p7V4TuXVWUCbG8TSWEe7ksX4Vhw==", + "version": "3.0.104", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.104.tgz", + "integrity": "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", - "@vercel/oidc": "3.1.0" + "@vercel/oidc": "3.2.0" }, "engines": { "node": ">=18" @@ -3736,9 +3736,9 @@ } }, "node_modules/@openrouter/ai-sdk-provider": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-2.5.1.tgz", - "integrity": "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-2.8.1.tgz", + "integrity": "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ==", "license": "Apache-2.0", "engines": { "node": ">=18" @@ -5072,9 +5072,10 @@ "dev": true }, "node_modules/@vercel/oidc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz", - "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", + "license": "Apache-2.0", "engines": { "node": ">= 20" } @@ -5353,12 +5354,12 @@ } }, "node_modules/ai": { - "version": "6.0.159", - "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.159.tgz", - "integrity": "sha512-S18ozG7Dkm3Ud1tzOtAK5acczD4vygfml80RkpM9VWMFpvAFwAKSHaGYkATvPQHIE+VpD1tJY9zcTXLZ/zR5cw==", + "version": "6.0.168", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.168.tgz", + "integrity": "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "3.0.96", + "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" @@ -13517,9 +13518,9 @@ } }, "node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "dev": true, "funding": [ { @@ -15275,9 +15276,9 @@ } }, "node_modules/react-router": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", - "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", + "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -15297,12 +15298,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", - "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", + "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", "license": "MIT", "dependencies": { - "react-router": "7.14.1" + "react-router": "7.14.2" }, "engines": { "node": ">=20.0.0" diff --git a/package.json b/package.json index 70494a2..6f142e2 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,14 @@ "react-dom": "18.3.1", "react-dropzone": "11.7.1", "react-icons": "5.6.0", - "react-router-dom": "7.14.1", + "react-router-dom": "7.14.2", "react-select-search": "3.0.10", "react-syntax-highlighter": "15.6.6", "react-tooltip": "5.30.1", "recharts": "2.15.4", "web-vitals": "2.1.4", - "@openrouter/ai-sdk-provider": "2.5.1", - "ai": "6.0.159" + "@openrouter/ai-sdk-provider": "2.8.1", + "ai": "6.0.168" }, "optionalDependencies": { "fsevents": "2.3.3" @@ -68,9 +68,9 @@ "@types/react-dom": "19.2.3", "@types/react-syntax-highlighter": "13.5.2", "autoprefixer": "10.5.0", - "postcss": "8.5.9", + "postcss": "8.5.12", "react-scripts": "5.0.1", "tailwindcss": "3.4.19" }, - "homepage": "./" + "homepage": "/" } diff --git a/public/index.html b/public/index.html index 50c44fb..897dafb 100644 --- a/public/index.html +++ b/public/index.html @@ -5,6 +5,18 @@ + diff --git a/renovate.json b/renovate.json index 9ff60cb..67bd494 100644 --- a/renovate.json +++ b/renovate.json @@ -14,7 +14,8 @@ "bump" ], "groupName": "all patch and minor dependencies", - "groupSlug": "all-patch-and-minor" + "groupSlug": "all-patch-and-minor", + "minimumReleaseAge": "14 days" } ], "ignoreDeps": ["node", "cimg/node"], diff --git a/src/App.tsx b/src/App.tsx index 2587cab..61a4421 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,16 @@ import { useEffect } from "react"; -import { HashRouter, Route, Routes } from "react-router-dom"; +import { BrowserRouter, Navigate, Route, Routes, useParams } from "react-router-dom"; +import { Tooltip } from "react-tooltip"; import { ContextProvider } from "./Context"; import LandingPage from "./pages/LandingPage"; import Lookup from "./pages/Lookup"; import VerifyRedirect from "./pages/VerifyRedirect"; +const LegacyLookupRedirect = () => { + const { address } = useParams(); + return ; +}; + function App() { useEffect(() => { if (process.env.REACT_APP_TAG !== "master") { @@ -14,16 +20,19 @@ function App() { return (
+ - + } /> - } /> - } /> + } /> + } /> + } /> + } /> } /> } /> - +
); diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index d0e8dde..0df7d3a 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -5,7 +5,7 @@ import { Tooltip } from "react-tooltip"; import { SiMatrix } from "react-icons/si"; import { RiTwitterXFill } from "react-icons/ri"; import logoText from "../../assets/logo-rounded.svg"; -import { DOCS_URL, PLAYGROUND_URL } from "../../constants"; +import { DOCS_URL, PLAYGROUND_URL, REPOSITORY_URL } from "../../constants"; const Header = ({ className }: { className?: string }) => { const [showNav, setShowNav] = useState(false); @@ -74,12 +74,12 @@ const Header = ({ className }: { className?: string }) => { > Verify - + Lookup Contract Repo diff --git a/src/components/MatchBadge/index.tsx b/src/components/MatchBadge/index.tsx new file mode 100644 index 0000000..ecbc885 --- /dev/null +++ b/src/components/MatchBadge/index.tsx @@ -0,0 +1,43 @@ +import { IoCheckmarkDoneCircle, IoCheckmarkCircle } from "react-icons/io5"; + +interface MatchBadgeProps { + match: "match" | "exact_match" | null; + className?: string; + small?: boolean; +} + +const MatchBadge = ({ match, className = "", small = false }: MatchBadgeProps) => { + const sizeClasses = small ? "px-2 py-1 text-xs md:text-sm whitespace-nowrap" : "px-2 py-1 md:px-3 md:py-1 text-sm md:text-base whitespace-nowrap"; + const iconSize = small ? "text-xl" : "text-2xl md:text-3xl"; + + if (!match) { + return ( + + - No Match + + ); + } + + const isExactMatch = match === "exact_match"; + const label = isExactMatch ? "Exact Match" : "Match"; + const tooltipContent = isExactMatch + ? "Exact match: The onchain and compiled bytecode match exactly, including the metadata hashes." + : "Match: The onchain and compiled bytecode match, but metadata hashes differ or don't exist."; + + return ( + + + {isExactMatch ? : } + + {label} + + ); +}; + +export default MatchBadge; diff --git a/src/components/PageLayout/index.tsx b/src/components/PageLayout/index.tsx new file mode 100644 index 0000000..77eb46e --- /dev/null +++ b/src/components/PageLayout/index.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from "react"; + +interface PageLayoutProps { + children: ReactNode; + maxWidth?: "max-w-4xl" | "max-w-6xl"; + title?: string; + subtitle?: string; +} + +const PageLayout = ({ children, maxWidth = "max-w-4xl", title, subtitle }: PageLayoutProps) => { + return ( +
+
+
+
+
+ {(title || subtitle) && ( +
+ {title && ( +

{title}

+ )} + {subtitle && ( +

{subtitle}

+ )} +
+ )} +
{children}
+
+
+
+
+ ); +}; + +export default PageLayout; diff --git a/src/constants.ts b/src/constants.ts index 41e9820..fd04d96 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,5 @@ export const REPOSITORY_URL = process.env.REACT_APP_REPOSITORY_URL; +export const VERIFY_URL = process.env.REACT_APP_VERIFY_URL; export const SERVER_URL = process.env.REACT_APP_SERVER_URL; export const BIGQUERY_API_URL = process.env.REACT_APP_BIGQUERY_API_URL; export const BIGQUERY_DATASET_NAME = diff --git a/src/pages/Lookup/Field.tsx b/src/pages/Lookup/Field.tsx index 58b6561..d26c16c 100644 --- a/src/pages/Lookup/Field.tsx +++ b/src/pages/Lookup/Field.tsx @@ -1,75 +1,50 @@ import { isAddress, getAddress } from "@ethersproject/address"; -import { ChangeEventHandler, FormEventHandler, useState } from "react"; -import Input from "../../components/Input"; +import { useState } from "react"; import LoadingOverlay from "../../components/LoadingOverlay"; -import Toast from "../../components/Toast"; type FieldProp = { loading: boolean; handleRequest: (address: string) => void; }; -const Field = ({ loading, handleRequest }: FieldProp) => { - const [address, setAddress] = useState(""); - const [error, setError] = useState(""); +const EXAMPLE_ADDRESS = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; - const checkAndSendRequest = (address: string) => { - setAddress(address) - if (!isAddress(address)) { - setError("Invalid Address"); - return; +const Field = ({ loading, handleRequest }: FieldProp) => { + const [value, setValue] = useState(""); + const [touched, setTouched] = useState(false); + // Show error once they've typed enough to be attempting an address, or after blur + const invalid = value.length > 0 && !isAddress(value) && (touched || value.length >= 42); + + const handleChange = (input: string) => { + setValue(input); + if (isAddress(input)) { + handleRequest(getAddress(input)); } - // Get checksummed format - const checksummedAddress = getAddress(address); - setAddress(checksummedAddress) - handleRequest(checksummedAddress); - } - - const handleSubmit: FormEventHandler = (e) => { - e.preventDefault(); - checkAndSendRequest(address) - }; - - const handleChange: ChangeEventHandler = (e) => { - const newAddress = e.currentTarget.value; - checkAndSendRequest(newAddress) - }; - - const handleExample = () => { - const exampleAddress = "0x1F98431c8aD98523631AE4a59f267346ea31F984"; // Uniswap - checkAndSendRequest(exampleAddress); }; return ( -
-
- {loading && } -
- - - {!!error && ( - setError("")} - /> - )}{" "} -
- -
- +
+ {loading && } + handleChange(e.target.value)} + onBlur={() => setTouched(true)} + placeholder="0x…" + className={`w-full px-3 py-2 border rounded-md shadow-sm font-mono text-sm focus:outline-none focus:ring-2 focus:ring-ceruleanBlue-500 focus:border-ceruleanBlue-500 ${ + invalid ? "border-red-400" : "border-gray-300" + }`} + /> + {invalid &&

Invalid contract address

} +
+
); diff --git a/src/pages/Lookup/Result.tsx b/src/pages/Lookup/Result.tsx index 6e03236..6902f04 100644 --- a/src/pages/Lookup/Result.tsx +++ b/src/pages/Lookup/Result.tsx @@ -1,240 +1,127 @@ import { useContext } from "react"; -import { renderToString } from "react-dom/server"; -import { HiBadgeCheck, HiOutlineArrowLeft, HiOutlineInformationCircle, HiX } from "react-icons/hi"; -import { Link } from "react-router-dom"; -import { Tooltip } from "react-tooltip"; -import Button from "../../components/Button"; -import { REPOSITORY_URL } from "../../constants"; +import { HiOutlineArrowLeft } from "react-icons/hi"; +import { IoOpenOutline } from "react-icons/io5"; +import MatchBadge from "../../components/MatchBadge"; +import { REPOSITORY_URL, VERIFY_URL } from "../../constants"; import { Context } from "../../Context"; -import { CheckAllByAddressResult } from "../../types"; -import { isBrowser } from "react-device-detect"; +import { AllChainsResponse, VerifiedContractMinimal } from "../../types"; type ResultProp = { - response: CheckAllByAddressResult; - goBack: React.DispatchWithoutAction; + address: string; + response: AllChainsResponse; + goBack: () => void; }; -const URL_TYPE = { - REMIX: "remix", - REPO: "repo", -}; +const remixUrl = (chainId: string, address: string) => + `https://remix.ethereum.org/?#activate=contract-verification&call=contract-verification//lookupAndSave//sourcify//${chainId}//${address}`; -const generateUrl = (type: string, chainId: string, address: string, status: string) => { - if (type === URL_TYPE.REMIX) - return `https://remix.ethereum.org/?#activate=contract-verification&call=contract-verification//lookupAndSave//sourcify//${chainId}//${address}`; - return `${REPOSITORY_URL}/${chainId}/${address}/`; -}; +const repoUrl = (chainId: string, address: string) => `${REPOSITORY_URL}/${chainId}/${address}/`; -type NetworkRowProp = { - chainId: string; - status: string; - address: any; -}; -type FoundProp = { - response: CheckAllByAddressResult; - goBack: () => void; -}; -type NotFoundProp = { - address: any; - goBack: () => void; -}; -type MatchStatusProps = { - status: string; -}; -const PerfectMatchInfoText = ( - - An exact match indicates the Solidity source code does not deviate a single byte from the source code when deployed.{" "} -
See{" "} - - docs - {" "} - for details. -
-); -const PartialMatchInfoText = ( - - A match indicates the Solidity source code functionally corresponds to the deployed contract but some aspects of the - source code might differ from the original source code.
See{" "} - - docs - {" "} - for details. -
-); -const MatchStatusBadge = ({ status }: MatchStatusProps) => { - if (status === "perfect") { - return ( - <> - - - exact match - - - ); - } - if (status === "partial") { - return ( - <> - - - match - - - ); - } - return null; + +type ChainRowProps = { + contract: VerifiedContractMinimal; }; -const NetworkRow = ({ address, chainId, status }: NetworkRowProp) => { - const { sourcifyChainMap } = useContext(Context); - const chainToName = (chainId: any) => { - return sourcifyChainMap[chainId]?.title || sourcifyChainMap[chainId]?.name; - }; +const ChainRow = ({ contract }: ChainRowProps) => { + const { sourcifyChainMap } = useContext(Context); + const chain = sourcifyChainMap[parseInt(contract.chainId)]; + const chainName = chain?.title || chain?.name || `Chain ${contract.chainId}`; + const verifiedDate = new Date(contract.verifiedAt).toISOString().split("T")[0]; return ( - <> - {isBrowser ? ( - - - {chainToName(chainId)}{" "} - - - - - - - View in Sourcify Repository - - - - - View in Remix - - - - ) : ( -
-
- {chainToName(chainId)}{" "} -
-
- -
- - + + +
{chainName}
+
Chain ID: {contract.chainId}
+ + + + + + + + {verifiedDate} + + - )} - + + ); }; -const InfoText = () => ( - - Sourcify verification means a matching Solidity source code of the
contract is available on the Sourcify - repo.
See{" "} - - docs - {" "} - for details. -
-); +const Result = ({ address, response, goBack }: ResultProp) => { + const { results } = response; -const Found = ({ response, goBack }: FoundProp) => { - const chains = response?.chainIds; return ( -
- -
-

- The contract at address {response?.address} is{" "} - - verified - - -

-

{chains.length > 0 && on the following networks:}

+
+ {/* Header: back arrow + full address */} +
+ +

{address}

- {chains.length > 0 ? ( - - {chains.map(({ chainId, status }) => ( - - ))} -
+ + {results.length > 0 ? ( +
+ + + + + + + + + + + + {results.map((contract) => ( + + ))} + +
ChainCreation MatchRuntime MatchVerified AtSource
+
) : ( -
This contract has not yet been deployed on any chain
+
+

+ The contract at{" "} + {address} is not + verified on Sourcify. +

+

Do you have the source code?

+ + Verify Contract + +
)} -
-

Not verified on the chain you are looking for?

- - - - -
-
- ); -}; -const NotFound = ({ address, goBack }: NotFoundProp) => { - return ( - <> -
-

- The contract at address {address} is not verified on Sourcify. -

-
-
-

Do you have the source code and metadata?

- - - - -
- - ); -}; - -const verificationIcon = (status: string | undefined) => { - if (status === "false") { - return ; - } - return ; -}; - -const Result = ({ response, goBack }: ResultProp) => { - return ( -
- goBack()} /> -
- {verificationIcon(response?.status)} - {!!response && response?.status !== "false" ? ( - - ) : ( - - )} -
); }; diff --git a/src/pages/Lookup/index.tsx b/src/pages/Lookup/index.tsx index 0bc7c90..79ee994 100644 --- a/src/pages/Lookup/index.tsx +++ b/src/pages/Lookup/index.tsx @@ -1,9 +1,10 @@ -import { useContext, useEffect, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import Header from "../../components/Header"; +import PageLayout from "../../components/PageLayout"; import Toast from "../../components/Toast"; import { Context } from "../../Context"; -import { CheckAllByAddressResult } from "../../types"; -import { checkAllByAddresses } from "../../utils/api"; +import { AllChainsResponse } from "../../types"; +import { getVerifiedContractAllChains } from "../../utils/api"; import Field from "./Field"; import Result from "./Result"; import { useParams, useNavigate } from "react-router-dom"; @@ -12,67 +13,67 @@ import { isAddress, getAddress } from "@ethersproject/address"; const Lookup = () => { const navigate = useNavigate(); const [loading, setLoading] = useState(false); - const [response, setResponse] = useState(undefined); + const [response, setResponse] = useState(undefined); + const [displayAddress, setDisplayAddress] = useState(undefined); + const queriedAddressRef = useRef(undefined); const { address } = useParams(); - const { sourcifyChains, errorMessage, setErrorMessage } = useContext(Context); + const { errorMessage, setErrorMessage } = useContext(Context); - const handleRequest = async (_address: string) => { - if (!sourcifyChains?.length) { - return; - } - setLoading(true); - try { - const result = await checkAllByAddresses( - _address, - `0,${sourcifyChains.map((c) => c.chainId.toString()).join(",")}` - ); - const currentAddressMatches = result.find((match) => (match.address = _address)); - setResponse(currentAddressMatches); - navigate(`/lookup/${_address}`); - } catch (err: any) { - setErrorMessage(err.message || "An error occurred, try again!"); - setErrorMessage("")} />; - } finally { - setLoading(false); - } - }; + const handleRequest = useCallback( + async (_address: string) => { + queriedAddressRef.current = _address; + setLoading(true); + try { + const result = await getVerifiedContractAllChains(_address); + setResponse(result); + setDisplayAddress(_address); + navigate(`/address/${_address}`); + } catch (err: any) { + queriedAddressRef.current = undefined; + setErrorMessage(err.message || "An error occurred, try again!"); + } finally { + setLoading(false); + } + }, + [navigate, setErrorMessage] + ); const goBack = () => { + queriedAddressRef.current = undefined; setResponse(undefined); - navigate(`/lookup`); + setDisplayAddress(undefined); + navigate("/address"); }; + // Only depends on the URL param — state changes from goBack cannot re-trigger this. useEffect(() => { - if (address && address !== response?.address) { - if (!isAddress(address)) { - return; - } - // Get checksummed format - const checksummedAddress = getAddress(address); - handleRequest(checksummedAddress); + if (!address) { + queriedAddressRef.current = undefined; + setResponse(undefined); + setDisplayAddress(undefined); + return; + } + if (queriedAddressRef.current === address) return; + if (!isAddress(address)) { + setErrorMessage("Invalid contract address in URL"); + return; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sourcifyChains, address]); + const checksummedAddress = getAddress(address); + queriedAddressRef.current = checksummedAddress; + handleRequest(checksummedAddress); + }, [address, handleRequest, setErrorMessage]); return ( -
+
-
- setErrorMessage("")} /> -
-

Contract Lookup

-

Look for verified contracts in the Sourcify repository

-
-
-
- {!!response ? ( - - ) : ( - - )} -
-
-
+ setErrorMessage("")} /> + + {response ? ( + + ) : ( + + )} +
); }; diff --git a/src/types.ts b/src/types.ts index 34dcd0c..feae48f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,15 +1,20 @@ -export type CheckAllByAddressResult = { +export interface VerifiedContractMinimal { + match: "match" | "exact_match" | null; + creationMatch: "match" | "exact_match" | null; + runtimeMatch: "match" | "exact_match" | null; + chainId: string; address: string; - status?: string; - chainIds: { - chainId: string; - status: string; - }[]; -}; + verifiedAt: string; + matchId: string; +} + +export interface AllChainsResponse { + results: VerifiedContractMinimal[]; +} export type Chain = { name: string; - title?: string; // Longer name for some networks + title?: string; chainId: number; shortName: string; network: string; diff --git a/src/utils/api.tsx b/src/utils/api.tsx index 8e7f763..7ecc86b 100644 --- a/src/utils/api.tsx +++ b/src/utils/api.tsx @@ -1,48 +1,13 @@ import { SERVER_URL, BIGQUERY_API_URL } from "../constants"; -import { Chain, CheckAllByAddressResult } from "../types"; +import { AllChainsResponse, Chain } from "../types"; -type ChainIdsResponse = { - chainId: string; - status: string; +export const getVerifiedContractAllChains = async (address: string): Promise => { + const response = await fetch(`${SERVER_URL}/v2/contract/all-chains/${address}`); + if (response.status === 404) return { results: [] }; + if (!response.ok) throw new Error(`Lookup failed: ${response.status} ${response.statusText}`); + return response.json(); }; -export type ServersideAddressCheck = { - address: string; - status: string; - chainIds?: ChainIdsResponse[]; -}; - -export const checkAllByAddresses = async ( - addresses: string, - chainIds: string -): Promise => { - const response = await fetch( - `${SERVER_URL}/checkAllByAddresses?addresses=${addresses}&chainIds=${chainIds}`, - { - method: "GET", - } - ); - - if (!response.ok) { - // e.g. HTTP 400 invalid address - let jsonError; - try { - jsonError = await response.json(); - } catch (e) { - throw new Error("Cannot parse the error message"); - } - throw new Error(jsonError.message); - } - - return await response.json(); -}; - -/** - * @function to fetch Sourcify's chains array and return as an object with the chainId as keys. - * - * The Ethereum networks are placed on top, the rest of the networks are sorted alphabetically. - * - */ export const getSourcifyChains = async (): Promise => { const chainsArray = await (await fetch(`${SERVER_URL}/chains`)).json(); return chainsArray; @@ -62,9 +27,7 @@ export interface BigQueryResponse { rows: any[]; } -export const bigquery = async ( - sql: string -): Promise => { +export const bigquery = async (sql: string): Promise => { const response = await fetch(`${BIGQUERY_API_URL}`, { method: "POST", headers: { @@ -74,7 +37,6 @@ export const bigquery = async ( }); if (!response.ok) { - // e.g. HTTP 400 invalid address let jsonError; try { jsonError = await response.json(); diff --git a/tailwind.config.js b/tailwind.config.js index 52dda17..a83051b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -7,6 +7,7 @@ module.exports = { extend: { colors: { ceruleanBlue: { + 50: "#f3f6ff", 100: "#E8EFFF", 200: "#A9BDEE", 300: "#7693DA",