From ca46eb573ae5d184f1c65c06987f4984536a08f6 Mon Sep 17 00:00:00 2001 From: Timur Badretdinov <4247901+Destiner@users.noreply.github.com> Date: Sun, 15 Jun 2025 19:35:46 +0100 Subject: [PATCH] Improve verification error logging --- app/app.vue | 19 +++-- app/components/contract/ChainItem.vue | 50 +++++++++++- app/components/contract/ChainList.vue | 19 ++++- app/data/cache.json | 1 - app/pages/contract/[address].vue | 35 ++++++++ app/utils/verification.ts | 28 ++++++- server/api/verify.ts | 111 ++++++++++++++++++++++++++ 7 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 server/api/verify.ts diff --git a/app/app.vue b/app/app.vue index 5b83613..0d18887 100644 --- a/app/app.vue +++ b/app/app.vue @@ -42,14 +42,17 @@ import IconGitHub from '@/components/__common/icon/GitHub.vue'; --font-size-small: 14px; --font-size-medium: 16px; --font-size-big: 20px; - --font-sans: 'Inter Variable', -apple-system, 'BlinkMacSystemFont', - avenir next, avenir, segoe ui, helvetica neue, helvetica, 'Ubuntu', roboto, - noto, arial, sans-serif; - --font-serif: 'Iowan Old Style', 'Apple Garamond', 'Baskerville', - 'Times New Roman', 'Droid Serif', 'Times', 'Source Serif Pro', serif, - 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; - --font-mono: 'Inconsolata Variable', 'Menlo', 'Consolas', 'Monaco', - 'Liberation Mono', 'Lucida Console', monospace; + --font-sans: + 'Inter Variable', -apple-system, 'BlinkMacSystemFont', avenir next, avenir, + segoe ui, helvetica neue, helvetica, 'Ubuntu', roboto, noto, arial, + sans-serif; + --font-serif: + 'Iowan Old Style', 'Apple Garamond', 'Baskerville', 'Times New Roman', + 'Droid Serif', 'Times', 'Source Serif Pro', serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol'; + --font-mono: + 'Inconsolata Variable', 'Menlo', 'Consolas', 'Monaco', 'Liberation Mono', + 'Lucida Console', monospace; } .app { diff --git a/app/components/contract/ChainItem.vue b/app/components/contract/ChainItem.vue index 0007bf8..f219dce 100644 --- a/app/components/contract/ChainItem.vue +++ b/app/components/contract/ChainItem.vue @@ -28,12 +28,20 @@ + + {{ verifying ? 'Verifying...' : 'Verify' }} + diff --git a/app/components/contract/ChainList.vue b/app/components/contract/ChainList.vue index ded05a7..ef3aefb 100644 --- a/app/components/contract/ChainList.vue +++ b/app/components/contract/ChainList.vue @@ -7,6 +7,9 @@ :status="chain.status" :address="address" :verification="chain.verification" + :verifiedChains="verifiedChains" + @verificationUpdate="update(chain.id, $event)" + @log="log" /> @@ -21,13 +24,19 @@ import ChainItem from './ChainItem.vue'; import type { Chain } from '@/utils/chains'; import type { VerificationStatus } from '@/utils/verification'; -const { chains } = defineProps<{ +const emit = defineEmits<{ + (e: 'updateVerification', chain: Chain, status: VerificationStatus): void; + (e: 'log', message: string): void; +}>(); + +const { chains, verifiedChains } = defineProps<{ address: Address; chains: { id: Chain; status: Status; verification: VerificationStatus | null; }[]; + verifiedChains: Chain[]; }>(); const sortedChains = computed(() => { @@ -51,6 +60,14 @@ const sortedChains = computed(() => { return statusToPriority(a.status) - statusToPriority(b.status); }); }); + +function update(chain: Chain, status: VerificationStatus): void { + emit('updateVerification', chain, status); +} + +function log(message: string): void { + emit('log', message); +} diff --git a/app/utils/verification.ts b/app/utils/verification.ts index 6a330e1..a5063eb 100644 --- a/app/utils/verification.ts +++ b/app/utils/verification.ts @@ -9,6 +9,11 @@ interface CheckVerificationResponse { type VerificationStatus = 'verified' | 'unverified' | 'unknown'; +interface VerifyContractResponse { + status: 'ok' | 'error'; + error?: string; +} + async function checkContractVerification( address: Address, chain: Chain, @@ -28,5 +33,26 @@ async function checkContractVerification( return checkResponse.status; } -export { checkContractVerification }; +async function verifyContract( + address: Address, + chain: Chain, + sourceChain: Chain, +): Promise<{ success: boolean; error?: string }> { + const verifyResponse = await ky + .post('/api/verify', { + json: { + chain: chain.toString(), + address, + sourceChain: sourceChain.toString(), + }, + }) + .json(); + + return { + success: verifyResponse.status === 'ok', + error: verifyResponse.error, + }; +} + +export { checkContractVerification, verifyContract }; export type { VerificationStatus }; diff --git a/server/api/verify.ts b/server/api/verify.ts new file mode 100644 index 0000000..80cc431 --- /dev/null +++ b/server/api/verify.ts @@ -0,0 +1,111 @@ +import { defineEventHandler, readBody } from 'h3'; +import ky from 'ky'; +import type { Address } from 'viem'; + +interface VerifyRequestBody { + chain: string; + address: Address; + sourceChain: string; +} + +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; + }[]; +} + +interface VerifySourceCodeResponse { + status: '0' | '1'; + message: string; + result: string; +} + +interface VerifyResponse { + status: 'ok' | 'error'; + error?: string; +} + +export default defineEventHandler(async (event): Promise => { + const etherscanApiKey = process.env.ETHERSCAN_API_KEY; + if (!etherscanApiKey) { + return { status: 'error', error: 'Missing ETHERSCAN_API_KEY' }; + } + + const { chain, address, sourceChain } = + (await readBody(event)) || {}; + + if (!chain || !address || !sourceChain) { + return { status: 'error', error: 'Missing parameters' }; + } + + try { + const source = await ky + .get('https://api.etherscan.io/v2/api', { + searchParams: { + chainid: sourceChain, + module: 'contract', + action: 'getsourcecode', + address, + apikey: etherscanApiKey, + }, + timeout: 5_000, + }) + .json(); + + if (source.status !== '1' || source.message !== 'OK') { + return { + status: 'error', + error: Array.isArray(source.result) + ? JSON.stringify(source.result) + : String(source.result || source.message), + }; + } + + const data = source.result[0]; + if (!data || !data.SourceCode) { + return { status: 'error', error: 'Empty source code' }; + } + + const verify = await ky + .post('https://api.etherscan.io/v2/api', { + searchParams: { + chainid: chain, + module: 'contract', + action: 'verifysourcecode', + apikey: etherscanApiKey, + contractaddress: address, + sourceCode: data.SourceCode, + contractname: data.ContractName, + compilerversion: data.CompilerVersion, + optimizationUsed: data.OptimizationUsed, + runs: data.Runs, + constructorArguements: data.ConstructorArguments, + evmVersion: data.EVMVersion, + licenseType: data.LicenseType, + }, + timeout: 5_000, + }) + .json(); + + if (verify.status === '1') { + return { status: 'ok' }; + } + return { status: 'error', error: verify.result || verify.message }; + } catch (err: unknown) { + return { status: 'error', error: (err as Error).message }; + } +});