UNPKG

@nomicfoundation/hardhat-verify

Version:
451 lines (398 loc) 12.4 kB
import type { SourcifyErrorResponse, SourcifyLookupResponse, SourcifyVerificationStatusResponse, SourcifyVerificationResponse, } from "./sourcify.types.js"; import type { VerificationProvider, VerificationStatusResponse, BaseVerifyFunctionArgs, CreateSourcifyOptions, ResolveConfigOptions, } from "./types.js"; import type { Dispatcher, DispatcherOptions, HttpResponse, } from "@nomicfoundation/hardhat-utils/request"; import type { ChainDescriptorsConfig, VerificationProvidersConfig, } from "hardhat/types/config"; import type { CompilerInput } from "hardhat/types/solidity"; import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { ensureError } from "@nomicfoundation/hardhat-utils/error"; import { isObject, sleep } from "@nomicfoundation/hardhat-utils/lang"; import { getProxyUrl, getRequest, postJsonRequest, ResponseStatusCodeError, shouldUseProxy, } from "@nomicfoundation/hardhat-utils/request"; export const SOURCIFY_PROVIDER_NAME: keyof VerificationProvidersConfig = "sourcify"; const VERIFICATION_STATUS_POLLING_SECONDS = 3; export const SOURCIFY_API_URL = "https://sourcify.dev/server"; export interface SourcifyVerifyFunctionArgs extends BaseVerifyFunctionArgs { creationTxHash?: string; } export class Sourcify implements VerificationProvider { public readonly chainId: string; public readonly name: string; public readonly url: string; public readonly apiUrl: string; public readonly dispatcherOrDispatcherOptions?: | Dispatcher | DispatcherOptions; public readonly pollingIntervalMs: number; public static async resolveConfig({ chainId, verificationProvidersConfig, dispatcher, }: ResolveConfigOptions): Promise<CreateSourcifyOptions> { return { verificationProviderConfig: verificationProvidersConfig.sourcify, chainId, dispatcher, }; } public static async create({ verificationProviderConfig, chainId, dispatcher, }: CreateSourcifyOptions): Promise<Sourcify> { return new Sourcify({ chainId, apiUrl: verificationProviderConfig.apiUrl, dispatcher, }); } // Not used by sourcify, but required by the VerificationProvider interface public static async getSupportedChains(): Promise<ChainDescriptorsConfig> { return new Map(); } constructor(sourcifyConfig: { chainId: number; name?: string; apiUrl?: string; dispatcher?: Dispatcher; }) { this.chainId = String(sourcifyConfig.chainId); this.name = sourcifyConfig.name ?? "Sourcify"; this.apiUrl = sourcifyConfig.apiUrl ?? SOURCIFY_API_URL; this.url = `${this.apiUrl}/repo-ui`; const proxyUrl = shouldUseProxy(this.apiUrl) ? getProxyUrl(this.apiUrl) : undefined; this.dispatcherOrDispatcherOptions = sourcifyConfig.dispatcher ?? (proxyUrl !== undefined ? { proxy: proxyUrl } : {}); this.pollingIntervalMs = sourcifyConfig.dispatcher !== undefined ? 0 : VERIFICATION_STATUS_POLLING_SECONDS; } public getContractUrl(address: string): string { return `${this.url}/${this.chainId}/${address}`; } public getVerificationJobUrl(guid: string): string { return `${this.apiUrl}/verify-ui/jobs/${guid}`; } public async isVerified(address: string): Promise<boolean> { let response: HttpResponse; let responseBody: SourcifyLookupResponse | SourcifyErrorResponse; try { response = await getRequest( `${this.apiUrl}/v2/contract/${this.chainId}/${address}`, undefined, this.dispatcherOrDispatcherOptions, ); responseBody = await response.body.json(); } catch (error) { ensureError(error); if ( error instanceof ResponseStatusCodeError && isSourcifyLookupResponse(error.body) ) { // Unverified contracts are returned with status 404 return error.body.match !== null; } if ( error instanceof ResponseStatusCodeError && isSourcifyErrorResponse(error.body) ) { throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_STATUS_CODE_ERROR, { name: this.name, url: this.apiUrl, statusCode: error.statusCode, errorMessage: error.body.message, }, ); } throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_FAILED, { name: this.name, url: this.apiUrl, errorMessage: error.cause instanceof Error ? error.cause.message : error.message, }, ); } if (!isSourcifyLookupResponse(responseBody)) { throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_UNEXPECTED_RESPONSE, { message: JSON.stringify(responseBody) }, ); } return responseBody.match !== null; } public async verify({ contractAddress, compilerInput, contractName, compilerVersion, creationTxHash, }: SourcifyVerifyFunctionArgs): Promise<string> { const body: { stdJsonInput: CompilerInput; contractIdentifier: string; compilerVersion: string; creationTransactionHash?: string; } = { stdJsonInput: compilerInput, contractIdentifier: contractName, compilerVersion, }; if (creationTxHash !== undefined) { body.creationTransactionHash = creationTxHash; } let response: HttpResponse; let responseBody: SourcifyVerificationResponse | SourcifyErrorResponse; try { response = await postJsonRequest( `${this.apiUrl}/v2/verify/${this.chainId}/${contractAddress}`, body, undefined, this.dispatcherOrDispatcherOptions, ); responseBody = await response.body.json(); } catch (error) { ensureError(error); if ( error instanceof ResponseStatusCodeError && isSourcifyErrorResponse(error.body) ) { if (error.body.customCode === "already_verified") { throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_ALREADY_VERIFIED, { contract: contractName, address: contractAddress, }, ); } throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_REQUEST_FAILED, { message: error.body.message }, ); } throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_FAILED, { name: this.name, url: this.apiUrl, errorMessage: error.cause instanceof Error ? error.cause.message : error.message, }, ); } if (!isSourcifyVerificationResponse(responseBody)) { throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_UNEXPECTED_RESPONSE, { message: JSON.stringify(responseBody) }, ); } return responseBody.verificationId; } public async pollVerificationStatus( guid: string, contractAddress: string, contractName: string, ): Promise<{ success: boolean; message: string; }> { let response: HttpResponse; let responseBody: | SourcifyVerificationStatusResponse | SourcifyErrorResponse; try { response = await getRequest( `${this.apiUrl}/v2/verify/${guid}`, undefined, this.dispatcherOrDispatcherOptions, ); responseBody = await response.body.json(); } catch (error) { ensureError(error); if ( error instanceof ResponseStatusCodeError && isSourcifyErrorResponse(error.body) ) { throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_STATUS_POLLING_FAILED, { message: error.body.message }, ); } throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_FAILED, { name: this.name, url: this.apiUrl, errorMessage: error.cause instanceof Error ? error.cause.message : error.message, }, ); } if (!isSourcifyVerificationStatusResponse(responseBody)) { throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_UNEXPECTED_RESPONSE, { message: JSON.stringify(responseBody) }, ); } const verificationStatus = new SourcifyVerificationStatus(responseBody); if (verificationStatus.isPending()) { await sleep(this.pollingIntervalMs); return await this.pollVerificationStatus( guid, contractAddress, contractName, ); } if (verificationStatus.isAlreadyVerified()) { throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_ALREADY_VERIFIED, { contract: contractName, address: contractAddress, }, ); } if (verificationStatus.isBytecodeMissingInNetworkError()) { throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_MISSING_BYTECODE, { url: this.apiUrl, address: contractAddress, }, ); } if (!(verificationStatus.isFailure() || verificationStatus.isSuccess())) { // Reaching this point shouldn't be possible unless the API is behaving in a new way. throw new HardhatError( HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_UNEXPECTED_RESPONSE, { message: verificationStatus.message }, ); } const success = verificationStatus.isSuccess(); let message = verificationStatus.message; if (!success) { message = `${message} More info at: ${this.getVerificationJobUrl(guid)}`; } return { success, message, }; } } function isSourcifyErrorResponse( response: unknown, ): response is SourcifyErrorResponse { return ( isObject(response) && "customCode" in response && "message" in response && "errorId" in response ); } function isSourcifyLookupResponse( response: unknown, ): response is SourcifyLookupResponse { return ( isObject(response) && "match" in response && "creationMatch" in response && "runtimeMatch" in response && "chainId" in response && "address" in response ); } function isSourcifyVerificationResponse( response: unknown, ): response is SourcifyVerificationResponse { return isObject(response) && "verificationId" in response; } function isSourcifyVerificationStatusResponse( response: unknown, ): response is SourcifyVerificationStatusResponse { return ( isObject(response) && "isJobCompleted" in response && "verificationId" in response && "jobStartTime" in response && "contract" in response ); } class SourcifyVerificationStatus implements VerificationStatusResponse { public readonly response: SourcifyVerificationStatusResponse; constructor(response: SourcifyVerificationStatusResponse) { this.response = response; } public get message(): string { if (!this.response.isJobCompleted) { return "Pending in queue"; } if (this.response.error !== undefined) { return this.response.error.message; } return `Contract verified with status "${this.response.contract.match}"`; } public isPending(): boolean { return !this.response.isJobCompleted; } public isFailure(): boolean { return this.response.isJobCompleted && this.response.error !== undefined; } public isSuccess(): boolean { return ( this.response.isJobCompleted && this.response.error === undefined && this.response.contract.match !== null ); } public isBytecodeMissingInNetworkError(): boolean { return ( this.response.isJobCompleted && this.response.error?.customCode === "contract_not_deployed" ); } public isAlreadyVerified(): boolean { return ( this.response.isJobCompleted && this.response.error?.customCode === "already_verified" ); } /** * SourcifyVerificationStatusResponse represents a successful verification, * so this always returns true. */ public isOk(): boolean { return true; } }