diff --git a/packages/hardhat-zksync-upgradable/src/index.ts b/packages/hardhat-zksync-upgradable/src/index.ts index f35adb32a..796e50560 100644 --- a/packages/hardhat-zksync-upgradable/src/index.ts +++ b/packages/hardhat-zksync-upgradable/src/index.ts @@ -107,16 +107,26 @@ subtask('verify:etherscan').setAction(async (args, hre, runSuper) => { return await verify(args, hre, runSuper); } - throw new ZkSyncUpgradablePluginError( - 'This task is only available for zkSync network, use `verify:verify` instead', - ); + const { verify } = await import('./verify/verify-proxy'); + return await verify(args, hre, runSuper); }); -subtask('verify:verify').setAction(async (args, hre, runSuper) => { +subtask('verify:zksync-etherscan').setAction(async (args, hre, runSuper) => { if (!hre.network.zksync) { - // eslint-disable-next-line @typescript-eslint/no-shadow - const { verify } = await import('@openzeppelin/hardhat-upgrades/dist/verify-proxy'); - return await verify(args, hre, runSuper); + throw new ZkSyncUpgradablePluginError( + 'This task is only available for zkSync network, use `verify:verify` instead', + ); + } + + const { verify } = await import('./verify/verify-proxy'); + return await verify(args, hre, runSuper); +}); + +subtask('verify:zksync-blockexplorer').setAction(async (args, hre, runSuper) => { + if (!hre.network.zksync) { + throw new ZkSyncUpgradablePluginError( + 'This task is only available for zkSync network, use `verify:verify` instead', + ); } const { verify } = await import('./verify/verify-proxy'); diff --git a/packages/hardhat-zksync-verify/README.md b/packages/hardhat-zksync-verify/README.md index bc4d89583..94f9e561f 100644 --- a/packages/hardhat-zksync-verify/README.md +++ b/packages/hardhat-zksync-verify/README.md @@ -31,8 +31,6 @@ Import the plugin in the hardhat.config.ts file: `import "@matterlabs/hardhat-zksync-verify";` -Add the verifyURL property to the ZKsync Era network in the hardhat.config.ts file as shown below: - ``` networks: { sepolia: { @@ -42,12 +40,24 @@ networks: { url: "https://sepolia.era.zksync.dev", // The testnet RPC URL of ZKsync Era network. ethNetwork: "sepolia", // The Ethereum Web3 RPC URL, or the identifier of the network (e.g. `mainnet` or `sepolia`) zksync: true, - // Verification endpoint for Sepolia - verifyURL: 'https://explorer.sepolia.era.zksync.dev/contract_verification' } }, ``` +### Updates introduced in plugin version 1.7.0. + +Etherscan verification is supported. To enable it, configure the etherscan property in the Hardhat configuration: + +``` +etherscan: { + apiKey: 'APIKEY' +} +``` +If the etherscan property is configured and enabled, verification will run on Etherscan. Otherwise, the plugin will default to verifying on the ZKsync block explorer. + +For more information on how to create api keys, please [visit the documentation](https://docs.zksync.network/getting-started/viewing-api-usage-statistics). +For more information on how to configre etherscan for multiple api keys, please [visit the documentation](https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify#multiple-api-keys-and-alternative-block-explorers) + | 🔧 properties | 📄 Description | |----------------------------|--------------------------------------------------------------------------------------------------------------------------------------| | zkTestnet | Arbitrary ZKsync Era network name. You can select this as the default network using the defaultNetwork property. | @@ -55,7 +65,9 @@ networks: { | ethNetwork | Field with the URL of the Ethereum node. | | ethers | Provider for the network if the configuration is not provided. This field is required for all ZKsync networks used by this plugin. | | zksync | Flag that indicates a ZKsync Era network configuration. This field is set to true for all ZKsync Era networks. | -| verifyURL | Field that points to the verification endpoint for the specific ZKsync network. This parameter is optional. | +| verifyURL | This field specifies the verification endpoint for the connected ZKsync network. From version 1.7.0, the plugin automatically resolves this endpoint based on the network configuration. If you are using a custom chain with an API compatible with the zksync block explorer, you can manually set the URL here. | +| browserVerifyURL | Introduced in version 1.7.0 of the plugin, this field automatically resolves the browser URL based on the network configuration. If you're using a custom chain, you can manually specify the URL here. | +| enableVerifyURL | Introduced in version 1.7.0 of the plugin, this flag forces verification on the ZKsync block explorer. It allows you to verify the same contract on both Etherscan and the ZKsync block explorer. | Default values for verifyURL are: @@ -73,12 +85,6 @@ When executed in this manner, the verification task attempts to compare the comp With the --contract parameter you can also specify which contract from your local setup you want to verify by specifying its Fully qualified name. Fully qualified name structure looks like this: "contracts/AContract.sol:TheContract" -The following command checks the status of the verification request for the specific verification ID: - -`yarn hardhat verify-status --verification-id ` - - - **Constructor arguments** If your contract was deployed with the specific constructor arguments, you need to specify them when running the verify task. For example: diff --git a/packages/hardhat-zksync-verify/src/constants.ts b/packages/hardhat-zksync-verify/src/constants.ts index ac8dffeac..1da361e12 100644 --- a/packages/hardhat-zksync-verify/src/constants.ts +++ b/packages/hardhat-zksync-verify/src/constants.ts @@ -7,6 +7,13 @@ export const TASK_VERIFY = 'verify'; export const TASK_VERIFY_VERIFY = 'verify:verify'; export const TASK_VERIFY_CONTRACT = 'zk:verify:contract'; export const TASK_CHECK_VERIFICATION_STATUS = 'verify-status'; +export const TASK_VERIFY_ETHERSCAN = 'verify:etherscan'; + +export const TASK_VERIFY_ZKSYNC_EXPLORER = 'verify:zksync-blockexplorer'; +export const TASK_VERIFY_ZKSYNC_ETHERSCAN = 'verify:zksync-etherscan'; + +export const TASK_VERIFY_RESOLVE_ARGUMENTS = 'verify:resolve-arguments'; +export const TASK_VERIFY_GET_VERIFICATION_SUBTASKS = 'verify:get-verification-subtasks'; export const TASK_VERIFY_GET_CONSTRUCTOR_ARGUMENTS = 'verify:get-constructor-arguments'; export const TASK_VERIFY_GET_LIBRARIES = 'verify:get-libraries'; @@ -93,13 +100,11 @@ export const COMPILER_VERSION_NOT_SUPPORTED = export const WRONG_CONSTRUCTOR_ARGUMENTS = 'types/values length mismatch'; -export const PENDING_CONTRACT_INFORMATION_MESSAGE = (verificationId: number) => ` +export const PENDING_CONTRACT_INFORMATION_MESSAGE = (browserUrl?: string) => ` Your verification request has been sent, but our servers are currently overloaded and we could not confirm that the verification was successful. Please try one of the following options: - 1. Use the your verification request ID (${verificationId}) to check the status of the pending verification process by typing the command 'yarn hardhat verify-status --verification-id ${verificationId}' - 2. Manually check the contract's code on the zksync block explorer: https://explorer.zksync.io/ - 3. Run the verification process again - `; + 1. Manually check the contract's code on the explorer ${browserUrl ? `: ${browserUrl}` : '.'} + 2. Run the verification process again`; export const SINGLE_FILE_CODE_FORMAT = 'solidity-single-file'; export const JSON_INPUT_CODE_FORMAT = 'solidity-standard-json-input'; diff --git a/packages/hardhat-zksync-verify/src/explorers/constants.ts b/packages/hardhat-zksync-verify/src/explorers/constants.ts new file mode 100644 index 000000000..89032f5d2 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/constants.ts @@ -0,0 +1,8 @@ +export const TRYING_VERIFICATION_WITH_FULL_COMPILER_INPUT = ( + contractName: string, +) => `We tried verifying your contract ${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 the explorer.`; + +export const PROVIDED_CHAIN_IS_NOT_SUPPORTED_FOR_VERIFICATION = (chainId: number) => + `The provided chain with id ${chainId} is not supported by default!`; diff --git a/packages/hardhat-zksync-verify/src/explorers/errors.ts b/packages/hardhat-zksync-verify/src/explorers/errors.ts new file mode 100644 index 000000000..0d0fdde8c --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/errors.ts @@ -0,0 +1,9 @@ +import { ZkSyncVerifyPluginError } from '../errors'; + +export class ZksyncContractVerificationInvalidStatusCodeError extends ZkSyncVerifyPluginError { + constructor(url: string, statusCode: number, responseText: string) { + super(`Failed to send contract verification request. + Endpoint URL: ${url} + The HTTP server response is not ok. Status code: ${statusCode} Response text: ${responseText}`); + } +} diff --git a/packages/hardhat-zksync-verify/src/explorers/service.ts b/packages/hardhat-zksync-verify/src/explorers/service.ts new file mode 100644 index 000000000..49e1dd692 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/service.ts @@ -0,0 +1,196 @@ +import { EthereumProvider, HardhatRuntimeEnvironment } from 'hardhat/types'; +import chalk from 'chalk'; +import { ChainConfig } from '@nomicfoundation/hardhat-verify/types'; +import { delay, encodeArguments, nextAttemptDelay, retrieveContractBytecode } from '../utils'; +import { ZkSyncVerifyPluginError } from '../errors'; +import { Bytecode } from '../solc/bytecode'; +import { + COMPILER_VERSION_NOT_SUPPORTED, + CONST_ARGS_ARRAY_ERROR, + JSON_INPUT_CODE_FORMAT, + PENDING_CONTRACT_INFORMATION_MESSAGE, + TASK_COMPILE, + TASK_VERIFY_GET_COMPILER_VERSIONS, + TASK_VERIFY_GET_CONTRACT_INFORMATION, +} from '../constants'; +import { ContractInformation } from '../solc/types'; +import { getMinimalResolvedFiles, getSolidityStandardJsonInput } from '../plugin'; +import { ZkSyncEtherscanExplorerVerifyRequest, ZkSyncExplorerVerifyRequest } from './verify-contract-request'; +import { VerificationStatusResponse } from './verification-status-response'; +import { PROVIDED_CHAIN_IS_NOT_SUPPORTED_FOR_VERIFICATION } from './constants'; + +export type VerificationServiceVerificationIdReturnType = string | number; +export type VerificationServiceVerifyRequest = ZkSyncExplorerVerifyRequest | ZkSyncEtherscanExplorerVerifyRequest; +export type VerificationServiceInitialVerifyRequest = ZkSyncExplorerVerifyRequest; +export type VerificationServiceVerificationStatus = VerificationStatusResponse; +export interface VerificationServiceVerifyResponse< + V extends VerificationServiceVerificationIdReturnType = VerificationServiceVerificationIdReturnType, +> { + verificationId: V; + contractVerifyDataInfo: ContractVerifyDataInfo; +} + +export interface ContractVerifyDataInfo { + contractName: string; + contractAddress: string; +} + +export abstract class VerificationService< + ReturnVerificationIdType extends + VerificationServiceVerificationIdReturnType = VerificationServiceVerificationIdReturnType, + ContractVerifyRequestType extends VerificationServiceVerifyRequest = VerificationServiceVerifyRequest, + VerificationStatusType extends VerificationServiceVerificationStatus = VerificationServiceVerificationStatus, + VerificationServiceVerifyResponseType = VerificationServiceVerifyResponse, +> { + constructor( + protected hre: HardhatRuntimeEnvironment, + protected verifyUrl: string, + protected browserUrl?: string, + ) {} + protected abstract generateRequest( + initialRequest: VerificationServiceInitialVerifyRequest, + ): ContractVerifyRequestType; + protected abstract getVerificationId( + initialRequest: VerificationServiceInitialVerifyRequest, + ): Promise; + public abstract getVerificationStatus( + verificationId: ReturnVerificationIdType, + contractVerifyDataInfo: ContractVerifyDataInfo, + ): Promise; + protected abstract getSupportedCompilerVersions(): Promise; + protected abstract getSolcVersion(contractInformation: ContractInformation): Promise; + protected abstract getContractBorwserUrl(address: string): string | undefined; + + public static async getCurrentChainConfig( + ethereumProvider: EthereumProvider, + customChains: ChainConfig[], + builtinChains: ChainConfig[], + ): Promise { + const currentChainId = parseInt(await ethereumProvider.send('eth_chainId'), 16); + + const currentChainConfig = [...[...customChains].reverse(), ...builtinChains].find( + ({ chainId }) => chainId === currentChainId, + ); + + if (currentChainConfig === undefined) { + throw new ZkSyncVerifyPluginError(PROVIDED_CHAIN_IS_NOT_SUPPORTED_FOR_VERIFICATION(currentChainId)); + } + + return currentChainConfig; + } + + public async verify( + address: string, + contract: string, + constructorArguments: any, + libraries: any, + noCompile: boolean, + isWithFullContext: boolean = false, + ): Promise { + const { isAddress } = await import('@ethersproject/address'); + if (!isAddress(address)) { + throw new ZkSyncVerifyPluginError(`${address} is an invalid address.`); + } + + const deployedBytecodeHex = await retrieveContractBytecode(address, this.hre); + const deployedBytecode = new Bytecode(deployedBytecodeHex); + + if (!noCompile) { + await this.hre.run(TASK_COMPILE, { quiet: true }); + } + + const compilerVersions: string[] = await this.hre.run(TASK_VERIFY_GET_COMPILER_VERSIONS); + + const contractInformation: ContractInformation = await this.hre.run(TASK_VERIFY_GET_CONTRACT_INFORMATION, { + contract, + deployedBytecode, + matchingCompilerVersions: compilerVersions, + libraries, + }); + + const optimizationUsed = contractInformation.compilerInput.settings.optimizer.enabled ?? false; + + let deployArgumentsEncoded; + if (!Array.isArray(constructorArguments)) { + if (constructorArguments.startsWith('0x')) { + deployArgumentsEncoded = constructorArguments; + } else { + throw new ZkSyncVerifyPluginError(chalk.red(CONST_ARGS_ARRAY_ERROR)); + } + } else { + deployArgumentsEncoded = `0x${await encodeArguments( + contractInformation.contractOutput.abi, + constructorArguments, + )}`; + } + + const compilerPossibleVersions = await this.getSupportedCompilerVersions(); + const compilerVersion: string = contractInformation.solcVersion; + if (!compilerPossibleVersions.includes(compilerVersion)) { + throw new ZkSyncVerifyPluginError(COMPILER_VERSION_NOT_SUPPORTED); + } + + const request: VerificationServiceInitialVerifyRequest = { + contractAddress: address, + sourceCode: getSolidityStandardJsonInput( + this.hre, + await getMinimalResolvedFiles(this.hre, contractInformation.sourceName), + contractInformation.compilerInput, + ), + codeFormat: JSON_INPUT_CODE_FORMAT, + contractName: `${contractInformation.sourceName}:${contractInformation.contractName}`, + compilerSolcVersion: await this.getSolcVersion(contractInformation), + compilerZksolcVersion: `v${contractInformation.contractOutput.metadata.zk_version}`, + constructorArguments: deployArgumentsEncoded, + optimizationUsed, + }; + if (isWithFullContext) { + request.sourceCode.sources = contractInformation.compilerInput.sources; + } + + const verificationId = await this.getVerificationId(request); + + console.info(chalk.cyan(`Your verification ID is: ${verificationId}`)); + + return { + contractVerifyDataInfo: { + contractName: request.contractName, + contractAddress: request.contractAddress, + }, + verificationId, + } as VerificationServiceVerifyResponseType; + } + + public async getVerificationStatusWithRetry( + verificationId: ReturnVerificationIdType, + contractVerifyDataInfo: ContractVerifyDataInfo, + maxRetries = 11, + baseRetries = 5, + baseDelayInMs = 2000, + ): Promise { + let retries = 0; + let response: VerificationStatusType; + + while (true) { + response = await this.getVerificationStatus(verificationId, contractVerifyDataInfo); + + if (response.isPending()) { + retries += 1; + if (retries > maxRetries) { + throw new ZkSyncVerifyPluginError(PENDING_CONTRACT_INFORMATION_MESSAGE(this.browserUrl)); + } + + const delayInMs = nextAttemptDelay(retries, baseDelayInMs, baseRetries); + await delay(delayInMs); + } else { + break; + } + } + + return response; + } +} + +export interface VerificationServiceVerificationIdResponse { + isOk(): boolean; +} diff --git a/packages/hardhat-zksync-verify/src/explorers/verification-status-response.ts b/packages/hardhat-zksync-verify/src/explorers/verification-status-response.ts new file mode 100644 index 000000000..55ecf22c7 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/verification-status-response.ts @@ -0,0 +1,8 @@ +export interface VerificationStatusResponse { + isPending(): boolean; + isFailure(): boolean; + isSuccess(): boolean; + getError(): string | undefined; + errorExists(): boolean; + isOk(): boolean; +} diff --git a/packages/hardhat-zksync-verify/src/explorers/verify-contract-request.ts b/packages/hardhat-zksync-verify/src/explorers/verify-contract-request.ts new file mode 100644 index 000000000..ca75870b7 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/verify-contract-request.ts @@ -0,0 +1,27 @@ +import { CompilerInput } from 'hardhat/types'; + +export interface ZkSyncExplorerVerifyRequest { + contractAddress: string; + contractName: string; + sourceCode: CompilerInput; + codeFormat: string; + compilerSolcVersion: string; + compilerZksolcVersion: string; + optimizationUsed: boolean; + constructorArguments: string; +} + +export interface ZkSyncEtherscanExplorerVerifyRequest { + action: 'verifysourcecode'; + module: 'contract'; + apikey: string; + compilermode: 'zksync'; + zksolcVersion: string; + contractaddress: string; + contractname: string; + sourceCode: CompilerInput; + codeformat: string; + compilerversion: string; + optimizationUsed?: string; + constructorArguements: string; +} diff --git a/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/chain-config.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/chain-config.ts new file mode 100644 index 000000000..2c1f093f3 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/chain-config.ts @@ -0,0 +1,20 @@ +import { ChainConfig } from '@nomicfoundation/hardhat-verify/types'; + +export const builtinChains: ChainConfig[] = [ + { + network: 'zksyncmainnet', + chainId: 324, + urls: { + apiURL: 'https://zksync2-mainnet-explorer.zksync.io/contract_verification', + browserURL: 'https://explorer.zksync.io/', + }, + }, + { + network: 'zksyncsepolia', + chainId: 300, + urls: { + apiURL: 'https://explorer.sepolia.era.zksync.dev/contract_verification', + browserURL: 'https://sepolia.explorer.zksync.io/', + }, + }, +]; diff --git a/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/errors.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/errors.ts new file mode 100644 index 000000000..3714c4c98 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/errors.ts @@ -0,0 +1,18 @@ +import { ZkSyncVerifyPluginError } from '../../errors'; + +export class ZksyncMissingApiKeyError extends ZkSyncVerifyPluginError { + constructor(network: string) { + super(`You are trying to verify a contract in '${network}', but no API token was found for this network. Please provide one in your hardhat config. For example: + + { + ... + etherscan: { + apiKey: { + ${network}: 'your API key' + } + } + } + + See https://etherscan.io/apis`); + } +} diff --git a/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/service.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/service.ts new file mode 100644 index 000000000..a8fddb244 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/service.ts @@ -0,0 +1,153 @@ +import axios from 'axios'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { ChainConfig } from '@nomicfoundation/hardhat-verify/types'; +import { extractQueryParams, handleAxiosError } from '../../utils'; +import { + ContractVerifyDataInfo, + VerificationService, + VerificationServiceInitialVerifyRequest, + VerificationServiceVerificationIdResponse, +} from '../service'; +import { ZkSyncExplorerVerifyRequest } from '../verify-contract-request'; +import { ZkSyncVerifyPluginError } from '../../errors'; +import { ContractInformation } from '../../solc/types'; +import { COMPILATION_ERRORS, NO_MATCHING_CONTRACT } from '../../constants'; +import { ZksyncContractVerificationInvalidStatusCodeError } from '../errors'; +import { ZksyncBlockExplorerResponse } from './verification-status-response'; +import { builtinChains } from './chain-config'; + +export class ZkSyncExplorerService extends VerificationService< + number, + ZkSyncExplorerVerifyRequest, + ZksyncBlockExplorerResponse +> { + public static async fromChainConfig(hre: HardhatRuntimeEnvironment, chainConfig: ChainConfig) { + const apiUrl = chainConfig.urls.apiURL; + const browserUrl = chainConfig.urls.browserURL + ? chainConfig.urls.browserURL.trim().replace(/\/$/, '') + : undefined; + + return new ZkSyncExplorerService(hre, apiUrl, browserUrl); + } + + public async getVerificationStatus( + verificationId: number, + contractInformation: ContractVerifyDataInfo, + ): Promise { + try { + const [verifyURL, params] = extractQueryParams(this.verifyUrl); + + const response = await axios.get(`${verifyURL}/${verificationId}`, { params }); + + if (response.status !== 200) { + throw new ZksyncContractVerificationInvalidStatusCodeError( + this.verifyUrl, + response.status, + response.data, + ); + } + + const verificationStatusResponse = new ZksyncBlockExplorerResponse(response); + + if (verificationStatusResponse.isPending()) { + return verificationStatusResponse; + } + + if (verificationStatusResponse.isFailure()) { + if ( + verificationStatusResponse.getError() !== NO_MATCHING_CONTRACT && + COMPILATION_ERRORS.filter((compilationError) => + compilationError.pattern.test(verificationStatusResponse.getError()), + ).length === 0 + ) { + throw new ZkSyncVerifyPluginError(verificationStatusResponse.getError()); + } + + return verificationStatusResponse; + } + + if (!verificationStatusResponse.isOk()) { + throw new ZkSyncVerifyPluginError(verificationStatusResponse.getError()); + } + + if (verificationStatusResponse.isSuccess()) { + console.log(`Successfully verified contract ${contractInformation.contractName} on the block explorer. + ${this.getContractBorwserUrl(contractInformation.contractAddress)} + `); + } + + return verificationStatusResponse; + } catch (error) { + handleAxiosError(error); + } + } + protected generateRequest(initialRequest: VerificationServiceInitialVerifyRequest): ZkSyncExplorerVerifyRequest { + return initialRequest as ZkSyncExplorerVerifyRequest; + } + + protected async getVerificationId(req: VerificationServiceInitialVerifyRequest): Promise { + try { + const [verifyUrl, params] = extractQueryParams(this.verifyUrl); + const request = this.generateRequest(req); + const response = await axios.post(verifyUrl, request, params); + const zkSyncBlockExplorerResponse = new ZkSyncBlockExplorerVerificationIdResponse(response); + + if (!zkSyncBlockExplorerResponse.isOk()) { + throw new ZkSyncVerifyPluginError(zkSyncBlockExplorerResponse.message); + } + + return parseInt(zkSyncBlockExplorerResponse.message, 10); + } catch (error) { + handleAxiosError(error); + } + } + + public async getSupportedCompilerVersions(): Promise { + try { + const [verifyUrl, params] = extractQueryParams(this.verifyUrl); + const response = await axios.get(`${verifyUrl}/solc_versions`, { params }); + return response.data; + } catch (error) { + handleAxiosError(error); + } + } + + protected async getSolcVersion(contractInformation: ContractInformation): Promise { + return contractInformation.solcVersion; + } + + protected getContractBorwserUrl(address: string): string | undefined { + return this.browserUrl || this.browserUrl === '' ? `${this.browserUrl}/address/${address}#contract` : ''; + } +} + +export async function getProvidedChainConfig(hre: HardhatRuntimeEnvironment) { + const currentChainId = parseInt(await hre.network.provider.send('eth_chainId'), 16); + return hre.network.config.verifyURL + ? { + network: hre.network.name, + chainId: currentChainId, + urls: { + apiURL: hre.network.config.verifyURL, + browserURL: + hre.network.config.browserVerifyURL ?? + builtinChains.find((b) => b.chainId === currentChainId)?.urls.browserURL ?? + '', + }, + } + : undefined; +} + +export class ZkSyncBlockExplorerVerificationIdResponse implements VerificationServiceVerificationIdResponse { + public readonly status: number; + public readonly message: string; + + constructor(response: any) { + this.status = parseInt(response.status, 10); + this.message = response.data; + } + + public isOk() { + return this.status === 200; + } +} diff --git a/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/task-actions.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/task-actions.ts new file mode 100644 index 000000000..f479ecf5e --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/task-actions.ts @@ -0,0 +1,60 @@ +import { subtask, types } from 'hardhat/config'; +import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types'; +import chalk from 'chalk'; +import { TASK_VERIFY_ZKSYNC_EXPLORER } from '../../constants'; +import { ZkSyncVerifyPluginError } from '../../errors'; +import { TRYING_VERIFICATION_WITH_FULL_COMPILER_INPUT } from '../constants'; +import { getProvidedChainConfig, ZkSyncExplorerService } from './service'; +import { builtinChains } from './chain-config'; + +subtask(TASK_VERIFY_ZKSYNC_EXPLORER) + .addParam('address') + .addOptionalParam('constructorArguments', undefined, undefined, types.any) + .addOptionalParam('libraries', undefined, undefined, types.any) + .addOptionalParam('contract') + .addFlag('force') + .addFlag('noCompile') + .setAction(async (taskArgs: TaskArguments, hre: HardhatRuntimeEnvironment) => { + const providedChain = await getProvidedChainConfig(hre); + const chainConfig = await ZkSyncExplorerService.getCurrentChainConfig( + hre.network.provider, + providedChain ? [providedChain] : [], + builtinChains, + ); + + const explorer = await ZkSyncExplorerService.fromChainConfig(hre, chainConfig); + + const { verificationId, contractVerifyDataInfo } = await explorer.verify( + taskArgs.address, + taskArgs.contract, + taskArgs.constructorArguments, + taskArgs.libraries, + taskArgs.noCompile, + ); + const result = await explorer.getVerificationStatusWithRetry(verificationId, contractVerifyDataInfo); + + if (result.isSuccess()) { + return; + } + + console.warn(chalk.red(TRYING_VERIFICATION_WITH_FULL_COMPILER_INPUT(contractVerifyDataInfo.contractName))); + + const { verificationId: verificationIdFallback } = await explorer.verify( + taskArgs.address, + taskArgs.contract, + taskArgs.constructorArguments, + taskArgs.libraries, + taskArgs.noCompile, + true, + ); + const fallbackResult = await explorer.getVerificationStatusWithRetry( + verificationIdFallback, + contractVerifyDataInfo, + ); + + if (fallbackResult.isSuccess()) { + return; + } + + throw new ZkSyncVerifyPluginError(fallbackResult.getError()); + }); diff --git a/packages/hardhat-zksync-verify/src/zksync-block-explorer/verification-status-response.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/verification-status-response.ts similarity index 54% rename from packages/hardhat-zksync-verify/src/zksync-block-explorer/verification-status-response.ts rename to packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/verification-status-response.ts index f0ec1fa1e..04e66a905 100644 --- a/packages/hardhat-zksync-verify/src/zksync-block-explorer/verification-status-response.ts +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-block-explorer/verification-status-response.ts @@ -1,12 +1,7 @@ -enum VerificationStatusEnum { - SUCCESSFUL = 'successful', - FAILED = 'failed', - QUEUED = 'queued', - IN_PROGRESS = 'in_progress', -} +import { VerificationStatusResponse } from '../verification-status-response'; -export class VerificationStatusResponse { - public readonly status: VerificationStatusEnum; +export class ZksyncBlockExplorerResponse implements VerificationStatusResponse { + public readonly status: string; public readonly error: string | undefined; public readonly compilationErrors: string[] | undefined; @@ -35,18 +30,18 @@ export class VerificationStatusResponse { } public isPending() { - return this.status === VerificationStatusEnum.IN_PROGRESS; + return this.status === 'in_progress' || this.status === 'queued'; } - public isVerificationFailure() { - return this.status === VerificationStatusEnum.FAILED; + public isFailure() { + return this.status === 'failed'; } - public isQueued() { - return this.status === VerificationStatusEnum.QUEUED; + public isOk() { + return this.status === 'successful'; } - public isVerificationSuccess() { - return this.status === VerificationStatusEnum.SUCCESSFUL; + public isSuccess() { + return this.isOk(); } } diff --git a/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/chain-config.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/chain-config.ts new file mode 100644 index 000000000..4f9dc728f --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/chain-config.ts @@ -0,0 +1,20 @@ +import { ChainConfig } from '@nomicfoundation/hardhat-verify/types'; + +export const builtinChains: ChainConfig[] = [ + { + network: 'zksyncmainnet', + chainId: 324, + urls: { + apiURL: 'https://api-era.zksync.network/api', + browserURL: 'https://era.zksync.network/', + }, + }, + { + network: 'zksyncsepolia', + chainId: 300, + urls: { + apiURL: 'https://api-sepolia-era.zksync.network/api', + browserURL: 'https://sepolia-era.zksync.network/', + }, + }, +]; diff --git a/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/constants.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/constants.ts new file mode 100644 index 000000000..e491efc4e --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/constants.ts @@ -0,0 +1,5 @@ +export const SOLC_COMPILERS_LIST = + 'https://raw.githubusercontent.com/ethereum/solc-bin/refs/heads/gh-pages/bin/list.json'; +export const SOLC_COMPILERS_LIST_ERROR = 'Unable to fetch solc versions from GitHub!'; +export const SOLC_COMPILER_VERSION_NOTFOUND = (version: string) => + `Solidity compiler version ${version} is not found. See https://etherscan.io/solcversions for list of supported solc versions`; diff --git a/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/service.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/service.ts new file mode 100644 index 000000000..e58551d67 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/service.ts @@ -0,0 +1,205 @@ +import { ApiKey, ChainConfig } from '@nomicfoundation/hardhat-verify/types'; +import axios from 'axios'; +import qs from 'querystring'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { + ContractVerifyDataInfo, + VerificationService, + VerificationServiceInitialVerifyRequest, + VerificationServiceVerificationIdResponse, +} from '../service'; +import { ZksyncMissingApiKeyError } from '../zksync-block-explorer/errors'; +import { extractQueryParams, handleAxiosError } from '../../utils'; +import { ZkSyncVerifyPluginError } from '../../errors'; +import { ZkSyncEtherscanExplorerVerifyRequest } from '../verify-contract-request'; +import { TASK_VERIFY_GET_COMPILER_VERSIONS } from '../../constants'; +import { ContractInformation } from '../../solc/types'; +import { ZksyncContractVerificationInvalidStatusCodeError } from '../errors'; +import { ZksyncEtherscanResponse } from './verification-status-response'; +import { SOLC_COMPILER_VERSION_NOTFOUND, SOLC_COMPILERS_LIST, SOLC_COMPILERS_LIST_ERROR } from './constants'; + +export class ZkSyncEtherscanExplorerService extends VerificationService< + string, + ZkSyncEtherscanExplorerVerifyRequest, + ZksyncEtherscanResponse +> { + constructor( + hre: HardhatRuntimeEnvironment, + private apikey: string, + verifyUrl: string, + browserUrl?: string, + ) { + super(hre, verifyUrl, browserUrl); + } + + public static async fromChainConfig( + hre: HardhatRuntimeEnvironment, + apiKey: ApiKey | undefined, + chainConfig: ChainConfig, + ) { + const resolvedApiKey = resolveApiKey(apiKey, chainConfig.network); + const apiUrl = chainConfig.urls.apiURL; + const browserUrl = chainConfig.urls.browserURL + ? chainConfig.urls.browserURL.trim().replace(/\/$/, '') + : undefined; + + return new ZkSyncEtherscanExplorerService(hre, resolvedApiKey, apiUrl, browserUrl); + } + + public async getVerificationStatus( + verificationId: string, + contractInformation: ContractVerifyDataInfo, + ): Promise { + try { + const [verifyUrl, params] = extractQueryParams(this.verifyUrl); + const response = await axios.get(verifyUrl, { + params: { + apikey: this.apikey, + module: 'contract', + action: 'checkverifystatus', + guid: verificationId, + ...params, + }, + }); + + if (response.status !== 200) { + throw new ZksyncContractVerificationInvalidStatusCodeError( + this.verifyUrl, + response.status, + response.data, + ); + } + + const zkSyncBlockExplorerResponse = new ZksyncEtherscanResponse(response.data); + + if (zkSyncBlockExplorerResponse.isPending()) { + return zkSyncBlockExplorerResponse; + } + + if (zkSyncBlockExplorerResponse.isFailure() || zkSyncBlockExplorerResponse.isAlreadyVerified()) { + return zkSyncBlockExplorerResponse; + } + + if (!zkSyncBlockExplorerResponse.isOk()) { + throw new ZkSyncVerifyPluginError(zkSyncBlockExplorerResponse.message); + } + + if (zkSyncBlockExplorerResponse.isSuccess()) { + console.log(`Successfully verified contract ${contractInformation.contractName} on the block explorer. + ${this.getContractBorwserUrl(contractInformation.contractAddress)} + `); + } + + return zkSyncBlockExplorerResponse; + } catch (error) { + handleAxiosError(error); + } + } + + protected generateRequest( + initialRequest: VerificationServiceInitialVerifyRequest, + ): ZkSyncEtherscanExplorerVerifyRequest { + return { + compilermode: 'zksync', + module: 'contract', + action: 'verifysourcecode', + apikey: this.apikey, + zksolcVersion: initialRequest.compilerZksolcVersion, + contractaddress: initialRequest.contractAddress, + contractname: initialRequest.contractName, + sourceCode: initialRequest.sourceCode, + codeformat: 'solidity-standard-json-input', + compilerversion: initialRequest.compilerSolcVersion, + constructorArguements: initialRequest.constructorArguments.slice(2), + }; + } + + protected async getVerificationId(req: VerificationServiceInitialVerifyRequest): Promise { + try { + const [verifyUrl, params] = extractQueryParams(this.verifyUrl); + const request = this.generateRequest(req); + const response = await axios.post( + verifyUrl, + qs.stringify({ + ...request, + ...params, + sourceCode: JSON.stringify(request.sourceCode), + }), + ); + const zkSyncEtherscanResponse = new ZkSyncEtherscanVerificationIdResponse(response.data); + + if (!zkSyncEtherscanResponse.isOk()) { + throw new ZkSyncVerifyPluginError(zkSyncEtherscanResponse.result); + } + + return zkSyncEtherscanResponse.result; + } catch (error) { + handleAxiosError(error); + } + } + + protected getContractBorwserUrl(address: string): string | undefined { + return this.browserUrl || this.browserUrl === '' ? `${this.browserUrl}/address/${address}#code` : ''; + } + + protected async getSupportedCompilerVersions(): Promise { + return await this.hre.run(TASK_VERIFY_GET_COMPILER_VERSIONS); + } + + protected async getSolcVersion(contractInformation: ContractInformation): Promise { + const solcVersion = contractInformation.solcLongVersion; + + if (!solcVersion.startsWith('zkVM')) { + return `v${solcVersion}`; + } + + const response = await axios.get(SOLC_COMPILERS_LIST); + + if (response.status !== 200) { + throw new ZkSyncVerifyPluginError(SOLC_COMPILERS_LIST_ERROR); + } + + const normalSolcVersion = solcVersion.split('-')[1]; + const solcBuild = response.data.builds.find((b: any) => b.version === normalSolcVersion && !b.prerelease); + + if (!solcBuild) { + throw new ZkSyncVerifyPluginError(SOLC_COMPILER_VERSION_NOTFOUND(normalSolcVersion)); + } + + return `v${solcBuild.longVersion}`; + } +} + +function resolveApiKey(apiKey: ApiKey | undefined, network: string) { + if (apiKey === undefined || apiKey === '') { + throw new ZksyncMissingApiKeyError(network); + } + + if (typeof apiKey === 'string') { + return apiKey; + } + + const key = apiKey[network]; + + if (key === undefined || key === '') { + throw new ZksyncMissingApiKeyError(network); + } + + return key; +} + +export class ZkSyncEtherscanVerificationIdResponse implements VerificationServiceVerificationIdResponse { + public readonly status: number; + public readonly message: string; + public readonly result: string; + + constructor(response: any) { + this.status = parseInt(response.status, 10); + this.message = response.message; + this.result = response.result; + } + + public isOk() { + return this.status === 1 && this.message === 'OK'; + } +} diff --git a/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/task-actions.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/task-actions.ts new file mode 100644 index 000000000..4fa46cf68 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/task-actions.ts @@ -0,0 +1,84 @@ +import { subtask, types } from 'hardhat/config'; +import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types'; +import chalk from 'chalk'; +import { TASK_VERIFY_ETHERSCAN, TASK_VERIFY_RESOLVE_ARGUMENTS, TASK_VERIFY_ZKSYNC_ETHERSCAN } from '../../constants'; +import { ZkSyncVerifyPluginError } from '../../errors'; +import { TRYING_VERIFICATION_WITH_FULL_COMPILER_INPUT } from '../constants'; +import { ZkSyncEtherscanExplorerService } from './service'; +import { builtinChains } from './chain-config'; + +subtask(TASK_VERIFY_ETHERSCAN) + .addFlag('noCompile') + .setAction(async (taskArgs: TaskArguments, { run, network }: HardhatRuntimeEnvironment, runSuper) => { + if (!network.config.zksync) { + return await runSuper(taskArgs); + } + const { address, constructorArguments, libraries, contractFQN, force } = await run( + TASK_VERIFY_RESOLVE_ARGUMENTS, + taskArgs, + ); + + return await run(TASK_VERIFY_ZKSYNC_ETHERSCAN, { + address, + constructorArguments, + libraries, + contractFQN, + force, + noCompile: taskArgs.noCompile, + }); + }); + +subtask(TASK_VERIFY_ZKSYNC_ETHERSCAN) + .addParam('address') + .addOptionalParam('constructorArguments', undefined, undefined, types.any) + .addOptionalParam('libraries', undefined, undefined, types.any) + .addOptionalParam('contract') + .addFlag('force') + .addFlag('noCompile') + .setAction(async (taskArgs: TaskArguments, hre: HardhatRuntimeEnvironment) => { + const chainConfig = await ZkSyncEtherscanExplorerService.getCurrentChainConfig( + hre.network.provider, + hre.config.etherscan.customChains, + builtinChains, + ); + + const etherscan = await ZkSyncEtherscanExplorerService.fromChainConfig( + hre, + hre.config.etherscan.apiKey, + chainConfig, + ); + + const { verificationId, contractVerifyDataInfo } = await etherscan.verify( + taskArgs.address, + taskArgs.contract, + taskArgs.constructorArguments, + taskArgs.libraries, + taskArgs.noCompile, + ); + const result = await etherscan.getVerificationStatusWithRetry(verificationId, contractVerifyDataInfo); + + if (result.isSuccess()) { + return; + } + + console.warn(chalk.red(TRYING_VERIFICATION_WITH_FULL_COMPILER_INPUT(contractVerifyDataInfo.contractName))); + + const { verificationId: verificationIdFallback } = await etherscan.verify( + taskArgs.address, + taskArgs.contract, + taskArgs.constructorArguments, + taskArgs.libraries, + taskArgs.noCompile, + true, + ); + const fallbackResult = await etherscan.getVerificationStatusWithRetry( + verificationIdFallback, + contractVerifyDataInfo, + ); + + if (fallbackResult.isSuccess()) { + return; + } + + throw new ZkSyncVerifyPluginError(fallbackResult.message); + }); diff --git a/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/verification-status-response.ts b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/verification-status-response.ts new file mode 100644 index 000000000..1505e5612 --- /dev/null +++ b/packages/hardhat-zksync-verify/src/explorers/zksync-etherscan/verification-status-response.ts @@ -0,0 +1,45 @@ +import { VerificationStatusResponse } from '../verification-status-response'; + +export class ZksyncEtherscanResponse implements VerificationStatusResponse { + public readonly status: number; + public readonly message: string; + + constructor(response: any) { + this.status = parseInt(response.status, 10); + this.message = response.result; + } + public errorExists(): boolean { + return !this.isSuccess() || !this.isPending(); + } + + public getError(): string | undefined { + return this.errorExists() ? this.message : undefined; + } + + public isPending() { + return this.message === 'Pending in queue'; + } + + public isFailure() { + return this.message.startsWith('Fail - Unable to verify'); + } + + public isSuccess() { + return this.message === 'Pass - Verified'; + } + + public isBytecodeMissingInNetworkError() { + return this.message.startsWith('Unable to locate ContractCode at'); + } + + public isAlreadyVerified() { + return ( + this.message.startsWith('Contract source code already verified') || + this.message.startsWith('Already Verified') + ); + } + + public isOk() { + return this.status === 1; + } +} diff --git a/packages/hardhat-zksync-verify/src/index.ts b/packages/hardhat-zksync-verify/src/index.ts index c41e6d16b..46935bda5 100644 --- a/packages/hardhat-zksync-verify/src/index.ts +++ b/packages/hardhat-zksync-verify/src/index.ts @@ -1,26 +1,38 @@ import '@nomicfoundation/hardhat-verify'; import { extendEnvironment, subtask, task, types } from 'hardhat/config'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; import './type-extensions'; +import './explorers/zksync-block-explorer/task-actions'; +import './explorers/zksync-etherscan/task-actions'; import { TASK_VERIFY, TASK_VERIFY_GET_COMPILER_VERSIONS, TASK_VERIFY_VERIFY, - TESTNET_VERIFY_URL, TASK_VERIFY_GET_CONTRACT_INFORMATION, - TASK_CHECK_VERIFICATION_STATUS, TASK_VERIFY_GET_CONSTRUCTOR_ARGUMENTS, + TASK_VERIFY_GET_VERIFICATION_SUBTASKS, + TASK_VERIFY_RESOLVE_ARGUMENTS, } from './constants'; -import { getCompilerVersions, verify, verifyContract, getContractInfo, getConstructorArguments } from './task-actions'; -import { checkVerificationStatus } from './plugin'; +import { + getCompilerVersions, + verify, + verifyContract, + getContractInfo, + getConstructorArguments, + resolveArguments, + getVerificationSubtasks, +} from './task-actions'; -extendEnvironment((hre: HardhatRuntimeEnvironment) => { - hre.network.verifyURL = hre.network.config.verifyURL ?? TESTNET_VERIFY_URL; +extendEnvironment((hre) => { + if (hre.network.config.zksync) { + hre.network.config.enableVerifyURL = hre.network.config.enableVerifyURL ?? false; + } }); +subtask(TASK_VERIFY_GET_VERIFICATION_SUBTASKS).setAction(getVerificationSubtasks); + task(TASK_VERIFY, 'Verifies contract on Ethereum and ZKsync networks') .addFlag('noCompile', 'Run verify without compile') .setAction(verify); @@ -33,6 +45,36 @@ subtask(TASK_VERIFY_GET_CONSTRUCTOR_ARGUMENTS).setAction(getConstructorArguments subtask(TASK_VERIFY_GET_CONTRACT_INFORMATION).setAction(getContractInfo); -task(TASK_CHECK_VERIFICATION_STATUS) - .addParam('verificationId', 'An ID returned by the verification request', undefined, types.int) - .setAction(checkVerificationStatus); +subtask(TASK_VERIFY_RESOLVE_ARGUMENTS, 'Resolve verify arguments') + .addOptionalPositionalParam('address', 'Address of the contract to verify') + .addOptionalVariadicPositionalParam( + 'constructorArgsParams', + 'Contract constructor arguments. Cannot be used if the --constructor-args option is provided', + [], + ) + .addOptionalParam( + 'constructorArgs', + 'Path to a Javascript module that exports the constructor arguments', + undefined, + types.inputFile, + ) + .addOptionalParam( + 'libraries', + 'Path to a Javascript module that exports a dictionary of library addresses. ' + + 'Use if there are undetectable library addresses in your contract. ' + + 'Library addresses are undetectable if they are only used in the contract constructor', + undefined, + types.inputFile, + ) + .addOptionalParam( + 'contract', + 'Fully qualified name of the contract to verify. Skips automatic detection of the contract. ' + + 'Use if the deployed bytecode matches more than one contract in your project', + ) + .addFlag( + 'force', + 'Enforce contract verification even if the contract is already verified. ' + + 'Use to re-verify partially verified contracts on Blockscout', + ) + .addFlag('noCompile') + .setAction(resolveArguments); diff --git a/packages/hardhat-zksync-verify/src/plugin.ts b/packages/hardhat-zksync-verify/src/plugin.ts index 6107e4ab4..d70b7a4b9 100644 --- a/packages/hardhat-zksync-verify/src/plugin.ts +++ b/packages/hardhat-zksync-verify/src/plugin.ts @@ -5,12 +5,10 @@ import { import { Artifacts, CompilerInput, DependencyGraph, HardhatRuntimeEnvironment, ResolvedFile } from 'hardhat/types'; import { isFullyQualifiedName, parseFullyQualifiedName } from 'hardhat/utils/contract-names'; import path from 'path'; -import chalk from 'chalk'; import { isBreakableCompilerVersion } from '@matterlabs/hardhat-zksync-solc/dist/src/utils'; import { CONTRACT_NAME_NOT_FOUND, NO_MATCHING_CONTRACT, LIBRARIES_EXPORT_ERROR } from './constants'; import { Bytecode, extractMatchingContractInformation } from './solc/bytecode'; import { ZkSyncVerifyPluginError } from './errors'; -import { executeVeificationWithRetry } from './utils'; export async function inferContractArtifacts( artifacts: Artifacts, @@ -131,14 +129,3 @@ export async function getLibraries(librariesModule: string) { ); } } - -export async function checkVerificationStatus(args: { verificationId: number }, hre: HardhatRuntimeEnvironment) { - const isValidVerification = await executeVeificationWithRetry(args.verificationId, hre.network.verifyURL); - - if (isValidVerification?.errorExists()) { - throw new ZkSyncVerifyPluginError(`Backend verification error: ${isValidVerification.getError()}`); - } - - console.info(chalk.green(`Contract successfully verified on ZKsync block explorer!`)); - return true; -} diff --git a/packages/hardhat-zksync-verify/src/solc/bytecode.ts b/packages/hardhat-zksync-verify/src/solc/bytecode.ts index f04f1e92e..fcb27860e 100644 --- a/packages/hardhat-zksync-verify/src/solc/bytecode.ts +++ b/packages/hardhat-zksync-verify/src/solc/bytecode.ts @@ -67,6 +67,7 @@ export async function extractMatchingContractInformation( compilerInput: buildInfo.input, contractOutput: buildInfo.output.contracts[sourceName][contractName], solcVersion: buildInfo.solcVersion, + solcLongVersion: buildInfo.solcLongVersion, sourceName, contractName, }; diff --git a/packages/hardhat-zksync-verify/src/solc/types.ts b/packages/hardhat-zksync-verify/src/solc/types.ts index 1f872da10..2b38fc884 100644 --- a/packages/hardhat-zksync-verify/src/solc/types.ts +++ b/packages/hardhat-zksync-verify/src/solc/types.ts @@ -10,6 +10,7 @@ export type ContractName = string; export interface ContractInformation { compilerInput: CompilerInput; + solcLongVersion: string; solcVersion: string; sourceName: string; contractName: string; diff --git a/packages/hardhat-zksync-verify/src/task-actions.ts b/packages/hardhat-zksync-verify/src/task-actions.ts index dcaaac2fc..19b798132 100644 --- a/packages/hardhat-zksync-verify/src/task-actions.ts +++ b/packages/hardhat-zksync-verify/src/task-actions.ts @@ -1,47 +1,34 @@ import { HardhatRuntimeEnvironment, RunSuperFunction, TaskArguments } from 'hardhat/types'; import { parseFullyQualifiedName } from 'hardhat/utils/contract-names'; -import chalk from 'chalk'; + import path from 'path'; import { getLatestEraVersion } from '@matterlabs/hardhat-zksync-solc/dist/src/utils'; import { ZKSOLC_COMPILER_PATH_VERSION } from '@matterlabs/hardhat-zksync-solc/dist/src/constants'; -import { getSupportedCompilerVersions, verifyContractRequest } from './zksync-block-explorer/service'; +import { VerificationSubtask } from '@nomicfoundation/hardhat-verify'; +import chalk from 'chalk'; import { - TASK_COMPILE, TASK_VERIFY_GET_CONSTRUCTOR_ARGUMENTS, - TASK_VERIFY_VERIFY, - TESTNET_VERIFY_URL, NO_VERIFIABLE_ADDRESS_ERROR, - CONST_ARGS_ARRAY_ERROR, - TASK_VERIFY_GET_COMPILER_VERSIONS, - TASK_VERIFY_GET_CONTRACT_INFORMATION, NO_MATCHING_CONTRACT, - COMPILER_VERSION_NOT_SUPPORTED, - TASK_CHECK_VERIFICATION_STATUS, - JSON_INPUT_CODE_FORMAT, - UNSUCCESSFUL_CONTEXT_COMPILATION_MESSAGE, ENCODED_ARAGUMENTS_NOT_FOUND_ERROR, CONSTRUCTOR_MODULE_IMPORTING_ERROR, BUILD_INFO_NOT_FOUND_ERROR, - COMPILATION_ERRORS, USING_COMPILER_PATH_ERROR, + TASK_VERIFY_RESOLVE_ARGUMENTS, + TASK_VERIFY_ZKSYNC_ETHERSCAN, + TASK_VERIFY_ZKSYNC_EXPLORER, + TASK_VERIFY_GET_VERIFICATION_SUBTASKS, } from './constants'; -import { encodeArguments, extractModule, normalizeCompilerVersions, retrieveContractBytecode } from './utils'; +import { extractModule, normalizeCompilerVersions, printVerificationErrors } from './utils'; import { Libraries } from './types'; import { ZkSyncVerifyPluginError } from './errors'; -import { Bytecode, extractMatchingContractInformation } from './solc/bytecode'; +import { extractMatchingContractInformation } from './solc/bytecode'; -import { ContractInformation } from './solc/types'; -import { - checkContractName, - getLibraries, - getMinimalResolvedFiles, - getSolidityStandardJsonInput, - inferContractArtifacts, -} from './plugin'; +import { checkContractName, getLibraries, inferContractArtifacts } from './plugin'; import { SolcMultiUserConfigExtractor, SolcSoloUserConfigExtractor, @@ -49,26 +36,19 @@ import { SolcUserConfigExtractor, } from './config-extractor'; -export async function verify( +export async function resolveArguments( args: { address: string; constructorArgs: string; contract: string; constructorArgsParams: any[]; libraries: string; + force: boolean; noCompile: boolean; }, hre: HardhatRuntimeEnvironment, - runSuper: RunSuperFunction, + _: RunSuperFunction, ) { - if (!hre.network.zksync) { - return await runSuper(args); - } - - if (hre.network.verifyURL === undefined) { - hre.network.verifyURL = TESTNET_VERIFY_URL; - } - if (args.address === undefined) { throw new ZkSyncVerifyPluginError(NO_VERIFIABLE_ADDRESS_ERROR); } @@ -80,13 +60,49 @@ export async function verify( const libraries: Libraries = await getLibraries(args.libraries); - await hre.run(TASK_VERIFY_VERIFY, { + return { address: args.address, constructorArguments, contract: args.contract, libraries, noCompile: args.noCompile, - }); + }; +} + +export async function verify( + args: { + address: string; + constructorArgs: string; + contract: string; + constructorArgsParams: any[]; + libraries: string; + noCompile: boolean; + }, + hre: HardhatRuntimeEnvironment, + runSuper: RunSuperFunction, +) { + if (!hre.network.zksync) { + return await runSuper(args); + } + + const resolvedAruments = await hre.run(TASK_VERIFY_RESOLVE_ARGUMENTS, args); + + const verificationSubtasks: VerificationSubtask[] = await hre.run(TASK_VERIFY_GET_VERIFICATION_SUBTASKS); + + const errors: Record = {}; + for (const { label, subtaskName } of verificationSubtasks) { + try { + await hre.run(subtaskName, resolvedAruments); + } catch (error) { + errors[label] = error as ZkSyncVerifyPluginError; + } + } + + const hasErrors = Object.keys(errors).length > 0; + if (hasErrors) { + printVerificationErrors(errors); + process.exit(1); + } } const extractors: SolcUserConfigExtractor[] = [ @@ -161,7 +177,6 @@ export async function getConstructorArguments( try { const constructorArguments = await extractModule(constructorArgsModulePath); - // Since our plugin supports both encoded and decoded constructor arguments, we need to check how are they passed if (!Array.isArray(constructorArguments) && !constructorArguments.startsWith('0x')) { throw new ZkSyncVerifyPluginError(ENCODED_ARAGUMENTS_NOT_FOUND_ERROR(constructorArgsModulePath)); } @@ -171,106 +186,81 @@ export async function getConstructorArguments( } } -export async function verifyContract( - { address, contract: contractFQN, constructorArguments, libraries, noCompile }: TaskArguments, - hre: HardhatRuntimeEnvironment, +export async function getVerificationSubtasks( + _: TaskArguments, + { config, network }: HardhatRuntimeEnvironment, runSuper: RunSuperFunction, -): Promise { - if (!hre.network.zksync) { - return await runSuper({ address, contractFQN, constructorArguments, libraries }); +): Promise { + if (!network.zksync) { + return await runSuper(); } - const { isAddress } = await import('@ethersproject/address'); - if (!isAddress(address)) { - throw new ZkSyncVerifyPluginError(`${address} is an invalid address.`); + const verificationSubtasks: VerificationSubtask[] = []; + let isEtherscanRunned = false; + if (config.etherscan.apiKey && config.etherscan.enabled) { + isEtherscanRunned = true; + verificationSubtasks.push({ + label: 'ZkSyncEtherscan', + subtaskName: TASK_VERIFY_ZKSYNC_ETHERSCAN, + }); } - const deployedBytecodeHex = await retrieveContractBytecode(address, hre); - const deployedBytecode = new Bytecode(deployedBytecodeHex); + if (network.config.enableVerifyURL) { + verificationSubtasks.push({ + label: 'ZkSyncBlockExplorer', + subtaskName: TASK_VERIFY_ZKSYNC_EXPLORER, + }); + + return verificationSubtasks; + } - if (!noCompile) { - await hre.run(TASK_COMPILE, { quiet: true }); + if (isEtherscanRunned) { + return verificationSubtasks; } - const compilerVersions: string[] = await hre.run(TASK_VERIFY_GET_COMPILER_VERSIONS); + console.warn( + chalk.yellow( + `[WARNING] Since Etherscan is disabled or the API key is missing, verification will default to the ZKSync block explorer.`, + ), + ); - const contractInformation: ContractInformation = await hre.run(TASK_VERIFY_GET_CONTRACT_INFORMATION, { - contractFQN, - deployedBytecode, - matchingCompilerVersions: compilerVersions, - libraries, + verificationSubtasks.push({ + label: 'ZkSyncBlockExplorer', + subtaskName: TASK_VERIFY_ZKSYNC_EXPLORER, }); - const optimizationUsed = contractInformation.compilerInput.settings.optimizer.enabled ?? false; - - const solcVersion = contractInformation.solcVersion; + return verificationSubtasks; +} - let deployArgumentsEncoded; - if (!Array.isArray(constructorArguments)) { - if (constructorArguments.startsWith('0x')) { - deployArgumentsEncoded = constructorArguments; - } else { - throw new ZkSyncVerifyPluginError(chalk.red(CONST_ARGS_ARRAY_ERROR)); - } - } else { - deployArgumentsEncoded = `0x${await encodeArguments( - contractInformation.contractOutput.abi, - constructorArguments, - )}`; +export async function verifyContract( + args: TaskArguments, + { config, network, run }: HardhatRuntimeEnvironment, + runSuper: RunSuperFunction, +) { + if (!network.zksync) { + return await runSuper(args); + } + let isEtherscanRunned = false; + if (config.etherscan.apiKey && config.etherscan.enabled) { + isEtherscanRunned = true; + await run(TASK_VERIFY_ZKSYNC_ETHERSCAN, args); } - const compilerPossibleVersions = await getSupportedCompilerVersions(hre.network.verifyURL); - const compilerVersion: string = contractInformation.solcVersion; - if (!compilerPossibleVersions.includes(compilerVersion)) { - throw new ZkSyncVerifyPluginError(COMPILER_VERSION_NOT_SUPPORTED); + if (network.config.enableVerifyURL) { + return await run(TASK_VERIFY_ZKSYNC_EXPLORER, args); } - const compilerZksolcVersion = `v${contractInformation.contractOutput.metadata.zk_version}`; - contractInformation.contractName = `${contractInformation.sourceName}:${contractInformation.contractName}`; + if (isEtherscanRunned) { + return; + } - const request = { - contractAddress: address, - sourceCode: getSolidityStandardJsonInput( - hre, - await getMinimalResolvedFiles(hre, contractInformation.sourceName), - contractInformation.compilerInput, + console.warn( + chalk.yellow( + `[WARNING] Since Etherscan is disabled or the API key is missing, verification will default to the ZKSync block explorer.`, ), - codeFormat: JSON_INPUT_CODE_FORMAT, - contractName: contractInformation.contractName, - compilerSolcVersion: solcVersion, - compilerZksolcVersion, - constructorArguments: deployArgumentsEncoded, - optimizationUsed, - }; - - const response = await verifyContractRequest(request, hre.network.verifyURL); - const verificationId = parseInt(response.message, 10); - - console.info(chalk.cyan(`Your verification ID is: ${verificationId}`)); - - try { - await hre.run(TASK_CHECK_VERIFICATION_STATUS, { verificationId }); - } catch (error: any) { - // The first verirication attempt with 'minimal' source code was unnsuccessful. - // Now try with the full source code from the compilation context. - if ( - error.message !== NO_MATCHING_CONTRACT && - COMPILATION_ERRORS.filter((compilationError) => compilationError.pattern.test(error.message)).length === 0 - ) { - throw error; - } - console.info(chalk.red(UNSUCCESSFUL_CONTEXT_COMPILATION_MESSAGE)); - - request.sourceCode.sources = contractInformation.compilerInput.sources; - - const fallbackResponse = await verifyContractRequest(request, hre.network.verifyURL); - const fallbackVerificationId = parseInt(fallbackResponse.message, 10); - - console.info(chalk.cyan(`Your verification ID is: ${fallbackVerificationId}`)); - await hre.run(TASK_CHECK_VERIFICATION_STATUS, { verificationId: fallbackVerificationId }); - } + ); - return verificationId; + return await run(TASK_VERIFY_ZKSYNC_EXPLORER, args); } export async function getContractInfo( diff --git a/packages/hardhat-zksync-verify/src/type-extensions.ts b/packages/hardhat-zksync-verify/src/type-extensions.ts index aa7243b75..6785a0ce8 100644 --- a/packages/hardhat-zksync-verify/src/type-extensions.ts +++ b/packages/hardhat-zksync-verify/src/type-extensions.ts @@ -10,20 +10,30 @@ declare module 'hardhat/types/artifacts' { declare module 'hardhat/types/config' { interface HttpNetworkUserConfig { verifyURL?: string; + browserVerifyURL?: string; + enableVerifyURL?: boolean; } interface HardhatNetworkUserConfig { verifyURL?: string; + browserVerifyURL?: string; + enableVerifyURL?: boolean; } interface HttpNetworkConfig { verifyURL?: string; + browserVerifyURL?: string; + enableVerifyURL?: boolean; } interface HardhatNetworkConfig { verifyURL?: string; + browserVerifyURL?: string; + enableVerifyURL?: boolean; } interface HardhatConfig { verifyURL?: string; + browserVerifyURL?: string; + enableVerifyURL?: boolean; } interface SolcConfig { @@ -38,5 +48,7 @@ declare module 'hardhat/types/config' { declare module 'hardhat/types/runtime' { interface Network { verifyURL: string; + browserVerifyURL: string; + enableVerifyURL: boolean; } } diff --git a/packages/hardhat-zksync-verify/src/utils.ts b/packages/hardhat-zksync-verify/src/utils.ts index 99b7c0083..c846f20ac 100644 --- a/packages/hardhat-zksync-verify/src/utils.ts +++ b/packages/hardhat-zksync-verify/src/utils.ts @@ -1,10 +1,8 @@ import axios from 'axios'; import chalk from 'chalk'; import { HardhatRuntimeEnvironment, SolcUserConfig } from 'hardhat/types'; -import { VerificationStatusResponse } from './zksync-block-explorer/verification-status-response'; -import { checkVerificationStatusService } from './zksync-block-explorer/service'; import { ZkSyncVerifyPluginError } from './errors'; -import { PENDING_CONTRACT_INFORMATION_MESSAGE, WRONG_CONSTRUCTOR_ARGUMENTS } from './constants'; +import { WRONG_CONSTRUCTOR_ARGUMENTS } from './constants'; import { CompilerSolcUserConfigNormalizer, OverrideCompilerSolcUserConfigNormalizer, @@ -52,31 +50,6 @@ export function nextAttemptDelay(currentAttempt: number, baseDelay: number, base return baseDelay * 2 ** (currentAttempt - baseNumberOfAttempts); } -export async function executeVeificationWithRetry( - requestId: number, - verifyURL: string, - maxRetries = 11, - baseRetries = 5, - baseDelayInMs = 2000, -): Promise { - let retries = 0; - - while (true) { - const response = await checkVerificationStatusService(requestId, verifyURL); - if (response.isVerificationSuccess() || response.isVerificationFailure()) { - return response; - } - retries += 1; - if (retries > maxRetries) { - console.info(chalk.cyan(PENDING_CONTRACT_INFORMATION_MESSAGE(requestId))); - return; - } - - const delayInMs = nextAttemptDelay(retries, baseDelayInMs, baseRetries); - await delay(delayInMs); - } -} - export async function retrieveContractBytecode(address: string, hre: HardhatRuntimeEnvironment): Promise { const provider = hre.network.provider; const bytecodeString = (await provider.send('eth_getCode', [address, 'latest'])) as string; @@ -91,25 +64,6 @@ export async function retrieveContractBytecode(address: string, hre: HardhatRunt return deployedBytecode; } -export function removeMultipleSubstringOccurrences(inputString: string, stringToRemove: string): string { - const lines = inputString.split('\n'); - let output = ''; - let firstIdentifierFound = false; - - for (const line of lines) { - if (line.trim().includes(stringToRemove)) { - if (!firstIdentifierFound) { - output += `${line}\n`; - firstIdentifierFound = true; - } - } else { - output += `${line}\n`; - } - } - - return output.trim(); -} - export function parseWrongConstructorArgumentsError(string: string): string { // extract the values of the "types" and "values" keys from the string const data = JSON.parse(string.split('count=')[1].split(', value=')[0]); @@ -153,3 +107,13 @@ export function extractQueryParams(url: string): [string, { [k: string]: string return [newURL, params]; } + +export function printVerificationErrors(errors: Record) { + let errorMessage = 'hardhat-zksync-verify found one or more errors during the verification process:\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-zksync-verify/src/zksync-block-explorer/service.ts b/packages/hardhat-zksync-verify/src/zksync-block-explorer/service.ts deleted file mode 100644 index 3debe8655..000000000 --- a/packages/hardhat-zksync-verify/src/zksync-block-explorer/service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import axios from 'axios'; - -import { extractQueryParams, handleAxiosError } from '../utils'; -import { ZkSyncVerifyPluginError } from '../errors'; -import { VerificationStatusResponse } from './verification-status-response'; -import { ZkSyncBlockExplorerVerifyRequest } from './verify-contract-request'; - -export class ZkSyncBlockExplorerResponse { - public readonly status: number; - public readonly message: string; - - constructor(response: any) { - this.status = parseInt(response.status, 10); - this.message = response.data; - } - - public isOk() { - return this.status === 200; - } -} - -export async function checkVerificationStatusService( - requestId: number, - verifyURL: string, -): Promise { - let verificationStatusResponse; - let params; - - try { - [verifyURL, params] = extractQueryParams(verifyURL); - - const data = await axios.get(`${verifyURL}/${requestId}`, { params }); - verificationStatusResponse = new VerificationStatusResponse(data); - - return verificationStatusResponse; - } catch (error) { - handleAxiosError(error); - } -} - -export async function verifyContractRequest( - req: ZkSyncBlockExplorerVerifyRequest, - verifyURL: string, -): Promise { - let data; - let params; - try { - [verifyURL, params] = extractQueryParams(verifyURL); - - data = await axios.post(verifyURL, req, { headers: { 'Content-Type': 'application/json' }, params }); - - const zkSyncBlockExplorerResponse = new ZkSyncBlockExplorerResponse(data); - - if (!zkSyncBlockExplorerResponse.isOk()) { - throw new ZkSyncVerifyPluginError(zkSyncBlockExplorerResponse.message); - } - - return zkSyncBlockExplorerResponse; - } catch (error) { - handleAxiosError(error); - } -} - -export async function getSupportedCompilerVersions(verifyURL: string | undefined): Promise { - let params; - try { - if (verifyURL !== undefined) { - [verifyURL, params] = extractQueryParams(verifyURL); - } - - const response = await axios.get(`${verifyURL}/solc_versions`, { params }); - return response.data; - } catch (error) { - handleAxiosError(error); - } -} diff --git a/packages/hardhat-zksync-verify/src/zksync-block-explorer/verify-contract-request.ts b/packages/hardhat-zksync-verify/src/zksync-block-explorer/verify-contract-request.ts deleted file mode 100644 index 11c6420df..000000000 --- a/packages/hardhat-zksync-verify/src/zksync-block-explorer/verify-contract-request.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CompilerInput } from 'hardhat/types'; - -export interface ZkSyncBlockExplorerVerifyRequest { - contractAddress: string; - contractName: string; - sourceCode: string | CompilerInput; - codeFormat: string; - compilerSolcVersion: string; - compilerZksolcVersion: string; - optimizationUsed: boolean; - constructorArguments: string; -} diff --git a/packages/hardhat-zksync-verify/test/tests/plugin.test.ts b/packages/hardhat-zksync-verify/test/tests/plugin.test.ts index 8a08f9420..ddcb7082a 100644 --- a/packages/hardhat-zksync-verify/test/tests/plugin.test.ts +++ b/packages/hardhat-zksync-verify/test/tests/plugin.test.ts @@ -5,7 +5,6 @@ import { fail } from 'assert'; import path from 'path'; import { checkContractName, - checkVerificationStatus, flattenContractFile, getLibraries, getSolidityStandardJsonInput, @@ -16,7 +15,6 @@ import * as bytecodes from '../../src/solc/bytecode'; import { useEnvironment } from '../helpers'; import { CONTRACT_NAME_NOT_FOUND, NO_MATCHING_CONTRACT } from '../../src/constants'; import { ContractInformation } from '../../src/solc/types'; -import { VerificationStatusResponse } from '../../src/zksync-block-explorer/verification-status-response'; describe('Plugin', () => { const artifacts: Artifacts = { @@ -406,55 +404,4 @@ Instead, this name was received: ${contractFQN}`); expect(libraries).to.deep.equal({}); }); }); - - describe('checkVerificationStatus', async function () { - useEnvironment('localGreeter'); - - it('should throw ZkSyncVerifyPluginError when backend verification error exists', async function () { - sinon.stub(VerificationStatusResponse.prototype, 'errorExists').returns(true); - - const args = { - verificationId: 123, - }; - - try { - await checkVerificationStatus(args, this.env); - fail('Expected an error to be thrown'); - } catch (error: any) { - expect(error.message).to.equal( - 'Backend verification error: Deployed bytecode is not equal to generated one from given source', - ); - } - }); - - it.skip('should log a success message and return true when verification is successful', async function () { - sinon.stub(VerificationStatusResponse.prototype, 'errorExists').returns(false); - - const args = { - verificationId: 123, - }; - - const consoleInfoSpy = sinon.spy(console, 'info'); - const result = await checkVerificationStatus(args, this.env); - - sinon.assert.calledWith( - consoleInfoSpy, - '\u001b[32mContract successfully verified on ZKsync block explorer!\u001b[39m', - ); - expect(result).to.equal(true); - }); - }); - - describe('normalizeBytecode', () => { - it('should not modify bytecode when it does not start with the expected placeholder', async () => { - const mockBytecode = 'abcdef1234567890'; - const symbols = { - object: '73', - }; - - const result = await bytecodes.normalizeBytecode(mockBytecode, symbols as any); - - expect(result.normalizedBytecode).to.equal(mockBytecode); - }); - }); }); diff --git a/packages/hardhat-zksync-verify/test/tests/solc/bytecode.test.ts b/packages/hardhat-zksync-verify/test/tests/solc/bytecode.test.ts index 1b8fe3bce..95cd67ce3 100644 --- a/packages/hardhat-zksync-verify/test/tests/solc/bytecode.test.ts +++ b/packages/hardhat-zksync-verify/test/tests/solc/bytecode.test.ts @@ -144,6 +144,7 @@ describe('extractMatchingContractInformation', () => { }, }, solcVersion: 'solcVersion', + solcLongVersion: 'solcLongVersion', }; const deployedBytecode: Bytecode = new Bytecode('deployedBytecode'); @@ -159,6 +160,7 @@ describe('extractMatchingContractInformation', () => { compilerInput: 'compilerInput', contractOutput: buildInfo.output.contracts[sourceName][contractName], solcVersion: 'solcVersion', + solcLongVersion: 'solcLongVersion', sourceName, contractName, }); diff --git a/packages/hardhat-zksync-verify/test/tests/task-actions.test.ts b/packages/hardhat-zksync-verify/test/tests/task-actions.test.ts index 2d99ee024..6338a5f67 100644 --- a/packages/hardhat-zksync-verify/test/tests/task-actions.test.ts +++ b/packages/hardhat-zksync-verify/test/tests/task-actions.test.ts @@ -2,520 +2,14 @@ import { assert, expect } from 'chai'; import sinon from 'sinon'; import { fail } from 'assert'; -import { TASK_COMPILE, TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH } from 'hardhat/builtin-tasks/task-names'; import { ZKSOLC_COMPILER_PATH_VERSION } from '@matterlabs/hardhat-zksync-solc/dist/src/constants'; -import { - getCompilerVersions, - getConstructorArguments, - getContractInfo, - verify, - verifyContract, -} from '../../src/task-actions'; -import { - NO_MATCHING_CONTRACT, - NO_VERIFIABLE_ADDRESS_ERROR, - TASK_CHECK_VERIFICATION_STATUS, - TASK_VERIFY_GET_COMPILER_VERSIONS, - TASK_VERIFY_GET_CONSTRUCTOR_ARGUMENTS, - TASK_VERIFY_GET_CONTRACT_INFORMATION, - TASK_VERIFY_VERIFY, - USING_COMPILER_PATH_ERROR, -} from '../../src/constants'; +import { getCompilerVersions, getConstructorArguments, getContractInfo, verify } from '../../src/task-actions'; +import { NO_MATCHING_CONTRACT, TASK_VERIFY_ZKSYNC_ETHERSCAN, USING_COMPILER_PATH_ERROR } from '../../src/constants'; import * as utils from '../../src/utils'; -import * as metadata from '../../src/solc/metadata'; -import * as service from '../../src/zksync-block-explorer/service'; + import * as plugin from '../../src/plugin'; import * as bytecode from '../../src/solc/bytecode'; -import { ZkSyncVerifyPluginError } from '../../src/errors'; - -describe('verifyContract', async function () { - beforeEach(() => { - sinon.stub(utils, 'retrieveContractBytecode').resolves('0x1234567890'); - sinon.stub(metadata, 'inferSolcVersion').resolves('0.8.0'); - sinon.stub(service, 'getSupportedCompilerVersions').resolves(['0.8.0']); - sinon.stub(plugin, 'getSolidityStandardJsonInput').resolves({ - language: 'Solidity', - sources: { - 'contracts/Contract.sol': { - content: 'contract Contract {}', - }, - }, - }); - }); - - const args = { - address: '0x1234567890123456789012345678901234567890', - constructorArguments: [], - contract: 'contract', - libraries: 'libraries', - noCompile: false, - }; - - afterEach(() => { - sinon.restore(); - }); - it('should call runSuper if zksync is false', async function () { - const runSuperStub = sinon.stub().resolves(0); - const hre = { - network: { - zksync: false, - }, - run: sinon.stub(), - }; - - await verifyContract({}, hre as any, runSuperStub as any); - expect(runSuperStub.calledOnce).to.equal(true); - }); - - it('should throw an error if the address is invalid', async function () { - const runSuperStub = sinon.stub().resolves(0); - const hre = { - network: { - zksync: true, - }, - run: sinon.stub(), - }; - - try { - await verifyContract({ address: 'invalid_address' }, hre as any, runSuperStub as any); - fail('Expected an error to be thrown'); - } catch (error: any) { - expect(error.message).to.equal('invalid_address is an invalid address.'); - } - }); - - it('should call runSuper if zksync is false', async function () { - const runSuperStub = sinon.stub().resolves(0); - const hre = { - network: { - zksync: false, - }, - run: sinon.stub(), - }; - - await verifyContract({}, hre as any, runSuperStub as any); - expect(runSuperStub.calledOnce).to.equal(true); - }); - - it('should throw an error if the address is invalid', async function () { - const runSuperStub = sinon.stub().resolves(0); - const hre = { - network: { - zksync: true, - }, - run: sinon.stub(), - }; - - try { - await verifyContract({ address: 'invalid_address' }, hre as any, runSuperStub as any); - fail('Expected an error to be thrown'); - } catch (error: any) { - expect(error.message).to.equal('invalid_address is an invalid address.'); - } - }); - - it('should call run with the correct arguments if zksync is true and address is valid', async function () { - sinon.stub(service, 'verifyContractRequest').resolves({ - status: 200, - message: '1', - isOk: () => true, - }); - - const runSuperStub = sinon.stub().resolves(0); - const hre = { - config: { - paths: { - sources: 'contracts', - root: 'root', - }, - zksolc: { - settings: { - contractsToCompile: [], - }, - }, - }, - network: { - config: { - url: 'http://localhost:3000', - }, - zksync: true, - verifyURL: 'http://localhost:3000/verify', - }, - run: sinon - .stub() - .onThirdCall() - .resolves({ - contractName: 'Contract', - sourceName: 'contracts/Contract.sol', - compilerInput: { - language: 'Solidity', - sources: { - 'contracts/Contract.sol': { - content: 'contract Contract {}', - }, - }, - settings: { - optimizer: { - enabled: true, - }, - outputSelection: { - '*': { - '*': ['evm'], - }, - }, - }, - }, - contractOutput: { - abi: [], - metadata: { - zk_version: '0.1.0', - solc_metadata: '0x1234567890', - optimizer_settings: '0x1234567890', - }, - evm: { - bytecode: { - linkReferences: {}, - object: '0x1234567890', - opcodes: '0x1234567890', - sourceMap: '0x1234567890', - }, - deployedBytecode: { - linkReferences: {}, - object: '0x1234567890', - opcodes: '0x1234567890', - sourceMap: '0x1234567890', - }, - methodIdentifiers: {}, - }, - }, - solcVersion: '0.8.0', - }) - .onCall(3) - .resolves({ - getResolvedFiles: sinon.stub().resolves(), - }), - }; - - await verifyContract(args, hre as any, runSuperStub as any); - expect(runSuperStub.calledOnce).to.equal(false); - expect(hre.run.firstCall.args[0]).to.equal(TASK_COMPILE); - expect(hre.run.secondCall.args[0]).to.equal(TASK_VERIFY_GET_COMPILER_VERSIONS); - expect(hre.run.thirdCall.args[0]).to.equal(TASK_VERIFY_GET_CONTRACT_INFORMATION); - expect(hre.run.getCall(3).args[0]).to.equal(TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH); - expect(hre.run.getCall(4).args[0]).to.equal(TASK_CHECK_VERIFICATION_STATUS); - }); - - it('should call fallback request sent if there is compilation error', async function () { - const errorMessage = - 'Backend verification error: Compilation errorDeclarationError: Undeclared identifier.--> contracts-preprocessed/libraries/EfficientCall.sol:134:32:|134 | address callAddr = RAW_FAR_CALL_BY_REF_CALL_ADDRESS;|'; - - sinon - .stub(service, 'verifyContractRequest') - .onFirstCall() - .resolves({ status: 503, message: '1', isOk: () => false }) - .onSecondCall() - .resolves({ - status: 200, - message: '2', - isOk: () => true, - }); - - const runSuperStub = sinon.stub().resolves(0); - const hre = { - config: { - paths: { - sources: 'contracts', - root: 'root', - }, - zksolc: { - settings: { - contractsToCompile: [], - }, - }, - }, - network: { - config: { - url: 'http://localhost:3000', - }, - zksync: true, - verifyURL: 'http://localhost:3000/verify', - }, - run: sinon - .stub() - .onThirdCall() - .resolves({ - contractName: 'Contract', - sourceName: 'contracts/Contract.sol', - compilerInput: { - language: 'Solidity', - sources: { - 'contracts/Contract.sol': { - content: 'contract Contract {}', - }, - }, - settings: { - optimizer: { - enabled: true, - }, - outputSelection: { - '*': { - '*': ['evm'], - }, - }, - }, - }, - contractOutput: { - abi: [], - metadata: { - zk_version: '0.1.0', - solc_metadata: '0x1234567890', - optimizer_settings: '0x1234567890', - }, - evm: { - bytecode: { - linkReferences: {}, - object: '0x1234567890', - opcodes: '0x1234567890', - sourceMap: '0x1234567890', - }, - deployedBytecode: { - linkReferences: {}, - object: '0x1234567890', - opcodes: '0x1234567890', - sourceMap: '0x1234567890', - }, - methodIdentifiers: {}, - }, - }, - solcVersion: '0.8.0', - }) - .onCall(3) - .resolves({ - getResolvedFiles: sinon.stub().resolves(), - }) - .onCall(4) - .throwsException(new ZkSyncVerifyPluginError(errorMessage)) - .onCall(5) - .resolves(true), - }; - - await verifyContract(args, hre as any, runSuperStub as any); - expect(runSuperStub.calledOnce).to.equal(false); - expect(hre.run.firstCall.args[0]).to.equal(TASK_COMPILE); - expect(hre.run.secondCall.args[0]).to.equal(TASK_VERIFY_GET_COMPILER_VERSIONS); - expect(hre.run.thirdCall.args[0]).to.equal(TASK_VERIFY_GET_CONTRACT_INFORMATION); - expect(hre.run.getCall(3).args[0]).to.equal(TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH); - expect(hre.run.getCall(4).args[0]).to.equal(TASK_CHECK_VERIFICATION_STATUS); - expect(hre.run.getCall(5).args[0]).to.equal(TASK_CHECK_VERIFICATION_STATUS); - }); - - it('should call fallback request sent if there is missing source file', async function () { - const errorMessage = 'Backend verification error: There is no contracts/Contract.sol source file'; - - sinon - .stub(service, 'verifyContractRequest') - .onFirstCall() - .resolves({ status: 503, message: '1', isOk: () => false }) - .onSecondCall() - .resolves({ - status: 200, - message: '2', - isOk: () => true, - }); - - const runSuperStub = sinon.stub().resolves(0); - const hre = { - config: { - paths: { - sources: 'contracts', - root: 'root', - }, - zksolc: { - settings: { - contractsToCompile: [], - }, - }, - }, - network: { - config: { - url: 'http://localhost:3000', - }, - zksync: true, - verifyURL: 'http://localhost:3000/verify', - }, - run: sinon - .stub() - .onThirdCall() - .resolves({ - contractName: 'Contract', - sourceName: 'contracts/Contract.sol', - compilerInput: { - language: 'Solidity', - sources: { - 'contracts/Contract.sol': { - content: 'contract Contract {}', - }, - }, - settings: { - optimizer: { - enabled: true, - }, - outputSelection: { - '*': { - '*': ['evm'], - }, - }, - }, - }, - contractOutput: { - abi: [], - metadata: { - zk_version: '0.1.0', - solc_metadata: '0x1234567890', - optimizer_settings: '0x1234567890', - }, - evm: { - bytecode: { - linkReferences: {}, - object: '0x1234567890', - opcodes: '0x1234567890', - sourceMap: '0x1234567890', - }, - deployedBytecode: { - linkReferences: {}, - object: '0x1234567890', - opcodes: '0x1234567890', - sourceMap: '0x1234567890', - }, - methodIdentifiers: {}, - }, - }, - solcVersion: '0.8.0', - }) - .onCall(3) - .resolves({ - getResolvedFiles: sinon.stub().resolves(), - }) - .onCall(4) - .throwsException(new ZkSyncVerifyPluginError(errorMessage)) - .onCall(5) - .resolves(true), - }; - - await verifyContract(args, hre as any, runSuperStub as any); - expect(runSuperStub.calledOnce).to.equal(false); - expect(hre.run.firstCall.args[0]).to.equal(TASK_COMPILE); - expect(hre.run.secondCall.args[0]).to.equal(TASK_VERIFY_GET_COMPILER_VERSIONS); - expect(hre.run.thirdCall.args[0]).to.equal(TASK_VERIFY_GET_CONTRACT_INFORMATION); - expect(hre.run.getCall(3).args[0]).to.equal(TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH); - expect(hre.run.getCall(4).args[0]).to.equal(TASK_CHECK_VERIFICATION_STATUS); - expect(hre.run.getCall(5).args[0]).to.equal(TASK_CHECK_VERIFICATION_STATUS); - }); - - it('should call fallback request sent if there is missing contract file', async function () { - const errorMessage = - 'Backend verification error: Contract with contracts/Contract.sol name is missing in sources'; - sinon - .stub(service, 'verifyContractRequest') - .onFirstCall() - .resolves({ status: 503, message: '1', isOk: () => false }) - .onSecondCall() - .resolves({ - status: 200, - message: '2', - isOk: () => true, - }); - - const runSuperStub = sinon.stub().resolves(0); - const hre = { - config: { - paths: { - sources: 'contracts', - root: 'root', - }, - zksolc: { - settings: { - contractsToCompile: [], - }, - }, - }, - network: { - config: { - url: 'http://localhost:3000', - }, - zksync: true, - verifyURL: 'http://localhost:3000/verify', - }, - run: sinon - .stub() - .onThirdCall() - .resolves({ - contractName: 'Contract', - sourceName: 'contracts/Contract.sol', - compilerInput: { - language: 'Solidity', - sources: { - 'contracts/Contract.sol': { - content: 'contract Contract {}', - }, - }, - settings: { - optimizer: { - enabled: true, - }, - outputSelection: { - '*': { - '*': ['evm'], - }, - }, - }, - }, - contractOutput: { - abi: [], - metadata: { - zk_version: '0.1.0', - solc_metadata: '0x1234567890', - optimizer_settings: '0x1234567890', - }, - evm: { - bytecode: { - linkReferences: {}, - object: '0x1234567890', - opcodes: '0x1234567890', - sourceMap: '0x1234567890', - }, - deployedBytecode: { - linkReferences: {}, - object: '0x1234567890', - opcodes: '0x1234567890', - sourceMap: '0x1234567890', - }, - methodIdentifiers: {}, - }, - }, - solcVersion: '0.8.0', - }) - .onCall(3) - .resolves({ - getResolvedFiles: sinon.stub().resolves(), - }) - .onCall(4) - .throwsException(new ZkSyncVerifyPluginError(errorMessage)) - .onCall(5) - .resolves(true), - }; - - await verifyContract(args, hre as any, runSuperStub as any); - expect(runSuperStub.calledOnce).to.equal(false); - expect(hre.run.firstCall.args[0]).to.equal(TASK_COMPILE); - expect(hre.run.secondCall.args[0]).to.equal(TASK_VERIFY_GET_COMPILER_VERSIONS); - expect(hre.run.thirdCall.args[0]).to.equal(TASK_VERIFY_GET_CONTRACT_INFORMATION); - expect(hre.run.getCall(3).args[0]).to.equal(TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH); - expect(hre.run.getCall(4).args[0]).to.equal(TASK_CHECK_VERIFICATION_STATUS); - expect(hre.run.getCall(5).args[0]).to.equal(TASK_CHECK_VERIFICATION_STATUS); - }); -}); describe('getCompilerVersions', async function () { it('should call runSuper if zksync is false', async function () { const runSuperStub = sinon.stub().resolves([]); @@ -643,36 +137,6 @@ describe('verify', async function () { expect(runSuperStub.calledOnce).to.equal(true); }); - it('should throw an error if address is undefined', async function () { - sinon.stub(plugin, 'getLibraries').resolves({}); - const runSuperStub = sinon.stub().resolves(0); - const hre = { - network: { - zksync: true, - verifyURL: undefined, - }, - run: sinon.stub().resolves({}), - }; - - try { - await verify( - { - address: undefined as any, - constructorArgs: '', - contract: '', - constructorArgsParams: [], - libraries: '', - noCompile: false, - }, - hre as any, - runSuperStub as any, - ); - fail('Expected an error to be thrown'); - } catch (error: any) { - expect(error.message).to.equal(NO_VERIFIABLE_ADDRESS_ERROR); - } - }); - it('should call run with the correct arguments', async function () { sinon.stub(plugin, 'getLibraries').resolves({}); const runSuperStub = sinon.stub().resolves(0); @@ -681,7 +145,17 @@ describe('verify', async function () { zksync: true, verifyURL: 'http://localhost:3000/verify', }, - run: sinon.stub().resolves({}), + run: sinon + .stub() + .onFirstCall() + .resolves({}) + .onSecondCall() + .resolves([ + { + label: 'ZkSyncEtherscan', + subtaskName: TASK_VERIFY_ZKSYNC_ETHERSCAN, + }, + ]), }; await verify( @@ -698,15 +172,9 @@ describe('verify', async function () { ); expect(runSuperStub.calledOnce).to.equal(false); - expect(hre.run.firstCall.args[0]).to.equal(TASK_VERIFY_GET_CONSTRUCTOR_ARGUMENTS); - expect(hre.run.secondCall.args[0]).to.equal(TASK_VERIFY_VERIFY); - expect(hre.run.secondCall.args[1]).to.deep.equal({ - address: '0x1234567890', - constructorArguments: {}, - contract: 'Contract', - libraries: {}, - noCompile: false, - }); + expect(hre.run.firstCall.args[0]).to.equal('verify:resolve-arguments'); + expect(hre.run.secondCall.args[0]).to.equal('verify:get-verification-subtasks'); + expect(hre.run.thirdCall.args[0]).to.equal('verify:zksync-etherscan'); }); }); describe('getConstructorArguments', async function () { @@ -1013,6 +481,7 @@ describe('getContractInfo', async function () { }, }, solcVersion: '0.8.0', + solcLongVersion: '0.8.0-commit-7222f', }; const runSuperStub = sinon.stub().resolves({}); diff --git a/packages/hardhat-zksync-verify/test/tests/tests.ts b/packages/hardhat-zksync-verify/test/tests/tests.ts deleted file mode 100644 index ace4794e4..000000000 --- a/packages/hardhat-zksync-verify/test/tests/tests.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { assert } from 'chai'; -import { useEnvironment } from '../helpers'; - -describe('verify plugin', async function () { - const testnetVerifyURL = 'https://explorer.sepolia.era.zksync.dev/contract_verification'; - - describe('Testnet verifyURL extraction from config', async function () { - useEnvironment('localGreeter', 'testnet'); - - it('Reads verifyURL form network config for existing network ', async function () { - assert.equal(this.env.network.verifyURL, testnetVerifyURL); - }); - }); - - describe('Unknown verifyURL in config', async function () { - useEnvironment('localGreeter', 'customNetwork'); - - it('Checks impoting deafault verifyURL when it does not exist in the config ', async function () { - assert.equal(this.env.network.verifyURL, testnetVerifyURL); - }); - }); -}); diff --git a/packages/hardhat-zksync-verify/test/tests/utils.test.ts b/packages/hardhat-zksync-verify/test/tests/utils.test.ts index 2665d447f..5de7d29f7 100644 --- a/packages/hardhat-zksync-verify/test/tests/utils.test.ts +++ b/packages/hardhat-zksync-verify/test/tests/utils.test.ts @@ -4,121 +4,12 @@ import axion from 'axios'; import { delay, encodeArguments, - executeVeificationWithRetry, extractQueryParams, getZkVmNormalizedVersion, handleAxiosError, parseWrongConstructorArgumentsError, - removeMultipleSubstringOccurrences, retrieveContractBytecode, } from '../../src/utils'; -import * as service from '../../src/zksync-block-explorer/service'; -import { SolcMultiUserConfigExtractor } from '../../src/config-extractor'; - -describe('executeVeificationWithRetry', () => { - let checkVerificationStatusServiceStub: sinon.SinonStub; - - beforeEach(() => { - sinon.restore(); - }); - - it('should return the verification status response when verification is successful', async () => { - const requestId = 123; - const verifyURL = 'https://example.com/verify'; - const response = { - isVerificationSuccess: sinon.stub().returns(true), - isVerificationFailure: sinon.stub().returns(false), - }; - - checkVerificationStatusServiceStub = sinon - .stub(service, 'checkVerificationStatusService') - .resolves(response as any); - - const result = await executeVeificationWithRetry(requestId, verifyURL); - - expect(result).to.equal(response); - expect(checkVerificationStatusServiceStub.calledOnceWith(requestId, verifyURL)).to.equal(true); - }); - - it('should return the verification status response when verification is failed', async () => { - const requestId = 123; - const verifyURL = 'https://example.com/verify'; - const response = { - isVerificationSuccess: sinon.stub().returns(false), - isVerificationFailure: sinon.stub().returns(true), - }; - - checkVerificationStatusServiceStub = sinon - .stub(service, 'checkVerificationStatusService') - .resolves(response as any); - - const result = await executeVeificationWithRetry(requestId, verifyURL); - - expect(result).to.equal(response); - expect(checkVerificationStatusServiceStub.calledOnceWith(requestId, verifyURL)).to.equal(true); - }); - - it('should handle undefined overrides gracefully', () => { - const solidityConfig = { - compilers: [ - { - version: '0.8.17', - eraVersion: 'latest', - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - outputSelection: {}, - metadata: {}, - }, - }, - ], - overrides: undefined, - }; - const extractor = new SolcMultiUserConfigExtractor(); - - const result = extractor.extract(solidityConfig); - - expect(result.compilers).to.deep.equal([ - { - version: '0.8.17', - eraVersion: 'latest', - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - outputSelection: {}, - metadata: {}, - }, - }, - ]); - expect(result!.overides!.size).to.equal(0); - }); - - it('should return undefined when max retries exceeded', async () => { - const requestId = 123; - const verifyURL = 'https://example.com/verify'; - const maxRetries = 2; - const delayInMs = 100; - - const response = { - isVerificationSuccess: sinon.stub().returns(false), - isVerificationFailure: sinon.stub().returns(false), - }; - - checkVerificationStatusServiceStub = sinon - .stub(service, 'checkVerificationStatusService') - .resolves(response as any); - - const result = await executeVeificationWithRetry(requestId, verifyURL, maxRetries, delayInMs); - - expect(result).to.equal(undefined); - expect(checkVerificationStatusServiceStub.callCount).to.equal(maxRetries + 1); - expect(checkVerificationStatusServiceStub.calledWith(requestId, verifyURL)).to.equal(true); - }); -}); describe('handleAxiosError', () => { beforeEach(() => { @@ -244,48 +135,6 @@ describe('retrieveContractBytecode', () => { }); }); -describe('removeMultipleSubstringOccurrences', () => { - it('should remove all occurrences of the specified substring', () => { - const inputString = 'Hello, World!\n Hello, World! \nHello, World!'; - const stringToRemove = 'Hello, World!'; - const expectedOutput = 'Hello, World!'; - - const result = removeMultipleSubstringOccurrences(inputString, stringToRemove); - - expect(result).to.equal(expectedOutput); - }); - - it('should handle empty input string', () => { - const inputString = ''; - const stringToRemove = 'Hello, '; - const expectedOutput = ''; - - const result = removeMultipleSubstringOccurrences(inputString, stringToRemove); - - expect(result).to.equal(expectedOutput); - }); - - it('should handle empty string to remove', () => { - const inputString = 'Hello, World!'; - const stringToRemove = ''; - const expectedOutput = 'Hello, World!'; - - const result = removeMultipleSubstringOccurrences(inputString, stringToRemove); - - expect(result).to.equal(expectedOutput); - }); - - it('should handle no occurrences of the substring', () => { - const inputString = 'Hello, World!'; - const stringToRemove = 'Foo, '; - const expectedOutput = 'Hello, World!'; - - const result = removeMultipleSubstringOccurrences(inputString, stringToRemove); - - expect(result).to.equal(expectedOutput); - }); -}); - describe('parseWrongConstructorArgumentsError', () => { it('should return the correct error message', () => { const inputString = 'Error: count=2, value=5, types=[string, uint256]'; diff --git a/packages/hardhat-zksync-verify/test/tests/zksync-block-explorer/errors.test.ts b/packages/hardhat-zksync-verify/test/tests/zksync-block-explorer/errors.test.ts new file mode 100644 index 000000000..9984afaa0 --- /dev/null +++ b/packages/hardhat-zksync-verify/test/tests/zksync-block-explorer/errors.test.ts @@ -0,0 +1,10 @@ +import { expect } from 'chai'; +import { ZksyncMissingApiKeyError } from '../../../src/explorers/zksync-block-explorer/errors'; + +describe('ZksyncMissingApiKeyError', () => { + it('should create an instance with the correct message', () => { + const network = 'testnet'; + const error = new ZksyncMissingApiKeyError(network); + expect(error.message).to.contains(`You are trying to verify a contract in '${network}'`); + }); +}); diff --git a/packages/hardhat-zksync-verify/test/tests/zksync-block-explorer/service.test.ts b/packages/hardhat-zksync-verify/test/tests/zksync-block-explorer/service.test.ts index 3e004f73d..31fe945ce 100644 --- a/packages/hardhat-zksync-verify/test/tests/zksync-block-explorer/service.test.ts +++ b/packages/hardhat-zksync-verify/test/tests/zksync-block-explorer/service.test.ts @@ -1,25 +1,178 @@ import { expect } from 'chai'; -import sinon from 'sinon'; +import sinon, { SinonSandbox } from 'sinon'; import axios from 'axios'; -import { - checkVerificationStatusService, - verifyContractRequest, - getSupportedCompilerVersions, - ZkSyncBlockExplorerResponse, -} from '../../../src/zksync-block-explorer/service'; -import { VerificationStatusResponse } from '../../../src/zksync-block-explorer/verification-status-response'; -import { ZkSyncBlockExplorerVerifyRequest } from '../../../src/zksync-block-explorer/verify-contract-request'; +import { ChainConfig } from '@nomicfoundation/hardhat-verify/types'; +import { EthereumProvider } from 'hardhat/types'; +import assert from 'assert'; +import { ZksyncBlockExplorerResponse } from '../../../src/explorers/zksync-block-explorer/verification-status-response'; +import { ZkSyncExplorerService } from '../../../src/explorers/zksync-block-explorer/service'; +import * as utils from '../../../src/utils'; +import * as metadata from '../../../src/solc/metadata'; describe('ZkSyncBlockExplorer Service', () => { + describe('fromChainConfig', () => { + let hre: any; + let chainConfig: ChainConfig; + beforeEach(() => { + hre = {}; + chainConfig = { + network: 'sepolia', + chainId: 300, + urls: { + apiURL: 'https://api.example.com', + browserURL: 'https://browser.example.com', + }, + }; + }); + + it('should create an instance of ZkSyncExplorerService with valid config', async () => { + const service = await ZkSyncExplorerService.fromChainConfig(hre, chainConfig); + expect(service).instanceOf(ZkSyncExplorerService); + }); + }); + + describe('getCurrentChainConfig', () => { + const customChains: ChainConfig[] = [ + { + network: 'customChain1', + chainId: 5000, + urls: { + apiURL: '', + browserURL: '', + }, + }, + { + network: 'customChain2', + chainId: 5000, + urls: { + apiURL: '', + browserURL: '', + }, + }, + { + network: 'customChain3', + chainId: 4999, + urls: { + apiURL: '', + browserURL: '', + }, + }, + ]; + + const defaultChains: ChainConfig[] = [ + { + network: 'defaultChains1', + chainId: 300, + urls: { + apiURL: '', + browserURL: '', + }, + }, + { + network: 'defaultChains2', + chainId: 260, + urls: { + apiURL: '', + browserURL: '', + }, + }, + { + network: 'defaultChains3', + chainId: 250, + urls: { + apiURL: '', + browserURL: '', + }, + }, + ]; + + it('should return the last matching custom chain defined by the user', async function () { + const networkName = 'customChain2'; + const ethereumProvider = { + async send() { + return (5000).toString(16); + }, + } as unknown as EthereumProvider; + + const currentChainConfig = await ZkSyncExplorerService.getCurrentChainConfig( + ethereumProvider, + customChains, + defaultChains, + ); + + assert.equal(currentChainConfig.network, networkName); + assert.equal(currentChainConfig.chainId, 5000); + }); + + it('should return a built-in chain if no custom chain matches', async function () { + const networkName = 'defaultChains2'; + const ethereumProvider = { + async send() { + return (260).toString(16); + }, + } as unknown as EthereumProvider; + const currentChainConfig = await ZkSyncExplorerService.getCurrentChainConfig( + ethereumProvider, + customChains, + defaultChains, + ); + + assert.equal(currentChainConfig.network, networkName); + assert.equal(currentChainConfig.chainId, 260); + }); + + it('should return hardhat if the selected network is hardhat and it was added as a custom chain', async () => { + const networkName = 'hardhat'; + const ethereumProvider = { + async send() { + return (31337).toString(16); + }, + } as unknown as EthereumProvider; + + const currentChainConfig = await ZkSyncExplorerService.getCurrentChainConfig( + ethereumProvider, + [ + ...customChains, + { + network: 'hardhat', + chainId: 31337, + urls: { + apiURL: '', + browserURL: '', + }, + }, + ], + defaultChains, + ); + + assert.equal(currentChainConfig.network, networkName); + assert.equal(currentChainConfig.chainId, 31337); + }); + + it('should throw if there are no matches at all', async () => { + const ethereumProvider = { + async send() { + return (21343214123).toString(16); + }, + } as unknown as EthereumProvider; + + try { + await ZkSyncExplorerService.getCurrentChainConfig(ethereumProvider, customChains, defaultChains); + } catch (e: any) { + expect(e.message).to.contains('The provided chain with id 21343214123 is not supported by default!'); + } + }); + }); + describe('checkVerificationStatusService', () => { + const sandbox: SinonSandbox = sinon.createSandbox(); afterEach(() => { - sinon.restore(); + sandbox.restore(); }); it('should return the verification status response', async () => { const requestId = 123; - const verifyURL = 'https://example.com/verify'; - + const explorer = new ZkSyncExplorerService({} as any, 'https://example.com/verify', 'https://example.com/'); const response = { status: 200, data: { @@ -30,20 +183,21 @@ describe('ZkSyncBlockExplorer Service', () => { }, }; - sinon.stub(axios, 'get').resolves(response); - - const result = await checkVerificationStatusService(requestId, verifyURL); + sandbox.stub(axios, 'get').resolves(response); + const result = await explorer.getVerificationStatus(requestId, { + contractAddress: '0x0000000000000001', + contractName: 'contracts/Example.sol:Example', + }); expect(!result.errorExists()); - expect(result).to.be.instanceOf(VerificationStatusResponse); + expect(result).to.be.instanceOf(ZksyncBlockExplorerResponse); expect(result.status).to.equal(response.data.status); - expect(result.isVerificationSuccess()).to.equal(true); + expect(result.isSuccess()).to.equal(true); }); it('should return the error', async () => { const requestId = 123; - const verifyURL = 'https://example.com/verify'; - + const explorer = new ZkSyncExplorerService({} as any, 'https://example.com/verify', 'https://example.com/'); const response = { status: 400, data: { @@ -54,25 +208,30 @@ describe('ZkSyncBlockExplorer Service', () => { }, }; - sinon.stub(axios, 'get').resolves(response); - - const result = await checkVerificationStatusService(requestId, verifyURL); + sandbox.stub(axios, 'get').resolves(response); - expect(!result.isPending()); - expect(!result.isQueued()); - expect(result.getError().includes('already verified')); + try { + await explorer.getVerificationStatus(requestId, { + contractAddress: '0x0000000000000001', + contractName: 'contracts/Example.sol:Example', + }); + } catch (error: any) { + expect(error.message).to.contains(`Failed to send contract verification request`); + } }); it('should handle axios error', async () => { const requestId = 123; - const verifyURL = 'https://example.com/verify'; - + const explorer = new ZkSyncExplorerService({} as any, 'https://example.com/verify', 'https://example.com/'); const error = new Error('Network error'); - sinon.stub(axios, 'get').rejects(error); + sandbox.stub(axios, 'get').rejects(error); try { - await checkVerificationStatusService(requestId, verifyURL); + await explorer.getVerificationStatus(requestId, { + contractAddress: '0x0000000000000001', + contractName: 'contracts/Example.sol:Example', + }); } catch (err: any) { expect(err.message).to.equal(err.message); } @@ -80,121 +239,240 @@ describe('ZkSyncBlockExplorer Service', () => { }); describe('verifyContractRequest', () => { + const sandbox: SinonSandbox = sinon.createSandbox(); + beforeEach(() => { + sandbox.stub(utils, 'retrieveContractBytecode').resolves('0x1234567890'); + sandbox.stub(metadata, 'inferSolcVersion').resolves('0.8.0'); + }); afterEach(() => { - sinon.restore(); + sandbox.restore(); }); it('should return the ZkSyncBlockExplorerResponse when verification is successful', async () => { - const req: ZkSyncBlockExplorerVerifyRequest = { - codeFormat: 'solidity-standard-json-input', - compilerSolcVersion: '0.8.0', - compilerZksolcVersion: '0.1.0', - contractName: 'MyContract', - constructorArguments: '[]', - contractAddress: '0x123456', - optimizationUsed: true, - sourceCode: 'pragma solidity ^0.8.0; contract MyContract {}', - }; - const verifyURL = 'https://example.com/verify'; - - const response = { + sandbox.stub(axios, 'post').resolves({ + status: 200, + data: '24444', + }); + sandbox.stub(axios, 'get').resolves({ status: 200, - data: 'Verification successful', + data: ['0.8.0'], + }); + + sandbox.stub().resolves(0); + const hre = { + config: { + paths: { + sources: 'contracts', + root: 'root', + }, + zksolc: { + version: '1.5.4', + settings: { + contractsToCompile: [], + }, + }, + }, + network: { + config: { + url: 'http://localhost:3000', + }, + zksync: true, + verifyURL: 'http://localhost:3000/verify', + }, + run: sandbox + .stub() + .onSecondCall() + .resolves(['0.8.0']) + .onThirdCall() + .resolves({ + contractName: 'Contract', + sourceName: 'contracts/Contract.sol', + compilerInput: { + language: 'Solidity', + sources: { + 'contracts/Contract.sol': { + content: 'contract Contract {}', + }, + }, + settings: { + optimizer: { + enabled: true, + }, + outputSelection: { + '*': { + '*': ['evm'], + }, + }, + }, + }, + contractOutput: { + abi: [], + metadata: { + zk_version: '0.1.0', + solc_metadata: '0x1234567890', + optimizer_settings: '0x1234567890', + }, + evm: { + bytecode: { + linkReferences: {}, + object: '0x1234567890', + opcodes: '0x1234567890', + sourceMap: '0x1234567890', + }, + deployedBytecode: { + linkReferences: {}, + object: '0x1234567890', + opcodes: '0x1234567890', + sourceMap: '0x1234567890', + }, + methodIdentifiers: {}, + }, + }, + solcVersion: '0.8.0', + }) + .onCall(3) + .resolves({ + getResolvedFiles: sandbox.stub().resolves([ + { + sourceName: 'contracts/Contract.sol', + content: { + rawContent: 'contract Contract {}', + }, + }, + ]), + }), }; - - sinon.stub(axios, 'post').resolves(response); - - const result = await verifyContractRequest(req, verifyURL); - - expect(result).to.be.instanceOf(ZkSyncBlockExplorerResponse); - expect(result.status).to.equal(response.status); - expect(result.message).to.equal(response.data); + const explorer = new ZkSyncExplorerService( + hre as any, + 'https://example.com/verify', + 'https://example.com/', + ); + + const result = await explorer.verify( + '0x275a050Fd05883dbB572D76F8B5E53A892b370AD', + 'contracts/Contract.sol', + [], + {}, + false, + ); + + expect(result.verificationId).to.equal(24444); + expect(result.contractVerifyDataInfo.contractAddress).to.equal( + '0x275a050Fd05883dbB572D76F8B5E53A892b370AD', + ); + expect(result.contractVerifyDataInfo.contractName).to.equal('contracts/Contract.sol:Contract'); }); - it('should throw ZkSyncVerifyPluginError when verification fails', async () => { - const req: ZkSyncBlockExplorerVerifyRequest = { - codeFormat: 'solidity-standard-json-input', - compilerSolcVersion: '0.8.0', - compilerZksolcVersion: '0.1.0', - contractName: 'MyContract', - constructorArguments: '[]', - contractAddress: '0x123456', - optimizationUsed: true, - sourceCode: 'pragma solidity ^0.8.0; contract MyContract {}', - }; - const verifyURL = 'https://example.com/verify'; - - const response = { - status: 400, - data: 'Verification failed', + it('should throw the error when verification is unsuccessful', async () => { + sandbox.stub(axios, 'post').resolves({ + status: 500, + data: 'Cannot verify contract', + }); + sandbox.stub(axios, 'get').resolves({ + status: 200, + data: ['0.8.0'], + }); + + sandbox.stub().resolves(0); + const hre = { + config: { + paths: { + sources: 'contracts', + root: 'root', + }, + zksolc: { + version: '1.5.4', + settings: { + contractsToCompile: [], + }, + }, + }, + network: { + config: { + url: 'http://localhost:3000', + }, + zksync: true, + verifyURL: 'http://localhost:3000/verify', + }, + run: sandbox + .stub() + .onSecondCall() + .resolves(['0.8.0']) + .onThirdCall() + .resolves({ + contractName: 'Contract', + sourceName: 'contracts/Contract.sol', + compilerInput: { + language: 'Solidity', + sources: { + 'contracts/Contract.sol': { + content: 'contract Contract {}', + }, + }, + settings: { + optimizer: { + enabled: true, + }, + outputSelection: { + '*': { + '*': ['evm'], + }, + }, + }, + }, + contractOutput: { + abi: [], + metadata: { + zk_version: '0.1.0', + solc_metadata: '0x1234567890', + optimizer_settings: '0x1234567890', + }, + evm: { + bytecode: { + linkReferences: {}, + object: '0x1234567890', + opcodes: '0x1234567890', + sourceMap: '0x1234567890', + }, + deployedBytecode: { + linkReferences: {}, + object: '0x1234567890', + opcodes: '0x1234567890', + sourceMap: '0x1234567890', + }, + methodIdentifiers: {}, + }, + }, + solcVersion: '0.8.0', + }) + .onCall(3) + .resolves({ + getResolvedFiles: sandbox.stub().resolves([ + { + sourceName: 'contracts/Contract.sol', + content: { + rawContent: 'contract Contract {}', + }, + }, + ]), + }), }; - - sinon.stub(axios, 'post').resolves(response); + const explorer = new ZkSyncExplorerService( + hre as any, + 'https://example.com/verify', + 'https://example.com/', + ); try { - await verifyContractRequest(req, verifyURL); - expect.fail('Expected ZkSyncVerifyPluginError to be thrown'); - } catch (error: any) { - expect(error.message).to.includes( - 'Failed to send contract verification request\n Reason: ZkSyncVerifyPluginError: Verification failed', + await explorer.verify( + '0x275a050Fd05883dbB572D76F8B5E53A892b370AD', + 'contracts/Contract.sol', + [], + {}, + false, ); - } - }); - - it('should handle axios error', async () => { - const req: ZkSyncBlockExplorerVerifyRequest = { - codeFormat: 'solidity-standard-json-input', - compilerSolcVersion: '0.8.0', - compilerZksolcVersion: '0.1.0', - contractName: 'MyContract', - constructorArguments: '[]', - contractAddress: '0x123456', - optimizationUsed: true, - sourceCode: 'pragma solidity ^0.8.0; contract MyContract {}', - }; - const verifyURL = 'https://example.com/verify'; - - const error = new Error('Network error'); - - sinon.stub(axios, 'post').rejects(error); - - try { - await verifyContractRequest(req, verifyURL); - } catch (err: any) { - expect(err.message).to.equal(err.message); - } - }); - }); - - describe('getSupportedCompilerVersions', () => { - afterEach(() => { - sinon.restore(); - }); - - it('should return the list of supported compiler versions', async () => { - const verifyURL = 'https://example.com/verify'; - - const response = { - data: ['0.7.0', '0.8.0', '0.8.1'], - }; - - sinon.stub(axios, 'get').resolves(response); - - const result = await getSupportedCompilerVersions(verifyURL); - - expect(result).to.deep.equal(response.data); - }); - - it('should fail the get the supported compiler versions', async () => { - const verifyURL = 'https://example.com/verify'; - - sinon.stub(axios, 'get').rejects(new Error('Network Error')); - - try { - await getSupportedCompilerVersions(verifyURL); - throw new Error('Expected getSupportedCompilerVersions to throw'); - } catch (error) { - expect(error).to.be.an('error'); + } catch (e: any) { + expect(e.message).to.contains('Reason: ZkSyncVerifyPluginError: Cannot verify contract'); } }); }); diff --git a/packages/hardhat-zksync-verify/test/tests/zksync-etherscan/errors.test.ts b/packages/hardhat-zksync-verify/test/tests/zksync-etherscan/errors.test.ts new file mode 100644 index 000000000..b9c48c861 --- /dev/null +++ b/packages/hardhat-zksync-verify/test/tests/zksync-etherscan/errors.test.ts @@ -0,0 +1,16 @@ +import { expect } from 'chai'; +import { ZksyncContractVerificationInvalidStatusCodeError } from '../../../src/explorers/errors'; + +describe('ZksyncContractVerificationInvalidStatusCodeError', () => { + it('should create an error with the correct message', () => { + const url = 'http://example.com'; + const statusCode = 400; + const responseText = 'Bad Request'; + + const error = new ZksyncContractVerificationInvalidStatusCodeError(url, statusCode, responseText); + + expect(error.message).to.contains(`Failed to send contract verification request. + Endpoint URL: ${url} + The HTTP server response is not ok. Status code: ${statusCode} Response text: ${responseText}`); + }); +}); diff --git a/packages/hardhat-zksync-verify/test/tests/zksync-etherscan/service.test.ts b/packages/hardhat-zksync-verify/test/tests/zksync-etherscan/service.test.ts new file mode 100644 index 000000000..1050aa5ed --- /dev/null +++ b/packages/hardhat-zksync-verify/test/tests/zksync-etherscan/service.test.ts @@ -0,0 +1,500 @@ +import { expect } from 'chai'; +import sinon, { SinonSandbox } from 'sinon'; +import axios from 'axios'; +import { ChainConfig } from '@nomicfoundation/hardhat-verify/types'; +import { EthereumProvider } from 'hardhat/types'; +import assert from 'assert'; +import * as metadata from '../../../src/solc/metadata'; +import * as utils from '../../../src/utils'; +import { ZkSyncEtherscanExplorerService } from '../../../src/explorers/zksync-etherscan/service'; +import { ZksyncEtherscanResponse } from '../../../src/explorers/zksync-etherscan/verification-status-response'; + +describe('ZkSyncEtherscan Service', () => { + describe('fromChainConfig', () => { + let hre: any; + let chainConfig: ChainConfig; + beforeEach(() => { + hre = {}; + chainConfig = { + network: 'sepolia', + chainId: 300, + urls: { + apiURL: 'https://api.example.com', + browserURL: 'https://browser.example.com', + }, + }; + }); + + it('should create an instance of ZkSyncEtherscan with valid config', async () => { + const service = await ZkSyncEtherscanExplorerService.fromChainConfig(hre, 'api', chainConfig); + expect(service).instanceOf(ZkSyncEtherscanExplorerService); + }); + }); + + describe('getCurrentChainConfig', () => { + const customChains: ChainConfig[] = [ + { + network: 'customChain1', + chainId: 5000, + urls: { + apiURL: '', + browserURL: '', + }, + }, + { + network: 'customChain2', + chainId: 5000, + urls: { + apiURL: '', + browserURL: '', + }, + }, + { + network: 'customChain3', + chainId: 4999, + urls: { + apiURL: '', + browserURL: '', + }, + }, + ]; + + const defaultChains: ChainConfig[] = [ + { + network: 'defaultChains1', + chainId: 300, + urls: { + apiURL: '', + browserURL: '', + }, + }, + { + network: 'defaultChains2', + chainId: 260, + urls: { + apiURL: '', + browserURL: '', + }, + }, + { + network: 'defaultChains3', + chainId: 250, + urls: { + apiURL: '', + browserURL: '', + }, + }, + ]; + + it('should return the last matching custom chain defined by the user', async function () { + const networkName = 'customChain2'; + const ethereumProvider = { + async send() { + return (5000).toString(16); + }, + } as unknown as EthereumProvider; + + const currentChainConfig = await ZkSyncEtherscanExplorerService.getCurrentChainConfig( + ethereumProvider, + customChains, + defaultChains, + ); + + assert.equal(currentChainConfig.network, networkName); + assert.equal(currentChainConfig.chainId, 5000); + }); + + it('should return a built-in chain if no custom chain matches', async function () { + const networkName = 'defaultChains2'; + const ethereumProvider = { + async send() { + return (260).toString(16); + }, + } as unknown as EthereumProvider; + const currentChainConfig = await ZkSyncEtherscanExplorerService.getCurrentChainConfig( + ethereumProvider, + customChains, + defaultChains, + ); + + assert.equal(currentChainConfig.network, networkName); + assert.equal(currentChainConfig.chainId, 260); + }); + + it('should return hardhat if the selected network is hardhat and it was added as a custom chain', async () => { + const networkName = 'hardhat'; + const ethereumProvider = { + async send() { + return (31337).toString(16); + }, + } as unknown as EthereumProvider; + + const currentChainConfig = await ZkSyncEtherscanExplorerService.getCurrentChainConfig( + ethereumProvider, + [ + ...customChains, + { + network: 'hardhat', + chainId: 31337, + urls: { + apiURL: '', + browserURL: '', + }, + }, + ], + defaultChains, + ); + + assert.equal(currentChainConfig.network, networkName); + assert.equal(currentChainConfig.chainId, 31337); + }); + + it('should throw if there are no matches at all', async () => { + const ethereumProvider = { + async send() { + return (21343214123).toString(16); + }, + } as unknown as EthereumProvider; + + try { + await ZkSyncEtherscanExplorerService.getCurrentChainConfig( + ethereumProvider, + customChains, + defaultChains, + ); + } catch (e: any) { + expect(e.message).to.contains('The provided chain with id 21343214123 is not supported by default!'); + } + }); + }); + + describe('checkVerificationStatusService', () => { + const sandbox: SinonSandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore(); + }); + + it('should return the verification status response', async () => { + const requestId = '123'; + const explorer = new ZkSyncEtherscanExplorerService( + {} as any, + 'https://example.com/verify', + 'https://example.com/', + ); + const response = { + status: 200, + data: { + status: 1, + result: 'Pass - Verified', + }, + }; + + sandbox.stub(axios, 'get').resolves(response); + const result = await explorer.getVerificationStatus(requestId, { + contractAddress: '0x0000000000000001', + contractName: 'contracts/Example.sol:Example', + }); + + expect(!result.errorExists()); + expect(result).to.be.instanceOf(ZksyncEtherscanResponse); + expect(result.status).to.equal(response.data.status); + expect(result.isSuccess()).to.equal(true); + }); + + it('should return the error', async () => { + const requestId = '123'; + const explorer = new ZkSyncEtherscanExplorerService( + {} as any, + 'https://example.com/verify', + 'https://example.com/', + ); + const response = { + status: 400, + data: { + status: 'failed', + message: 'Verification unsuccessful', + error: 'already verified', + compilationErrors: ['has compilation errors'], + }, + }; + + sandbox.stub(axios, 'get').resolves(response); + + try { + await explorer.getVerificationStatus(requestId, { + contractAddress: '0x0000000000000001', + contractName: 'contracts/Example.sol:Example', + }); + } catch (error: any) { + expect(error.message).to.contains(`Failed to send contract verification request`); + } + }); + + it('should handle axios error', async () => { + const requestId = '123'; + const explorer = new ZkSyncEtherscanExplorerService( + {} as any, + 'https://example.com/verify', + 'https://example.com/', + ); + const error = new Error('Network error'); + + sandbox.stub(axios, 'get').rejects(error); + + try { + await explorer.getVerificationStatus(requestId, { + contractAddress: '0x0000000000000001', + contractName: 'contracts/Example.sol:Example', + }); + } catch (err: any) { + expect(err.message).to.equal(err.message); + } + }); + }); + + describe('verifyContractRequest', () => { + const sandbox: SinonSandbox = sinon.createSandbox(); + beforeEach(() => { + sandbox.stub(utils, 'retrieveContractBytecode').resolves('0x1234567890'); + sandbox.stub(metadata, 'inferSolcVersion').resolves('0.8.0'); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should return the ZkSyncEtherscan when verification is successful', async () => { + sandbox.stub(axios, 'post').resolves({ + status: 200, + data: { + status: '1', + message: 'OK', + result: '24444', + }, + }); + + sandbox.stub().resolves(0); + const hre = { + config: { + paths: { + sources: 'contracts', + root: 'root', + }, + zksolc: { + version: '1.5.4', + settings: { + contractsToCompile: [], + }, + }, + }, + network: { + config: { + url: 'http://localhost:3000', + }, + zksync: true, + verifyURL: 'http://localhost:3000/verify', + }, + run: sandbox + .stub() + .onSecondCall() + .resolves(['0.8.0']) + .onThirdCall() + .resolves({ + contractName: 'Contract', + sourceName: 'contracts/Contract.sol', + compilerInput: { + language: 'Solidity', + sources: { + 'contracts/Contract.sol': { + content: 'contract Contract {}', + }, + }, + settings: { + optimizer: { + enabled: true, + }, + outputSelection: { + '*': { + '*': ['evm'], + }, + }, + }, + }, + contractOutput: { + abi: [], + metadata: { + zk_version: '0.1.0', + solc_metadata: '0x1234567890', + optimizer_settings: '0x1234567890', + }, + evm: { + bytecode: { + linkReferences: {}, + object: '0x1234567890', + opcodes: '0x1234567890', + sourceMap: '0x1234567890', + }, + deployedBytecode: { + linkReferences: {}, + object: '0x1234567890', + opcodes: '0x1234567890', + sourceMap: '0x1234567890', + }, + methodIdentifiers: {}, + }, + }, + solcVersion: '0.8.0', + solcLongVersion: '0.8.0', + }) + .onCall(3) + .resolves(['0.8.0']) + .onCall(4) + .resolves({ + getResolvedFiles: sandbox.stub().resolves([ + { + sourceName: 'contracts/Contract.sol', + content: { + rawContent: 'contract Contract {}', + }, + }, + ]), + }), + }; + const explorer = new ZkSyncEtherscanExplorerService( + hre as any, + 'https://example.com/verify', + 'https://example.com/', + ); + + const result = await explorer.verify( + '0x275a050Fd05883dbB572D76F8B5E53A892b370AD', + 'contracts/Contract.sol', + [], + {}, + false, + ); + + expect(result.verificationId).to.equal('24444'); + expect(result.contractVerifyDataInfo.contractAddress).to.equal( + '0x275a050Fd05883dbB572D76F8B5E53A892b370AD', + ); + expect(result.contractVerifyDataInfo.contractName).to.equal('contracts/Contract.sol:Contract'); + }); + + it('should throw the error when verification is unsuccessful', async () => { + sandbox.stub(axios, 'post').resolves({ + status: 200, + data: { + status: '0', + message: 'NOTOK', + result: 'Error!', + }, + }); + + sandbox.stub().resolves(0); + const hre = { + config: { + paths: { + sources: 'contracts', + root: 'root', + }, + zksolc: { + version: '1.5.4', + settings: { + contractsToCompile: [], + }, + }, + }, + network: { + config: { + url: 'http://localhost:3000', + }, + zksync: true, + verifyURL: 'http://localhost:3000/verify', + }, + run: sandbox + .stub() + .onSecondCall() + .resolves(['0.8.0']) + .onThirdCall() + .resolves({ + contractName: 'Contract', + sourceName: 'contracts/Contract.sol', + compilerInput: { + language: 'Solidity', + sources: { + 'contracts/Contract.sol': { + content: 'contract Contract {}', + }, + }, + settings: { + optimizer: { + enabled: true, + }, + outputSelection: { + '*': { + '*': ['evm'], + }, + }, + }, + }, + contractOutput: { + abi: [], + metadata: { + zk_version: '0.1.0', + solc_metadata: '0x1234567890', + optimizer_settings: '0x1234567890', + }, + evm: { + bytecode: { + linkReferences: {}, + object: '0x1234567890', + opcodes: '0x1234567890', + sourceMap: '0x1234567890', + }, + deployedBytecode: { + linkReferences: {}, + object: '0x1234567890', + opcodes: '0x1234567890', + sourceMap: '0x1234567890', + }, + methodIdentifiers: {}, + }, + }, + solcVersion: '0.8.0', + solcLongVersion: '0.8.0', + }) + .onCall(3) + .resolves(['0.8.0']) + .onCall(4) + .resolves({ + getResolvedFiles: sandbox.stub().resolves([ + { + sourceName: 'contracts/Contract.sol', + content: { + rawContent: 'contract Contract {}', + }, + }, + ]), + }), + }; + const explorer = new ZkSyncEtherscanExplorerService( + hre as any, + 'https://example.com/verify', + 'https://example.com/', + ); + + try { + await explorer.verify( + '0x275a050Fd05883dbB572D76F8B5E53A892b370AD', + 'contracts/Contract.sol', + [], + {}, + false, + ); + } catch (e: any) { + console.log(e.message); + expect(e.message).to.contains('Reason: ZkSyncVerifyPluginError: Error!'); + } + }); + }); +});