@ethersphere/bee-factory
Version:
Local Ethereum Swarm development stack
767 lines • 31.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.beeImageExists = beeImageExists;
exports.buildBeeImage = buildBeeImage;
exports.hubImageName = hubImageName;
exports.tryPullPrebuiltImages = tryPullPrebuiltImages;
exports.restoreAnvilState = restoreAnvilState;
exports.pullImageIfNeeded = pullImageIfNeeded;
exports.createNetwork = createNetwork;
exports.removeNetwork = removeNetwork;
exports.stopAndRemoveContainer = stopAndRemoveContainer;
exports.startAnvil = startAnvil;
exports.startBeeNodeWithTag = startBeeNodeWithTag;
exports.cleanupAll = cleanupAll;
exports.waitForHttp = waitForHttp;
exports.waitForContainerHttp = waitForContainerHttp;
exports.waitForBeeReady = waitForBeeReady;
exports.waitForRchashReady = waitForRchashReady;
exports.getQueenBootnodeAddr = getQueenBootnodeAddr;
exports.formPeerMesh = formPeerMesh;
const dockerode_1 = __importDefault(require("dockerode"));
const http = __importStar(require("http"));
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
const config_1 = require("../config");
const docker = new dockerode_1.default();
// ---------------------------------------------------------------------------
// Bee image helpers
// ---------------------------------------------------------------------------
async function beeImageExists(ref) {
try {
await docker.getImage(`${config_1.BEE_LOCAL_IMAGE}:${ref}`).inspect();
return true;
}
catch {
return false;
}
}
function runCommand(cmd, args, options = {}) {
return new Promise((resolve, reject) => {
const proc = (0, child_process_1.spawn)(cmd, args, { stdio: options.stdio ?? 'inherit', cwd: options.cwd });
proc.on('close', (code) => {
if (code === 0)
resolve();
else
reject(new Error(`${cmd} exited with code ${code}`));
});
proc.on('error', reject);
});
}
async function buildBeeImage(ref) {
const image = `${config_1.BEE_LOCAL_IMAGE}:${ref}`;
const upstreamImage = `${config_1.BEE_LOCAL_IMAGE}-upstream:${ref}`;
const buildDir = path.join(os.tmpdir(), 'bee-factory-bee-build');
if (fs.existsSync(buildDir)) {
fs.rmSync(buildDir, { recursive: true, force: true });
}
try {
// 1. Build upstream bee image (declares VOLUME /home/bee/.bee).
await runCommand('git', ['clone', '--depth=1', '--branch', ref, config_1.BEE_REPO_URL, buildDir]);
await runCommand('docker', ['build', '--build-arg', 'REACHABILITY_OVERRIDE_PUBLIC=true', '-f', 'Dockerfile.dev', '-t', upstreamImage, '.'], { cwd: buildDir });
// 2. Rewrap the binary in our own image WITHOUT a VOLUME declaration. Docker
// `commit` cannot capture anything inside a declared volume, so the
// upstream image makes "stateful" published images impossible — keys,
// statestore, and reserve chunks would all be discarded on push.
const wrapperDir = path.join(buildDir, '.bee-factory-wrapper');
fs.mkdirSync(wrapperDir, { recursive: true });
fs.writeFileSync(path.join(wrapperDir, 'Dockerfile'), [
`FROM ${upstreamImage} AS upstream`,
'FROM debian:bookworm-slim',
'RUN apt-get update && apt-get install -y --no-install-recommends \\',
' ca-certificates iputils-ping netcat-openbsd telnet curl wget jq net-tools \\',
' && apt-get clean && rm -rf /var/lib/apt/lists/* \\',
' && groupadd -r bee --gid 999 \\',
' && useradd -r -g bee --uid 999 --no-log-init -m bee \\',
' && mkdir -p /home/bee/.bee/keys && chown -R 999:999 /home/bee/.bee',
'COPY --from=upstream /usr/local/bin/bee /usr/local/bin/bee',
'EXPOSE 1633/tcp 1634/tcp',
'USER bee',
'WORKDIR /home/bee',
'ENTRYPOINT ["bee"]',
'',
].join('\n'));
await runCommand('docker', ['build', '-t', image, '.'], { cwd: wrapperDir });
}
finally {
fs.rmSync(buildDir, { recursive: true, force: true });
}
}
// ---------------------------------------------------------------------------
// Hub image helpers
// ---------------------------------------------------------------------------
// Override via BEE_FACTORY_HUB_ORG to point at a different registry/org.
// In CI verification this is set to "localhost:5000" so the same publish+pull
// flow can be exercised end-to-end against a local Docker registry sidecar.
const HUB_ORG = process.env.BEE_FACTORY_HUB_ORG || 'ethersphere';
function normalizeHubTag(tag) {
return tag === 'master' ? 'latest' : tag;
}
function containerToHubName(containerName) {
if (containerName === config_1.ANVIL_CONTAINER)
return 'bee-factory-blockchain';
if (containerName === 'bee-factory-bee-0')
return 'bee-factory-queen';
const workerMatch = containerName.match(/^bee-factory-bee-(\d+)$/);
if (workerMatch)
return `bee-factory-worker-${workerMatch[1]}`;
return containerName;
}
function hubImageName(containerName, tag) {
return `${HUB_ORG}/${containerToHubName(containerName)}:${normalizeHubTag(tag)}`;
}
async function tryPullPrebuiltImages(tag) {
const images = [
hubImageName(config_1.ANVIL_CONTAINER, tag),
...config_1.BEE_NODES.map(n => hubImageName(n.name, tag)),
];
try {
for (const image of images) {
await pullImageIfNeeded(image);
}
return true;
}
catch {
return false;
}
}
async function restoreAnvilState() {
const tmpFile = path.join(os.tmpdir(), 'bee-factory-anvil-restore.json');
try {
await runCommand('docker', ['cp', `${config_1.ANVIL_CONTAINER}:/anvil-state.json`, tmpFile], { stdio: 'pipe' });
const data = JSON.parse(fs.readFileSync(tmpFile, 'utf8'));
await anvilLoadStateRpc(data.state);
return data.addresses;
}
finally {
if (fs.existsSync(tmpFile))
fs.unlinkSync(tmpFile);
}
}
function anvilLoadStateRpc(state) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ jsonrpc: '2.0', method: 'anvil_loadState', params: [state], id: 1 });
const options = {
hostname: 'localhost',
port: config_1.ANVIL_PORT,
path: '/',
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
timeout: 120000,
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk.toString()));
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (parsed.error)
return reject(new Error(`anvil_loadState: ${parsed.error.message}`));
resolve();
}
catch {
reject(new Error('Invalid JSON from Anvil RPC'));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('anvil_loadState timed out')); });
req.write(body);
req.end();
});
}
// ---------------------------------------------------------------------------
// Image helpers
// ---------------------------------------------------------------------------
async function pullImageIfNeeded(image) {
// Check if image already exists locally
try {
await docker.getImage(image).inspect();
return; // already present
}
catch {
// not found locally – pull it
}
await new Promise((resolve, reject) => {
docker.pull(image, (err, stream) => {
if (err)
return reject(err);
docker.modem.followProgress(stream, (err2) => {
if (err2)
return reject(err2);
resolve();
});
});
});
}
// ---------------------------------------------------------------------------
// Network helpers
// ---------------------------------------------------------------------------
async function createNetwork() {
const networks = await docker.listNetworks({ filters: { name: [config_1.DOCKER_NETWORK] } });
if (networks.length > 0)
return; // already exists
await docker.createNetwork({
Name: config_1.DOCKER_NETWORK,
Driver: 'bridge',
CheckDuplicate: true,
});
}
async function removeNetwork() {
const networks = await docker.listNetworks({ filters: { name: [config_1.DOCKER_NETWORK] } });
for (const net of networks) {
if (net.Name === config_1.DOCKER_NETWORK) {
const network = docker.getNetwork(net.Id);
try {
await network.remove();
}
catch {
// ignore – may already be gone
}
}
}
}
// ---------------------------------------------------------------------------
// Container lifecycle
// ---------------------------------------------------------------------------
async function removeContainerIfExists(name) {
try {
const container = docker.getContainer(name);
const info = await container.inspect();
if (info.State.Running) {
await container.stop({ t: 5 });
}
await container.remove({ force: true });
}
catch {
// container doesn't exist – that's fine
}
}
async function stopAndRemoveContainer(name) {
await removeContainerIfExists(name);
}
async function startAnvil(blockTime, imageOverride) {
await removeContainerIfExists(config_1.ANVIL_CONTAINER);
// The foundry image uses ENTRYPOINT ["/bin/sh", "-c"], so Cmd must be a
// single shell string — an array would have only the first element executed.
const anvilArgs = [
'anvil',
'--host', '0.0.0.0',
'--chain-id', String(config_1.CHAIN_ID),
'--accounts', '20',
'--balance', '10000',
'--block-time', `${blockTime || config_1.DEFAULT_BLOCK_TIME_IN_SECONDS}`,
];
const anvilCmd = anvilArgs.join(' ');
const container = await docker.createContainer({
name: config_1.ANVIL_CONTAINER,
Image: imageOverride ?? config_1.ANVIL_IMAGE,
Hostname: 'anvil',
Cmd: [anvilCmd],
ExposedPorts: { [`${config_1.ANVIL_PORT}/tcp`]: {} },
HostConfig: {
PortBindings: {
[`${config_1.ANVIL_PORT}/tcp`]: [{ HostIp: '0.0.0.0', HostPort: String(config_1.ANVIL_PORT) }],
},
NetworkMode: config_1.DOCKER_NETWORK,
},
NetworkingConfig: {
EndpointsConfig: {
[config_1.DOCKER_NETWORK]: { Aliases: ['anvil'] },
},
},
});
await container.start();
}
// Cache of `bee start --help` flag support per image, keyed by image ref.
// Probing spawns a container, so we only do it once per image.
const beeFlagSupportCache = new Map();
/**
* Returns true if the bee binary in `image` advertises `flag` in its
* `start --help` output. Newer bee (>= v2.8.1-rc3) requires
* `--bzz-token-address` on custom networks, while older versions reject it as
* an unknown flag — so we add it only when the binary actually supports it.
*/
async function beeSupportsFlag(image, flag) {
let flags = beeFlagSupportCache.get(image);
if (!flags) {
const help = await new Promise((resolve) => {
const proc = (0, child_process_1.spawn)('docker', ['run', '--rm', '--entrypoint', 'bee', image, 'start', '--help']);
let out = '';
proc.stdout.on('data', (chunk) => (out += chunk.toString()));
proc.stderr.on('data', (chunk) => (out += chunk.toString()));
proc.on('close', () => resolve(out));
proc.on('error', () => resolve(out));
});
flags = new Set((help.match(/--[a-z0-9-]+/g) ?? []));
beeFlagSupportCache.set(image, flags);
}
return flags.has(flag);
}
async function buildBeeCmd(config, contractAddresses, image, bootnodeAddr, blockTime) {
const cmd = [
'start',
'--full-node',
`--api-addr=:${config.apiPort}`,
`--p2p-addr=:${config.p2pPort}`,
`--blockchain-rpc-endpoint=http://anvil:${config_1.ANVIL_PORT}`,
`--block-time=${blockTime || config_1.DEFAULT_BLOCK_TIME_IN_SECONDS}`,
`--password=${config_1.BEE_NODE_PASSWORD}`,
'--verbosity=5',
`--network-id=${config_1.CHAIN_ID}`,
'--mainnet=false',
'--allow-private-cidrs',
'--welcome-message=bee-factory',
'--cors-allowed-origins=*',
'--skip-postage-snapshot',
'--warmup-time=1s',
'--swap-enable',
'--swap-initial-deposit=100000000000000000', // 10 BZZ
`--postage-stamp-address=${contractAddresses.postageStamp}`,
`--price-oracle-address=${contractAddresses.swapPriceOracle}`,
`--staking-address=${contractAddresses.stakeRegistry}`,
`--redistribution-address=${contractAddresses.redistribution}`,
`--swap-factory-address=${contractAddresses.swapFactory}`,
`--postage-stamp-start-block=${contractAddresses.postageStampStartBlock}`,
'--withdrawal-addresses-whitelist="0xd238ff944bacb478cbed5efcae784d7bf4f2ff80"'
];
// Newer bee requires the BZZ token address on custom networks; older versions
// reject the flag as unknown. Only pass it when the binary supports it.
if (await beeSupportsFlag(image, '--bzz-token-address')) {
cmd.push(`--bzz-token-address=${contractAddresses.bzzToken}`);
}
if (bootnodeAddr) {
cmd.push(`--bootnode=${bootnodeAddr}`);
}
return cmd;
}
async function startBeeNodeWithTag(config, contractAddresses, keystoreDir, tag, bootnodeAddr, blockTime, imageOverride) {
await removeContainerIfExists(config.name);
const hostname = config.name.replace(/^bee-factory-/, '');
const image = imageOverride ?? `${config_1.BEE_LOCAL_IMAGE}:${tag}`;
const cmd = await buildBeeCmd(config, contractAddresses, image, bootnodeAddr, blockTime);
const exposedPorts = {
[`${config.apiPort}/tcp`]: {},
[`${config.p2pPort}/tcp`]: {},
};
const portBindings = {
[`${config.apiPort}/tcp`]: [{ HostIp: '0.0.0.0', HostPort: String(config.apiPort) }],
[`${config.p2pPort}/tcp`]: [{ HostIp: '0.0.0.0', HostPort: String(config.p2pPort) }],
};
const container = await docker.createContainer({
name: config.name,
Image: image,
Hostname: hostname,
Cmd: cmd,
ExposedPorts: exposedPorts,
HostConfig: {
PortBindings: portBindings,
NetworkMode: config_1.DOCKER_NETWORK,
// No bind mounts on /home/bee/.bee — keys + statestore must live inside
// the container's writable layer so `docker commit` captures them when
// publishing stateful images.
},
NetworkingConfig: {
EndpointsConfig: {
[config_1.DOCKER_NETWORK]: { Aliases: [hostname] },
},
},
});
// Fresh mode: copy the deterministic swarm.key into the container before
// start so bee binds to the expected Ethereum address. Bee will generate
// libp2p_v2.key and pss.key itself on first run.
// Prebuilt mode (keystoreDir undefined): the committed image already has all
// three keys baked in — leave them alone.
if (keystoreDir) {
await runCommand('docker', ['cp', `${keystoreDir}/.`, `${config.name}:/home/bee/.bee/keys/`], { stdio: 'pipe' });
}
await container.start();
}
// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------
async function cleanupAll() {
// Find and remove all bee-factory-* containers
const containers = await docker.listContainers({ all: true });
const beeFactoryContainers = containers.filter((c) => c.Names.some((n) => n.startsWith('/bee-factory-')));
await Promise.all(beeFactoryContainers.map(async (c) => {
const container = docker.getContainer(c.Id);
try {
if (c.State === 'running') {
await container.stop({ t: 5 });
}
await container.remove({ force: true });
}
catch {
// ignore
}
}));
await removeNetwork();
}
// ---------------------------------------------------------------------------
// Health / readiness polling
// ---------------------------------------------------------------------------
/**
* Poll url until we get any HTTP response (any status), or 200 specifically.
* For Anvil (JSON-RPC over HTTP) we send a minimal eth_chainId POST because
* a plain GET returns 400 – but a 400 still proves the server is up.
*/
async function waitForHttp(url, timeoutMs) {
const deadline = Date.now() + timeoutMs;
const interval = 2000;
// Use a JSON-RPC probe for the Anvil RPC URL, plain GET otherwise
const isRpc = url.endsWith(':8545') || url.includes(':8545/');
while (Date.now() < deadline) {
const ok = isRpc ? await httpPostJsonRpc(url) : await httpGetAny(url);
if (ok)
return;
await sleep(interval);
}
throw new Error(`Timed out waiting for ${url} to become ready (${timeoutMs}ms)`);
}
/**
* Like waitForHttp but also watches the named container — if it exits before
* the URL becomes ready, immediately throws with the container's tail logs so
* CI failures are actionable rather than a bare timeout.
*/
async function waitForContainerHttp(containerName, url, timeoutMs) {
const deadline = Date.now() + timeoutMs;
const interval = 2000;
while (Date.now() < deadline) {
const ok = await httpGetAny(url);
if (ok)
return;
// Check if the container has already exited
try {
const info = await docker.getContainer(containerName).inspect();
if (!info.State.Running) {
const logs = await getContainerLogs(containerName);
throw new Error(`Container ${containerName} exited (code ${info.State.ExitCode}) before becoming ready.\n\nContainer logs:\n${logs}`);
}
}
catch (err) {
if (err instanceof Error && err.message.includes('exited'))
throw err;
// container inspect failed — container gone entirely
throw new Error(`Container ${containerName} disappeared before becoming ready.`);
}
await sleep(interval);
}
// Timed out — include logs to help diagnose the hang
const logs = await getContainerLogs(containerName).catch(() => '(unavailable)');
throw new Error(`Timed out waiting for ${url} to become ready (${timeoutMs}ms).\n\nContainer logs:\n${logs}`);
}
async function getContainerLogs(containerName) {
const container = docker.getContainer(containerName);
const stream = await container.logs({ stdout: true, stderr: true, tail: 50 });
// dockerode returns a Buffer for non-TTY containers
return stream.toString('utf8').trim();
}
/**
* Poll /status on a bee node until isWarmingUp is false, failing fast if the
* container exits. Call this after waitForContainerHttp confirms the API is up.
*/
async function waitForBeeReady(containerName, apiPort, timeoutMs) {
const deadline = Date.now() + timeoutMs;
const interval = 2000;
const url = `http://localhost:${apiPort}/status`;
while (Date.now() < deadline) {
try {
const body = await httpGetJson(url);
if (body.isWarmingUp === false)
return;
}
catch {
// not ready yet
}
try {
const info = await docker.getContainer(containerName).inspect();
if (!info.State.Running) {
const logs = await getContainerLogs(containerName);
throw new Error(`Container ${containerName} exited (code ${info.State.ExitCode}) during warmup.\n\nContainer logs:\n${logs}`);
}
}
catch (err) {
if (err instanceof Error && err.message.includes('exited'))
throw err;
throw new Error(`Container ${containerName} disappeared during warmup.`);
}
await sleep(interval);
}
const logs = await getContainerLogs(containerName).catch(() => '(unavailable)');
throw new Error(`Timed out waiting for ${containerName} to finish warming up (${timeoutMs}ms).\n\nContainer logs:\n${logs}`);
}
/**
* Poll /rchash until it returns 200. After restoring chain state and restarting
* Bee from a committed image, the reserve sampler needs the chain head to
* advance past a redistribution round and Bee to reprocess events before
* sampling chunks with proofs succeeds. Polling the actual endpoint is more
* reliable than a fixed sleep.
*/
async function waitForRchashReady(apiPort, timeoutMs) {
const deadline = Date.now() + timeoutMs;
const interval = 3000;
let lastError = 'no response';
while (Date.now() < deadline) {
try {
const addrs = await httpGetJson(`http://localhost:${apiPort}/addresses`);
const overlay = String(addrs.overlay ?? '');
if (overlay) {
const probe = await httpGetStatusAndBody(`http://localhost:${apiPort}/rchash/0/${overlay}/${overlay}`, 20000);
if (probe.status === 200)
return;
lastError = `status ${probe.status} body ${probe.body.slice(0, 200)}`;
}
else {
lastError = 'overlay address not yet available';
}
}
catch (err) {
lastError = err instanceof Error ? err.message : String(err);
}
await sleep(interval);
}
throw new Error(`Timed out waiting for rchash to become ready on port ${apiPort} (${timeoutMs}ms). Last: ${lastError}`);
}
/** Returns true if the server replies with any HTTP status (even 4xx). */
function httpGetAny(url) {
return new Promise((resolve) => {
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port ? Number(parsedUrl.port) : 80,
path: parsedUrl.pathname + parsedUrl.search,
method: 'GET',
timeout: 3000,
};
const req = http.request(options, (res) => {
res.resume();
resolve(true); // any response means server is up
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.end();
});
}
/** Probe an Anvil JSON-RPC endpoint with eth_chainId POST. */
function httpPostJsonRpc(url) {
return new Promise((resolve) => {
const body = JSON.stringify({ jsonrpc: '2.0', method: 'eth_chainId', params: [], id: 1 });
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port ? Number(parsedUrl.port) : 80,
path: parsedUrl.pathname + parsedUrl.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
timeout: 3000,
};
const req = http.request(options, (res) => {
res.resume();
resolve(res.statusCode === 200);
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.write(body);
req.end();
});
}
// ---------------------------------------------------------------------------
// Queen bootnode address
// ---------------------------------------------------------------------------
/**
* Poll GET /addresses on the queen node until it returns a non-empty list of
* underlay addresses, then return the first one that contains the internal
* Docker network IP (i.e. not a loopback or external address).
*/
async function getQueenBootnodeAddr(apiPort) {
const url = `http://localhost:${apiPort}/addresses`;
const deadline = Date.now() + 60000;
const interval = 2000;
while (Date.now() < deadline) {
try {
const body = await httpGetJson(url);
if (body && Array.isArray(body.underlay) && body.underlay.length > 0) {
// Prefer an address that is NOT loopback (127.0.0.1) and NOT IPv6 loopback
const nonLoopback = body.underlay.find((addr) => !addr.includes('127.0.0.1') && !addr.includes('/ip4/0.0.0.0'));
if (nonLoopback)
return nonLoopback;
// Fall back to first address
return body.underlay[0];
}
}
catch {
// not ready yet
}
await sleep(interval);
}
throw new Error('Timed out waiting for queen bootnode address');
}
/**
* Returns a node's Docker-network underlay multiaddr (the /ip4/172.x... entry),
* which is the only address other containers can dial. Polls /addresses until
* one is published.
*/
async function getNodeUnderlayAddr(apiPort, timeoutMs = 60000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const body = await httpGetJson(`http://localhost:${apiPort}/addresses`);
if (Array.isArray(body.underlay)) {
const onNet = body.underlay.find((a) => !a.includes('127.0.0.1') && !a.includes('/ip4/0.0.0.0') && !a.startsWith('/ip6/'));
if (onNet)
return onNet;
}
}
catch {
// not ready
}
await sleep(1000);
}
throw new Error(`Timed out waiting for underlay address on port ${apiPort}`);
}
/**
* Forces full peer mesh by POSTing /connect for every (A → B) pair. Bee
* normally fills its peer table via hive gossip from the bootnode, but the
* gossip cycle can take >2 min on noisy CI runners and races with
* generateTraffic's peer-existence check. Issuing /connect directly removes
* the race — a successful 200 means the libp2p handshake completed, after
* which the peer is in /peers immediately.
*
* 4xx from /connect (e.g. already connected) is treated as success; everything
* else is logged but not fatal so a single transient hiccup can't fail boot.
*/
async function formPeerMesh(nodes) {
const addrs = await Promise.all(nodes.map((n) => getNodeUnderlayAddr(n.apiPort)));
const tasks = [];
for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes.length; j++) {
if (i === j)
continue;
tasks.push(connectPeer(nodes[i].apiPort, addrs[j]).catch(() => { }));
}
}
await Promise.all(tasks);
}
function connectPeer(apiPort, peerMultiaddr) {
const path = `/connect/${peerMultiaddr.startsWith('/') ? peerMultiaddr.slice(1) : peerMultiaddr}`;
return new Promise((resolve, reject) => {
const req = http.request({ hostname: 'localhost', port: apiPort, path, method: 'POST', timeout: 15000 }, (res) => {
res.resume();
const code = res.statusCode ?? 0;
if (code < 500)
return resolve(); // 2xx = connected, 4xx = already connected / bad input — treat both as ok
reject(new Error(`/connect returned ${code}`));
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('/connect timed out')); });
req.end();
});
}
function httpGetStatusAndBody(url, timeoutMs) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port ? Number(parsedUrl.port) : 80,
path: parsedUrl.pathname + parsedUrl.search,
method: 'GET',
timeout: timeoutMs,
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk.toString()));
res.on('end', () => resolve({ status: res.statusCode ?? 0, body: data }));
});
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timed out'));
});
req.end();
});
}
function httpGetJson(url) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port ? Number(parsedUrl.port) : 80,
path: parsedUrl.pathname + parsedUrl.search,
method: 'GET',
timeout: 3000,
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk.toString()));
res.on('end', () => {
try {
resolve(JSON.parse(data));
}
catch {
reject(new Error('Invalid JSON'));
}
});
});
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timed out'));
});
req.end();
});
}
// ---------------------------------------------------------------------------
// Utilities
// ---------------------------------------------------------------------------
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
//# sourceMappingURL=manager.js.map