feat: script deterministic V1 demo state
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { atomicWrite, finalizeDeployment, preflightDeploy } from "./finalize-manifest.mjs";
|
||||
import { selectManifest } from "./select-manifest.mjs";
|
||||
|
||||
const TOKEN = "0x1000000000000000000000000000000000000001";
|
||||
const PROXY = "0x2000000000000000000000000000000000000002";
|
||||
const IMPLEMENTATION = "0x3000000000000000000000000000000000000003";
|
||||
const OWNER = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
|
||||
const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
|
||||
|
||||
test("preflight rejects an existing target canonical manifest with the safe recovery command", async () => {
|
||||
await withFixture(async (root) => {
|
||||
await writeJson(join(root, "deployments", "anvil.json"), manifest());
|
||||
await assert.rejects(
|
||||
() => preflightDeploy({ root, network: "anvil" }),
|
||||
/make reset-local/
|
||||
);
|
||||
await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "base-sepolia", chainId: 84532 }));
|
||||
await assert.rejects(
|
||||
() => preflightDeploy({ root, network: "base-sepolia" }),
|
||||
/make archive-base-manifest/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("finalizer writes only a receipt-confirmed manifest and preserves active until selection", async () => {
|
||||
await withFixture(async (root) => {
|
||||
const pending = manifest({ deploymentBlock: 0 });
|
||||
await writeJson(join(root, "deployments", "pending.json"), pending);
|
||||
await writeJson(join(root, "deployments", "active.json"), manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 88 }));
|
||||
await writeBroadcast(root, pending, { hash: "0xaaa", contractAddress: PROXY });
|
||||
|
||||
const output = await finalizeDeployment({ root, rpc: fakeRpc() });
|
||||
assert.equal(output.path, join(root, "deployments", "anvil.json"));
|
||||
assert.deepEqual(await readJson(output.path), { ...pending, deploymentBlock: 42 });
|
||||
assert.deepEqual(await readJson(join(root, "deployments", "active.json")), manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 88 }));
|
||||
});
|
||||
});
|
||||
|
||||
test("finalizer rejects invalid receipt, transaction, RPC, code, slot, and secret data without touching confirmed files", async () => {
|
||||
const cases = [
|
||||
["failed receipt", { rpc: fakeRpc({ receipt: { status: "0x0", blockNumber: "0x2a" } }) }, /not successful/],
|
||||
["missing receipt", { rpc: fakeRpc({ receipt: null }) }, /missing receipt/],
|
||||
["ambiguous proxy transaction", { broadcast: { extraProxy: true } }, /exactly one/],
|
||||
["partial broadcast", { broadcast: { omitProxy: true } }, /exactly one/],
|
||||
["non-creation proxy transaction", { broadcast: { transactionType: "CALL" } }, /exactly one/],
|
||||
["wrong chain", { rpc: fakeRpc({ chainId: "0x14a34" }) }, /chain ID/],
|
||||
["missing code", { rpc: fakeRpc({ missingCode: TOKEN }) }, /has no code/],
|
||||
["implementation slot mismatch", { rpc: fakeRpc({ slot: TOKEN }) }, /implementation slot/],
|
||||
["secret-bearing pending manifest", { pending: manifest({ rpcUrl: "https://user:password@example.invalid" }) }, /credential|secret/i],
|
||||
];
|
||||
|
||||
for (const [name, options, expected] of cases) {
|
||||
await withFixture(async (root) => {
|
||||
const pending = options.pending ?? manifest({ deploymentBlock: 0 });
|
||||
await writeJson(join(root, "deployments", "pending.json"), pending);
|
||||
await writeJson(join(root, "deployments", "anvil.json"), manifest({ deploymentBlock: 7 }));
|
||||
await writeJson(join(root, "deployments", "active.json"), manifest({ deploymentBlock: 8 }));
|
||||
await writeBroadcast(root, pending, options.broadcast);
|
||||
const beforeCanonical = await readFile(join(root, "deployments", "anvil.json"));
|
||||
const beforeActive = await readFile(join(root, "deployments", "active.json"));
|
||||
|
||||
await assert.rejects(() => finalizeDeployment({ root, rpc: options.rpc ?? fakeRpc() }), expected, name);
|
||||
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), beforeCanonical, name);
|
||||
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive, name);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("selection atomically replaces active with only a valid named canonical manifest", async () => {
|
||||
await withFixture(async (root) => {
|
||||
const anvil = manifest({ deploymentBlock: 31 });
|
||||
const base = manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 32 });
|
||||
await writeJson(join(root, "deployments", "anvil.json"), anvil);
|
||||
await writeJson(join(root, "deployments", "base-sepolia.json"), base);
|
||||
|
||||
await selectManifest({ root, network: "anvil" });
|
||||
const anvilBytes = await readFile(join(root, "deployments", "anvil.json"));
|
||||
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), anvilBytes);
|
||||
await selectManifest({ root, network: "base-sepolia" });
|
||||
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), anvilBytes);
|
||||
assert.deepEqual(await readJson(join(root, "deployments", "active.json")), base);
|
||||
|
||||
await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 0 }));
|
||||
const beforeActive = await readFile(join(root, "deployments", "active.json"));
|
||||
await assert.rejects(() => selectManifest({ root, network: "base-sepolia" }), /deploymentBlock/);
|
||||
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive);
|
||||
});
|
||||
});
|
||||
|
||||
test("atomic writes stage a same-directory temporary file before renaming it into place", async () => {
|
||||
const target = "/tmp/deployments/active.json";
|
||||
const calls = [];
|
||||
const io = {
|
||||
writeFile: async (path, contents) => calls.push(["write", path, contents]),
|
||||
rename: async (source, destination) => calls.push(["rename", source, destination]),
|
||||
};
|
||||
|
||||
await atomicWrite(target, "confirmed", io);
|
||||
|
||||
assert.equal(calls[0][0], "write");
|
||||
assert.equal(dirname(calls[0][1]), dirname(target));
|
||||
assert.notEqual(calls[0][1], target);
|
||||
assert.deepEqual(calls[1], ["rename", calls[0][1], target]);
|
||||
});
|
||||
|
||||
function manifest(overrides = {}) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
network: "anvil",
|
||||
chainId: 31337,
|
||||
deploymentBlock: 1,
|
||||
rpcUrl: "http://127.0.0.1:8545",
|
||||
explorerUrl: "",
|
||||
token: TOKEN,
|
||||
proxy: PROXY,
|
||||
implementation: IMPLEMENTATION,
|
||||
owner: OWNER,
|
||||
actorLabels: ["owner", "Alice", "Bob"],
|
||||
actors: [OWNER, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function withFixture(fn) {
|
||||
const root = await mkdtemp(join(tmpdir(), "uups-finalizer-"));
|
||||
try {
|
||||
await import("node:fs/promises").then(({ mkdir }) => mkdir(join(root, "deployments"), { recursive: true }));
|
||||
await fn(root);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function writeBroadcast(root, pending, options = {}) {
|
||||
const directory = join(root, "broadcast", "DeployV1.s.sol", String(pending.chainId));
|
||||
await import("node:fs/promises").then(({ mkdir }) => mkdir(directory, { recursive: true }));
|
||||
const transactions = options.omitProxy
|
||||
? [{ hash: "0xbbb", transactionType: "CREATE", contractAddress: pending.token }]
|
||||
: [{ hash: "0xaaa", transactionType: options.transactionType ?? "CREATE", contractAddress: pending.proxy }];
|
||||
if (options.extraProxy) transactions.push({ hash: "0xccc", transactionType: "CREATE", contractAddress: pending.proxy });
|
||||
await writeJson(join(directory, "run-latest.json"), { transactions });
|
||||
}
|
||||
|
||||
function fakeRpc(overrides = {}) {
|
||||
return async (method, params) => {
|
||||
if (method === "eth_chainId") return overrides.chainId ?? "0x7a69";
|
||||
if (method === "eth_getTransactionReceipt") return Object.hasOwn(overrides, "receipt") ? overrides.receipt : { status: "0x1", blockNumber: "0x2a" };
|
||||
if (method === "eth_getCode") return params[0].toLowerCase() === overrides.missingCode?.toLowerCase() ? "0x" : "0x6000";
|
||||
if (method === "eth_getStorageAt") {
|
||||
assert.equal(params[1], IMPLEMENTATION_SLOT);
|
||||
return `0x000000000000000000000000${(overrides.slot ?? IMPLEMENTATION).slice(2)}`;
|
||||
}
|
||||
throw new Error(`unexpected RPC method: ${method}`);
|
||||
};
|
||||
}
|
||||
|
||||
async function writeJson(path, value) {
|
||||
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function readJson(path) {
|
||||
return JSON.parse(await readFile(path, "utf8"));
|
||||
}
|
||||
Reference in New Issue
Block a user