From ad260b044a30da8b25b91b60490fd3d3c5b7f9e6 Mon Sep 17 00:00:00 2001 From: Timur Badretdinov <4247901+Destiner@users.noreply.github.com> Date: Wed, 18 Jun 2025 18:36:52 +0100 Subject: [PATCH] Fix verification form data and lint issues --- app/components/contract/ChainItem.vue | 58 +++++++++++- app/components/contract/ChainList.vue | 21 ++++- app/data/cache.json | 1 - app/pages/contract/[address].vue | 35 ++++++++ app/utils/verification.ts | 32 ++++++- server/api/verify.ts | 123 ++++++++++++++++++++++++++ 6 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 server/api/verify.ts diff --git a/app/components/contract/ChainItem.vue b/app/components/contract/ChainItem.vue index 0007bf8..dcfb24e 100644 --- a/app/components/contract/ChainItem.vue +++ b/app/components/contract/ChainItem.vue @@ -28,15 +28,24 @@ + + {{ verifying ? 'Verifying...' : 'Verify' }} + diff --git a/app/components/contract/ChainList.vue b/app/components/contract/ChainList.vue index ded05a7..0ed332b 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" + :verified-chains="verifiedChains" + @verification-update="(status) => update(chain.id, status)" + @log="log" /> @@ -21,15 +24,23 @@ import ChainItem from './ChainItem.vue'; import type { Chain } from '@/utils/chains'; import type { VerificationStatus } from '@/utils/verification'; -const { chains } = defineProps<{ +const props = defineProps<{ address: Address; chains: { id: Chain; status: Status; verification: VerificationStatus | null; }[]; + verifiedChains: Chain[]; }>(); +const emit = defineEmits<{ + 'update-verification': [chain: Chain, status: VerificationStatus]; + log: [message: string]; +}>(); + +const { chains, verifiedChains } = props; + const sortedChains = computed(() => { function statusToPriority(status: Status): number { switch (status) { @@ -51,6 +62,14 @@ const sortedChains = computed(() => { return statusToPriority(a.status) - statusToPriority(b.status); }); }); + +function update(chain: Chain, status: VerificationStatus): void { + emit('update-verification', chain, status); +} + +function log(message: string): void { + emit('log', message); +} diff --git a/app/utils/verification.ts b/app/utils/verification.ts index 6a330e1..08bcbc1 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,30 @@ async function checkContractVerification( return checkResponse.status; } -export { checkContractVerification }; +async function verifyContract( + address: Address, + chain: Chain, + sourceChain: Chain, +): Promise<{ success: boolean; error?: string }> { + try { + const verifyResponse = await ky + .post('/api/verify', { + json: { + chain: chain.toString(), + address, + sourceChain: sourceChain.toString(), + }, + }) + .json(); + + return { + success: verifyResponse.status === 'ok', + error: verifyResponse.error, + }; + } catch (err: unknown) { + return { success: false, error: (err as Error).message }; + } +} + +export { checkContractVerification, verifyContract }; export type { VerificationStatus }; diff --git a/server/api/verify.ts b/server/api/verify.ts new file mode 100644 index 0000000..46096fa --- /dev/null +++ b/server/api/verify.ts @@ -0,0 +1,123 @@ +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 codeFormat = (() => { + try { + JSON.parse(data.SourceCode); + return 'solidity-standard-json-input'; + } catch { + return 'solidity-single-file'; + } + })(); + + const verify = await ky + .post('https://api.etherscan.io/v2/api', { + searchParams: { + chainid: String(chain), + module: 'contract', + action: 'verifysourcecode', + apikey: etherscanApiKey, + }, + body: new URLSearchParams({ + codeformat: codeFormat, + 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: 20_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 }; + } +});