UNPKG

@nomicfoundation/hardhat-verify

Version:
311 lines 13.3 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:blockscout"); export const BLOCKSCOUT_PROVIDER_NAME = "blockscout"; const VERIFICATION_STATUS_POLLING_SECONDS = 3; let supportedChainsCache; export class Blockscout { name; url; apiUrl; dispatcherOrDispatcherOptions; pollingIntervalMs; static async resolveConfig({ chainId, networkName, chainDescriptors, dispatcher, shouldUseCache = true, }) { const chainDescriptor = chainDescriptors.get(toBigInt(chainId)); let blockExplorerConfig = chainDescriptor?.blockExplorers.blockscout; if (blockExplorerConfig === undefined) { const supportedChains = await Blockscout.getSupportedChains(dispatcher, shouldUseCache); blockExplorerConfig = supportedChains.get(toBigInt(chainId)) ?.blockExplorers.blockscout; } 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: "Blockscout", chainId, }); } return { blockExplorerConfig, dispatcher, }; } static async create({ blockExplorerConfig, dispatcher, }) { return new Blockscout({ ...blockExplorerConfig, dispatcher, }); } static async getSupportedChains(dispatcher, shouldUseCache = true) { if (supportedChainsCache !== undefined && shouldUseCache) { return supportedChainsCache; } const supportedChains = new Map(); try { const response = await getRequest("https://chains.blockscout.com/api/chains", undefined, dispatcher); const chainListData = await response.body.json(); for (const [chainId, chain] of Object.entries(chainListData)) { const blockExplorer = chain.explorers.find((explorer) => explorer.hostedBy.toLowerCase() === "blockscout"); if (blockExplorer === undefined) { continue; } supportedChains.set(toBigInt(chainId), { name: chain.name, chainType: "generic", blockExplorers: { blockscout: { url: blockExplorer.url, apiUrl: `${blockExplorer.url}/api`, }, }, }); } } catch (error) { // ignore errors log("Failed to fetch supported chains from Blockscout"); log(error); return new Map(); } if (shouldUseCache) { supportedChainsCache = supportedChains; } return supportedChains; } constructor(blockscoutConfig) { this.name = blockscoutConfig.name ?? "Blockscout"; this.url = blockscoutConfig.url; this.apiUrl = blockscoutConfig.apiUrl; const proxyUrl = shouldUseProxy(this.apiUrl) ? getProxyUrl(this.apiUrl) : undefined; this.dispatcherOrDispatcherOptions = blockscoutConfig.dispatcher ?? (proxyUrl !== undefined ? { proxy: proxyUrl } : {}); this.pollingIntervalMs = blockscoutConfig.dispatcher !== undefined ? 0 : VERIFICATION_STATUS_POLLING_SECONDS; } 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", address, }, }, this.dispatcherOrDispatcherOptions); responseBody = /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Cast to BlockscoutGetSourceCodeResponse 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", }, }, this.dispatcherOrDispatcherOptions); responseBody = /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Cast to BlockscoutResponse 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 blockscoutResponse = new BlockscoutVerificationResponse(responseBody); if (blockscoutResponse.isBytecodeMissingInNetworkError()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_MISSING_BYTECODE, { url: this.apiUrl, address: contractAddress, }); } if (blockscoutResponse.isAlreadyVerified()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_ALREADY_VERIFIED, { contract: contractName, address: contractAddress, }); } if (blockscoutResponse.addressIsNotAContract()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.ADDRESS_NOT_A_CONTRACT, { verificationProvider: this.name, address: contractAddress, }); } if (!blockscoutResponse.isOk()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_REQUEST_FAILED, { message: blockscoutResponse.message }); } return blockscoutResponse.message; } async pollVerificationStatus(guid, contractAddress, contractName) { let response; let responseBody; try { response = await getRequest(this.apiUrl, { queryParams: { module: "contract", action: "checkverifystatus", guid, }, }, this.dispatcherOrDispatcherOptions); responseBody = /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Cast to BlockscoutResponse 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 blockscoutResponse = new BlockscoutVerificationStatusResponse(responseBody); if (blockscoutResponse.isPending()) { await sleep(this.pollingIntervalMs); return await this.pollVerificationStatus(guid, contractAddress, contractName); } if (blockscoutResponse.isAlreadyVerified()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_ALREADY_VERIFIED, { contract: contractName, address: contractAddress, }); } if (!blockscoutResponse.isOk()) { throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_STATUS_POLLING_FAILED, { message: blockscoutResponse.message }); } if (!(blockscoutResponse.isFailure() || blockscoutResponse.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: blockscoutResponse.message }); } return { success: blockscoutResponse.isSuccess(), message: blockscoutResponse.message, }; } } class BlockscoutVerificationResponse { 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("Smart-contract already verified."); } addressIsNotAContract() { return this.message.startsWith("The address is not a smart contract"); } isOk() { return this.status === 1; } } class BlockscoutVerificationStatusResponse { status; message; constructor(response) { this.status = Number(response.status); this.message = response.result; } isPending() { return this.message === "Pending in queue"; } isFailure() { return this.message === "Fail - Unable to verify"; } isSuccess() { return this.message === "Pass - Verified"; } isAlreadyVerified() { return this.message.startsWith("Smart-contract already verified."); } isOk() { return this.status === 1; } } //# sourceMappingURL=blockscout.js.map