Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --sourcify to hardhat ignition verify and deploy #785

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/core/src/types/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ export interface VerifyInfo {
sourceCode: string;
name: string;
args: string;
metadata?: any;
}

export type VerifyStatus = { type: "success"; contractURL: string };

/**
* The result of requesting the verification info for a deployment.
* It returns the chainConfig followed by an array of VerifyInfo objects, one for each contract to be verified.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ async function convertExStateToVerifyInfo(
sourceCode: JSON.stringify(sourceCode),
name: `${artifact.sourceName}:${contractName}`,
args: encodeDeploymentArguments(artifact, constructorArgs),
metadata: buildInfo
};

return verifyInfo;
Expand Down
121 changes: 56 additions & 65 deletions packages/hardhat-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import "@nomicfoundation/hardhat-verify";
import { Etherscan } from "@nomicfoundation/hardhat-verify/etherscan";
import {
DeploymentParameters,
IgnitionError,
Expand All @@ -20,10 +19,12 @@ import path from "path";
import "./type-extensions";
import { calculateDeploymentStatusDisplay } from "./ui/helpers/calculate-deployment-status-display";
import { bigintReviver } from "./utils/bigintReviver";
import { getApiKeyAndUrls } from "./utils/getApiKeyAndUrls";
import { resolveDeploymentId } from "./utils/resolve-deployment-id";
import { shouldBeHardhatPluginError } from "./utils/shouldBeHardhatPluginError";
import { verifyEtherscanContract } from "./utils/verifyEtherscanContract";
import {
verifyContract,
checkVerifierHardhatConfig,
} from "./utils/verifyContract";

/* ignition config defaults */
const IGNITION_DIR = "ignition";
Expand Down Expand Up @@ -92,6 +93,8 @@ ignitionScope
.addOptionalParam("strategy", "Set the deployment strategy to use", "basic")
.addFlag("reset", "Wipes the existing deployment state before deploying")
.addFlag("verify", "Verify the deployment on Etherscan")
.addFlag("sourcify", "Verify the deployment on Sourcify")
.addFlag("etherscan", "Verify the deployment on Etherscan")
.setDescription("Deploy a module to the specified network")
.setAction(
async (
Expand All @@ -103,6 +106,8 @@ ignitionScope
reset,
verify,
strategy: strategyName,
sourcify,
etherscan,
}: {
modulePath: string;
parameters?: string;
Expand All @@ -111,10 +116,11 @@ ignitionScope
reset: boolean;
verify: boolean;
strategy: string;
sourcify: boolean;
etherscan: boolean;
},
hre
) => {
const { default: chalk } = await import("chalk");
const { default: Prompt } = await import("prompts");
const { deploy } = await import("@nomicfoundation/ignition-core");

Expand All @@ -124,18 +130,14 @@ ignitionScope
const { loadModule } = await import("./utils/load-module");
const { PrettyEventHandler } = await import("./ui/pretty-event-handler");

// Backward compatible --verfify defaults to both etherscan & sourcify
if (verify) {
if (
hre.config.etherscan === undefined ||
hre.config.etherscan.apiKey === undefined ||
hre.config.etherscan.apiKey === ""
) {
throw new NomicLabsHardhatPluginError(
"@nomicfoundation/hardhat-ignition",
"No etherscan API key configured"
);
}
etherscan = true;
sourcify = true;
}
verify = etherscan || sourcify;

checkVerifierHardhatConfig(hre, { sourcify, etherscan });

const chainId = Number(
await hre.network.provider.request({
Expand Down Expand Up @@ -347,13 +349,9 @@ ignitionScope
} catch {}

if (result.type === "SUCCESSFUL_DEPLOYMENT" && verify) {
console.log("");
console.log(chalk.bold("Verifying deployed contracts"));
console.log("");

await hre.run(
{ scope: "ignition", task: "verify" },
{ deploymentId }
{ deploymentId, sourcify, etherscan }
);
}

Expand Down Expand Up @@ -549,6 +547,13 @@ ignitionScope
"includeUnrelatedContracts",
"Include all compiled contracts in the verification"
)
.addOptionalParam(
"service",
"Service to use: etherscan, sourcify",
"etherscan"
)
.addFlag("sourcify", "Use the Sourcify contract verification service")
.addFlag("etherscan", "Use the Etherscan contract verification service")
.addPositionalParam("deploymentId", "The id of the deployment to verify")
.setDescription(
"Verify contracts from a deployment against the configured block explorers"
Expand All @@ -558,29 +563,39 @@ ignitionScope
{
deploymentId,
includeUnrelatedContracts = false,
}: { deploymentId: string; includeUnrelatedContracts: boolean },
sourcify,
etherscan,
}: {
deploymentId: string;
includeUnrelatedContracts: boolean;
sourcify: boolean;
etherscan: boolean;
},
hre
) => {
if (!etherscan && !sourcify) {
throw new NomicLabsHardhatPluginError(
"hardhat-ignition",
`No verification service selected. Supported services are: etherscan, sourcify`
);
}

const { getVerificationInformation } = await import(
"@nomicfoundation/ignition-core"
);
const { default: chalk } = await import("chalk");

checkVerifierHardhatConfig(hre, { sourcify, etherscan });

const deploymentDir = path.join(
hre.config.paths.ignition,
"deployments",
deploymentId
);

if (
hre.config.etherscan === undefined ||
hre.config.etherscan.apiKey === undefined ||
hre.config.etherscan.apiKey === ""
) {
throw new NomicLabsHardhatPluginError(
"@nomicfoundation/hardhat-ignition",
"No etherscan API key configured"
);
}
console.log("");
console.log(chalk.bold("Verifying deployed contracts..."));
console.log("");

try {
for await (const [
Expand All @@ -591,45 +606,21 @@ ignitionScope
hre.config.etherscan.customChains,
includeUnrelatedContracts
)) {
const apiKeyAndUrls = getApiKeyAndUrls(
hre.config.etherscan.apiKey,
chainConfig
const result = await verifyContract(
hre,
chainConfig,
{ sourcify, etherscan },
contractInfo
);

const instance = new Etherscan(...apiKeyAndUrls);

console.log(
`Verifying contract "${contractInfo.name}" for network ${chainConfig.network}...`
);

const result = await verifyEtherscanContract(instance, contractInfo);
if (result.etherscan !== undefined) {
console.log(` ${result.etherscan.contractURL}`);
console.log("");
}

if (result.type === "success") {
console.log(
`Successfully verified contract "${contractInfo.name}" for network ${chainConfig.network}:\n - ${result.contractURL}`
);
if (result.sourcify !== undefined) {
console.log(` ${result.sourcify.contractURL}`);
console.log("");
} else {
if (/already verified/gi.test(result.reason.message)) {
const contractURL = instance.getContractUrl(contractInfo.address);
console.log(
`Contract ${contractInfo.name} already verified on network ${chainConfig.network}:\n - ${contractURL}`
);
console.log("");
continue;
} else {
if (!includeUnrelatedContracts) {
throw new NomicLabsHardhatPluginError(
"hardhat-ignition",
`Verification failed. Please run \`hardhat ignition verify ${deploymentId} --include-unrelated-contracts\` to attempt verifying all contracts.`
);
} else {
throw new NomicLabsHardhatPluginError(
"hardhat-ignition",
result.reason.message
);
}
}
}
}
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ChainConfig } from "@nomicfoundation/hardhat-verify/types";

import { NomicLabsHardhatPluginError } from "hardhat/plugins";

export function getApiKeyAndUrls(
export function getEtherscanApiKeyAndUrls(
etherscanApiKey: string | Record<string, string>,
chainConfig: ChainConfig
): [apiKey: string, apiUrl: string, webUrl: string] {
Expand Down
151 changes: 151 additions & 0 deletions packages/hardhat-plugin/src/utils/verifyContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type {
ChainConfig,
VerifyInfo,
VerifyStatus,
} from "@nomicfoundation/ignition-core";

import { Etherscan } from "@nomicfoundation/hardhat-verify/etherscan";
import { Sourcify } from "@nomicfoundation/hardhat-verify/sourcify";
import { NomicLabsHardhatPluginError } from "hardhat/plugins";
import { HardhatRuntimeEnvironment } from "hardhat/types";

import { getEtherscanApiKeyAndUrls } from "./getEtherscanApiKeyAndUrls";

const DEFAULT_SOURCIFY_API_URL = "https://sourcify.dev/server";
const DEFAULT_SOURCIFY_BROWSER_URL = "https://repo.sourcify.dev";

export class VerificationError extends Error {}

interface Verifiers {
sourcify: boolean;
etherscan: boolean;
}

function verifySourcifyHardhatConfig(hre: HardhatRuntimeEnvironment) {
if (
hre.config.sourcify !== undefined &&
(hre.config.sourcify.apiUrl === "" || hre.config.sourcify.browserUrl === "")
) {
throw new NomicLabsHardhatPluginError(
"@nomicfoundation/hardhat-ignition",
"Sourcify is disabled or has invalid API & Browser URLs"
);
}
}

async function verifySourcifyContract(
instance: Sourcify,
info: VerifyInfo
): Promise<VerifyStatus> {
const verificationArgs = { "metadata.json": JSON.stringify(info.metadata) };
const verificationResult = await instance.verify(
info.address,
verificationArgs
);

const verificationStatus = await instance.isVerified(info.address);

if (verificationStatus !== false) {
const contractURL = instance.getContractUrl(
info.address,
verificationStatus
);
return { type: "success", contractURL };
} else {
// todo: what case would cause verification status not to succeed without throwing?
throw new VerificationError(verificationResult.error);
}
}

function verifyEtherscanHardhatConfig(hre: HardhatRuntimeEnvironment) {
if (
hre.config.etherscan === undefined ||
hre.config.etherscan.apiKey === undefined ||
hre.config.etherscan.apiKey === ""
) {
throw new NomicLabsHardhatPluginError(
"@nomicfoundation/hardhat-ignition",
"No etherscan API key configured"
);
}
}

export function checkVerifierHardhatConfig(
hre: HardhatRuntimeEnvironment,
verifiers: Verifiers
) {
if (verifiers.etherscan) verifyEtherscanHardhatConfig(hre);
if (verifiers.sourcify) verifySourcifyHardhatConfig(hre);
}

export async function verifyEtherscanContract(
instance: Etherscan,
{ address, compilerVersion, sourceCode, name, args }: VerifyInfo
): Promise<VerifyStatus> {
const contractURL = instance.getContractUrl(address);

try {
const { message: guid } = await instance.verify(
address,
sourceCode,
name,
compilerVersion,
args
);

const verificationStatus = await instance.getVerificationStatus(guid);

if (verificationStatus.isSuccess()) {
return { type: "success", contractURL };
} else {
throw new VerificationError(verificationStatus.message);
}
} catch (e) {
// Let this pass through as equivalent to a successful verification
if (e instanceof Error && /already verified/gi.test(e.message)) {
return { type: "success", contractURL };
} else {
throw e;
}
}
}

function sourcifyInstance(
hre: HardhatRuntimeEnvironment,
chainConfig: ChainConfig
) {
verifySourcifyHardhatConfig(hre);
return new Sourcify(
chainConfig.chainId,
hre.config.sourcify.apiUrl ?? DEFAULT_SOURCIFY_API_URL,
hre.config.sourcify.browserUrl ?? DEFAULT_SOURCIFY_BROWSER_URL
);
}

function etherscanInstance(
hre: HardhatRuntimeEnvironment,
chainConfig: ChainConfig
) {
verifyEtherscanHardhatConfig(hre);
const apiKeyAndUrls = getEtherscanApiKeyAndUrls(
hre.config.etherscan.apiKey,
chainConfig
);
return new Etherscan(...apiKeyAndUrls);
}

export async function verifyContract(
hre: HardhatRuntimeEnvironment,
chainConfig: ChainConfig,
verifiers: Verifiers,
info: VerifyInfo
) {
return {
etherscan: verifiers.etherscan
? await verifyEtherscanContract(etherscanInstance(hre, chainConfig), info)
: undefined,
sourcify: verifiers.sourcify
? await verifySourcifyContract(sourcifyInstance(hre, chainConfig), info)
: undefined,
};
}
Loading