diff --git a/packages/hardhat-zksync-verify/package.json b/packages/hardhat-zksync-verify/package.json index 513a9e7e2..41e00a980 100644 --- a/packages/hardhat-zksync-verify/package.json +++ b/packages/hardhat-zksync-verify/package.json @@ -22,7 +22,7 @@ "fmt": "yarn prettier --write", "eslint": "eslint 'src/**/*.ts' 'test/**/*.ts'", "prettier": "prettier 'src/**/*.ts' 'test/**/*.ts'", - "test": "NODE_ENV=test c8 mocha test/tests.ts --no-timeout --exit", + "test": "c8 mocha --recursive \"test/tests/**/*.ts\" --exit", "build": "tsc --build .", "clean": "rimraf dist" }, diff --git a/packages/hardhat-zksync-verify/src/plugin.ts b/packages/hardhat-zksync-verify/src/plugin.ts index dfb2568c7..0c0c08815 100644 --- a/packages/hardhat-zksync-verify/src/plugin.ts +++ b/packages/hardhat-zksync-verify/src/plugin.ts @@ -2,7 +2,6 @@ import { TASK_FLATTEN_GET_FLATTENED_SOURCE } from 'hardhat/builtin-tasks/task-na import { Artifacts, HardhatRuntimeEnvironment, ResolvedFile } from 'hardhat/types'; import { isFullyQualifiedName, parseFullyQualifiedName } from 'hardhat/utils/contract-names'; import { - MULTIPLE_MATCHING_CONTRACTS, CONTRACT_NAME_NOT_FOUND, NO_MATCHING_CONTRACT, LIBRARIES_EXPORT_ERROR, @@ -48,8 +47,6 @@ export async function inferContractArtifacts( if (contractMatches.length === 0) throw new ZkSyncVerifyPluginError(NO_MATCHING_CONTRACT); - if (contractMatches.length > 1) throw new ZkSyncVerifyPluginError(MULTIPLE_MATCHING_CONTRACTS); - return contractMatches[0]; } diff --git a/packages/hardhat-zksync-verify/src/task-actions.ts b/packages/hardhat-zksync-verify/src/task-actions.ts index 6ce03614c..75b149ee8 100644 --- a/packages/hardhat-zksync-verify/src/task-actions.ts +++ b/packages/hardhat-zksync-verify/src/task-actions.ts @@ -21,7 +21,7 @@ import { BUILD_INFO_NOT_FOUND_ERROR, } from './constants'; -import { encodeArguments, retrieveContractBytecode } from './utils'; +import { encodeArguments, extractModule, retrieveContractBytecode } from './utils'; import { Libraries } from './types'; import { ZkSyncVerifyPluginError } from './errors'; import { parseFullyQualifiedName } from 'hardhat/utils/contract-names'; @@ -109,7 +109,7 @@ export async function getConstructorArguments( const constructorArgsModulePath = path.resolve(process.cwd(), args.constructorArgsModule); try { - const constructorArguments = (await import(constructorArgsModulePath)).default; + 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')) { @@ -246,7 +246,7 @@ export async function getContractInfo( deployedBytecode ); - if (contractInformation === null) { + if (contractInformation === undefined || contractInformation === null) { throw new ZkSyncVerifyPluginError(NO_MATCHING_CONTRACT); } } else { diff --git a/packages/hardhat-zksync-verify/src/utils.ts b/packages/hardhat-zksync-verify/src/utils.ts index dfedf2f11..afa257fa3 100644 --- a/packages/hardhat-zksync-verify/src/utils.ts +++ b/packages/hardhat-zksync-verify/src/utils.ts @@ -99,3 +99,8 @@ export function parseWrongConstructorArgumentsError(string: string): string { return `The number of constructor arguments you provided (${data['values']}) does not match the number of constructor arguments the contract has been deployed with (${data['types']}).`; } + +export async function extractModule(constructorArgsModulePath: string) { + const constructorArguments = (await import(constructorArgsModulePath)).default; + return constructorArguments; +} \ No newline at end of file diff --git a/packages/hardhat-zksync-verify/test/fixture-projects/localGreeter/args.js b/packages/hardhat-zksync-verify/test/fixture-projects/localGreeter/args.js new file mode 100644 index 000000000..ee3507708 --- /dev/null +++ b/packages/hardhat-zksync-verify/test/fixture-projects/localGreeter/args.js @@ -0,0 +1,3 @@ +module.exports = { + "name": "localGreeter", +} \ No newline at end of file diff --git a/packages/hardhat-zksync-verify/test/fixture-projects/localGreeter/contracts/Contract.sol b/packages/hardhat-zksync-verify/test/fixture-projects/localGreeter/contracts/Contract.sol new file mode 100644 index 000000000..efcb9fa85 --- /dev/null +++ b/packages/hardhat-zksync-verify/test/fixture-projects/localGreeter/contracts/Contract.sol @@ -0,0 +1,5 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.16; + +contract Contract { +} \ No newline at end of file diff --git a/packages/hardhat-zksync-verify/test/tests.ts b/packages/hardhat-zksync-verify/test/tests.ts deleted file mode 100644 index e1f384823..000000000 --- a/packages/hardhat-zksync-verify/test/tests.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { assert } from 'chai'; -import { useEnvironment } from './helpers'; - -describe('verify plugin', async function () { - const sourceName: string = 'contracts/Greeter.sol'; - const contractName: string = 'Greeter'; - 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/plugin.test.ts b/packages/hardhat-zksync-verify/test/tests/plugin.test.ts new file mode 100644 index 000000000..0e2161d8c --- /dev/null +++ b/packages/hardhat-zksync-verify/test/tests/plugin.test.ts @@ -0,0 +1,342 @@ +import { expect } from "chai"; +import sinon from "sinon"; +import { + checkContractName, + checkVerificationStatus, + flattenContractFile, + getLibraries, + getSolidityStandardJsonInput, + inferContractArtifacts, +} from "../../src/plugin"; +import { Artifacts, ResolvedFile } from "hardhat/types"; +import { Bytecode } from "../../src/solc/bytecode"; +import * as bytecodes from "../../src/solc/bytecode"; +import { useEnvironment } from "../helpers"; +import { NO_MATCHING_CONTRACT } from "../../src/constants"; +import { fail } from "assert"; +import { ContractInformation } from "../../src/solc/types"; +import { VerificationStatusResponse } from "../../src/zksync-block-explorer/verification-status-response"; +import path from "path"; + +describe("Plugin", () => { + let artifacts: Artifacts = { + readArtifact: sinon.stub().resolves({}), + readArtifactSync: sinon.stub().returns({}), + artifactExists: sinon.stub().resolves(true), + getAllFullyQualifiedNames: sinon.stub().resolves([]), + getBuildInfo: sinon.stub().resolves({ solcVersion: "0.8.0" }), + getBuildInfoSync: sinon.stub().returns({ solcVersion: "0.8.0" }), + getArtifactPaths: sinon.stub().resolves([]), + getDebugFilePaths: sinon.stub().resolves([]), + getBuildInfoPaths: sinon.stub().resolves([]), + saveArtifactAndDebugFile: sinon.stub().resolves(), + saveBuildInfo: sinon.stub().resolves(), + formArtifactPathFromFullyQualifiedName: sinon.stub().returns(""), + }; + + let extractMatchingContractInformationStub: sinon.SinonStub; + + beforeEach(() => { + sinon.restore(); + }); + + function extractMatchingContractInformation( + contractInformation: any, + ): Promise { + return contractInformation; + } + + let bytecode: Bytecode = new Bytecode("0x1234567890"); + + describe("inferContractArtifacts", () => { + it("should throw ZkSyncVerifyPluginError when no matching contract is found", async () => { + extractMatchingContractInformationStub = sinon + .stub(bytecodes, "extractMatchingContractInformation") + .returns( + extractMatchingContractInformation({ + 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: "v0.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", + }), + ); + + try { + await inferContractArtifacts(artifacts, [], bytecode); + fail("Expected an error to be thrown"); + } catch (error: any) { + expect(error.message).to.equal(NO_MATCHING_CONTRACT); + } + }); + it("should return the contract information when a matching contract is found", async () => { + extractMatchingContractInformationStub = sinon + .stub(bytecodes, "extractMatchingContractInformation") + .returns( + extractMatchingContractInformation({ + 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: "v0.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", + }), + ); + + artifacts.getAllFullyQualifiedNames = sinon + .stub() + .resolves(["contracts/Contract.sol:Contract"]); + + const matchingCompilerVersions = ["0.8.0"]; + + const contractInformation: ContractInformation = + await inferContractArtifacts( + artifacts, + matchingCompilerVersions, + bytecode, + ); + + expect(contractInformation.contractName).to.equal("Contract"); + expect(contractInformation.sourceName).to.equal("contracts/Contract.sol"); + expect(contractInformation.contractOutput.evm.bytecode.object).to.equal( + "0x1234567890", + ); + }); + }); + + describe("flattenContractFile", async function () { + useEnvironment("localGreeter"); + + it("should return the flattened source code", async function () { + const filePath = "contracts/Contract.sol"; + + const flattenedSourceCode = await flattenContractFile(this.env, filePath); + + expect(flattenedSourceCode).to.includes("contract Contract {"); + }); + }); + + describe("checkContractName", async function () { + it("should throw ZkSyncVerifyPluginError when contractFQN is not a valid fully qualified name", async function () { + artifacts.artifactExists = sinon.stub().resolves(false); + artifacts.getAllFullyQualifiedNames = sinon + .stub() + .resolves(["contracts/Contract.sol:Contract"]); + const contractFQN = "invalid_contract_name"; + + try { + await checkContractName(artifacts, contractFQN); + fail("Expected an error to be thrown"); + } catch (error: any) { + expect(error.message).to + .equal(`A valid fully qualified name was expected. Fully qualified names look like this: "contracts/AContract.sol:TheContract" +Instead, this name was received: ${contractFQN}`); + } + }); + + it("should throw ZkSyncVerifyPluginError when the contract does not exist", async function () { + artifacts.artifactExists = sinon.stub().resolves(false); + const contractFQN = "contracts/Contract.sol:Contract"; + + try { + await checkContractName(artifacts, contractFQN); + fail("Expected an error to be thrown"); + } catch (error: any) { + expect(error.message).to.equal( + "The contract contracts/Contract.sol:Contract is not present in your project.", + ); + } + }); + + it("should not throw an error when the contract exists", async function () { + artifacts.artifactExists = sinon.stub().resolves(true); + const contractFQN = "contracts/Contract.sol:Contract"; + + try { + await checkContractName(artifacts, contractFQN); + } catch (error: any) { + fail("Expected no error to be thrown"); + } + }); + }); + + describe("getSolidityStandardJsonInput", async function () { + useEnvironment("localGreeter"); + it("should return the Solidity standard JSON input", async function () { + const resolvedFiles: ResolvedFile[] = [ + { + sourceName: "contracts/Contract.sol", + absolutePath: "contracts/Contract.sol", + lastModificationDate: new Date(), + contentHash: "0x1234567890", + getVersionedName: () => "contracts/Contract.sol", + content: { + rawContent: "contract Contract {}", + imports: [], + versionPragmas: ["0.8.0"], + }, + }, + ]; + + const solidityStandardJsonInput = getSolidityStandardJsonInput( + this.env, + resolvedFiles, + ); + + expect(solidityStandardJsonInput.language).to.equal("Solidity"); + expect( + solidityStandardJsonInput.sources["contracts/Contract.sol"].content, + ).to.equal("contract Contract {}"); + expect(solidityStandardJsonInput.settings.optimizer.enabled).to.be.true; + expect(solidityStandardJsonInput.settings.areLibrariesMissing).to.be + .false; + }); + }); + + describe("getLibraries", async function () { + it("should throw ZkSyncVerifyPluginError when importing the libraries module fails", async function () { + const librariesModule = "../args.js"; + + try { + await getLibraries(librariesModule); + fail("Expected an error to be thrown"); + } catch (error: any) { + expect(error.message).to.includes( + `Importing the module for the libraries dictionary failed. Reason: Cannot find module '${path.resolve( + process.cwd(), + librariesModule, + )}'`, + ); + } + }); + + it("should return the libraries object when importing the libraries module succeeds", async function () { + const librariesModule = "../localGreeter/args.js"; + + const libraries = await getLibraries(librariesModule); + + expect(libraries).to.deep.equal({ + name: "localGreeter", + }); + }); + }); + + describe("checkVerificationStatus", async function () { + useEnvironment("localGreeter"); + + it("should throw ZkSyncVerifyPluginError when backend verification error exists", async function () { + let verificationStatusResponseStub = 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("should log a success message and return true when verification is successful", async function () { + let verificationStatusResponseStub = 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.be.true; + }); + }); +}); diff --git a/packages/hardhat-zksync-verify/test/tests/solc/bytecode.test.ts b/packages/hardhat-zksync-verify/test/tests/solc/bytecode.test.ts new file mode 100644 index 000000000..38fb6f752 --- /dev/null +++ b/packages/hardhat-zksync-verify/test/tests/solc/bytecode.test.ts @@ -0,0 +1,134 @@ +import { expect } from "chai"; +import { Bytecode, compareBytecode, extractMatchingContractInformation } from "../../../src/solc/bytecode"; +import * as bytecodes from "../../../src/solc/bytecode"; +import sinon from "sinon"; + +describe("compareBytecode", () => { + beforeEach(() => { + sinon.restore(); + }); + + it("should return null when the lengths of deployed and runtime executable sections are different", async () => { + const deployedBytecode: Bytecode = new Bytecode("deployedBytecode"); + const runtimeBytecodeSymbols = { + object: "runtimeBytecode", + }; + + const result = await compareBytecode(deployedBytecode, runtimeBytecodeSymbols as any); + + expect(result).to.be.null; + }); + + it("should return null when the normalized bytecode does not match the reference bytecode", async () => { + const deployedBytecode: Bytecode = new Bytecode("deployedBytecode"); + const runtimeBytecodeSymbols = { + object: "runtimeBytecode", + }; + + // Stub the normalizeBytecode function to return different normalized bytecodes + const normalizeBytecodeStub = sinon.stub().resolves({ + normalizedBytecode: "differentNormalizedBytecode", + }); + sinon.replace( + // Replace the import of normalizeBytecode with the stub + bytecodes, + "normalizeBytecode", + normalizeBytecodeStub + ); + + const result = await compareBytecode(deployedBytecode, runtimeBytecodeSymbols as any); + + expect(result).to.be.null; + }); + + it("should return the normalized bytecode when it matches the reference bytecode", async () => { + const deployedBytecode: Bytecode = new Bytecode("deployedBytecode"); + const runtimeBytecodeSymbols = { + object: "deployedBytecode", + }; + + const result = await compareBytecode(deployedBytecode, runtimeBytecodeSymbols as any); + + expect(result).to.deep.equal({ + normalizedBytecode: "deployedBytecode", + }); + }); +}); + +describe("extractMatchingContractInformation", () => { + it("should return null when the contract does not have 'evm' property", async () => { + const sourceName = "sourceName"; + const contractName = "contractName"; + const buildInfo = { + output: { + contracts: { + [sourceName]: { + [contractName]: {}, + }, + }, + }, + }; + const deployedBytecode: Bytecode = new Bytecode("deployedBytecode"); + + const result = await extractMatchingContractInformation(sourceName, contractName, buildInfo as any, deployedBytecode); + + expect(result).to.be.null; + }); + + it("should return null when the runtime bytecode symbols are null", async () => { + const sourceName = "sourceName"; + const contractName = "contractName"; + const buildInfo = { + output: { + contracts: { + [sourceName]: { + [contractName]: { + evm: { + bytecode: null, + }, + }, + }, + }, + }, + }; + const deployedBytecode: Bytecode = new Bytecode("deployedBytecode"); + + const result = await extractMatchingContractInformation(sourceName, contractName, buildInfo as any, deployedBytecode); + + expect(result).to.be.null; + }); + + it("should return the contract information when the bytecode is matched", async () => { + const sourceName = "sourceName"; + const contractName = "contractName"; + const buildInfo = { + input: "compilerInput", + output: { + contracts: { + [sourceName]: { + [contractName]: { + evm: { + bytecode: { + object: "deployedBytecode", + }, + }, + }, + }, + }, + }, + solcVersion: "solcVersion", + }; + const deployedBytecode: Bytecode = new Bytecode("deployedBytecode"); + + const result = await bytecodes.extractMatchingContractInformation(sourceName, contractName, buildInfo as any, deployedBytecode); + + expect(result).to.deep.equal({ + normalizedBytecode: "deployedBytecode", + compilerInput: "compilerInput", + contractOutput: buildInfo.output.contracts[sourceName][contractName], + solcVersion: "solcVersion", + sourceName, + contractName, + }); + }); +}); \ No newline at end of file diff --git a/packages/hardhat-zksync-verify/test/tests/task-actions.test.ts b/packages/hardhat-zksync-verify/test/tests/task-actions.test.ts new file mode 100644 index 000000000..6adde5540 --- /dev/null +++ b/packages/hardhat-zksync-verify/test/tests/task-actions.test.ts @@ -0,0 +1,697 @@ +import { expect, use } from "chai"; +import sinon from "sinon"; +import { getCompilerVersions, getConstructorArguments, getContractInfo, verify, verifyContract } from "../../src/task-actions"; +import { fail } from "assert"; +import { + BUILD_INFO_NOT_FOUND_ERROR, + 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, +} 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 { TASK_COMPILE, TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH } from "hardhat/builtin-tasks/task-names"; + +describe("verifyContract", async function () { + it("should call runSuper if zksync is false", async function () { + let runSuperStub = sinon.stub().resolves(0); + let hre = { + network: { + zksync: false, + }, + run: sinon.stub(), + }; + + await verifyContract({}, hre as any, runSuperStub as any); + expect(runSuperStub.calledOnce).to.be.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.be.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 () { + let retriveByteCodeStub = sinon + .stub(utils, "retrieveContractBytecode") + .resolves("0x1234567890"); + let inferSolcVersion = sinon + .stub(metadata, "inferSolcVersion") + .resolves("0.8.0"); + let getSupportedCompilerVersionsStub = sinon + .stub(service, "getSupportedCompilerVersions") + .resolves(["0.8.0"]); + let getSolidityStandardJsonInputStub = sinon + .stub(plugin, "getSolidityStandardJsonInput") + .resolves({ + language: "Solidity", + sources: { + "contracts/Contract.sol": { + content: "contract Contract {}", + }, + }, + }); + let verifyContractRequestStub = sinon + .stub(service, "verifyContractRequest") + .resolves({ + status: 200, + message: "1", + isOk: () => true, + }); + + const runSuperStub = sinon.stub().resolves(0); + const hre = { + 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(), + }), + }; + + const args = { + address: "0x1234567890123456789012345678901234567890", + constructorArguments: [], + contract: "contract", + libraries: "libraries", + noCompile: false, + }; + + await verifyContract(args, hre as any, runSuperStub as any); + expect(runSuperStub.calledOnce).to.be.false; + expect(hre.run.firstCall.args[0]).to.equal(TASK_VERIFY_GET_COMPILER_VERSIONS); + expect(hre.run.secondCall.args[0]).to.equal(TASK_COMPILE); + 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, + ); + }); +}); +describe("getCompilerVersions", async function () { + it("should call runSuper if zksync is false", async function () { + let runSuperStub = sinon.stub().resolves([]); + let hre = { + network: { + zksync: false, + }, + config: { + solidity: { + compilers: [], + }, + }, + run: sinon.stub(), + }; + + const result = await getCompilerVersions({}, hre as any, runSuperStub as any); + expect(result).to.deep.equal([]); + expect(runSuperStub.calledOnce).to.be.true; + }); + + it("should return compiler versions if zksync is true", async function () { + const runSuperStub = sinon.stub().resolves([]); + const hre = { + network: { + zksync: true, + }, + config: { + solidity: { + compilers: [ + { version: "0.8.0" }, + { version: "0.7.0" }, + ], + overrides: { + "contracts/Contract.sol": { version: "0.6.0" }, + }, + }, + }, + run: sinon.stub(), + }; + + const result = await getCompilerVersions({}, hre as any, runSuperStub as any); + expect(result).to.deep.equal(["0.8.0", "0.7.0", "0.6.0"]); + expect(runSuperStub.called).to.be.false; + }); +}); + +describe("verify", async function () { + + afterEach(() => { + sinon.restore(); + }); + + it("should call runSuper if zksync is false", async function () { + let runSuperStub = sinon.stub().resolves(0); + let hre = { + network: { + zksync: false, + verifyURL: "http://localhost:3000/verify", + }, + run: sinon.stub().resolves({}), + }; + + await verify({ + address: "0x1234567890", + constructorArgs: "", + contract: "", + constructorArgsParams: [], + libraries: "", + noCompile: false, + }, hre as any, runSuperStub as any); + + expect(runSuperStub.calledOnce).to.be.true; +}); + + it("should throw an error if address is undefined", async function () { + let libraries = sinon.stub(plugin, "getLibraries").resolves({}); + let runSuperStub = sinon.stub().resolves(0); + let hre = { + network: { + zksync: true, + verifyURL: "http://localhost:3000/verify", + }, + 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 () { + let libraries = sinon.stub(plugin, "getLibraries").resolves({}); + let runSuperStub = sinon.stub().resolves(0); + let hre = { + network: { + zksync: true, + verifyURL: "http://localhost:3000/verify", + }, + run: sinon.stub().resolves({}), + }; + + await verify({ + address: "0x1234567890", + constructorArgs: "", + contract: "Contract", + constructorArgsParams: [], + libraries: "", + noCompile: false, + }, hre as any, runSuperStub as any); + + expect(runSuperStub.calledOnce).to.be.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, + }); + }); +}); +describe("getConstructorArguments", async function () { + afterEach(() => { + sinon.restore(); + }); + + it("should call runSuper if zksync is false", async function () { + const args = {}; + const hre = { + network: { + zksync: false, + }, + run: sinon.stub().resolves(), + }; + const runSuperStub = sinon.stub().resolves(); + + await getConstructorArguments(args, hre as any, runSuperStub as any); + + expect(runSuperStub.calledOnce).to.be.true; + }); + + it("should return constructorArgsParams if constructorArgsModule is not a string", async function () { + const args = { + constructorArgsModule: 123, + constructorArgsParams: [1, 2, 3], + }; + const hre = { + network: { + zksync: true, + }, + }; + const runSuperStub = sinon.stub().resolves(); + + const result = await getConstructorArguments(args, hre as any, runSuperStub as any); + + expect(result).to.deep.equal([1, 2, 3]); + }); + + it("should import constructorArgsModule and return constructorArguments if zksync is true and constructorArgsModule is a string", async function () { + const args = { + constructorArgsModule: "path/to/module", + }; + const hre = { + network: { + zksync: true, + }, + }; + const runSuperStub = sinon.stub().resolves(); + const importStub = sinon.stub(utils, 'extractModule').resolves("0x1234567890"); + + const result = await getConstructorArguments(args, hre as any, runSuperStub as any); + + expect(importStub.calledOnce).to.be.true; + expect(importStub.firstCall.args[0]).to.includes("path/to/module"); + expect(result).to.deep.equal('0x1234567890'); + }); + + it("should throw an error if importing constructorArgsModule fails", async function () { + const args = { + constructorArgsModule: "path/to/module", + }; + const hre = { + network: { + zksync: true, + }, + }; + + const runSuperStub = sinon.stub().resolves(); + const importStub = sinon.stub(utils, 'extractModule').throws(new Error("Import error")); + + try { + await getConstructorArguments(args, hre as any, runSuperStub as any); + fail("Expected an error to be thrown"); + } catch (error: any) { + expect(error.message).to.includes("Importing the module for the constructor arguments list failed."); + } + }); +}); + +describe("getContractInfo", async function () { + afterEach(() => { + sinon.restore(); + }); + + it("should call runSuper if zksync is false", async function () { + const args = { + contractFQN: "Contract", + deployedBytecode: "0x1234567890", + matchingCompilerVersions: [], + libraries: {}, + }; + const hre = { + network: { + zksync: false, + }, + }; + const runSuperStub = sinon.stub().resolves({}); + + await getContractInfo(args, hre as any, runSuperStub as any); + + expect(runSuperStub.calledOnce).to.be.true; + expect(runSuperStub.firstCall.args[0]).to.deep.equal(args); + }); + + it("should throw an error if contractFQN is undefined", async function () { + const args = { + contractFQN: undefined, + deployedBytecode: "0x1234567890", + matchingCompilerVersions: [], + libraries: {}, + }; + const hre = { + artifacts: sinon.stub().resolves({}), + network: { + zksync: true, + }, + }; + const runSuperStub = sinon.stub().resolves({}); + const inferContractArtifactsStub = sinon.stub(plugin, 'inferContractArtifacts').throws(new Error("contractFQN is undefined")); + + try { + await getContractInfo(args, hre as any, runSuperStub as any); + fail("Expected an error to be thrown"); + } catch (error: any) { + expect(error.message).to.equal('contractFQN is undefined'); + } + }); + + it("should throw an error if no matching contract is found", async function () { + const args = { + contractFQN: "contracts/Contract2.sol:Contract2", + deployedBytecode: "0x1234567890", + matchingCompilerVersions: [], + libraries: {}, + }; + const hre = { + artifacts: { + getAllFullyQualifiedNames : sinon + .stub() + .resolves(["contracts/Contract.sol:Contract"]), + getBuildInfo: sinon.stub().resolves({ + output: { + contracts: { + "contracts/Contract.sol": { + Contract: { + evm: { + bytecode: { + object: "0x1234567890", + }, + }, + }, + }, + }, + }, + solcVersion: "0.8.0", + }), + }, + network: { + zksync: true, + }, + }; + const runSuperStub = sinon.stub().resolves({}); + const extractMatchingContractInformationStub = sinon.stub(bytecode, 'extractMatchingContractInformation').resolves(); + + try { + await getContractInfo(args, hre as any, runSuperStub as any); + fail("Expected an error to be thrown"); + } catch (error: any) { + expect(error.message).to.equal(NO_MATCHING_CONTRACT); + } + }); + + it("should return contract information if contractFQN is defined and matching contract is found", async function () { + const args = { + contractFQN: "contracts/Contract.sol:Contract", + deployedBytecode: "0x1234567890", + matchingCompilerVersions: [], + libraries: {}, + }; + const hre = { + artifacts: { + getAllFullyQualifiedNames : sinon + .stub() + .resolves(["contracts/Contract.sol:Contract"]), + getBuildInfo: sinon.stub().resolves({ + output: { + contracts: { + "contracts/Contract.sol": { + Contract: { + evm: { + bytecode: { + object: "0x1234567890", + }, + }, + }, + }, + }, + }, + solcVersion: "0.8.0", + }), + }, + network: { + zksync: true, + }, + }; + + const contractInformation = { + 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: "v0.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", + }; + + const runSuperStub = sinon.stub().resolves({}); + const extractMatchingContractInformationStub = sinon.stub(bytecode, 'extractMatchingContractInformation').resolves(contractInformation); + + const result = await getContractInfo(args, hre as any, runSuperStub as any); + + expect(result).to.deep.equal(contractInformation); + expect(runSuperStub.called).to.be.false; + expect(extractMatchingContractInformationStub.calledOnce).to.be.true; + expect(extractMatchingContractInformationStub.firstCall.args[0]).to.equal("contracts/Contract.sol"); + expect(extractMatchingContractInformationStub.firstCall.args[1]).to.equal("Contract"); + expect(extractMatchingContractInformationStub.firstCall.args[3]).to.equal(args.deployedBytecode); + }); + + it("should return contract information if contractFQN is undefined and matching contract is found", async function () { + const args = { + contractFQN: undefined, + deployedBytecode: "0x1234567890", + matchingCompilerVersions: [], + libraries: {}, + }; + + const hre = { + artifacts: { + getAllFullyQualifiedNames : sinon + .stub() + .resolves(["contracts/Contract.sol:Contract"]), + getBuildInfo: sinon.stub().resolves({ + output: { + contracts: { + "contracts/Contract.sol": { + Contract: { + evm: { + bytecode: { + object: "0x1234567890", + }, + }, + }, + }, + }, + }, + solcVersion: "0.8.0", + }), + }, + network: { + zksync: true, + }, + }; + + let contractInformation = { + 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", + }; + const runSuperStub = sinon.stub().resolves({}); + const inferContractArtifactsStub = sinon.stub(plugin, 'inferContractArtifacts').resolves(contractInformation); + + const result = await getContractInfo(args, hre as any, runSuperStub as any); + + expect(result).to.deep.equal(contractInformation); + expect(runSuperStub.called).to.be.false; + expect(hre.artifacts.getBuildInfo.called).to.be.false; + expect(inferContractArtifactsStub.calledOnce).to.be.true; + expect(inferContractArtifactsStub.firstCall.args[0]).to.equal(hre.artifacts); + expect(inferContractArtifactsStub.firstCall.args[1]).to.equal(args.matchingCompilerVersions); + expect(inferContractArtifactsStub.firstCall.args[2]).to.equal(args.deployedBytecode); + }); +}); \ No newline at end of file diff --git a/packages/hardhat-zksync-verify/test/tests/tests.ts b/packages/hardhat-zksync-verify/test/tests/tests.ts new file mode 100644 index 000000000..5644bd8cd --- /dev/null +++ b/packages/hardhat-zksync-verify/test/tests/tests.ts @@ -0,0 +1,25 @@ +import { assert } from "chai"; +import { useEnvironment } from "../helpers"; + +describe("verify plugin", async function () { + const sourceName: string = "contracts/Greeter.sol"; + const contractName: string = "Greeter"; + 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 new file mode 100644 index 000000000..7fc80fccc --- /dev/null +++ b/packages/hardhat-zksync-verify/test/tests/utils.test.ts @@ -0,0 +1,296 @@ +import { expect } from "chai"; +import sinon from "sinon"; +import { + delay, + encodeArguments, + executeVeificationWithRetry, + handleAxiosError, + parseWrongConstructorArgumentsError, + removeMultipleSubstringOccurrences, +} from "../../src/utils"; +import * as service from "../../src/zksync-block-explorer/service"; +import axion from "axios"; + +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.be.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.be.true; + }); + + 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.be.undefined; + expect(checkVerificationStatusServiceStub.callCount).to.equal( + maxRetries + 1, + ); + expect(checkVerificationStatusServiceStub.calledWith(requestId, verifyURL)) + .to.be.true; + }); +}); + +describe("handleAxiosError", () => { + beforeEach(() => { + sinon.restore(); + }); + + it("should throw an error with the Axios error details", () => { + const error = { + code: "SOME_CODE", + response: { + data: "Some error message", + }, + }; + + let axionStub = sinon.stub(axion, "isAxiosError").returns(true); + + expect(() => handleAxiosError(error)).to.throw( + `Axios error (code: SOME_CODE) during the contract verification request\n Reason: ${error.response?.data}`, + ); + }); + + it("should throw a ZkSyncVerifyPluginError with the error message", () => { + const error = "Some error message"; + + expect(() => handleAxiosError(error)).to.throw( + `Failed to send contract verification request\n Reason: ${error}`, + ); + }); +}); + +describe("delay", () => { + it("should delay for the specified amount of time", async () => { + const ms = 1000; + const startTime = Date.now(); + await delay(ms); + const endTime = Date.now(); + const elapsedTime = endTime - startTime; + expect(elapsedTime).to.be.at.least(ms); + }); +}); + +describe("encodeArguments", () => { + it("should encode constructor arguments correctly", async () => { + const abi = [ + { + type: "constructor", + inputs: [ + { type: "string", name: "name" }, + { type: "uint256", name: "age" }, + ], + }, + ]; + const constructorArgs = ["John Doe", 25]; + const encodedData = + "0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001900000000000000000000000000000000000000000000000000000000000000084a6f686e20446f65000000000000000000000000000000000000000000000000"; + + const result = await encodeArguments(abi, constructorArgs); + + expect(result).to.equal(encodedData); + }); + + it("should throw an error when constructor arguments are incorrect", async () => { + const abi = [ + { + type: "constructor", + inputs: [ + { type: "string", name: "name" }, + { type: "uint256", name: "age" }, + ], + }, + ]; + const constructorArgs = ["John Doe", "25", "43"]; + + try { + await encodeArguments(abi, constructorArgs); + // Fail the test if no error is thrown + expect.fail("Expected an error to be thrown"); + } catch (error: any) { + expect(error.message).to.equal( + "The number of constructor arguments you provided (3) does not match the number of constructor arguments the contract has been deployed with (2).", + ); + } + }); +}); + +/*describe("retrieveContractBytecode", () => { + let providerSendStub: sinon.SinonStub; + + beforeEach(() => { + sinon.restore(); + }); + + it("should return the deployed bytecode when the address has bytecode", async () => { + const address = "0x1234567890abcdef"; + const hreNetwork = { + config: { + url: "https://example.com", + }, + name: "test-network", + }; + const bytecodeString = "0xabcdef1234567890"; + const providerResponse = `0x${bytecodeString}`; + + providerSendStub = sinon.stub().resolves(providerResponse); + sinon.stub(zk, "Provider").resolves({ + send: providerSendStub, + }); + + const result = await retrieveContractBytecode(address, hreNetwork); + + expect(result).to.equal(bytecodeString); + expect(providerSendStub.calledOnceWith("eth_getCode", [address, "latest"])).to.be.true; + }); + + it("should throw an error when the address has no bytecode", async () => { + const address = "0x1234567890abcdef"; + const hreNetwork = { + config: { + url: "https://example.com", + }, + name: "test-network", + }; + const bytecodeString = ""; + + providerSendStub = sinon.stub().resolves(bytecodeString); + sinon.stub(zk, "Provider").resolves({ + send: providerSendStub, + }); + + try { + await retrieveContractBytecode(address, hreNetwork); + // Fail the test if no error is thrown + expect.fail("Expected an error to be thrown"); + } catch (error: any) { + expect(error.message).to.equal( + `The address ${address} has no bytecode. Is the contract deployed to this network?\n The selected network is ${hreNetwork.name}.` + ); + } + }); +});*/ + +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]"; + const expectedOutput = + "The number of constructor arguments you provided (undefined) does not match the number of constructor arguments the contract has been deployed with (undefined)."; + + const result = parseWrongConstructorArgumentsError(inputString); + + expect(result).to.equal(expectedOutput); + }); +}); 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 new file mode 100644 index 000000000..8a4487c2b --- /dev/null +++ b/packages/hardhat-zksync-verify/test/tests/zksync-block-explorer/service.test.ts @@ -0,0 +1,166 @@ +import { expect } from "chai"; +import sinon 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 { ZkSyncVerifyPluginError } from "../../../src/errors"; +import { handleAxiosError } from "../../../src/utils"; + +describe("ZkSyncBlockExplorer Service", () => { + describe("checkVerificationStatusService", () => { + afterEach(() => { + sinon.restore(); + }); + + it("should return the verification status response", async () => { + const requestId = 123; + const verifyURL = "https://example.com/verify"; + + const response = { + status: 200, + data: { + status: "successful", + message: "Verification successful", + error: undefined, + compilationErrors: undefined, + }, + }; + + sinon.stub(axios, "get").resolves(response); + + const result = await checkVerificationStatusService(requestId, verifyURL); + + expect(result).to.be.instanceOf(VerificationStatusResponse); + expect(result.status).to.equal(response.data.status); + expect(result.isVerificationSuccess()).to.be.true; + }); + + it("should handle axios error", async () => { + const requestId = 123; + const verifyURL = "https://example.com/verify"; + + const error = new Error("Network error"); + + sinon.stub(axios, "get").rejects(error); + + try { + await checkVerificationStatusService(requestId, verifyURL); + } catch (error: any) { + expect(error.message).to.equal(error.message); + } + }); + }); + + describe("verifyContractRequest", () => { + afterEach(() => { + sinon.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 = { + status: 200, + data: "Verification successful", + }; + + 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); + }); + + 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", + }; + + sinon.stub(axios, "post").resolves(response); + + 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", + ); + } + }); + + 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 (error: any) { + expect(error.message).to.equal(error.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); + }); + }); +});