@nomicfoundation/hardhat-verify
Version:
Hardhat plugin for verifying contracts
432 lines • 18.2 kB
JavaScript
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