Files
LexiChain/scripts/dev-with-chain.mjs
2026-05-10 18:25:58 +01:00

248 lines
6.3 KiB
JavaScript

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);
});