@scarretero/besu-docker-manager
Version:
Library for managing Besu node networks using Docker
733 lines (725 loc) • 33.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileService = exports.CryptoLib = exports.DockerNetwork = void 0;
exports.executeCommand = executeCommand;
exports.createKeys = createKeys;
const elliptic_1 = require("elliptic");
const ethers_1 = require("ethers");
const buffer_1 = require("buffer");
const keccak256_1 = __importDefault(require("keccak256"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const child_process_1 = require("child_process");
const rootDir = path_1.default.resolve();
function executeCommand(command) {
try {
return (0, child_process_1.execSync)(command, { encoding: 'utf-8' });
}
catch (error) {
if (error instanceof Error) {
throw new Error(`Command execution failed: ${error.message}`);
}
throw error;
}
}
class DockerNetwork {
static BASE_PATH = path_1.default.resolve(rootDir, "networks");
_networkData;
_name;
_fileService;
_containers = [];
_besuNodes = [];
_chainId;
_newMinerIsSigner = false;
constructor(name) {
this._name = name;
this._fileService = new FileService(DockerNetwork.BASE_PATH);
try {
// Get network metadata from docker inspect
const networkData = JSON.parse(executeCommand(`docker network inspect ${name}`));
//console.log("\x1b[33m%s\x1b[0m", `Network data:`);
//console.log(networkData);
this._networkData = networkData;
// Read chain ID from genesis file if it exists
try {
const genesisContent = this._fileService.readFileSync(name, "genesis.json");
const genesis = JSON.parse(genesisContent);
this._chainId = genesis.config.chainId;
}
catch (error) {
//console.log(`Could not read genesis file: ${error}`);
this._chainId = 1337; // default
}
// Populate existing nodes from docker containers
this._populateExistingNodes();
}
catch (error) {
console.log(`Network ${name} may not exist yet: ${error}`);
this._networkData = null;
this._chainId = 1337;
}
}
_populateExistingNodes() {
try {
// Get container IDs first
const containerIds = executeCommand(`docker ps -aq --filter "network=${this._name}"`).trim();
// Only proceed if there are containers
if (!containerIds) {
return;
}
// CORRECTION: split IDs and inspect each container individually
const containerIdList = containerIds.split('\n').filter(Boolean);
if (containerIdList.length === 0)
return;
for (const containerId of containerIdList) {
const containerJson = executeCommand(`docker inspect ${containerId}`);
const containers = JSON.parse(containerJson);
for (const container of containers) {
const labels = container.Config.Labels;
if (labels && labels.network === this._name) {
let nodeType;
for (const possibleType of ['bootnode', 'miner', 'rpc', 'node']) {
if (labels.nodo && labels.nodo.startsWith(possibleType)) {
nodeType = possibleType;
break;
}
}
if (!nodeType)
continue;
const port = labels.port;
const networkSettings = container.NetworkSettings.Networks[this._name];
const ip = networkSettings ? networkSettings.IPAddress : '';
this._besuNodes.push({
name: container.Name.replace('/', ''),
ip: ip,
port: port,
type: nodeType
});
}
}
}
}
catch (error) {
console.log(`No existing containers found for network ${this._name}: ${error}`);
}
}
get fileService() {
return this._fileService;
}
get networkData() {
return this._networkData;
}
get containers() {
return this._containers;
}
get besuNodes() {
return this._besuNodes;
}
get chainId() {
return this._chainId;
}
static create(name, chainId, subnet, label, signerAddress = '', prefundedAddresses = [], values = [], newMinerIsSigner = false) {
const fileService = new FileService(DockerNetwork.BASE_PATH);
// create folder
if (!fs_1.default.existsSync(DockerNetwork.BASE_PATH)) {
fileService.createFolder(name);
}
// create docker network
label.push({ key: "folderBase", value: path_1.default.join(process.cwd(), "networks") });
label.push({ key: "folder", value: path_1.default.join(process.cwd(), "networks", name) });
const dockerNetwork = DockerNetwork.createDockerNetwork(name, subnet, label);
// Generate keys for different node types
const bootnodeKeys = createKeys(fileService, name, subnet, "bootnode");
var minerKeys;
if (signerAddress !== '')
minerKeys = createKeys(fileService, name, subnet, "miner", signerAddress);
else
minerKeys = createKeys(fileService, name, subnet, "miner");
const validatorAddress = minerKeys.address;
const rpcKeys = createKeys(fileService, name, subnet, "rpc");
const nodeKeys = createKeys(fileService, name, subnet, "node");
// Create genesis with miner as initial signatory and prefunded addresses
const genesis = createGenesis(chainId, validatorAddress, prefundedAddresses, values);
fileService.createFile(name, "genesis.json", JSON.stringify(genesis, null, 2));
// Create config with bootnode enode
const config = createConfig(bootnodeKeys.enode);
fileService.createFile(name, "config.toml", config);
// Create config for fullnode with bootnode enode
const configFullNode = createConfigFullNode(bootnodeKeys.enode);
fileService.createFile(name, "config-fullnode.toml", configFullNode);
const dockerNetworkInstance = new DockerNetwork(name);
dockerNetworkInstance._chainId = chainId;
dockerNetworkInstance._newMinerIsSigner = newMinerIsSigner;
return dockerNetworkInstance;
function createConfig(bootnode) {
return `genesis-file="/data/genesis.json"
# Networking
p2p-host="0.0.0.0"
p2p-port=30303
p2p-enabled=true
# Bootnodes
bootnodes=["${bootnode}"]
# JSON-RPC
rpc-http-enabled=true
rpc-http-host="0.0.0.0"
rpc-http-port=8545
rpc-http-cors-origins=["*"]
rpc-http-api=["ETH","NET","CLIQUE","ADMIN","MINER","TRACE","DEBUG","TXPOOL","PERM"]
host-allowlist=["*"]
# Mining (will be enabled only for miner nodes)
miner-enabled=false
miner-coinbase="0x0000000000000000000000000000000000000000"
`;
}
function createConfigFullNode(bootnode) {
return `genesis-file="/data/genesis.json"
# Networking
p2p-host="0.0.0.0"
p2p-port=30303
p2p-enabled=true
# Bootnodes
bootnodes=["${bootnode}"]
`;
}
function createGenesis(chainId, address, prefunded = [], values = []) {
const validatorAddress = address.replace("0x", "");
const alloc = {};
// Add miner address
alloc[`0x${validatorAddress}`] = {
"balance": "0x2000000000000000000000000000000000000000000000000000000000000"
};
// Add prefunded addresses
for (let i = 0; i < prefunded.length; i++) {
const addr = prefunded[i].replace("0x", "");
let value = values[i] !== undefined ? values[i] : "0x1000000000000000000000000000000000000000000000000000000000000";
// Conversion ETH -> wei if string
if (typeof value === "string" && !value.startsWith("0x")) {
// Use ethers for conversion ETH -> wei
value = BigInt(ethers_1.ethers.parseEther(value).toString());
}
if (typeof value === "bigint")
value = "0x" + value.toString(16);
alloc[`0x${addr}`] = { balance: value };
}
return {
"config": {
"chainId": chainId,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"clique": {
"blockperiodseconds": 4,
"epochlength": 30000,
"createemptyblocks": true
}
},
"extraData": `0x0000000000000000000000000000000000000000000000000000000000000000${validatorAddress}0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`,
"gasLimit": "0x47b760",
"difficulty": "0x1",
"alloc": alloc
};
}
}
static createDockerNetwork(name, subnet, label) {
const labelString = label.map(l => `${l.key}=${l.value}`).join(',');
console.log(`Creating docker network ${name} with subnet ${subnet} and labels ${labelString}`);
try {
executeCommand(`docker network create ${name} --subnet ${subnet} --label ${labelString}`);
}
catch (error) {
throw new Error(`Failed to create docker network: ${error}`);
}
return new DockerNetwork(name);
}
static async removeDockerNetwork(name) {
const networkPath = path_1.default.join(DockerNetwork.BASE_PATH, name);
// remove folder
if (fs_1.default.existsSync(networkPath)) {
fs_1.default.rmSync(networkPath, { recursive: true, force: true });
}
// remove containers from docker network
try {
executeCommand(`docker rm -f $(docker ps -aq --filter "network=${name}") 2>/dev/null || true`);
// Wait for containers to be removed
await new Promise(resolve => setTimeout(resolve, 3000));
}
catch (error) {
console.log(`Error removing containers from network: ${error}`);
}
// remove docker network
try {
executeCommand(`docker network rm ${name} 2>/dev/null || true`);
// Wait for network to be removed
await new Promise(resolve => setTimeout(resolve, 3000));
}
catch (error) {
console.log(`Error removing network: ${error}`);
}
}
async addNode(nodeName, nodeType, port, ip) {
// Remove existing container if it exists
try {
executeCommand(`docker rm -f ${this._name}-${nodeName} 2>/dev/null || true`);
}
catch (error) {
console.log(`Container ${this._name}-${nodeName} doesn't exist or couldn't be removed: ${error}`);
}
let nodeDirName = nodeName;
if (nodeType === 'miner') {
// Not work if the network is created with the AI Manager because the MCP server use another port for miner
// Get all existing miner ports and the new miner port to determine the directory name
/*const allMinerPorts = this._besuNodes
.filter(n => n.type === 'miner')
.map(n => parseInt(n.port));
allMinerPorts.push(parseInt(port));
const lowestPort = Math.min(...allMinerPorts);
// If it's the first miner (the one with the lowest port), use "miner" as the directory name else use "miner{port}"
nodeDirName = parseInt(port) === lowestPort ? "miner" : `miner${port}`;*/
// From network page, the first miner started at 18555
// From MCP server, the first miner started at 28555
const portNum = parseInt(port);
const isFirstMiner = (portNum >= 18555 && portNum <= 18564) ||
(portNum >= 28555 && portNum <= 28564);
nodeDirName = isFirstMiner ? "miner" : `miner${port}`;
}
// Create the node directory name
const nodeDir = path_1.default.join(this._fileService.folder, this._name, nodeDirName);
if (!fs_1.default.existsSync(nodeDir)) {
fs_1.default.mkdirSync(nodeDir, { recursive: true });
const subnet = this._networkData?.[0]?.IPAM?.Config?.[0]?.Subnet || "";
createKeys(this._fileService, this._name, subnet, nodeDirName);
}
try {
let dockerCommand;
if (nodeType === "bootnode") {
if (ip) {
// change ip in enode file
var default_ip = `${ip.split('.').slice(0, -1).join('.')}.10`;
if (default_ip !== ip) {
//console.log(`Cambiar IP en el enode por ${ip}`);
const bootnodeEnode = await this._fileService.readFile(this._name, "bootnode/enode");
const updatedEnode = bootnodeEnode.replace(/@([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):30303/, `@${ip}:30303`);
await this._fileService.createFile(this._name, "bootnode/enode", updatedEnode);
const configToml = await this._fileService.readFile(this._name, "config.toml");
const updatedConfigToml = configToml.replace(/@([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):30303/, `@${ip}:30303`);
await this._fileService.createFile(this._name, "config.toml", updatedConfigToml);
const configFullnodeToml = await this._fileService.readFile(this._name, "config-fullnode.toml");
const updatedConfigFullnodeToml = configFullnodeToml.replace(/@([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):30303/, `@${ip}:30303`);
await this._fileService.createFile(this._name, "config-fullnode.toml", updatedConfigFullnodeToml);
}
}
dockerCommand = `docker run -d \
--name ${this._name}-${nodeName} \
--label nodo=${nodeName} \
${ip ? `--ip ${ip}` : ""} \
--label network=${this._name} \
--label port=${port} \
-p ${port}:8545 \
--network ${this._name} \
-v ${this._fileService.folder}/${this._name}:/data \
hyperledger/besu:latest \
--config-file=/data/config.toml \
--data-path=/data/${nodeName}/data \
--node-private-key-file=/data/${nodeName}/key \
--genesis-file=/data/genesis.json`;
}
else if (nodeType === "miner") {
// Read miner address for coinbase
const minerAddress = await this._fileService.readFile(this._name, `${nodeDirName}/address`);
dockerCommand = `docker run -d \
--name ${this._name}-${nodeName} \
--label nodo=${nodeName} \
${ip ? `--ip ${ip}` : ""} \
--label network=${this._name} \
--label port=${port} \
-p ${port}:8545 \
--network ${this._name} \
-v ${this._fileService.folder}/${this._name}:/data \
hyperledger/besu:latest \
--config-file=/data/config.toml \
--data-path=/data/${nodeDirName}/data \
--node-private-key-file=/data/${nodeDirName}/key \
--genesis-file=/data/genesis.json \
--miner-enabled=true \
--miner-coinbase=${minerAddress.trim()}`;
}
else if (nodeType === "rpc") {
// RPC node
dockerCommand = `docker run -d \
--name ${this._name}-${nodeName} \
--label nodo=${nodeName} \
${ip ? `--ip ${ip}` : ""} \
--label network=${this._name} \
--label port=${port} \
-p ${port}:8545 \
--network ${this._name} \
-v ${this._fileService.folder}/${this._name}:/data \
hyperledger/besu:latest \
--config-file=/data/config.toml \
--data-path=/data/${nodeName}/data \
--genesis-file=/data/genesis.json`;
}
else {
// Full node
dockerCommand = `docker run -d \
--name ${this._name}-${nodeName} \
--label nodo=${nodeName} \
${ip ? `--ip ${ip}` : ""} \
--label network=${this._name} \
--network ${this._name} \
-v ${this._fileService.folder}/${this._name}:/data \
hyperledger/besu:latest \
--config-file=/data/config-fullnode.toml \
--data-path=/data/${nodeName}/data \
--genesis-file=/data/genesis.json`;
}
//console.log('\x1b[34m%s\x1b[0m', `Executing command: ${dockerCommand}`);
executeCommand(dockerCommand);
console.log(`Node ${nodeType} added to network ${this._name}`);
// Add node to nodes list
this._besuNodes.push({
name: `${this._name}-${nodeName}`,
ip: ip || '',
port: port,
type: nodeType
});
// Wait a bit for the node to start
await this._sleep(5000);
}
catch (error) {
throw new Error(`Error adding ${nodeType} node: ${error}`);
}
}
async addBootnode(name, port, ip) {
await this.addNode(name, 'bootnode', port, ip);
}
async addMiner(name, port, ip) {
await this.addNode(name, 'miner', port, ip);
// Wait longer for miner to be ready and synced
await this._sleep(7500);
// If the new miner is a signer, add it to Clique consensus
if (this._newMinerIsSigner) {
// Add miner as a signatory in Clique consensus
try {
// Get existing miner nodes (excluding the new one)
const existingMiners = this._besuNodes.filter(node => node.type === 'miner' && node.port !== port);
// If there are no existing miners, the new miner is automatically a signer (from genesis)
if (existingMiners.length === 0) {
console.log('First miner - no proposals needed');
return;
}
// Get the address of the new miner
let newMinerAddress = await this._fileService.readFile(this._name, `${name}/address`);
newMinerAddress = newMinerAddress.trim();
if (!newMinerAddress.startsWith("0x")) {
newMinerAddress = "0x" + newMinerAddress;
}
// Check current signers before proposals
const provider = new ethers_1.ethers.JsonRpcProvider(`http://localhost:${existingMiners[0].port}`);
const currentSigners = await provider.send("clique_getSigners", []);
console.log('Current signers before proposals:', currentSigners);
// Each existing miner needs to propose the new miner
for (const existingMiner of existingMiners) {
try {
const minerProvider = new ethers_1.ethers.JsonRpcProvider(`http://localhost:${existingMiner.port}`);
// Get the existing miner's address
// If the node has the lowest port, it's the first miner (folder "miner")
const allMinerPorts = existingMiners.map(m => parseInt(m.port));
const lowestPort = Math.min(...allMinerPorts);
const minerDirName = parseInt(existingMiner.port) === lowestPort ? "miner" : `miner${existingMiner.port}`;
let proposerAddress = await this._fileService.readFile(this._name, `${minerDirName}/address`);
proposerAddress = proposerAddress.trim();
if (!proposerAddress.startsWith("0x")) {
proposerAddress = "0x" + proposerAddress;
}
// Check if the proposer is actually a signer
const isSigner = await minerProvider.send("clique_getSigners", [])
.then(signers => signers.includes(proposerAddress.toLowerCase()));
if (!isSigner) {
console.log(`Warning: ${proposerAddress} (${minerDirName}) is not a signer, skipping proposal`);
continue;
}
// Propose the new miner as a signatory
const result = await minerProvider.send("clique_propose", [newMinerAddress, true]);
console.log(`Miner ${proposerAddress} (${minerDirName}) proposed new miner ${newMinerAddress}:`, result);
// Wait between proposals
await this._sleep(2000);
}
catch (error) {
console.error(`Error with miner ${existingMiner.port} proposing new miner:`, error);
}
}
// Wait for proposals to be processed
await this._sleep(5000);
// Check if the new miner was added successfully
const updatedSigners = await provider.send("clique_getSigners", []);
console.log('Current signers after proposals:', updatedSigners);
if (updatedSigners.includes(newMinerAddress.toLowerCase())) {
console.log(`Success: ${newMinerAddress} is now a signer`);
}
else {
console.log(`Warning: ${newMinerAddress} is not yet a signer. May need more votes.`);
}
}
catch (error) {
console.error("Error adding miner to Clique consensus:", error);
}
}
}
async addRpc(name, port, ip) {
await this.addNode(name, 'rpc', port, ip);
}
async addFullNode(name, port, ip) {
await this.addNode(name, 'node', port, ip);
}
async removeNode(nodeName) {
const containerName = `${this._name}-${nodeName}`;
try {
executeCommand(`docker rm -f ${containerName} 2>/dev/null || true`);
console.log(`Node ${nodeName} removed from network ${this._name}`);
// Remove node from nodes list
this._besuNodes = this._besuNodes.filter(node => node.name !== containerName);
}
catch (error) {
throw new Error(`Error removing node ${nodeName}: ${error}`);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
async start() {
for (const besuNode of this._besuNodes) {
try {
executeCommand(`docker start ${besuNode.name}`);
console.log(`Started ${besuNode.name}`);
}
catch (error) {
console.log(`Error starting container ${besuNode.name}: ${error}`);
}
await new Promise(resolve => setTimeout(resolve, 2500));
}
}
async stop() {
for (const besuNode of this._besuNodes) {
try {
executeCommand(`docker stop ${besuNode.name}`);
console.log(`Stopped ${besuNode.name}`);
}
catch (error) {
console.log(`Error stopping besu node ${besuNode.name}: ${error}`);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
async getBalance(address) {
const bootnodePort = this._besuNodes.find(node => node.type === 'bootnode')?.port;
if (!bootnodePort) {
throw new Error('Bootnode not found');
}
const provider = new ethers_1.ethers.JsonRpcProvider(`http://localhost:${bootnodePort}`);
const balance = await provider.getBalance(address);
return balance;
}
async test() {
const bootnodePort = this._besuNodes.find(node => node.type === 'bootnode')?.port;
if (!bootnodePort) {
throw new Error('Bootnode not found');
}
const provider = new ethers_1.ethers.JsonRpcProvider(`http://localhost:${bootnodePort}`);
// Get miner private key
let minerPrivateKey = await this._fileService.readFile(this._name, "miner/key");
minerPrivateKey = minerPrivateKey.trim();
if (!minerPrivateKey.startsWith("0x")) {
minerPrivateKey = "0x" + minerPrivateKey;
}
const wallet = new ethers_1.ethers.Wallet(minerPrivateKey, provider);
console.log(`Miner wallet address: ${wallet.address}`);
// Generate test addresses from mnemonic
const mnemonic = "test test test test test test test test test test test junk";
const addresses = [];
for (let i = 0; i < 10; i++) {
const fullPath = `m/44'/60'/0'/0/${i}`;
const testWallet = ethers_1.ethers.HDNodeWallet.fromPhrase(mnemonic, "", fullPath);
addresses.push(testWallet.address);
console.log(`Address ${i}: ${testWallet.address}`);
}
console.log("\nTransferring 100 ETH to test addresses...");
// Transfer to first 2 addresses
for (const address of addresses.slice(0, 2)) {
try {
const tx = await wallet.sendTransaction({
to: address,
value: ethers_1.ethers.parseEther("100")
});
console.log(`Sent 100 ETH to ${address}`);
console.log(`Transaction hash: ${tx.hash}`);
await tx.wait(); // Wait for transaction to be mined
console.log(`Transaction confirmed for ${address}`);
}
catch (error) {
console.error(`Error sending to ${address}:`, error);
}
}
return true;
}
async _sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
exports.DockerNetwork = DockerNetwork;
class CryptoLib {
ec;
constructor() {
this.ec = new elliptic_1.ec('secp256k1');
}
// Generate a new random key pair
generateKeyPair() {
const keyPair = this.ec.genKeyPair();
const privateKey = keyPair.getPrivate('hex');
const publicKey = keyPair.getPublic('hex');
const address = this.publicKeyToAddress(publicKey);
return {
privateKey,
publicKey,
address,
enode: ""
};
}
/*// Generate a key pair from a deterministic seed
generateKeyPairFromSeed(seed: string): KeyPair {
// Create a deterministic seed using keccak256
const seedHash = keccak256(Buffer.from(seed, 'utf8'));
// Generate key pair from the seed hash
const keyPair = this.ec.keyFromPrivate(seedHash);
const privateKey = keyPair.getPrivate('hex');
const publicKey = keyPair.getPublic('hex');
const address = this.publicKeyToAddress(publicKey);
return {
privateKey,
publicKey,
address,
enode: ""
};
}*/
sign(message, privateKey) {
const keyPair = this.ec.keyFromPrivate(privateKey);
const msgHash = (0, keccak256_1.default)(message);
const signature = keyPair.sign(msgHash);
return {
r: signature.r.toString('hex'),
s: signature.s.toString('hex'),
v: signature.recoveryParam
};
}
verify(message, signature, publicKey) {
const keyPair = this.ec.keyFromPublic(publicKey, 'hex');
const msgHash = (0, keccak256_1.default)(message);
return keyPair.verify(msgHash, {
r: signature.r,
s: signature.s
});
}
publicKeyToAddress(publicKey) {
// Remove '04' prefix if present
const pubKey = publicKey.startsWith('04') ? publicKey.slice(2) : publicKey;
// Convert hex public key string to Buffer
const pubKeyBuffer = buffer_1.Buffer.from(pubKey, 'hex');
// Hash the public key using Keccak-256
const addressBuffer = (0, keccak256_1.default)(pubKeyBuffer);
// Take last 20 bytes to get Ethereum address
const address = addressBuffer.slice(-20);
// Add '0x' prefix and convert to checksum address
return ethers_1.ethers.getAddress('0x' + buffer_1.Buffer.from(address).toString('hex'));
}
}
exports.CryptoLib = CryptoLib;
class FileService {
path;
constructor(path) {
this.path = path;
}
createFolder(folder) {
fs_1.default.mkdirSync(path_1.default.join(this.path, folder), { recursive: true });
}
async readFile(folder, file) {
return fs_1.default.readFileSync(path_1.default.join(this.path, folder, file), 'utf8');
}
readFileSync(folder, file) {
return fs_1.default.readFileSync(path_1.default.join(this.path, folder, file), 'utf8');
}
async createFile(folder, file, content) {
fs_1.default.writeFileSync(path_1.default.join(this.path, folder, file), content);
return file;
}
get folder() {
return this.path;
}
}
exports.FileService = FileService;
// --- Utility function to create node keys and files ---
function createKeys(fileService, name, subnet, nodeType, signerAddress = '') {
const cryptoLib = new CryptoLib();
var keys;
if (signerAddress !== '') {
// Search in Keypair directory if signerAddress exists
const baseDir = path_1.default.join(fileService.folder, "Keypair");
console.log(`Searching for existing keys in ${baseDir} for address ${signerAddress}`);
if (fs_1.default.existsSync(baseDir)) {
const subdirs = fs_1.default.readdirSync(baseDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
let found = false;
for (const dir of subdirs) {
const addressPath = path_1.default.join(baseDir, dir, 'address');
if (fs_1.default.existsSync(addressPath)) {
const addr = fs_1.default.readFileSync(addressPath, 'utf8').trim();
if (addr.toLowerCase() === signerAddress.toLowerCase()) {
// Load keys from the directory
const privPath = path_1.default.join(baseDir, dir, 'key');
const pubPath = path_1.default.join(baseDir, dir, 'publicKey');
if (fs_1.default.existsSync(privPath) && fs_1.default.existsSync(pubPath)) {
keys = {
privateKey: fs_1.default.readFileSync(privPath, 'utf8').trim(),
publicKey: fs_1.default.readFileSync(pubPath, 'utf8').trim(),
address: addr,
enode: ''
};
found = true;
break;
}
}
}
}
}
if (!keys) {
keys = cryptoLib.generateKeyPair();
}
}
else
keys = cryptoLib.generateKeyPair();
fileService.createFolder(`${name}/${nodeType}`);
fileService.createFile(`${name}/${nodeType}`, "key", keys.privateKey);
fileService.createFile(`${name}/${nodeType}`, "address", keys.address);
fileService.createFile(`${name}/${nodeType}`, "publicKey", keys.publicKey);
// Calculate default IP based on subnet and node type
const baseIP = subnet.split('/')[0].split('.').slice(0, -1).join('.');
var defaultIP = `${subnet.split('/')[0]
.split('.').slice(0, -1).join('.')}.10`;
const enode = `enode://${keys.publicKey.slice(2)}@${defaultIP}:30303`;
// Only save enode for bootnode
if (nodeType === 'bootnode') {
fileService.createFile(`${name}/${nodeType}`, "enode", enode);
}
return {
...keys,
enode: enode
};
}