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 UPGRADED_TOPIC = "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b"; 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 finalizeUpgrade({ root = process.cwd(), rpc, writeManifest = atomicWriteJson }) { if (typeof rpc !== "function") throw new Error("finalizer requires an RPC function"); const pendingPath = join(root, "deployments", "upgrade-pending.json"); const pending = await readJson(pendingPath); if (pending?.mode !== "upgrade" && pending?.mode !== "noop") throw new Error("unknown upgrade staging mode"); const activePath = join(root, "deployments", "active.json"); const active = await readManifest(activePath); const spec = networkSpec(active.network); const canonicalPath = join(root, "deployments", spec.canonical); const canonical = await readManifest(canonicalPath); const [activeBytes, canonicalBytes] = await Promise.all([readFile(activePath), readFile(canonicalPath)]); if (pending.mode === "noop") { assertConfirmedManifestPair(active, canonical, activeBytes, canonicalBytes); const chainId = parseRpcQuantity(await rpc("eth_chainId", []), "RPC chain ID"); if (chainId !== active.chainId) throw new Error("RPC chain ID does not match active manifest"); const methods = await readUpgradeMethods(root); validateNoopMarker(pending, active); const latestBlock = parseRpcQuantity(await rpc("eth_blockNumber", []), "latest block number"); if (latestBlock < pending.observedBlock) throw new Error("live block precedes no-op observation block"); const nonce = parseRpcQuantity( await rpc("eth_getTransactionCount", [active.owner, "latest"]), "owner nonce" ); if (nonce !== pending.ownerNonce) throw new Error("owner nonce changed after no-op observation"); await assertLiveVersionAndImplementation({ rpc, active, implementation: pending.implementation, methods }); await rm(pendingPath); return { mode: "noop", path: canonicalPath, manifest: active }; } const previous = recoverableUpgradeBaseline(pending, active, canonical, activeBytes, canonicalBytes); const chainId = parseRpcQuantity(await rpc("eth_chainId", []), "RPC chain ID"); if (chainId !== previous.chainId) throw new Error("RPC chain ID does not match active manifest"); const methods = await readUpgradeMethods(root); const broadcastPath = join(root, "broadcast", "UpgradeV2.s.sol", String(previous.chainId), "run-latest.json"); const broadcast = await readJson(broadcastPath); if (!Array.isArray(broadcast.transactions)) throw new Error("upgrade broadcast is partial: transactions are missing"); const hashes = [...new Set(broadcast.transactions.map((transaction) => transaction?.hash) .filter((hash) => typeof hash === "string"))]; const resolvedTransactions = await Promise.all(hashes.map(async (hash) => ({ hash, transaction: await rpc("eth_getTransactionByHash", [hash]), }))); const matching = resolvedTransactions.filter(({ transaction }) => typeof transaction?.to === "string" && sameAddress(transaction.to, previous.proxy)); if (matching.length !== 1) throw new Error(`expected exactly one live upgrade transaction to proxy, found ${matching.length}`); const receipt = await rpc("eth_getTransactionReceipt", [matching[0].hash]); if (!receipt || !isSuccessfulReceipt(receipt.status)) throw new Error("upgrade receipt was not successful"); const upgradeBlock = parseRpcQuantity(receipt.blockNumber, "upgrade receipt block number"); if (upgradeBlock === 0) throw new Error("upgrade receipt block number must be nonzero"); if (!Array.isArray(receipt.logs) || !receipt.logs.some((log) => isUpgradeLog(log, previous.proxy, pending.implementation))) { throw new Error("successful upgrade receipt is missing the expected Upgraded event"); } await assertLiveVersionAndImplementation({ rpc, active: previous, implementation: pending.implementation, methods }); await assertUpgradeSnapshot({ rpc, active: previous, pending, methods }); const updated = { ...previous, implementation: pending.implementation }; validateManifest(updated); const updatedBytes = Buffer.from(`${JSON.stringify(updated, null, 2)}\n`); if (!canonicalBytes.equals(updatedBytes)) await writeManifest(canonicalPath, updated); if (!activeBytes.equals(updatedBytes)) await writeManifest(activePath, updated); const [confirmedCanonical, confirmedActive] = await Promise.all([ readFile(canonicalPath), readFile(activePath), ]); if (!confirmedCanonical.equals(updatedBytes) || !confirmedActive.equals(updatedBytes)) { throw new Error("active and canonical manifests did not converge after upgrade finalization"); } await rm(pendingPath); return { mode: "upgrade", path: canonicalPath, manifest: updated, upgradeBlock }; } function assertConfirmedManifestPair(active, canonical, activeBytes, canonicalBytes) { if (!activeBytes.equals(canonicalBytes) || JSON.stringify(active) !== JSON.stringify(canonical)) { throw new Error("active and canonical manifest identity must match before upgrade finalization"); } } function recoverableUpgradeBaseline(pending, active, canonical, activeBytes, canonicalBytes) { assertAddress(pending?.previousImplementation, "upgrade previous implementation"); assertAddress(pending?.implementation, "upgrade implementation"); const allowedImplementation = (manifest) => sameAddress(manifest.implementation, pending.previousImplementation) || sameAddress(manifest.implementation, pending.implementation); if (!allowedImplementation(active) || !allowedImplementation(canonical)) { throw new Error("active and canonical manifest divergence is not justified by the upgrade marker"); } if (sameAddress(active.implementation, canonical.implementation)) { assertConfirmedManifestPair(active, canonical, activeBytes, canonicalBytes); } const previousActive = { ...active, implementation: pending.previousImplementation }; const previousCanonical = { ...canonical, implementation: pending.previousImplementation }; if (JSON.stringify(previousActive) !== JSON.stringify(previousCanonical)) { throw new Error("active and canonical manifest divergence is not justified by the upgrade marker"); } validateUpgradeMarker(pending, previousActive); return previousActive; } function validateNoopMarker(marker, active) { assertExactKeys(marker, ["mode", "chainId", "observedBlock", "ownerNonce", "proxy", "implementation"], "no-op marker"); for (const field of ["chainId", "observedBlock", "ownerNonce"]) { if (!Number.isSafeInteger(marker[field]) || marker[field] < 0) throw new Error(`no-op marker ${field} is invalid`); } if (marker.chainId !== active.chainId || !sameAddress(marker.proxy, active.proxy) || !sameAddress(marker.implementation, active.implementation)) { throw new Error("no-op marker chain/proxy/implementation identity does not match active manifest"); } } function validateUpgradeMarker(marker, active) { assertExactKeys(marker, ["mode", "network", "chainId", "token", "proxy", "previousImplementation", "implementation", "owner", "deploymentBlock", "snapshot"], "upgrade marker"); for (const field of ["network", "chainId", "deploymentBlock"]) { if (marker[field] !== active[field]) throw new Error(`upgrade marker ${field} identity does not match active manifest`); } for (const field of ["token", "proxy", "previousImplementation", "owner"]) { const activeField = field === "previousImplementation" ? "implementation" : field; if (!sameAddress(marker[field], active[activeField])) throw new Error(`upgrade marker ${field} identity does not match active manifest`); } assertAddress(marker.implementation, "upgrade implementation"); if (sameAddress(marker.implementation, active.implementation)) throw new Error("upgrade implementation did not change"); const snapshot = marker.snapshot; assertExactKeys(snapshot, ["proxy", "implementation", "owner", "asset", "paused", "balances", "liabilities", "reserves", "surplus", "deploymentBlock", "version"], "upgrade snapshot"); if (!sameAddress(snapshot.proxy, active.proxy) || snapshot.deploymentBlock !== active.deploymentBlock) { throw new Error("upgrade snapshot proxy or deployment identity changed"); } if (!sameAddress(snapshot.implementation, active.implementation) || !sameAddress(snapshot.owner, active.owner) || !sameAddress(snapshot.asset, active.token) || snapshot.version !== 1 || typeof snapshot.paused !== "boolean") { throw new Error("upgrade snapshot identity does not match active manifest"); } if (!Array.isArray(snapshot.balances) || snapshot.balances.length !== active.actors.length) { throw new Error("upgrade snapshot actor count does not match active manifest"); } snapshot.balances.forEach((record, index) => { assertExactKeys(record, ["address", "balance"], `upgrade snapshot actor ${index}`); if (!sameAddress(record.address, active.actors[index].address)) throw new Error("upgrade snapshot actor identity changed"); assertNonnegativeInteger(record.balance, "upgrade snapshot actor balance"); }); for (const field of ["liabilities", "reserves", "surplus"]) assertNonnegativeInteger(snapshot[field], `upgrade snapshot ${field}`); if (snapshot.reserves < snapshot.liabilities || snapshot.reserves - snapshot.liabilities !== snapshot.surplus) { throw new Error("upgrade snapshot accounting is inconsistent"); } } async function readUpgradeMethods(root) { const bank = await readJson(join(root, "out", "BankV2.sol", "BankV2.json")); const token = await readJson(join(root, "out", "MockUSDC.sol", "MockUSDC.json")); const requiredBank = ["owner()", "asset()", "paused()", "balanceOf(address)", "totalLiabilities()", "contractVersion()"]; const result = { bank: {}, token: {} }; for (const signature of requiredBank) result.bank[signature] = methodSelector(bank, signature, "BankV2"); result.token["balanceOf(address)"] = methodSelector(token, "balanceOf(address)", "MockUSDC"); return result; } function methodSelector(artifact, signature, contractName) { const selector = artifact?.methodIdentifiers?.[signature]; if (typeof selector !== "string" || !/^[0-9a-fA-F]{8}$/.test(selector)) { throw new Error(`${contractName} artifact is missing method identifier ${signature}`); } return `0x${selector.toLowerCase()}`; } async function assertLiveVersionAndImplementation({ rpc, active, implementation, methods }) { const code = await rpc("eth_getCode", [implementation, "latest"]); if (typeof code !== "string" || code.length <= 2) throw new Error("upgrade implementation has no live code"); const version = decodeUint(await rpcCall(rpc, active.proxy, methods.bank["contractVersion()"]), "contract version"); if (version !== 2) throw new Error("live contract version is not 2"); const slot = await rpc("eth_getStorageAt", [active.proxy, IMPLEMENTATION_SLOT, "latest"]); if (slotAddress(slot) !== implementation.toLowerCase()) throw new Error("live proxy implementation slot does not match upgrade marker"); } async function assertUpgradeSnapshot({ rpc, active, pending, methods }) { const snapshot = pending.snapshot; const owner = decodeAddress(await rpcCall(rpc, active.proxy, methods.bank["owner()"]), "owner"); const asset = decodeAddress(await rpcCall(rpc, active.proxy, methods.bank["asset()"]), "asset"); const paused = decodeBool(await rpcCall(rpc, active.proxy, methods.bank["paused()"]), "paused"); if (!sameAddress(owner, snapshot.owner)) throw new Error("live owner changed during upgrade"); if (!sameAddress(asset, snapshot.asset)) throw new Error("live asset changed during upgrade"); if (paused !== snapshot.paused) throw new Error("live pause state changed during upgrade"); for (const [index, actor] of snapshot.balances.entries()) { const balance = decodeUint( await rpcCall(rpc, active.proxy, `${methods.bank["balanceOf(address)"]}${encodeAddressWord(actor.address)}`), `actor ${index} balance` ); if (balance !== actor.balance) throw new Error(`live actor ${index} balance changed during upgrade`); } const liabilities = decodeUint(await rpcCall(rpc, active.proxy, methods.bank["totalLiabilities()"]), "liabilities"); const reserves = decodeUint( await rpcCall(rpc, active.token, `${methods.token["balanceOf(address)"]}${encodeAddressWord(active.proxy)}`), "reserves" ); if (liabilities !== snapshot.liabilities) throw new Error("live liabilities changed during upgrade"); if (reserves !== snapshot.reserves) throw new Error("live reserves changed during upgrade"); if (reserves - liabilities !== snapshot.surplus) throw new Error("live surplus changed during upgrade"); } function rpcCall(rpc, to, data) { return rpc("eth_call", [{ to, data }, "latest"]); } function encodeAddressWord(address) { assertAddress(address, "call address"); return address.slice(2).toLowerCase().padStart(64, "0"); } function decodeUint(value, label) { if (typeof value !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(value)) throw new Error(`${label} RPC result is invalid`); const parsed = BigInt(value); if (parsed > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`${label} exceeds JavaScript safe integer range`); return Number(parsed); } function decodeAddress(value, label) { if (typeof value !== "string" || !/^0x0{24}[0-9a-fA-F]{40}$/.test(value)) throw new Error(`${label} RPC result is invalid`); return `0x${value.slice(-40)}`; } function decodeBool(value, label) { const decoded = decodeUint(value, label); if (decoded !== 0 && decoded !== 1) throw new Error(`${label} RPC result is not boolean`); return decoded === 1; } function isUpgradeLog(log, proxy, implementation) { return typeof log?.address === "string" && sameAddress(log.address, proxy) && Array.isArray(log.topics) && log.topics.length >= 2 && log.topics[0]?.toLowerCase() === UPGRADED_TOPIC && slotAddress(log.topics[1]) === implementation.toLowerCase(); } function sameAddress(first, second) { return typeof first === "string" && typeof second === "string" && /^0x[0-9a-fA-F]{40}$/.test(first) && /^0x[0-9a-fA-F]{40}$/.test(second) && first.toLowerCase() === second.toLowerCase(); } function assertExactKeys(value, expected, label) { if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`); const actual = Object.keys(value).sort(); const wanted = [...expected].sort(); if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) throw new Error(`${label} schema is invalid`); } function assertNonnegativeInteger(value, label) { if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a nonnegative integer`); } 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 ["rpcUrl", "explorerBaseUrl"]) { if (!Object.hasOwn(manifest, field)) throw new Error(`manifest baseSepolia is missing ${field}`); assertPublicUrl(manifest[field], field); } if (new URL(manifest.rpcUrl).protocol !== "https:") { throw new Error("manifest Base Sepolia rpcUrl must use HTTPS"); } if (manifest.explorerBaseUrl !== "https://sepolia.basescan.org") { throw new Error("manifest Base Sepolia explorerBaseUrl must use the BaseScan root"); } } 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`); } for (const key of url.searchParams.keys()) { if (/(key|token|secret|password|credential)/i.test(key)) { throw new Error(`manifest ${field} must not contain credential query parameters`); } } } 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 !== 2 || actors[0].label !== "Presenter" || actors[1].label !== "Recipient") { throw new Error("manifest baseSepolia actors must contain exactly Presenter and Recipient"); } } if (actors[0].address.toLowerCase() !== owner.toLowerCase()) { throw new Error(`manifest owner must be the ${network === "baseSepolia" ? "Presenter" : "first actor"}`); } } 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]) }); } if (command === "upgrade") { if (network !== "--rpc-url" || typeof rest[0] !== "string" || rest.length !== 1) throw new Error("usage: finalize-manifest.mjs upgrade --rpc-url "); return finalizeUpgrade({ root, rpc: fetchRpc(rest[0]) }); } throw new Error("usage: finalize-manifest.mjs preflight-deploy | deploy --rpc-url | upgrade --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; }); }