import { spawnSync } from "child_process"; import fs from "fs"; import path from "path"; import { JsonRpcProvider, Provider } from "ethers"; import { loadConfig, readJsonFile, writeJsonFile } from "./lib/config"; import { importMigrationState, migrationConfirmationToken, MigrationContractPlan, resolveConfinedMigrationPath, resolveGateToolCandidateCheck, validateMigrationStatePlan, } from "./lib/migrationState"; import { getERC1967Admin, getERC1967Implementation, } from "./lib/erc1967"; type ParsedArgs = Record; const parseArgs = (argv: string[]): ParsedArgs => { const parsed: ParsedArgs = {}; for (let index = 0; index < argv.length; index += 1) { const item = argv[index]; if (!item.startsWith("--")) throw new Error(`Unsupported argument: ${item}`); const equalIndex = item.indexOf("="); const key = item.slice(2, equalIndex > 2 ? equalIndex : undefined); const inlineValue = equalIndex > 2 ? item.slice(equalIndex + 1) : undefined; const next = argv[index + 1]; const value = inlineValue ?? ( next && !next.startsWith("--") ? next : "true" ); if (inlineValue === undefined && next && !next.startsWith("--")) index += 1; (parsed[key] ||= []).push(value); } return parsed; }; const singleArg = ( args: ParsedArgs, key: string, options: { required?: boolean } = {}, ): string | undefined => { const values = args[key] || []; if (values.length > 1) throw new Error(`--${key} may be provided only once`); const value = values[0]?.trim(); if (options.required && (!value || value === "true")) { throw new Error(`--${key} is required`); } return value && value !== "true" ? value : undefined; }; const confinedPlanPath = (root: string, requested: string): string => { return resolveConfinedMigrationPath(root, requested, { label: "Migration plan", requireFile: true, }); }; const commandName = (command: string): string => ( process.platform === "win32" ? `${command}.cmd` : command ); const main = async (): Promise => { const args = parseArgs(process.argv.slice(2)); const root = path.resolve(singleArg(args, "root") || process.cwd()); const planPath = confinedPlanPath( root, singleArg(args, "plan", { required: true }) as string, ); const plan = validateMigrationStatePlan(readJsonFile(planPath)); const confirmations = Object.keys(plan.networks).sort().map((network) => ({ network, token: migrationConfirmationToken(plan, network), })); if (args["print-confirmations"]?.length) { console.log(JSON.stringify({ migrationId: plan.migrationId, confirmations }, null, 2)); return; } process.env.GATE_MIGRATION_READ_ONLY = "1"; const { artifacts, config: hardhatConfig, ethers: hardhatEthers, network: hardhatNetwork, run: hardhatRun, } = await import("hardhat"); const workflowConfig = loadConfig(root); const rpcProviders = new Map(); const providerFor = ( networkName: string, ): Pick => { if (networkName === hardhatNetwork.name) return hardhatEthers.provider; const existing = rpcProviders.get(networkName); if (existing) return existing; const networkConfig = hardhatConfig.networks[networkName]; if (!networkConfig || !("url" in networkConfig) || typeof networkConfig.url !== "string") { throw new Error(`Hardhat network ${networkName} does not define an HTTP RPC URL`); } const provider = new JsonRpcProvider(networkConfig.url); rpcProviders.set(networkName, provider); return provider; }; const inspect = async ( networkName: string, contract: MigrationContractPlan, ) => { const provider = providerFor(networkName); const chainId = Number((await provider.getNetwork()).chainId); const implementationAddress = await getERC1967Implementation(provider, contract.address); const adminAddress = implementationAddress ? await getERC1967Admin(provider, contract.address) : undefined; const proxyKind = implementationAddress ? (adminAddress ? "transparent" as const : "uups" as const) : "none" as const; const contractCode = await provider.getCode(contract.address); const implementationCode = implementationAddress ? await provider.getCode(implementationAddress) : undefined; let artifactExists = true; try { await artifacts.readArtifact(contract.actualContract); } catch { artifactExists = false; } return { chainId, hasCode: contractCode !== "0x" && (implementationCode === undefined || implementationCode !== "0x"), artifactExists, proxyKind, ...(implementationAddress ? { implementationAddress } : {}), address: contract.address, }; }; await hardhatRun("compile", { quiet: true }); try { const evidence = await importMigrationState({ root, plan, targets: workflowConfig, confirmations: args.confirm || [], inspect, verifyCandidate: async (contractInfo) => { const checkRoot = fs.mkdtempSync(path.join(root, ".gate-migration-check-")); const candidatePath = path.join(checkRoot, "contractInfo.json"); const reportPath = path.join(checkRoot, "bytecode-check.json"); try { writeJsonFile(candidatePath, contractInfo); const result = spawnSync(commandName("npx"), [ "--no-install", "gate-tool", "check", "--config", candidatePath, "--output", reportPath, ], { cwd: root, env: process.env, stdio: "inherit", }); return resolveGateToolCandidateCheck(result, reportPath); } finally { fs.rmSync(checkRoot, { recursive: true, force: true }); } }, }); console.log(JSON.stringify({ status: evidence.status, migrationId: evidence.migrationId, planHash: evidence.planHash, evidencePath: `scripts/migrations/${evidence.migrationId}/state-import.json`, })); } finally { for (const provider of rpcProviders.values()) provider.destroy(); } }; void main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); console.error("Use --print-confirmations with the same --plan before importing state."); process.exitCode = 1; });