diff --git a/app/components/__common/ScanButton.vue b/app/components/__common/ScanButton.vue new file mode 100644 index 0000000..e06d56c --- /dev/null +++ b/app/components/__common/ScanButton.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/app/components/__common/icon/Cross.vue b/app/components/__common/icon/Cross.vue new file mode 100644 index 0000000..95bea01 --- /dev/null +++ b/app/components/__common/icon/Cross.vue @@ -0,0 +1,15 @@ + diff --git a/app/components/__common/icon/QuestionMark.vue b/app/components/__common/icon/QuestionMark.vue new file mode 100644 index 0000000..0e75419 --- /dev/null +++ b/app/components/__common/icon/QuestionMark.vue @@ -0,0 +1,15 @@ + diff --git a/app/components/contract/BlockInfo.vue b/app/components/contract/BlockInfo.vue index f27ad1e..9e72480 100644 --- a/app/components/contract/BlockInfo.vue +++ b/app/components/contract/BlockInfo.vue @@ -1,31 +1,59 @@ 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', + }; + } +});