@ethersphere/bee-factory
Version:
Local Ethereum Swarm development stack
380 lines • 19.7 kB
JavaScript
;
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