From 03746cf574779f68cb449b76dbd2743e8a146a63 Mon Sep 17 00:00:00 2001 From: Manuel Wedler Date: Thu, 1 Aug 2024 14:24:20 +0200 Subject: [PATCH] Get source code with Blockscout lookup --- .../src/app/Verifiers/BlockscoutVerifier.ts | 38 +++++++++++- .../src/app/Verifiers/EtherscanVerifier.ts | 58 +++++++++++++++---- .../src/app/Verifiers/SourcifyVerifier.ts | 6 +- 3 files changed, 88 insertions(+), 14 deletions(-) diff --git a/apps/contract-verification/src/app/Verifiers/BlockscoutVerifier.ts b/apps/contract-verification/src/app/Verifiers/BlockscoutVerifier.ts index 0658321f24..c408e7dab1 100644 --- a/apps/contract-verification/src/app/Verifiers/BlockscoutVerifier.ts +++ b/apps/contract-verification/src/app/Verifiers/BlockscoutVerifier.ts @@ -1,6 +1,26 @@ +import { SourceFile } from '../types' import { EtherscanVerifier } from './EtherscanVerifier' +// Etherscan and Blockscout return different objects from the getsourcecode method +interface BlockscoutSource { + AdditionalSources: Array<{ SourceCode: string; Filename: string }> + ConstructorArguments: string + OptimizationRuns: number + IsProxy: string + SourceCode: string + ABI: string + ContractName: string + CompilerVersion: string + OptimizationUsed: string + Runs: string + EVMVersion: string + FileName: string + Address: string +} + export class BlockscoutVerifier extends EtherscanVerifier { + LOOKUP_STORE_DIR = 'blockscout-verified' + constructor(apiUrl: string) { // apiUrl and explorerUrl are the same for Blockscout super(apiUrl, apiUrl, undefined) @@ -8,7 +28,23 @@ export class BlockscoutVerifier extends EtherscanVerifier { getContractCodeUrl(address: string): string { const url = new URL(this.explorerUrl + `/address/${address}`) - // url.searchParams.append('tab', 'contract') + url.searchParams.append('tab', 'contract') return url.href } + + processReceivedFiles(source: unknown, contractAddress: string): { sourceFiles: SourceFile[]; targetFilePath?: string } { + const blockscoutSource = source as BlockscoutSource + + const result: SourceFile[] = [] + const filePrefix = `/${this.LOOKUP_STORE_DIR}/${contractAddress}` + + const targetFilePath = `${filePrefix}/${blockscoutSource.FileName}` + result.push({ content: blockscoutSource.SourceCode, path: targetFilePath }) + + for (const additional of blockscoutSource.AdditionalSources ?? []) { + result.push({ content: additional.SourceCode, path: `${filePrefix}/${additional.Filename}` }) + } + + return { sourceFiles: result, targetFilePath } + } } diff --git a/apps/contract-verification/src/app/Verifiers/EtherscanVerifier.ts b/apps/contract-verification/src/app/Verifiers/EtherscanVerifier.ts index 744885091d..f5fc04eb21 100644 --- a/apps/contract-verification/src/app/Verifiers/EtherscanVerifier.ts +++ b/apps/contract-verification/src/app/Verifiers/EtherscanVerifier.ts @@ -1,6 +1,6 @@ import { CompilerAbstract } from '@remix-project/remix-solidity' import { AbstractVerifier } from './AbstractVerifier' -import type { LookupResponse, SubmittedContract, VerificationResponse, VerificationStatus } from '../types' +import type { LookupResponse, SourceFile, SubmittedContract, VerificationResponse, VerificationStatus } from '../types' interface EtherscanRpcResponse { status: '0' | '1' @@ -14,7 +14,31 @@ interface EtherscanCheckStatusResponse { result: 'Pending in queue' | 'Pass - Verified' | 'Fail - Unable to verify' | 'Already Verified' | 'Unknown UID' } +interface EtherscanSource { + 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 EtherscanGetSourceCodeResponse { + status: '0' | '1' + message: string + result: EtherscanSource[] +} + export class EtherscanVerifier extends AbstractVerifier { + LOOKUP_STORE_DIR = 'etherscan-verified' + constructor(apiUrl: string, explorerUrl: string, protected apiKey?: string) { super(apiUrl, explorerUrl) } @@ -184,7 +208,7 @@ export class EtherscanVerifier extends AbstractVerifier { async lookup(contractAddress: string, chainId: string): Promise { const url = new URL(this.apiUrl + '/api') url.searchParams.append('module', 'contract') - url.searchParams.append('action', 'getabi') + url.searchParams.append('action', 'getsourcecode') url.searchParams.append('address', contractAddress) if (this.apiKey) { url.searchParams.append('apikey', this.apiKey) @@ -198,22 +222,36 @@ export class EtherscanVerifier extends AbstractVerifier { throw new Error(responseText) } - const lookupResponse: EtherscanRpcResponse = await response.json() + const lookupResponse: EtherscanGetSourceCodeResponse = await response.json() - const lookupUrl = this.getContractCodeUrl(contractAddress) + if (lookupResponse.status !== '1' || !lookupResponse.message.startsWith('OK')) { + const errorResponse = lookupResponse as unknown as EtherscanRpcResponse + console.error('Error on Etherscan API lookup at ' + this.apiUrl + '\nStatus: ' + errorResponse.status + '\nMessage: ' + errorResponse.message + '\nResult: ' + errorResponse.result) + throw new Error(errorResponse.result) + } - if (lookupResponse.result === 'Contract source code not verified') { - return { status: 'not verified', lookupUrl } - } else if (lookupResponse.status !== '1' || !lookupResponse.message.startsWith('OK')) { - console.error('Error on Etherscan API lookup at ' + this.apiUrl + '\nStatus: ' + lookupResponse.status + '\nMessage: ' + lookupResponse.message + '\nResult: ' + lookupResponse.result) - throw new Error(lookupResponse.result) + if (lookupResponse.result[0].ABI === 'Contract source code not verified' || !lookupResponse.result[0].SourceCode) { + return { status: 'not verified' } } - return { status: 'verified', lookupUrl } + const lookupUrl = this.getContractCodeUrl(contractAddress) + console.log(lookupResponse) + const { sourceFiles, targetFilePath } = this.processReceivedFiles(lookupResponse.result[0], contractAddress) + + return { status: 'verified', lookupUrl, sourceFiles, targetFilePath } } getContractCodeUrl(address: string): string { const url = new URL(this.explorerUrl + `/address/${address}#code`) return url.href } + + processReceivedFiles(source: EtherscanSource, contractAddress: string): { sourceFiles: SourceFile[]; targetFilePath?: string } { + const result: SourceFile[] = [] + const filePrefix = `/${this.LOOKUP_STORE_DIR}/${contractAddress}` + // TODO + const targetFilePath = '' + + return { sourceFiles: result, targetFilePath } + } } diff --git a/apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts b/apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts index 38efa00e04..351cd3e9a2 100644 --- a/apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts +++ b/apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts @@ -43,7 +43,7 @@ interface SourcifyLookupResponse { } export class SourcifyVerifier extends AbstractVerifier { - SOURCIFY_DIR = 'sourcify-verified' + LOOKUP_STORE_DIR = 'sourcify-verified' async verify(submittedContract: SubmittedContract, compilerAbstract: CompilerAbstract): Promise { const metadataStr = compilerAbstract.data.contracts[submittedContract.filePath][submittedContract.contractName].metadata @@ -138,14 +138,14 @@ export class SourcifyVerifier extends AbstractVerifier { } getContractCodeUrl(address: string, chainId: string, fullMatch: boolean): string { - const url = new URL(this.explorerUrl + `/contracts/${fullMatch ? 'full_match' : 'partial_match'}/${chainId}/${address}`) + const url = new URL(this.explorerUrl + `/contracts/${fullMatch ? 'full_match' : 'partial_match'}/${chainId}/${address}/`) return url.href } processReceivedFiles(files: SourcifyFile[], contractAddress: string): { sourceFiles: SourceFile[]; targetFilePath?: string } { const result: SourceFile[] = [] let targetFilePath: string - const filePrefix = `/${this.SOURCIFY_DIR}/${contractAddress}` + const filePrefix = `/${this.LOOKUP_STORE_DIR}/${contractAddress}` for (const file of files) { let filePath: string