@nomicfoundation/hardhat-verify
Version:
Hardhat plugin for verifying contracts
311 lines • 13.3 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: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