mirror of
https://github.com/luxdao/zodiac.git
synced 2026-10-30 20:34:38 +00:00
435 lines
11 KiB
TypeScript
435 lines
11 KiB
TypeScript
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
import path from "path";
|
|
|
|
import { getAddress } from "ethers";
|
|
|
|
import { CanonicalAddresses, KnownContracts } from "../src/contracts.js";
|
|
import {
|
|
checkVerificationStatus,
|
|
getCode as getExplorerCode,
|
|
getSourceCode,
|
|
verifySourceCode,
|
|
} from "../src/explorer.js";
|
|
import { defaultMastercopiesDir } from "../src/mastercopies.js";
|
|
import { NetworkConfig, networks } from "../src/networks.js";
|
|
import { BytecodeFile, SourceCodeFile } from "../src/types.js";
|
|
import {
|
|
CHECK,
|
|
CROSS,
|
|
QUESTION,
|
|
clearProgress,
|
|
color,
|
|
printStatusLine,
|
|
printTable,
|
|
renderProgress,
|
|
showTransient,
|
|
} from "../src/ui.js";
|
|
|
|
interface VerificationCell {
|
|
label: string;
|
|
}
|
|
|
|
interface VerifiableAsset {
|
|
name: string;
|
|
bytecode: BytecodeFile;
|
|
sourceCode: SourceCodeFile;
|
|
}
|
|
|
|
const knownContractNames = Object.values(KnownContracts);
|
|
|
|
/**
|
|
* CLI entry for:
|
|
*
|
|
* zodiac verify <name> [version]
|
|
* zodiac verify list <name> [version]
|
|
*/
|
|
export async function runVerify(args: string[]): Promise<void> {
|
|
const [subcommand, name, version] = args;
|
|
|
|
switch (subcommand) {
|
|
case "list": {
|
|
if (!name) {
|
|
throw new Error("Usage: zodiac verify list <name> [version]");
|
|
}
|
|
await listVerifications(name, version);
|
|
break;
|
|
}
|
|
case undefined:
|
|
throw new Error(
|
|
"Usage: zodiac verify <name> [version] | zodiac verify list <name> [version]"
|
|
);
|
|
default:
|
|
await verifyKnown(subcommand, name);
|
|
}
|
|
}
|
|
|
|
async function verifyKnown(name: string, version?: string): Promise<void> {
|
|
const contractName = resolveName(name);
|
|
const versions = CanonicalAddresses[contractName] || {};
|
|
const versionKeys = resolveVersions(contractName, versions, version);
|
|
|
|
const total = networks.length * versionKeys.length;
|
|
let completed = 0;
|
|
const rows: { network: string; cells: VerificationCell[] }[] = [];
|
|
|
|
renderProgress(completed, total, "Verifying contracts");
|
|
for (const network of networks) {
|
|
const cells: VerificationCell[] = [];
|
|
|
|
for (const version of versionKeys) {
|
|
const address = versions[version as keyof typeof versions];
|
|
const result = address
|
|
? await verifyTarget({
|
|
contractName,
|
|
version,
|
|
network,
|
|
address,
|
|
})
|
|
: await failedVerification({
|
|
contractName,
|
|
version,
|
|
network,
|
|
reason: "no address on record",
|
|
});
|
|
|
|
cells.push(result);
|
|
completed += 1;
|
|
renderProgress(completed, total, "Verifying contracts");
|
|
}
|
|
|
|
rows.push({ network: network.name, cells });
|
|
}
|
|
|
|
clearProgress();
|
|
printTable(["network", ...versionKeys], rows, [
|
|
`${color(CHECK, "green")} verified`,
|
|
`${color(CROSS, "red")} failed verification`,
|
|
]);
|
|
}
|
|
|
|
async function listVerifications(
|
|
name: string,
|
|
version?: string
|
|
): Promise<void> {
|
|
const contractName = resolveName(name);
|
|
const versions = CanonicalAddresses[contractName] || {};
|
|
const versionKeys = resolveVersions(contractName, versions, version);
|
|
|
|
const total = networks.length * versionKeys.length;
|
|
let completed = 0;
|
|
const rows: { network: string; cells: VerificationCell[] }[] = [];
|
|
|
|
renderProgress(completed, total, "Checking verifications");
|
|
for (const network of networks) {
|
|
const cells: VerificationCell[] = [];
|
|
|
|
for (const version of versionKeys) {
|
|
const address = versions[version as keyof typeof versions];
|
|
cells.push(
|
|
address
|
|
? await checkVerification({
|
|
contractName,
|
|
version,
|
|
network,
|
|
address,
|
|
})
|
|
: errorCell()
|
|
);
|
|
completed += 1;
|
|
renderProgress(completed, total, "Checking verifications");
|
|
}
|
|
|
|
rows.push({ network: network.name, cells });
|
|
}
|
|
|
|
clearProgress();
|
|
printTable(["network", ...versionKeys], rows, [
|
|
`${color(CHECK, "green")} verified`,
|
|
`${color(CROSS, "red")} not verified`,
|
|
`${color(QUESTION, "yellow")} couldn't get status`,
|
|
]);
|
|
}
|
|
|
|
function resolveName(name: string): KnownContracts {
|
|
const matched = knownContractNames.find(
|
|
(n) => n.toLowerCase() === name.toLowerCase()
|
|
);
|
|
if (!matched) {
|
|
throw new Error(
|
|
`Unknown contract "${name}". Known contracts: ${knownContractNames.join(
|
|
", "
|
|
)}`
|
|
);
|
|
}
|
|
return matched;
|
|
}
|
|
|
|
function resolveVersions(
|
|
contractName: KnownContracts,
|
|
versions: Record<string, string>,
|
|
version?: string
|
|
): string[] {
|
|
const keys = Object.keys(versions);
|
|
if (keys.length === 0) {
|
|
throw new Error(`No versions on record for ${contractName}.`);
|
|
}
|
|
|
|
if (version === undefined) return keys;
|
|
|
|
if (!keys.includes(version)) {
|
|
throw new Error(
|
|
`Unknown version "${version}" for ${contractName}. Known versions: ${keys.join(
|
|
", "
|
|
)}`
|
|
);
|
|
}
|
|
return [version];
|
|
}
|
|
|
|
async function checkVerification({
|
|
contractName,
|
|
version,
|
|
network,
|
|
address,
|
|
}: {
|
|
contractName: KnownContracts;
|
|
version: string;
|
|
network: NetworkConfig;
|
|
address: string;
|
|
}): Promise<VerificationCell> {
|
|
if (!network.etherscanApiUrl) return errorCell();
|
|
|
|
const assets = loadVerifiableAssets({ contractName, version, address });
|
|
if (assets.length === 0) return errorCell();
|
|
|
|
let sawError = false;
|
|
for (const asset of assets) {
|
|
const status = await assetVerificationStatus(network, asset);
|
|
if (status === "unverified") return unverifiedCell();
|
|
if (status === "unknown") sawError = true;
|
|
}
|
|
|
|
return sawError ? errorCell() : verifiedCell();
|
|
}
|
|
|
|
async function verifyTarget({
|
|
contractName,
|
|
version,
|
|
network,
|
|
address,
|
|
}: {
|
|
contractName: KnownContracts;
|
|
version: string;
|
|
network: NetworkConfig;
|
|
address: string;
|
|
}): Promise<VerificationCell> {
|
|
if (!network.etherscanApiUrl) {
|
|
return failedVerification({
|
|
contractName,
|
|
version,
|
|
network,
|
|
reason: "no supported explorer",
|
|
});
|
|
}
|
|
|
|
const assets = loadVerifiableAssets({ contractName, version, address });
|
|
if (assets.length === 0) {
|
|
return failedVerification({
|
|
contractName,
|
|
version,
|
|
network,
|
|
reason: "no local verification artifact",
|
|
});
|
|
}
|
|
|
|
try {
|
|
let submitted = false;
|
|
for (const asset of assets) {
|
|
const status = await assetVerificationStatus(network, asset);
|
|
if (status === "verified") continue;
|
|
if (status === "unknown") {
|
|
throw new Error(`${asset.name}: couldn't get status`);
|
|
}
|
|
|
|
const code = await getExplorerCode({
|
|
address: asset.bytecode.address,
|
|
network: network.name,
|
|
});
|
|
if (code === "0x") {
|
|
throw new Error(`${asset.name}: not deployed`);
|
|
}
|
|
|
|
submitted = true;
|
|
showTransient(
|
|
`Verifying ${contractName}@${version} on ${network.name}...`
|
|
);
|
|
await submitAndWaitForVerification(network, asset);
|
|
}
|
|
|
|
if (submitted) {
|
|
printStatusLine(
|
|
`${color(CHECK, "green")} ${contractName}@${version} verified on ${
|
|
network.name
|
|
}: ${address}`
|
|
);
|
|
}
|
|
return verifiedCell();
|
|
} catch (error) {
|
|
printStatusLine(
|
|
`${color(CROSS, "red")} ${contractName}@${version} failed on ${
|
|
network.name
|
|
}: ${verificationErrorMessage(error)}`
|
|
);
|
|
return failedCell();
|
|
}
|
|
}
|
|
|
|
async function assetVerificationStatus(
|
|
network: NetworkConfig,
|
|
asset: VerifiableAsset
|
|
): Promise<"verified" | "unverified" | "unknown"> {
|
|
try {
|
|
await getSourceCode({
|
|
address: asset.bytecode.address,
|
|
network: network.name,
|
|
});
|
|
return "verified";
|
|
} catch (error) {
|
|
return isUnverifiedError(error) ? "unverified" : "unknown";
|
|
}
|
|
}
|
|
|
|
async function submitAndWaitForVerification(
|
|
network: NetworkConfig,
|
|
asset: VerifiableAsset
|
|
): Promise<void> {
|
|
const sourceCode = asset.sourceCode;
|
|
const guid = await verifySourceCode({
|
|
address: asset.bytecode.address,
|
|
network: network.name,
|
|
sourceCode: JSON.stringify(sourceCode.input),
|
|
contractName: `${sourceCode.sourceName}:${sourceCode.contractName}`,
|
|
compilerVersion: sourceCode.compilerVersion,
|
|
constructorArguments: sourceCode.constructorArguments || "",
|
|
evmVersion: evmVersionFor(sourceCode),
|
|
});
|
|
if (guid.toLowerCase().includes("already verified")) return;
|
|
|
|
const result = await waitForVerification(network, guid);
|
|
if (!result.toLowerCase().includes("pass")) {
|
|
throw new Error(`${asset.name}: ${result}`);
|
|
}
|
|
}
|
|
|
|
async function waitForVerification(
|
|
network: NetworkConfig,
|
|
guid: string
|
|
): Promise<string> {
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
const result = await checkVerificationStatus({
|
|
network: network.name,
|
|
guid,
|
|
});
|
|
const lower = result.toLowerCase();
|
|
if (lower.includes("pending") || lower.includes("queued")) {
|
|
await new Promise((resolve) => setTimeout(resolve, 3_000));
|
|
continue;
|
|
}
|
|
return result;
|
|
}
|
|
return "verification timed out";
|
|
}
|
|
|
|
async function failedVerification({
|
|
contractName,
|
|
version,
|
|
network,
|
|
reason,
|
|
}: {
|
|
contractName: KnownContracts;
|
|
version: string;
|
|
network: NetworkConfig;
|
|
reason: string;
|
|
}): Promise<VerificationCell> {
|
|
printStatusLine(
|
|
`${color(CROSS, "red")} ${contractName}@${version} failed on ${
|
|
network.name
|
|
}: ${reason}`
|
|
);
|
|
return failedCell();
|
|
}
|
|
|
|
function verifiedCell(): VerificationCell {
|
|
return { label: color(CHECK, "green") };
|
|
}
|
|
|
|
function unverifiedCell(): VerificationCell {
|
|
return { label: color(CROSS, "red") };
|
|
}
|
|
|
|
function errorCell(): VerificationCell {
|
|
return { label: color(QUESTION, "yellow") };
|
|
}
|
|
|
|
function failedCell(): VerificationCell {
|
|
return { label: color(CROSS, "red") };
|
|
}
|
|
|
|
function loadVerifiableAssets({
|
|
contractName,
|
|
version,
|
|
address,
|
|
}: {
|
|
contractName: KnownContracts;
|
|
version: string;
|
|
address: string;
|
|
}): VerifiableAsset[] {
|
|
const dir = path.join(defaultMastercopiesDir(), contractName, version);
|
|
if (!existsSync(dir)) return [];
|
|
|
|
const assets = readdirSync(dir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => {
|
|
const bytecodePath = path.join(dir, entry.name, "bytecode.json");
|
|
const sourceCodePath = path.join(dir, entry.name, "sourcecode.json");
|
|
if (!existsSync(bytecodePath) || !existsSync(sourceCodePath)) {
|
|
return undefined;
|
|
}
|
|
|
|
const bytecode = JSON.parse(
|
|
readFileSync(bytecodePath, "utf8")
|
|
) as BytecodeFile;
|
|
const sourceCode = JSON.parse(
|
|
readFileSync(sourceCodePath, "utf8")
|
|
) as SourceCodeFile;
|
|
return { name: entry.name, bytecode, sourceCode };
|
|
})
|
|
.filter((asset): asset is VerifiableAsset => asset !== undefined);
|
|
|
|
const normalizedAddress = getAddress(address);
|
|
return assets.sort((a, b) => {
|
|
const aIsMain = getAddress(a.bytecode.address) === normalizedAddress;
|
|
const bIsMain = getAddress(b.bytecode.address) === normalizedAddress;
|
|
if (aIsMain === bIsMain) return a.name.localeCompare(b.name);
|
|
return aIsMain ? 1 : -1;
|
|
});
|
|
}
|
|
|
|
function evmVersionFor(sourceCode: SourceCodeFile): string {
|
|
const evmVersion = sourceCode.input?.settings?.evmVersion;
|
|
return evmVersion && evmVersion !== "Default" ? evmVersion : "default";
|
|
}
|
|
|
|
function isUnverifiedError(error: unknown): boolean {
|
|
const message = ((error as Error)?.message || String(error)).toLowerCase();
|
|
return (
|
|
message.includes("not verified") ||
|
|
message.includes("source code not verified") ||
|
|
message.includes("contract source code not verified")
|
|
);
|
|
}
|
|
|
|
function verificationErrorMessage(error: unknown): string {
|
|
return (error as Error)?.message || String(error);
|
|
}
|