import crypto from "crypto"; import fs from "fs"; import path from "path"; export interface ReleaseFingerprint { manifestHash: string; artifactHash: string; parametersHash: string; configHash: string; } export interface ArtifactFingerprintInput { contract: string; bytecode: string; deployedBytecode: string; linkReferences: unknown; deployedLinkReferences: unknown; solcVersion: string; optimizer: unknown; evmVersion?: string; } export interface ReleaseFingerprintInput { manifest: string; artifacts: ArtifactFingerprintInput[]; parameters: Record; network: string; chainId?: number; target: Record; } export interface ReleaseNetwork extends Partial { status: string; simulationResult: string; simulatedAt: string; simulationCallerNetworkHash?: string; } export interface DeploymentRelease { version: 1; sourceSuperTaskId: string; developmentNetwork: string; manifestHash: string; artifactHash: string; networks: Record; } const asObject = (value: unknown, label: string): Record => { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(label + " must be an object"); } return value as Record; }; const requiredString = (source: Record, key: string, label: string): string => { const value = source[key]; if (typeof value !== "string" || !value.trim()) { throw new Error(label + "." + key + " must be a non-empty string"); } return value.trim(); }; const normalizeForJson = (value: unknown): unknown => { if (Array.isArray(value)) { return value.map((item) => normalizeForJson(item)); } if (value && typeof value === "object") { return Object.fromEntries( Object.keys(value as Record) .sort() .filter((key) => (value as Record)[key] !== undefined) .map((key) => [key, normalizeForJson((value as Record)[key])]), ); } return value; }; export const stableJson = (value: unknown): string => { return JSON.stringify(normalizeForJson(value)); }; export const hashValue = (value: unknown): string => { return crypto.createHash("sha256").update(stableJson(value)).digest("hex"); }; const hashText = (value: string): string => { return crypto.createHash("sha256").update(value).digest("hex"); }; const optionalObject = (value: unknown): Record => { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; }; const normalizedAddress = (value: unknown): unknown => { return typeof value === "string" ? value.toLowerCase() : value; }; export const hashArtifactInputs = (artifacts: ArtifactFingerprintInput[]): string => { const normalized = artifacts .map((artifact) => ({ contract: artifact.contract, bytecode: artifact.bytecode, deployedBytecode: artifact.deployedBytecode, linkReferences: artifact.linkReferences, deployedLinkReferences: artifact.deployedLinkReferences, solcVersion: artifact.solcVersion, optimizer: artifact.optimizer, evmVersion: artifact.evmVersion || null, })) .sort((left, right) => left.contract.localeCompare(right.contract)); return hashValue(normalized); }; export const hashTargetConfig = ( network: string, chainId: number | undefined, target: Record, ): string => { const create2 = optionalObject(target.create2); const multicall = optionalObject(target.multicall); const uups = optionalObject(target.uups); return hashValue({ network, chainId: chainId ?? null, create2: { factory: normalizedAddress(create2.factory), caller: normalizedAddress(create2.caller), leadingZeroBytes: create2.leadingZeroBytes, }, controlledMulticall: normalizedAddress(multicall.controlledMulticall), uupsPlaceholderImplementation: normalizedAddress(uups.placeholderImplementation), }); }; export const buildReleaseFingerprint = ( input: ReleaseFingerprintInput, ): ReleaseFingerprint => { return { manifestHash: hashText(input.manifest), artifactHash: hashArtifactInputs(input.artifacts), parametersHash: hashValue(input.parameters), configHash: hashTargetConfig(input.network, input.chainId, input.target), }; }; export const validateRelease = (value: unknown): DeploymentRelease => { const release = asObject(value, "release.json"); if (release.version !== 1) { throw new Error("release.json.version must be 1"); } const networksValue = asObject(release.networks, "release.json.networks"); const networks: Record = {}; for (const [network, networkValue] of Object.entries(networksValue)) { const entry = asObject(networkValue, "release.json.networks." + network); const parsed: ReleaseNetwork = { status: requiredString(entry, "status", "release.json.networks." + network), simulationResult: requiredString(entry, "simulationResult", "release.json.networks." + network), simulatedAt: requiredString(entry, "simulatedAt", "release.json.networks." + network), }; for (const key of ["manifestHash", "artifactHash", "parametersHash", "configHash"] as const) { if (entry[key] !== undefined) { parsed[key] = requiredString(entry, key, "release.json.networks." + network); } } if (entry.simulationCallerNetworkHash !== undefined) { parsed.simulationCallerNetworkHash = requiredString( entry, "simulationCallerNetworkHash", "release.json.networks." + network, ); } networks[network] = parsed; } return { version: 1, sourceSuperTaskId: requiredString(release, "sourceSuperTaskId", "release.json"), developmentNetwork: requiredString(release, "developmentNetwork", "release.json"), manifestHash: requiredString(release, "manifestHash", "release.json"), artifactHash: requiredString(release, "artifactHash", "release.json"), networks, }; }; export const loadRelease = (taskDir: string): DeploymentRelease => { const filePath = path.join(taskDir, "release.json"); if (!fs.existsSync(filePath)) { throw new Error("Missing release.json in deployment task"); } try { return validateRelease(JSON.parse(fs.readFileSync(filePath, "utf8"))); } catch (error) { if (error instanceof SyntaxError) { throw new Error("release.json must contain valid JSON: " + error.message); } throw error; } }; export const assertReleaseNetworkReady = ( release: DeploymentRelease, network: string, currentFingerprint: ReleaseFingerprint, ): ReleaseNetwork => { const entry = release.networks[network]; if (!entry) { throw new Error("Network " + network + " is not present in release.json"); } if (entry.status !== "ready") { throw new Error("Network " + network + " is not release-ready"); } if (!entry.simulationCallerNetworkHash || !/^[0-9a-f]{64}$/.test(entry.simulationCallerNetworkHash)) { throw new Error("Network " + network + " release caller network hash is missing or invalid"); } const expected: ReleaseFingerprint = { manifestHash: entry.manifestHash || release.manifestHash, artifactHash: entry.artifactHash || release.artifactHash, parametersHash: entry.parametersHash || "", configHash: entry.configHash || "", }; for (const key of ["manifestHash", "artifactHash", "parametersHash", "configHash"] as const) { if (!expected[key] || expected[key] !== currentFingerprint[key]) { throw new Error("Network " + network + " release fingerprint changed: " + key); } } return entry; };