248 lines
6.3 KiB
JavaScript
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);
|
|
});
|