Readme
This commit is contained in:
247
scripts/dev-with-chain.mjs
Normal file
247
scripts/dev-with-chain.mjs
Normal file
@@ -0,0 +1,247 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user