import fs from "fs"; import path from "path"; import YAML from "yaml"; export type DeploymentEnvironment = "local" | "development" | "production"; export interface OpenZeppelinTargetConfig { environment: DeploymentEnvironment; openzeppelin?: { manifestDir?: unknown }; } const TARGET_ENVIRONMENTS = new Set([ "local", "development", "production", ]); const SAFE_MANIFEST_DIR = /^\.openzeppelin(?:\.[a-z0-9][a-z0-9-]*)?$/; const assertConfinedManifestDirectory = (root: string, manifestDir: string): void => { const rootPath = fs.realpathSync(root); const candidatePath = path.resolve(root, manifestDir); if (path.dirname(candidatePath) !== path.resolve(root)) { throw new Error(`OpenZeppelin manifest directory escapes the project: ${manifestDir}`); } if (!fs.existsSync(candidatePath)) return; const realCandidate = fs.realpathSync(candidatePath); const relative = path.relative(rootPath, realCandidate); if (relative === "" || relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) { throw new Error(`OpenZeppelin manifest symlink resolves outside the project: ${manifestDir}`); } }; export function resolveOpenZeppelinManifestDir( root: string, network: string, target: OpenZeppelinTargetConfig, ): string { if (!TARGET_ENVIRONMENTS.has(target?.environment)) { throw new Error(`Network ${network} must declare a valid deployment environment`); } const manifestDir = target.openzeppelin?.manifestDir; if (typeof manifestDir !== "string" || !SAFE_MANIFEST_DIR.test(manifestDir)) { throw new Error(`Network ${network} must declare one safe OpenZeppelin manifest directory segment`); } if (target.environment === "production" && manifestDir !== ".openzeppelin") { throw new Error(`Production network ${network} must use exactly .openzeppelin`); } if (target.environment !== "production" && manifestDir === ".openzeppelin") { throw new Error(`Non-production network ${network} must not use .openzeppelin`); } assertConfinedManifestDirectory(root, manifestDir); return manifestDir; } export function loadOpenZeppelinManifestDir(root: string, network: string): string { const configPath = path.join(root, "config.yaml"); const config = YAML.parse(fs.readFileSync(configPath, "utf8"), { merge: true }) as unknown; if (!config || typeof config !== "object" || Array.isArray(config)) { throw new Error("config.yaml must define network keys at the top level"); } const target = (config as Record)[network]; if (!target || typeof target !== "object" || Array.isArray(target) || network.startsWith("_")) { throw new Error(`Missing config.yaml entry for network "${network}"`); } return resolveOpenZeppelinManifestDir( root, network, target as OpenZeppelinTargetConfig, ); } export function resolveEffectiveManifestTarget(input: { network: string; mode?: string; target?: string; }): string { const network = input.network.trim(); const target = input.target?.trim(); if (!network) throw new Error("Hardhat network is required to resolve the OpenZeppelin manifest"); if (input.mode === "simulate" && network === "hardhat" && !target) { throw new Error("Target is required for a Hardhat fork simulation"); } if (target && network !== "hardhat" && target !== network) { throw new Error(`Manifest target ${target} conflicts with Hardhat network ${network}`); } return target || network; } export function openZeppelinProcessEnvironment(input: { root: string; network: string; mode?: string; target?: string; env?: NodeJS.ProcessEnv; }): NodeJS.ProcessEnv { const target = resolveEffectiveManifestTarget(input); const manifestDir = loadOpenZeppelinManifestDir(input.root, target); const env = input.env ?? process.env; if (env.GATE_WORKFLOW_TARGET && env.GATE_WORKFLOW_TARGET !== target) { throw new Error(`GATE_WORKFLOW_TARGET conflict: expected ${target}, received ${env.GATE_WORKFLOW_TARGET}`); } if (env.MANIFEST_DEFAULT_DIR && env.MANIFEST_DEFAULT_DIR !== manifestDir) { throw new Error(`MANIFEST_DEFAULT_DIR conflict: expected ${manifestDir}, received ${env.MANIFEST_DEFAULT_DIR}`); } return { ...env, GATE_WORKFLOW_TARGET: target, MANIFEST_DEFAULT_DIR: manifestDir, }; } const argvValue = (argv: string[], name: string): string | undefined => { for (let index = 0; index < argv.length; index += 1) { const value = argv[index]; if (value === name) return argv[index + 1]; if (value.startsWith(`${name}=`)) return value.slice(name.length + 1); } return undefined; }; export function configureOpenZeppelinManifestEnvironment(input: { root: string; target?: string; argv?: string[]; env?: NodeJS.ProcessEnv; }): string { const env = input.env ?? process.env; const network = argvValue(input.argv ?? process.argv, "--network")?.trim() || input.target?.trim() || "hardhat"; const next = openZeppelinProcessEnvironment({ root: input.root, network, target: input.target, env, }); env.GATE_WORKFLOW_TARGET = next.GATE_WORKFLOW_TARGET; env.MANIFEST_DEFAULT_DIR = next.MANIFEST_DEFAULT_DIR; return next.MANIFEST_DEFAULT_DIR!; }