UNPKG

@ethersphere/bee-factory

Version:

Local Ethereum Swarm development stack

380 lines 19.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.start = start; const chalk_1 = __importDefault(require("chalk")); const ora_1 = __importDefault(require("ora")); const ethers_1 = require("ethers"); const config_1 = require("../config"); const deploy_1 = require("../blockchain/deploy"); const fund_1 = require("../blockchain/fund"); const snapshot_1 = require("../blockchain/snapshot"); const keystore_1 = require("../keystore"); const manager_1 = require("../docker/manager"); const traffic_generator_1 = require("../services/traffic_generator"); async function start(options) { const { tag, fresh, blockTime } = options; console.log(chalk_1.default.bold.cyan('\nbee-factory — Local Bee Stack')); console.log(chalk_1.default.dim('────────────────────────────────────────\n')); // 1. Clean slate { const spinner = (0, ora_1.default)('Cleaning up existing containers...').start(); try { await (0, manager_1.cleanupAll)(); spinner.succeed(chalk_1.default.green('Clean slate ready.')); } catch (err) { spinner.fail(chalk_1.default.red('Cleanup failed.')); throw err; } } // Check for pre-built hub images (master → latest). If all 6 images are // available, pull them and skip the build + warmup steps entirely. const usePrebuilt = !fresh && await (0, manager_1.tryPullPrebuiltImages)(tag); if (usePrebuilt) { console.log(chalk_1.default.green(`✓ Pre-built images found — quickstart mode.\n`)); } // 2. Pull Anvil image (skipped when using pre-built hub images) if (!usePrebuilt) { const spinner = (0, ora_1.default)(`Pulling ${config_1.ANVIL_IMAGE}...`).start(); try { await (0, manager_1.pullImageIfNeeded)(config_1.ANVIL_IMAGE); spinner.succeed(chalk_1.default.green(`Image ready: ${config_1.ANVIL_IMAGE}`)); } catch (err) { spinner.fail(chalk_1.default.red(`Failed to pull ${config_1.ANVIL_IMAGE}`)); throw err; } } // 3. Ensure Bee image — skipped when using pre-built hub images if (!usePrebuilt) { const localImage = `${config_1.BEE_LOCAL_IMAGE}:${tag}`; if (await (0, manager_1.beeImageExists)(tag)) { console.log(chalk_1.default.green(`✓ Bee image ready: ${localImage}`)); } else { console.log(chalk_1.default.yellow(`Bee image ${localImage} not found — building from source.`)); console.log(chalk_1.default.dim(` Cloning ${config_1.BEE_REPO_URL} at ref "${tag}"...`)); console.log(chalk_1.default.dim(` This may take several minutes (compiling Go).\n`)); try { await (0, manager_1.buildBeeImage)(tag); console.log(chalk_1.default.green(`\n✓ Bee image built: ${localImage}`)); } catch (err) { console.log(chalk_1.default.red(`\nFailed to build Bee image.`)); throw err; } } console.log(); } // 4. Create Docker network { const spinner = (0, ora_1.default)('Creating Docker network...').start(); try { await (0, manager_1.createNetwork)(); spinner.succeed(chalk_1.default.green('Docker network ready.')); } catch (err) { spinner.fail(chalk_1.default.red('Failed to create Docker network.')); throw err; } } // 5. Start Anvil { const spinner = (0, ora_1.default)('Starting Anvil blockchain...').start(); console.log(chalk_1.default.dim(`\n Block time: ${blockTime ?? 'default (1)'} seconds\n`)); try { await (0, manager_1.startAnvil)(blockTime, usePrebuilt ? (0, manager_1.hubImageName)(config_1.ANVIL_CONTAINER, tag) : undefined); spinner.text = 'Waiting for Anvil RPC to be ready...'; await (0, manager_1.waitForHttp)(`http://localhost:${config_1.ANVIL_PORT}`, 60000); spinner.succeed(chalk_1.default.green(`Anvil running on port ${config_1.ANVIL_PORT}.`)); } catch (err) { spinner.fail(chalk_1.default.red('Failed to start Anvil.')); throw err; } } // 6. Deploy contracts + fund wallets, or restore from snapshot let addresses; if (usePrebuilt) { const spinner = (0, ora_1.default)('Restoring Anvil state from pre-built image...').start(); try { addresses = await (0, manager_1.restoreAnvilState)(); spinner.succeed(chalk_1.default.green('Anvil state restored from pre-built image.')); } catch (err) { spinner.fail(chalk_1.default.red('Failed to read contract addresses from pre-built image.')); throw err; } } else { const useSnapshot = !fresh && (0, snapshot_1.hasSnapshot)(); if (useSnapshot) { const spinner = (0, ora_1.default)('Loading Anvil state from snapshot...').start(); try { addresses = await (0, snapshot_1.applySnapshot)(); spinner.succeed(chalk_1.default.green('Anvil state loaded from snapshot (contracts + funded wallets restored).')); } catch (err) { spinner.fail(chalk_1.default.red('Failed to load snapshot.')); throw err; } } else { if (fresh) { console.log(chalk_1.default.dim(' --fresh: skipping snapshot, deploying from scratch.\n')); } // 5a. Deploy contracts { const spinner = (0, ora_1.default)('Deploying smart contracts...').start(); try { const provider = new ethers_1.ethers.JsonRpcProvider(`http://localhost:${config_1.ANVIL_PORT}`); addresses = await (0, deploy_1.deployContracts)(provider); spinner.succeed(chalk_1.default.green('Smart contracts deployed.')); console.log(chalk_1.default.dim(' BZZ Token: ') + chalk_1.default.white(addresses.bzzToken)); console.log(chalk_1.default.dim(' PostageStamp: ') + chalk_1.default.white(addresses.postageStamp)); console.log(chalk_1.default.dim(' PriceOracle: ') + chalk_1.default.white(addresses.priceOracle)); console.log(chalk_1.default.dim(' StakeRegistry: ') + chalk_1.default.white(addresses.stakeRegistry)); console.log(chalk_1.default.dim(' Redistribution: ') + chalk_1.default.white(addresses.redistribution)); console.log(chalk_1.default.dim(' SwapFactory: ') + chalk_1.default.white(addresses.swapFactory)); console.log(chalk_1.default.dim(' SwapPriceOracle:') + chalk_1.default.white(addresses.swapPriceOracle)); console.log(); } catch (err) { spinner.fail(chalk_1.default.red('Contract deployment failed.')); throw err; } } // 5b. Fund Bee wallets { const spinner = (0, ora_1.default)('Funding Bee node wallets...').start(); try { const provider = new ethers_1.ethers.JsonRpcProvider(`http://localhost:${config_1.ANVIL_PORT}`); await (0, fund_1.fundNodes)(provider, addresses.bzzToken); spinner.succeed(chalk_1.default.green('Bee wallets funded (1 ETH + 100 BZZ each).')); } catch (err) { spinner.fail(chalk_1.default.red('Failed to fund Bee wallets.')); throw err; } } // 5c. Save snapshot for future fast starts { const spinner = (0, ora_1.default)('Saving Anvil state snapshot...').start(); try { await (0, snapshot_1.saveSnapshot)(addresses); spinner.succeed(chalk_1.default.green('Anvil state snapshot saved — future starts will load instantly.')); } catch (err) { // Non-fatal: snapshot saving is best-effort spinner.warn(chalk_1.default.yellow(`Snapshot save failed (non-fatal): ${String(err)}`)); } } } } // end else (not usePrebuilt) // 7. Generate keystore files. Skipped in prebuilt mode — the committed images // already contain swarm.key / libp2p_v2.key / pss.key for each node. const keystoreMap = new Map(); if (!usePrebuilt) { const spinner = (0, ora_1.default)('Generating keystore files...').start(); try { const results = await (0, keystore_1.generateAllKeystores)(config_1.BEE_NODES); for (const { node, keystoreDir } of results) { keystoreMap.set(node.index, keystoreDir); } spinner.succeed(chalk_1.default.green('Keystore files generated.')); } catch (err) { spinner.fail(chalk_1.default.red('Failed to generate keystore files.')); throw err; } } // 8. Start queen node (bee-0) — wait for API only, not warmup yet. // Warmup requires peer events; the queen has no peers until workers connect. const queen = config_1.BEE_NODES[0]; { const spinner = (0, ora_1.default)(`Starting queen node (${queen.name})...`).start(); try { await (0, manager_1.startBeeNodeWithTag)(queen, addresses, keystoreMap.get(0), tag, undefined, blockTime, usePrebuilt ? (0, manager_1.hubImageName)(queen.name, tag) : undefined); spinner.text = `Waiting for queen API at port ${queen.apiPort}...`; await (0, manager_1.waitForContainerHttp)(queen.name, `http://localhost:${queen.apiPort}/health`, 120000); spinner.succeed(chalk_1.default.green(`Queen node API ready on port ${queen.apiPort}.`)); } catch (err) { spinner.fail(chalk_1.default.red(`Failed to start queen node.`)); throw err; } } // 9. Get queen bootnode address let queenBootnode; { const spinner = (0, ora_1.default)('Fetching queen bootnode address...').start(); try { queenBootnode = await (0, manager_1.getQueenBootnodeAddr)(queen.apiPort); spinner.succeed(chalk_1.default.green(`Queen bootnode: ${queenBootnode}`)); } catch (err) { spinner.fail(chalk_1.default.red('Failed to get queen bootnode address.')); throw err; } } // 10. Start all worker nodes in parallel and wait for each API to respond. const workers = config_1.BEE_NODES.slice(1); { const spinner = (0, ora_1.default)('Starting worker nodes...').start(); try { await Promise.all(workers.map(async (node) => { await (0, manager_1.startBeeNodeWithTag)(node, addresses, keystoreMap.get(node.index), tag, queenBootnode, blockTime, usePrebuilt ? (0, manager_1.hubImageName)(node.name, tag) : undefined); await (0, manager_1.waitForContainerHttp)(node.name, `http://localhost:${node.apiPort}/health`, 120000); spinner.info(chalk_1.default.green(`${node.name} API ready on port ${node.apiPort}.`)); spinner.start('Starting worker nodes...'); })); spinner.succeed(chalk_1.default.green('All worker nodes ready.')); } catch (err) { spinner.fail(chalk_1.default.red('A worker node failed to start.')); throw err; } } // 11. Wait for all nodes to finish warming up now that workers are peered with the queen. // The stabilization detector needs at least one peer event to start its timer; that only // happens after workers connect, so we defer this check until all containers are up. { const spinner = (0, ora_1.default)('Waiting for all nodes to finish warming up...').start(); try { await Promise.all(config_1.BEE_NODES.map((node) => (0, manager_1.waitForBeeReady)(node.name, node.apiPort, 120000))); spinner.succeed(chalk_1.default.green('All nodes warmed up.')); } catch (err) { spinner.fail(chalk_1.default.red('A node failed to finish warming up.')); throw err; } } // 11b. Force a full peer mesh. isWarmingUp flips as soon as each worker // connects to the queen, but worker↔worker links come from hive gossip whose // cycle is occasionally >2 min on CI. Calling /connect explicitly removes // that race so traffic generation and reserve sampling see all peers // immediately. { const spinner = (0, ora_1.default)('Forming peer mesh...').start(); try { await (0, manager_1.formPeerMesh)(config_1.BEE_NODES); spinner.succeed(chalk_1.default.green('Peer mesh formed.')); } catch (err) { spinner.fail(chalk_1.default.red('Failed to form peer mesh.')); throw err; } } // 12. Ensure reserve sampler is ready if (usePrebuilt) { // After state restore, Bee nodes need to complete a full redistribution round // to re-establish consensus_time before the reserve sampler becomes usable. // Mine well past one round and poll rchash directly — a fixed sleep is too // fragile because reserve indexing time varies with CI machine load. { const spinner = (0, ora_1.default)('Advancing chain and waiting for reserve sampler...').start(); try { await fetch(`http://localhost:${config_1.ANVIL_PORT}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'anvil_mine', params: ['0xa0'], id: 1 }), // 160 blocks > 1 round }); await (0, manager_1.waitForRchashReady)(config_1.BEE_NODES[0].apiPort, 180000); spinner.succeed(chalk_1.default.green('Reserve sampler ready.')); } catch (err) { spinner.fail(chalk_1.default.red('Reserve sampler did not become ready.')); throw err; } } } else { // Buy a batch and start uploading until Node 2 has at least 1 cheque const spinner = (0, ora_1.default)(`Ensuring Node 2 has at least 1 claimable cheque...`).start(); try { await (0, traffic_generator_1.generateTraffic)(); spinner.succeed(chalk_1.default.green(`Worker Node 2 has at least 1 cheque.`)); } catch (err) { spinner.fail(chalk_1.default.red('Failed to generate traffic and cheques.')); throw err; } // Uploaded chunks are rejected as "too new" by the reserve sampler until the next // redistribution round's reveal phase sets a later consensus timestamp. Mine past // one full round (152 blocks), then wait for Bee nodes to process the new blocks // (Bee polls the chain every 5 seconds). { const spinner = (0, ora_1.default)('Advancing chain past redistribution round for chunk eligibility...').start(); try { await fetch(`http://localhost:${config_1.ANVIL_PORT}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'anvil_mine', params: ['0xa0'], id: 1 }), // 160 blocks > 1 round }); await new Promise((r) => setTimeout(r, 25000)); // five Bee polling cycles spinner.succeed(chalk_1.default.green('Chunks eligible for reserve sampling.')); } catch (err) { spinner.fail(chalk_1.default.red('Failed to advance chain.')); throw err; } } } // 13. Summary table printSummary(addresses, tag); } function printSummary(addresses, tag) { console.log('\n' + chalk_1.default.bold.cyan('════════════════════════════════════════')); console.log(chalk_1.default.bold.cyan(' bee-factory Stack is Running')); console.log(chalk_1.default.bold.cyan('════════════════════════════════════════')); console.log(); console.log(chalk_1.default.bold('Bee Nodes:')); console.log(chalk_1.default.dim(' Image: ') + chalk_1.default.white(`${config_1.BEE_LOCAL_IMAGE}:${tag}`)); console.log(); const rows = [ ['Node', 'API Port', 'P2P Port'], ['────────────────', '────────', '────────'], ...config_1.BEE_NODES.map((n) => [ n.name, String(n.apiPort), String(n.p2pPort), ]), ]; for (const row of rows) { console.log(' ' + chalk_1.default.yellow(row[0].padEnd(20)) + chalk_1.default.white(row[1].padEnd(12)) + chalk_1.default.white(row[2])); } console.log(); console.log(chalk_1.default.bold('Anvil RPC:') + ' ' + chalk_1.default.white(`http://localhost:8545`)); console.log(chalk_1.default.bold('Chain ID: ') + ' ' + chalk_1.default.white('1337')); console.log(); console.log(chalk_1.default.bold('Smart Contracts:')); console.log(' ' + chalk_1.default.dim('BZZ Token: ') + chalk_1.default.white(addresses.bzzToken)); console.log(' ' + chalk_1.default.dim('PostageStamp: ') + chalk_1.default.white(addresses.postageStamp)); console.log(' ' + chalk_1.default.dim('PriceOracle: ') + chalk_1.default.white(addresses.priceOracle)); console.log(' ' + chalk_1.default.dim('StakeRegistry: ') + chalk_1.default.white(addresses.stakeRegistry)); console.log(' ' + chalk_1.default.dim('Redistribution: ') + chalk_1.default.white(addresses.redistribution)); console.log(' ' + chalk_1.default.dim('SwapFactory: ') + chalk_1.default.white(addresses.swapFactory)); console.log(' ' + chalk_1.default.dim('SwapPriceOracle: ') + chalk_1.default.white(addresses.swapPriceOracle)); console.log(); console.log(chalk_1.default.bold('Node Wallets:')); console.log(chalk_1.default.dim(' (Foundry test keys — never use in production)')); console.log(); for (const node of config_1.BEE_NODES) { const address = new ethers_1.ethers.Wallet(node.privateKey).address; const role = node.index === 0 ? 'queen' : `worker ${node.index}`; console.log(' ' + chalk_1.default.yellow(`${node.name}`) + chalk_1.default.dim(` (${role})`)); console.log(' ' + chalk_1.default.dim('Address: ') + chalk_1.default.white(address)); console.log(' ' + chalk_1.default.dim('Private Key: ') + chalk_1.default.dim(node.privateKey)); console.log(); } console.log(); console.log(chalk_1.default.dim('Run ') + chalk_1.default.bold('bee-factory stop') + chalk_1.default.dim(' to tear everything down.')); console.log(); } //# sourceMappingURL=start.js.map