Files
uupl-smart-contract/tools/test-sync-web-artifacts.mjs
T

108 lines
7.2 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { extractAbi, renderContractsModule, syncArtifacts } from "./sync-web-artifacts.mjs";
import { publishManifest } from "./publish-web-manifest.mjs";
const address = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
const bankAbi = [
{ type: "function", name: "contractVersion", inputs: [], outputs: [{ name: "", type: "uint256" }], stateMutability: "pure" },
{ type: "function", name: "paused", inputs: [], outputs: [{ name: "", type: "bool" }], stateMutability: "view" },
{ type: "function", name: "asset", inputs: [], outputs: [{ name: "", type: "address" }], stateMutability: "view" },
{ type: "function", name: "owner", inputs: [], outputs: [{ name: "", type: "address" }], stateMutability: "view" },
{ type: "function", name: "totalLiabilities", inputs: [], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" },
{ type: "function", name: "balanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" },
{ type: "event", name: "Deposited", inputs: [{ name: "account", type: "address", indexed: true }, { name: "amount", type: "uint256", indexed: false }], anonymous: false },
{ type: "event", name: "Withdrawn", inputs: [{ name: "account", type: "address", indexed: true }, { name: "amount", type: "uint256", indexed: false }], anonymous: false },
{ type: "event", name: "Paused", inputs: [{ name: "account", type: "address", indexed: false }], anonymous: false },
{ type: "event", name: "Unpaused", inputs: [{ name: "account", type: "address", indexed: false }], anonymous: false },
{ type: "event", name: "OwnershipTransferred", inputs: [{ name: "previousOwner", type: "address", indexed: true }, { name: "newOwner", type: "address", indexed: true }], anonymous: false },
{ type: "event", name: "Upgraded", inputs: [{ name: "implementation", type: "address", indexed: true }], anonymous: false },
];
const tokenAbi = [{ type: "function", name: "balanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" }];
const bankV2Abi = [
...bankAbi,
{ type: "function", name: "transferBalance", inputs: [{ name: "recipient", type: "address" }, { name: "amount", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
{ type: "event", name: "BalanceTransferred", inputs: [{ name: "from", type: "address", indexed: true }, { name: "to", type: "address", indexed: true }, { name: "amount", type: "uint256", indexed: false }], anonymous: false },
];
const manifest = {
schemaVersion: 1, network: "anvil", chainId: 31337, deploymentBlock: 3,
rpcUrl: "http://127.0.0.1:8545", token: address, proxy: address,
implementation: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", owner,
actors: [{ label: "owner", address: owner }],
};
async function withFixture(run) {
const root = await mkdtemp(join(tmpdir(), "uups-artifacts-"));
try { await run(root); } finally { await rm(root, { recursive: true, force: true }); }
}
async function expectRejects(action, pattern) {
await assert.rejects(action, pattern);
}
await withFixture(async (root) => {
const bank = join(root, "BankV1.json");
const bankV2 = join(root, "BankV2.json");
const token = join(root, "MockUSDC.json");
const output = join(root, "contracts.ts");
// Catches a production bridge that silently produces an ABI module from incomplete artifacts.
await expectRejects(() => syncArtifacts({ bankArtifactPath: bank, bankV2ArtifactPath: bankV2, tokenArtifactPath: token, outputPath: output }), /BankV1 artifact/i);
await writeFile(bank, JSON.stringify({ abi: bankAbi }));
await writeFile(token, JSON.stringify({ abi: tokenAbi }));
await expectRejects(() => syncArtifacts({ bankArtifactPath: bank, bankV2ArtifactPath: bankV2, tokenArtifactPath: token, outputPath: output }), /BankV2 artifact/i);
await writeFile(bankV2, JSON.stringify({ abi: bankV2Abi }));
await rm(token);
await expectRejects(() => syncArtifacts({ bankArtifactPath: bank, bankV2ArtifactPath: bankV2, tokenArtifactPath: token, outputPath: output }), /MockUSDC artifact/i);
await writeFile(token, JSON.stringify({ abi: tokenAbi }));
// Catches a bridge that exports an ABI missing a V1 contract function or event.
assert.throws(() => extractAbi({ abi: bankAbi.filter((entry) => entry.name !== "Withdrawn") }, "BankV1"), /Withdrawn/);
// Catches generated calls/log decoders accepting ABI entries with the right name but wrong wire signature.
const wrongFunction = structuredClone(bankAbi);
wrongFunction.find((entry) => entry.name === "balanceOf").outputs = [];
assert.throws(() => extractAbi({ abi: wrongFunction }, "BankV1"), /signature.*balanceOf/i);
const wrongEvent = structuredClone(bankAbi);
wrongEvent.find((entry) => entry.name === "Deposited").inputs[0].indexed = false;
assert.throws(() => extractAbi({ abi: wrongEvent }, "BankV1"), /signature.*Deposited/i);
assert.throws(() => extractAbi({ abi: bankV2Abi.filter((entry) => entry.name !== "transferBalance") }, "BankV2"), /transferBalance/);
assert.throws(() => extractAbi({ abi: bankV2Abi.filter((entry) => entry.name !== "BalanceTransferred") }, "BankV2"), /BalanceTransferred/);
const rendered = renderContractsModule(extractAbi({ abi: bankAbi }, "BankV1"), extractAbi({ abi: bankV2Abi }, "BankV2"), extractAbi({ abi: tokenAbi }, "MockUSDC"));
assert.match(rendered, /export const bankV1Abi = .* as const;/s);
assert.match(rendered, /export const bankV2Abi = .* as const;/s);
assert.match(rendered, /export const mockUsdcAbi = .* as const;/s);
assert.match(rendered, /transferBalance/);
assert.match(rendered, /BalanceTransferred/);
// Catches a bridge that requires an active manifest or reads V2 as part of ABI generation.
await syncArtifacts({ bankArtifactPath: bank, bankV2ArtifactPath: bankV2, tokenArtifactPath: token, outputPath: output });
assert.equal(await readFile(output, "utf8"), rendered);
await writeFile(output, "stale\n");
await expectRejects(() => syncArtifacts({ bankArtifactPath: bank, bankV2ArtifactPath: bankV2, tokenArtifactPath: token, outputPath: output, check: true }), /stale/i);
});
await withFixture(async (root) => {
const activePath = join(root, "active.json");
const outputPath = join(root, "deployment.json");
await writeFile(activePath, JSON.stringify(manifest));
// Catches a publisher that copies pending, secret-bearing, or malformed live state into the browser bundle.
await publishManifest({ activePath, outputPath });
assert.deepEqual(JSON.parse(await readFile(outputPath, "utf8")), manifest);
for (const invalid of [
{ ...manifest, deploymentBlock: 0 },
{ ...manifest, rpcUrl: "https://token@example.test" },
{ ...manifest, rpcUrl: "https://example.test/?secret=value" },
{ ...manifest, privateKey: "not-public" },
{ ...manifest, proxy: "0x0000000000000000000000000000000000000000" },
]) {
await writeFile(activePath, JSON.stringify(invalid));
await expectRejects(() => publishManifest({ activePath, outputPath }), /manifest|deployment|credential|secret|address/i);
}
});
console.log("artifact bridge tests passed");