import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const bankRequirements = [ functionSignature("contractVersion", [], ["uint256"], "pure"), functionSignature("paused", [], ["bool"], "view"), functionSignature("asset", [], ["address"], "view"), functionSignature("owner", [], ["address"], "view"), functionSignature("totalLiabilities", [], ["uint256"], "view"), functionSignature("balanceOf", [parameter("account", "address")], ["uint256"], "view"), eventSignature("Deposited", [parameter("account", "address", true), parameter("amount", "uint256", false)]), eventSignature("Withdrawn", [parameter("account", "address", true), parameter("amount", "uint256", false)]), eventSignature("Paused", [parameter("account", "address", false)]), eventSignature("Unpaused", [parameter("account", "address", false)]), eventSignature("OwnershipTransferred", [parameter("previousOwner", "address", true), parameter("newOwner", "address", true)]), eventSignature("Upgraded", [parameter("implementation", "address", true)]), ]; const tokenRequirements = [functionSignature("balanceOf", [parameter("account", "address")], ["uint256"], "view")]; export function extractAbi(artifact, contractName) { if (!artifact || typeof artifact !== "object" || !Array.isArray(artifact.abi)) { throw new Error(`${contractName} artifact must contain an ABI`); } const requirements = contractName === "BankV1" ? bankRequirements : contractName === "MockUSDC" ? tokenRequirements : null; if (!requirements) throw new Error(`unsupported contract artifact ${contractName}`); for (const requirement of requirements) { const entries = artifact.abi.filter((entry) => entry && entry.type === requirement.type && entry.name === requirement.name); if (entries.length === 0) throw new Error(`${contractName} ABI is missing required ${requirement.type} ${requirement.name}`); if (!entries.some((entry) => matchesSignature(entry, requirement))) { throw new Error(`${contractName} ABI has an invalid signature for ${requirement.name}`); } } return artifact.abi; } function parameter(name, type, indexed) { return { name, type, ...(indexed === undefined ? {} : { indexed }) }; } function functionSignature(name, inputs, outputs, stateMutability) { return { type: "function", name, inputs, outputs: outputs.map((type) => parameter("", type)), stateMutability }; } function eventSignature(name, inputs) { return { type: "event", name, inputs, anonymous: false }; } function matchesSignature(entry, requirement) { return entry.stateMutability === requirement.stateMutability && entry.anonymous === requirement.anonymous && matchesParameters(entry.inputs, requirement.inputs) && (requirement.outputs === undefined || matchesParameters(entry.outputs, requirement.outputs)); } function matchesParameters(actual, expected) { return Array.isArray(actual) && actual.length === expected.length && actual.every((parameter, index) => { const required = expected[index]; return parameter && parameter.name === required.name && parameter.type === required.type && parameter.indexed === required.indexed; }); } export function renderContractsModule(bankV1Abi, mockUsdcAbi) { return `/* This file is generated by tools/sync-web-artifacts.mjs. Do not edit. */\n\nexport const bankV1Abi = ${JSON.stringify(bankV1Abi, null, 2)} as const;\n\nexport const mockUsdcAbi = ${JSON.stringify(mockUsdcAbi, null, 2)} as const;\n`; } export async function syncArtifacts({ bankArtifactPath = resolve(repositoryRoot, "out/BankV1.sol/BankV1.json"), tokenArtifactPath = resolve(repositoryRoot, "out/MockUSDC.sol/MockUSDC.json"), outputPath = resolve(repositoryRoot, "web/src/generated/contracts.ts"), check = false, } = {}) { const [bankArtifact, tokenArtifact] = await Promise.all([ readArtifact(bankArtifactPath, "BankV1"), readArtifact(tokenArtifactPath, "MockUSDC"), ]); const contents = renderContractsModule(extractAbi(bankArtifact, "BankV1"), extractAbi(tokenArtifact, "MockUSDC")); if (check) { let existing; try { existing = await readFile(outputPath, "utf8"); } catch { throw new Error("generated contracts module is stale or missing"); } if (existing !== contents) throw new Error("generated contracts module is stale"); return contents; } await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, contents); return contents; } async function readArtifact(path, contractName) { let contents; try { contents = await readFile(path, "utf8"); } catch { throw new Error(`${contractName} artifact is missing at ${path}`); } try { return JSON.parse(contents); } catch { throw new Error(`${contractName} artifact contains invalid JSON`); } } if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { const check = process.argv.slice(2).every((argument) => argument === "--check") && process.argv.includes("--check"); if (process.argv.slice(2).some((argument) => argument !== "--check")) { console.error("usage: sync-web-artifacts.mjs [--check]"); process.exitCode = 1; } else { syncArtifacts({ check }).catch((error) => { console.error(error.message); process.exitCode = 1; }); } }