UNPKG

@ethersphere/bee-factory

Version:

Local Ethereum Swarm development stack

767 lines 31.6 kB
"use strict"; 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