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", }, baseSepolia: { name: "baseSepolia", chainId: 84532, canonical: "base-sepolia.json", recovery: "make archive-base-manifest", }, }; const REQUIRED_MANIFEST_FIELDS = ["schemaVersion", "network", "chainId", "deploymentBlock", "token", "proxy", "implementation", "owner", "actors"]; const OPTIONAL_MANIFEST_FIELDS = ["rpcUrl", "explorerBaseUrl"]; const ANVIL_TEST_WORDS = [...Array(11).fill("test"), "junk"].join(" "); const PROHIBITED_STRING_VALUE = /(?:private[_ -]?key|mnemonic|secret|password|credential|api[_ -]?key)|0x[a-fA-F0-9]{64}/i; export function networkSpec(network) { const spec = NETWORKS[network]; 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"}`); } assertManifestUrls(manifest, spec); for (const field of ["token", "proxy", "implementation", "owner"]) assertAddress(manifest[field], field); if (!Array.isArray(manifest.actors) || manifest.actors.length === 0) throw new Error("manifest actors must be a nonempty array"); const labels = new Set(); const actors = new Set(); for (const [index, actorRecord] of manifest.actors.entries()) { if (!actorRecord || typeof actorRecord !== "object" || Array.isArray(actorRecord)) throw new Error(`actors[${index}] must be an object`); const keys = Object.keys(actorRecord); if (keys.length !== 2 || !Object.hasOwn(actorRecord, "label") || !Object.hasOwn(actorRecord, "address")) { throw new Error(`actors[${index}] must contain exactly label and address`); } const { label, address } = actorRecord; if (typeof label !== "string" || label.trim() === "" || labels.has(label)) throw new Error("manifest actor labels must be unique nonempty strings"); labels.add(label); assertAddress(address, `actors[${index}].address`); const actor = address.toLowerCase(); if (actors.has(actor)) throw new Error("manifest actors must be unique"); actors.add(actor); } assertActorConfiguration(manifest); } 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 REQUIRED_MANIFEST_FIELDS) { if (!Object.hasOwn(manifest, field)) throw new Error(`manifest is missing required field ${field}`); } for (const field of Object.keys(manifest)) { if (![...REQUIRED_MANIFEST_FIELDS, ...OPTIONAL_MANIFEST_FIELDS].includes(field)) throw new Error(`manifest contains unknown field ${field}`); } } function assertManifestUrls(manifest, spec) { if (manifest.network === "anvil") { if (Object.hasOwn(manifest, "rpcUrl") && manifest.rpcUrl !== spec.rpcUrl) { throw new Error("manifest anvil rpcUrl must use the local public endpoint"); } if (Object.hasOwn(manifest, "explorerBaseUrl")) throw new Error("manifest anvil must omit explorerBaseUrl"); return; } for (const field of OPTIONAL_MANIFEST_FIELDS) { if (Object.hasOwn(manifest, field)) assertPublicUrl(manifest[field], field); } } function assertPublicUrl(value, field) { if (typeof value !== "string") throw new Error(`manifest ${field} must be a URL string`); let url; try { url = new URL(value); } catch { throw new Error(`manifest ${field} must be a public URL`); } if ((url.protocol !== "https:" && url.protocol !== "http:") || url.username || url.password) { throw new Error(`manifest ${field} must be a public URL without credentials`); } } function assertActorConfiguration(manifest) { const { actors, network, owner } = manifest; if (network === "anvil") { const expected = [ ["owner", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"], ["Alice", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"], ["Bob", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"], ]; if (actors.length !== expected.length || actors.some((actor, index) => actor.label !== expected[index][0] || actor.address.toLowerCase() !== expected[index][1].toLowerCase())) { throw new Error("manifest anvil actors must match the documented local actor configuration"); } } else if (actors.length !== 1 || actors[0].label !== "owner") { throw new Error("manifest baseSepolia must contain only the owner actor"); } if (actors[0].address.toLowerCase() !== owner.toLowerCase()) throw new Error("manifest owner must be actor zero"); } function rejectProhibitedStringValues(value, path = "") { if (typeof value === "string") { if (value.includes(ANVIL_TEST_WORDS) || 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(key, nestedPath); rejectProhibitedStringValues(nested, nestedPath); } } function publicManifest(manifest, deploymentBlock) { return { schemaVersion: manifest.schemaVersion, network: manifest.network, chainId: manifest.chainId, deploymentBlock, ...(Object.hasOwn(manifest, "rpcUrl") ? { rpcUrl: manifest.rpcUrl } : {}), ...(Object.hasOwn(manifest, "explorerBaseUrl") ? { explorerBaseUrl: manifest.explorerBaseUrl } : {}), token: manifest.token, proxy: manifest.proxy, implementation: manifest.implementation, owner: manifest.owner, actors: manifest.actors.map(({ label, address }) => ({ label, address })), }; } 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; }); }