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 &&
}
-
+
+ {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 ? (
+
+
+
+
+ | Chain |
+ Creation Match |
+ Runtime Match |
+ Verified At |
+ Source |
+
+
+
+ {results.map((contract) => (
+
+ ))}
+
+
+
) : (
-
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",