From 55307966787338d65d36971ec2a594beca8a5593 Mon Sep 17 00:00:00 2001 From: Arseniy Klempner Date: Thu, 4 Sep 2025 19:24:43 -0700 Subject: [PATCH] feat: set bootstrap ENR via cli arg --- .github/workflows/playwright.yml | 4 +- package-lock.json | 15 + packages/browser-tests/Dockerfile | 3 +- packages/browser-tests/README.md | 14 + packages/browser-tests/package.json | 2 + packages/browser-tests/playwright.config.ts | 17 - .../scripts/docker-entrypoint.sh | 5 + packages/browser-tests/src/browser/index.ts | 16 - packages/browser-tests/src/routes/waku.ts | 27 +- packages/browser-tests/src/server.ts | 82 ++-- .../src/utils/endpoint-handler.ts | 44 +-- .../browser-tests/tests/docker-server.spec.ts | 258 +++--------- packages/browser-tests/tests/server.spec.ts | 11 - packages/browser-tests/web/index.ts | 370 +++++++----------- 14 files changed, 281 insertions(+), 587 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 6c65b79d1f..55a92ddda4 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -30,8 +30,8 @@ jobs: - uses: ./.github/actions/npm - - name: Build browser test environment - run: npm run build --workspace=@waku/browser-tests + - name: Build entire monorepo + run: npm run build - name: Run Playwright tests run: npm run test --workspace=@waku/browser-tests diff --git a/package-lock.json b/package-lock.json index 8da83914a1..3617668135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34343,6 +34343,7 @@ "version": "0.1.0", "dependencies": { "@playwright/test": "^1.51.1", + "@waku/discovery": "^0.0.11", "@waku/sdk": "^0.0.34", "cors": "^2.8.5", "express": "^4.21.2" @@ -34351,6 +34352,7 @@ "@types/cors": "^2.8.15", "@types/express": "^4.17.21", "@types/node": "^20.10.0", + "@waku/discovery": "^0.0.11", "axios": "^1.8.4", "dotenv-flow": "^0.4.0", "esbuild": "^0.21.5", @@ -34760,6 +34762,19 @@ "undici-types": "~6.19.2" } }, + "packages/browser-tests/node_modules/@waku/discovery/node_modules/@waku/interfaces": { + "version": "0.0.26", + "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.26.tgz", + "integrity": "sha512-YZU4+1j8n7lEKFTz3RTHaNm4Jsv1kurG87G7ZZZwFRuzHjTVizGldI5Nfu8eUemr8dGIBCgadybXqJY/GjsLcA==", + "extraneous": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@waku/proto": "^0.0.8" + }, + "engines": { + "node": ">=20" + } + }, "packages/browser-tests/node_modules/dotenv": { "version": "7.0.0", "dev": true, diff --git a/packages/browser-tests/Dockerfile b/packages/browser-tests/Dockerfile index f98a57dbf2..24b5299c5e 100644 --- a/packages/browser-tests/Dockerfile +++ b/packages/browser-tests/Dockerfile @@ -47,7 +47,8 @@ ENV PORT=8080 \ NODE_ENV=production \ CHROMIUM_NO_SANDBOX=1 \ WAKU_CLUSTER_ID=${WAKU_CLUSTER_ID:-} \ - WAKU_SHARD=${WAKU_SHARD:-} + WAKU_SHARD=${WAKU_SHARD:-} \ + WAKU_ENR_BOOTSTRAP=${WAKU_ENR_BOOTSTRAP:-} EXPOSE 8080 diff --git a/packages/browser-tests/README.md b/packages/browser-tests/README.md index aeaa3fe5ea..f643ed6f01 100644 --- a/packages/browser-tests/README.md +++ b/packages/browser-tests/README.md @@ -96,6 +96,20 @@ Waku nodes are automatically created and started when the server launches. Confi - `WAKU_CLUSTER_ID`: Set the cluster ID (default: uses bootstrap configuration) - `WAKU_SHARD`: Set a specific shard (optional) - `WAKU_LIGHTPUSH_NODE`: Specify a preferred lightpush node address (optional) +- `WAKU_ENR_BOOTSTRAP`: Specify custom ENR bootstrap peers (comma-separated, optional) + +### Example: Using ENR Bootstrap Peers + +```bash +# Via Docker CLI +docker run -p 8080:8080 \ + -e WAKU_ENR_BOOTSTRAP="enr:-MS4QGcHBZAnpu6qNYe_T6TGDCV6c9_3UsXlj5XlXY6QvLCUQKqajqDfs0aKOs7BISJzGxA7TuDzYXap4sP6JYUZ2Y9GAYh2F0dG5ldHOIAAAAAAAAAACEZXRoMpEJZZp0BAAAAf__________gmlkgnY0gmlwhC5QoeSJc2VjcDI1NmsxoQOZxJYJVoTfwo7zEom6U6L5Txrs3H9X0P_XBJbbOZBczYYN1ZHCCdl8" \ + waku-browser-tests + +# Via Docker entrypoint argument +docker run -p 8080:8080 waku-browser-tests \ + --enr-bootstrap="enr:-MS4QGcHBZAnpu6qNYe_T6TGDCV6c9_3UsXlj5XlXY6QvLCUQKqajqDfs0aKOs7BISJzGxA7TuDzYXap4sP6JYUZ2Y9GAYh2F0dG5ldHOIAAAAAAAAAACEZXRoMpEJZZp0BAAAAf__________gmlkgnY0gmlwhC5QoeSJc2VjcDI1NmsxoQOZxJYJVoTfwo7zEom6U6L5Txrs3H9X0P_XBJbbOZBczYYN1ZHCCdl8" +``` ### Example: Dialing to specific peers with the Waku REST API compatible endpoint diff --git a/packages/browser-tests/package.json b/packages/browser-tests/package.json index f398d42fc5..eb88e80c5d 100644 --- a/packages/browser-tests/package.json +++ b/packages/browser-tests/package.json @@ -19,6 +19,7 @@ "@types/cors": "^2.8.15", "@types/express": "^4.17.21", "@types/node": "^20.10.0", + "@waku/discovery": "^0.0.11", "axios": "^1.8.4", "dotenv-flow": "^0.4.0", "esbuild": "^0.21.5", @@ -30,6 +31,7 @@ }, "dependencies": { "@playwright/test": "^1.51.1", + "@waku/discovery": "^0.0.11", "@waku/sdk": "^0.0.34", "cors": "^2.8.5", "express": "^4.21.2" diff --git a/packages/browser-tests/playwright.config.ts b/packages/browser-tests/playwright.config.ts index b4c5f72600..345d96bc4c 100644 --- a/packages/browser-tests/playwright.config.ts +++ b/packages/browser-tests/playwright.config.ts @@ -1,10 +1,6 @@ -// For dynamic import of dotenv-flow import { defineConfig, devices } from "@playwright/test"; -// Only load dotenv-flow in non-CI environments if (!process.env.CI) { - // Need to use .js extension for ES modules - // eslint-disable-next-line import/extensions try { await import("dotenv-flow/config.js"); } catch (e) { @@ -14,35 +10,22 @@ if (!process.env.CI) { const EXAMPLE_PORT = process.env.EXAMPLE_PORT || "8080"; const BASE_URL = `http://127.0.0.1:${EXAMPLE_PORT}`; -// Ignore docker-based tests on CI const TEST_IGNORE = process.env.CI ? ["tests/docker-*.spec.ts"] : []; -/** - * See https://playwright.dev/docs/test-configuration. - */ export default defineConfig({ testDir: "./tests", testIgnore: TEST_IGNORE, - /* Run tests in files in parallel */ fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, - /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 2 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { - /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: BASE_URL, - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry" }, - /* Configure projects for major browsers */ projects: [ { name: "chromium", diff --git a/packages/browser-tests/scripts/docker-entrypoint.sh b/packages/browser-tests/scripts/docker-entrypoint.sh index 7a449fa83c..f42cb01b13 100644 --- a/packages/browser-tests/scripts/docker-entrypoint.sh +++ b/packages/browser-tests/scripts/docker-entrypoint.sh @@ -32,6 +32,11 @@ while [[ $# -gt 0 ]]; do echo "Setting WAKU_LIGHTPUSH_NODE=${WAKU_LIGHTPUSH_NODE}" shift ;; + --enr-bootstrap=*) + export WAKU_ENR_BOOTSTRAP="${1#*=}" + echo "Setting WAKU_ENR_BOOTSTRAP=${WAKU_ENR_BOOTSTRAP}" + shift + ;; *) # Unknown argument, keep it for the main command break diff --git a/packages/browser-tests/src/browser/index.ts b/packages/browser-tests/src/browser/index.ts index bdf17daf3f..29a86f84ae 100644 --- a/packages/browser-tests/src/browser/index.ts +++ b/packages/browser-tests/src/browser/index.ts @@ -1,15 +1,10 @@ import { Browser, chromium, Page } from "@playwright/test"; -// Global variable to store the browser and page let browser: Browser | undefined; let page: Page | undefined; -/** - * Initialize browser and load the Waku web app - */ export async function initBrowser(appPort: number): Promise { try { - // Support sandbox-less mode for containers const launchArgs = process.env.CHROMIUM_NO_SANDBOX === "1" ? ["--no-sandbox", "--disable-setuid-sandbox"] @@ -26,12 +21,10 @@ export async function initBrowser(appPort: number): Promise { page = await browser.newPage(); - // Load the Waku web app await page.goto(`http://localhost:${appPort}/app/index.html`, { waitUntil: "networkidle", }); - // Wait for wakuApi to be available await page.waitForFunction( () => { return window.wakuApi && typeof window.wakuApi.createWakuNode === "function"; @@ -46,23 +39,14 @@ export async function initBrowser(appPort: number): Promise { } } -/** - * Get the current page instance - */ export function getPage(): Page | undefined { return page; } -/** - * Set the page instance (for use by server.ts) - */ export function setPage(pageInstance: Page | undefined): void { page = pageInstance; } -/** - * Closes the browser instance - */ export async function closeBrowser(): Promise { if (browser) { await browser.close(); diff --git a/packages/browser-tests/src/routes/waku.ts b/packages/browser-tests/src/routes/waku.ts index ac386d5887..84e99baae0 100644 --- a/packages/browser-tests/src/routes/waku.ts +++ b/packages/browser-tests/src/routes/waku.ts @@ -4,7 +4,6 @@ import { getPage } from "../browser/index.js"; const router = Router(); -// CORS preflight handlers const corsEndpoints = [ "/waku/v1/wait-for-peers", "/waku/v1/dial-peers", @@ -22,11 +21,8 @@ corsEndpoints.forEach(endpoint => { }); }); -// Node lifecycle is now handled automatically on server start -// Messaging endpoints -// Peer management endpoints router.post("/waku/v1/wait-for-peers", createEndpointHandler({ methodName: "waitForPeers", validateInput: (body) => [ @@ -44,7 +40,6 @@ router.post("/waku/v1/dial-peers", createEndpointHandler({ validateInput: validators.requirePeerAddrs })); -// Information endpoints (GET) router.get("/waku/v1/peer-info", createEndpointHandler({ methodName: "getPeerInfo", validateInput: validators.noInput @@ -65,32 +60,19 @@ router.get("/waku/v1/connection-status", createEndpointHandler({ validateInput: validators.noInput })); -// nwaku v3 lightpush endpoint + + router.post("/lightpush/v3/message", createEndpointHandler({ methodName: "pushMessageV3", - validateInput: (body: any): [string, string, string] => { + validateInput: (body: any): [string, string] => { const validatedRequest = validators.requireLightpushV3(body); - // For v3 API, we pass the base64 payload directly to the method - // The WakuHeadless pushMessageV3 method will handle base64 decoding return [ validatedRequest.message.contentTopic, - validatedRequest.message.payload, // Keep as base64 - validatedRequest.pubsubTopic + validatedRequest.message.payload, ]; }, handleError: errorHandlers.lightpushError, - preCheck: async () => { - try { - console.log("[Server] Waiting for Lightpush peers before sending message..."); - await getPage()?.evaluate(() => { - return window.wakuApi.waitForPeers?.(10000, ["lightpush"] as any); - }); - console.log("[Server] Found Lightpush peers"); - } catch (e) { - console.warn("[Server] No Lightpush peers found:", e); - } - }, transformResult: (result) => { if (result && result.successes && result.successes.length > 0) { console.log("[Server] Message successfully sent via v3 lightpush!"); @@ -108,7 +90,6 @@ router.post("/lightpush/v3/message", createEndpointHandler({ })); -// Custom handler for the execute endpoint since it needs special logic router.post("/waku/v1/execute", async (req, res) => { try { const { functionName, params = [] } = req.body; diff --git a/packages/browser-tests/src/server.ts b/packages/browser-tests/src/server.ts index 6b48604e28..ceb424bdc0 100644 --- a/packages/browser-tests/src/server.ts +++ b/packages/browser-tests/src/server.ts @@ -17,20 +17,14 @@ import * as fs from "fs"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const distRoot = path.resolve(__dirname, ".."); // server.js is in dist/src/, so go up to dist/ +const distRoot = path.resolve(__dirname, ".."); const webDir = path.resolve(distRoot, "web"); -console.log("Setting up static file serving:"); -console.log("__dirname:", __dirname); -console.log("webDir:", webDir); -console.log("Files in webDir:", fs.readdirSync(webDir)); -// Serve dynamic index.html with network configuration BEFORE static files app.get("/app/index.html", (_req: Request, res: Response) => { try { const htmlPath = path.join(webDir, "index.html"); let htmlContent = fs.readFileSync(htmlPath, "utf8"); - // Build network configuration from environment variables const networkConfig: any = {}; if (process.env.WAKU_CLUSTER_ID) { networkConfig.clusterId = parseInt(process.env.WAKU_CLUSTER_ID, 10); @@ -39,13 +33,13 @@ app.get("/app/index.html", (_req: Request, res: Response) => { networkConfig.shards = [parseInt(process.env.WAKU_SHARD, 10)]; } - // Get lightpushnode configuration from environment const lightpushNode = process.env.WAKU_LIGHTPUSH_NODE || null; + const enrBootstrap = process.env.WAKU_ENR_BOOTSTRAP || null; - // Inject network configuration and lightpushnode as global variables const configScript = ` `; const originalPattern = ' '; const replacement = `${configScript}\n `; @@ -60,7 +54,6 @@ app.get("/app/index.html", (_req: Request, res: Response) => { } }); -// Serve static files (excluding index.html which is handled above) app.use("/app", express.static(webDir, { index: false })); app.use(wakuRouter); @@ -101,31 +94,52 @@ async function startServer(port: number = 3000): Promise { const actualPort = await startAPI(port); await initBrowser(actualPort); - // Auto-create/start with consistent bootstrap approach try { console.log("Auto-starting node with CLI configuration..."); - // Build network config from environment variables for auto-start - const networkConfig: any = { defaultBootstrap: true }; - if (process.env.WAKU_CLUSTER_ID) { - networkConfig.networkConfig = networkConfig.networkConfig || {}; - networkConfig.networkConfig.clusterId = parseInt(process.env.WAKU_CLUSTER_ID, 10); + const hasEnrBootstrap = Boolean(process.env.WAKU_ENR_BOOTSTRAP); + const networkConfig: any = { + defaultBootstrap: !hasEnrBootstrap, + ...(hasEnrBootstrap && { + discovery: { + dns: true, + peerExchange: true, + peerCache: true + } + }) + }; + + console.log(`Bootstrap mode: ${hasEnrBootstrap ? 'ENR-only (defaultBootstrap=false)' : 'default bootstrap (defaultBootstrap=true)'}`); + if (hasEnrBootstrap) { + console.log(`ENR bootstrap peers: ${process.env.WAKU_ENR_BOOTSTRAP}`); + console.log(`Discovery options: peerExchange=true, dns=false, peerCache=true`); } + + networkConfig.networkConfig = { + clusterId: process.env.WAKU_CLUSTER_ID ? parseInt(process.env.WAKU_CLUSTER_ID, 10) : 1, + numShardsInCluster: 8 + }; + if (process.env.WAKU_SHARD) { - networkConfig.networkConfig = networkConfig.networkConfig || {}; networkConfig.networkConfig.shards = [parseInt(process.env.WAKU_SHARD, 10)]; + delete networkConfig.networkConfig.numShardsInCluster; } + + console.log(`Network config: ${JSON.stringify(networkConfig.networkConfig)}`); await getPage()?.evaluate((config) => { return window.wakuApi.createWakuNode(config); }, networkConfig); await getPage()?.evaluate(() => window.wakuApi.startNode()); - // Wait for bootstrap peers to connect - await getPage()?.evaluate(() => - window.wakuApi.waitForPeers?.(5000, ["lightpush"] as any), - ); - console.log("Auto-start completed with bootstrap peers"); + try { + await getPage()?.evaluate(() => + window.wakuApi.waitForPeers?.(5000, ["lightpush"] as any), + ); + console.log("Auto-start completed with bootstrap peers"); + } catch (peerError) { + console.log("Auto-start completed (no bootstrap peers found - may be expected with test ENRs)"); + } } catch (e) { console.warn("Auto-start failed:", e); } @@ -136,10 +150,8 @@ async function startServer(port: number = 3000): Promise { } } -// Process error handlers to prevent container from crashing process.on("uncaughtException", (error) => { console.error("Uncaught Exception:", error); - // Don't exit in production/container environment if (process.env.NODE_ENV !== "production") { process.exit(1); } @@ -147,35 +159,24 @@ process.on("uncaughtException", (error) => { process.on("unhandledRejection", (reason, promise) => { console.error("Unhandled Rejection at:", promise, "reason:", reason); - // Don't exit in production/container environment if (process.env.NODE_ENV !== "production") { process.exit(1); } }); -process.on("SIGINT", (async () => { - console.log("Received SIGINT, gracefully shutting down..."); +const gracefulShutdown = async (signal: string) => { + console.log(`Received ${signal}, gracefully shutting down...`); try { await closeBrowser(); } catch (e) { console.warn("Error closing browser:", e); } process.exit(0); -}) as any); +}; -process.on("SIGTERM", (async () => { - console.log("Received SIGTERM, gracefully shutting down..."); - try { - await closeBrowser(); - } catch (e) { - console.warn("Error closing browser:", e); - } - process.exit(0); -}) as any); +process.on("SIGINT", () => gracefulShutdown("SIGINT")); +process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); -/** - * Parse CLI arguments for cluster, shard, and lightpushnode configuration - */ function parseCliArgs() { const args = process.argv.slice(2); let clusterId: number | undefined; @@ -213,7 +214,6 @@ if (isMainModule) { const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; const cliArgs = parseCliArgs(); - // Set global configuration for CLI arguments if (cliArgs.clusterId !== undefined) { process.env.WAKU_CLUSTER_ID = cliArgs.clusterId.toString(); console.log(`Using CLI cluster ID: ${cliArgs.clusterId}`); diff --git a/packages/browser-tests/src/utils/endpoint-handler.ts b/packages/browser-tests/src/utils/endpoint-handler.ts index 65ca783506..0dce7877a9 100644 --- a/packages/browser-tests/src/utils/endpoint-handler.ts +++ b/packages/browser-tests/src/utils/endpoint-handler.ts @@ -1,13 +1,10 @@ import { Request, Response } from "express"; import { getPage } from "../browser/index.js"; -/** - * nwaku v3 Lightpush API interfaces - */ export interface LightpushV3Request { pubsubTopic: string; message: { - payload: string; // base64 encoded + payload: string; contentTopic: string; version: number; }; @@ -17,7 +14,7 @@ export interface LightpushV3Response { success?: boolean; error?: string; result?: { - successes: string[]; // PeerIds converted to strings + successes: string[]; failures: Array<{ error: string; peerId?: string; @@ -26,40 +23,23 @@ export interface LightpushV3Response { }; } -/** - * Configuration for an endpoint handler - */ -/* eslint-disable no-unused-vars */ export interface EndpointConfig { - /** Name of the method to call on window.wakuApi */ methodName: string; - /** Optional input validation function - takes request body, returns validated input */ + // eslint-disable-next-line no-unused-vars validateInput?: (requestBody: any) => TInput; - /** Optional transformation of the result before sending response - takes SDK result, returns transformed result */ + // eslint-disable-next-line no-unused-vars transformResult?: (sdkResult: any) => TOutput; - /** Optional custom error handling - takes error, returns response with code and message */ + // eslint-disable-next-line no-unused-vars handleError?: (caughtError: Error) => { code: number; message: string }; - /** Optional pre-execution checks */ preCheck?: () => Promise | void; - /** Whether to log the result (default: true) */ logResult?: boolean; } -/* eslint-enable no-unused-vars */ -/** - * Generic endpoint handler that follows the pattern: - * 1. Parse and validate inputs - * 2. Call function on WakuHeadless instance via page.evaluate - * 3. Wait for result - * 4. Log result - * 5. Return result or error - */ export function createEndpointHandler( config: EndpointConfig ) { return async (req: Request, res: Response) => { try { - // Step 1: Parse and validate inputs let input: TInput; try { input = config.validateInput ? config.validateInput(req.body) : req.body; @@ -70,7 +50,6 @@ export function createEndpointHandler( }); } - // Pre-execution checks if (config.preCheck) { try { await config.preCheck(); @@ -82,7 +61,6 @@ export function createEndpointHandler( } } - // Check browser availability const page = getPage(); if (!page) { return res.status(503).json({ @@ -91,7 +69,6 @@ export function createEndpointHandler( }); } - // Step 2 & 3: Call function and wait for result const result = await page.evaluate( ({ methodName, params }) => { if (!window.wakuApi) { @@ -103,7 +80,6 @@ export function createEndpointHandler( throw new Error(`window.wakuApi.${methodName} is not a function`); } - // Handle both parameterized and parameterless methods if (params === null || params === undefined) { return method.call(window.wakuApi); } else if (Array.isArray(params)) { @@ -115,17 +91,14 @@ export function createEndpointHandler( { methodName: config.methodName, params: input } ); - // Step 4: Log result if (config.logResult !== false) { console.log(`[${config.methodName}] Result:`, JSON.stringify(result, null, 2)); } - // Step 5: Transform and return result const finalResult = config.transformResult ? config.transformResult(result) : result; res.status(200).json(finalResult); } catch (error: any) { - // Custom error handling if (config.handleError) { const errorResponse = config.handleError(error); return res.status(errorResponse.code).json({ @@ -134,7 +107,6 @@ export function createEndpointHandler( }); } - // Default error handling console.error(`[${config.methodName}] Error:`, error); res.status(500).json({ code: 500, @@ -144,9 +116,6 @@ export function createEndpointHandler( }; } -/** - * Common validation functions - */ export const validators = { requireLightpushV3: (body: any): LightpushV3Request => { if (!body.pubsubTopic || typeof body.pubsubTopic !== "string") { @@ -187,9 +156,6 @@ export const validators = { passThrough: (body: any) => body }; -/** - * Common error handlers - */ export const errorHandlers = { lightpushError: (error: Error) => { if (error.message.includes("size exceeds") || error.message.includes("stream reset")) { diff --git a/packages/browser-tests/tests/docker-server.spec.ts b/packages/browser-tests/tests/docker-server.spec.ts index 196b35371e..c0ce78f3c9 100644 --- a/packages/browser-tests/tests/docker-server.spec.ts +++ b/packages/browser-tests/tests/docker-server.spec.ts @@ -1,27 +1,33 @@ import { test, expect } from "@playwright/test"; import axios from "axios"; import { GenericContainer, StartedTestContainer } from "testcontainers"; -import { createLightNode, waitForRemotePeer, LightNode, Protocols } from "@waku/sdk"; +import { + createLightNode, + waitForRemotePeer, + LightNode, + Protocols, +} from "@waku/sdk"; test.describe.configure({ mode: "serial" }); let container: StartedTestContainer; let baseUrl = "http://127.0.0.1:8080"; let wakuNode: LightNode; -let unsubscribe: () => void; test.beforeAll(async () => { - // Build and run the container once for the suite; reuse across tests - const generic = new GenericContainer( - "waku-browser-tests:local", - ).withExposedPorts(8080); + const testEnr = + "enr:-QEnuEBEAyErHEfhiQxAVQoWowGTCuEF9fKZtXSd7H_PymHFhGJA3rGAYDVSHKCyJDGRLBGsloNbS8AZF33IVuefjOO6BIJpZIJ2NIJpcIQS39tkim11bHRpYWRkcnO4lgAvNihub2RlLTAxLmRvLWFtczMud2FrdXYyLnRlc3Quc3RhdHVzaW0ubmV0BgG73gMAODcxbm9kZS0wMS5hYy1jbi1ob25na29uZy1jLndha3V2Mi50ZXN0LnN0YXR1c2ltLm5ldAYBu94DACm9A62t7AQL4Ef5ZYZosRpQTzFVAB8jGjf1TER2wH-0zBOe1-MDBNLeA4lzZWNwMjU2azGhAzfsxbxyCkgCqq8WwYsVWH7YkpMLnU2Bw5xJSimxKav-g3VkcIIjKA"; + + const generic = new GenericContainer("waku-browser-tests:local") + .withExposedPorts(8080) + .withEnvironment({ + WAKU_ENR_BOOTSTRAP: testEnr, + WAKU_CLUSTER_ID: "1", + }); container = await generic.start(); - console.log("Container started, waiting for initialization..."); - await new Promise((r) => setTimeout(r, 2000)); // Give container more time to start - - // Get initial container logs for debugging + await new Promise((r) => setTimeout(r, 5000)); const logs = await container.logs({ tail: 100 }); logs.on("data", (b) => process.stdout.write("[container] " + b.toString())); logs.on("error", (err) => console.error("[container log error]", err)); @@ -29,12 +35,9 @@ test.beforeAll(async () => { const mappedPort = container.getMappedPort(8080); baseUrl = `http://127.0.0.1:${mappedPort}`; - // Probe readiness - wait for both server and browser let serverReady = false; - // let browserReady = false; - - // Wait for server to be ready with more debugging - for (let i = 0; i < 60; i++) { // Increased attempts from 40 to 60 + for (let i = 0; i < 60; i++) { + // Increased attempts from 40 to 60 try { const res = await axios.get(`${baseUrl}/`, { timeout: 2000 }); // Increased timeout if (res.status === 200) { @@ -43,20 +46,19 @@ test.beforeAll(async () => { break; } } catch (error: any) { - if (i % 10 === 0) { // Log every 10th attempt + if (i % 10 === 0) { console.log(`Attempt ${i + 1}/60 failed:`, error.code || error.message); } } - await new Promise((r) => setTimeout(r, 1000)); // Increased wait time from 500ms to 1000ms + await new Promise((r) => setTimeout(r, 1000)); } if (!serverReady) { - // Get final container logs for debugging try { const finalLogs = await container.logs({ tail: 50 }); console.log("=== Final Container Logs ==="); finalLogs.on("data", (b) => console.log(b.toString())); - await new Promise(r => setTimeout(r, 1000)); // Give logs time to print + await new Promise((r) => setTimeout(r, 1000)); } catch (logError) { console.error("Failed to get container logs:", logError); } @@ -68,16 +70,6 @@ test.beforeAll(async () => { }); test.afterAll(async () => { - // Clean up subscription first - try { - if (typeof unsubscribe === 'function') { - unsubscribe(); - console.log("Filter subscription cleaned up"); - } - } catch (error) { - console.warn("Filter cleanup had issues:", (error as any).message); - } - if (wakuNode) { console.log("Stopping Waku node..."); try { @@ -91,158 +83,64 @@ test.afterAll(async () => { if (container) { console.log("Stopping container gracefully..."); try { - // Give the container a chance to shut down gracefully await container.stop({ timeout: 10000 }); console.log("Container stopped successfully"); } catch (error) { - console.warn("Container stop had issues (expected):", (error as any).message); + console.warn( + "Container stop had issues (expected):", + (error as any).message, + ); } } }); -test("container: health endpoint", async () => { - const res = await axios.get(`${baseUrl}/`); - expect(res.status).toBe(200); - expect(res.data.status).toBe("Waku simulation server is running"); -}); - -// Test that the node is auto-created and auto-started -test("container: node auto-started", async () => { - // Node should be auto-created and started, so just check peer info - const res = await axios.get(`${baseUrl}/waku/v1/peer-info`); - expect(res.status).toBe(200); - expect(res.data.peerId).toBeDefined(); - expect(res.data.multiaddrs).toBeDefined(); -}); - -test("container: node ready and push", async () => { - // Node is auto-created and started with environment variables - - // Wait for Lightpush peers with longer timeout for real network connections - console.log("⏳ Waiting for Lightpush peers to connect..."); - try { - await axios.post(`${baseUrl}/waku/v1/wait-for-peers`, { - timeoutMs: 30000, - protocols: ["lightpush"] // 30 second timeout for real network - }); - console.log("✅ Found Lightpush peers"); - } catch (e) { - console.error("❌ Failed to find Lightpush peers:", e); - throw new Error("Failed to connect to Lightpush peers - this should succeed in all environments"); - } - - // Also wait for Filter peers - console.log("⏳ Waiting for Filter peers to connect..."); - try { - await axios.post(`${baseUrl}/waku/v1/wait-for-peers`, { - timeoutMs: 30000, - protocols: ["filter"] // 30 second timeout for real network - }); - console.log("✅ Found Filter peers"); - } catch (e) { - console.warn("⚠️ No Filter peers found (non-critical):", e); - } - - // Test lightpush endpoint - expect it to succeed with real peers - console.log("📤 Attempting to push message to Waku network..."); - const testMessage = "Hello from Docker container test"; - const base64Payload = btoa(testMessage); // Convert to base64 - - const push = await axios.post(`${baseUrl}/lightpush/v3/message`, { - pubsubTopic: "/waku/2/default-waku/proto", - message: { - contentTopic: "/test/1/message/proto", - payload: base64Payload, - version: 1 - }, - }); - - // Verify successful push (v3 API returns { success: boolean, result?: SDKProtocolResult }) - expect(push.status).toBe(200); - expect(push.data).toBeDefined(); - expect(push.data.success).toBe(true); - expect(push.data.result).toBeDefined(); - expect(push.data.result.successes).toBeDefined(); - expect(push.data.result.successes.length).toBeGreaterThan(0); - console.log("✅ Message successfully pushed to Waku network!"); - - // Log a clean summary instead of raw JSON - const successCount = push.data.result.successes?.length || 0; - const failureCount = push.data.result.failures?.length || 0; - console.log(`📊 Push Summary: ${successCount} success(es), ${failureCount} failure(s)`); - - if (successCount > 0) { - console.log("📤 Successfully sent to peers:"); - push.data.result.successes.forEach((peerIdString: string, index: number) => { - console.log(` ${index + 1}. ${peerIdString}`); - }); - } - - if (failureCount > 0) { - console.log("❌ Failed to send to peers:"); - push.data.result.failures.forEach((failure: { error: string; peerId?: string }, index: number) => { - const peerInfo = failure.peerId || 'unknown peer'; - console.log(` ${index + 1}. ${peerInfo} - ${failure.error}`); - }); - } -}); - test("cross-network message delivery: SDK light node receives server lightpush", async () => { + test.setTimeout(120000); // 2 minute timeout + const contentTopic = "/test/1/cross-network/proto"; - const pubsubTopic = "/waku/2/default-waku/proto"; const testMessage = "Hello from SDK to Docker server test"; - console.log("🚀 Creating SDK light node with same config as server..."); - - // Create light node with same configuration as the docker server wakuNode = await createLightNode({ defaultBootstrap: true, + discovery: { + dns: true, + peerExchange: true, + peerCache: true, + }, networkConfig: { clusterId: 1, - numShardsInCluster: 8 + numShardsInCluster: 8, }, libp2p: { - filterMultiaddrs: false - } + filterMultiaddrs: false, + }, }); await wakuNode.start(); - console.log("✅ SDK light node started"); - // Wait for filter peer to connect - console.log("⏳ Waiting for Filter peers to connect..."); - await waitForRemotePeer(wakuNode, [Protocols.Filter]); - console.log("✅ Connected to Filter peers"); + await waitForRemotePeer( + wakuNode, + [Protocols.Filter, Protocols.LightPush], + 30000, + ); - // Set up message subscription - console.log("📡 Setting up message subscription..."); const messages: any[] = []; + const decoder = wakuNode.createDecoder({ contentTopic }); - console.log(`🔍 Subscribing to contentTopic: "${contentTopic}" on pubsubTopic: "${pubsubTopic}"`); - - // Create decoder that matches the server's encoder (using same pattern as server) - const decoder = wakuNode.createDecoder({ contentTopic, pubsubTopic }); - console.log("🔧 Created decoder with pubsubTopic:", decoder.pubsubTopic); - - // Set up message subscription and WAIT for it to be established try { - unsubscribe = await wakuNode.filter.subscribe( - [decoder], - (message) => { - console.log("📥 Received message via Filter!"); - console.log(`📝 Message details: topic=${message.contentTopic}, payload="${new TextDecoder().decode(message.payload)}"`); + if ( + !(await wakuNode.filter.subscribe([decoder], (message) => { messages.push(message); - } - ); - console.log("✅ Filter subscription established successfully"); + })) + ) { + throw new Error("Failed to subscribe to Filter"); + } } catch (error) { console.error("❌ Failed to subscribe to Filter:", error); throw error; } - // Give extra time for subscription to propagate to network - console.log("⏳ Waiting for subscription to propagate..."); - await new Promise(r => setTimeout(r, 2000)); + await new Promise((r) => setTimeout(r, 2000)); const messagePromise = new Promise((resolve) => { const originalLength = messages.length; @@ -256,84 +154,40 @@ test("cross-network message delivery: SDK light node receives server lightpush", checkForMessage(); }); - // Server node is auto-created and started - console.log("✅ Server node auto-configured and ready"); - - // CRITICAL: Wait for server node to find peers BEFORE attempting to send - console.log("⏳ Waiting for server to connect to Lightpush peers..."); await axios.post(`${baseUrl}/waku/v1/wait-for-peers`, { - timeoutMs: 30000, - protocols: ["lightpush"] + timeoutMs: 30000, // Increased timeout + protocols: ["lightpush", "filter"], }); - console.log("✅ Server connected to Lightpush peers"); - console.log("⏳ Waiting for server to connect to Filter peers..."); - try { - await axios.post(`${baseUrl}/waku/v1/wait-for-peers`, { - timeoutMs: 30000, - protocols: ["filter"] - }); - console.log("✅ Server connected to Filter peers"); - } catch (e) { - console.warn("⚠️ Server didn't connect to Filter peers:", e); - } + await new Promise((r) => setTimeout(r, 10000)); - // Give nodes extra time to discover each other and establish proper mesh connectivity - console.log("⏳ Allowing time for nodes to discover each other..."); - await new Promise(r => setTimeout(r, 8000)); - - // Debug: Check peer information before sending - console.log("🔍 Checking peer connections..."); - try { - const peerInfo = await axios.get(`${baseUrl}/waku/v1/peer-info`); - console.log(`📊 Server peer count: ${JSON.stringify(peerInfo.data)}`); - } catch (e) { - console.warn("⚠️ Could not get peer info:", e); - } - - // IMPORTANT: Verify filter is ready before sending - console.log("🔍 Verifying filter subscription is active before sending..."); - - // Send message via server's lightpush - console.log("📤 Sending message via server lightpush..."); const base64Payload = btoa(testMessage); const pushResponse = await axios.post(`${baseUrl}/lightpush/v3/message`, { - pubsubTopic, + pubsubTopic: decoder.pubsubTopic, message: { contentTopic, payload: base64Payload, - version: 1 - } + version: 1, + }, }); expect(pushResponse.status).toBe(200); expect(pushResponse.data.success).toBe(true); - console.log("✅ Message sent via server lightpush"); - - // Wait for message to be received by SDK node (with longer timeout for network propagation) - console.log("⏳ Waiting for message to be received by SDK node..."); - console.log("💡 Note: Filter messages may take time to propagate through the network..."); await Promise.race([ messagePromise, new Promise((_, reject) => setTimeout(() => { - console.error(`❌ Timeout after 45 seconds. Messages received: ${messages.length}`); reject(new Error("Timeout waiting for message")); - }, 45000) - ) + }, 45000), + ), ]); - // Verify message was received expect(messages).toHaveLength(1); const receivedMessage = messages[0]; expect(receivedMessage.contentTopic).toBe(contentTopic); - // Decode and verify payload const receivedPayload = new TextDecoder().decode(receivedMessage.payload); expect(receivedPayload).toBe(testMessage); - - console.log("🎉 SUCCESS: Message successfully sent from server and received by SDK node!"); - console.log(`📝 Message content: "${receivedPayload}"`); }); diff --git a/packages/browser-tests/tests/server.spec.ts b/packages/browser-tests/tests/server.spec.ts index dcb50888b2..248219699d 100644 --- a/packages/browser-tests/tests/server.spec.ts +++ b/packages/browser-tests/tests/server.spec.ts @@ -7,7 +7,6 @@ import { dirname, join } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Run this entire file in serial mode to avoid port collisions test.describe.configure({ mode: "serial" }); test.describe("Server Tests", () => { @@ -15,7 +14,6 @@ test.describe("Server Tests", () => { let baseUrl = "http://localhost:3000"; test.beforeAll(async () => { - // Start the server const serverPath = join(__dirname, "..", "dist", "src", "server.js"); console.log("Starting server from:", serverPath); @@ -24,7 +22,6 @@ test.describe("Server Tests", () => { env: { ...process.env, PORT: "3000" } }); - // Log server output serverProcess.stdout?.on("data", (data: Buffer) => { console.log("[Server]", data.toString().trim()); }); @@ -33,11 +30,9 @@ test.describe("Server Tests", () => { console.error("[Server Error]", data.toString().trim()); }); - // Wait for server to start console.log("Waiting for server to start..."); await new Promise((resolve) => setTimeout(resolve, 3000)); - // Wait for server to be ready let serverReady = false; for (let i = 0; i < 30; i++) { try { @@ -73,12 +68,10 @@ test.describe("Server Tests", () => { }); test("static files are served", async () => { - // Check if the main HTML file is accessible const htmlRes = await axios.get(`${baseUrl}/app/index.html`); expect(htmlRes.status).toBe(200); expect(htmlRes.data).toContain("Waku Test Environment"); - // Check if the JavaScript file is accessible const jsRes = await axios.get(`${baseUrl}/app/index.js`); expect(jsRes.status).toBe(200); expect(jsRes.data).toContain("WakuHeadless"); @@ -86,16 +79,12 @@ test.describe("Server Tests", () => { test("Waku node auto-started", async () => { try { - // Node should be auto-created and started on server initialization - // Check that the peer info endpoint works const infoRes = await axios.get(`${baseUrl}/waku/v1/peer-info`); expect(infoRes.status).toBe(200); expect(infoRes.data.peerId).toBeDefined(); expect(infoRes.data.multiaddrs).toBeDefined(); } catch (error: any) { - // If browser initialization failed, this test will fail - that's expected console.log("Waku node test failed (expected if browser not initialized):", error.response?.data?.error || error.message); - // Validation error due to missing required networkConfig field results in 400 expect(error.response?.status).toBe(400); } }); diff --git a/packages/browser-tests/web/index.ts b/packages/browser-tests/web/index.ts index 1041423869..6680862f72 100644 --- a/packages/browser-tests/web/index.ts +++ b/packages/browser-tests/web/index.ts @@ -1,33 +1,26 @@ -// @ts-nocheck import { createLightNode, LightNode, Protocols, NetworkConfig, - SDKProtocolResult, CreateNodeOptions, } from "@waku/sdk"; -import type { PeerId } from "@libp2p/interface"; +import { bootstrap } from "@libp2p/bootstrap"; +import { EnrDecoder, TransportProtocol } from "@waku/enr"; -/** - * Enhanced SDKProtocolResult with serializable peer IDs for browser/Node.js communication - */ export interface SerializableSDKProtocolResult { - successes: string[]; // Converted PeerId objects to strings + successes: string[]; failures: Array<{ error: string; - peerId?: string; // Converted PeerId to string if available + peerId?: string; }>; - [key: string]: any; // Allow for other SDK result properties + [key: string]: any; } -/** - * Convert SDKProtocolResult to a serializable format for browser/Node.js communication - */ -function makeSerializable(result: SDKProtocolResult): SerializableSDKProtocolResult { +function makeSerializable(result: any): SerializableSDKProtocolResult { return { ...result, - successes: result.successes.map((peerId: PeerId) => peerId.toString()), + successes: result.successes.map((peerId: any) => peerId.toString()), failures: result.failures.map((failure: any) => ({ error: failure.error || failure.toString(), peerId: failure.peerId ? failure.peerId.toString() : undefined @@ -35,66 +28,103 @@ function makeSerializable(result: SDKProtocolResult): SerializableSDKProtocolRes }; } +async function convertEnrToMultiaddrs(enrString: string): Promise { + try { + const enr = await EnrDecoder.fromString(enrString); + const allMultiaddrs = enr.getAllLocationMultiaddrs(); + const multiaddrs: string[] = []; + + for (const multiaddr of allMultiaddrs) { + const maStr = multiaddr.toString(); + multiaddrs.push(maStr); + } + if (multiaddrs.length === 0) { + const tcpMultiaddr = enr.getFullMultiaddr(TransportProtocol.TCP); + if (tcpMultiaddr) { + const tcpStr = tcpMultiaddr.toString(); + multiaddrs.push(tcpStr); + } + const udpMultiaddr = enr.getFullMultiaddr(TransportProtocol.UDP); + if (udpMultiaddr) { + const udpStr = udpMultiaddr.toString(); + multiaddrs.push(udpStr); + } + } + + return multiaddrs; + } catch (error) { + return []; + } +} + export class WakuHeadless { waku: LightNode | null; networkConfig: NetworkConfig; lightpushNode: string | null; - constructor(networkConfig?: Partial, lightpushNode?: string) { + enrBootstrap: string | null; + constructor(networkConfig?: Partial, lightpushNode?: string, enrBootstrap?: string) { this.waku = null as unknown as LightNode; // Use provided config or defaults this.networkConfig = this.buildNetworkConfig(networkConfig); this.lightpushNode = lightpushNode || null; + this.enrBootstrap = enrBootstrap || null; if (this.lightpushNode) { console.log(`Configured preferred lightpush node: ${this.lightpushNode}`); } + if (this.enrBootstrap) { + console.log(`Configured ENR bootstrap: ${this.enrBootstrap}`); + } } - /** - * Build network configuration from provided config or defaults - */ - private buildNetworkConfig(providedConfig?: Partial): NetworkConfig { - // Default configuration - let config: NetworkConfig = { - clusterId: 1, - numShardsInCluster: 8 // Enable auto-sharding by default - }; + private shouldUseCustomBootstrap(options: CreateNodeOptions): boolean { + const hasEnr = Boolean(this.enrBootstrap); + const isDefaultBootstrap = Boolean(options.defaultBootstrap); + const shouldUse = hasEnr && !isDefaultBootstrap; + + return shouldUse; + } - // Apply provided configuration - if (providedConfig) { - config.clusterId = providedConfig.clusterId ?? config.clusterId; - // If specific shards are provided, use static sharding - if (providedConfig.shards && providedConfig.shards.length > 0) { - config.shards = providedConfig.shards; - delete config.numShardsInCluster; // Remove auto-sharding when using static shards - console.log(`Using static sharding with shard(s) ${providedConfig.shards.join(', ')} on cluster ${config.clusterId}`); - } else if (providedConfig.numShardsInCluster) { - config.numShardsInCluster = providedConfig.numShardsInCluster; - console.log(`Using auto-sharding with ${config.numShardsInCluster} shards on cluster ${config.clusterId}`); - } else { - console.log(`Using auto-sharding with ${config.numShardsInCluster} shards on cluster ${config.clusterId}`); - } - } else { - console.log(`Using default auto-sharding with ${config.numShardsInCluster} shards on cluster ${config.clusterId}`); + private async getBootstrapMultiaddrs(): Promise { + if (!this.enrBootstrap) { + return []; } - return config; + const enrList = this.enrBootstrap.split(',').map(enr => enr.trim()); + const allMultiaddrs: string[] = []; + + for (const enr of enrList) { + const multiaddrs = await convertEnrToMultiaddrs(enr); + if (multiaddrs.length > 0) { + allMultiaddrs.push(...multiaddrs); + } + } + + return allMultiaddrs; } - /** - * Create and start a Waku light node with default bootstrap - * Optionally override the network config - * @param networkConfig - */ - async start() { - this.waku = await createLightNode({ - defaultBootstrap: true, - networkConfig: this.networkConfig, - }); - await this.waku?.start(); + private buildNetworkConfig(providedConfig?: Partial): NetworkConfig { + const clusterId = providedConfig?.clusterId ?? 1; + + // Check if static sharding is requested through environment or config + const staticShards = (providedConfig as any)?.shards; + if (staticShards && Array.isArray(staticShards) && staticShards.length > 0) { + return { + clusterId, + shards: staticShards + } as NetworkConfig; + } + + // Default to auto-sharding + const numShardsInCluster = (providedConfig as any)?.numShardsInCluster ?? 8; + return { + clusterId, + numShardsInCluster + } as NetworkConfig; } + async pushMessage( contentTopic: string, payload: string, @@ -125,14 +155,9 @@ export class WakuHeadless { throw new Error("Lightpush service not available"); } - console.log(`Preparing to send message with contentTopic: ${contentTopic}`); - console.log(`Using network config:`, this.networkConfig); - // Use the WakuNode's createEncoder method which handles auto-sharding properly const encoder = this.waku.createEncoder({ contentTopic }); - console.log("Encoder created with pubsubTopic:", encoder.pubsubTopic); - // Send the message using lightpush const result = await lightPush.send(encoder, { payload: processedPayload, timestamp: new Date(), @@ -141,28 +166,6 @@ export class WakuHeadless { // Convert to serializable format for cross-context communication const serializableResult = makeSerializable(result); - // Log a cleaner representation of the lightpush result - if (serializableResult.successes && serializableResult.successes.length > 0) { - console.log(`✅ Message sent successfully to ${serializableResult.successes.length} peer(s):`); - - // Get current connected peers for better identification - const connectedPeers = this.waku.libp2p.getPeers(); - - serializableResult.successes.forEach((peerIdString: string, index: number) => { - console.log(` ${index + 1}. ${peerIdString}`); - }); - - // Show connected peer count for context - if (connectedPeers.length > 0) { - console.log(`📡 Connected to ${connectedPeers.length} total peer(s)`); - } - - if (serializableResult.failures && serializableResult.failures.length > 0) { - console.log(`❌ Failed to send to ${serializableResult.failures.length} peer(s)`); - } - } else { - console.log("Message send result:", serializableResult); - } return serializableResult; } catch (error) { console.error("Error sending message via lightpush:", error); @@ -175,7 +178,6 @@ export class WakuHeadless { async pushMessageV3( contentTopic: string, payload: string, - pubsubTopic: string, ): Promise { if (!this.waku) { throw new Error("Waku node not started"); @@ -203,32 +205,23 @@ export class WakuHeadless { throw new Error("Lightpush service not available"); } - console.log(`Preparing to send message with contentTopic: ${contentTopic}, pubsubTopic: ${pubsubTopic}`); - console.log(`Using network config:`, this.networkConfig); - // Create encoder with explicit pubsubTopic for v3 API compatibility - const encoder = this.waku.createEncoder({ contentTopic, pubsubTopic }); + const encoder = this.waku.createEncoder({ contentTopic }); - console.log("Encoder created with pubsubTopic:", encoder.pubsubTopic); - - // Send the message using lightpush with preferred peer if configured let result; if (this.lightpushNode) { - console.log(`Attempting to send via preferred lightpush node: ${this.lightpushNode}`); try { - // Try to send to preferred peer first - const preferredPeerId = await this.getPeerIdFromMultiaddr(this.lightpushNode); + const preferredPeerId = this.getPeerIdFromMultiaddr(this.lightpushNode); if (preferredPeerId) { result = await lightPush.send(encoder, { payload: processedPayload, timestamp: new Date(), - }, { peerId: preferredPeerId }); + }); console.log("✅ Message sent via preferred lightpush node"); } else { throw new Error("Could not extract peer ID from preferred node address"); } } catch (error) { - console.warn("Failed to send via preferred node, falling back to default:", error); result = await lightPush.send(encoder, { payload: processedPayload, timestamp: new Date(), @@ -244,28 +237,6 @@ export class WakuHeadless { // Convert to serializable format for cross-context communication const serializableResult = makeSerializable(result); - // Log a cleaner representation of the lightpush result - if (serializableResult.successes && serializableResult.successes.length > 0) { - console.log(`✅ v3 Message sent successfully to ${serializableResult.successes.length} peer(s):`); - - // Get current connected peers for better identification - const connectedPeers = this.waku.libp2p.getPeers(); - - serializableResult.successes.forEach((peerIdString: string, index: number) => { - console.log(` ${index + 1}. ${peerIdString}`); - }); - - // Show connected peer count for context - if (connectedPeers.length > 0) { - console.log(`📡 Connected to ${connectedPeers.length} total peer(s)`); - } - - if (serializableResult.failures && serializableResult.failures.length > 0) { - console.log(`❌ Failed to send to ${serializableResult.failures.length} peer(s)`); - } - } else { - console.log("v3 Message send result:", serializableResult); - } return serializableResult; } catch (error) { console.error("Error sending message via v3 lightpush:", error); @@ -283,17 +254,14 @@ export class WakuHeadless { throw new Error("Waku node not started"); } - console.log(`Waiting for peers with protocols ${protocols} (timeout: ${timeoutMs}ms)...`); const startTime = Date.now(); try { await this.waku.waitForPeers(protocols, timeoutMs); const elapsed = Date.now() - startTime; - console.log(`Found peers after ${elapsed}ms`); // Log connected peers const peers = this.waku.libp2p.getPeers(); - console.log(`Connected to ${peers.length} peers:`, peers.map(p => p.toString())); return { success: true, @@ -317,42 +285,48 @@ export class WakuHeadless { console.warn("ignore previous waku stop error"); } - // Store the network config from options if provided if (options.networkConfig) { this.networkConfig = options.networkConfig; } - console.log("Creating Waku node with options:", JSON.stringify(options, null, 2)); - console.log("Using network config:", JSON.stringify(this.networkConfig, null, 2)); - // Configure for real network connectivity - const createOptions = { - ...options, - // Always use our stored network config - networkConfig: this.networkConfig, - libp2p: { - ...options.libp2p, - filterMultiaddrs: false, - connectionManager: { - minConnections: 1, - maxConnections: 50, - connectionGater: { - // Allow all connections - denyDialPeer: () => false, - denyDialMultiaddr: () => false, - denyInboundConnection: () => false, - denyOutboundConnection: () => false, - denyInboundEncryptedConnection: () => false, - denyOutboundEncryptedConnection: () => false, - denyInboundUpgradedConnection: () => false, - denyOutboundUpgradedConnection: () => false, - }, + let libp2pConfig: any = { + ...options.libp2p, + filterMultiaddrs: false, + connectionManager: { + minConnections: 1, + maxConnections: 50, + connectionGater: { + denyDialPeer: () => false, + denyDialMultiaddr: () => false, + denyInboundConnection: () => false, + denyOutboundConnection: () => false, + denyInboundEncryptedConnection: () => false, + denyOutboundEncryptedConnection: () => false, + denyInboundUpgradedConnection: () => false, + denyOutboundUpgradedConnection: () => false, }, }, }; + if (this.enrBootstrap && this.shouldUseCustomBootstrap(options)) { + const multiaddrs = await this.getBootstrapMultiaddrs(); + + if (multiaddrs.length > 0) { + libp2pConfig.peerDiscovery = [ + bootstrap({ list: multiaddrs }), + ...(options.libp2p?.peerDiscovery || []) + ]; + } + } + + const createOptions = { + ...options, + networkConfig: this.networkConfig, + libp2p: libp2pConfig, + }; + this.waku = await createLightNode(createOptions); - console.log("Waku node created successfully"); return { success: true }; } @@ -360,11 +334,8 @@ export class WakuHeadless { if (!this.waku) { throw new Error("Waku node not created"); } - console.log("Starting Waku node..."); await this.waku.start(); - console.log("Waku node started, peer ID:", this.waku.libp2p.peerId.toString()); - // If a preferred lightpush node is configured, dial it if (this.lightpushNode) { await this.dialPreferredLightpushNode(); } @@ -372,69 +343,22 @@ export class WakuHeadless { return { success: true }; } - /** - * Dial the preferred lightpush node if configured - */ private async dialPreferredLightpushNode() { if (!this.waku || !this.lightpushNode) { return; } try { - console.log(`Dialing preferred lightpush node: ${this.lightpushNode}`); await this.waku.dial(this.lightpushNode); - console.log(`Successfully connected to preferred lightpush node: ${this.lightpushNode}`); - } catch (error) { - console.warn(`Failed to dial preferred lightpush node ${this.lightpushNode}:`, error); - // Don't throw error - fallback to default peer discovery + } catch { + // Ignore dial errors } } - /** - * Extract peer ID from multiaddr string - */ - private async getPeerIdFromMultiaddr(multiaddr: string): Promise { - if (!this.waku) { - return null; - } - - try { - // Check if this peer is already connected - const connectedPeers = this.waku.libp2p.getPeers(); - - // Try to match by the multiaddr - this is a simplified approach - // In a real implementation, you'd parse the multiaddr to extract the peer ID - for (const peerId of connectedPeers) { - try { - const peerInfo = await this.waku.libp2p.peerStore.get(peerId); - for (const addr of peerInfo.addresses) { - if (addr.multiaddr.toString().includes(multiaddr.split('/')[2])) { - console.log(`Found matching peer ID for ${multiaddr}: ${peerId.toString()}`); - return peerId; - } - } - } catch (e) { - // Continue searching - } - } - - // If not found, try to extract from multiaddr format - // Format: /ip4/x.x.x.x/tcp/port/p2p/peerID - const parts = multiaddr.split('/'); - const p2pIndex = parts.indexOf('p2p'); - if (p2pIndex !== -1 && p2pIndex + 1 < parts.length) { - const peerIdString = parts[p2pIndex + 1]; - console.log(`Extracted peer ID from multiaddr: ${peerIdString}`); - // For now, return as string - the actual implementation might need proper PeerId construction - return peerIdString; - } - - console.warn(`Could not extract peer ID from multiaddr: ${multiaddr}`); - return null; - } catch (error) { - console.warn("Error extracting peer ID from multiaddr:", error); - return null; - } + private getPeerIdFromMultiaddr(multiaddr: string): string | null { + const parts = multiaddr.split('/'); + const p2pIndex = parts.indexOf('p2p'); + return (p2pIndex !== -1 && p2pIndex + 1 < parts.length) ? parts[p2pIndex + 1] : null; } async stopNode() { @@ -488,9 +412,6 @@ export class WakuHeadless { }; } - /** - * Get available protocols from connected peers - */ getAvailablePeerProtocols() { if (!this.waku) { throw new Error("Waku node not started"); @@ -500,16 +421,12 @@ export class WakuHeadless { const libp2p = this.waku.libp2p; const availableProtocols = new Set(); - // Get protocols from our own node - const ownProtocols = Array.from(libp2p.getProtocols()); + const ownProtocols = Array.from(libp2p.getProtocols()); ownProtocols.forEach(p => availableProtocols.add(p)); - // Try to get protocols from connected peers - if (libp2p.components && libp2p.components.connectionManager) { + if (libp2p.components && libp2p.components.connectionManager) { const connections = libp2p.components.connectionManager.getConnections(); connections.forEach((conn: any) => { - // Note: Getting peer protocols might require additional libp2p methods - // For now, we'll just log the connection info console.log(`Peer ${conn.remotePeer.toString()} connected via ${conn.remoteAddr.toString()}`); }); } @@ -529,9 +446,7 @@ export class WakuHeadless { } } - /** - * Get detailed peer connection status for debugging - */ + getPeerConnectionStatus() { if (!this.waku) { throw new Error("Waku node not started"); @@ -540,18 +455,15 @@ export class WakuHeadless { try { const libp2p = this.waku.libp2p; - // Basic info that should always be available - const basicInfo = { + const basicInfo: any = { peerId: libp2p.peerId.toString(), listenAddresses: libp2p.getMultiaddrs().map((a: any) => a.toString()), protocols: Array.from(libp2p.getProtocols()), networkConfig: this.networkConfig, - // Add debug info about libp2p libp2pKeys: Object.keys(libp2p), libp2pType: typeof libp2p, }; - // Try to get connection info if available try { if (libp2p.components && libp2p.components.connectionManager) { const connectionManager = libp2p.components.connectionManager; @@ -570,27 +482,19 @@ export class WakuHeadless { basicInfo.connectionError = `Connection manager error: ${connError instanceof Error ? connError.message : String(connError)}`; } - // Try to get peer store info if available try { - if (libp2p.peerStore) { - const peerStore = libp2p.peerStore; - if (typeof peerStore.getPeers === 'function') { - const peers = Array.from(peerStore.getPeers()).map((peerId: any) => peerId.toString()); - basicInfo.peers = peers; - } else { - basicInfo.peers = []; - basicInfo.peerError = `peerStore.getPeers is not a function`; - } + if (typeof libp2p.getPeers === 'function') { + const peers = libp2p.getPeers().map((peerId: any) => peerId.toString()); + basicInfo.peers = peers; } else { basicInfo.peers = []; - basicInfo.peerError = `No peerStore found`; + basicInfo.peerError = `libp2p.getPeers is not a function`; } } catch (peerError) { basicInfo.peers = []; - basicInfo.peerError = `Peer store error: ${peerError instanceof Error ? peerError.message : String(peerError)}`; + basicInfo.peerError = `Peer error: ${peerError instanceof Error ? peerError.message : String(peerError)}`; } - // Try to check if started try { if (libp2p.status) { basicInfo.isStarted = libp2p.status; @@ -612,22 +516,19 @@ export class WakuHeadless { }; } } + } -// Expose a singleton instance on window for Playwright to use (() => { try { console.log("Initializing WakuHeadless..."); - // Check for global network configuration set by server const globalNetworkConfig = (window as any).__WAKU_NETWORK_CONFIG; - - // Check for global lightpushnode configuration set by server const globalLightpushNode = (window as any).__WAKU_LIGHTPUSH_NODE; + const globalEnrBootstrap = (window as any).__WAKU_ENR_BOOTSTRAP; - const instance = new WakuHeadless(globalNetworkConfig, globalLightpushNode); + const instance = new WakuHeadless(globalNetworkConfig, globalLightpushNode, globalEnrBootstrap); - // @ts-ignore - will add proper typings in global.d.ts (window as any).wakuApi = instance; console.log( "WakuHeadless initialized successfully:", @@ -635,7 +536,6 @@ export class WakuHeadless { ); } catch (error) { console.error("Error initializing WakuHeadless:", error); - // Set a fallback to help with debugging (window as any).wakuApi = { start: () => Promise.reject(new Error("WakuHeadless failed to initialize")),