-
exact bytecode match
-
different bytecode
-
failed to get code
-
not deployed (no code)
-
querying
+
+
+ exact bytecode match
+ different bytecode
+ failed to get code
+ not deployed (no code)
+ querying
+
+
+
verified
+
unverified
+
unknown
+
diff --git a/app/components/contract/ChainItem.vue b/app/components/contract/ChainItem.vue
index 43a4516..0007bf8 100644
--- a/app/components/contract/ChainItem.vue
+++ b/app/components/contract/ChainItem.vue
@@ -1,6 +1,21 @@
-
-
+
+
+
+
+
@@ -18,12 +19,14 @@ import type { Status } from './BlockStatus.vue';
import ChainItem from './ChainItem.vue';
import type { Chain } from '@/utils/chains';
+import type { VerificationStatus } from '@/utils/verification';
const { chains } = defineProps<{
address: Address;
chains: {
id: Chain;
status: Status;
+ verification: VerificationStatus | null;
}[];
}>();
diff --git a/app/pages/contract/[address].vue b/app/pages/contract/[address].vue
index 507f722..b3b32b2 100644
--- a/app/pages/contract/[address].vue
+++ b/app/pages/contract/[address].vue
@@ -15,6 +15,20 @@
+
+
+ Check verification status
+
+
+ Checking {{ checkedVerifications }} / {{ CHAINS.length }}
+
+
{
- chains.value = getInitialChainStatus();
- fetchCode();
- },
- {
- immediate: true,
- },
+const checkingVerification = ref(false);
+const verificationStatus = ref>({});
+const checkedVerifications = computed(
+ () => Object.keys(verificationStatus.value).length,
);
+async function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+async function checkVerification(): Promise {
+ verificationStatus.value = {};
+ checkingVerification.value = true;
+ for (const chain of CHAINS) {
+ const codeHash = codeHashes.value[chain];
+ const status = codeHash
+ ? await checkContractVerification(address.value, chain)
+ : null;
+ if (status !== null) {
+ await sleep(500);
+ }
+ verificationStatus.value[chain] = status;
+ }
+ checkingVerification.value = false;
+}
async function getCodeHash(chain: Chain): Promise {
const cachedCodeHash =
@@ -90,30 +118,44 @@ async function getCodeHash(chain: Chain): Promise {
return code;
}
+const codeHashes = ref>>({});
+function getCodeStatus(
+ referenceCodeHash: Hex | undefined,
+ codeHash: Hex | null | undefined,
+): Status {
+ return codeHash
+ ? referenceCodeHash === codeHash
+ ? 'success'
+ : 'warning'
+ : codeHash === null
+ ? 'empty'
+ : 'error';
+}
+const chains = computed(() => {
+ // Use the first non-null code hash as reference
+ const referenceCodeHash = Object.values(codeHashes.value).find(
+ (codeHash) => codeHash !== null && codeHash !== undefined,
+ );
+ return CHAINS.map((chain) => ({
+ id: chain,
+ status:
+ chain in codeHashes.value
+ ? getCodeStatus(referenceCodeHash, codeHashes.value[chain])
+ : 'progress',
+ verification: verificationStatus.value[chain] || null,
+ }));
+});
+watch(
+ address,
+ () => {
+ fetchCode();
+ },
+ {
+ immediate: true,
+ },
+);
async function fetchCode(): Promise {
- function processCodeHash(
- chain: Chain,
- codeHash: Hex | null | undefined,
- ): void {
- if (!referenceCodeHash && codeHash) {
- referenceCodeHash = codeHash;
- }
- const status = codeHash
- ? referenceCodeHash === codeHash
- ? 'success'
- : 'warning'
- : codeHash === null
- ? 'empty'
- : 'error';
- const chainIndex = chains.value.findIndex(
- (chainStatus) => chainStatus.id === chain,
- );
- const chainValue = chains.value[chainIndex];
- if (chainValue) {
- chainValue.status = status;
- }
- }
- let referenceCodeHash: Hex | null = null;
+ codeHashes.value = {};
// Get cached code first
const cachedCodeHashes = (
cache as Record>>
@@ -121,7 +163,7 @@ async function fetchCode(): Promise {
for (const chainKey in cachedCodeHashes) {
const chain = parseInt(chainKey) as Chain;
const codeHash = cachedCodeHashes[chain];
- processCodeHash(chain, codeHash);
+ codeHashes.value[chain] = codeHash;
}
// Split chains into batches to query contract code in parallel
const batchSize = 10;
@@ -136,21 +178,11 @@ async function fetchCode(): Promise {
await Promise.all(
batch.map(async (chain) => {
const codeHash = await getCodeHash(chain);
- processCodeHash(chain, codeHash);
+ codeHashes.value[chain] = codeHash;
}),
);
}
}
-
-function getInitialChainStatus(): {
- id: Chain;
- status: Status;
-}[] {
- return CHAINS.map((chain) => ({
- id: chain,
- status: 'progress',
- }));
-}
diff --git a/app/utils/verification.ts b/app/utils/verification.ts
new file mode 100644
index 0000000..6a330e1
--- /dev/null
+++ b/app/utils/verification.ts
@@ -0,0 +1,32 @@
+import ky from 'ky';
+import type { Address } from 'viem';
+
+import type { Chain } from '@/utils/chains';
+
+interface CheckVerificationResponse {
+ status: 'verified' | 'unverified' | 'error';
+}
+
+type VerificationStatus = 'verified' | 'unverified' | 'unknown';
+
+async function checkContractVerification(
+ address: Address,
+ chain: Chain,
+): Promise {
+ const checkResponse = await ky
+ .get('/api/check-verification', {
+ searchParams: {
+ chain: chain.toString(),
+ address,
+ },
+ })
+ .json();
+
+ if (checkResponse.status === 'error') {
+ return 'unknown';
+ }
+ return checkResponse.status;
+}
+
+export { checkContractVerification };
+export type { VerificationStatus };
diff --git a/server/api/check-verification.ts b/server/api/check-verification.ts
new file mode 100644
index 0000000..41d329c
--- /dev/null
+++ b/server/api/check-verification.ts
@@ -0,0 +1,80 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { defineEventHandler, getQuery } from 'h3';
+import ky from 'ky';
+import type { Address } from 'viem';
+
+interface GetSourceCodeResponse {
+ status: '0' | '1';
+ message: 'OK' | 'NOTOK';
+ result: {
+ SourceCode: string;
+ ABI: string;
+ ContractName: string;
+ CompilerVersion: string;
+ OptimizationUsed: string;
+ Runs: string;
+ ConstructorArguments: string;
+ EVMVersion: string;
+ Library: string;
+ LicenseType: string;
+ Proxy: string;
+ Implementation: string;
+ SwarmSource: string;
+ }[];
+}
+
+export default defineEventHandler(async (event) => {
+ const etherscanApiKey = process.env.ETHERSCAN_API_KEY;
+
+ if (!etherscanApiKey) {
+ return {
+ status: 'error',
+ };
+ }
+
+ const { chain, address } = getQuery<{
+ chain: string;
+ address: Address;
+ }>(event);
+
+ try {
+ const json = await ky
+ .get('https://api.etherscan.io/v2/api', {
+ searchParams: {
+ chainid: chain,
+ module: 'contract',
+ action: 'getsourcecode',
+ address,
+ apikey: etherscanApiKey,
+ },
+ timeout: 5_000,
+ })
+ .json();
+
+ if (json.status !== '1') {
+ return {
+ status: 'error',
+ };
+ }
+ if (json.message !== 'OK') {
+ return {
+ status: 'error',
+ };
+ }
+ const resultItem = json.result[0];
+ if (!resultItem) {
+ return {
+ status: 'error',
+ };
+ }
+ const status = resultItem.SourceCode ? 'verified' : 'unverified';
+
+ return {
+ status,
+ };
+ } catch {
+ return {
+ status: 'error',
+ };
+ }
+});