From 31028c5392d995f85fa7a3d9905a23d311800bd2 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Thu, 20 Jul 2023 11:41:34 -0300 Subject: [PATCH 01/36] Add sourcify task boilerplate --- packages/hardhat-verify/src/index.ts | 47 ++++++++++++++++-- .../hardhat-verify/src/internal/config.ts | 48 +++++++++++-------- .../hardhat-verify/src/internal/task-names.ts | 9 +++- .../src/internal/type-extensions.ts | 4 +- packages/hardhat-verify/src/types.ts | 5 ++ packages/hardhat-verify/test/helpers.ts | 5 +- .../hardhat-verify/test/integration/index.ts | 14 +++++- packages/hardhat-verify/test/unit/config.ts | 4 ++ 8 files changed, 108 insertions(+), 28 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index 16c061c5ac..88d0199ca2 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -5,6 +5,7 @@ import type { DependencyGraph, } from "hardhat/types"; +import chalk from "chalk"; import { extendConfig, subtask, task, types } from "hardhat/config"; import { isFullyQualifiedName } from "hardhat/utils/contract-names"; import { @@ -22,8 +23,12 @@ import { TASK_VERIFY_VERIFY, TASK_VERIFY_ETHERSCAN, TASK_VERIFY_PRINT_SUPPORTED_NETWORKS, + TASK_VERIFY_SOURCIFY, } from "./internal/task-names"; -import { etherscanConfigExtender } from "./internal/config"; +import { + etherscanConfigExtender, + sourcifyConfigExtender, +} from "./internal/config"; import { MissingAddressError, InvalidAddressError, @@ -111,6 +116,7 @@ interface VerificationResponse { } extendConfig(etherscanConfigExtender); +extendConfig(sourcifyConfigExtender); /** * Main verification task. @@ -210,9 +216,42 @@ subtask(TASK_VERIFY_RESOLVE_ARGUMENTS) /** * Returns a list of verification subtasks. */ -subtask(TASK_VERIFY_GET_VERIFICATION_SUBTASKS, async (): Promise => { - return [TASK_VERIFY_ETHERSCAN]; -}); +subtask( + TASK_VERIFY_GET_VERIFICATION_SUBTASKS, + async (_, { config }): Promise => { + const verificationSubtasks = []; + + if (config.etherscan.enabled) { + verificationSubtasks.push(TASK_VERIFY_ETHERSCAN); + } + + if (config.sourcify.enabled) { + verificationSubtasks.push(TASK_VERIFY_SOURCIFY); + } else { + console.warn( + chalk.yellow( + `WARNING: Skipping Sourcify verification: Sourcify is disabled. To enable it, add this entry to your config: + +sourcify: { + enabled: true +} + +Learn more at https://...` + ) + ); + } + + if (verificationSubtasks.length === 0) { + console.warn( + chalk.yellow( + `WARNING: No verification services are enabled. Please enable at least one verification service in your configuration.` + ) + ); + } + + return verificationSubtasks; + } +); /** * Main Etherscan verification subtask. diff --git a/packages/hardhat-verify/src/internal/config.ts b/packages/hardhat-verify/src/internal/config.ts index ae5a4155fd..7a19513909 100644 --- a/packages/hardhat-verify/src/internal/config.ts +++ b/packages/hardhat-verify/src/internal/config.ts @@ -1,6 +1,6 @@ import type LodashCloneDeepT from "lodash.clonedeep"; import type { HardhatConfig, HardhatUserConfig } from "hardhat/types"; -import type { EtherscanConfig } from "../types"; +import type { EtherscanConfig, SourcifyConfig } from "../types"; import chalk from "chalk"; @@ -8,27 +8,37 @@ export function etherscanConfigExtender( config: HardhatConfig, userConfig: Readonly ): void { - const defaultConfig: EtherscanConfig = { + const defaultEtherscanConfig: EtherscanConfig = { apiKey: "", customChains: [], + enabled: true, }; + const cloneDeep = require("lodash.clonedeep") as typeof LodashCloneDeepT; + const userEtherscanConfig = cloneDeep(userConfig.etherscan); + config.etherscan = { ...defaultEtherscanConfig, ...userEtherscanConfig }; - if (userConfig.etherscan !== undefined) { - const cloneDeep = require("lodash.clonedeep") as typeof LodashCloneDeepT; - const customConfig = cloneDeep(userConfig.etherscan); - - config.etherscan = { ...defaultConfig, ...customConfig }; - } else { - config.etherscan = defaultConfig; - - // check that there is no etherscan entry in the networks object, since - // this is a common mistake made by users - if (config.networks?.etherscan !== undefined) { - console.warn( - chalk.yellow( - "WARNING: you have an 'etherscan' entry in your networks configuration. This is likely a mistake. The etherscan configuration should be at the root of the configuration, not within the networks object." - ) - ); - } + // check that there is no etherscan entry in the networks object, since + // this is a common mistake made by users + if ( + userConfig.etherscan === undefined && + config.networks?.etherscan !== undefined + ) { + console.warn( + chalk.yellow( + "WARNING: you have an 'etherscan' entry in your networks configuration. This is likely a mistake. The etherscan configuration should be at the root of the configuration, not within the networks object." + ) + ); } } + +export function sourcifyConfigExtender( + config: HardhatConfig, + userConfig: Readonly +): void { + const defaultSourcifyConfig: SourcifyConfig = { + enabled: false, + }; + const cloneDeep = require("lodash.clonedeep") as typeof LodashCloneDeepT; + const userSourcifyConfig = cloneDeep(userConfig.sourcify); + config.sourcify = { ...defaultSourcifyConfig, ...userSourcifyConfig }; +} diff --git a/packages/hardhat-verify/src/internal/task-names.ts b/packages/hardhat-verify/src/internal/task-names.ts index 9fc4b15247..9261ecbf13 100644 --- a/packages/hardhat-verify/src/internal/task-names.ts +++ b/packages/hardhat-verify/src/internal/task-names.ts @@ -3,6 +3,10 @@ export const TASK_VERIFY_GET_VERIFICATION_SUBTASKS = "verify:get-verification-subtasks"; export const TASK_VERIFY_RESOLVE_ARGUMENTS = "verify:resolve-arguments"; export const TASK_VERIFY_VERIFY = "verify:verify"; +export const TASK_VERIFY_PRINT_SUPPORTED_NETWORKS = + "verify:print-supported-networks"; + +// Etherscan export const TASK_VERIFY_ETHERSCAN = "verify:etherscan"; export const TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION = "verify:etherscan-get-contract-information"; @@ -10,5 +14,6 @@ export const TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT = "verify:etherscan-get-minimal-input"; export const TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION = "verify:etherscan-attempt-verification"; -export const TASK_VERIFY_PRINT_SUPPORTED_NETWORKS = - "verify:print-supported-networks"; + +// Sourcify +export const TASK_VERIFY_SOURCIFY = "verify:sourcify"; diff --git a/packages/hardhat-verify/src/internal/type-extensions.ts b/packages/hardhat-verify/src/internal/type-extensions.ts index 185192ca4a..df43cdd28d 100644 --- a/packages/hardhat-verify/src/internal/type-extensions.ts +++ b/packages/hardhat-verify/src/internal/type-extensions.ts @@ -1,13 +1,15 @@ -import type { EtherscanConfig } from "../types"; +import type { EtherscanConfig, SourcifyConfig } from "../types"; import "hardhat/types/config"; declare module "hardhat/types/config" { interface HardhatUserConfig { etherscan?: Partial; + sourcify?: Partial; } interface HardhatConfig { etherscan: EtherscanConfig; + sourcify: SourcifyConfig; } } diff --git a/packages/hardhat-verify/src/types.ts b/packages/hardhat-verify/src/types.ts index 566084b86e..405b37df5f 100644 --- a/packages/hardhat-verify/src/types.ts +++ b/packages/hardhat-verify/src/types.ts @@ -10,6 +10,11 @@ export interface ChainConfig { export interface EtherscanConfig { apiKey: ApiKey; customChains: ChainConfig[]; + enabled: boolean; +} + +export interface SourcifyConfig { + enabled: boolean; } export type ApiKey = string | Record; diff --git a/packages/hardhat-verify/test/helpers.ts b/packages/hardhat-verify/test/helpers.ts index 05b58c75ad..24802875be 100644 --- a/packages/hardhat-verify/test/helpers.ts +++ b/packages/hardhat-verify/test/helpers.ts @@ -2,6 +2,7 @@ import type {} from "@nomicfoundation/hardhat-ethers"; import type { FactoryOptions, HardhatRuntimeEnvironment } from "hardhat/types"; import path from "path"; +import debug from "debug"; import { resetHardhatContext } from "hardhat/plugins-testing"; declare module "mocha" { @@ -10,6 +11,8 @@ declare module "mocha" { } } +const log = debug("hardhat:hardhat-verify:tests"); + export const useEnvironment = (fixtureProjectName: string): void => { before("Loading hardhat environment", function () { process.chdir(path.join(__dirname, "fixture-projects", fixtureProjectName)); @@ -36,7 +39,7 @@ export const deployContract = async ( const contract = await factory.deploy(...constructorArguments); await contract.deploymentTransaction()?.wait(confirmations); const contractAddress = await contract.getAddress(); - console.log(`Deployed ${contractName} at ${contractAddress}`); + log(`Deployed ${contractName} at ${contractAddress}`); return contractAddress; }; diff --git a/packages/hardhat-verify/test/integration/index.ts b/packages/hardhat-verify/test/integration/index.ts index 6f6b4c17cd..0e0c05c2c2 100644 --- a/packages/hardhat-verify/test/integration/index.ts +++ b/packages/hardhat-verify/test/integration/index.ts @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; -import sinon from "sinon"; +import sinon, { SinonStub } from "sinon"; import { assert, expect } from "chai"; import { TASK_CLEAN, TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; import { SolcConfig } from "hardhat/types/config"; @@ -19,6 +19,17 @@ describe("verify task integration tests", () => { useEnvironment("hardhat-project"); mockEnvironment(); + // suppress warnings + let warnStub: SinonStub; + before(() => { + warnStub = sinon.stub(console, "warn"); + }); + + // suppress warnings + after(() => { + warnStub.restore(); + }); + it("should return after printing the supported networks", async function () { const logStub = sinon.stub(console, "log"); const taskResponse = await this.hre.run(TASK_VERIFY, { @@ -38,6 +49,7 @@ describe("verify task integration tests", () => { // cleanup the etherscan config since we have hardhat defined as custom chain const originalConfig = this.hre.config.etherscan; this.hre.config.etherscan = { + enabled: true, apiKey: "", customChains: [], }; diff --git a/packages/hardhat-verify/test/unit/config.ts b/packages/hardhat-verify/test/unit/config.ts index 43ac52f8ba..82de9a8f42 100644 --- a/packages/hardhat-verify/test/unit/config.ts +++ b/packages/hardhat-verify/test/unit/config.ts @@ -27,6 +27,7 @@ describe("Chain Config", () => { }, }; const expected: EtherscanConfig = { + enabled: true, apiKey: { goerli: "", }, @@ -49,6 +50,7 @@ describe("Chain Config", () => { it("should override the hardhat config with the user config", async () => { const hardhatConfig = {} as HardhatConfig; hardhatConfig.etherscan = { + enabled: true, apiKey: { goerli: "", }, @@ -90,6 +92,7 @@ describe("Chain Config", () => { }, }; const expected: EtherscanConfig = { + enabled: true, apiKey: { mainnet: "", sepolia: "", @@ -122,6 +125,7 @@ describe("Chain Config", () => { const hardhatConfig = {} as HardhatConfig; const userConfig: HardhatUserConfig = {}; const expected: EtherscanConfig = { + enabled: true, apiKey: "", customChains: [], }; From c3e233b02333f098e0839b57b99e84a74fbc7824 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Thu, 20 Jul 2023 13:11:14 -0300 Subject: [PATCH 02/36] Add tests --- packages/hardhat-verify/test/unit/config.ts | 228 +++++++++++--------- packages/hardhat-verify/test/unit/index.ts | 140 +++++++++++- 2 files changed, 261 insertions(+), 107 deletions(-) diff --git a/packages/hardhat-verify/test/unit/config.ts b/packages/hardhat-verify/test/unit/config.ts index 82de9a8f42..09d0f3b11b 100644 --- a/packages/hardhat-verify/test/unit/config.ts +++ b/packages/hardhat-verify/test/unit/config.ts @@ -1,16 +1,37 @@ import type { HardhatConfig, HardhatUserConfig } from "hardhat/types"; -import type { EtherscanConfig } from "../../src/types"; +import type { EtherscanConfig, SourcifyConfig } from "../../src/types"; import sinon from "sinon"; import { assert, expect } from "chai"; -import { etherscanConfigExtender } from "../../src/internal/config"; +import { + etherscanConfigExtender, + sourcifyConfigExtender, +} from "../../src/internal/config"; -describe("Chain Config", () => { - it("should extend the hardhat config with the user config", async () => { - const hardhatConfig = {} as HardhatConfig; - const userConfig: HardhatUserConfig = { - etherscan: { +describe("Extend config", () => { + describe("Etherscan config extender", () => { + it("should extend the hardhat config with the user config", async () => { + const hardhatConfig = {} as HardhatConfig; + const userConfig: HardhatUserConfig = { + etherscan: { + apiKey: { + goerli: "", + }, + customChains: [ + { + network: "goerli", + chainId: 5, + urls: { + apiURL: "https://api-goerli.etherscan.io/api", + browserURL: "https://goerli.etherscan.io", + }, + }, + ], + }, + }; + const expected: EtherscanConfig = { + enabled: true, apiKey: { goerli: "", }, @@ -24,49 +45,58 @@ describe("Chain Config", () => { }, }, ], - }, - }; - const expected: EtherscanConfig = { - enabled: true, - apiKey: { - goerli: "", - }, - customChains: [ - { - network: "goerli", - chainId: 5, - urls: { - apiURL: "https://api-goerli.etherscan.io/api", - browserURL: "https://goerli.etherscan.io", - }, - }, - ], - }; - etherscanConfigExtender(hardhatConfig, userConfig); + }; + etherscanConfigExtender(hardhatConfig, userConfig); - assert.deepEqual(hardhatConfig.etherscan, expected); - }); + assert.deepEqual(hardhatConfig.etherscan, expected); + }); - it("should override the hardhat config with the user config", async () => { - const hardhatConfig = {} as HardhatConfig; - hardhatConfig.etherscan = { - enabled: true, - apiKey: { - goerli: "", - }, - customChains: [ - { - network: "goerli", - chainId: 5, - urls: { - apiURL: "https://api-goerli.etherscan.io/api", - browserURL: "https://goerli.etherscan.io", + it("should override the hardhat config with the user config", async () => { + const hardhatConfig = {} as HardhatConfig; + hardhatConfig.etherscan = { + enabled: true, + apiKey: { + goerli: "", + }, + customChains: [ + { + network: "goerli", + chainId: 5, + urls: { + apiURL: "https://api-goerli.etherscan.io/api", + browserURL: "https://goerli.etherscan.io", + }, }, + ], + }; + const userConfig: HardhatUserConfig = { + etherscan: { + apiKey: { + mainnet: "", + sepolia: "", + }, + customChains: [ + { + network: "mainnet", + chainId: 1, + urls: { + apiURL: "https://api.etherscan.io/api", + browserURL: "https://etherscan.io", + }, + }, + { + network: "sepolia", + chainId: 11155111, + urls: { + apiURL: "https://api-sepolia.etherscan.io/api", + browserURL: "https://sepolia.etherscan.io", + }, + }, + ], }, - ], - }; - const userConfig: HardhatUserConfig = { - etherscan: { + }; + const expected: EtherscanConfig = { + enabled: true, apiKey: { mainnet: "", sepolia: "", @@ -89,71 +119,59 @@ describe("Chain Config", () => { }, }, ], - }, - }; - const expected: EtherscanConfig = { - enabled: true, - apiKey: { - mainnet: "", - sepolia: "", - }, - customChains: [ - { - network: "mainnet", - chainId: 1, - urls: { - apiURL: "https://api.etherscan.io/api", - browserURL: "https://etherscan.io", - }, - }, - { - network: "sepolia", - chainId: 11155111, - urls: { - apiURL: "https://api-sepolia.etherscan.io/api", - browserURL: "https://sepolia.etherscan.io", - }, - }, - ], - }; - etherscanConfigExtender(hardhatConfig, userConfig); + }; + etherscanConfigExtender(hardhatConfig, userConfig); - assert.deepEqual(hardhatConfig.etherscan, expected); - }); + assert.deepEqual(hardhatConfig.etherscan, expected); + }); - it("should set default values when user config is not provided", async () => { - const hardhatConfig = {} as HardhatConfig; - const userConfig: HardhatUserConfig = {}; - const expected: EtherscanConfig = { - enabled: true, - apiKey: "", - customChains: [], - }; - etherscanConfigExtender(hardhatConfig, userConfig); + it("should set default values when user config is not provided", async () => { + const hardhatConfig = {} as HardhatConfig; + const userConfig: HardhatUserConfig = {}; + const expected: EtherscanConfig = { + enabled: true, + apiKey: "", + customChains: [], + }; + etherscanConfigExtender(hardhatConfig, userConfig); - assert.deepEqual(hardhatConfig.etherscan, expected); - }); + assert.deepEqual(hardhatConfig.etherscan, expected); + }); - it("should display a warning message if there is an etherscan entry in the networks object", async () => { - const warnStub = sinon.stub(console, "warn"); - const hardhatConfig = { - networks: { - etherscan: { - apiKey: { - goerli: "", + it("should display a warning message if there is an etherscan entry in the networks object", async () => { + const warnStub = sinon.stub(console, "warn"); + const hardhatConfig = { + networks: { + etherscan: { + apiKey: { + goerli: "", + }, }, }, - }, - }; - const userConfig: HardhatUserConfig = {}; + }; + const userConfig: HardhatUserConfig = {}; + + // @ts-expect-error + etherscanConfigExtender(hardhatConfig, userConfig); + expect(warnStub).to.be.calledOnceWith( + sinon.match( + /WARNING: you have an 'etherscan' entry in your networks configuration./ + ) + ); + warnStub.restore(); + }); + }); + + describe("Sourcify config extender", () => { + it("should set default values when user config is not provided", async () => { + const hardhatConfig = {} as HardhatConfig; + const userConfig: HardhatUserConfig = {}; + const expected: SourcifyConfig = { + enabled: false, + }; + sourcifyConfigExtender(hardhatConfig, userConfig); - // @ts-expect-error - etherscanConfigExtender(hardhatConfig, userConfig); - expect(warnStub).to.be.calledOnceWith( - sinon.match( - /WARNING: you have an 'etherscan' entry in your networks configuration./ - ) - ); - warnStub.restore(); + assert.deepEqual(hardhatConfig.sourcify, expected); + }); }); }); diff --git a/packages/hardhat-verify/test/unit/index.ts b/packages/hardhat-verify/test/unit/index.ts index 93f179a072..2defe7e24a 100644 --- a/packages/hardhat-verify/test/unit/index.ts +++ b/packages/hardhat-verify/test/unit/index.ts @@ -1,6 +1,11 @@ import { assert, expect } from "chai"; +import sinon, { SinonStub } from "sinon"; + import { + TASK_VERIFY_ETHERSCAN, + TASK_VERIFY_GET_VERIFICATION_SUBTASKS, TASK_VERIFY_RESOLVE_ARGUMENTS, + TASK_VERIFY_SOURCIFY, TASK_VERIFY_VERIFY, } from "../../src/internal/task-names"; import { getRandomAddress, useEnvironment } from "../helpers"; @@ -8,7 +13,7 @@ import { getRandomAddress, useEnvironment } from "../helpers"; describe("verify task", () => { useEnvironment("hardhat-project"); - describe("verify:resolve-arguments", () => { + describe(TASK_VERIFY_RESOLVE_ARGUMENTS, () => { it("should throw if address is not provided", async function () { await expect( this.hre.run(TASK_VERIFY_RESOLVE_ARGUMENTS, { @@ -73,7 +78,7 @@ describe("verify task", () => { }); }); - describe("verify:verify", () => { + describe(TASK_VERIFY_VERIFY, () => { it("should throw if address is not provided", async function () { await expect( this.hre.run(TASK_VERIFY_VERIFY, { @@ -126,4 +131,135 @@ describe("verify task", () => { ).to.be.rejectedWith(/The libraries parameter should be a dictionary./); }); }); + + describe(TASK_VERIFY_GET_VERIFICATION_SUBTASKS, () => { + // suppress warnings + let warnStub: SinonStub; + beforeEach(() => { + warnStub = sinon.stub(console, "warn"); + }); + + // suppress warnings + afterEach(() => { + warnStub.restore(); + }); + + it("should return the etherscan subtask by default", async function () { + const verificationSubtasks: string[] = await this.hre.run( + TASK_VERIFY_GET_VERIFICATION_SUBTASKS + ); + + assert.isTrue(verificationSubtasks.includes(TASK_VERIFY_ETHERSCAN)); + }); + + it("should return the etherscan subtask if it is enabled", async function () { + const originalConfig = this.hre.config.etherscan; + this.hre.config.etherscan = { + enabled: true, + apiKey: "", + customChains: [], + }; + + const verificationSubtasks: string[] = await this.hre.run( + TASK_VERIFY_GET_VERIFICATION_SUBTASKS + ); + + this.hre.config.etherscan = originalConfig; + + assert.isTrue(verificationSubtasks.includes(TASK_VERIFY_ETHERSCAN)); + }); + + it("should ignore the etherscan subtask if it is disabled", async function () { + const originalConfig = this.hre.config.etherscan; + this.hre.config.etherscan = { + enabled: false, + apiKey: "", + customChains: [], + }; + + const verificationSubtasks: string[] = await this.hre.run( + TASK_VERIFY_GET_VERIFICATION_SUBTASKS + ); + + this.hre.config.etherscan = originalConfig; + + assert.isFalse(verificationSubtasks.includes(TASK_VERIFY_ETHERSCAN)); + }); + + it("should ignore the sourcify subtask by default", async function () { + const verificationSubtasks: string[] = await this.hre.run( + TASK_VERIFY_GET_VERIFICATION_SUBTASKS + ); + + assert.isFalse(verificationSubtasks.includes(TASK_VERIFY_SOURCIFY)); + expect(warnStub).to.be.calledOnceWith( + sinon.match( + /WARNING: Skipping Sourcify verification: Sourcify is disabled./ + ) + ); + }); + + it("should return the sourcify subtask if it is enabled", async function () { + const originalConfig = this.hre.config.sourcify; + this.hre.config.sourcify = { + enabled: true, + }; + + const verificationSubtasks: string[] = await this.hre.run( + TASK_VERIFY_GET_VERIFICATION_SUBTASKS + ); + + this.hre.config.sourcify = originalConfig; + + assert.isTrue(verificationSubtasks.includes(TASK_VERIFY_SOURCIFY)); + }); + + it("should ignore the sourcify subtask if it is disabled", async function () { + const originalConfig = this.hre.config.sourcify; + this.hre.config.sourcify = { + enabled: false, + }; + + const verificationSubtasks: string[] = await this.hre.run( + TASK_VERIFY_GET_VERIFICATION_SUBTASKS + ); + + this.hre.config.sourcify = originalConfig; + + assert.isFalse(verificationSubtasks.includes(TASK_VERIFY_SOURCIFY)); + expect(warnStub).to.be.calledOnceWith( + sinon.match( + /WARNING: Skipping Sourcify verification: Sourcify is disabled./ + ) + ); + }); + + it("should provide a warning message if both etherscan and sourcify are disabled", async function () { + const originalEtherscanConfig = this.hre.config.etherscan; + this.hre.config.etherscan = { + enabled: false, + apiKey: "", + customChains: [], + }; + const originalSourcifyConfig = this.hre.config.etherscan; + this.hre.config.sourcify = { + enabled: false, + }; + + const verificationSubtasks: string[] = await this.hre.run( + TASK_VERIFY_GET_VERIFICATION_SUBTASKS + ); + + this.hre.config.etherscan = originalEtherscanConfig; + this.hre.config.sourcify = originalSourcifyConfig; + + assert.equal(verificationSubtasks.length, 0); + assert.isTrue(warnStub.calledTwice); + expect(warnStub).to.be.calledWith( + sinon.match( + /WARNING: Skipping Sourcify verification: Sourcify is disabled./ + ) + ); + }); + }); }); From c5c5a7c4436c8a6c388cced5b3c7b30cd9d8e3c8 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Mon, 24 Jul 2023 15:59:36 -0300 Subject: [PATCH 03/36] Output errors at the end of the verification process --- packages/hardhat-verify/src/index.ts | 317 +++++++++--------- .../hardhat-verify/src/internal/utilities.ts | 44 +++ .../hardhat-verify/test/integration/index.ts | 48 +-- packages/hardhat-verify/test/unit/index.ts | 32 -- .../hardhat-verify/test/unit/utilities.ts | 68 ++++ 5 files changed, 301 insertions(+), 208 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index 88d0199ca2..00578064e8 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -43,6 +43,7 @@ import { UnexpectedNumberOfFilesError, VerificationAPIUnexpectedMessageError, ContractVerificationFailedError, + HardhatVerifyError, } from "./internal/errors"; import { sleep, @@ -51,6 +52,7 @@ import { printSupportedNetworks, resolveConstructorArguments, resolveLibraries, + printVerificationErrors, } from "./internal/utilities"; import { Etherscan } from "./internal/etherscan"; import { Bytecode } from "./internal/solc/bytecode"; @@ -156,62 +158,27 @@ task(TASK_VERIFY, "Verifies a contract on Etherscan") await run(TASK_VERIFY_PRINT_SUPPORTED_NETWORKS); return; } - const verificationArgs: VerificationArgs = await run( - TASK_VERIFY_RESOLVE_ARGUMENTS, - taskArgs - ); const verificationSubtasks: string[] = await run( TASK_VERIFY_GET_VERIFICATION_SUBTASKS ); + const errors: Record = {}; + let hasErrors = false; for (const verificationSubtask of verificationSubtasks) { - await run(verificationSubtask, verificationArgs); - } - }); - -subtask(TASK_VERIFY_RESOLVE_ARGUMENTS) - .addOptionalParam("address") - .addOptionalParam("constructorArgsParams", undefined, [], types.any) - .addOptionalParam("constructorArgs", undefined, undefined, types.inputFile) - .addOptionalParam("libraries", undefined, undefined, types.inputFile) - .addOptionalParam("contract") - .setAction( - async ({ - address, - constructorArgsParams, - constructorArgs: constructorArgsModule, - contract, - libraries: librariesModule, - }: VerifyTaskArgs): Promise => { - if (address === undefined) { - throw new MissingAddressError(); + try { + await run(verificationSubtask, taskArgs); + } catch (error) { + hasErrors = true; + errors[verificationSubtask] = error as HardhatVerifyError; } + } - const { isAddress } = await import("@ethersproject/address"); - if (!isAddress(address)) { - throw new InvalidAddressError(address); - } - - if (contract !== undefined && !isFullyQualifiedName(contract)) { - throw new InvalidContractNameError(contract); - } - - const constructorArgs = await resolveConstructorArguments( - constructorArgsParams, - constructorArgsModule - ); - - const libraries = await resolveLibraries(librariesModule); - - return { - address, - constructorArgs, - libraries, - contractFQN: contract, - }; + if (hasErrors) { + printVerificationErrors(errors); + process.exit(1); } - ); + }); /** * Returns a list of verification subtasks. @@ -261,119 +228,174 @@ Learn more at https://...` */ subtask(TASK_VERIFY_ETHERSCAN) .addParam("address") - .addParam("constructorArgs", undefined, undefined, types.any) - .addParam("libraries", undefined, undefined, types.any) - .addOptionalParam("contractFQN") + .addOptionalParam("constructorArgsParams", undefined, undefined, types.any) + .addOptionalParam("constructorArgs") + // TODO: [remove-verify-subtask] change to types.inputFile + .addOptionalParam("libraries", undefined, undefined, types.any) + .addOptionalParam("contract") .addFlag("listNetworks") - .setAction( - async ( - { address, constructorArgs, libraries, contractFQN }: VerificationArgs, - { config, network, run } - ) => { - const chainConfig = await Etherscan.getCurrentChainConfig( - network.name, - network.provider, - config.etherscan.customChains - ); + .setAction(async (taskArgs: VerifyTaskArgs, { config, network, run }) => { + const { + address, + constructorArgs, + libraries, + contractFQN, + }: VerificationArgs = await run(TASK_VERIFY_RESOLVE_ARGUMENTS, taskArgs); + + const chainConfig = await Etherscan.getCurrentChainConfig( + network.name, + network.provider, + config.etherscan.customChains + ); - const etherscan = Etherscan.fromChainConfig( - config.etherscan.apiKey, - chainConfig - ); + const etherscan = Etherscan.fromChainConfig( + config.etherscan.apiKey, + chainConfig + ); - const isVerified = await etherscan.isVerified(address); - if (isVerified) { - const contractURL = etherscan.getContractUrl(address); - console.log(`The contract ${address} has already been verified. + const isVerified = await etherscan.isVerified(address); + if (isVerified) { + const contractURL = etherscan.getContractUrl(address); + console.log(`The contract ${address} has already been verified. ${contractURL}`); - return; - } + return; + } - const configCompilerVersions = await getCompilerVersions(config.solidity); + const configCompilerVersions = await getCompilerVersions(config.solidity); - const deployedBytecode = await Bytecode.getDeployedContractBytecode( - address, - network.provider, + const deployedBytecode = await Bytecode.getDeployedContractBytecode( + address, + network.provider, + network.name + ); + + const matchingCompilerVersions = await deployedBytecode.getMatchingVersions( + configCompilerVersions + ); + // don't error if the bytecode appears to be OVM bytecode, because we can't infer a specific OVM solc version from the bytecode + if (matchingCompilerVersions.length === 0 && !deployedBytecode.isOvm()) { + throw new CompilerVersionsMismatchError( + configCompilerVersions, + deployedBytecode.getVersion(), network.name ); + } - const matchingCompilerVersions = - await deployedBytecode.getMatchingVersions(configCompilerVersions); - // don't error if the bytecode appears to be OVM bytecode, because we can't infer a specific OVM solc version from the bytecode - if (matchingCompilerVersions.length === 0 && !deployedBytecode.isOvm()) { - throw new CompilerVersionsMismatchError( - configCompilerVersions, - deployedBytecode.getVersion(), - network.name - ); + const contractInformation: ExtendedContractInformation = await run( + TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION, + { + contractFQN, + deployedBytecode, + matchingCompilerVersions, + libraries, } + ); - const contractInformation: ExtendedContractInformation = await run( - TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION, - { - contractFQN, - deployedBytecode, - matchingCompilerVersions, - libraries, - } - ); + const minimalInput: CompilerInput = await run( + TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT, + { + sourceName: contractInformation.sourceName, + } + ); - const minimalInput: CompilerInput = await run( - TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT, - { - sourceName: contractInformation.sourceName, - } - ); + const encodedConstructorArguments = await encodeArguments( + contractInformation.contractOutput.abi, + contractInformation.sourceName, + contractInformation.contractName, + constructorArgs + ); - const encodedConstructorArguments = await encodeArguments( - contractInformation.contractOutput.abi, - contractInformation.sourceName, - contractInformation.contractName, - constructorArgs - ); + // First, try to verify the contract using the minimal input + const { success: minimalInputVerificationSuccess }: VerificationResponse = + await run(TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, { + address, + compilerInput: minimalInput, + contractInformation, + verificationInterface: etherscan, + encodedConstructorArguments, + }); - // First, try to verify the contract using the minimal input - const { success: minimalInputVerificationSuccess }: VerificationResponse = - await run(TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, { - address, - compilerInput: minimalInput, - contractInformation, - verificationInterface: etherscan, - encodedConstructorArguments, - }); - - if (minimalInputVerificationSuccess) { - return; - } + if (minimalInputVerificationSuccess) { + return; + } - console.log(`We tried verifying your contract ${contractInformation.contractName} without including any unrelated one, but it failed. + console.log(`We tried verifying your contract ${contractInformation.contractName} without including any unrelated one, but it failed. Trying again with the full solc input used to compile and deploy it. This means that unrelated contracts may be displayed on Etherscan... `); - // If verifying with the minimal input failed, try again with the full compiler input - const { - success: fullCompilerInputVerificationSuccess, - message: verificationMessage, - }: VerificationResponse = await run( - TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, - { - address, - compilerInput: contractInformation.compilerInput, - contractInformation, - verificationInterface: etherscan, - encodedConstructorArguments, - } - ); + // If verifying with the minimal input failed, try again with the full compiler input + const { + success: fullCompilerInputVerificationSuccess, + message: verificationMessage, + }: VerificationResponse = await run( + TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, + { + address, + compilerInput: contractInformation.compilerInput, + contractInformation, + verificationInterface: etherscan, + encodedConstructorArguments, + } + ); + + if (fullCompilerInputVerificationSuccess) { + return; + } + + throw new ContractVerificationFailedError( + verificationMessage, + contractInformation.undetectableLibraries + ); + }); + +subtask(TASK_VERIFY_RESOLVE_ARGUMENTS) + .addOptionalParam("address") + .addOptionalParam("constructorArgsParams", undefined, [], types.any) + .addOptionalParam("constructorArgs", undefined, undefined, types.inputFile) + // TODO: [remove-verify-subtask] change to types.inputFile + .addOptionalParam("libraries", undefined, undefined, types.any) + .addOptionalParam("contract") + .setAction( + async ({ + address, + constructorArgsParams, + constructorArgs: constructorArgsModule, + contract, + libraries: librariesModule, + }: VerifyTaskArgs): Promise => { + if (address === undefined) { + throw new MissingAddressError(); + } - if (fullCompilerInputVerificationSuccess) { - return; + const { isAddress } = await import("@ethersproject/address"); + if (!isAddress(address)) { + throw new InvalidAddressError(address); } - throw new ContractVerificationFailedError( - verificationMessage, - contractInformation.undetectableLibraries + if (contract !== undefined && !isFullyQualifiedName(contract)) { + throw new InvalidContractNameError(contract); + } + + const constructorArgs = await resolveConstructorArguments( + constructorArgsParams, + constructorArgsModule ); + + // TODO: [remove-verify-subtask] librariesModule should always be string + let libraries; + if (typeof librariesModule === "object") { + libraries = librariesModule; + } else { + libraries = await resolveLibraries(librariesModule); + } + + return { + address, + constructorArgs, + libraries, + contractFQN: contract, + }; } ); @@ -542,8 +564,8 @@ ${contractURL}`); /** * This subtask is used for backwards compatibility. - * It validates the parameters as it is done in TASK_VERIFY_RESOLVE_ARGUMENTS - * and calls TASK_VERIFY_ETHERSCAN directly. + * TODO [remove-verify-subtask]: if you're going to remove this subtask, + * update TASK_VERIFY_ETHERSCAN and TASK_VERIFY_RESOLVE_ARGUMENTS accordingly */ subtask(TASK_VERIFY_VERIFY) .addOptionalParam("address") @@ -555,19 +577,6 @@ subtask(TASK_VERIFY_VERIFY) { address, constructorArguments, libraries, contract }: VerifySubtaskArgs, { run } ) => { - if (address === undefined) { - throw new MissingAddressError(); - } - - const { isAddress } = await import("@ethersproject/address"); - if (!isAddress(address)) { - throw new InvalidAddressError(address); - } - - if (contract !== undefined && !isFullyQualifiedName(contract)) { - throw new InvalidContractNameError(contract); - } - // This can only happen if the subtask is invoked from within Hardhat by a user script or another task. if (!Array.isArray(constructorArguments)) { throw new InvalidConstructorArgumentsError(); @@ -579,9 +588,9 @@ subtask(TASK_VERIFY_VERIFY) await run(TASK_VERIFY_ETHERSCAN, { address, - constructorArgs: constructorArguments, + constructorArgsParams: constructorArguments, libraries, - contractFQN: contract, + contract, }); } ); diff --git a/packages/hardhat-verify/src/internal/utilities.ts b/packages/hardhat-verify/src/internal/utilities.ts index 5ddabe5328..b89bdd584e 100644 --- a/packages/hardhat-verify/src/internal/utilities.ts +++ b/packages/hardhat-verify/src/internal/utilities.ts @@ -11,6 +11,7 @@ import { ABIArgumentTypeError, EtherscanVersionNotSupportedError, ExclusiveConstructorArgumentsError, + HardhatVerifyError, ImportingModuleError, InvalidConstructorArgumentsModuleError, InvalidLibrariesModuleError, @@ -77,6 +78,49 @@ To learn how to add custom networks, follow these instructions: https://hardhat. ); } +/** + * Prints verification errors to the console. + * @param errors - An object containing verification errors, where the keys + * are the names of verification subtasks and the values are HardhatVerifyError + * objects describing the specific errors. + * @remarks This function formats and logs the verification errors to the + * console with a red color using chalk. Each error is displayed along with the + * name of the verification provider it belongs to. + * @example + * const errors: Record = { + * verify:etherscan: { message: 'Error message for Etherscan' }, + * verify:sourcify: { message: 'Error message for Sourcify' }, + * // Add more errors here... + * }; + * printVerificationErrors(errors); + * // Output: + * // hardhat-verify found one or more errors during the verification process: + * // + * // Etherscan: + * // Error message for Etherscan + * // + * // Sourcify: + * // Error message for Sourcify + * // + * // ... (more errors if present) + */ +export function printVerificationErrors( + errors: Record +) { + let errorMessage = + "hardhat-verify found one or more errors during the verification process:\n\n"; + + for (const verificationSubtask of Object.keys(errors)) { + const error = errors[verificationSubtask]; + const verificationProvider = verificationSubtask + .split(":")[1] + .replace(/^\w/, (c) => c.toUpperCase()); + errorMessage += `${verificationProvider}:\n${error.message}\n\n`; + } + + console.error(chalk.red(errorMessage)); +} + /** * Returns the list of constructor arguments from the constructorArgsModule * or the constructorArgsParams if the first is not defined. diff --git a/packages/hardhat-verify/test/integration/index.ts b/packages/hardhat-verify/test/integration/index.ts index 0e0c05c2c2..aa5dbf6407 100644 --- a/packages/hardhat-verify/test/integration/index.ts +++ b/packages/hardhat-verify/test/integration/index.ts @@ -4,7 +4,11 @@ import sinon, { SinonStub } from "sinon"; import { assert, expect } from "chai"; import { TASK_CLEAN, TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; import { SolcConfig } from "hardhat/types/config"; -import { TASK_VERIFY, TASK_VERIFY_VERIFY } from "../../src/internal/task-names"; +import { + TASK_VERIFY, + TASK_VERIFY_ETHERSCAN, + TASK_VERIFY_VERIFY, +} from "../../src/internal/task-names"; import { deployContract, getRandomAddress, useEnvironment } from "../helpers"; import { interceptGetStatus, @@ -55,7 +59,7 @@ describe("verify task integration tests", () => { }; await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address, constructorArgsParams: [], }) @@ -75,7 +79,7 @@ describe("verify task integration tests", () => { const logStub = sinon.stub(console, "log"); const address = getRandomAddress(this.hre); - const taskResponse = await this.hre.run(TASK_VERIFY, { + const taskResponse = await this.hre.run(TASK_VERIFY_ETHERSCAN, { address, constructorArgsParams: [], }); @@ -144,7 +148,7 @@ https://hardhat.etherscan.io/address/${address}#code` const address = getRandomAddress(this.hre); await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address, constructorArgsParams: [], }) @@ -163,7 +167,7 @@ https://hardhat.etherscan.io/address/${address}#code` ]; await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -183,7 +187,7 @@ https://hardhat.etherscan.io/address/${address}#code` // task will fail since we deleted all the artifacts await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -201,7 +205,7 @@ https://hardhat.etherscan.io/address/${address}#code` it("should throw if the deployed bytecode matches more than one contract", async function () { await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: duplicatedContractAddress, constructorArgsParams: [], }) @@ -214,7 +218,7 @@ https://hardhat.etherscan.io/address/${address}#code` const contractFQN = "contracts/SimpleContract.sol:NotFound"; await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], contract: contractFQN, @@ -228,7 +232,7 @@ https://hardhat.etherscan.io/address/${address}#code` it("should throw if there is an invalid address in the libraries parameter", async function () { await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], libraries: "invalid-libraries.js", @@ -240,7 +244,7 @@ https://hardhat.etherscan.io/address/${address}#code` it("should throw if the specified library is not used by the contract", async function () { await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: bothLibsContractAddress, constructorArgsParams: [], libraries: "not-used-libraries.js", @@ -252,7 +256,7 @@ https://hardhat.etherscan.io/address/${address}#code` it("should throw if the specified library is listed more than once in the libraries parameter", async function () { await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: onlyNormalLibContractAddress, constructorArgsParams: [], libraries: "duplicated-libraries.js", @@ -264,7 +268,7 @@ https://hardhat.etherscan.io/address/${address}#code` it("should throw if deployed library address does not match the address defined in the libraries parameter", async function () { await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: onlyNormalLibContractAddress, constructorArgsParams: [], libraries: "mismatched-address-libraries.js", @@ -278,7 +282,7 @@ https://hardhat.etherscan.io/address/${address}#code` it("should throw if there are undetectable libraries not specified by the libraries parameter", async function () { await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: bothLibsContractAddress, constructorArgsParams: [], libraries: "missing-undetectable-libraries.js", @@ -293,7 +297,7 @@ This can occur if the library is only called in the contract constructor. The mi it("should throw if the verification request fails", async function () { // do not intercept the verifysourcecode request so it throws an error await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -306,7 +310,7 @@ This can occur if the library is only called in the contract constructor. The mi interceptVerify({ error: "error verifying contract" }, 500); await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -322,7 +326,7 @@ The HTTP server response is not ok. Status code: 500 Response text: {"error":"er }); await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -340,7 +344,7 @@ The HTTP server response is not ok. Status code: 500 Response text: {"error":"er }); await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -356,7 +360,7 @@ The HTTP server response is not ok. Status code: 500 Response text: {"error":"er const logStub = sinon.stub(console, "log"); await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -379,7 +383,7 @@ for verification on the block explorer. Waiting for verification result... const logStub = sinon.stub(console, "log"); await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -407,7 +411,7 @@ for verification on the block explorer. Waiting for verification result... const logStub = sinon.stub(console, "log"); await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -435,7 +439,7 @@ for verification on the block explorer. Waiting for verification result... const logStub = sinon.stub(console, "log"); await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: simpleContractAddress, constructorArgsParams: [], }) @@ -552,7 +556,7 @@ https://hardhat.etherscan.io/address/${simpleContractAddress}#code`); const logStub = sinon.stub(console, "log"); await expect( - this.hre.run(TASK_VERIFY, { + this.hre.run(TASK_VERIFY_ETHERSCAN, { address: bothLibsContractAddress, constructorArgsParams: ["50"], libraries: "libraries.js", diff --git a/packages/hardhat-verify/test/unit/index.ts b/packages/hardhat-verify/test/unit/index.ts index 2defe7e24a..785418f424 100644 --- a/packages/hardhat-verify/test/unit/index.ts +++ b/packages/hardhat-verify/test/unit/index.ts @@ -79,36 +79,6 @@ describe("verify task", () => { }); describe(TASK_VERIFY_VERIFY, () => { - it("should throw if address is not provided", async function () { - await expect( - this.hre.run(TASK_VERIFY_VERIFY, { - constructorArguments: [], - libraries: {}, - }) - ).to.be.rejectedWith(/You didn’t provide any address./); - }); - - it("should throw if address is invalid", async function () { - await expect( - this.hre.run(TASK_VERIFY_VERIFY, { - address: "invalidAddress", - constructorArguments: [], - libraries: {}, - }) - ).to.be.rejectedWith(/invalidAddress is an invalid address./); - }); - - it("should throw if contract is not a fully qualified name", async function () { - await expect( - this.hre.run(TASK_VERIFY_VERIFY, { - address: getRandomAddress(this.hre), - constructorArguments: [], - libraries: {}, - contract: "not-a-fully-qualified-name", - }) - ).to.be.rejectedWith(/A valid fully qualified name was expected./); - }); - it("should throw if constructorArguments is not an array", async function () { await expect( this.hre.run(TASK_VERIFY_VERIFY, { @@ -138,8 +108,6 @@ describe("verify task", () => { beforeEach(() => { warnStub = sinon.stub(console, "warn"); }); - - // suppress warnings afterEach(() => { warnStub.restore(); }); diff --git a/packages/hardhat-verify/test/unit/utilities.ts b/packages/hardhat-verify/test/unit/utilities.ts index aff9e8e217..57ab49eb0f 100644 --- a/packages/hardhat-verify/test/unit/utilities.ts +++ b/packages/hardhat-verify/test/unit/utilities.ts @@ -1,17 +1,85 @@ import type { JsonFragment } from "@ethersproject/abi"; import type { SolidityConfig } from "hardhat/types"; +import type { ChainConfig } from "../../src/types"; import path from "path"; import { assert, expect } from "chai"; +import sinon from "sinon"; +import chalk from "chalk"; import { encodeArguments, getCompilerVersions, + printSupportedNetworks, + printVerificationErrors, resolveConstructorArguments, resolveLibraries, } from "../../src/internal/utilities"; +import { HardhatVerifyError } from "../../src/internal/errors"; +import { builtinChains } from "../../src/internal/chain-config"; describe("Utilities", () => { + describe("printSupportedNetworks", () => { + it("should print supported and custom networks", async () => { + const customChains: ChainConfig[] = [ + { + network: "MyNetwork", + chainId: 1337, + urls: { + apiURL: "https://api.mynetwork.io/api", + browserURL: "https://mynetwork.io", + }, + }, + ]; + + const logStub = sinon.stub(console, "log"); + + await printSupportedNetworks(customChains); + + sinon.restore(); + + assert.isTrue(logStub.calledOnce); + const actualTableOutput = logStub.getCall(0).args[0]; + const allChains = [...builtinChains, ...customChains]; + allChains.forEach(({ network, chainId }) => { + const regex = new RegExp(`║\\s*${network}\\s*│\\s*${chainId}\\s*║`); + assert.isTrue(regex.test(actualTableOutput)); + }); + }); + }); + + describe("printVerificationErrors", () => { + it("should print verification errors", () => { + const errors: Record = { + "verify:etherscan": new HardhatVerifyError("Etherscan error message"), + "verify:sourcify": new HardhatVerifyError("Sourcify error message"), + }; + + const errorStub = sinon.stub(console, "error"); + + printVerificationErrors(errors); + + sinon.restore(); + + assert.isTrue(errorStub.calledOnce); + const errorMessage = errorStub.getCall(0).args[0]; + assert.equal( + errorMessage, + chalk.red( + `hardhat-verify found one or more errors during the verification process: + +Etherscan: +Etherscan error message + +Sourcify: +Sourcify error message + +` + ) + ); + }); + }); + describe("resolveConstructorArguments", () => { it("should return the constructorArgsParams if constructorArgsModule is not defined", async () => { const constructorArgsParams = ["1", "arg2", "false"]; From ef25d8e21fd86588940a4464075d2bccbf973a1e Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Mon, 24 Jul 2023 17:48:01 -0300 Subject: [PATCH 04/36] Show sourcify warning after the etherscan validation --- packages/hardhat-verify/src/index.ts | 32 ++++++++++--------- .../hardhat-verify/src/internal/task-names.ts | 2 ++ packages/hardhat-verify/test/unit/index.ts | 27 +++++++--------- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index 00578064e8..60e8361393 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -24,6 +24,7 @@ import { TASK_VERIFY_ETHERSCAN, TASK_VERIFY_PRINT_SUPPORTED_NETWORKS, TASK_VERIFY_SOURCIFY, + TASK_VERIFY_SOURCIFY_DISABLED_WARNING, } from "./internal/task-names"; import { etherscanConfigExtender, @@ -180,9 +181,6 @@ task(TASK_VERIFY, "Verifies a contract on Etherscan") } }); -/** - * Returns a list of verification subtasks. - */ subtask( TASK_VERIFY_GET_VERIFICATION_SUBTASKS, async (_, { config }): Promise => { @@ -195,20 +193,10 @@ subtask( if (config.sourcify.enabled) { verificationSubtasks.push(TASK_VERIFY_SOURCIFY); } else { - console.warn( - chalk.yellow( - `WARNING: Skipping Sourcify verification: Sourcify is disabled. To enable it, add this entry to your config: - -sourcify: { - enabled: true -} - -Learn more at https://...` - ) - ); + verificationSubtasks.push(TASK_VERIFY_SOURCIFY_DISABLED_WARNING); } - if (verificationSubtasks.length === 0) { + if (!config.etherscan.enabled && !config.sourcify.enabled) { console.warn( chalk.yellow( `WARNING: No verification services are enabled. Please enable at least one verification service in your configuration.` @@ -220,6 +208,20 @@ Learn more at https://...` } ); +subtask(TASK_VERIFY_SOURCIFY_DISABLED_WARNING, async (): Promise => { + console.warn( + chalk.yellow( + `WARNING: Skipping Sourcify verification: Sourcify is disabled. To enable it, add this entry to your config: + +sourcify: { +enabled: true +} + +Learn more at https://...` + ) + ); +}); + /** * Main Etherscan verification subtask. * diff --git a/packages/hardhat-verify/src/internal/task-names.ts b/packages/hardhat-verify/src/internal/task-names.ts index 9261ecbf13..995d3801ae 100644 --- a/packages/hardhat-verify/src/internal/task-names.ts +++ b/packages/hardhat-verify/src/internal/task-names.ts @@ -17,3 +17,5 @@ export const TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION = // Sourcify export const TASK_VERIFY_SOURCIFY = "verify:sourcify"; +export const TASK_VERIFY_SOURCIFY_DISABLED_WARNING = + "verify:sourcify-disabled-warning"; diff --git a/packages/hardhat-verify/test/unit/index.ts b/packages/hardhat-verify/test/unit/index.ts index 785418f424..ecc54b0f91 100644 --- a/packages/hardhat-verify/test/unit/index.ts +++ b/packages/hardhat-verify/test/unit/index.ts @@ -6,6 +6,7 @@ import { TASK_VERIFY_GET_VERIFICATION_SUBTASKS, TASK_VERIFY_RESOLVE_ARGUMENTS, TASK_VERIFY_SOURCIFY, + TASK_VERIFY_SOURCIFY_DISABLED_WARNING, TASK_VERIFY_VERIFY, } from "../../src/internal/task-names"; import { getRandomAddress, useEnvironment } from "../helpers"; @@ -160,10 +161,8 @@ describe("verify task", () => { ); assert.isFalse(verificationSubtasks.includes(TASK_VERIFY_SOURCIFY)); - expect(warnStub).to.be.calledOnceWith( - sinon.match( - /WARNING: Skipping Sourcify verification: Sourcify is disabled./ - ) + assert.isTrue( + verificationSubtasks.includes(TASK_VERIFY_SOURCIFY_DISABLED_WARNING) ); }); @@ -180,6 +179,9 @@ describe("verify task", () => { this.hre.config.sourcify = originalConfig; assert.isTrue(verificationSubtasks.includes(TASK_VERIFY_SOURCIFY)); + assert.isFalse( + verificationSubtasks.includes(TASK_VERIFY_SOURCIFY_DISABLED_WARNING) + ); }); it("should ignore the sourcify subtask if it is disabled", async function () { @@ -195,10 +197,8 @@ describe("verify task", () => { this.hre.config.sourcify = originalConfig; assert.isFalse(verificationSubtasks.includes(TASK_VERIFY_SOURCIFY)); - expect(warnStub).to.be.calledOnceWith( - sinon.match( - /WARNING: Skipping Sourcify verification: Sourcify is disabled./ - ) + assert.isTrue( + verificationSubtasks.includes(TASK_VERIFY_SOURCIFY_DISABLED_WARNING) ); }); @@ -214,19 +214,14 @@ describe("verify task", () => { enabled: false, }; - const verificationSubtasks: string[] = await this.hre.run( - TASK_VERIFY_GET_VERIFICATION_SUBTASKS - ); + await this.hre.run(TASK_VERIFY_GET_VERIFICATION_SUBTASKS); this.hre.config.etherscan = originalEtherscanConfig; this.hre.config.sourcify = originalSourcifyConfig; - assert.equal(verificationSubtasks.length, 0); - assert.isTrue(warnStub.calledTwice); + assert.isTrue(warnStub.calledOnce); expect(warnStub).to.be.calledWith( - sinon.match( - /WARNING: Skipping Sourcify verification: Sourcify is disabled./ - ) + sinon.match(/WARNING: No verification services are enabled./) ); }); }); From 48e3d374d52e2ea985427ed511160cc94df751e6 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Mon, 24 Jul 2023 18:09:07 -0300 Subject: [PATCH 05/36] Move etherscan & sourcify subtasks to their own files --- packages/hardhat-verify/src/index.ts | 449 +----------------- .../src/internal/tasks/etherscan.ts | 434 +++++++++++++++++ .../src/internal/tasks/sourcify.ts | 18 + 3 files changed, 463 insertions(+), 438 deletions(-) create mode 100644 packages/hardhat-verify/src/internal/tasks/etherscan.ts create mode 100644 packages/hardhat-verify/src/internal/tasks/sourcify.ts diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index 60e8361393..37acda2cad 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -1,25 +1,10 @@ -import type LodashCloneDeepT from "lodash.clonedeep"; -import type { - CompilationJob, - CompilerInput, - DependencyGraph, -} from "hardhat/types"; +import type { LibraryToAddress } from "./internal/solc/artifacts"; import chalk from "chalk"; import { extendConfig, subtask, task, types } from "hardhat/config"; -import { isFullyQualifiedName } from "hardhat/utils/contract-names"; -import { - TASK_COMPILE_SOLIDITY_GET_COMPILATION_JOB_FOR_FILE, - TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT, - TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH, -} from "hardhat/builtin-tasks/task-names"; import { TASK_VERIFY, - TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, - TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION, - TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT, TASK_VERIFY_GET_VERIFICATION_SUBTASKS, - TASK_VERIFY_RESOLVE_ARGUMENTS, TASK_VERIFY_VERIFY, TASK_VERIFY_ETHERSCAN, TASK_VERIFY_PRINT_SUPPORTED_NETWORKS, @@ -31,45 +16,21 @@ import { sourcifyConfigExtender, } from "./internal/config"; import { - MissingAddressError, - InvalidAddressError, - InvalidContractNameError, InvalidConstructorArgumentsError, InvalidLibrariesError, - CompilerVersionsMismatchError, - ContractNotFoundError, - BuildInfoNotFoundError, - BuildInfoCompilerVersionMismatchError, - DeployedBytecodeMismatchError, - UnexpectedNumberOfFilesError, - VerificationAPIUnexpectedMessageError, - ContractVerificationFailedError, HardhatVerifyError, } from "./internal/errors"; import { - sleep, - encodeArguments, - getCompilerVersions, printSupportedNetworks, - resolveConstructorArguments, - resolveLibraries, printVerificationErrors, } from "./internal/utilities"; -import { Etherscan } from "./internal/etherscan"; -import { Bytecode } from "./internal/solc/bytecode"; -import { - ContractInformation, - ExtendedContractInformation, - extractInferredContractInformation, - extractMatchingContractInformation, - getLibraryInformation, - LibraryToAddress, -} from "./internal/solc/artifacts"; import "./internal/type-extensions"; +import "./internal/tasks/etherscan"; +import "./internal/tasks/sourcify"; // Main task args -interface VerifyTaskArgs { +export interface VerifyTaskArgs { address?: string; constructorArgsParams: string[]; constructorArgs?: string; @@ -86,38 +47,6 @@ interface VerifySubtaskArgs { contract?: string; } -// parsed verification args -interface VerificationArgs { - address: string; - constructorArgs: string[]; - libraries: LibraryToAddress; - contractFQN?: string; -} - -interface GetContractInformationArgs { - contractFQN?: string; - deployedBytecode: Bytecode; - matchingCompilerVersions: string[]; - libraries: LibraryToAddress; -} - -interface GetMinimalInputArgs { - sourceName: string; -} - -interface AttemptVerificationArgs { - address: string; - compilerInput: CompilerInput; - contractInformation: ExtendedContractInformation; - verificationInterface: Etherscan; - encodedConstructorArguments: string; -} - -interface VerificationResponse { - success: boolean; - message: string; -} - extendConfig(etherscanConfigExtender); extendConfig(sourcifyConfigExtender); @@ -181,6 +110,13 @@ task(TASK_VERIFY, "Verifies a contract on Etherscan") } }); +subtask( + TASK_VERIFY_PRINT_SUPPORTED_NETWORKS, + "Prints the supported networks list" +).setAction(async ({}, { config }) => { + await printSupportedNetworks(config.etherscan.customChains); +}); + subtask( TASK_VERIFY_GET_VERIFICATION_SUBTASKS, async (_, { config }): Promise => { @@ -208,362 +144,6 @@ subtask( } ); -subtask(TASK_VERIFY_SOURCIFY_DISABLED_WARNING, async (): Promise => { - console.warn( - chalk.yellow( - `WARNING: Skipping Sourcify verification: Sourcify is disabled. To enable it, add this entry to your config: - -sourcify: { -enabled: true -} - -Learn more at https://...` - ) - ); -}); - -/** - * Main Etherscan verification subtask. - * - * Verifies a contract in Etherscan by coordinating various subtasks related - * to contract verification. - */ -subtask(TASK_VERIFY_ETHERSCAN) - .addParam("address") - .addOptionalParam("constructorArgsParams", undefined, undefined, types.any) - .addOptionalParam("constructorArgs") - // TODO: [remove-verify-subtask] change to types.inputFile - .addOptionalParam("libraries", undefined, undefined, types.any) - .addOptionalParam("contract") - .addFlag("listNetworks") - .setAction(async (taskArgs: VerifyTaskArgs, { config, network, run }) => { - const { - address, - constructorArgs, - libraries, - contractFQN, - }: VerificationArgs = await run(TASK_VERIFY_RESOLVE_ARGUMENTS, taskArgs); - - const chainConfig = await Etherscan.getCurrentChainConfig( - network.name, - network.provider, - config.etherscan.customChains - ); - - const etherscan = Etherscan.fromChainConfig( - config.etherscan.apiKey, - chainConfig - ); - - const isVerified = await etherscan.isVerified(address); - if (isVerified) { - const contractURL = etherscan.getContractUrl(address); - console.log(`The contract ${address} has already been verified. -${contractURL}`); - return; - } - - const configCompilerVersions = await getCompilerVersions(config.solidity); - - const deployedBytecode = await Bytecode.getDeployedContractBytecode( - address, - network.provider, - network.name - ); - - const matchingCompilerVersions = await deployedBytecode.getMatchingVersions( - configCompilerVersions - ); - // don't error if the bytecode appears to be OVM bytecode, because we can't infer a specific OVM solc version from the bytecode - if (matchingCompilerVersions.length === 0 && !deployedBytecode.isOvm()) { - throw new CompilerVersionsMismatchError( - configCompilerVersions, - deployedBytecode.getVersion(), - network.name - ); - } - - const contractInformation: ExtendedContractInformation = await run( - TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION, - { - contractFQN, - deployedBytecode, - matchingCompilerVersions, - libraries, - } - ); - - const minimalInput: CompilerInput = await run( - TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT, - { - sourceName: contractInformation.sourceName, - } - ); - - const encodedConstructorArguments = await encodeArguments( - contractInformation.contractOutput.abi, - contractInformation.sourceName, - contractInformation.contractName, - constructorArgs - ); - - // First, try to verify the contract using the minimal input - const { success: minimalInputVerificationSuccess }: VerificationResponse = - await run(TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, { - address, - compilerInput: minimalInput, - contractInformation, - verificationInterface: etherscan, - encodedConstructorArguments, - }); - - if (minimalInputVerificationSuccess) { - return; - } - - console.log(`We tried verifying your contract ${contractInformation.contractName} without including any unrelated one, but it failed. -Trying again with the full solc input used to compile and deploy it. -This means that unrelated contracts may be displayed on Etherscan... -`); - - // If verifying with the minimal input failed, try again with the full compiler input - const { - success: fullCompilerInputVerificationSuccess, - message: verificationMessage, - }: VerificationResponse = await run( - TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, - { - address, - compilerInput: contractInformation.compilerInput, - contractInformation, - verificationInterface: etherscan, - encodedConstructorArguments, - } - ); - - if (fullCompilerInputVerificationSuccess) { - return; - } - - throw new ContractVerificationFailedError( - verificationMessage, - contractInformation.undetectableLibraries - ); - }); - -subtask(TASK_VERIFY_RESOLVE_ARGUMENTS) - .addOptionalParam("address") - .addOptionalParam("constructorArgsParams", undefined, [], types.any) - .addOptionalParam("constructorArgs", undefined, undefined, types.inputFile) - // TODO: [remove-verify-subtask] change to types.inputFile - .addOptionalParam("libraries", undefined, undefined, types.any) - .addOptionalParam("contract") - .setAction( - async ({ - address, - constructorArgsParams, - constructorArgs: constructorArgsModule, - contract, - libraries: librariesModule, - }: VerifyTaskArgs): Promise => { - if (address === undefined) { - throw new MissingAddressError(); - } - - const { isAddress } = await import("@ethersproject/address"); - if (!isAddress(address)) { - throw new InvalidAddressError(address); - } - - if (contract !== undefined && !isFullyQualifiedName(contract)) { - throw new InvalidContractNameError(contract); - } - - const constructorArgs = await resolveConstructorArguments( - constructorArgsParams, - constructorArgsModule - ); - - // TODO: [remove-verify-subtask] librariesModule should always be string - let libraries; - if (typeof librariesModule === "object") { - libraries = librariesModule; - } else { - libraries = await resolveLibraries(librariesModule); - } - - return { - address, - constructorArgs, - libraries, - contractFQN: contract, - }; - } - ); - -subtask(TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION) - .addParam("deployedBytecode", undefined, undefined, types.any) - .addParam("matchingCompilerVersions", undefined, undefined, types.any) - .addParam("libraries", undefined, undefined, types.any) - .addOptionalParam("contractFQN") - .setAction( - async ( - { - contractFQN, - deployedBytecode, - matchingCompilerVersions, - libraries, - }: GetContractInformationArgs, - { network, artifacts } - ): Promise => { - let contractInformation: ContractInformation | null; - - if (contractFQN !== undefined) { - const artifactExists = await artifacts.artifactExists(contractFQN); - - if (!artifactExists) { - throw new ContractNotFoundError(contractFQN); - } - - const buildInfo = await artifacts.getBuildInfo(contractFQN); - if (buildInfo === undefined) { - throw new BuildInfoNotFoundError(contractFQN); - } - - if ( - !matchingCompilerVersions.includes(buildInfo.solcVersion) && - !deployedBytecode.isOvm() - ) { - throw new BuildInfoCompilerVersionMismatchError( - contractFQN, - deployedBytecode.getVersion(), - deployedBytecode.hasVersionRange(), - buildInfo.solcVersion, - network.name - ); - } - - contractInformation = extractMatchingContractInformation( - contractFQN, - buildInfo, - deployedBytecode - ); - - if (contractInformation === null) { - throw new DeployedBytecodeMismatchError(network.name, contractFQN); - } - } else { - contractInformation = await extractInferredContractInformation( - artifacts, - network, - matchingCompilerVersions, - deployedBytecode - ); - } - - // map contractInformation libraries - const libraryInformation = await getLibraryInformation( - contractInformation, - libraries - ); - - return { - ...contractInformation, - ...libraryInformation, - }; - } - ); - -subtask(TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT) - .addParam("sourceName") - .setAction(async ({ sourceName }: GetMinimalInputArgs, { run }) => { - const cloneDeep = require("lodash.clonedeep") as typeof LodashCloneDeepT; - const dependencyGraph: DependencyGraph = await run( - TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH, - { sourceNames: [sourceName] } - ); - - const resolvedFiles = dependencyGraph - .getResolvedFiles() - .filter((resolvedFile) => resolvedFile.sourceName === sourceName); - - if (resolvedFiles.length !== 1) { - throw new UnexpectedNumberOfFilesError(); - } - - const compilationJob: CompilationJob = await run( - TASK_COMPILE_SOLIDITY_GET_COMPILATION_JOB_FOR_FILE, - { - dependencyGraph, - file: resolvedFiles[0], - } - ); - - const minimalInput: CompilerInput = await run( - TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT, - { - compilationJob, - } - ); - - return cloneDeep(minimalInput); - }); - -subtask(TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION) - .addParam("address") - .addParam("compilerInput", undefined, undefined, types.any) - .addParam("contractInformation", undefined, undefined, types.any) - .addParam("verificationInterface", undefined, undefined, types.any) - .addParam("encodedConstructorArguments") - .setAction( - async ({ - address, - compilerInput, - contractInformation, - verificationInterface, - encodedConstructorArguments, - }: AttemptVerificationArgs): Promise => { - // Ensure the linking information is present in the compiler input; - compilerInput.settings.libraries = contractInformation.libraries; - - const { message: guid } = await verificationInterface.verify( - address, - JSON.stringify(compilerInput), - `${contractInformation.sourceName}:${contractInformation.contractName}`, - `v${contractInformation.solcLongVersion}`, - encodedConstructorArguments - ); - - console.log(`Successfully submitted source code for contract -${contractInformation.sourceName}:${contractInformation.contractName} at ${address} -for verification on the block explorer. Waiting for verification result... -`); - - // Compilation is bound to take some time so there's no sense in requesting status immediately. - await sleep(700); - const verificationStatus = - await verificationInterface.getVerificationStatus(guid); - - if (!(verificationStatus.isFailure() || verificationStatus.isSuccess())) { - // Reaching this point shouldn't be possible unless the API is behaving in a new way. - throw new VerificationAPIUnexpectedMessageError( - verificationStatus.message - ); - } - - if (verificationStatus.isSuccess()) { - const contractURL = verificationInterface.getContractUrl(address); - console.log(`Successfully verified contract ${contractInformation.contractName} on the block explorer. -${contractURL}`); - } - - return { - success: verificationStatus.isSuccess(), - message: verificationStatus.message, - }; - } - ); - /** * This subtask is used for backwards compatibility. * TODO [remove-verify-subtask]: if you're going to remove this subtask, @@ -596,10 +176,3 @@ subtask(TASK_VERIFY_VERIFY) }); } ); - -subtask( - TASK_VERIFY_PRINT_SUPPORTED_NETWORKS, - "Prints the supported networks list" -).setAction(async ({}, { config }) => { - await printSupportedNetworks(config.etherscan.customChains); -}); diff --git a/packages/hardhat-verify/src/internal/tasks/etherscan.ts b/packages/hardhat-verify/src/internal/tasks/etherscan.ts new file mode 100644 index 0000000000..90c62ded05 --- /dev/null +++ b/packages/hardhat-verify/src/internal/tasks/etherscan.ts @@ -0,0 +1,434 @@ +import type LodashCloneDeepT from "lodash.clonedeep"; +import type { + CompilerInput, + DependencyGraph, + CompilationJob, +} from "hardhat/types"; +import type { VerifyTaskArgs } from "../.."; +import type { + LibraryToAddress, + ExtendedContractInformation, + ContractInformation, +} from "../solc/artifacts"; + +import { subtask, types } from "hardhat/config"; +import { + TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH, + TASK_COMPILE_SOLIDITY_GET_COMPILATION_JOB_FOR_FILE, + TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT, +} from "hardhat/builtin-tasks/task-names"; +import { isFullyQualifiedName } from "hardhat/utils/contract-names"; + +import { + CompilerVersionsMismatchError, + ContractVerificationFailedError, + MissingAddressError, + InvalidAddressError, + InvalidContractNameError, + ContractNotFoundError, + BuildInfoNotFoundError, + BuildInfoCompilerVersionMismatchError, + DeployedBytecodeMismatchError, + UnexpectedNumberOfFilesError, + VerificationAPIUnexpectedMessageError, +} from "../errors"; +import { Etherscan } from "../etherscan"; +import { + extractMatchingContractInformation, + extractInferredContractInformation, + getLibraryInformation, +} from "../solc/artifacts"; +import { Bytecode } from "../solc/bytecode"; +import { + TASK_VERIFY_ETHERSCAN, + TASK_VERIFY_RESOLVE_ARGUMENTS, + TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION, + TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT, + TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, +} from "../task-names"; +import { + getCompilerVersions, + encodeArguments, + resolveConstructorArguments, + resolveLibraries, + sleep, +} from "../utilities"; + +// parsed verification args +interface VerificationArgs { + address: string; + constructorArgs: string[]; + libraries: LibraryToAddress; + contractFQN?: string; +} + +interface GetContractInformationArgs { + contractFQN?: string; + deployedBytecode: Bytecode; + matchingCompilerVersions: string[]; + libraries: LibraryToAddress; +} + +interface GetMinimalInputArgs { + sourceName: string; +} + +interface AttemptVerificationArgs { + address: string; + compilerInput: CompilerInput; + contractInformation: ExtendedContractInformation; + verificationInterface: Etherscan; + encodedConstructorArguments: string; +} + +interface VerificationResponse { + success: boolean; + message: string; +} + +/** + * Main Etherscan verification subtask. + * + * Verifies a contract in Etherscan by coordinating various subtasks related + * to contract verification. + */ +subtask(TASK_VERIFY_ETHERSCAN) + .addParam("address") + .addOptionalParam("constructorArgsParams", undefined, undefined, types.any) + .addOptionalParam("constructorArgs") + // TODO: [remove-verify-subtask] change to types.inputFile + .addOptionalParam("libraries", undefined, undefined, types.any) + .addOptionalParam("contract") + .addFlag("listNetworks") + .setAction(async (taskArgs: VerifyTaskArgs, { config, network, run }) => { + const { + address, + constructorArgs, + libraries, + contractFQN, + }: VerificationArgs = await run(TASK_VERIFY_RESOLVE_ARGUMENTS, taskArgs); + + const chainConfig = await Etherscan.getCurrentChainConfig( + network.name, + network.provider, + config.etherscan.customChains + ); + + const etherscan = Etherscan.fromChainConfig( + config.etherscan.apiKey, + chainConfig + ); + + const isVerified = await etherscan.isVerified(address); + if (isVerified) { + const contractURL = etherscan.getContractUrl(address); + console.log(`The contract ${address} has already been verified. +${contractURL}`); + return; + } + + const configCompilerVersions = await getCompilerVersions(config.solidity); + + const deployedBytecode = await Bytecode.getDeployedContractBytecode( + address, + network.provider, + network.name + ); + + const matchingCompilerVersions = await deployedBytecode.getMatchingVersions( + configCompilerVersions + ); + // don't error if the bytecode appears to be OVM bytecode, because we can't infer a specific OVM solc version from the bytecode + if (matchingCompilerVersions.length === 0 && !deployedBytecode.isOvm()) { + throw new CompilerVersionsMismatchError( + configCompilerVersions, + deployedBytecode.getVersion(), + network.name + ); + } + + const contractInformation: ExtendedContractInformation = await run( + TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION, + { + contractFQN, + deployedBytecode, + matchingCompilerVersions, + libraries, + } + ); + + const minimalInput: CompilerInput = await run( + TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT, + { + sourceName: contractInformation.sourceName, + } + ); + + const encodedConstructorArguments = await encodeArguments( + contractInformation.contractOutput.abi, + contractInformation.sourceName, + contractInformation.contractName, + constructorArgs + ); + + // First, try to verify the contract using the minimal input + const { success: minimalInputVerificationSuccess }: VerificationResponse = + await run(TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, { + address, + compilerInput: minimalInput, + contractInformation, + verificationInterface: etherscan, + encodedConstructorArguments, + }); + + if (minimalInputVerificationSuccess) { + return; + } + + console.log(`We tried verifying your contract ${contractInformation.contractName} without including any unrelated one, but it failed. +Trying again with the full solc input used to compile and deploy it. +This means that unrelated contracts may be displayed on Etherscan... +`); + + // If verifying with the minimal input failed, try again with the full compiler input + const { + success: fullCompilerInputVerificationSuccess, + message: verificationMessage, + }: VerificationResponse = await run( + TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, + { + address, + compilerInput: contractInformation.compilerInput, + contractInformation, + verificationInterface: etherscan, + encodedConstructorArguments, + } + ); + + if (fullCompilerInputVerificationSuccess) { + return; + } + + throw new ContractVerificationFailedError( + verificationMessage, + contractInformation.undetectableLibraries + ); + }); + +subtask(TASK_VERIFY_RESOLVE_ARGUMENTS) + .addOptionalParam("address") + .addOptionalParam("constructorArgsParams", undefined, [], types.any) + .addOptionalParam("constructorArgs", undefined, undefined, types.inputFile) + // TODO: [remove-verify-subtask] change to types.inputFile + .addOptionalParam("libraries", undefined, undefined, types.any) + .addOptionalParam("contract") + .setAction( + async ({ + address, + constructorArgsParams, + constructorArgs: constructorArgsModule, + contract, + libraries: librariesModule, + }: VerifyTaskArgs): Promise => { + if (address === undefined) { + throw new MissingAddressError(); + } + + const { isAddress } = await import("@ethersproject/address"); + if (!isAddress(address)) { + throw new InvalidAddressError(address); + } + + if (contract !== undefined && !isFullyQualifiedName(contract)) { + throw new InvalidContractNameError(contract); + } + + const constructorArgs = await resolveConstructorArguments( + constructorArgsParams, + constructorArgsModule + ); + + // TODO: [remove-verify-subtask] librariesModule should always be string + let libraries; + if (typeof librariesModule === "object") { + libraries = librariesModule; + } else { + libraries = await resolveLibraries(librariesModule); + } + + return { + address, + constructorArgs, + libraries, + contractFQN: contract, + }; + } + ); + +subtask(TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION) + .addParam("deployedBytecode", undefined, undefined, types.any) + .addParam("matchingCompilerVersions", undefined, undefined, types.any) + .addParam("libraries", undefined, undefined, types.any) + .addOptionalParam("contractFQN") + .setAction( + async ( + { + contractFQN, + deployedBytecode, + matchingCompilerVersions, + libraries, + }: GetContractInformationArgs, + { network, artifacts } + ): Promise => { + let contractInformation: ContractInformation | null; + + if (contractFQN !== undefined) { + let artifactExists; + try { + artifactExists = await artifacts.artifactExists(contractFQN); + } catch (error) { + artifactExists = false; + } + + if (!artifactExists) { + throw new ContractNotFoundError(contractFQN); + } + + const buildInfo = await artifacts.getBuildInfo(contractFQN); + if (buildInfo === undefined) { + throw new BuildInfoNotFoundError(contractFQN); + } + + if ( + !matchingCompilerVersions.includes(buildInfo.solcVersion) && + !deployedBytecode.isOvm() + ) { + throw new BuildInfoCompilerVersionMismatchError( + contractFQN, + deployedBytecode.getVersion(), + deployedBytecode.hasVersionRange(), + buildInfo.solcVersion, + network.name + ); + } + + contractInformation = extractMatchingContractInformation( + contractFQN, + buildInfo, + deployedBytecode + ); + + if (contractInformation === null) { + throw new DeployedBytecodeMismatchError(network.name, contractFQN); + } + } else { + contractInformation = await extractInferredContractInformation( + artifacts, + network, + matchingCompilerVersions, + deployedBytecode + ); + } + + // map contractInformation libraries + const libraryInformation = await getLibraryInformation( + contractInformation, + libraries + ); + + return { + ...contractInformation, + ...libraryInformation, + }; + } + ); + +subtask(TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT) + .addParam("sourceName") + .setAction(async ({ sourceName }: GetMinimalInputArgs, { run }) => { + const cloneDeep = require("lodash.clonedeep") as typeof LodashCloneDeepT; + const dependencyGraph: DependencyGraph = await run( + TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH, + { sourceNames: [sourceName] } + ); + + const resolvedFiles = dependencyGraph + .getResolvedFiles() + .filter((resolvedFile) => resolvedFile.sourceName === sourceName); + + if (resolvedFiles.length !== 1) { + throw new UnexpectedNumberOfFilesError(); + } + + const compilationJob: CompilationJob = await run( + TASK_COMPILE_SOLIDITY_GET_COMPILATION_JOB_FOR_FILE, + { + dependencyGraph, + file: resolvedFiles[0], + } + ); + + const minimalInput: CompilerInput = await run( + TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT, + { + compilationJob, + } + ); + + return cloneDeep(minimalInput); + }); + +subtask(TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION) + .addParam("address") + .addParam("compilerInput", undefined, undefined, types.any) + .addParam("contractInformation", undefined, undefined, types.any) + .addParam("verificationInterface", undefined, undefined, types.any) + .addParam("encodedConstructorArguments") + .setAction( + async ({ + address, + compilerInput, + contractInformation, + verificationInterface, + encodedConstructorArguments, + }: AttemptVerificationArgs): Promise => { + // Ensure the linking information is present in the compiler input; + compilerInput.settings.libraries = contractInformation.libraries; + + const { message: guid } = await verificationInterface.verify( + address, + JSON.stringify(compilerInput), + `${contractInformation.sourceName}:${contractInformation.contractName}`, + `v${contractInformation.solcLongVersion}`, + encodedConstructorArguments + ); + + console.log(`Successfully submitted source code for contract +${contractInformation.sourceName}:${contractInformation.contractName} at ${address} +for verification on the block explorer. Waiting for verification result... +`); + + // Compilation is bound to take some time so there's no sense in requesting status immediately. + await sleep(700); + const verificationStatus = + await verificationInterface.getVerificationStatus(guid); + + if (!(verificationStatus.isFailure() || verificationStatus.isSuccess())) { + // Reaching this point shouldn't be possible unless the API is behaving in a new way. + throw new VerificationAPIUnexpectedMessageError( + verificationStatus.message + ); + } + + if (verificationStatus.isSuccess()) { + const contractURL = verificationInterface.getContractUrl(address); + console.log(`Successfully verified contract ${contractInformation.contractName} on the block explorer. +${contractURL}`); + } + + return { + success: verificationStatus.isSuccess(), + message: verificationStatus.message, + }; + } + ); diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts new file mode 100644 index 0000000000..40fbfb2f97 --- /dev/null +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -0,0 +1,18 @@ +import chalk from "chalk"; +import { subtask } from "hardhat/config"; + +import { TASK_VERIFY_SOURCIFY_DISABLED_WARNING } from "../task-names"; + +subtask(TASK_VERIFY_SOURCIFY_DISABLED_WARNING, async () => { + console.warn( + chalk.yellow( + `WARNING: Skipping Sourcify verification: Sourcify is disabled. To enable it, add this entry to your config: + +sourcify: { +enabled: true +} + +Learn more at https://...` + ) + ); +}); From 110860d243860829ffc00d1ae713e66921a6d2ba Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Mon, 24 Jul 2023 18:15:51 -0300 Subject: [PATCH 06/36] Rename resolve arguments taks & add sourcify placeholder --- packages/hardhat-verify/src/index.ts | 2 +- .../hardhat-verify/src/internal/task-names.ts | 3 ++- .../src/internal/tasks/etherscan.ts | 9 ++++--- .../src/internal/tasks/sourcify.ts | 15 ++++++++++- packages/hardhat-verify/test/unit/index.ts | 27 ++++++++++--------- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index 37acda2cad..dd579e26f8 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -147,7 +147,7 @@ subtask( /** * This subtask is used for backwards compatibility. * TODO [remove-verify-subtask]: if you're going to remove this subtask, - * update TASK_VERIFY_ETHERSCAN and TASK_VERIFY_RESOLVE_ARGUMENTS accordingly + * update TASK_VERIFY_ETHERSCAN and TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS accordingly */ subtask(TASK_VERIFY_VERIFY) .addOptionalParam("address") diff --git a/packages/hardhat-verify/src/internal/task-names.ts b/packages/hardhat-verify/src/internal/task-names.ts index 995d3801ae..e3103b0ac2 100644 --- a/packages/hardhat-verify/src/internal/task-names.ts +++ b/packages/hardhat-verify/src/internal/task-names.ts @@ -1,13 +1,14 @@ export const TASK_VERIFY = "verify"; export const TASK_VERIFY_GET_VERIFICATION_SUBTASKS = "verify:get-verification-subtasks"; -export const TASK_VERIFY_RESOLVE_ARGUMENTS = "verify:resolve-arguments"; export const TASK_VERIFY_VERIFY = "verify:verify"; export const TASK_VERIFY_PRINT_SUPPORTED_NETWORKS = "verify:print-supported-networks"; // Etherscan export const TASK_VERIFY_ETHERSCAN = "verify:etherscan"; +export const TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS = + "verify:etherscan-resolve-arguments"; export const TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION = "verify:etherscan-get-contract-information"; export const TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT = diff --git a/packages/hardhat-verify/src/internal/tasks/etherscan.ts b/packages/hardhat-verify/src/internal/tasks/etherscan.ts index 90c62ded05..f330cd295b 100644 --- a/packages/hardhat-verify/src/internal/tasks/etherscan.ts +++ b/packages/hardhat-verify/src/internal/tasks/etherscan.ts @@ -41,7 +41,7 @@ import { import { Bytecode } from "../solc/bytecode"; import { TASK_VERIFY_ETHERSCAN, - TASK_VERIFY_RESOLVE_ARGUMENTS, + TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS, TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION, TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT, TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, @@ -106,7 +106,10 @@ subtask(TASK_VERIFY_ETHERSCAN) constructorArgs, libraries, contractFQN, - }: VerificationArgs = await run(TASK_VERIFY_RESOLVE_ARGUMENTS, taskArgs); + }: VerificationArgs = await run( + TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS, + taskArgs + ); const chainConfig = await Etherscan.getCurrentChainConfig( network.name, @@ -215,7 +218,7 @@ This means that unrelated contracts may be displayed on Etherscan... ); }); -subtask(TASK_VERIFY_RESOLVE_ARGUMENTS) +subtask(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS) .addOptionalParam("address") .addOptionalParam("constructorArgsParams", undefined, [], types.any) .addOptionalParam("constructorArgs", undefined, undefined, types.inputFile) diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index 40fbfb2f97..5e7c61c473 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -1,7 +1,20 @@ import chalk from "chalk"; import { subtask } from "hardhat/config"; -import { TASK_VERIFY_SOURCIFY_DISABLED_WARNING } from "../task-names"; +import { + TASK_VERIFY_SOURCIFY, + TASK_VERIFY_SOURCIFY_DISABLED_WARNING, +} from "../task-names"; + +/** + * Main Sourcify verification subtask. + * + * Verifies a contract in Sourcify by coordinating various subtasks related + * to contract verification. + */ +subtask(TASK_VERIFY_SOURCIFY, async () => { + // code here +}); subtask(TASK_VERIFY_SOURCIFY_DISABLED_WARNING, async () => { console.warn( diff --git a/packages/hardhat-verify/test/unit/index.ts b/packages/hardhat-verify/test/unit/index.ts index ecc54b0f91..f00e241ae3 100644 --- a/packages/hardhat-verify/test/unit/index.ts +++ b/packages/hardhat-verify/test/unit/index.ts @@ -4,7 +4,7 @@ import sinon, { SinonStub } from "sinon"; import { TASK_VERIFY_ETHERSCAN, TASK_VERIFY_GET_VERIFICATION_SUBTASKS, - TASK_VERIFY_RESOLVE_ARGUMENTS, + TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS, TASK_VERIFY_SOURCIFY, TASK_VERIFY_SOURCIFY_DISABLED_WARNING, TASK_VERIFY_VERIFY, @@ -14,10 +14,10 @@ import { getRandomAddress, useEnvironment } from "../helpers"; describe("verify task", () => { useEnvironment("hardhat-project"); - describe(TASK_VERIFY_RESOLVE_ARGUMENTS, () => { + describe(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS, () => { it("should throw if address is not provided", async function () { await expect( - this.hre.run(TASK_VERIFY_RESOLVE_ARGUMENTS, { + this.hre.run(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS, { constructorArgsParams: [], constructorArgs: "constructor-args.js", libraries: "libraries.js", @@ -27,7 +27,7 @@ describe("verify task", () => { it("should throw if address is invalid", async function () { await expect( - this.hre.run(TASK_VERIFY_RESOLVE_ARGUMENTS, { + this.hre.run(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS, { address: "invalidAddress", constructorArgsParams: [], constructorArgs: "constructor-args.js", @@ -38,7 +38,7 @@ describe("verify task", () => { it("should throw if contract is not a fully qualified name", async function () { await expect( - this.hre.run(TASK_VERIFY_RESOLVE_ARGUMENTS, { + this.hre.run(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS, { address: getRandomAddress(this.hre), constructorArgsParams: [], constructorArgs: "constructor-args.js", @@ -67,13 +67,16 @@ describe("verify task", () => { }, contractFQN: "contracts/TestContract.sol:TestContract", }; - const processedArgs = await this.hre.run(TASK_VERIFY_RESOLVE_ARGUMENTS, { - address, - constructorArgsParams: [], - constructorArgs: "constructor-args.js", - libraries: "libraries.js", - contract: "contracts/TestContract.sol:TestContract", - }); + const processedArgs = await this.hre.run( + TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS, + { + address, + constructorArgsParams: [], + constructorArgs: "constructor-args.js", + libraries: "libraries.js", + contract: "contracts/TestContract.sol:TestContract", + } + ); assert.deepEqual(processedArgs, expectedArgs); }); From bd4bbd0a40cecba8354fa2fb057e5f211974c06a Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Mon, 24 Jul 2023 18:26:03 -0300 Subject: [PATCH 07/36] Add a new line after successful verification on etherscan --- packages/hardhat-verify/src/internal/tasks/etherscan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hardhat-verify/src/internal/tasks/etherscan.ts b/packages/hardhat-verify/src/internal/tasks/etherscan.ts index f330cd295b..5a703ab428 100644 --- a/packages/hardhat-verify/src/internal/tasks/etherscan.ts +++ b/packages/hardhat-verify/src/internal/tasks/etherscan.ts @@ -426,7 +426,7 @@ for verification on the block explorer. Waiting for verification result... if (verificationStatus.isSuccess()) { const contractURL = verificationInterface.getContractUrl(address); console.log(`Successfully verified contract ${contractInformation.contractName} on the block explorer. -${contractURL}`); +${contractURL}\n`); } return { From 65546a9bfbb73a9aba20a54bd10186528fba9f33 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Mon, 24 Jul 2023 18:37:47 -0300 Subject: [PATCH 08/36] Fix tests --- packages/hardhat-verify/test/integration/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/hardhat-verify/test/integration/index.ts b/packages/hardhat-verify/test/integration/index.ts index aa5dbf6407..e5fe36fd9a 100644 --- a/packages/hardhat-verify/test/integration/index.ts +++ b/packages/hardhat-verify/test/integration/index.ts @@ -479,7 +479,7 @@ for verification on the block explorer. Waiting for verification result... `); expect(logStub.getCall(1)).to.be .calledWith(`Successfully verified contract SimpleContract on the block explorer. -https://hardhat.etherscan.io/address/${simpleContractAddress}#code`); +https://hardhat.etherscan.io/address/${simpleContractAddress}#code\n`); logStub.restore(); assert.isUndefined(taskResponse); }); @@ -529,7 +529,7 @@ for verification on the block explorer. Waiting for verification result... `); expect(logStub.getCall(3)).to.be .calledWith(`Successfully verified contract SimpleContract on the block explorer. -https://hardhat.etherscan.io/address/${simpleContractAddress}#code`); +https://hardhat.etherscan.io/address/${simpleContractAddress}#code\n`); logStub.restore(); assert.equal(verifyCallCount, 2); assert.equal(getStatusCallCount, 2); @@ -612,7 +612,7 @@ for verification on the block explorer. Waiting for verification result... `); expect(logStub.getCall(1)).to.be .calledWith(`Successfully verified contract BothLibs on the block explorer. -https://hardhat.etherscan.io/address/${bothLibsContractAddress}#code`); +https://hardhat.etherscan.io/address/${bothLibsContractAddress}#code\n`); logStub.restore(); assert.isUndefined(taskResponse); }); From b6ade10003fcd822ffe509a71aa4143c9e1ebf31 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 25 Jul 2023 15:28:34 -0300 Subject: [PATCH 09/36] Show sourcify warning when the configuration is missing --- packages/hardhat-verify/src/index.ts | 4 ++-- packages/hardhat-verify/test/unit/index.ts | 18 ------------------ 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index dd579e26f8..09690d1def 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -119,7 +119,7 @@ subtask( subtask( TASK_VERIFY_GET_VERIFICATION_SUBTASKS, - async (_, { config }): Promise => { + async (_, { config, userConfig }): Promise => { const verificationSubtasks = []; if (config.etherscan.enabled) { @@ -128,7 +128,7 @@ subtask( if (config.sourcify.enabled) { verificationSubtasks.push(TASK_VERIFY_SOURCIFY); - } else { + } else if (userConfig.sourcify?.enabled === undefined) { verificationSubtasks.push(TASK_VERIFY_SOURCIFY_DISABLED_WARNING); } diff --git a/packages/hardhat-verify/test/unit/index.ts b/packages/hardhat-verify/test/unit/index.ts index f00e241ae3..22214b88ce 100644 --- a/packages/hardhat-verify/test/unit/index.ts +++ b/packages/hardhat-verify/test/unit/index.ts @@ -187,24 +187,6 @@ describe("verify task", () => { ); }); - it("should ignore the sourcify subtask if it is disabled", async function () { - const originalConfig = this.hre.config.sourcify; - this.hre.config.sourcify = { - enabled: false, - }; - - const verificationSubtasks: string[] = await this.hre.run( - TASK_VERIFY_GET_VERIFICATION_SUBTASKS - ); - - this.hre.config.sourcify = originalConfig; - - assert.isFalse(verificationSubtasks.includes(TASK_VERIFY_SOURCIFY)); - assert.isTrue( - verificationSubtasks.includes(TASK_VERIFY_SOURCIFY_DISABLED_WARNING) - ); - }); - it("should provide a warning message if both etherscan and sourcify are disabled", async function () { const originalEtherscanConfig = this.hre.config.etherscan; this.hre.config.etherscan = { From a32e685895a21650ae2af24e609e7c2ff9300c23 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 25 Jul 2023 15:37:53 -0300 Subject: [PATCH 10/36] Changeset --- .changeset/tough-gorillas-matter.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/tough-gorillas-matter.md diff --git a/.changeset/tough-gorillas-matter.md b/.changeset/tough-gorillas-matter.md new file mode 100644 index 0000000000..1a146741c1 --- /dev/null +++ b/.changeset/tough-gorillas-matter.md @@ -0,0 +1,8 @@ +--- +"@nomicfoundation/hardhat-verify": major +--- + +- Added the option to deactivate verification providers by using the 'enabled' field in the plugin's configuration. +- Improved error handling: Errors are now displayed at the end of the verification process, providing better visibility. +- Introduced a warning when the Sourcify configuration is missing, helping users avoid potential issues. +- Implemented enhancements to allow multiple verification providers to run simultaneously, preventing any blocking. From 8887850896e80c05b3d1aa0f8edb3a5c431648e3 Mon Sep 17 00:00:00 2001 From: Marco Castignoli Date: Tue, 22 Aug 2023 15:34:50 +0200 Subject: [PATCH 11/36] add basic Sourcify verification --- .../hardhat-verify/src/internal/sourcify.ts | 152 +++++++++++++++++ .../hardhat-verify/src/internal/task-names.ts | 2 + .../src/internal/tasks/sourcify.ts | 159 +++++++++++++++++- .../hardhat-verify/src/internal/undici.ts | 15 ++ packages/hardhat-verify/src/sourcify.ts | 1 + .../hardhat-verify/test/integration/index.ts | 55 ++++++ .../test/integration/mocks/sourcify.ts | 44 +++++ packages/hardhat-verify/test/unit/sourcify.ts | 20 +++ 8 files changed, 444 insertions(+), 4 deletions(-) create mode 100644 packages/hardhat-verify/src/internal/sourcify.ts create mode 100644 packages/hardhat-verify/src/sourcify.ts create mode 100644 packages/hardhat-verify/test/integration/mocks/sourcify.ts create mode 100644 packages/hardhat-verify/test/unit/sourcify.ts diff --git a/packages/hardhat-verify/src/internal/sourcify.ts b/packages/hardhat-verify/src/internal/sourcify.ts new file mode 100644 index 0000000000..12ec280b11 --- /dev/null +++ b/packages/hardhat-verify/src/internal/sourcify.ts @@ -0,0 +1,152 @@ +import type { Dispatcher } from "undici"; +import { + ContractVerificationRequestError, + ContractVerificationInvalidStatusCodeError, +} from "./errors"; +import { sendGetRequest, sendPostJSONRequest } from "./undici"; +import { NomicLabsHardhatPluginError } from "hardhat/plugins"; + +class HardhatSourcifyError extends NomicLabsHardhatPluginError { + constructor(message: string, parent?: Error) { + super("@nomicfoundation/hardhat-verify", message, parent); + } +} + +interface SourcifyVerifyRequestParams { + address: string; + files: { + [index: string]: string; + }; + sourceName: string; + chosenContract?: number; +} + +// Used for polling the result of the contract verification. +const VERIFICATION_STATUS_POLLING_TIME = 3000; + +export class Sourcify { + private _apiUrl: string; + private _browserUrl: string; + private _chainId: number; + + constructor(chainId: number) { + this._apiUrl = "https://sourcify.dev/server"; + this._browserUrl = "https://repo.sourcify.dev"; + this._chainId = chainId; + } + + // https://docs.sourcify.dev/docs/api/server/check-all-by-addresses/ + public async isVerified(address: string) { + const parameters = new URLSearchParams({ + addresses: address, + chainIds: `${this._chainId}`, + }); + + const url = new URL(`${this._apiUrl}/check-all-by-addresses`); + url.search = parameters.toString(); + + const response = await sendGetRequest(url); + const json = await response.body.json(); + + const contract = json.find( + (_contract: { address: string; status: string }) => + _contract.address === address + ); + if (contract.status === "perfect" || contract.status === "partial") { + return contract.status; + } else { + return false; + } + } + + // https://docs.sourcify.dev/docs/api/server/verify/ + public async verify({ + address, + files, + chosenContract, + }: SourcifyVerifyRequestParams): Promise { + const parameters = { + address, + files, + chosenContract, + chain: `${this._chainId}`, + }; + + let response: Dispatcher.ResponseData; + try { + response = await sendPostJSONRequest( + new URL(this._apiUrl), + JSON.stringify(parameters) + ); + } catch (error) { + throw new ContractVerificationRequestError(this._apiUrl, error as Error); + } + + if (!(response.statusCode >= 200 && response.statusCode <= 299)) { + // This could be always interpreted as JSON if there were any such guarantee in the Sourcify API. + const responseText = await response.body.text(); + throw new ContractVerificationInvalidStatusCodeError( + this._apiUrl, + response.statusCode, + responseText + ); + } + + const responseJson = await response.body.json(); + const sourcifyResponse = new SourcifyResponse(responseJson); + + if (!sourcifyResponse.isOk()) { + throw new HardhatSourcifyError(sourcifyResponse.error); + } + + return sourcifyResponse; + } + + public getContractUrl(address: string, _matchType: string) { + let matchType; + if (_matchType === "perfect") { + matchType = "full_match"; + } else if (_matchType === "partial") { + matchType = "partial_match"; + } else { + throw "Match type not supported"; + } + return `${this._browserUrl}/contracts/${matchType}/${this._chainId}/${address}/`; + } +} + +interface SourcifyContract { + address: string; + chainId: string; + status: string; + storageTimestamp: string; +} + +class SourcifyResponse { + public readonly error: string; + + public readonly result: SourcifyContract[]; + + constructor(response: any) { + this.error = response.error; + this.result = response.result; + } + + public isFailure() { + return this.error !== undefined; + } + + public isSuccess() { + return this.error === undefined; + } + + public isOk() { + return ( + this.result[0].status === "perfect" || this.result[0].status === "partial" + ); + } + + public getStatus() { + return this.result[0].status; + } +} diff --git a/packages/hardhat-verify/src/internal/task-names.ts b/packages/hardhat-verify/src/internal/task-names.ts index e3103b0ac2..3391fc5253 100644 --- a/packages/hardhat-verify/src/internal/task-names.ts +++ b/packages/hardhat-verify/src/internal/task-names.ts @@ -18,5 +18,7 @@ export const TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION = // Sourcify export const TASK_VERIFY_SOURCIFY = "verify:sourcify"; +export const TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION = + "verify:sourcify-attempt-verification"; export const TASK_VERIFY_SOURCIFY_DISABLED_WARNING = "verify:sourcify-disabled-warning"; diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index 5e7c61c473..d26449db5b 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -1,20 +1,171 @@ import chalk from "chalk"; import { subtask } from "hardhat/config"; +import { Sourcify } from "../sourcify"; +import { TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; + +import { + BuildInfoNotFoundError, + ContractNotFoundError, + ContractVerificationFailedError, + VerificationAPIUnexpectedMessageError, +} from "../errors"; import { TASK_VERIFY_SOURCIFY, + TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION, TASK_VERIFY_SOURCIFY_DISABLED_WARNING, } from "../task-names"; +interface VerificationResponse { + success: boolean; + message: string; +} + +interface VerificationArgs { + address: string; + constructorArgs: string[]; + contractFQN?: string; +} + +interface VerificationInterfaceVerifyParams { + address: string; + files?: { + [index: string]: string; + }; + chosenContract?: number; +} + +interface VerificationInterface { + isVerified(address: string): Promise; + verify(params: VerificationInterfaceVerifyParams): Promise; + getVerificationStatus(guid: string): Promise; + getContractUrl(address: string, status?: string): string; +} + +interface AttemptVerificationArgs { + address: string; + verificationInterface: VerificationInterface; + contractFQN: string; +} + /** * Main Sourcify verification subtask. * * Verifies a contract in Sourcify by coordinating various subtasks related * to contract verification. */ -subtask(TASK_VERIFY_SOURCIFY, async () => { - // code here -}); +subtask(TASK_VERIFY_SOURCIFY) + .addParam("address") + .addFlag("listNetworks") + .addParam("contractFQN") + .setAction( + async ( + { address, contractFQN }: VerificationArgs, + { config, network, run, artifacts } + ) => { + if (!network.config.chainId) { + console.log("Missing chainId"); + return; + } + + if (!contractFQN) { + console.log("Missing contract fully qualified name"); + return; + } + + let artifactExists; + try { + artifactExists = await artifacts.artifactExists(contractFQN); + } catch (error) { + artifactExists = false; + } + + if (!artifactExists) { + throw new ContractNotFoundError(contractFQN); + } + + const sourcify = new Sourcify(network.config.chainId); + + const status = await sourcify.isVerified(address); + if (status !== false) { + const contractURL = sourcify.getContractUrl(address, status); + console.log(`The contract ${address} has already been verified. +${contractURL}`); + return; + } + + // Make sure that contract artifacts are up-to-date + await run(TASK_COMPILE, { quiet: true }); + + // First, try to verify the contract using the minimal input + const { + success: minimalInputVerificationSuccess, + message: verificationMessage, + }: VerificationResponse = await run( + TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION, + { + address, + verificationInterface: sourcify, + contractFQN, + } + ); + + if (minimalInputVerificationSuccess) { + return; + } + + throw new ContractVerificationFailedError(verificationMessage, []); + } + ); + +subtask(TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION) + .addParam("address") + .addParam("contractFQN") + .setAction( + async ( + { address, verificationInterface, contractFQN }: AttemptVerificationArgs, + { artifacts } + ): Promise => { + const buildInfo = await artifacts.getBuildInfo(contractFQN); + if (buildInfo === undefined) { + throw new BuildInfoNotFoundError(contractFQN); + } + + const artifact = await artifacts.readArtifact(contractFQN); + const chosenContract = Object.keys(buildInfo.output.contracts).findIndex( + (source) => source === artifact.sourceName + ); + + const response = await verificationInterface.verify({ + address, + files: { + hardhatOutputBuffer: JSON.stringify(buildInfo), + }, + chosenContract, + }); + + if (!(response.isFailure() || response.isSuccess())) { + // Reaching this point shouldn't be possible unless the API is behaving in a new way. + throw new VerificationAPIUnexpectedMessageError(response.message); + } + + if (response.isSuccess()) { + const contractURL = verificationInterface.getContractUrl( + address, + response.getStatus() + ); + console.log(`Successfully verified contract ${ + contractFQN.split(":")[1] + } on Sourcify. +${contractURL}`); + } + + return { + success: response.isSuccess(), + message: "Contract successfuly verified on Sourcify", + }; + } + ); subtask(TASK_VERIFY_SOURCIFY_DISABLED_WARNING, async () => { console.warn( @@ -22,7 +173,7 @@ subtask(TASK_VERIFY_SOURCIFY_DISABLED_WARNING, async () => { `WARNING: Skipping Sourcify verification: Sourcify is disabled. To enable it, add this entry to your config: sourcify: { -enabled: true + enabled: true } Learn more at https://...` diff --git a/packages/hardhat-verify/src/internal/undici.ts b/packages/hardhat-verify/src/internal/undici.ts index e5657d5927..385ad6677d 100644 --- a/packages/hardhat-verify/src/internal/undici.ts +++ b/packages/hardhat-verify/src/internal/undici.ts @@ -36,3 +36,18 @@ function getDispatcher(): Undici.Dispatcher { return getGlobalDispatcher(); } + +export async function sendPostJSONRequest( + url: URL, + body: string +): Promise { + const { request } = await import("undici"); + const dispatcher = getDispatcher(); + + return request(url, { + dispatcher, + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + }); +} diff --git a/packages/hardhat-verify/src/sourcify.ts b/packages/hardhat-verify/src/sourcify.ts new file mode 100644 index 0000000000..7e146ea86f --- /dev/null +++ b/packages/hardhat-verify/src/sourcify.ts @@ -0,0 +1 @@ +export { Sourcify } from "./internal/sourcify"; diff --git a/packages/hardhat-verify/test/integration/index.ts b/packages/hardhat-verify/test/integration/index.ts index e5fe36fd9a..38821574e8 100644 --- a/packages/hardhat-verify/test/integration/index.ts +++ b/packages/hardhat-verify/test/integration/index.ts @@ -8,6 +8,7 @@ import { TASK_VERIFY, TASK_VERIFY_ETHERSCAN, TASK_VERIFY_VERIFY, + TASK_VERIFY_SOURCIFY, } from "../../src/internal/task-names"; import { deployContract, getRandomAddress, useEnvironment } from "../helpers"; import { @@ -16,6 +17,11 @@ import { interceptVerify, mockEnvironment, } from "./mocks/etherscan"; +import { + interceptSourcifyIsVerified, + interceptSourcifyVerify, + mockEnvironmentSourcify, +} from "./mocks/sourcify"; import "../../src/internal/type-extensions"; @@ -622,3 +628,52 @@ https://hardhat.etherscan.io/address/${bothLibsContractAddress}#code\n`); }); }); }); + +describe("verify task Sourcify's integration tests", () => { + useEnvironment("hardhat-project"); + mockEnvironmentSourcify(); + + describe("with a non-verified contract", () => { + let simpleContractAddress: string; + + before(async function () { + await this.hre.run(TASK_COMPILE, { force: true, quiet: true }); + simpleContractAddress = await deployContract( + "SimpleContract", + [], + this.hre + ); + }); + + it("should verify a contract on Sourcify", async function () { + interceptSourcifyIsVerified([ + { address: simpleContractAddress, status: "false" }, + ]); + interceptSourcifyVerify({ + result: [ + { + address: simpleContractAddress, + status: "perfect", + }, + ], + }); + const logStub = sinon.stub(console, "log"); + + const taskResponse = await this.hre.run(TASK_VERIFY_SOURCIFY, { + address: simpleContractAddress, + contractFQN: "contracts/SimpleContract.sol:SimpleContract", + }); + + assert.equal(logStub.callCount, 1); + (expect(logStub.getCall(0)).to.be as any) + .calledWith(`Successfully verified contract SimpleContract on Sourcify. +https://repo.sourcify.dev/contracts/full_match/31337/${simpleContractAddress}/`); + logStub.restore(); + assert.isUndefined(taskResponse); + }); + + after(async function () { + await this.hre.run(TASK_CLEAN); + }); + }); +}); diff --git a/packages/hardhat-verify/test/integration/mocks/sourcify.ts b/packages/hardhat-verify/test/integration/mocks/sourcify.ts new file mode 100644 index 0000000000..245acb328d --- /dev/null +++ b/packages/hardhat-verify/test/integration/mocks/sourcify.ts @@ -0,0 +1,44 @@ +import { + Dispatcher, + getGlobalDispatcher, + MockAgent, + setGlobalDispatcher, +} from "undici"; + +const mockAgent = new MockAgent({ + keepAliveTimeout: 10, + keepAliveMaxTimeout: 10, +}); + +const client = mockAgent.get("https://sourcify.dev"); + +export const mockEnvironmentSourcify = () => { + let globalDispatcher: Dispatcher; + // enable network connections for everything but etherscan API + mockAgent.enableNetConnect(/^(?!https:\/\/sourcify\.dev)/); + + before(() => { + globalDispatcher = getGlobalDispatcher(); + setGlobalDispatcher(mockAgent); + }); + + after(() => { + setGlobalDispatcher(globalDispatcher); + }); +}; + +export const interceptSourcifyIsVerified = (response: any) => + client + .intercept({ + method: "GET", + path: /\/server\/check-all-by-addresses\?addresses=0x[a-fA-F0-9]{40}&chainIds=[0-9]+/, + }) + .reply(200, response); + +export const interceptSourcifyVerify = (response: any, statusCode: number = 200) => + client + .intercept({ + path: "/server", + method: "POST", + }) + .reply(statusCode, response); diff --git a/packages/hardhat-verify/test/unit/sourcify.ts b/packages/hardhat-verify/test/unit/sourcify.ts new file mode 100644 index 0000000000..609236df6f --- /dev/null +++ b/packages/hardhat-verify/test/unit/sourcify.ts @@ -0,0 +1,20 @@ +import { assert } from "chai"; +import { Sourcify } from "../../src/sourcify"; + +describe("Sourcify", () => { + const chainId = 100; + + describe("getContractUrl", () => { + it("should return the contract url", () => { + const expectedContractAddress = + "https://repo.sourcify.dev/contracts/full_match/100/0xC4c622862a8F548997699bE24EA4bc504e5cA865/"; + let sourcify = new Sourcify(chainId); + let contractUrl = sourcify.getContractUrl( + "0xC4c622862a8F548997699bE24EA4bc504e5cA865", + "perfect" + ); + + assert.equal(contractUrl, expectedContractAddress); + }); + }); +}); From 5fbb16ceaf321733ac0c2ab72e118e88b6820955 Mon Sep 17 00:00:00 2001 From: Marco Castignoli Date: Tue, 29 Aug 2023 14:50:41 +0200 Subject: [PATCH 12/36] implement the requested fixes and features for Sourcify support * tests still missing --- packages/hardhat-verify/src/index.ts | 5 +- .../hardhat-verify/src/internal/errors.ts | 30 +++++ .../hardhat-verify/src/internal/etherscan.ts | 3 + .../hardhat-verify/src/internal/sourcify.ts | 109 ++++++++++-------- .../src/internal/tasks/etherscan.ts | 8 +- .../src/internal/tasks/sourcify.ts | 98 ++++++++-------- .../hardhat-verify/src/internal/undici.ts | 20 +--- packages/hardhat-verify/src/types.ts | 3 +- .../hardhat-project/hardhat.config.js | 9 ++ .../hardhat-verify/test/integration/index.ts | 6 +- 10 files changed, 163 insertions(+), 128 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index 09690d1def..3ff37bc4e9 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -128,7 +128,10 @@ subtask( if (config.sourcify.enabled) { verificationSubtasks.push(TASK_VERIFY_SOURCIFY); - } else if (userConfig.sourcify?.enabled === undefined) { + } else if ( + userConfig.sourcify?.enabled === undefined || + userConfig.sourcify?.enabled === false + ) { verificationSubtasks.push(TASK_VERIFY_SOURCIFY_DISABLED_WARNING); } diff --git a/packages/hardhat-verify/src/internal/errors.ts b/packages/hardhat-verify/src/internal/errors.ts index e9a8139875..93e23fde00 100644 --- a/packages/hardhat-verify/src/internal/errors.ts +++ b/packages/hardhat-verify/src/internal/errors.ts @@ -468,3 +468,33 @@ ${undetectableLibraries.map((x) => ` * ${x}`).join("\n")}` }`); } } + +export class HardhatSourcifyError extends HardhatVerifyError { + constructor(message: string) { + super(`Error while contacting the Sourcify server: ${message}`); + } +} + +export class MatchTypeNotSupportedError extends HardhatVerifyError { + constructor(matchType: string) { + super(`Match type not supported: ${matchType}`); + } +} + +export class NonUniqueContractNameError extends HardhatVerifyError { + constructor() { + super(`Non-unique contract name is used`); + } +} + +export class SourcifyHardhatNetworkNotSupportedError extends HardhatVerifyError { + constructor() { + super( + `The selected network is "hardhat", which is not supported for contract verification. Please choose a network supported by Sourcify. + +If you intended to use a different network, ensure that you provide the --network parameter when running the command. + +For example: npx hardhat verify --network ` + ); + } +} diff --git a/packages/hardhat-verify/src/internal/etherscan.ts b/packages/hardhat-verify/src/internal/etherscan.ts index 31ea6d6ada..913cfb8e1f 100644 --- a/packages/hardhat-verify/src/internal/etherscan.ts +++ b/packages/hardhat-verify/src/internal/etherscan.ts @@ -72,6 +72,9 @@ export class Etherscan { apiKey: ApiKey | undefined, chainConfig: ChainConfig ) { + if (!chainConfig.urls) { + throw new ChainConfigNotFoundError(chainConfig.chainId); + } const resolvedApiKey = resolveApiKey(apiKey, chainConfig.network); const apiUrl = chainConfig.urls.apiURL; const browserUrl = chainConfig.urls.browserURL.trim().replace(/\/$/, ""); diff --git a/packages/hardhat-verify/src/internal/sourcify.ts b/packages/hardhat-verify/src/internal/sourcify.ts index 12ec280b11..53e57e6b09 100644 --- a/packages/hardhat-verify/src/internal/sourcify.ts +++ b/packages/hardhat-verify/src/internal/sourcify.ts @@ -2,32 +2,21 @@ import type { Dispatcher } from "undici"; import { ContractVerificationRequestError, ContractVerificationInvalidStatusCodeError, + HardhatSourcifyError, + MatchTypeNotSupportedError, + SourcifyHardhatNetworkNotSupportedError, + ChainConfigNotFoundError, } from "./errors"; -import { sendGetRequest, sendPostJSONRequest } from "./undici"; -import { NomicLabsHardhatPluginError } from "hardhat/plugins"; - -class HardhatSourcifyError extends NomicLabsHardhatPluginError { - constructor(message: string, parent?: Error) { - super("@nomicfoundation/hardhat-verify", message, parent); - } -} - -interface SourcifyVerifyRequestParams { - address: string; - files: { - [index: string]: string; - }; - sourceName: string; - chosenContract?: number; -} - -// Used for polling the result of the contract verification. -const VERIFICATION_STATUS_POLLING_TIME = 3000; +import { sendGetRequest, sendPostRequest } from "./undici"; +import { EthereumProvider } from "hardhat/src/types"; +import { ChainConfig } from "../types"; +import { builtinChains } from "./chain-config"; +import { HARDHAT_NETWORK_NAME } from "hardhat/src/plugins"; export class Sourcify { - private _apiUrl: string; - private _browserUrl: string; - private _chainId: number; + public _apiUrl: string; + public _browserUrl: string; + public _chainId: number; constructor(chainId: number) { this._apiUrl = "https://sourcify.dev/server"; @@ -35,7 +24,34 @@ export class Sourcify { this._chainId = chainId; } - // https://docs.sourcify.dev/docs/api/server/check-all-by-addresses/ + public static async getCurrentChainConfig( + networkName: string, + ethereumProvider: EthereumProvider, + customChains: ChainConfig[] + ): Promise { + const currentChainId = parseInt( + await ethereumProvider.send("eth_chainId"), + 16 + ); + + const currentChainConfig = [ + // custom chains has higher precedence than builtin chains + ...[...customChains].reverse(), // the last entry has higher precedence + ...builtinChains, + ].find(({ chainId }) => chainId === currentChainId); + + if (currentChainConfig === undefined) { + if (networkName === HARDHAT_NETWORK_NAME) { + throw new SourcifyHardhatNetworkNotSupportedError(); + } + + throw new ChainConfigNotFoundError(currentChainId); + } + + return currentChainConfig; + } + + // https://sourcify.dev/server/api-docs/#/Repository/get_check_all_by_addresses public async isVerified(address: string) { const parameters = new URLSearchParams({ addresses: address, @@ -49,8 +65,7 @@ export class Sourcify { const json = await response.body.json(); const contract = json.find( - (_contract: { address: string; status: string }) => - _contract.address === address + (_contract: { address: string }) => _contract.address === address ); if (contract.status === "perfect" || contract.status === "partial") { return contract.status; @@ -59,12 +74,14 @@ export class Sourcify { } } - // https://docs.sourcify.dev/docs/api/server/verify/ - public async verify({ - address, - files, - chosenContract, - }: SourcifyVerifyRequestParams): Promise { + // https://sourcify.dev/server/api-docs/#/Stateless%20Verification/post_verify + public async verify( + address: string, + files: { + [index: string]: string; + }, + chosenContract?: number + ): Promise { const parameters = { address, files, @@ -74,21 +91,21 @@ export class Sourcify { let response: Dispatcher.ResponseData; try { - response = await sendPostJSONRequest( + response = await sendPostRequest( new URL(this._apiUrl), - JSON.stringify(parameters) + JSON.stringify(parameters), + { "Content-Type": "application/json" } ); } catch (error) { throw new ContractVerificationRequestError(this._apiUrl, error as Error); } if (!(response.statusCode >= 200 && response.statusCode <= 299)) { - // This could be always interpreted as JSON if there were any such guarantee in the Sourcify API. - const responseText = await response.body.text(); + const responseJson = await response.body.json(); throw new ContractVerificationInvalidStatusCodeError( this._apiUrl, response.statusCode, - responseText + JSON.stringify(responseJson) ); } @@ -96,7 +113,7 @@ export class Sourcify { const sourcifyResponse = new SourcifyResponse(responseJson); if (!sourcifyResponse.isOk()) { - throw new HardhatSourcifyError(sourcifyResponse.error); + throw new HardhatSourcifyError(sourcifyResponse.error || ""); } return sourcifyResponse; @@ -109,7 +126,7 @@ export class Sourcify { } else if (_matchType === "partial") { matchType = "partial_match"; } else { - throw "Match type not supported"; + throw new MatchTypeNotSupportedError(_matchType); } return `${this._browserUrl}/contracts/${matchType}/${this._chainId}/${address}/`; } @@ -132,21 +149,19 @@ class SourcifyResponse { this.result = response.result; } - public isFailure() { - return this.error !== undefined; - } - public isSuccess() { - return this.error === undefined; + return this.getError() === undefined; } public isOk() { - return ( - this.result[0].status === "perfect" || this.result[0].status === "partial" - ); + return this.getStatus() === "perfect" || this.getStatus() === "partial"; } public getStatus() { return this.result[0].status; } + + public getError() { + return this.error; + } } diff --git a/packages/hardhat-verify/src/internal/tasks/etherscan.ts b/packages/hardhat-verify/src/internal/tasks/etherscan.ts index 5a703ab428..d09e7289f5 100644 --- a/packages/hardhat-verify/src/internal/tasks/etherscan.ts +++ b/packages/hardhat-verify/src/internal/tasks/etherscan.ts @@ -99,7 +99,6 @@ subtask(TASK_VERIFY_ETHERSCAN) // TODO: [remove-verify-subtask] change to types.inputFile .addOptionalParam("libraries", undefined, undefined, types.any) .addOptionalParam("contract") - .addFlag("listNetworks") .setAction(async (taskArgs: VerifyTaskArgs, { config, network, run }) => { const { address, @@ -286,12 +285,7 @@ subtask(TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION) let contractInformation: ContractInformation | null; if (contractFQN !== undefined) { - let artifactExists; - try { - artifactExists = await artifacts.artifactExists(contractFQN); - } catch (error) { - artifactExists = false; - } + const artifactExists = await artifacts.artifactExists(contractFQN); if (!artifactExists) { throw new ContractNotFoundError(contractFQN); diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index d26449db5b..0e3977f204 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -1,5 +1,5 @@ import chalk from "chalk"; -import { subtask } from "hardhat/config"; +import { subtask, types } from "hardhat/config"; import { Sourcify } from "../sourcify"; import { TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; @@ -7,6 +7,8 @@ import { BuildInfoNotFoundError, ContractNotFoundError, ContractVerificationFailedError, + InvalidContractNameError, + NonUniqueContractNameError, VerificationAPIUnexpectedMessageError, } from "../errors"; @@ -15,6 +17,8 @@ import { TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION, TASK_VERIFY_SOURCIFY_DISABLED_WARNING, } from "../task-names"; +import { isFullyQualifiedName } from "hardhat/src/utils/contract-names"; +import { Artifact } from "hardhat/src/types"; interface VerificationResponse { success: boolean; @@ -24,27 +28,12 @@ interface VerificationResponse { interface VerificationArgs { address: string; constructorArgs: string[]; - contractFQN?: string; -} - -interface VerificationInterfaceVerifyParams { - address: string; - files?: { - [index: string]: string; - }; - chosenContract?: number; -} - -interface VerificationInterface { - isVerified(address: string): Promise; - verify(params: VerificationInterfaceVerifyParams): Promise; - getVerificationStatus(guid: string): Promise; - getContractUrl(address: string, status?: string): string; + contract?: string; } interface AttemptVerificationArgs { address: string; - verificationInterface: VerificationInterface; + verificationInterface: Sourcify; contractFQN: string; } @@ -56,35 +45,41 @@ interface AttemptVerificationArgs { */ subtask(TASK_VERIFY_SOURCIFY) .addParam("address") - .addFlag("listNetworks") - .addParam("contractFQN") + .addOptionalParam("contract") .setAction( async ( - { address, contractFQN }: VerificationArgs, + { address, contract }: VerificationArgs, { config, network, run, artifacts } ) => { - if (!network.config.chainId) { - console.log("Missing chainId"); + if (!contract) { + console.log( + "In order to verify on Sourcify you must provide a contract fully qualified name" + ); return; } - if (!contractFQN) { - console.log("Missing contract fully qualified name"); + const chainConfig = await Sourcify.getCurrentChainConfig( + network.name, + network.provider, + config.sourcify.customChains || [] + ); + + if (!chainConfig.chainId) { + console.log("Missing chainId"); return; } - let artifactExists; - try { - artifactExists = await artifacts.artifactExists(contractFQN); - } catch (error) { - artifactExists = false; + if (contract !== undefined && !isFullyQualifiedName(contract)) { + throw new InvalidContractNameError(contract || ""); } + const artifactExists = await artifacts.artifactExists(contract); + if (!artifactExists) { - throw new ContractNotFoundError(contractFQN); + throw new ContractNotFoundError(contract); } - const sourcify = new Sourcify(network.config.chainId); + const sourcify = new Sourcify(chainConfig.chainId); const status = await sourcify.isVerified(address); if (status !== false) { @@ -94,23 +89,18 @@ ${contractURL}`); return; } - // Make sure that contract artifacts are up-to-date - await run(TASK_COMPILE, { quiet: true }); - - // First, try to verify the contract using the minimal input const { - success: minimalInputVerificationSuccess, + success: verificationSuccess, message: verificationMessage, }: VerificationResponse = await run( TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION, { address, verificationInterface: sourcify, - contractFQN, + contractFQN: contract, } ); - - if (minimalInputVerificationSuccess) { + if (verificationSuccess) { return; } @@ -121,6 +111,7 @@ ${contractURL}`); subtask(TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION) .addParam("address") .addParam("contractFQN") + .addParam("verificationInterface", undefined, undefined, types.any) .setAction( async ( { address, verificationInterface, contractFQN }: AttemptVerificationArgs, @@ -131,25 +122,30 @@ subtask(TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION) throw new BuildInfoNotFoundError(contractFQN); } - const artifact = await artifacts.readArtifact(contractFQN); + let artifact: Artifact; + try { + artifact = await artifacts.readArtifact(contractFQN); + } catch (e) { + throw new NonUniqueContractNameError(); + } + const chosenContract = Object.keys(buildInfo.output.contracts).findIndex( (source) => source === artifact.sourceName ); - const response = await verificationInterface.verify({ + if (chosenContract === -1) { + throw new ContractNotFoundError(artifact.sourceName); + } + + const response = await verificationInterface.verify( address, - files: { + { hardhatOutputBuffer: JSON.stringify(buildInfo), }, - chosenContract, - }); - - if (!(response.isFailure() || response.isSuccess())) { - // Reaching this point shouldn't be possible unless the API is behaving in a new way. - throw new VerificationAPIUnexpectedMessageError(response.message); - } + chosenContract + ); - if (response.isSuccess()) { + if (response.isOk()) { const contractURL = verificationInterface.getContractUrl( address, response.getStatus() diff --git a/packages/hardhat-verify/src/internal/undici.ts b/packages/hardhat-verify/src/internal/undici.ts index 385ad6677d..39c045f921 100644 --- a/packages/hardhat-verify/src/internal/undici.ts +++ b/packages/hardhat-verify/src/internal/undici.ts @@ -14,7 +14,8 @@ export async function sendGetRequest( export async function sendPostRequest( url: URL, - body: string + body: string, + headers: Record = {} ): Promise { const { request } = await import("undici"); const dispatcher = getDispatcher(); @@ -22,7 +23,7 @@ export async function sendPostRequest( return request(url, { dispatcher, method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, + headers, body, }); } @@ -36,18 +37,3 @@ function getDispatcher(): Undici.Dispatcher { return getGlobalDispatcher(); } - -export async function sendPostJSONRequest( - url: URL, - body: string -): Promise { - const { request } = await import("undici"); - const dispatcher = getDispatcher(); - - return request(url, { - dispatcher, - method: "POST", - headers: { "Content-Type": "application/json" }, - body, - }); -} diff --git a/packages/hardhat-verify/src/types.ts b/packages/hardhat-verify/src/types.ts index 405b37df5f..40544ff38e 100644 --- a/packages/hardhat-verify/src/types.ts +++ b/packages/hardhat-verify/src/types.ts @@ -1,7 +1,7 @@ export interface ChainConfig { network: string; chainId: number; - urls: { + urls?: { apiURL: string; browserURL: string; }; @@ -15,6 +15,7 @@ export interface EtherscanConfig { export interface SourcifyConfig { enabled: boolean; + customChains?: ChainConfig[]; } export type ApiKey = string | Record; diff --git a/packages/hardhat-verify/test/fixture-projects/hardhat-project/hardhat.config.js b/packages/hardhat-verify/test/fixture-projects/hardhat-project/hardhat.config.js index a04064c1fd..cc01486a97 100644 --- a/packages/hardhat-verify/test/fixture-projects/hardhat-project/hardhat.config.js +++ b/packages/hardhat-verify/test/fixture-projects/hardhat-project/hardhat.config.js @@ -28,4 +28,13 @@ module.exports = { }, ], }, + sourcify: { + enabled: false, + customChains: [ + { + network: "hardhat", + chainId: 31337, + }, + ], + }, }; diff --git a/packages/hardhat-verify/test/integration/index.ts b/packages/hardhat-verify/test/integration/index.ts index 38821574e8..8097cb7abc 100644 --- a/packages/hardhat-verify/test/integration/index.ts +++ b/packages/hardhat-verify/test/integration/index.ts @@ -230,9 +230,7 @@ https://hardhat.etherscan.io/address/${address}#code` contract: contractFQN, }) ).to.be.rejectedWith( - new RegExp( - `The contract ${contractFQN} is not present in your project.` - ) + new RegExp(`HH700: Artifact for contract "${contractFQN}" not found. `) ); }); @@ -661,7 +659,7 @@ describe("verify task Sourcify's integration tests", () => { const taskResponse = await this.hre.run(TASK_VERIFY_SOURCIFY, { address: simpleContractAddress, - contractFQN: "contracts/SimpleContract.sol:SimpleContract", + contract: "contracts/SimpleContract.sol:SimpleContract", }); assert.equal(logStub.callCount, 1); From 7468d9ee70dd9ca806f596644ed4d999cd89365d Mon Sep 17 00:00:00 2001 From: Marco Castignoli Date: Mon, 18 Sep 2023 10:29:21 +0200 Subject: [PATCH 13/36] fix lint --- .../hardhat-verify/src/internal/etherscan.ts | 2 +- .../hardhat-verify/src/internal/sourcify.ts | 12 +++++----- .../src/internal/tasks/sourcify.ts | 23 ++++++++++++------- .../test/integration/mocks/sourcify.ts | 5 +++- packages/hardhat-verify/test/unit/sourcify.ts | 4 ++-- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/hardhat-verify/src/internal/etherscan.ts b/packages/hardhat-verify/src/internal/etherscan.ts index 913cfb8e1f..310dd50c10 100644 --- a/packages/hardhat-verify/src/internal/etherscan.ts +++ b/packages/hardhat-verify/src/internal/etherscan.ts @@ -72,7 +72,7 @@ export class Etherscan { apiKey: ApiKey | undefined, chainConfig: ChainConfig ) { - if (!chainConfig.urls) { + if (chainConfig.urls === undefined) { throw new ChainConfigNotFoundError(chainConfig.chainId); } const resolvedApiKey = resolveApiKey(apiKey, chainConfig.network); diff --git a/packages/hardhat-verify/src/internal/sourcify.ts b/packages/hardhat-verify/src/internal/sourcify.ts index 53e57e6b09..7fe1376754 100644 --- a/packages/hardhat-verify/src/internal/sourcify.ts +++ b/packages/hardhat-verify/src/internal/sourcify.ts @@ -1,4 +1,7 @@ import type { Dispatcher } from "undici"; +import { EthereumProvider } from "hardhat/types"; +import { HARDHAT_NETWORK_NAME } from "hardhat/plugins"; +import { ChainConfig } from "../types"; import { ContractVerificationRequestError, ContractVerificationInvalidStatusCodeError, @@ -8,10 +11,7 @@ import { ChainConfigNotFoundError, } from "./errors"; import { sendGetRequest, sendPostRequest } from "./undici"; -import { EthereumProvider } from "hardhat/src/types"; -import { ChainConfig } from "../types"; import { builtinChains } from "./chain-config"; -import { HARDHAT_NETWORK_NAME } from "hardhat/src/plugins"; export class Sourcify { public _apiUrl: string; @@ -101,11 +101,11 @@ export class Sourcify { } if (!(response.statusCode >= 200 && response.statusCode <= 299)) { - const responseJson = await response.body.json(); + const responseErrorJson = await response.body.json(); throw new ContractVerificationInvalidStatusCodeError( this._apiUrl, response.statusCode, - JSON.stringify(responseJson) + JSON.stringify(responseErrorJson) ); } @@ -113,7 +113,7 @@ export class Sourcify { const sourcifyResponse = new SourcifyResponse(responseJson); if (!sourcifyResponse.isOk()) { - throw new HardhatSourcifyError(sourcifyResponse.error || ""); + throw new HardhatSourcifyError(sourcifyResponse.error); } return sourcifyResponse; diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index 0e3977f204..cd331fb82b 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -1,7 +1,9 @@ import chalk from "chalk"; import { subtask, types } from "hardhat/config"; +import { isFullyQualifiedName } from "hardhat/utils/contract-names"; +import { Artifact } from "hardhat/types"; +import { ChainConfig } from "../../types"; import { Sourcify } from "../sourcify"; -import { TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; import { BuildInfoNotFoundError, @@ -9,7 +11,6 @@ import { ContractVerificationFailedError, InvalidContractNameError, NonUniqueContractNameError, - VerificationAPIUnexpectedMessageError, } from "../errors"; import { @@ -17,8 +18,6 @@ import { TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION, TASK_VERIFY_SOURCIFY_DISABLED_WARNING, } from "../task-names"; -import { isFullyQualifiedName } from "hardhat/src/utils/contract-names"; -import { Artifact } from "hardhat/src/types"; interface VerificationResponse { success: boolean; @@ -51,26 +50,34 @@ subtask(TASK_VERIFY_SOURCIFY) { address, contract }: VerificationArgs, { config, network, run, artifacts } ) => { - if (!contract) { + if (contract === undefined) { console.log( "In order to verify on Sourcify you must provide a contract fully qualified name" ); return; } + let customChains: ChainConfig[] = []; + if ( + config.sourcify.customChains !== undefined && + config.sourcify.customChains.length > 0 + ) { + customChains = config.sourcify.customChains; + } + const chainConfig = await Sourcify.getCurrentChainConfig( network.name, network.provider, - config.sourcify.customChains || [] + customChains ); - if (!chainConfig.chainId) { + if (chainConfig.chainId === undefined) { console.log("Missing chainId"); return; } if (contract !== undefined && !isFullyQualifiedName(contract)) { - throw new InvalidContractNameError(contract || ""); + throw new InvalidContractNameError(contract); } const artifactExists = await artifacts.artifactExists(contract); diff --git a/packages/hardhat-verify/test/integration/mocks/sourcify.ts b/packages/hardhat-verify/test/integration/mocks/sourcify.ts index 245acb328d..e38308b938 100644 --- a/packages/hardhat-verify/test/integration/mocks/sourcify.ts +++ b/packages/hardhat-verify/test/integration/mocks/sourcify.ts @@ -35,7 +35,10 @@ export const interceptSourcifyIsVerified = (response: any) => }) .reply(200, response); -export const interceptSourcifyVerify = (response: any, statusCode: number = 200) => +export const interceptSourcifyVerify = ( + response: any, + statusCode: number = 200 +) => client .intercept({ path: "/server", diff --git a/packages/hardhat-verify/test/unit/sourcify.ts b/packages/hardhat-verify/test/unit/sourcify.ts index 609236df6f..c4dcbf1c18 100644 --- a/packages/hardhat-verify/test/unit/sourcify.ts +++ b/packages/hardhat-verify/test/unit/sourcify.ts @@ -8,8 +8,8 @@ describe("Sourcify", () => { it("should return the contract url", () => { const expectedContractAddress = "https://repo.sourcify.dev/contracts/full_match/100/0xC4c622862a8F548997699bE24EA4bc504e5cA865/"; - let sourcify = new Sourcify(chainId); - let contractUrl = sourcify.getContractUrl( + const sourcify = new Sourcify(chainId); + const contractUrl = sourcify.getContractUrl( "0xC4c622862a8F548997699bE24EA4bc504e5cA865", "perfect" ); From 0004735c3036ab6dd08ac527c19b927b07509801 Mon Sep 17 00:00:00 2001 From: Marco Castignoli Date: Tue, 19 Sep 2023 09:42:52 +0200 Subject: [PATCH 14/36] fix chosenContract type from number to string --- packages/hardhat-verify/src/internal/sourcify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hardhat-verify/src/internal/sourcify.ts b/packages/hardhat-verify/src/internal/sourcify.ts index 7fe1376754..e610019bc7 100644 --- a/packages/hardhat-verify/src/internal/sourcify.ts +++ b/packages/hardhat-verify/src/internal/sourcify.ts @@ -85,7 +85,7 @@ export class Sourcify { const parameters = { address, files, - chosenContract, + chosenContract: `${chosenContract}`, chain: `${this._chainId}`, }; From 1032446884a0db0c7a65bc0dfda47beedb4b27ba Mon Sep 17 00:00:00 2001 From: Marco Castignoli Date: Tue, 19 Sep 2023 10:03:16 +0200 Subject: [PATCH 15/36] fix lint --- packages/hardhat-verify/src/internal/sourcify.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/hardhat-verify/src/internal/sourcify.ts b/packages/hardhat-verify/src/internal/sourcify.ts index e610019bc7..3ff9c38165 100644 --- a/packages/hardhat-verify/src/internal/sourcify.ts +++ b/packages/hardhat-verify/src/internal/sourcify.ts @@ -82,13 +82,16 @@ export class Sourcify { }, chosenContract?: number ): Promise { - const parameters = { + const parameters: any = { address, files, - chosenContract: `${chosenContract}`, chain: `${this._chainId}`, }; + if (chosenContract !== undefined) { + parameters.chosenContract = `${chosenContract}`; + } + let response: Dispatcher.ResponseData; try { response = await sendPostRequest( From a119326070d2b960f4b235f8a7df829df9a02e19 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Fri, 13 Oct 2023 18:24:34 -0300 Subject: [PATCH 16/36] Add headers to Etherscan post request --- packages/hardhat-verify/src/internal/etherscan.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/hardhat-verify/src/internal/etherscan.ts b/packages/hardhat-verify/src/internal/etherscan.ts index 310dd50c10..b8dd4a5edc 100644 --- a/packages/hardhat-verify/src/internal/etherscan.ts +++ b/packages/hardhat-verify/src/internal/etherscan.ts @@ -147,7 +147,8 @@ export class Etherscan { try { response = await sendPostRequest( new URL(this.apiUrl), - parameters.toString() + parameters.toString(), + { "Content-Type": "application/x-www-form-urlencoded" } ); } catch (error: any) { throw new ContractVerificationRequestError(this.apiUrl, error); From dc4e5adc4628f169b64f14e7a127e371dd931f64 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Fri, 13 Oct 2023 18:33:15 -0300 Subject: [PATCH 17/36] Change Sourcify warning to info --- packages/hardhat-verify/src/index.ts | 2 +- packages/hardhat-verify/src/internal/tasks/sourcify.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index 3ff37bc4e9..80e204338b 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -138,7 +138,7 @@ subtask( if (!config.etherscan.enabled && !config.sourcify.enabled) { console.warn( chalk.yellow( - `WARNING: No verification services are enabled. Please enable at least one verification service in your configuration.` + `[WARNING] No verification services are enabled. Please enable at least one verification service in your configuration.` ) ); } diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index cd331fb82b..45a7e15680 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -171,9 +171,9 @@ ${contractURL}`); ); subtask(TASK_VERIFY_SOURCIFY_DISABLED_WARNING, async () => { - console.warn( - chalk.yellow( - `WARNING: Skipping Sourcify verification: Sourcify is disabled. To enable it, add this entry to your config: + console.info( + chalk.cyan( + `[INFO] Skipping Sourcify verification: Sourcify is disabled. To enable it, add this entry to your config: sourcify: { enabled: true From b7d3c4877167765588e2135a7f653f584275ac82 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Fri, 13 Oct 2023 18:54:27 -0300 Subject: [PATCH 18/36] Add readme placeholder for sourcify --- packages/hardhat-verify/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/hardhat-verify/README.md b/packages/hardhat-verify/README.md index f55de8ee9d..0fe62ebc45 100644 --- a/packages/hardhat-verify/README.md +++ b/packages/hardhat-verify/README.md @@ -262,6 +262,8 @@ if (!instance.isVerified("0x123abc...")) { } ``` +### Verifying on Sourcify + ## How it works The plugin works by fetching the bytecode in the given address and using it to check which contract in your project corresponds to it. Besides that, some sanity checks are performed locally to make sure that the verification won't fail. From a6c3433f7951b2f15004774dbeb26b98fd5fa32b Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Fri, 13 Oct 2023 18:55:06 -0300 Subject: [PATCH 19/36] Display the sourcify warning as the first message in the output --- packages/hardhat-verify/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index 80e204338b..a5f43a9403 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -132,7 +132,7 @@ subtask( userConfig.sourcify?.enabled === undefined || userConfig.sourcify?.enabled === false ) { - verificationSubtasks.push(TASK_VERIFY_SOURCIFY_DISABLED_WARNING); + verificationSubtasks.unshift(TASK_VERIFY_SOURCIFY_DISABLED_WARNING); } if (!config.etherscan.enabled && !config.sourcify.enabled) { From 9028b988619e1784c9eb62351a31702e579757e2 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Fri, 13 Oct 2023 18:55:30 -0300 Subject: [PATCH 20/36] Improve sourcify info message --- packages/hardhat-verify/src/internal/tasks/sourcify.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index 45a7e15680..0a2985b02f 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -173,13 +173,13 @@ ${contractURL}`); subtask(TASK_VERIFY_SOURCIFY_DISABLED_WARNING, async () => { console.info( chalk.cyan( - `[INFO] Skipping Sourcify verification: Sourcify is disabled. To enable it, add this entry to your config: + `[INFO] Sourcify Verification Skipped: Sourcify verification is currently disabled. To enable it, add the following entry to your Hardhat configuration: sourcify: { enabled: true } -Learn more at https://...` +For more information, visit https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify#verifying-on-sourcify` ) ); }); From a39462071a193936bc37be28d65032689455b81c Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Sat, 14 Oct 2023 18:13:10 -0300 Subject: [PATCH 21/36] Improve error handing on verification providers --- .../hardhat-verify/src/internal/errors.ts | 47 ++-- .../hardhat-verify/src/internal/etherscan.ts | 103 +++++---- .../src/internal/etherscan.types.ts | 47 ++++ .../hardhat-verify/src/internal/sourcify.ts | 205 +++++++++++------- .../src/internal/sourcify.types.ts | 40 ++++ .../src/internal/tasks/sourcify.ts | 2 +- .../hardhat-verify/src/internal/undici.ts | 4 + .../hardhat-verify/src/internal/utilities.ts | 7 + .../hardhat-verify/test/integration/index.ts | 16 +- packages/hardhat-verify/test/unit/index.ts | 2 +- packages/hardhat-verify/test/unit/sourcify.ts | 3 +- 11 files changed, 317 insertions(+), 159 deletions(-) create mode 100644 packages/hardhat-verify/src/internal/etherscan.types.ts create mode 100644 packages/hardhat-verify/src/internal/sourcify.types.ts diff --git a/packages/hardhat-verify/src/internal/errors.ts b/packages/hardhat-verify/src/internal/errors.ts index 93e23fde00..498d236638 100644 --- a/packages/hardhat-verify/src/internal/errors.ts +++ b/packages/hardhat-verify/src/internal/errors.ts @@ -132,21 +132,17 @@ To see the list of supported networks, run this command: } } -export class ContractVerificationRequestError extends HardhatVerifyError { - constructor(url: string, parent: Error) { - super( - `Failed to send contract verification request. -Endpoint URL: ${url} -Reason: ${parent.message}`, - parent - ); - } -} - export class ContractVerificationInvalidStatusCodeError extends HardhatVerifyError { - constructor(url: string, statusCode: number, responseText: string) { + constructor( + url: string, + statusCode: number, + responseText: string, + requestBody?: string + ) { super(`Failed to send contract verification request. -Endpoint URL: ${url} +Endpoint URL: ${url}${ + requestBody !== undefined ? `\nRequest body: ${requestBody}\n` : "" + } The HTTP server response is not ok. Status code: ${statusCode} Response text: ${responseText}`); } } @@ -453,6 +449,19 @@ Message: ${message}`); } } +export class UnexpectedError extends HardhatVerifyError { + constructor(e: unknown, functionName: string) { + const defaultErrorDetails = `Unexpected error in ${functionName}`; + const errorDetails = + e instanceof Error + ? e.message ?? defaultErrorDetails + : defaultErrorDetails; + super(`An unexpected error occurred during the verification process. +Please report this issue to the Hardhat team. +Error Details: ${errorDetails}`); + } +} + export class ContractVerificationFailedError extends HardhatVerifyError { constructor(message: string, undetectableLibraries: string[]) { super(`The contract verification failed. @@ -469,18 +478,6 @@ ${undetectableLibraries.map((x) => ` * ${x}`).join("\n")}` } } -export class HardhatSourcifyError extends HardhatVerifyError { - constructor(message: string) { - super(`Error while contacting the Sourcify server: ${message}`); - } -} - -export class MatchTypeNotSupportedError extends HardhatVerifyError { - constructor(matchType: string) { - super(`Match type not supported: ${matchType}`); - } -} - export class NonUniqueContractNameError extends HardhatVerifyError { constructor() { super(`Non-unique contract name is used`); diff --git a/packages/hardhat-verify/src/internal/etherscan.ts b/packages/hardhat-verify/src/internal/etherscan.ts index b8dd4a5edc..2e1e0e8df2 100644 --- a/packages/hardhat-verify/src/internal/etherscan.ts +++ b/packages/hardhat-verify/src/internal/etherscan.ts @@ -1,13 +1,16 @@ import type { Dispatcher } from "undici"; import type { EthereumProvider } from "hardhat/types"; import type { ChainConfig, ApiKey } from "../types"; +import type { + EtherscanGetSourceCodeResponse, + EtherscanVerifyResponse, +} from "./etherscan.types"; import { HARDHAT_NETWORK_NAME } from "hardhat/plugins"; import { ContractStatusPollingError, ContractStatusPollingInvalidStatusCodeError, - ContractVerificationRequestError, ContractVerificationMissingBytecodeError, ContractVerificationInvalidStatusCodeError, HardhatVerifyError, @@ -15,9 +18,10 @@ import { ContractStatusPollingResponseNotOkError, ChainConfigNotFoundError, HardhatNetworkNotSupportedError, + UnexpectedError, } from "./errors"; -import { sendGetRequest, sendPostRequest } from "./undici"; -import { sleep } from "./utilities"; +import { isSuccessStatusCode, sendGetRequest, sendPostRequest } from "./undici"; +import { ValidationResponse, sleep } from "./utilities"; import { builtinChains } from "./chain-config"; // Used for polling the result of the contract verification. @@ -99,15 +103,29 @@ export class Etherscan { const url = new URL(this.apiUrl); url.search = parameters.toString(); - const response = await sendGetRequest(url); - const json: any = await response.body.json(); + try { + const response: Dispatcher.ResponseData = await sendGetRequest(url); + const json: EtherscanGetSourceCodeResponse = await response.body.json(); + + if (!isSuccessStatusCode(response.statusCode)) { + throw new ContractVerificationInvalidStatusCodeError( + url.toString(), + response.statusCode, + JSON.stringify(json) + ); + } - if (json.message !== "OK") { - return false; - } + if (json.message !== "OK") { + return false; + } - const sourceCode = json?.result?.[0]?.SourceCode; - return sourceCode !== undefined && sourceCode !== ""; + return json.result[0].ABI !== "Contract source code not verified"; + } catch (e) { + if (e instanceof ContractVerificationInvalidStatusCodeError) { + throw e; + } + throw new UnexpectedError(e, "Etherscan.isVerified"); + } } /** @@ -143,41 +161,47 @@ export class Etherscan { constructorArguements: constructorArguments, }); - let response: Dispatcher.ResponseData; + const url = new URL(this.apiUrl); try { - response = await sendPostRequest( - new URL(this.apiUrl), + const response: Dispatcher.ResponseData = await sendPostRequest( + url, parameters.toString(), { "Content-Type": "application/x-www-form-urlencoded" } ); - } catch (error: any) { - throw new ContractVerificationRequestError(this.apiUrl, error); - } + const json: EtherscanVerifyResponse = await response.body.json(); + + if (!isSuccessStatusCode(response.statusCode)) { + throw new ContractVerificationInvalidStatusCodeError( + url.toString(), + response.statusCode, + JSON.stringify(json), + parameters.toString() + ); + } - if (!(response.statusCode >= 200 && response.statusCode <= 299)) { - // This could be always interpreted as JSON if there were any such guarantee in the Etherscan API. - const responseText = await response.body.text(); - throw new ContractVerificationInvalidStatusCodeError( - this.apiUrl, - response.statusCode, - responseText - ); - } + const etherscanResponse = new EtherscanResponse(json); - const etherscanResponse = new EtherscanResponse(await response.body.json()); + if (etherscanResponse.isBytecodeMissingInNetworkError()) { + throw new ContractVerificationMissingBytecodeError( + this.apiUrl, + contractAddress + ); + } - if (etherscanResponse.isBytecodeMissingInNetworkError()) { - throw new ContractVerificationMissingBytecodeError( - this.apiUrl, - contractAddress - ); - } + if (!etherscanResponse.isOk()) { + throw new HardhatVerifyError(etherscanResponse.message); + } - if (!etherscanResponse.isOk()) { - throw new HardhatVerifyError(etherscanResponse.message); + return etherscanResponse; + } catch (e) { + if ( + e instanceof ContractVerificationInvalidStatusCodeError || + e instanceof ContractVerificationMissingBytecodeError + ) { + throw e; + } + throw new UnexpectedError(e, "Etherscan.verify"); } - - return etherscanResponse; } /** @@ -207,7 +231,7 @@ export class Etherscan { throw new ContractStatusPollingError(url.toString(), error); } - if (!(response.statusCode >= 200 && response.statusCode <= 299)) { + if (!isSuccessStatusCode(response.statusCode)) { // This could be always interpreted as JSON if there were any such guarantee in the Etherscan API. const responseText = await response.body.text(); @@ -248,12 +272,11 @@ export class Etherscan { } } -class EtherscanResponse { +class EtherscanResponse implements ValidationResponse { public readonly status: number; - public readonly message: string; - constructor(response: any) { + constructor(response: EtherscanVerifyResponse) { this.status = parseInt(response.status, 10); this.message = response.result; } diff --git a/packages/hardhat-verify/src/internal/etherscan.types.ts b/packages/hardhat-verify/src/internal/etherscan.types.ts new file mode 100644 index 0000000000..b9268f02bb --- /dev/null +++ b/packages/hardhat-verify/src/internal/etherscan.types.ts @@ -0,0 +1,47 @@ +interface EtherscanGetSourceCodeNotOkResponse { + status: "0"; + message: "NOTOK"; + result: string; +} + +interface EtherscanGetSourceCodeOkResponse { + status: "1"; + message: "OK"; + result: EtherscanContract[]; +} + +interface EtherscanContract { + SourceCode: string; + ABI: string; + ContractName: string; + CompilerVersion: string; + OptimizationUsed: string; + Runs: string; + ConstructorArguments: string; + EVMVersion: string; + Library: string; + LicenseType: string; + Proxy: string; + Implementation: string; + SwarmSource: string; +} + +export type EtherscanGetSourceCodeResponse = + | EtherscanGetSourceCodeNotOkResponse + | EtherscanGetSourceCodeOkResponse; + +interface EtherscanVerifyNotOkResponse { + status: "0"; + message: "NOTOK"; + result: string; +} + +interface EtherscanVerifyOkResponse { + status: "1"; + message: "OK"; + result: string; +} + +export type EtherscanVerifyResponse = + | EtherscanVerifyNotOkResponse + | EtherscanVerifyOkResponse; diff --git a/packages/hardhat-verify/src/internal/sourcify.ts b/packages/hardhat-verify/src/internal/sourcify.ts index 3ff9c38165..e1eb480900 100644 --- a/packages/hardhat-verify/src/internal/sourcify.ts +++ b/packages/hardhat-verify/src/internal/sourcify.ts @@ -1,28 +1,28 @@ import type { Dispatcher } from "undici"; -import { EthereumProvider } from "hardhat/types"; +import type { EthereumProvider } from "hardhat/types"; +import type { ChainConfig } from "../types"; +import type { + SourcifyIsVerifiedResponse, + SourcifyVerifyResponse, +} from "./sourcify.types"; + import { HARDHAT_NETWORK_NAME } from "hardhat/plugins"; -import { ChainConfig } from "../types"; import { - ContractVerificationRequestError, ContractVerificationInvalidStatusCodeError, - HardhatSourcifyError, - MatchTypeNotSupportedError, SourcifyHardhatNetworkNotSupportedError, ChainConfigNotFoundError, + UnexpectedError, } from "./errors"; -import { sendGetRequest, sendPostRequest } from "./undici"; +import { isSuccessStatusCode, sendGetRequest, sendPostRequest } from "./undici"; import { builtinChains } from "./chain-config"; +import { ContractStatus } from "./sourcify.types"; +import { ValidationResponse } from "./utilities"; export class Sourcify { - public _apiUrl: string; - public _browserUrl: string; - public _chainId: number; - - constructor(chainId: number) { - this._apiUrl = "https://sourcify.dev/server"; - this._browserUrl = "https://repo.sourcify.dev"; - this._chainId = chainId; - } + public apiUrl: string = "https://sourcify.dev/server"; + public browserUrl: string = "https://repo.sourcify.dev"; + + constructor(public chainId: number) {} public static async getCurrentChainConfig( networkName: string, @@ -55,116 +55,155 @@ export class Sourcify { public async isVerified(address: string) { const parameters = new URLSearchParams({ addresses: address, - chainIds: `${this._chainId}`, + chainIds: `${this.chainId}`, }); - const url = new URL(`${this._apiUrl}/check-all-by-addresses`); + const url = new URL(`${this.apiUrl}/check-all-by-addresses`); url.search = parameters.toString(); - const response = await sendGetRequest(url); - const json = await response.body.json(); + try { + const response: Dispatcher.ResponseData = await sendGetRequest(url); + const json: SourcifyIsVerifiedResponse[] = await response.body.json(); + + if (!isSuccessStatusCode(response.statusCode)) { + throw new ContractVerificationInvalidStatusCodeError( + url.toString(), + response.statusCode, + JSON.stringify(json) + ); + } + + if (!Array.isArray(json)) { + throw new Error(`Unexpected response body: ${JSON.stringify(json)}`); + } - const contract = json.find( - (_contract: { address: string }) => _contract.address === address - ); - if (contract.status === "perfect" || contract.status === "partial") { - return contract.status; - } else { - return false; + const contract = json.find((match) => match.address === address); + if (contract === undefined) { + return false; + } + + if ( + "status" in contract && + contract.status === ContractStatus.NOT_FOUND + ) { + return false; + } + + if ("chainIds" in contract && contract.chainIds.length === 1) { + const { status } = contract.chainIds[0]; + if ( + status === ContractStatus.PERFECT || + status === ContractStatus.PARTIAL + ) { + return status; + } + } + + throw new Error(`Unexpected response body: ${JSON.stringify(json)}`); + } catch (e) { + if (e instanceof ContractVerificationInvalidStatusCodeError) { + throw e; + } + throw new UnexpectedError(e, "Sourcify.isVerified"); } } // https://sourcify.dev/server/api-docs/#/Stateless%20Verification/post_verify public async verify( address: string, - files: { - [index: string]: string; - }, + files: Record, chosenContract?: number ): Promise { const parameters: any = { address, files, - chain: `${this._chainId}`, + chain: `${this.chainId}`, }; if (chosenContract !== undefined) { parameters.chosenContract = `${chosenContract}`; } - let response: Dispatcher.ResponseData; + const url = new URL(this.apiUrl); try { - response = await sendPostRequest( - new URL(this._apiUrl), + const response: Dispatcher.ResponseData = await sendPostRequest( + url, JSON.stringify(parameters), { "Content-Type": "application/json" } ); - } catch (error) { - throw new ContractVerificationRequestError(this._apiUrl, error as Error); - } + const json: SourcifyVerifyResponse = await response.body.json(); + + if (!isSuccessStatusCode(response.statusCode)) { + throw new ContractVerificationInvalidStatusCodeError( + url.toString(), + response.statusCode, + JSON.stringify(json), + JSON.stringify(parameters) + ); + } - if (!(response.statusCode >= 200 && response.statusCode <= 299)) { - const responseErrorJson = await response.body.json(); - throw new ContractVerificationInvalidStatusCodeError( - this._apiUrl, - response.statusCode, - JSON.stringify(responseErrorJson) - ); - } + const sourcifyResponse = new SourcifyResponse(json); - const responseJson = await response.body.json(); - const sourcifyResponse = new SourcifyResponse(responseJson); + if (!sourcifyResponse.isOk()) { + throw new Error(`Verify response is not ok: ${JSON.stringify(json)}`); + } - if (!sourcifyResponse.isOk()) { - throw new HardhatSourcifyError(sourcifyResponse.error); + return sourcifyResponse; + } catch (e) { + if (e instanceof ContractVerificationInvalidStatusCodeError) { + throw e; + } + throw new UnexpectedError(e, "Sourcify.verify"); } - - return sourcifyResponse; } - public getContractUrl(address: string, _matchType: string) { - let matchType; - if (_matchType === "perfect") { - matchType = "full_match"; - } else if (_matchType === "partial") { - matchType = "partial_match"; - } else { - throw new MatchTypeNotSupportedError(_matchType); - } - return `${this._browserUrl}/contracts/${matchType}/${this._chainId}/${address}/`; + public getContractUrl( + address: string, + contractStatus: ContractStatus.PERFECT | ContractStatus.PARTIAL + ) { + const matchType = + contractStatus === ContractStatus.PERFECT + ? "full_match" + : "partial_match"; + return `${this.browserUrl}/contracts/${matchType}/${this.chainId}/${address}/`; } } -interface SourcifyContract { - address: string; - chainId: string; - status: string; - storageTimestamp: string; -} - -class SourcifyResponse { - public readonly error: string; - - public readonly result: SourcifyContract[]; - - constructor(response: any) { - this.error = response.error; - this.result = response.result; +class SourcifyResponse implements ValidationResponse { + public readonly error: string | undefined; + public readonly status: + | ContractStatus.PERFECT + | ContractStatus.PARTIAL + | undefined; + + constructor(response: SourcifyVerifyResponse) { + if ("error" in response) { + this.error = response.error; + } else if (response.result[0].status === ContractStatus.PERFECT) { + this.status = ContractStatus.PERFECT; + } else if (response.result[0].status === ContractStatus.PARTIAL) { + this.status = ContractStatus.PARTIAL; + } } - public isSuccess() { - return this.getError() === undefined; + public isPending() { + return false; } - public isOk() { - return this.getStatus() === "perfect" || this.getStatus() === "partial"; + public isFailure() { + return this.error !== undefined; } - public getStatus() { - return this.result[0].status; + public isSuccess() { + return this.error === undefined; } - public getError() { - return this.error; + public isOk(): this is { + status: ContractStatus.PERFECT | ContractStatus.PARTIAL; + } { + return ( + this.status === ContractStatus.PERFECT || + this.status === ContractStatus.PARTIAL + ); } } diff --git a/packages/hardhat-verify/src/internal/sourcify.types.ts b/packages/hardhat-verify/src/internal/sourcify.types.ts new file mode 100644 index 0000000000..acf09387b3 --- /dev/null +++ b/packages/hardhat-verify/src/internal/sourcify.types.ts @@ -0,0 +1,40 @@ +export enum ContractStatus { + PERFECT = "perfect", + PARTIAL = "partial", + NOT_FOUND = "false", +} + +interface SourcifyIsVerifiedNotOkResponse { + address: string; + status: ContractStatus.NOT_FOUND; +} + +interface SourcifyIsVerifiedOkResponse { + address: string; + chainIds: Array<{ + chainId: string; + status: ContractStatus.PERFECT | ContractStatus.PARTIAL; + }>; +} + +export type SourcifyIsVerifiedResponse = + | SourcifyIsVerifiedNotOkResponse + | SourcifyIsVerifiedOkResponse; + +interface SourcifyVerifyNotOkResponse { + error: string; +} + +interface SourcifyVerifyOkResponse { + result: Array<{ + address: string; + chainId: string; + status: string; + message?: string; + libraryMap?: Record; + }>; +} + +export type SourcifyVerifyResponse = + | SourcifyVerifyNotOkResponse + | SourcifyVerifyOkResponse; diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index 0a2985b02f..ebd8645b91 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -155,7 +155,7 @@ subtask(TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION) if (response.isOk()) { const contractURL = verificationInterface.getContractUrl( address, - response.getStatus() + response.status ); console.log(`Successfully verified contract ${ contractFQN.split(":")[1] diff --git a/packages/hardhat-verify/src/internal/undici.ts b/packages/hardhat-verify/src/internal/undici.ts index 39c045f921..a25acc8966 100644 --- a/packages/hardhat-verify/src/internal/undici.ts +++ b/packages/hardhat-verify/src/internal/undici.ts @@ -37,3 +37,7 @@ function getDispatcher(): Undici.Dispatcher { return getGlobalDispatcher(); } + +export function isSuccessStatusCode(statusCode: number): boolean { + return statusCode >= 200 && statusCode <= 299; +} diff --git a/packages/hardhat-verify/src/internal/utilities.ts b/packages/hardhat-verify/src/internal/utilities.ts index b89bdd584e..3d380e91de 100644 --- a/packages/hardhat-verify/src/internal/utilities.ts +++ b/packages/hardhat-verify/src/internal/utilities.ts @@ -252,3 +252,10 @@ export async function encodeArguments( return encodedConstructorArguments; } + +export abstract class ValidationResponse { + public isPending() {} + public isFailure() {} + public isSuccess() {} + public isOk() {} +} diff --git a/packages/hardhat-verify/test/integration/index.ts b/packages/hardhat-verify/test/integration/index.ts index 8097cb7abc..b0413bf24e 100644 --- a/packages/hardhat-verify/test/integration/index.ts +++ b/packages/hardhat-verify/test/integration/index.ts @@ -29,15 +29,15 @@ describe("verify task integration tests", () => { useEnvironment("hardhat-project"); mockEnvironment(); - // suppress warnings - let warnStub: SinonStub; + // suppress sourcify info message + let consoleInfoStub: SinonStub; before(() => { - warnStub = sinon.stub(console, "warn"); + consoleInfoStub = sinon.stub(console, "info"); }); // suppress warnings after(() => { - warnStub.restore(); + consoleInfoStub.restore(); }); it("should return after printing the supported networks", async function () { @@ -306,7 +306,7 @@ This can occur if the library is only called in the contract constructor. The mi constructorArgsParams: [], }) ).to.be.rejectedWith( - /Failed to send contract verification request.\nEndpoint URL: https:\/\/api-hardhat.etherscan.io\/api\nReason: getaddrinfo ENOTFOUND api-hardhat.etherscan.io/ + /An unexpected error occurred during the verification process\.\nPlease report this issue to the Hardhat team\.\nError Details: getaddrinfo ENOTFOUND api-hardhat\.etherscan\.io/ ); }); @@ -318,9 +318,9 @@ This can occur if the library is only called in the contract constructor. The mi address: simpleContractAddress, constructorArgsParams: [], }) - ).to.be.rejectedWith(`Failed to send contract verification request. -Endpoint URL: https://api-hardhat.etherscan.io/api -The HTTP server response is not ok. Status code: 500 Response text: {"error":"error verifying contract"}`); + ).to.be.rejectedWith( + /Failed to send contract verification request\.\nEndpoint URL: https:\/\/api-hardhat\.etherscan\.io\/api\nRequest body: (.*?)\nThe HTTP server response is not ok\. Status code: 500 Response text: {"error":"error verifying contract"}/s + ); }); it("should throw if the etherscan api can't find the bytecode at the contract address", async function () { diff --git a/packages/hardhat-verify/test/unit/index.ts b/packages/hardhat-verify/test/unit/index.ts index 22214b88ce..376e27011d 100644 --- a/packages/hardhat-verify/test/unit/index.ts +++ b/packages/hardhat-verify/test/unit/index.ts @@ -206,7 +206,7 @@ describe("verify task", () => { assert.isTrue(warnStub.calledOnce); expect(warnStub).to.be.calledWith( - sinon.match(/WARNING: No verification services are enabled./) + sinon.match(/\[WARNING\] No verification services are enabled./) ); }); }); diff --git a/packages/hardhat-verify/test/unit/sourcify.ts b/packages/hardhat-verify/test/unit/sourcify.ts index c4dcbf1c18..e6e9cfb619 100644 --- a/packages/hardhat-verify/test/unit/sourcify.ts +++ b/packages/hardhat-verify/test/unit/sourcify.ts @@ -1,5 +1,6 @@ import { assert } from "chai"; import { Sourcify } from "../../src/sourcify"; +import { ContractStatus } from "../../src/internal/sourcify.types"; describe("Sourcify", () => { const chainId = 100; @@ -11,7 +12,7 @@ describe("Sourcify", () => { const sourcify = new Sourcify(chainId); const contractUrl = sourcify.getContractUrl( "0xC4c622862a8F548997699bE24EA4bc504e5cA865", - "perfect" + ContractStatus.PERFECT ); assert.equal(contractUrl, expectedContractAddress); From f187c42c0e1126ed8a17952000795bfcbf2a1a24 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Sat, 21 Oct 2023 13:45:33 -0300 Subject: [PATCH 22/36] Cleanup --- packages/hardhat-verify/src/index.ts | 107 +++++++- .../hardhat-verify/src/internal/errors.ts | 37 +-- .../hardhat-verify/src/internal/etherscan.ts | 77 +++--- .../hardhat-verify/src/internal/sourcify.ts | 48 +--- .../hardhat-verify/src/internal/task-names.ts | 6 +- .../src/internal/tasks/etherscan.ts | 103 +------- .../src/internal/tasks/sourcify.ts | 231 +++++++++++------- .../hardhat-verify/src/internal/utilities.ts | 7 + packages/hardhat-verify/src/types.ts | 3 +- .../hardhat-verify/test/integration/index.ts | 12 +- 10 files changed, 326 insertions(+), 305 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index a5f43a9403..f580d2bf0d 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -1,7 +1,13 @@ -import type { LibraryToAddress } from "./internal/solc/artifacts"; +import type { + ContractInformation, + ExtendedContractInformation, + LibraryToAddress, +} from "./internal/solc/artifacts"; +import type { Bytecode } from "./internal/solc/bytecode"; import chalk from "chalk"; import { extendConfig, subtask, task, types } from "hardhat/config"; + import { TASK_VERIFY, TASK_VERIFY_GET_VERIFICATION_SUBTASKS, @@ -10,6 +16,7 @@ import { TASK_VERIFY_PRINT_SUPPORTED_NETWORKS, TASK_VERIFY_SOURCIFY, TASK_VERIFY_SOURCIFY_DISABLED_WARNING, + TASK_VERIFY_GET_CONTRACT_INFORMATION, } from "./internal/task-names"; import { etherscanConfigExtender, @@ -19,11 +26,20 @@ import { InvalidConstructorArgumentsError, InvalidLibrariesError, HardhatVerifyError, + ContractNotFoundError, + BuildInfoNotFoundError, + BuildInfoCompilerVersionMismatchError, + DeployedBytecodeMismatchError, } from "./internal/errors"; import { printSupportedNetworks, printVerificationErrors, } from "./internal/utilities"; +import { + extractMatchingContractInformation, + extractInferredContractInformation, + getLibraryInformation, +} from "./internal/solc/artifacts"; import "./internal/type-extensions"; import "./internal/tasks/etherscan"; @@ -47,6 +63,18 @@ interface VerifySubtaskArgs { contract?: string; } +export interface VerificationResponse { + success: boolean; + message: string; +} + +interface GetContractInformationArgs { + contractFQN?: string; + deployedBytecode: Bytecode; + matchingCompilerVersions: string[]; + libraries: LibraryToAddress; +} + extendConfig(etherscanConfigExtender); extendConfig(sourcifyConfigExtender); @@ -54,9 +82,9 @@ extendConfig(sourcifyConfigExtender); * Main verification task. * * This is a meta-task that gets all the verification tasks and runs them. - * Right now there's only a "verify-etherscan" task. + * It supports Etherscan and Sourcify. */ -task(TASK_VERIFY, "Verifies a contract on Etherscan") +task(TASK_VERIFY, "Verifies a contract on Etherscan or Sourcify") .addOptionalPositionalParam("address", "Address of the contract to verify") .addOptionalVariadicPositionalParam( "constructorArgsParams", @@ -147,6 +175,79 @@ subtask( } ); +subtask(TASK_VERIFY_GET_CONTRACT_INFORMATION) + .addParam("deployedBytecode", undefined, undefined, types.any) + .addParam("matchingCompilerVersions", undefined, undefined, types.any) + .addParam("libraries", undefined, undefined, types.any) + .addOptionalParam("contractFQN") + .setAction( + async ( + { + contractFQN, + deployedBytecode, + matchingCompilerVersions, + libraries, + }: GetContractInformationArgs, + { network, artifacts } + ): Promise => { + let contractInformation: ContractInformation | null; + + if (contractFQN !== undefined) { + const artifactExists = await artifacts.artifactExists(contractFQN); + + if (!artifactExists) { + throw new ContractNotFoundError(contractFQN); + } + + const buildInfo = await artifacts.getBuildInfo(contractFQN); + if (buildInfo === undefined) { + throw new BuildInfoNotFoundError(contractFQN); + } + + if ( + !matchingCompilerVersions.includes(buildInfo.solcVersion) && + !deployedBytecode.isOvm() + ) { + throw new BuildInfoCompilerVersionMismatchError( + contractFQN, + deployedBytecode.getVersion(), + deployedBytecode.hasVersionRange(), + buildInfo.solcVersion, + network.name + ); + } + + contractInformation = extractMatchingContractInformation( + contractFQN, + buildInfo, + deployedBytecode + ); + + if (contractInformation === null) { + throw new DeployedBytecodeMismatchError(network.name, contractFQN); + } + } else { + contractInformation = await extractInferredContractInformation( + artifacts, + network, + matchingCompilerVersions, + deployedBytecode + ); + } + + // map contractInformation libraries + const libraryInformation = await getLibraryInformation( + contractInformation, + libraries + ); + + return { + ...contractInformation, + ...libraryInformation, + }; + } + ); + /** * This subtask is used for backwards compatibility. * TODO [remove-verify-subtask]: if you're going to remove this subtask, diff --git a/packages/hardhat-verify/src/internal/errors.ts b/packages/hardhat-verify/src/internal/errors.ts index 498d236638..aeaac11514 100644 --- a/packages/hardhat-verify/src/internal/errors.ts +++ b/packages/hardhat-verify/src/internal/errors.ts @@ -5,6 +5,7 @@ import { ABIArgumentTypeErrorType, } from "./abi-validation-extras"; import { TASK_VERIFY_VERIFY } from "./task-names"; +import { truncate } from "./utilities"; export class HardhatVerifyError extends NomicLabsHardhatPluginError { constructor(message: string, parent?: Error) { @@ -111,7 +112,7 @@ Reason: ${parent.message}`, export class HardhatNetworkNotSupportedError extends HardhatVerifyError { constructor() { super( - `The selected network is "hardhat", which is not supported for contract verification. Please choose a network supported by Etherscan. + `The selected network is "hardhat", which is not supported for contract verification. If you intended to use a different network, ensure that you provide the --network parameter when running the command. @@ -141,7 +142,9 @@ export class ContractVerificationInvalidStatusCodeError extends HardhatVerifyErr ) { super(`Failed to send contract verification request. Endpoint URL: ${url}${ - requestBody !== undefined ? `\nRequest body: ${requestBody}\n` : "" + requestBody !== undefined + ? `\nRequest body: ${truncate(requestBody)}\n` + : "" } The HTTP server response is not ok. Status code: ${statusCode} Response text: ${responseText}`); } @@ -158,18 +161,6 @@ try to wait for five confirmations of your contract deployment transaction befor } } -export class ContractStatusPollingError extends HardhatVerifyError { - constructor(url: string, parent: Error) { - super( - `Failure during etherscan status polling. The verification may still succeed but -should be checked manually. -Endpoint URL: ${url} -Reason: ${parent.message}`, - parent - ); - } -} - export class ContractStatusPollingInvalidStatusCodeError extends HardhatVerifyError { constructor(statusCode: number, responseText: string) { super( @@ -477,21 +468,3 @@ ${undetectableLibraries.map((x) => ` * ${x}`).join("\n")}` }`); } } - -export class NonUniqueContractNameError extends HardhatVerifyError { - constructor() { - super(`Non-unique contract name is used`); - } -} - -export class SourcifyHardhatNetworkNotSupportedError extends HardhatVerifyError { - constructor() { - super( - `The selected network is "hardhat", which is not supported for contract verification. Please choose a network supported by Sourcify. - -If you intended to use a different network, ensure that you provide the --network parameter when running the command. - -For example: npx hardhat verify --network ` - ); - } -} diff --git a/packages/hardhat-verify/src/internal/etherscan.ts b/packages/hardhat-verify/src/internal/etherscan.ts index 2e1e0e8df2..8e6fc18af2 100644 --- a/packages/hardhat-verify/src/internal/etherscan.ts +++ b/packages/hardhat-verify/src/internal/etherscan.ts @@ -1,4 +1,3 @@ -import type { Dispatcher } from "undici"; import type { EthereumProvider } from "hardhat/types"; import type { ChainConfig, ApiKey } from "../types"; import type { @@ -9,7 +8,6 @@ import type { import { HARDHAT_NETWORK_NAME } from "hardhat/plugins"; import { - ContractStatusPollingError, ContractStatusPollingInvalidStatusCodeError, ContractVerificationMissingBytecodeError, ContractVerificationInvalidStatusCodeError, @@ -76,9 +74,6 @@ export class Etherscan { apiKey: ApiKey | undefined, chainConfig: ChainConfig ) { - if (chainConfig.urls === undefined) { - throw new ChainConfigNotFoundError(chainConfig.chainId); - } const resolvedApiKey = resolveApiKey(apiKey, chainConfig.network); const apiUrl = chainConfig.urls.apiURL; const browserUrl = chainConfig.urls.browserURL.trim().replace(/\/$/, ""); @@ -104,8 +99,9 @@ export class Etherscan { url.search = parameters.toString(); try { - const response: Dispatcher.ResponseData = await sendGetRequest(url); - const json: EtherscanGetSourceCodeResponse = await response.body.json(); + const response = await sendGetRequest(url); + const json = + (await response.body.json()) as EtherscanGetSourceCodeResponse; if (!isSuccessStatusCode(response.statusCode)) { throw new ContractVerificationInvalidStatusCodeError( @@ -163,12 +159,10 @@ export class Etherscan { const url = new URL(this.apiUrl); try { - const response: Dispatcher.ResponseData = await sendPostRequest( - url, - parameters.toString(), - { "Content-Type": "application/x-www-form-urlencoded" } - ); - const json: EtherscanVerifyResponse = await response.body.json(); + const response = await sendPostRequest(url, parameters.toString(), { + "Content-Type": "application/x-www-form-urlencoded", + }); + const json = (await response.body.json()) as EtherscanVerifyResponse; if (!isSuccessStatusCode(response.statusCode)) { throw new ContractVerificationInvalidStatusCodeError( @@ -224,42 +218,45 @@ export class Etherscan { const url = new URL(this.apiUrl); url.search = parameters.toString(); - let response; try { - response = await sendGetRequest(url); - } catch (error: any) { - throw new ContractStatusPollingError(url.toString(), error); - } + const response = await sendGetRequest(url); + const json = (await response.body.json()) as EtherscanVerifyResponse; - if (!isSuccessStatusCode(response.statusCode)) { - // This could be always interpreted as JSON if there were any such guarantee in the Etherscan API. - const responseText = await response.body.text(); + if (!isSuccessStatusCode(response.statusCode)) { + throw new ContractStatusPollingInvalidStatusCodeError( + response.statusCode, + JSON.stringify(json) + ); + } - throw new ContractStatusPollingInvalidStatusCodeError( - response.statusCode, - responseText - ); - } + const etherscanResponse = new EtherscanResponse(json); - const etherscanResponse = new EtherscanResponse(await response.body.json()); + if (etherscanResponse.isPending()) { + await sleep(VERIFICATION_STATUS_POLLING_TIME); - if (etherscanResponse.isPending()) { - await sleep(VERIFICATION_STATUS_POLLING_TIME); + return await this.getVerificationStatus(guid); + } - return this.getVerificationStatus(guid); - } + if (etherscanResponse.isFailure()) { + return etherscanResponse; + } - if (etherscanResponse.isFailure()) { - return etherscanResponse; - } + if (!etherscanResponse.isOk()) { + throw new ContractStatusPollingResponseNotOkError( + etherscanResponse.message + ); + } - if (!etherscanResponse.isOk()) { - throw new ContractStatusPollingResponseNotOkError( - etherscanResponse.message - ); + return etherscanResponse; + } catch (e) { + if ( + e instanceof ContractStatusPollingInvalidStatusCodeError || + e instanceof ContractStatusPollingResponseNotOkError + ) { + throw e; + } + throw new UnexpectedError(e, "Etherscan.getVerificationStatus"); } - - return etherscanResponse; } /** diff --git a/packages/hardhat-verify/src/internal/sourcify.ts b/packages/hardhat-verify/src/internal/sourcify.ts index e1eb480900..36a6a38b0a 100644 --- a/packages/hardhat-verify/src/internal/sourcify.ts +++ b/packages/hardhat-verify/src/internal/sourcify.ts @@ -1,20 +1,13 @@ -import type { Dispatcher } from "undici"; -import type { EthereumProvider } from "hardhat/types"; -import type { ChainConfig } from "../types"; import type { SourcifyIsVerifiedResponse, SourcifyVerifyResponse, } from "./sourcify.types"; -import { HARDHAT_NETWORK_NAME } from "hardhat/plugins"; import { ContractVerificationInvalidStatusCodeError, - SourcifyHardhatNetworkNotSupportedError, - ChainConfigNotFoundError, UnexpectedError, } from "./errors"; import { isSuccessStatusCode, sendGetRequest, sendPostRequest } from "./undici"; -import { builtinChains } from "./chain-config"; import { ContractStatus } from "./sourcify.types"; import { ValidationResponse } from "./utilities"; @@ -24,33 +17,6 @@ export class Sourcify { constructor(public chainId: number) {} - public static async getCurrentChainConfig( - networkName: string, - ethereumProvider: EthereumProvider, - customChains: ChainConfig[] - ): Promise { - const currentChainId = parseInt( - await ethereumProvider.send("eth_chainId"), - 16 - ); - - const currentChainConfig = [ - // custom chains has higher precedence than builtin chains - ...[...customChains].reverse(), // the last entry has higher precedence - ...builtinChains, - ].find(({ chainId }) => chainId === currentChainId); - - if (currentChainConfig === undefined) { - if (networkName === HARDHAT_NETWORK_NAME) { - throw new SourcifyHardhatNetworkNotSupportedError(); - } - - throw new ChainConfigNotFoundError(currentChainId); - } - - return currentChainConfig; - } - // https://sourcify.dev/server/api-docs/#/Repository/get_check_all_by_addresses public async isVerified(address: string) { const parameters = new URLSearchParams({ @@ -62,8 +28,8 @@ export class Sourcify { url.search = parameters.toString(); try { - const response: Dispatcher.ResponseData = await sendGetRequest(url); - const json: SourcifyIsVerifiedResponse[] = await response.body.json(); + const response = await sendGetRequest(url); + const json = (await response.body.json()) as SourcifyIsVerifiedResponse[]; if (!isSuccessStatusCode(response.statusCode)) { throw new ContractVerificationInvalidStatusCodeError( @@ -126,12 +92,10 @@ export class Sourcify { const url = new URL(this.apiUrl); try { - const response: Dispatcher.ResponseData = await sendPostRequest( - url, - JSON.stringify(parameters), - { "Content-Type": "application/json" } - ); - const json: SourcifyVerifyResponse = await response.body.json(); + const response = await sendPostRequest(url, JSON.stringify(parameters), { + "Content-Type": "application/json", + }); + const json = (await response.body.json()) as SourcifyVerifyResponse; if (!isSuccessStatusCode(response.statusCode)) { throw new ContractVerificationInvalidStatusCodeError( diff --git a/packages/hardhat-verify/src/internal/task-names.ts b/packages/hardhat-verify/src/internal/task-names.ts index 3391fc5253..5557ffc1df 100644 --- a/packages/hardhat-verify/src/internal/task-names.ts +++ b/packages/hardhat-verify/src/internal/task-names.ts @@ -4,13 +4,13 @@ export const TASK_VERIFY_GET_VERIFICATION_SUBTASKS = export const TASK_VERIFY_VERIFY = "verify:verify"; export const TASK_VERIFY_PRINT_SUPPORTED_NETWORKS = "verify:print-supported-networks"; +export const TASK_VERIFY_GET_CONTRACT_INFORMATION = + "verify:get-contract-information"; // Etherscan export const TASK_VERIFY_ETHERSCAN = "verify:etherscan"; export const TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS = "verify:etherscan-resolve-arguments"; -export const TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION = - "verify:etherscan-get-contract-information"; export const TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT = "verify:etherscan-get-minimal-input"; export const TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION = @@ -18,6 +18,8 @@ export const TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION = // Sourcify export const TASK_VERIFY_SOURCIFY = "verify:sourcify"; +export const TASK_VERIFY_SOURCIFY_RESOLVE_ARGUMENTS = + "verify:sourcify-resolve-arguments"; export const TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION = "verify:sourcify-attempt-verification"; export const TASK_VERIFY_SOURCIFY_DISABLED_WARNING = diff --git a/packages/hardhat-verify/src/internal/tasks/etherscan.ts b/packages/hardhat-verify/src/internal/tasks/etherscan.ts index d09e7289f5..99448ff01d 100644 --- a/packages/hardhat-verify/src/internal/tasks/etherscan.ts +++ b/packages/hardhat-verify/src/internal/tasks/etherscan.ts @@ -4,11 +4,10 @@ import type { DependencyGraph, CompilationJob, } from "hardhat/types"; -import type { VerifyTaskArgs } from "../.."; +import type { VerificationResponse, VerifyTaskArgs } from "../.."; import type { LibraryToAddress, ExtendedContractInformation, - ContractInformation, } from "../solc/artifacts"; import { subtask, types } from "hardhat/config"; @@ -25,26 +24,17 @@ import { MissingAddressError, InvalidAddressError, InvalidContractNameError, - ContractNotFoundError, - BuildInfoNotFoundError, - BuildInfoCompilerVersionMismatchError, - DeployedBytecodeMismatchError, UnexpectedNumberOfFilesError, VerificationAPIUnexpectedMessageError, } from "../errors"; import { Etherscan } from "../etherscan"; -import { - extractMatchingContractInformation, - extractInferredContractInformation, - getLibraryInformation, -} from "../solc/artifacts"; import { Bytecode } from "../solc/bytecode"; import { TASK_VERIFY_ETHERSCAN, TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS, - TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION, TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT, TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION, + TASK_VERIFY_GET_CONTRACT_INFORMATION, } from "../task-names"; import { getCompilerVersions, @@ -62,13 +52,6 @@ interface VerificationArgs { contractFQN?: string; } -interface GetContractInformationArgs { - contractFQN?: string; - deployedBytecode: Bytecode; - matchingCompilerVersions: string[]; - libraries: LibraryToAddress; -} - interface GetMinimalInputArgs { sourceName: string; } @@ -81,11 +64,6 @@ interface AttemptVerificationArgs { encodedConstructorArguments: string; } -interface VerificationResponse { - success: boolean; - message: string; -} - /** * Main Etherscan verification subtask. * @@ -124,7 +102,7 @@ subtask(TASK_VERIFY_ETHERSCAN) const isVerified = await etherscan.isVerified(address); if (isVerified) { const contractURL = etherscan.getContractUrl(address); - console.log(`The contract ${address} has already been verified. + console.log(`The contract ${address} has already been verified on Etherscan. ${contractURL}`); return; } @@ -150,7 +128,7 @@ ${contractURL}`); } const contractInformation: ExtendedContractInformation = await run( - TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION, + TASK_VERIFY_GET_CONTRACT_INFORMATION, { contractFQN, deployedBytecode, @@ -267,79 +245,6 @@ subtask(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS) } ); -subtask(TASK_VERIFY_ETHERSCAN_GET_CONTRACT_INFORMATION) - .addParam("deployedBytecode", undefined, undefined, types.any) - .addParam("matchingCompilerVersions", undefined, undefined, types.any) - .addParam("libraries", undefined, undefined, types.any) - .addOptionalParam("contractFQN") - .setAction( - async ( - { - contractFQN, - deployedBytecode, - matchingCompilerVersions, - libraries, - }: GetContractInformationArgs, - { network, artifacts } - ): Promise => { - let contractInformation: ContractInformation | null; - - if (contractFQN !== undefined) { - const artifactExists = await artifacts.artifactExists(contractFQN); - - if (!artifactExists) { - throw new ContractNotFoundError(contractFQN); - } - - const buildInfo = await artifacts.getBuildInfo(contractFQN); - if (buildInfo === undefined) { - throw new BuildInfoNotFoundError(contractFQN); - } - - if ( - !matchingCompilerVersions.includes(buildInfo.solcVersion) && - !deployedBytecode.isOvm() - ) { - throw new BuildInfoCompilerVersionMismatchError( - contractFQN, - deployedBytecode.getVersion(), - deployedBytecode.hasVersionRange(), - buildInfo.solcVersion, - network.name - ); - } - - contractInformation = extractMatchingContractInformation( - contractFQN, - buildInfo, - deployedBytecode - ); - - if (contractInformation === null) { - throw new DeployedBytecodeMismatchError(network.name, contractFQN); - } - } else { - contractInformation = await extractInferredContractInformation( - artifacts, - network, - matchingCompilerVersions, - deployedBytecode - ); - } - - // map contractInformation libraries - const libraryInformation = await getLibraryInformation( - contractInformation, - libraries - ); - - return { - ...contractInformation, - ...libraryInformation, - }; - } - ); - subtask(TASK_VERIFY_ETHERSCAN_GET_MINIMAL_INPUT) .addParam("sourceName") .setAction(async ({ sourceName }: GetMinimalInputArgs, { run }) => { diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index ebd8645b91..9cda152058 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -1,39 +1,45 @@ +import type { VerificationResponse, VerifyTaskArgs } from "../.."; +import type { + ExtendedContractInformation, + LibraryToAddress, +} from "../solc/artifacts"; + import chalk from "chalk"; import { subtask, types } from "hardhat/config"; import { isFullyQualifiedName } from "hardhat/utils/contract-names"; -import { Artifact } from "hardhat/types"; -import { ChainConfig } from "../../types"; -import { Sourcify } from "../sourcify"; +import { HARDHAT_NETWORK_NAME } from "hardhat/plugins"; +import { Sourcify } from "../sourcify"; import { BuildInfoNotFoundError, - ContractNotFoundError, + CompilerVersionsMismatchError, ContractVerificationFailedError, + HardhatNetworkNotSupportedError, + InvalidAddressError, InvalidContractNameError, - NonUniqueContractNameError, + MissingAddressError, } from "../errors"; - import { TASK_VERIFY_SOURCIFY, + TASK_VERIFY_SOURCIFY_RESOLVE_ARGUMENTS, + TASK_VERIFY_GET_CONTRACT_INFORMATION, TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION, TASK_VERIFY_SOURCIFY_DISABLED_WARNING, } from "../task-names"; +import { getCompilerVersions, resolveLibraries } from "../utilities"; +import { Bytecode } from "../solc/bytecode"; -interface VerificationResponse { - success: boolean; - message: string; -} - +// parsed verification args interface VerificationArgs { address: string; - constructorArgs: string[]; - contract?: string; + libraries: LibraryToAddress; + contractFQN?: string; } interface AttemptVerificationArgs { address: string; verificationInterface: Sourcify; - contractFQN: string; + contractInformation: ExtendedContractInformation; } /** @@ -45,121 +51,182 @@ interface AttemptVerificationArgs { subtask(TASK_VERIFY_SOURCIFY) .addParam("address") .addOptionalParam("contract") - .setAction( - async ( - { address, contract }: VerificationArgs, - { config, network, run, artifacts } - ) => { - if (contract === undefined) { - console.log( - "In order to verify on Sourcify you must provide a contract fully qualified name" - ); - return; - } + // TODO: [remove-verify-subtask] change to types.inputFile + .addOptionalParam("libraries", undefined, undefined, types.any) + .setAction(async (taskArgs: VerifyTaskArgs, { config, network, run }) => { + const { address, libraries, contractFQN }: VerificationArgs = await run( + TASK_VERIFY_SOURCIFY_RESOLVE_ARGUMENTS, + taskArgs + ); + + if (network.name === HARDHAT_NETWORK_NAME) { + throw new HardhatNetworkNotSupportedError(); + } - let customChains: ChainConfig[] = []; - if ( - config.sourcify.customChains !== undefined && - config.sourcify.customChains.length > 0 - ) { - customChains = config.sourcify.customChains; - } + const currentChainId = parseInt( + await network.provider.send("eth_chainId"), + 16 + ); + + const sourcify = new Sourcify(currentChainId); + + const status = await sourcify.isVerified(address); + if (status !== false) { + const contractURL = sourcify.getContractUrl(address, status); + console.log(`The contract ${address} has already been verified on Sourcify. +${contractURL}`); + return; + } - const chainConfig = await Sourcify.getCurrentChainConfig( - network.name, - network.provider, - customChains + const configCompilerVersions = await getCompilerVersions(config.solidity); + + const deployedBytecode = await Bytecode.getDeployedContractBytecode( + address, + network.provider, + network.name + ); + + const matchingCompilerVersions = await deployedBytecode.getMatchingVersions( + configCompilerVersions + ); + // don't error if the bytecode appears to be OVM bytecode, because we can't infer a specific OVM solc version from the bytecode + if (matchingCompilerVersions.length === 0 && !deployedBytecode.isOvm()) { + throw new CompilerVersionsMismatchError( + configCompilerVersions, + deployedBytecode.getVersion(), + network.name ); + } - if (chainConfig.chainId === undefined) { - console.log("Missing chainId"); - return; + const contractInformation: ExtendedContractInformation = await run( + TASK_VERIFY_GET_CONTRACT_INFORMATION, + { + contractFQN, + deployedBytecode, + matchingCompilerVersions, + libraries, } - - if (contract !== undefined && !isFullyQualifiedName(contract)) { - throw new InvalidContractNameError(contract); + ); + + const { + success: verificationSuccess, + message: verificationMessage, + }: VerificationResponse = await run( + TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION, + { + address, + verificationInterface: sourcify, + contractInformation, } + ); - const artifactExists = await artifacts.artifactExists(contract); + if (verificationSuccess) { + return; + } - if (!artifactExists) { - throw new ContractNotFoundError(contract); + throw new ContractVerificationFailedError(verificationMessage, []); + }); + +subtask(TASK_VERIFY_SOURCIFY_RESOLVE_ARGUMENTS) + .addOptionalParam("address") + .addOptionalParam("contract") + // TODO: [remove-verify-subtask] change to types.inputFile + .addOptionalParam("libraries", undefined, undefined, types.any) + .setAction( + async ({ + address, + contract, + libraries: librariesModule, + }: VerifyTaskArgs): Promise => { + if (address === undefined) { + throw new MissingAddressError(); } - const sourcify = new Sourcify(chainConfig.chainId); + const { isAddress } = await import("@ethersproject/address"); + if (!isAddress(address)) { + throw new InvalidAddressError(address); + } - const status = await sourcify.isVerified(address); - if (status !== false) { - const contractURL = sourcify.getContractUrl(address, status); - console.log(`The contract ${address} has already been verified. -${contractURL}`); - return; + if (contract !== undefined && !isFullyQualifiedName(contract)) { + throw new InvalidContractNameError(contract); } - const { - success: verificationSuccess, - message: verificationMessage, - }: VerificationResponse = await run( - TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION, - { - address, - verificationInterface: sourcify, - contractFQN: contract, - } - ); - if (verificationSuccess) { - return; + // TODO: [remove-verify-subtask] librariesModule should always be string + let libraries; + if (typeof librariesModule === "object") { + libraries = librariesModule; + } else { + libraries = await resolveLibraries(librariesModule); } - throw new ContractVerificationFailedError(verificationMessage, []); + return { + address, + libraries, + contractFQN: contract, + }; } ); subtask(TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION) .addParam("address") - .addParam("contractFQN") + .addParam("contractInformation", undefined, undefined, types.any) .addParam("verificationInterface", undefined, undefined, types.any) .setAction( async ( - { address, verificationInterface, contractFQN }: AttemptVerificationArgs, + { + address, + verificationInterface, + contractInformation, + }: AttemptVerificationArgs, { artifacts } ): Promise => { + const { sourceName, contractName, contractOutput, compilerInput } = + contractInformation; + + /* const contractFQN = `${sourceName}:${contractName}`; const buildInfo = await artifacts.getBuildInfo(contractFQN); if (buildInfo === undefined) { throw new BuildInfoNotFoundError(contractFQN); } - let artifact: Artifact; - try { - artifact = await artifacts.readArtifact(contractFQN); - } catch (e) { - throw new NonUniqueContractNameError(); - } + // Ensure the linking information is present in the compiler input; + buildInfo.input.settings.libraries = contractInformation.libraries; const chosenContract = Object.keys(buildInfo.output.contracts).findIndex( - (source) => source === artifact.sourceName + (source) => source === sourceName ); - if (chosenContract === -1) { - throw new ContractNotFoundError(artifact.sourceName); - } - const response = await verificationInterface.verify( address, { hardhatOutputBuffer: JSON.stringify(buildInfo), }, chosenContract + ); */ + + const metadata = (contractOutput as any).metadata; + const fileContent = compilerInput.sources[sourceName].content; + + const librarySources = Object.keys(contractInformation.libraries).reduce( + (acc: any, libraryName) => { + const libraryContent = compilerInput.sources[sourceName].content; + acc[libraryName] = libraryContent; + return acc; + }, + {} ); + const response = await verificationInterface.verify(address, { + "metadata.json": metadata, + [sourceName]: fileContent, + ...librarySources, + }); if (response.isOk()) { const contractURL = verificationInterface.getContractUrl( address, response.status ); - console.log(`Successfully verified contract ${ - contractFQN.split(":")[1] - } on Sourcify. + console.log(`Successfully verified contract ${contractName} on Sourcify. ${contractURL}`); } diff --git a/packages/hardhat-verify/src/internal/utilities.ts b/packages/hardhat-verify/src/internal/utilities.ts index 3d380e91de..106f1b566e 100644 --- a/packages/hardhat-verify/src/internal/utilities.ts +++ b/packages/hardhat-verify/src/internal/utilities.ts @@ -259,3 +259,10 @@ export abstract class ValidationResponse { public isSuccess() {} public isOk() {} } + +export function truncate(str: string, maxLength: number = 1000): string { + if (str.length > maxLength) { + return `${str.substring(0, maxLength - 3)}...`; + } + return str; +} diff --git a/packages/hardhat-verify/src/types.ts b/packages/hardhat-verify/src/types.ts index 40544ff38e..405b37df5f 100644 --- a/packages/hardhat-verify/src/types.ts +++ b/packages/hardhat-verify/src/types.ts @@ -1,7 +1,7 @@ export interface ChainConfig { network: string; chainId: number; - urls?: { + urls: { apiURL: string; browserURL: string; }; @@ -15,7 +15,6 @@ export interface EtherscanConfig { export interface SourcifyConfig { enabled: boolean; - customChains?: ChainConfig[]; } export type ApiKey = string | Record; diff --git a/packages/hardhat-verify/test/integration/index.ts b/packages/hardhat-verify/test/integration/index.ts index b0413bf24e..0d3e3b590b 100644 --- a/packages/hardhat-verify/test/integration/index.ts +++ b/packages/hardhat-verify/test/integration/index.ts @@ -91,7 +91,7 @@ describe("verify task integration tests", () => { }); expect(logStub).to.be.calledOnceWith( - `The contract ${address} has already been verified. + `The contract ${address} has already been verified on Etherscan. https://hardhat.etherscan.io/address/${address}#code` ); logStub.restore(); @@ -230,7 +230,9 @@ https://hardhat.etherscan.io/address/${address}#code` contract: contractFQN, }) ).to.be.rejectedWith( - new RegExp(`HH700: Artifact for contract "${contractFQN}" not found. `) + new RegExp( + `The contract ${contractFQN} is not present in your project.` + ) ); }); @@ -368,7 +370,9 @@ This can occur if the library is only called in the contract constructor. The mi address: simpleContractAddress, constructorArgsParams: [], }) - ).to.be.rejectedWith(/Failure during etherscan status polling./); + ).to.be.rejectedWith( + /An unexpected error occurred during the verification process\.\nPlease report this issue to the Hardhat team\.\nError Details: getaddrinfo ENOTFOUND api-hardhat\.etherscan\.io/ + ); expect(logStub).to.be .calledOnceWith(`Successfully submitted source code for contract @@ -656,6 +660,8 @@ describe("verify task Sourcify's integration tests", () => { ], }); const logStub = sinon.stub(console, "log"); + // set network name to localhost to avoid the "hardhat is not supported" error + this.hre.network.name = "localhost"; const taskResponse = await this.hre.run(TASK_VERIFY_SOURCIFY, { address: simpleContractAddress, From a63e10fffa2fd349c0ca47335fe8dc2634267e00 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Mon, 23 Oct 2023 12:45:27 -0300 Subject: [PATCH 23/36] Fix verification with libs in Sourcify --- .../src/internal/tasks/sourcify.ts | 59 +++++-------------- 1 file changed, 15 insertions(+), 44 deletions(-) diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index 9cda152058..1cd1eaac12 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -11,7 +11,6 @@ import { HARDHAT_NETWORK_NAME } from "hardhat/plugins"; import { Sourcify } from "../sourcify"; import { - BuildInfoNotFoundError, CompilerVersionsMismatchError, ContractVerificationFailedError, HardhatNetworkNotSupportedError, @@ -172,53 +171,25 @@ subtask(TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION) .addParam("contractInformation", undefined, undefined, types.any) .addParam("verificationInterface", undefined, undefined, types.any) .setAction( - async ( - { - address, - verificationInterface, - contractInformation, - }: AttemptVerificationArgs, - { artifacts } - ): Promise => { + async ({ + address, + verificationInterface, + contractInformation, + }: AttemptVerificationArgs): Promise => { const { sourceName, contractName, contractOutput, compilerInput } = contractInformation; - /* const contractFQN = `${sourceName}:${contractName}`; - const buildInfo = await artifacts.getBuildInfo(contractFQN); - if (buildInfo === undefined) { - throw new BuildInfoNotFoundError(contractFQN); - } - - // Ensure the linking information is present in the compiler input; - buildInfo.input.settings.libraries = contractInformation.libraries; - - const chosenContract = Object.keys(buildInfo.output.contracts).findIndex( - (source) => source === sourceName - ); - - const response = await verificationInterface.verify( - address, - { - hardhatOutputBuffer: JSON.stringify(buildInfo), - }, - chosenContract - ); */ - - const metadata = (contractOutput as any).metadata; - const fileContent = compilerInput.sources[sourceName].content; - - const librarySources = Object.keys(contractInformation.libraries).reduce( - (acc: any, libraryName) => { - const libraryContent = compilerInput.sources[sourceName].content; - acc[libraryName] = libraryContent; - return acc; - }, - {} - ); + const sourcesToContent = Object.keys( + contractInformation.libraries + ).reduce((acc: Record, libSourceName) => { + const libContent = compilerInput.sources[libSourceName].content; + acc[libSourceName] = libContent; + return acc; + }, {}); const response = await verificationInterface.verify(address, { - "metadata.json": metadata, - [sourceName]: fileContent, - ...librarySources, + "metadata.json": (contractOutput as any).metadata, + [sourceName]: compilerInput.sources[sourceName].content, + ...sourcesToContent, }); if (response.isOk()) { From 6f2219511cd4feed89d9512a03ac7c4c07c9a0a3 Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Tue, 24 Oct 2023 19:23:20 +0200 Subject: [PATCH 24/36] Turn abstract class into interface --- packages/hardhat-verify/src/internal/utilities.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/hardhat-verify/src/internal/utilities.ts b/packages/hardhat-verify/src/internal/utilities.ts index 106f1b566e..600a010f78 100644 --- a/packages/hardhat-verify/src/internal/utilities.ts +++ b/packages/hardhat-verify/src/internal/utilities.ts @@ -253,11 +253,11 @@ export async function encodeArguments( return encodedConstructorArguments; } -export abstract class ValidationResponse { - public isPending() {} - public isFailure() {} - public isSuccess() {} - public isOk() {} +export interface ValidationResponse { + isPending(): void; + isFailure(): void; + isSuccess(): void; + isOk(): void; } export function truncate(str: string, maxLength: number = 1000): string { From 4137beafcc6ddb9e76eab145c5641f681256b33c Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Tue, 24 Oct 2023 19:48:25 +0200 Subject: [PATCH 25/36] Rename variable --- packages/hardhat-verify/src/internal/tasks/sourcify.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index 1cd1eaac12..523ad66528 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -179,17 +179,18 @@ subtask(TASK_VERIFY_SOURCIFY_ATTEMPT_VERIFICATION) const { sourceName, contractName, contractOutput, compilerInput } = contractInformation; - const sourcesToContent = Object.keys( + const librarySourcesToContent = Object.keys( contractInformation.libraries ).reduce((acc: Record, libSourceName) => { const libContent = compilerInput.sources[libSourceName].content; acc[libSourceName] = libContent; return acc; }, {}); + const response = await verificationInterface.verify(address, { "metadata.json": (contractOutput as any).metadata, [sourceName]: compilerInput.sources[sourceName].content, - ...sourcesToContent, + ...librarySourcesToContent, }); if (response.isOk()) { From 343aa06ae16dc0dbf9adc925d52bf8d6005baba8 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Oct 2023 15:07:54 -0300 Subject: [PATCH 26/36] Simplify VERIFY_TASK error checking --- packages/hardhat-verify/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index f580d2bf0d..5baf2dbad4 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -122,16 +122,15 @@ task(TASK_VERIFY, "Verifies a contract on Etherscan or Sourcify") ); const errors: Record = {}; - let hasErrors = false; for (const verificationSubtask of verificationSubtasks) { try { await run(verificationSubtask, taskArgs); } catch (error) { - hasErrors = true; errors[verificationSubtask] = error as HardhatVerifyError; } } + const hasErrors = Object.keys(errors).length > 0; if (hasErrors) { printVerificationErrors(errors); process.exit(1); From 6a0e57e7ffd277f6cfee177aaac94909d8b8edee Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Oct 2023 15:09:51 -0300 Subject: [PATCH 27/36] Improve readability --- packages/hardhat-verify/src/internal/utilities.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/hardhat-verify/src/internal/utilities.ts b/packages/hardhat-verify/src/internal/utilities.ts index 600a010f78..b2bf947d3f 100644 --- a/packages/hardhat-verify/src/internal/utilities.ts +++ b/packages/hardhat-verify/src/internal/utilities.ts @@ -110,8 +110,7 @@ export function printVerificationErrors( let errorMessage = "hardhat-verify found one or more errors during the verification process:\n\n"; - for (const verificationSubtask of Object.keys(errors)) { - const error = errors[verificationSubtask]; + for (const [verificationSubtask, error] of Object.entries(errors)) { const verificationProvider = verificationSubtask .split(":")[1] .replace(/^\w/, (c) => c.toUpperCase()); From e3531b5a2de0464f19b9fed35c66e115302bdd94 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Oct 2023 15:54:35 -0300 Subject: [PATCH 28/36] Improve error printing --- packages/hardhat-verify/src/index.ts | 32 +++++++++---- .../hardhat-verify/src/internal/utilities.ts | 7 +-- packages/hardhat-verify/test/unit/index.ts | 47 ++++++++++++++----- .../hardhat-verify/test/unit/utilities.ts | 4 +- 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index 5baf2dbad4..b79a3dbbd9 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -75,6 +75,11 @@ interface GetContractInformationArgs { libraries: LibraryToAddress; } +export interface VerificationSubtask { + label: string; + subtaskName: string; +} + extendConfig(etherscanConfigExtender); extendConfig(sourcifyConfigExtender); @@ -117,16 +122,16 @@ task(TASK_VERIFY, "Verifies a contract on Etherscan or Sourcify") return; } - const verificationSubtasks: string[] = await run( + const verificationSubtasks: VerificationSubtask[] = await run( TASK_VERIFY_GET_VERIFICATION_SUBTASKS ); const errors: Record = {}; - for (const verificationSubtask of verificationSubtasks) { + for (const { label, subtaskName } of verificationSubtasks) { try { - await run(verificationSubtask, taskArgs); + await run(subtaskName, taskArgs); } catch (error) { - errors[verificationSubtask] = error as HardhatVerifyError; + errors[label] = error as HardhatVerifyError; } } @@ -146,20 +151,29 @@ subtask( subtask( TASK_VERIFY_GET_VERIFICATION_SUBTASKS, - async (_, { config, userConfig }): Promise => { - const verificationSubtasks = []; + async (_, { config, userConfig }): Promise => { + const verificationSubtasks: VerificationSubtask[] = []; if (config.etherscan.enabled) { - verificationSubtasks.push(TASK_VERIFY_ETHERSCAN); + verificationSubtasks.push({ + label: "Etherscan", + subtaskName: TASK_VERIFY_ETHERSCAN, + }); } if (config.sourcify.enabled) { - verificationSubtasks.push(TASK_VERIFY_SOURCIFY); + verificationSubtasks.push({ + label: "Sourcify", + subtaskName: TASK_VERIFY_SOURCIFY, + }); } else if ( userConfig.sourcify?.enabled === undefined || userConfig.sourcify?.enabled === false ) { - verificationSubtasks.unshift(TASK_VERIFY_SOURCIFY_DISABLED_WARNING); + verificationSubtasks.unshift({ + label: "Common", + subtaskName: TASK_VERIFY_SOURCIFY_DISABLED_WARNING, + }); } if (!config.etherscan.enabled && !config.sourcify.enabled) { diff --git a/packages/hardhat-verify/src/internal/utilities.ts b/packages/hardhat-verify/src/internal/utilities.ts index b2bf947d3f..bc4a21d840 100644 --- a/packages/hardhat-verify/src/internal/utilities.ts +++ b/packages/hardhat-verify/src/internal/utilities.ts @@ -110,11 +110,8 @@ export function printVerificationErrors( let errorMessage = "hardhat-verify found one or more errors during the verification process:\n\n"; - for (const [verificationSubtask, error] of Object.entries(errors)) { - const verificationProvider = verificationSubtask - .split(":")[1] - .replace(/^\w/, (c) => c.toUpperCase()); - errorMessage += `${verificationProvider}:\n${error.message}\n\n`; + for (const [subtaskLabel, error] of Object.entries(errors)) { + errorMessage += `${subtaskLabel}:\n${error.message}\n\n`; } console.error(chalk.red(errorMessage)); diff --git a/packages/hardhat-verify/test/unit/index.ts b/packages/hardhat-verify/test/unit/index.ts index 376e27011d..3febcfa71f 100644 --- a/packages/hardhat-verify/test/unit/index.ts +++ b/packages/hardhat-verify/test/unit/index.ts @@ -1,3 +1,4 @@ +import type { VerificationSubtask } from "../.."; import { assert, expect } from "chai"; import sinon, { SinonStub } from "sinon"; @@ -117,11 +118,15 @@ describe("verify task", () => { }); it("should return the etherscan subtask by default", async function () { - const verificationSubtasks: string[] = await this.hre.run( + const verificationSubtasks: VerificationSubtask[] = await this.hre.run( TASK_VERIFY_GET_VERIFICATION_SUBTASKS ); - assert.isTrue(verificationSubtasks.includes(TASK_VERIFY_ETHERSCAN)); + assert.isTrue( + verificationSubtasks.some( + ({ subtaskName }) => subtaskName === TASK_VERIFY_ETHERSCAN + ) + ); }); it("should return the etherscan subtask if it is enabled", async function () { @@ -132,13 +137,17 @@ describe("verify task", () => { customChains: [], }; - const verificationSubtasks: string[] = await this.hre.run( + const verificationSubtasks: VerificationSubtask[] = await this.hre.run( TASK_VERIFY_GET_VERIFICATION_SUBTASKS ); this.hre.config.etherscan = originalConfig; - assert.isTrue(verificationSubtasks.includes(TASK_VERIFY_ETHERSCAN)); + assert.isTrue( + verificationSubtasks.some( + ({ subtaskName }) => subtaskName === TASK_VERIFY_ETHERSCAN + ) + ); }); it("should ignore the etherscan subtask if it is disabled", async function () { @@ -149,23 +158,28 @@ describe("verify task", () => { customChains: [], }; - const verificationSubtasks: string[] = await this.hre.run( + const verificationSubtasks: VerificationSubtask[] = await this.hre.run( TASK_VERIFY_GET_VERIFICATION_SUBTASKS ); this.hre.config.etherscan = originalConfig; - assert.isFalse(verificationSubtasks.includes(TASK_VERIFY_ETHERSCAN)); + assert.isFalse( + verificationSubtasks.some( + ({ subtaskName }) => subtaskName === TASK_VERIFY_ETHERSCAN + ) + ); }); it("should ignore the sourcify subtask by default", async function () { - const verificationSubtasks: string[] = await this.hre.run( + const verificationSubtasks: VerificationSubtask[] = await this.hre.run( TASK_VERIFY_GET_VERIFICATION_SUBTASKS ); - assert.isFalse(verificationSubtasks.includes(TASK_VERIFY_SOURCIFY)); - assert.isTrue( - verificationSubtasks.includes(TASK_VERIFY_SOURCIFY_DISABLED_WARNING) + assert.isFalse( + verificationSubtasks.some( + ({ subtaskName }) => subtaskName === TASK_VERIFY_SOURCIFY + ) ); }); @@ -175,15 +189,22 @@ describe("verify task", () => { enabled: true, }; - const verificationSubtasks: string[] = await this.hre.run( + const verificationSubtasks: VerificationSubtask[] = await this.hre.run( TASK_VERIFY_GET_VERIFICATION_SUBTASKS ); this.hre.config.sourcify = originalConfig; - assert.isTrue(verificationSubtasks.includes(TASK_VERIFY_SOURCIFY)); + assert.isTrue( + verificationSubtasks.some( + ({ subtaskName }) => subtaskName === TASK_VERIFY_SOURCIFY + ) + ); assert.isFalse( - verificationSubtasks.includes(TASK_VERIFY_SOURCIFY_DISABLED_WARNING) + verificationSubtasks.some( + ({ subtaskName }) => + subtaskName === TASK_VERIFY_SOURCIFY_DISABLED_WARNING + ) ); }); diff --git a/packages/hardhat-verify/test/unit/utilities.ts b/packages/hardhat-verify/test/unit/utilities.ts index 57ab49eb0f..55a0e2cfad 100644 --- a/packages/hardhat-verify/test/unit/utilities.ts +++ b/packages/hardhat-verify/test/unit/utilities.ts @@ -51,8 +51,8 @@ describe("Utilities", () => { describe("printVerificationErrors", () => { it("should print verification errors", () => { const errors: Record = { - "verify:etherscan": new HardhatVerifyError("Etherscan error message"), - "verify:sourcify": new HardhatVerifyError("Sourcify error message"), + Etherscan: new HardhatVerifyError("Etherscan error message"), + Sourcify: new HardhatVerifyError("Sourcify error message"), }; const errorStub = sinon.stub(console, "error"); From 94ca033adcfcedaab3656ba307dd4d44ebf91f41 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Oct 2023 15:58:10 -0300 Subject: [PATCH 29/36] Use SourceCode to check if a contract is verified --- packages/hardhat-verify/src/internal/etherscan.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/hardhat-verify/src/internal/etherscan.ts b/packages/hardhat-verify/src/internal/etherscan.ts index 8e6fc18af2..2d39468855 100644 --- a/packages/hardhat-verify/src/internal/etherscan.ts +++ b/packages/hardhat-verify/src/internal/etherscan.ts @@ -115,7 +115,10 @@ export class Etherscan { return false; } - return json.result[0].ABI !== "Contract source code not verified"; + const sourceCode = json.result[0]?.SourceCode; + return ( + sourceCode !== undefined && sourceCode !== null && sourceCode !== "" + ); } catch (e) { if (e instanceof ContractVerificationInvalidStatusCodeError) { throw e; From 2a99de5908cd56766c3a77e2088d6b9f82bd85ef Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Oct 2023 16:05:16 -0300 Subject: [PATCH 30/36] Drop response body from error --- packages/hardhat-verify/src/internal/errors.ts | 14 ++------------ packages/hardhat-verify/src/internal/etherscan.ts | 3 +-- packages/hardhat-verify/src/internal/sourcify.ts | 3 +-- packages/hardhat-verify/src/internal/utilities.ts | 7 ------- packages/hardhat-verify/test/integration/index.ts | 2 +- 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/hardhat-verify/src/internal/errors.ts b/packages/hardhat-verify/src/internal/errors.ts index aeaac11514..a023cad2f5 100644 --- a/packages/hardhat-verify/src/internal/errors.ts +++ b/packages/hardhat-verify/src/internal/errors.ts @@ -5,7 +5,6 @@ import { ABIArgumentTypeErrorType, } from "./abi-validation-extras"; import { TASK_VERIFY_VERIFY } from "./task-names"; -import { truncate } from "./utilities"; export class HardhatVerifyError extends NomicLabsHardhatPluginError { constructor(message: string, parent?: Error) { @@ -134,18 +133,9 @@ To see the list of supported networks, run this command: } export class ContractVerificationInvalidStatusCodeError extends HardhatVerifyError { - constructor( - url: string, - statusCode: number, - responseText: string, - requestBody?: string - ) { + constructor(url: string, statusCode: number, responseText: string) { super(`Failed to send contract verification request. -Endpoint URL: ${url}${ - requestBody !== undefined - ? `\nRequest body: ${truncate(requestBody)}\n` - : "" - } +Endpoint URL: ${url} The HTTP server response is not ok. Status code: ${statusCode} Response text: ${responseText}`); } } diff --git a/packages/hardhat-verify/src/internal/etherscan.ts b/packages/hardhat-verify/src/internal/etherscan.ts index 2d39468855..25830ecdcc 100644 --- a/packages/hardhat-verify/src/internal/etherscan.ts +++ b/packages/hardhat-verify/src/internal/etherscan.ts @@ -171,8 +171,7 @@ export class Etherscan { throw new ContractVerificationInvalidStatusCodeError( url.toString(), response.statusCode, - JSON.stringify(json), - parameters.toString() + JSON.stringify(json) ); } diff --git a/packages/hardhat-verify/src/internal/sourcify.ts b/packages/hardhat-verify/src/internal/sourcify.ts index 36a6a38b0a..34a6eedef3 100644 --- a/packages/hardhat-verify/src/internal/sourcify.ts +++ b/packages/hardhat-verify/src/internal/sourcify.ts @@ -101,8 +101,7 @@ export class Sourcify { throw new ContractVerificationInvalidStatusCodeError( url.toString(), response.statusCode, - JSON.stringify(json), - JSON.stringify(parameters) + JSON.stringify(json) ); } diff --git a/packages/hardhat-verify/src/internal/utilities.ts b/packages/hardhat-verify/src/internal/utilities.ts index bc4a21d840..24ee02edf4 100644 --- a/packages/hardhat-verify/src/internal/utilities.ts +++ b/packages/hardhat-verify/src/internal/utilities.ts @@ -255,10 +255,3 @@ export interface ValidationResponse { isSuccess(): void; isOk(): void; } - -export function truncate(str: string, maxLength: number = 1000): string { - if (str.length > maxLength) { - return `${str.substring(0, maxLength - 3)}...`; - } - return str; -} diff --git a/packages/hardhat-verify/test/integration/index.ts b/packages/hardhat-verify/test/integration/index.ts index 0d3e3b590b..e259df75b4 100644 --- a/packages/hardhat-verify/test/integration/index.ts +++ b/packages/hardhat-verify/test/integration/index.ts @@ -321,7 +321,7 @@ This can occur if the library is only called in the contract constructor. The mi constructorArgsParams: [], }) ).to.be.rejectedWith( - /Failed to send contract verification request\.\nEndpoint URL: https:\/\/api-hardhat\.etherscan\.io\/api\nRequest body: (.*?)\nThe HTTP server response is not ok\. Status code: 500 Response text: {"error":"error verifying contract"}/s + /Failed to send contract verification request\.\nEndpoint URL: https:\/\/api-hardhat\.etherscan\.io\/api\n/s ); }); From 352644e4f644c6b6f3b172cdf540eb2018bd825f Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Oct 2023 16:07:07 -0300 Subject: [PATCH 31/36] Update sourcify message --- packages/hardhat-verify/src/internal/tasks/sourcify.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/hardhat-verify/src/internal/tasks/sourcify.ts b/packages/hardhat-verify/src/internal/tasks/sourcify.ts index 523ad66528..fff47008fd 100644 --- a/packages/hardhat-verify/src/internal/tasks/sourcify.ts +++ b/packages/hardhat-verify/src/internal/tasks/sourcify.ts @@ -123,7 +123,10 @@ ${contractURL}`); return; } - throw new ContractVerificationFailedError(verificationMessage, []); + throw new ContractVerificationFailedError( + verificationMessage, + contractInformation.undetectableLibraries + ); }); subtask(TASK_VERIFY_SOURCIFY_RESOLVE_ARGUMENTS) @@ -218,6 +221,8 @@ sourcify: { enabled: true } +Or set 'enabled' to false to hide this message. + For more information, visit https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify#verifying-on-sourcify` ) ); From 125ac410e0e15c6010441eba6fabe7359a2ce464 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Oct 2023 16:17:03 -0300 Subject: [PATCH 32/36] Only show sourcify warning if it is not defined --- packages/hardhat-verify/src/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/hardhat-verify/src/index.ts b/packages/hardhat-verify/src/index.ts index b79a3dbbd9..a012442dca 100644 --- a/packages/hardhat-verify/src/index.ts +++ b/packages/hardhat-verify/src/index.ts @@ -166,10 +166,7 @@ subtask( label: "Sourcify", subtaskName: TASK_VERIFY_SOURCIFY, }); - } else if ( - userConfig.sourcify?.enabled === undefined || - userConfig.sourcify?.enabled === false - ) { + } else if (userConfig.sourcify?.enabled === undefined) { verificationSubtasks.unshift({ label: "Common", subtaskName: TASK_VERIFY_SOURCIFY_DISABLED_WARNING, From 1a4809e529b56a0931e534aed471867107333596 Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Oct 2023 16:24:21 -0300 Subject: [PATCH 33/36] Remove unneeded stuff from changelog --- .changeset/tough-gorillas-matter.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.changeset/tough-gorillas-matter.md b/.changeset/tough-gorillas-matter.md index 1a146741c1..38eade3a20 100644 --- a/.changeset/tough-gorillas-matter.md +++ b/.changeset/tough-gorillas-matter.md @@ -2,7 +2,4 @@ "@nomicfoundation/hardhat-verify": major --- -- Added the option to deactivate verification providers by using the 'enabled' field in the plugin's configuration. -- Improved error handling: Errors are now displayed at the end of the verification process, providing better visibility. -- Introduced a warning when the Sourcify configuration is missing, helping users avoid potential issues. -- Implemented enhancements to allow multiple verification providers to run simultaneously, preventing any blocking. +- Add Sourcify as a verification provider. From 8a547e6f29d5ef89ac5c14ce570290dafc9a279b Mon Sep 17 00:00:00 2001 From: Luis Schaab Date: Tue, 24 Oct 2023 16:46:31 -0300 Subject: [PATCH 34/36] Add Sourcify section to the readme --- packages/hardhat-verify/README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/hardhat-verify/README.md b/packages/hardhat-verify/README.md index 0fe62ebc45..2f164fa88a 100644 --- a/packages/hardhat-verify/README.md +++ b/packages/hardhat-verify/README.md @@ -264,6 +264,30 @@ if (!instance.isVerified("0x123abc...")) { ### Verifying on Sourcify +To verify a contract using Sourcify, you need to add to your Hardhat config: + +```js +sourcify: { + enabled: true; +} +``` + +and then run the `verify` task, passing the address of the contract and the network where it's deployed: + +```bash +npx hardhat verify --network mainnet DEPLOYED_CONTRACT_ADDRESS +``` + +**Note:** Constructor arguments are not required for Sourcify verification, but you'll need to provide them if you also have Etherscan verification enabled. + +To disable Sourcify verification and suppress related messages, set `enabled` to `false`: + +```js +sourcify: { + enabled: false; +} +``` + ## How it works The plugin works by fetching the bytecode in the given address and using it to check which contract in your project corresponds to it. Besides that, some sanity checks are performed locally to make sure that the verification won't fail. From ce1a172b3fb3b7d9d726d196073635582b55b9e4 Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Tue, 24 Oct 2023 23:33:12 +0200 Subject: [PATCH 35/36] Update tough-gorillas-matter.md --- .changeset/tough-gorillas-matter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tough-gorillas-matter.md b/.changeset/tough-gorillas-matter.md index 38eade3a20..b3749e5536 100644 --- a/.changeset/tough-gorillas-matter.md +++ b/.changeset/tough-gorillas-matter.md @@ -2,4 +2,4 @@ "@nomicfoundation/hardhat-verify": major --- -- Add Sourcify as a verification provider. +- Added Sourcify as a verification provider. From c3368dac87512a1094df9def51bf3bfdc844d5a6 Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Tue, 24 Oct 2023 23:37:26 +0200 Subject: [PATCH 36/36] Update README.md Remove TOC and move Sourcify section --- packages/hardhat-verify/README.md | 67 ++++++++++++------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/packages/hardhat-verify/README.md b/packages/hardhat-verify/README.md index 2f164fa88a..2df56be85e 100644 --- a/packages/hardhat-verify/README.md +++ b/packages/hardhat-verify/README.md @@ -4,21 +4,6 @@ [Hardhat](https://hardhat.org) plugin to verify the source of code of deployed contracts. -1. [What](#what) -2. [Installation](#installation) -3. [Tasks](#tasks) -4. [Environment extensions](#environment-extensions) -5. [Usage](#usage) - 1. [Complex arguments](#complex-arguments) - 2. [Libraries with undetectable addresses](#libraries-with-undetectable-addresses) - 3. [Multiple API keys and alternative block explorers](#multiple-api-keys-and-alternative-block-explorers) - 4. [Adding support for other networks](#adding-support-for-other-networks) - 5. [Using programmatically](#using-programmatically) - 1. [Providing libraries from a script or task](#providing-libraries-from-a-script-or-task) - 2. [Advanced Usage: Using the Etherscan class from another plugin](#advanced-usage-using-the-etherscan-class-from-another-plugin) -6. [How it works](#how-it-works) -7. [Known limitations](#known-limitations) - ## What This plugin helps you verify the source code for your Solidity contracts. At the moment, it supports [Etherscan](https://etherscan.io)-based explorers and explorers compatible with its API like [Blockscout](https://www.blockscout.com/). @@ -189,6 +174,32 @@ Keep in mind that the name you are giving to the network in `customChains` is th To see which custom chains are supported, run `npx hardhat verify --list-networks`. +### Verifying on Sourcify + +To verify a contract using Sourcify, you need to add to your Hardhat config: + +```js +sourcify: { + enabled: true, +} +``` + +and then run the `verify` task, passing the address of the contract and the network where it's deployed: + +```bash +npx hardhat verify --network mainnet DEPLOYED_CONTRACT_ADDRESS +``` + +**Note:** Constructor arguments are not required for Sourcify verification, but you'll need to provide them if you also have Etherscan verification enabled. + +To disable Sourcify verification and suppress related messages, set `enabled` to `false`: + +```js +sourcify: { + enabled: false, +} +``` + ### Using programmatically To call the verification task from within a Hardhat task or script, use the `"verify:verify"` subtask. Assuming the same contract as [above](#complex-arguments), you can run the subtask like this: @@ -262,32 +273,6 @@ if (!instance.isVerified("0x123abc...")) { } ``` -### Verifying on Sourcify - -To verify a contract using Sourcify, you need to add to your Hardhat config: - -```js -sourcify: { - enabled: true; -} -``` - -and then run the `verify` task, passing the address of the contract and the network where it's deployed: - -```bash -npx hardhat verify --network mainnet DEPLOYED_CONTRACT_ADDRESS -``` - -**Note:** Constructor arguments are not required for Sourcify verification, but you'll need to provide them if you also have Etherscan verification enabled. - -To disable Sourcify verification and suppress related messages, set `enabled` to `false`: - -```js -sourcify: { - enabled: false; -} -``` - ## How it works The plugin works by fetching the bytecode in the given address and using it to check which contract in your project corresponds to it. Besides that, some sanity checks are performed locally to make sure that the verification won't fail.