import { spawn } from "child_process"; import { fileURLToPath } from "url"; import path from "path"; import process from "process"; import { readFile, writeFile } from "fs/promises"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootDir = path.resolve(__dirname, ".."); const blockchainDir = path.join(rootDir, "blockchain"); const rpcUrl = "http://127.0.0.1:8545"; const defaultDevPrivateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; const bootstrapFlag = "__LEXICHAIN_CHAIN_BOOTSTRAPPED"; const deployStatePath = path.join(blockchainDir, ".dev-deploy.json"); function spawnProcess(command, args, options) { return spawn(command, args, { shell: true, stdio: "inherit", ...options, }); } async function waitForRpc(timeoutMs) { const started = Date.now(); while (Date.now() - started < timeoutMs) { try { const response = await fetch(rpcUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [], }), }); if (response.ok) return; } catch { // Ignore transient connection failures while the node starts. } await new Promise((resolve) => setTimeout(resolve, 1000)); } throw new Error("Hardhat RPC did not become ready in time."); } async function isRpcReady() { try { const response = await fetch(rpcUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [], }), }); return response.ok; } catch { return false; } } function isHexAddress(value) { return /^0x[a-fA-F0-9]{40}$/.test(value); } async function hasContractCode(address) { if (!isHexAddress(address)) return false; try { const response = await fetch(rpcUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_getCode", params: [address, "latest"], }), }); if (!response.ok) return false; const payload = await response.json(); const code = typeof payload?.result === "string" ? payload.result : "0x"; return code !== "0x"; } catch { return false; } } async function readSavedAddress() { try { const raw = await readFile(deployStatePath, "utf8"); const parsed = JSON.parse(raw); const address = parsed?.address; return isHexAddress(address) ? address : ""; } catch { return ""; } } async function saveAddress(address) { try { await writeFile( deployStatePath, JSON.stringify({ address }, null, 2), "utf8", ); } catch { // Non-fatal in dev. } } async function resolveContractAddress() { const candidates = []; if (process.env.BLOCKCHAIN_CONTRACT_ADDRESS) { candidates.push(process.env.BLOCKCHAIN_CONTRACT_ADDRESS); } const savedAddress = await readSavedAddress(); if (savedAddress) candidates.push(savedAddress); const checked = new Set(); for (const candidate of candidates) { if (!candidate || checked.has(candidate)) continue; checked.add(candidate); if (await hasContractCode(candidate)) { console.log(`Using existing deployed contract: ${candidate}`); return candidate; } } console.log("Deploying contract to localhost..."); const deployedAddress = await runDeploy(); await saveAddress(deployedAddress); return deployedAddress; } function runDeploy() { return new Promise((resolve, reject) => { const deploy = spawn( "npx", ["hardhat", "run", "scripts/deploy.ts", "--network", "localhost"], { cwd: blockchainDir, shell: true, stdio: ["inherit", "pipe", "pipe"], env: process.env, }, ); let output = ""; deploy.stdout.on("data", (chunk) => { const text = chunk.toString(); output += text; process.stdout.write(text); }); deploy.stderr.on("data", (chunk) => { const text = chunk.toString(); output += text; process.stderr.write(text); }); deploy.on("close", (code) => { if (code !== 0) { reject(new Error("Hardhat deploy failed.")); return; } const addressMatch = output.match(/BLOCKCHAIN_CONTRACT_ADDRESS=(0x[a-fA-F0-9]{40})/) || output.match(/deployed to:\s*(0x[a-fA-F0-9]{40})/i); if (!addressMatch) { reject(new Error("Could not detect deployed contract address.")); return; } resolve(addressMatch[1]); }); }); } let nodeProcess; let nextProcess; let startedNodeHere = false; function shutdown(code) { if (nextProcess && !nextProcess.killed) { nextProcess.kill("SIGINT"); } if (startedNodeHere && nodeProcess && !nodeProcess.killed) { nodeProcess.kill("SIGINT"); } process.exit(code); } process.on("SIGINT", () => shutdown(0)); process.on("SIGTERM", () => shutdown(0)); (async () => { const rpcReady = await isRpcReady(); if (rpcReady) { console.log("Detected existing Hardhat node on 8545. Reusing it."); } else { console.log("Starting Hardhat node..."); startedNodeHere = true; nodeProcess = spawnProcess("npx", ["hardhat", "node"], { cwd: blockchainDir, }); await waitForRpc(30000); } const contractAddress = await resolveContractAddress(); console.log(`Using contract address: ${contractAddress}`); const env = { ...process.env, BLOCKCHAIN_NETWORK: "hardhat", BLOCKCHAIN_RPC_URL: rpcUrl, BLOCKCHAIN_CONTRACT_ADDRESS: contractAddress, BLOCKCHAIN_PRIVATE_KEY: process.env.BLOCKCHAIN_PRIVATE_KEY || defaultDevPrivateKey, [bootstrapFlag]: "1", }; if (process.env[bootstrapFlag] === "1") { throw new Error( "Recursive dev bootstrap detected. Use npm run dev:next inside the bootstrap script.", ); } console.log("Starting Next.js dev server..."); nextProcess = spawn("npm", ["run", "dev:next"], { cwd: rootDir, shell: true, stdio: "inherit", env, }); nextProcess.on("close", (code) => shutdown(code ?? 0)); })().catch((error) => { console.error(error instanceof Error ? error.message : error); shutdown(1); });