110 lines
6.0 KiB
JavaScript
110 lines
6.0 KiB
JavaScript
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")];
|
|
const bankV2Requirements = [
|
|
...bankRequirements,
|
|
functionSignature("transferBalance", [parameter("recipient", "address"), parameter("amount", "uint256")], [], "nonpayable"),
|
|
eventSignature("BalanceTransferred", [parameter("from", "address", true), parameter("to", "address", true), parameter("amount", "uint256", false)]),
|
|
];
|
|
|
|
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 === "BankV2" ? bankV2Requirements
|
|
: 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, bankV2Abi, 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 bankV2Abi = ${JSON.stringify(bankV2Abi, 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"),
|
|
bankV2ArtifactPath = resolve(repositoryRoot, "out/BankV2.sol/BankV2.json"),
|
|
tokenArtifactPath = resolve(repositoryRoot, "out/MockUSDC.sol/MockUSDC.json"),
|
|
outputPath = resolve(repositoryRoot, "web/src/generated/contracts.ts"),
|
|
check = false,
|
|
} = {}) {
|
|
const [bankArtifact, bankV2Artifact, tokenArtifact] = await Promise.all([
|
|
readArtifact(bankArtifactPath, "BankV1"),
|
|
readArtifact(bankV2ArtifactPath, "BankV2"),
|
|
readArtifact(tokenArtifactPath, "MockUSDC"),
|
|
]);
|
|
const contents = renderContractsModule(
|
|
extractAbi(bankArtifact, "BankV1"),
|
|
extractAbi(bankV2Artifact, "BankV2"),
|
|
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; });
|
|
}
|
|
}
|