import { randomUUID } from "node:crypto"; import { readFile, rename, rm, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; export const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; export const EDUCATIONAL_WARNING = "Educational demo — mock token — never use real funds."; const NETWORKS = { anvil: { name: "anvil", chainId: 31337, canonical: "anvil.json", recovery: "make reset-local", rpcUrl: "http://127.0.0.1:8545", explorerUrl: "", }, "base-sepolia": { name: "base-sepolia", chainId: 84532, canonical: "base-sepolia.json", recovery: "make archive-base-manifest", rpcUrl: "https://sepolia.base.org", explorerUrl: "https://sepolia.basescan.org", }, }; const MANIFEST_FIELDS = [ "schemaVersion", "network", "chainId", "deploymentBlock", "rpcUrl", "explorerUrl", "token", "proxy", "implementation", "owner", "actorLabels", "actors", ]; const ANVIL_TEST_PHRASE = "test test test test test test test test test test test junk"; const PROHIBITED_STRING_VALUE = /(?:private[_ -]?key|mnemonic|secret|password|credential|api[_ -]?key)|0x[a-fA-F0-9]{64}/i; export function networkSpec(network) { const normalized = network === "baseSepolia" ? "base-sepolia" : network; const spec = NETWORKS[normalized]; if (!spec) throw new Error(`unsupported deployment network: ${network}`); return spec; } export async function preflightDeploy({ root = process.cwd(), network }) { const spec = networkSpec(network); const target = join(root, "deployments", spec.canonical); try { await readFile(target); } catch (error) { if (error.code === "ENOENT") return; throw error; } throw new Error(`refusing to overwrite ${target}; recover safely with: ${spec.recovery}`); } export async function finalizeDeployment({ root = process.cwd(), rpc }) { if (typeof rpc !== "function") throw new Error("finalizer requires an RPC function"); const pendingPath = join(root, "deployments", "pending.json"); const pending = await readManifest(pendingPath, { pending: true }); const spec = networkSpec(pending.network); if (pending.chainId !== spec.chainId) throw new Error(`pending manifest chain ID does not match ${pending.network}`); if (pending.deploymentBlock !== 0) throw new Error("pending manifest deploymentBlock must be 0"); const broadcastPath = join(root, "broadcast", "DeployV1.s.sol", String(pending.chainId), "run-latest.json"); const broadcast = await readJson(broadcastPath); if (!Array.isArray(broadcast.transactions)) throw new Error("broadcast is partial: transactions are missing"); const proxyTransactions = broadcast.transactions.filter( (transaction) => typeof transaction?.contractAddress === "string" && transaction.contractAddress.toLowerCase() === pending.proxy.toLowerCase() && transaction.transactionType === "CREATE" && typeof transaction.hash === "string" ); if (proxyTransactions.length !== 1) { throw new Error(`expected exactly one proxy creation transaction, found ${proxyTransactions.length}`); } const proxyTransaction = proxyTransactions[0]; const receipt = await rpc("eth_getTransactionReceipt", [proxyTransaction.hash]); if (!receipt) throw new Error(`missing receipt for proxy transaction ${proxyTransaction.hash}`); if (!isSuccessfulReceipt(receipt.status)) throw new Error(`proxy receipt ${proxyTransaction.hash} was not successful`); const deploymentBlock = parseRpcQuantity(receipt.blockNumber, "receipt block number"); if (deploymentBlock === 0) throw new Error("receipt block number must be nonzero"); const actualChainId = parseRpcQuantity(await rpc("eth_chainId", []), "RPC chain ID"); if (actualChainId !== pending.chainId) { throw new Error(`RPC chain ID ${actualChainId} does not match pending manifest chain ID ${pending.chainId}`); } for (const [label, address] of [["token", pending.token], ["proxy", pending.proxy], ["implementation", pending.implementation]]) { const code = await rpc("eth_getCode", [address, "latest"]); if (typeof code !== "string" || !/^0x[0-9a-fA-F]+$/.test(code) || code.length <= 2) { throw new Error(`${label} ${address} has no code`); } } const storage = await rpc("eth_getStorageAt", [pending.proxy, IMPLEMENTATION_SLOT, "latest"]); if (slotAddress(storage) !== pending.implementation.toLowerCase()) { throw new Error("proxy implementation slot does not match pending manifest implementation"); } const confirmed = publicManifest(pending, deploymentBlock); validateManifest(confirmed); const path = join(root, "deployments", spec.canonical); await atomicWriteJson(path, confirmed); return { path, manifest: confirmed }; } export async function readManifest(path, { pending = false } = {}) { const manifest = await readJson(path); validateManifest(manifest, { pending }); return manifest; } export function validateManifest(manifest, { pending = false } = {}) { if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) throw new Error("manifest must be a JSON object"); assertExactSchema(manifest); rejectProhibitedStringValues(manifest); if (manifest.schemaVersion !== 1) throw new Error("manifest schemaVersion must be 1"); const spec = networkSpec(manifest.network); if (manifest.chainId !== spec.chainId) throw new Error(`manifest chain ID does not match ${manifest.network}`); if (!Number.isSafeInteger(manifest.deploymentBlock) || manifest.deploymentBlock < (pending ? 0 : 1)) { throw new Error(`manifest deploymentBlock must be ${pending ? "a nonnegative integer" : "at least 1"}`); } if (manifest.rpcUrl !== spec.rpcUrl || manifest.explorerUrl !== spec.explorerUrl) { throw new Error(`manifest display URLs must use the public ${manifest.network} endpoints`); } for (const field of ["token", "proxy", "implementation", "owner"]) assertAddress(manifest[field], field); if (!Array.isArray(manifest.actorLabels) || !Array.isArray(manifest.actors) || manifest.actorLabels.length !== manifest.actors.length || manifest.actors.length === 0) { throw new Error("manifest actors and actorLabels must be nonempty parallel arrays"); } const labels = new Set(); const actors = new Set(); for (let index = 0; index < manifest.actors.length; index += 1) { const label = manifest.actorLabels[index]; if (typeof label !== "string" || label.trim() === "" || labels.has(label)) throw new Error("manifest actor labels must be unique nonempty strings"); labels.add(label); assertAddress(manifest.actors[index], `actors[${index}]`); const actor = manifest.actors[index].toLowerCase(); if (actors.has(actor)) throw new Error("manifest actors must be unique"); actors.add(actor); } } export async function atomicWriteJson(path, value) { await atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`); } export async function atomicWrite(path, contents, io = { writeFile, rename }) { const temporary = join(dirname(path), `.${randomUUID()}.json`); const operations = { writeFile, rename, rm, ...io }; try { await operations.writeFile(temporary, contents, { mode: 0o600 }); await operations.rename(temporary, path); } catch (error) { await operations.rm(temporary, { force: true }).catch(() => {}); throw error; } } function assertAddress(value, label) { if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value) || /^0x0{40}$/i.test(value)) { throw new Error(`manifest ${label} must be a nonzero address`); } } function assertExactSchema(manifest) { for (const field of MANIFEST_FIELDS) { if (!Object.hasOwn(manifest, field)) throw new Error(`manifest is missing required field ${field}`); } for (const field of Object.keys(manifest)) { if (!MANIFEST_FIELDS.includes(field)) throw new Error(`manifest contains unknown field ${field}`); } } function rejectProhibitedStringValues(value, path = "") { if (typeof value === "string") { if (value.includes(ANVIL_TEST_PHRASE) || PROHIBITED_STRING_VALUE.test(value)) { throw new Error(`manifest contains prohibited secret material at ${path}`); } return; } if (Array.isArray(value)) { value.forEach((item, index) => rejectProhibitedStringValues(item, `${path}[${index}]`)); return; } if (!value || typeof value !== "object") return; for (const [key, nested] of Object.entries(value)) { const nestedPath = path ? `${path}.${key}` : key; rejectProhibitedStringValues(nested, nestedPath); } } function publicManifest(manifest, deploymentBlock) { return { schemaVersion: manifest.schemaVersion, network: manifest.network, chainId: manifest.chainId, deploymentBlock, rpcUrl: manifest.rpcUrl, explorerUrl: manifest.explorerUrl, token: manifest.token, proxy: manifest.proxy, implementation: manifest.implementation, owner: manifest.owner, actorLabels: [...manifest.actorLabels], actors: [...manifest.actors], }; } function isSuccessfulReceipt(status) { return status === "0x1" || status === 1 || status === "1"; } function parseRpcQuantity(value, label) { if (typeof value !== "string" || !/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${label} is not a hexadecimal RPC quantity`); const parsed = Number.parseInt(value, 16); if (!Number.isSafeInteger(parsed)) throw new Error(`${label} exceeds JavaScript safe integer range`); return parsed; } function slotAddress(value) { if (typeof value !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(value)) throw new Error("proxy implementation slot response is invalid"); return `0x${value.slice(-40)}`.toLowerCase(); } async function readJson(path) { try { return JSON.parse(await readFile(path, "utf8")); } catch (error) { if (error instanceof SyntaxError) throw new Error(`invalid JSON at ${path}`); throw error; } } function fetchRpc(rpcUrl) { let nextId = 1; return async (method, params) => { const response = await fetch(rpcUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: nextId++, method, params }), }); if (!response.ok) throw new Error(`RPC ${method} returned HTTP ${response.status}`); const body = await response.json(); if (body.error) throw new Error(`RPC ${method} failed: ${body.error.message ?? "unknown error"}`); return body.result; }; } export async function runFinalizeCli(argv, { root = process.cwd(), log = console.log } = {}) { log(EDUCATIONAL_WARNING); const [command, network, ...rest] = argv; if (command === "preflight-deploy" && network && rest.length === 0) return preflightDeploy({ root, network }); if (command === "deploy") { if (network !== "--rpc-url" || typeof rest[0] !== "string" || rest.length !== 1) throw new Error("usage: finalize-manifest.mjs deploy --rpc-url "); return finalizeDeployment({ root, rpc: fetchRpc(rest[0]) }); } throw new Error("usage: finalize-manifest.mjs preflight-deploy | deploy --rpc-url "); } if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { runFinalizeCli(process.argv.slice(2)).catch((error) => { console.error(error.message); process.exitCode = 1; }); }