/** * Helper: compile contracts/NotaryRegistry.sol + its two interfaces * + @openzeppelin/contracts Ownable using solc-js in-process. * * Keeps the E2E suite self-contained — no dependence on a prior * `hardhat compile` step, no new workspace wiring. */ import { readFileSync } from "fs"; import { dirname, join, resolve } from "path"; // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const solc = require("solc"); const REPO_ROOT = resolve(__dirname, "..", "..", "..", ".."); const CONTRACTS_ROOT = join(REPO_ROOT, "contracts"); const OZ_ROOT = join(CONTRACTS_ROOT, "node_modules", "@openzeppelin"); // ethers v6 accepts any JsonFragment-shaped array here. Declaring the // element type loosely keeps us decoupled from ethers' private type // exports while still being strictly typed against `unknown`. export type AbiFragment = Record; export interface CompiledArtifact { abi: AbiFragment[]; bytecode: string; } interface SolcSource { content: string; } interface SolcInput { language: "Solidity"; sources: Record; settings: { optimizer: { enabled: true; runs: number }; outputSelection: Record>; }; } interface SolcOutput { errors?: Array<{ severity: "error" | "warning"; formattedMessage: string }>; contracts: Record< string, Record >; } function readFromRoots(rel: string, roots: string[]): string { for (const root of roots) { try { return readFileSync(join(root, rel), "utf8"); } catch { // try next root } } throw new Error(`Could not resolve import ${rel} against roots ${roots.join(",")}`); } function findImports(requestedPath: string): { contents: string } | { error: string } { // @openzeppelin/... → contracts/node_modules/@openzeppelin/... if (requestedPath.startsWith("@openzeppelin/")) { const rel = requestedPath.replace("@openzeppelin/", ""); try { return { contents: readFileSync(join(OZ_ROOT, rel), "utf8") }; } catch (e) { return { error: `Could not read ${requestedPath}: ${(e as Error).message}` }; } } // Local ./interfaces/... paths resolve against contracts/ try { return { contents: readFromRoots(requestedPath, [CONTRACTS_ROOT]) }; } catch (e) { return { error: (e as Error).message }; } } /** * Recursively pull in all `import "..."` references starting from * NotaryRegistry.sol and return the full `sources` object solc needs. */ function collectSources(entryPath: string): Record { const sources: Record = {}; const stack: string[] = [entryPath]; const seen = new Set(); while (stack.length > 0) { const cur = stack.pop()!; if (seen.has(cur)) continue; seen.add(cur); let content: string; if (cur === entryPath) { content = readFileSync(join(CONTRACTS_ROOT, "NotaryRegistry.sol"), "utf8"); } else { const resolved = findImports(cur); if ("error" in resolved) { throw new Error(`Unresolved import: ${cur} (${resolved.error})`); } content = resolved.contents; } sources[cur] = { content }; // Parse `import "..."` statements. Interfaces may use relative paths // that we normalise back into keys solc expects. const importRe = /^\s*import\s+(?:\{[^}]+\}\s+from\s+)?"([^"]+)";/gm; let m: RegExpExecArray | null; while ((m = importRe.exec(content)) !== null) { const rawImport = m[1]; let normalised: string; if (rawImport.startsWith("@openzeppelin/")) { normalised = rawImport; } else if (rawImport.startsWith("./") || rawImport.startsWith("../")) { // Relative import — resolve against the dir of `cur`. const curDir = cur.includes("/") ? dirname(cur) : "."; const joined = join(curDir, rawImport); normalised = joined.startsWith(".") ? joined.slice(2) : joined; } else { normalised = rawImport; } if (!seen.has(normalised)) stack.push(normalised); } } return sources; } export function compileNotaryRegistry(): CompiledArtifact { const entry = "NotaryRegistry.sol"; const sources = collectSources(entry); const input: SolcInput = { language: "Solidity", sources, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] }, }, }, }; const output: SolcOutput = JSON.parse( solc.compile(JSON.stringify(input), { import: findImports }), ); const fatal = (output.errors ?? []).filter((e) => e.severity === "error"); if (fatal.length > 0) { const msg = fatal.map((e) => e.formattedMessage).join("\n"); throw new Error(`solc compile failed:\n${msg}`); } const artifact = output.contracts[entry]?.NotaryRegistry; if (!artifact) { throw new Error("NotaryRegistry not found in solc output"); } return { abi: artifact.abi, bytecode: "0x" + artifact.evm.bytecode.object, }; }