UNPKG

@nomicfoundation/hardhat-verify

Version:
432 lines 18.2 kB
import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint"; import { createDebug } from "@nomicfoundation/hardhat-utils/debug"; import { ensureError } from "@nomicfoundation/hardhat-utils/error"; import { sleep } from "@nomicfoundation/hardhat-utils/lang"; import { getProxyUrl, getRequest, postFormRequest, shouldUseProxy, } from "@nomicfoundation/hardhat-utils/request"; const log = createDebug("hardhat:verify:etherscan"); export const ETHERSCAN_PROVIDER_NAME = "etherscan"; const VERIFICATION_STATUS_POLLING_SECONDS = 3; export const ETHERSCAN_API_URL = "https://api.etherscan.io/v2/api"; let supportedChainsCache; export class Etherscan { chainId; name; url; apiUrl; apiKey; dispatcherOrDispatcherOptions; pollingIntervalMs; static async resolveConfig({ chainId, networkName, chainDescriptors, verificationProvidersConfig, dispatcher, shouldUseCache = true, }) { const chainDescriptor = chainDescriptors.get(toBigInt(chainId)); let blockExplorerConfig = chainDescriptor?.blockExplorers.etherscan; if (blockExplorerConfig === undefined) { const supportedChains = await Etherscan.getSupportedChains(dispatcher, shouldUseCache); blockExplorerConfig = supportedChains.get(toBigInt(chainId)) ?.blockExplorers.etherscan; } if (blockExplorerConfig === undefined) { if (chainDescriptor === undefined) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.NETWORK_NOT_SUPPORTED, { networkName, chainId, }); } throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.BLOCK_EXPLORER_NOT_CONFIGURED, { verificationProvider: "Etherscan", chainId, }); } return { blockExplorerConfig, verificationProviderConfig: verificationProvidersConfig.etherscan, chainId, dispatcher, }; } static async create({ blockExplorerConfig, verificationProviderConfig, chainId, dispatcher, }) { return new Etherscan({ chainId, ...blockExplorerConfig, apiKey: await verificationProviderConfig.apiKey.get(), dispatcher, }); } static async getSupportedChains(dispatcher, shouldUseCache = true) { if (supportedChainsCache !== undefined && shouldUseCache) { return supportedChainsCache; } const supportedChains = new Map(); try { const response = await getRequest("https://api.etherscan.io/v2/chainlist", undefined, dispatcher); const responseBody = await response.body.json(); const chainListData = responseBody.result; for (const chain of chainListData) { const chainId = toBigInt(chain.chainid); supportedChains.set(chainId, { name: chain.chainname, chainType: "generic", blockExplorers: { etherscan: { url: chain.blockexplorer, }, }, }); } } catch (error) { // ignore errors log("Failed to fetch supported chains from Etherscan"); log(error); return new Map(); } if (shouldUseCache) { supportedChainsCache = supportedChains; } return supportedChains; } constructor(etherscanConfig) { this.chainId = String(etherscanConfig.chainId); this.name = etherscanConfig.name ?? "Etherscan"; this.url = etherscanConfig.url; this.apiUrl = etherscanConfig.apiUrl ?? ETHERSCAN_API_URL; const proxyUrl = shouldUseProxy(this.apiUrl) ? getProxyUrl(this.apiUrl) : undefined; this.dispatcherOrDispatcherOptions = etherscanConfig.dispatcher ?? (proxyUrl !== undefined ? { proxy: proxyUrl } : {}); this.pollingIntervalMs = etherscanConfig.dispatcher !== undefined ? 0 : VERIFICATION_STATUS_POLLING_SECONDS; if (etherscanConfig.apiKey === "") { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_API_KEY_EMPTY, { verificationProvider: this.name, }); } this.apiKey = etherscanConfig.apiKey; } getContractUrl(address) { return `${this.url}/address/${address}#code`; } async isVerified(address) { let response; let responseBody; try { response = await getRequest(this.apiUrl, { queryParams: { module: "contract", action: "getsourcecode", chainid: this.chainId, apikey: this.apiKey, address, }, }, this.dispatcherOrDispatcherOptions); responseBody = /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Cast to EtherscanGetSourceCodeResponse because that's what we expect from the API TODO: check if the API returns a different type and throw an error if it does */ (await response.body.json()); } catch (error) { ensureError(error); 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, }); } const isSuccessStatusCode = response.statusCode >= 200 && response.statusCode <= 299; if (!isSuccessStatusCode) { // TODO: we should consider throwing EXPLORER_REQUEST_FAILED here too throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_STATUS_CODE_ERROR, { name: this.name, url: this.apiUrl, statusCode: response.statusCode, errorMessage: responseBody.result, }); } if (responseBody.status !== "1") { return false; } const sourceCode = responseBody.result[0]?.SourceCode; return typeof sourceCode === "string" && sourceCode !== ""; } async verify({ contractAddress, compilerInput, contractName, compilerVersion, constructorArguments, }) { const body = { contractaddress: contractAddress, sourceCode: JSON.stringify(compilerInput), codeformat: "solidity-standard-json-input", contractname: contractName, compilerversion: compilerVersion, constructorArguments, }; let response; let responseBody; try { response = await postFormRequest(this.apiUrl, body, { queryParams: { module: "contract", action: "verifysourcecode", chainid: this.chainId, apikey: this.apiKey, }, }, this.dispatcherOrDispatcherOptions); responseBody = /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Cast to EtherscanResponse because that's what we expect from the API TODO: check if the API returns a different type and throw an error if it does */ (await response.body.json()); } catch (error) { ensureError(error); 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, }); } const isSuccessStatusCode = response.statusCode >= 200 && response.statusCode <= 299; if (!isSuccessStatusCode) { // TODO: we should consider throwing EXPLORER_REQUEST_FAILED here too throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_STATUS_CODE_ERROR, { name: this.name, url: this.apiUrl, statusCode: response.statusCode, errorMessage: responseBody.result, }); } const etherscanResponse = new EtherscanVerificationResponse(responseBody); if (etherscanResponse.isBytecodeMissingInNetworkError()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_MISSING_BYTECODE, { url: this.apiUrl, address: contractAddress, }); } if (etherscanResponse.isAlreadyVerified()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_ALREADY_VERIFIED, { contract: contractName, address: contractAddress, }); } if (!etherscanResponse.isOk()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_REQUEST_FAILED, { message: etherscanResponse.message }); } return etherscanResponse.message; } async pollVerificationStatus(guid, contractAddress, contractName) { let response; let responseBody; try { response = await getRequest(this.apiUrl, { queryParams: { module: "contract", action: "checkverifystatus", chainid: this.chainId, apikey: this.apiKey, guid, }, }, this.dispatcherOrDispatcherOptions); responseBody = /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Cast to EtherscanResponse because that's what we expect from the API TODO: check if the API returns a different type and throw an error if it does */ (await response.body.json()); } catch (error) { ensureError(error); 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, }); } const isSuccessStatusCode = response.statusCode >= 200 && response.statusCode <= 299; if (!isSuccessStatusCode) { // TODO: we should consider throwing EXPLORER_REQUEST_FAILED here too throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_STATUS_CODE_ERROR, { name: this.name, url: this.apiUrl, statusCode: response.statusCode, errorMessage: responseBody.result, }); } const etherscanResponse = new EtherscanVerificationStatusResponse(responseBody); if (etherscanResponse.isPending()) { await sleep(this.pollingIntervalMs); return await this.pollVerificationStatus(guid, contractAddress, contractName); } if (etherscanResponse.isAlreadyVerified()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_ALREADY_VERIFIED, { contract: contractName, address: contractAddress, }); } if (etherscanResponse.isFailure() || etherscanResponse.isSuccess()) { return { success: etherscanResponse.isSuccess(), message: etherscanResponse.message, }; } if (!etherscanResponse.isOk()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_STATUS_POLLING_FAILED, { message: etherscanResponse.message }); } // 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: etherscanResponse.message }); } async customApiCall(params, options = { method: "GET" }) { const queryParams = { chainid: this.chainId, apikey: this.apiKey, ...params, }; let response; try { if (options.method === "GET") { response = await getRequest(this.apiUrl, { queryParams }, this.dispatcherOrDispatcherOptions); } else { response = await postFormRequest(this.apiUrl, options.body, { queryParams }, this.dispatcherOrDispatcherOptions); } } catch (error) { ensureError(error); 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, }); } const responseBody = /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Cast to EtherscanResponseBody because that's what we expect from the API */ (await response.body.json()); const isSuccessStatusCode = response.statusCode >= 200 && response.statusCode <= 299; if (!isSuccessStatusCode) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_STATUS_CODE_ERROR, { name: this.name, url: this.apiUrl, statusCode: response.statusCode, errorMessage: String(responseBody.result), }); } return responseBody; } } class EtherscanVerificationResponse { status; message; constructor(response) { this.status = Number(response.status); this.message = response.result; } isBytecodeMissingInNetworkError() { return this.message.startsWith("Unable to locate ContractCode at"); } isAlreadyVerified() { return (this.message.startsWith("Contract source code already verified") || this.message.startsWith("Already Verified")); } isOk() { return this.status === 1; } } class EtherscanVerificationStatusResponse { status; message; constructor(response) { this.status = Number(response.status); this.message = response.result; } isPending() { return this.message === "Pending in queue"; } isFailure() { return this.message.startsWith("Fail - Unable to verify"); } isSuccess() { return this.message === "Pass - Verified"; } isAlreadyVerified() { return (this.message.startsWith("Contract source code already verified") || this.message.startsWith("Already Verified")); } isOk() { return this.status === 1; } } export class LazyEtherscanImpl { #provider; #networkName; #chainDescriptors; #verificationProvidersConfig; #etherscan; constructor(provider, networkName, chainDescriptors, verificationProvidersConfig) { this.#provider = provider; this.#networkName = networkName; this.#chainDescriptors = chainDescriptors; this.#verificationProvidersConfig = verificationProvidersConfig; } /** * Lazily initializes the underlying Etherscan verification provider and caches * the created instance so that subsequent calls reuse the same object. */ async #getEtherscan() { if (this.#etherscan === undefined) { const { createVerificationProviderInstance } = await import("./verification.js"); this.#etherscan = /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Cast to Etherscan because we know the provider name is "etherscan" */ (await createVerificationProviderInstance({ provider: this.#provider, networkName: this.#networkName, chainDescriptors: this.#chainDescriptors, verificationProviderName: "etherscan", verificationProvidersConfig: this.#verificationProvidersConfig, })); } return this.#etherscan; } async getChainId() { const etherscan = await this.#getEtherscan(); return etherscan.chainId; } async getName() { const etherscan = await this.#getEtherscan(); return etherscan.name; } async getUrl() { const etherscan = await this.#getEtherscan(); return etherscan.url; } async getApiUrl() { const etherscan = await this.#getEtherscan(); return etherscan.apiUrl; } async getApiKey() { const etherscan = await this.#getEtherscan(); return etherscan.apiKey; } async getContractUrl(address) { const etherscan = await this.#getEtherscan(); return etherscan.getContractUrl(address); } async isVerified(address) { const etherscan = await this.#getEtherscan(); return await etherscan.isVerified(address); } async verify(args) { const etherscan = await this.#getEtherscan(); return await etherscan.verify(args); } async pollVerificationStatus(guid, contractAddress, contractName) { const etherscan = await this.#getEtherscan(); return await etherscan.pollVerificationStatus(guid, contractAddress, contractName); } async customApiCall(params, options) { const etherscan = await this.#getEtherscan(); return await etherscan.customApiCall(params, options); } } //# sourceMappingURL=etherscan.js.map