import fs from "fs"; import path from "path"; import YAML from "yaml"; import { getAddress, isAddress } from "ethers"; import { resolveOpenZeppelinManifestDir } from "../../config/openzeppelin"; const TARGET_ENVIRONMENTS = new Set(["local", "development", "production"]); export interface TargetConfig { environment: "local" | "development" | "production"; openzeppelin: { manifestDir: string; }; finality: { confirmations: number; }; admin?: string; create2?: { factory?: string; leadingZeroBytes?: number; caller?: string; }; uups?: { placeholderImplementation?: string; }; multicall?: { controlledMulticall?: string; }; [key: string]: unknown; } export type WorkflowConfig = Record; export const projectRoot = (): string => process.cwd(); export const readJsonFile = (filePath: string): T => { return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; }; export const readYamlFile = (filePath: string): T => { return YAML.parse(fs.readFileSync(filePath, "utf8"), { merge: true }) as T; }; export const writeJsonFile = (filePath: string, value: unknown): void => { fs.mkdirSync(path.dirname(filePath), { recursive: true }); const contents = `${JSON.stringify(value, null, 2)}\n`; const tempPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`; let descriptor: number | undefined; try { descriptor = fs.openSync(tempPath, "wx"); fs.writeFileSync(descriptor, contents); fs.fsyncSync(descriptor); fs.closeSync(descriptor); descriptor = undefined; fs.renameSync(tempPath, filePath); } catch (error) { if (descriptor !== undefined) { fs.closeSync(descriptor); descriptor = undefined; } if (fs.existsSync(tempPath)) { fs.unlinkSync(tempPath); } throw error; } }; export const loadConfig = (root = projectRoot()): WorkflowConfig => { const config = readYamlFile>(path.join(root, "config.yaml")); if (!config || typeof config !== "object" || Array.isArray(config)) { throw new Error("config.yaml must define network keys at the top level"); } const targets = Object.entries(config).filter(([key]) => !key.startsWith("_")); for (const [networkName, target] of targets) { if (!target || typeof target !== "object" || Array.isArray(target)) { throw new Error(`config.yaml entry for network "${networkName}" must be an object`); } const environment = (target as Record).environment; if ( typeof environment !== "string" || !TARGET_ENVIRONMENTS.has(environment) ) { throw new Error( `config.yaml entry for network "${networkName}" environment must be one of local, development, production`, ); } resolveOpenZeppelinManifestDir(root, networkName, target as TargetConfig); resolveFinalityConfirmations(target as TargetConfig, networkName); } return Object.fromEntries(targets) as WorkflowConfig; }; export const resolveFinalityConfirmations = ( target: TargetConfig, networkName = "target", ): number => { const confirmations = target.finality?.confirmations; if (!Number.isSafeInteger(confirmations) || confirmations < 0) { throw new Error( `config.yaml entry for network "${networkName}" finality.confirmations must be a non-negative safe integer`, ); } return confirmations; }; export const taskDir = (taskId: string, root = projectRoot()): string => { return path.join(root, "scripts", "tasks", taskId); }; export const resolveTarget = (config: WorkflowConfig, networkName: string): TargetConfig => { const target = config[networkName]; if (!target) { throw new Error(`Missing config.yaml entry for network "${networkName}"`); } return target; }; export const resolveAdmin = (target: TargetConfig, signerAddress: string): string => { const admin = target.admin?.trim(); if (!admin || admin.toLowerCase() === "deployer") { return getAddress(signerAddress); } if (!isAddress(admin)) { throw new Error(`Invalid admin address in config.yaml: ${admin}`); } return getAddress(admin); };