diff --git a/package-lock.json b/package-lock.json index 370aa3b75a..dcef29d446 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "packages/rln", "packages/sdk", "packages/relay", + "packages/run", "packages/tests", "packages/reliability-tests", "packages/browser-tests", @@ -7643,6 +7644,10 @@ "resolved": "packages/rln", "link": true }, + "node_modules/@waku/run": { + "resolved": "packages/run", + "link": true + }, "node_modules/@waku/sdk": { "resolved": "packages/sdk", "link": true @@ -32565,6 +32570,26 @@ "devOptional": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -35978,6 +36003,32 @@ "uuid": "dist/esm/bin/uuid" } }, + "packages/run": { + "name": "@waku/run", + "version": "0.0.1", + "license": "MIT OR Apache-2.0", + "bin": { + "waku-run": "dist/src/cli.js" + }, + "devDependencies": { + "@types/chai": "^4.3.11", + "@types/mocha": "^10.0.6", + "@waku/core": "*", + "@waku/interfaces": "*", + "@waku/sdk": "*", + "@waku/utils": "*", + "chai": "^4.3.10", + "cspell": "^8.6.1", + "mocha": "^10.3.0", + "npm-run-all": "^4.1.5", + "ts-node": "^10.9.2", + "tsx": "^4.7.0", + "typescript": "^5.3.3" + }, + "engines": { + "node": ">=18" + } + }, "packages/sdk": { "name": "@waku/sdk", "version": "0.0.35", diff --git a/package.json b/package.json index f406982678..02a8034760 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "packages/rln", "packages/sdk", "packages/relay", + "packages/run", "packages/tests", "packages/reliability-tests", "packages/browser-tests", diff --git a/packages/run/.env.example b/packages/run/.env.example new file mode 100644 index 0000000000..e05a76a704 --- /dev/null +++ b/packages/run/.env.example @@ -0,0 +1,20 @@ +# Waku Local Network Configuration + +# Docker Image +NWAKU_IMAGE=wakuorg/nwaku:v0.36.0 + +# Network Configuration +CLUSTER_ID=0 + +# Node Ports (change if ports are in use) +NODE1_WS_PORT=60000 +NODE1_REST_PORT=8646 +NODE2_WS_PORT=60001 +NODE2_REST_PORT=8647 + +# Postgres Configuration +POSTGRES_USER=postgres +POSTGRES_PASSWORD=test123 + +# Logging +LOG_LEVEL=INFO diff --git a/packages/run/.eslintrc.cjs b/packages/run/.eslintrc.cjs new file mode 100644 index 0000000000..c353a0323b --- /dev/null +++ b/packages/run/.eslintrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: "./tsconfig.dev.json" + }, + rules: { + "@typescript-eslint/no-non-null-assertion": "off" + }, + globals: { + process: true + }, + overrides: [ + { + files: ["*.js"], + rules: { + "no-console": "error" + } + } + ] +}; diff --git a/packages/run/.gitignore b/packages/run/.gitignore new file mode 100644 index 0000000000..896f7cf581 --- /dev/null +++ b/packages/run/.gitignore @@ -0,0 +1,11 @@ +# Environment variables +.env + +# Docker volumes and runtime data +postgres-data/ + +# Logs +*.log + +# Build output +dist/ diff --git a/packages/run/.mocharc.cjs b/packages/run/.mocharc.cjs new file mode 100644 index 0000000000..424cc14a01 --- /dev/null +++ b/packages/run/.mocharc.cjs @@ -0,0 +1,11 @@ +module.exports = { + extension: ['ts'], + require: ['ts-node/register'], + loader: 'ts-node/esm', + 'node-option': [ + 'experimental-specifier-resolution=node', + 'loader=ts-node/esm' + ], + timeout: 90000, + exit: true +}; diff --git a/packages/run/README.md b/packages/run/README.md new file mode 100644 index 0000000000..ca4db06ebe --- /dev/null +++ b/packages/run/README.md @@ -0,0 +1,436 @@ +# @waku/run + +> **Spin up a local Waku network for development without relying on external infrastructure** + +Perfect for hackathons, offline development, or when you need a controlled testing environment for your js-waku application. + +## What's Included + +- **2 nwaku nodes** connected to each other with all protocols enabled: + - ✅ Relay (gossipsub) + - ✅ Filter (light client subscriptions) + - ✅ LightPush (light client publishing) + - ✅ Store (message history) + - ✅ Peer Exchange (peer discovery) +- **PostgreSQL database** for message persistence +- **Isolated network** - nodes only connect to each other + +## Requirements + +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) or Docker Engine with Compose plugin +- Node.js 18+ (only for npx/npm usage) + +## Installation + +**Option 1: Use npx (no installation required)** +```bash +npx waku-run start +``` + +**Option 2: Install globally** +```bash +npm install -g @waku/run +waku-run start +``` + +**Option 3: Clone and run locally** +```bash +git clone https://github.com/waku-org/js-waku.git +cd js-waku/packages/run +npm install +npm run start +``` + +## Quick Start + +### 1. Start the Network + +**Option A: Using npx (recommended for quick setup)** +```bash +npx waku-run start +``` + +**Option B: Local development** +```bash +cd packages/run +npm run start +``` + +This will: +- Start 2 nwaku nodes and a PostgreSQL database +- Run in the background (detached mode) +- Display connection information you need for your app + +**Example output:** +```json +{ + "bootstrapPeers": [ + "/ip4/127.0.0.1/tcp/60000/ws/p2p/16Uiu2HAm...", + "/ip4/127.0.0.1/tcp/60001/ws/p2p/16Uiu2HAm..." + ], + "networkConfig": { + "clusterId": 1, + "numShardsInCluster": 8 + } +} +``` + +### 2. Connect Your js-waku App + +Copy the output from above and use it in your application: + +```javascript +import { createLightNode } from "@waku/sdk"; + +const waku = await createLightNode({ + defaultBootstrap: false, + bootstrapPeers: [ + "/ip4/127.0.0.1/tcp/60000/ws/p2p/16Uiu2HAm...", + "/ip4/127.0.0.1/tcp/60001/ws/p2p/16Uiu2HAm..." + ], + networkConfig: { + clusterId: 1, + numShardsInCluster: 8 + } +}); + +await waku.start(); + +// Your app is now connected to your local Waku network! +``` + +### 3. Stop When Done + +```bash +npm run stop +``` + +## Available Commands + +### Using npx (published package) + +| Command | Description | +|---------|-------------| +| `npx waku-run start` | Start the network (detached) and show connection info | +| `npx waku-run info` | Show connection info for running network | +| `docker compose down` | Stop the network and clean up | + +### Local development + +| Command | Description | +|---------|-------------| +| `npm run start` | Start the network (detached) and show connection info | +| `npm run stop` | Stop the network and clean up | +| `npm run restart` | Restart the network | +| `npm run logs` | View and follow logs from all nodes | +| `npm run info` | Show connection info for running network | +| `npm test` | Run integration tests | +| `npm run build` | Build TypeScript to JavaScript | + +### Direct Docker Compose Commands + +You can also use standard Docker Compose commands: + +```bash +# Start and see all logs +docker compose up + +# Start in background +docker compose up -d + +# View logs +docker compose logs -f + +# Check status +docker compose ps + +# Stop and clean up +docker compose down + +# Stop and remove volumes (fresh start) +docker compose down -v +``` + +## Configuration + +### Port Configuration + +If the default ports are in use, create a `.env` file: + +```bash +cp .env.example .env +``` + +Edit `.env` to change ports: + +```bash +NODE1_WS_PORT=60000 +NODE1_REST_PORT=8646 +NODE2_WS_PORT=60001 +NODE2_REST_PORT=8647 +``` + +### Cluster Configuration + +The default configuration uses: +- Cluster ID: 1 +- Number of shards: 8 + +To test with different network configurations, create a `.env` file: + +```bash +# .env file +CLUSTER_ID=16 # Change to a different cluster +``` + +Your js-waku app will automatically use the correct configuration from `npm run info`: + +```javascript +const waku = await createLightNode({ + defaultBootstrap: false, + bootstrapPeers: [...], + networkConfig: { + clusterId: 1, // Match your CLUSTER_ID + numShardsInCluster: 8 + } +}); +``` + +### Changing nwaku Version + +```bash +# .env file +NWAKU_IMAGE=wakuorg/nwaku:v0.35.0 +``` + +## Debugging + +### View Node Logs + +```bash +npm run logs + +# Or for a specific node +docker compose logs -f nwaku-1 +docker compose logs -f nwaku-2 +``` + +### Check Node Health + +```bash +# Node 1 +curl http://127.0.0.1:8646/health + +# Node 2 +curl http://127.0.0.1:8647/health +``` + +### Check Peer Connections + +```bash +# Node 1 debug info +curl http://127.0.0.1:8646/debug/v1/info + +# Node 2 debug info +curl http://127.0.0.1:8647/debug/v1/info +``` + +### View Database + +Connect to PostgreSQL to inspect stored messages: + +```bash +docker compose exec postgres psql -U postgres + +# In psql: +\dt # List tables +SELECT * FROM messages LIMIT 10; +``` + +## Troubleshooting + +### Nodes won't start + +**Check if Docker is running:** +```bash +docker ps +``` + +**Check logs for errors:** +```bash +docker compose logs +``` + +**Try a fresh start:** +```bash +docker compose down -v +npm run start +``` + +### Port conflicts + +If you see "port already in use": + +1. Change ports in `.env`: +```bash +NODE1_WS_PORT=50000 +NODE2_WS_PORT=50001 +``` + +2. Or find and stop conflicting processes: +```bash +# macOS/Linux +lsof -i :60000 +kill + +# Or use different ports +``` + +### Nodes won't discover each other + +This is expected on first start. The nodes use peer exchange and discovery protocols to find each other, which can take 10-30 seconds. + +**Check connection status:** +```bash +# Wait a moment after starting +sleep 15 +npm run info +``` + +### Can't connect from js-waku + +**Verify nodes are running:** +```bash +docker compose ps +``` + +**Check your firewall** - ensure localhost connections are allowed + +**Verify ports match** - the ports in your js-waku config must match the `.env` configuration + +## Advanced Usage + +### Customize Docker Compose + +Need more nodes, different configurations, or additional services? + +```bash +# Copy and customize +cp docker-compose.yml my-custom-compose.yml + +# Edit my-custom-compose.yml to add: +# - More nwaku nodes +# - Custom protocol configurations +# - Additional services (monitoring, etc.) + +# Run with your custom config +docker compose -f my-custom-compose.yml up +``` + +### Add More Nodes + +Add to `docker-compose.yml`: + +```yaml +nwaku-3: + <<: *nwaku-base + container_name: waku-local-node-3 + ports: + - "60002:60002/tcp" + - "8648:8648/tcp" + depends_on: + - postgres + - nwaku-1 + command: + - --relay=true + - --filter=true + # ... same config as other nodes + - --websocket-port=60002 + - --rest-port=8648 +``` + +### Enable Debug Logging + +```bash +# .env file +LOG_LEVEL=DEBUG +``` + +Or directly in docker-compose: +```yaml +- --log-level=DEBUG +- --log-format=json # Structured logs +``` + +## Use Cases + +- ✅ **Hackathon development** - Work without internet or unreliable connections +- ✅ **Local testing** - Test your app against real nwaku nodes +- ✅ **CI/CD integration tests** - Automated testing in isolated environments +- ✅ **Protocol experimentation** - Try different configurations safely +- ✅ **Offline demos** - Show your app working without external dependencies + +## Architecture + +``` +┌─────────────────────────────────────────────┐ +│ Your js-waku Application │ +│ │ +│ createLightNode({ │ +│ bootstrapPeers: [node1, node2] │ +│ }) │ +└──────────────┬──────────────────────────────┘ + │ WebSocket connections + │ (127.0.0.1:60000, 60001) + │ + ┌──────────┴──────────┐ + │ │ +┌───▼────┐ ┌────▼───┐ +│ nwaku-1│◄─────────►│nwaku-2 │ +│ │ relay │ │ +│ :60000 │ gossip │ :60001 │ +└───┬────┘ └────┬───┘ + │ │ + └──────────┬──────────┘ + │ + ┌─────▼─────┐ + │ PostgreSQL│ + │ :5432 │ + └───────────┘ +``` + +Both nodes: +- Run all Waku protocols (relay, filter, lightpush, store) +- Share a PostgreSQL database for message persistence +- Connected to each other via relay protocol +- Discover each other via peer exchange +- Expose WebSocket for js-waku connections +- Expose REST API for debugging + +## FAQ + +**Q: Do I need to wait for nodes to connect before starting my app?** +A: No, you can start your app immediately. js-waku will wait for peers to be available. + +**Q: Can I use this for production?** +A: No, this is for development only. For production, use The Waku Network or run your own fleet. + +**Q: Why PostgreSQL?** +A: The nwaku store protocol requires a database to persist messages. This allows your app to query message history. + +**Q: Can I connect from a different machine?** +A: Yes, but you'll need to change `127.0.0.1` to your machine's IP address in the multiaddrs and ensure your firewall allows the connections. + +**Q: How much disk space does this use?** +A: Minimal - the PostgreSQL database only stores messages from your local testing. Use `docker compose down -v` to remove all data. + +## Resources + +- [js-waku Documentation](https://docs.waku.org/guides/js-waku/) +- [nwaku GitHub](https://github.com/waku-org/nwaku) +- [Waku Protocol Specifications](https://rfc.vac.dev/) +- [Example Applications](https://github.com/waku-org/js-waku-examples) + +## License + +MIT OR Apache-2.0 diff --git a/packages/run/cspell.json b/packages/run/cspell.json new file mode 100644 index 0000000000..52602a1e15 --- /dev/null +++ b/packages/run/cspell.json @@ -0,0 +1,27 @@ +{ + "version": "0.2", + "language": "en", + "words": [ + "hackathons", + "hackathon", + "waku", + "Waku", + "nwaku", + "wakuorg", + "dockerode", + "multiaddr", + "multiaddrs", + "libp2p", + "pubsub", + "gossipsub", + "lightpush", + "Lightpush", + "psql", + "isready" + ], + "ignorePaths": [ + "node_modules", + "dist", + "*.log" + ] +} diff --git a/packages/run/docker-compose.yml b/packages/run/docker-compose.yml new file mode 100644 index 0000000000..3c6669df90 --- /dev/null +++ b/packages/run/docker-compose.yml @@ -0,0 +1,126 @@ +# Environment variable definitions +x-pg-pass: &pg_pass ${POSTGRES_PASSWORD:-test123} +x-pg-user: &pg_user ${POSTGRES_USER:-postgres} + +x-pg-environment: &pg_env + POSTGRES_USER: *pg_user + POSTGRES_PASSWORD: *pg_pass + +# Shared nwaku configuration +x-nwaku-base: &nwaku-base + image: ${NWAKU_IMAGE:-wakuorg/nwaku:v0.36.0} + restart: on-failure + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +services: + postgres: + image: postgres:15.4-alpine3.18 + restart: on-failure + environment: + <<: *pg_env + POSTGRES_DB: postgres + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + nwaku-1: + <<: *nwaku-base + container_name: waku-local-node-1 + ports: + - "${NODE1_TCP_PORT:-30303}:30303/tcp" + - "${NODE1_WS_PORT:-60000}:60000/tcp" + - "${NODE1_REST_PORT:-8646}:8646/tcp" + environment: + <<: *pg_env + depends_on: + postgres: + condition: service_healthy + command: + - --relay=true + - --filter=true + - --lightpush=true + - --store=true + - --peer-exchange=true + - --discv5-discovery=true + - --cluster-id=0 + - --shard=0 + - --shard=1 + - --shard=2 + - --shard=3 + - --shard=4 + - --shard=5 + - --shard=6 + - --shard=7 + - --listen-address=0.0.0.0 + - --tcp-port=30303 + - --websocket-support=true + - --websocket-port=60000 + - --ext-multiaddr=/ip4/127.0.0.1/tcp/60000/ws + - --ext-multiaddr-only=true + - --rest=true + - --rest-address=0.0.0.0 + - --rest-port=8646 + - --rest-admin=true + - --store-message-db-url=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-test123}@postgres:5432/postgres + - --log-level=${LOG_LEVEL:-INFO} + - --max-connections=150 + + nwaku-2: + <<: *nwaku-base + container_name: waku-local-node-2 + ports: + - "${NODE2_TCP_PORT:-30304}:30304/tcp" + - "${NODE2_WS_PORT:-60001}:60001/tcp" + - "${NODE2_REST_PORT:-8647}:8647/tcp" + environment: + <<: *pg_env + depends_on: + postgres: + condition: service_healthy + nwaku-1: + condition: service_started + command: + - --relay=true + - --filter=true + - --lightpush=true + - --store=true + - --peer-exchange=true + - --discv5-discovery=true + - --cluster-id=0 + - --shard=0 + - --shard=1 + - --shard=2 + - --shard=3 + - --shard=4 + - --shard=5 + - --shard=6 + - --shard=7 + - --listen-address=0.0.0.0 + - --tcp-port=30304 + - --websocket-support=true + - --websocket-port=60001 + - --ext-multiaddr=/ip4/127.0.0.1/tcp/60001/ws + - --ext-multiaddr-only=true + - --rest=true + - --rest-address=0.0.0.0 + - --rest-port=8647 + - --rest-admin=true + - --store-message-db-url=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-test123}@postgres:5432/postgres + - --log-level=${LOG_LEVEL:-INFO} + - --max-connections=150 + +volumes: + postgres-data: + +networks: + default: + name: waku-local-network diff --git a/packages/run/package.json b/packages/run/package.json new file mode 100644 index 0000000000..7d58720bbb --- /dev/null +++ b/packages/run/package.json @@ -0,0 +1,67 @@ +{ + "name": "@waku/run", + "version": "0.0.1", + "description": "Run a local Waku network for development and testing", + "type": "module", + "author": "Waku Team", + "homepage": "https://github.com/waku-org/js-waku/tree/master/packages/run#readme", + "repository": { + "type": "git", + "url": "https://github.com/waku-org/js-waku.git" + }, + "bugs": { + "url": "https://github.com/waku-org/js-waku/issues" + }, + "license": "MIT OR Apache-2.0", + "keywords": [ + "waku", + "decentralized", + "communication", + "web3", + "testing", + "development" + ], + "bin": { + "waku-run": "./dist/src/cli.js" + }, + "files": [ + "dist", + "docker-compose.yml", + ".env.example", + "README.md" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "npm run build", + "start": "tsx scripts/start.ts", + "stop": "docker compose down", + "restart": "npm run stop && npm run start", + "logs": "docker compose logs -f", + "info": "tsx scripts/info.ts", + "test": "NODE_ENV=test node ./src/run-tests.js \"tests/**/*.spec.ts\"", + "fix": "run-s fix:*", + "fix:lint": "eslint src scripts tests --fix", + "check": "run-s check:*", + "check:tsc": "tsc -p tsconfig.dev.json", + "check:lint": "eslint src scripts tests", + "check:spelling": "cspell \"{README.md,src/**/*.ts,scripts/**/*.ts,tests/**/*.ts}\"" + }, + "engines": { + "node": ">=18" + }, + "devDependencies": { + "@types/chai": "^4.3.11", + "@types/mocha": "^10.0.6", + "@waku/core": "*", + "@waku/interfaces": "*", + "@waku/sdk": "*", + "@waku/utils": "*", + "chai": "^4.3.10", + "cspell": "^8.6.1", + "mocha": "^10.3.0", + "npm-run-all": "^4.1.5", + "ts-node": "^10.9.2", + "tsx": "^4.7.0", + "typescript": "^5.3.3" + } +} diff --git a/packages/run/scripts/info.ts b/packages/run/scripts/info.ts new file mode 100755 index 0000000000..9be1daca6d --- /dev/null +++ b/packages/run/scripts/info.ts @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +import { execSync } from "child_process"; + +interface Colors { + reset: string; + cyan: string; + blue: string; + gray: string; + yellow: string; +} + +interface NodeInfo { + listenAddresses: string[]; +} + +// ANSI color codes +const colors: Colors = { + reset: "\x1b[0m", + cyan: "\x1b[36m", + blue: "\x1b[34m", + gray: "\x1b[90m", + yellow: "\x1b[33m" +}; + +try { + // Check if containers are running + const output: string = execSync("docker compose ps --quiet", { + encoding: "utf-8" + }).trim(); + + if (!output) { + process.stdout.write( + `${colors.gray}No nodes running. Start with: ${colors.cyan}npm run start${colors.reset}\n` + ); + process.exit(0); + } + + // Get cluster config from env or defaults + const clusterId: string = process.env.CLUSTER_ID || "0"; + const node1Port: string = process.env.NODE1_WS_PORT || "60000"; + const node2Port: string = process.env.NODE2_WS_PORT || "60001"; + + // Fetch node info + const node1Info: NodeInfo = await fetch( + "http://127.0.0.1:8646/debug/v1/info" + ).then((r) => r.json()); + const node2Info: NodeInfo = await fetch( + "http://127.0.0.1:8647/debug/v1/info" + ).then((r) => r.json()); + + const peer1: string = node1Info.listenAddresses[0].split("/p2p/")[1]; + const peer2: string = node2Info.listenAddresses[0].split("/p2p/")[1]; + + // Print TypeScript-style config + process.stdout.write( + `${colors.blue}import${colors.reset} { createLightNode } ${colors.blue}from${colors.reset} ${colors.yellow}"@waku/sdk"${colors.reset};\n` + ); + process.stdout.write(`\n`); + process.stdout.write( + `${colors.blue}const${colors.reset} waku = ${colors.blue}await${colors.reset} createLightNode({\n` + ); + process.stdout.write( + ` defaultBootstrap: ${colors.cyan}false${colors.reset},\n` + ); + process.stdout.write(` bootstrapPeers: [\n`); + process.stdout.write( + ` ${colors.yellow}"/ip4/127.0.0.1/tcp/${node1Port}/ws/p2p/${peer1}"${colors.reset},\n` + ); + process.stdout.write( + ` ${colors.yellow}"/ip4/127.0.0.1/tcp/${node2Port}/ws/p2p/${peer2}"${colors.reset}\n` + ); + process.stdout.write(` ],\n`); + process.stdout.write(` networkConfig: {\n`); + process.stdout.write( + ` clusterId: ${colors.cyan}${clusterId}${colors.reset},\n` + ); + process.stdout.write( + ` numShardsInCluster: ${colors.cyan}8${colors.reset}\n` + ); + process.stdout.write(` }\n`); + process.stdout.write(`});\n`); +} catch (error: unknown) { + const err = error as { cause?: { code?: string }; message?: string }; + if (err.cause?.code === "ECONNREFUSED") { + process.stderr.write( + `${colors.yellow}⚠${colors.reset} Nodes are still starting. Try again in a few seconds.\n` + ); + process.exit(1); + } else { + process.stderr.write( + `${colors.yellow}✗${colors.reset} Error: ${err.message || String(error)}\n` + ); + process.exit(1); + } +} diff --git a/packages/run/scripts/start.ts b/packages/run/scripts/start.ts new file mode 100755 index 0000000000..33c4044731 --- /dev/null +++ b/packages/run/scripts/start.ts @@ -0,0 +1,141 @@ +#!/usr/bin/env node + +import { execSync } from "child_process"; + +interface Colors { + reset: string; + cyan: string; + green: string; + blue: string; + gray: string; + yellow: string; +} + +interface NodeInfo { + listenAddresses: string[]; +} + +// ANSI color codes +const colors: Colors = { + reset: "\x1b[0m", + cyan: "\x1b[36m", + green: "\x1b[32m", + blue: "\x1b[34m", + gray: "\x1b[90m", + yellow: "\x1b[33m" +}; + +async function waitWithProgress(ms: number): Promise { + const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + const startTime = Date.now(); + let frameIndex = 0; + + return new Promise((resolve) => { + const interval = setInterval(() => { + const elapsed = Date.now() - startTime; + + if (elapsed >= ms) { + clearInterval(interval); + process.stdout.write("\r" + " ".repeat(50) + "\r"); + resolve(); + return; + } + + const frame = frames[frameIndex % frames.length]; + process.stdout.write( + `\r${colors.cyan}${frame}${colors.reset} Waiting for nodes to start...` + ); + frameIndex++; + }, 100); + }); +} + +process.stdout.write( + `${colors.cyan}Starting local Waku development environment...${colors.reset}\n` +); + +try { + // Start docker compose quietly + execSync("docker compose up -d", { + stdio: ["ignore", "ignore", "pipe"], + encoding: "utf-8" + }); + + // Wait for nodes to be ready + await waitWithProgress(20000); + + // Get cluster config from env or defaults + const clusterId: string = process.env.CLUSTER_ID || "0"; + const node1Port: string = process.env.NODE1_WS_PORT || "60000"; + const node2Port: string = process.env.NODE2_WS_PORT || "60001"; + + // Fetch node info + const node1Info: NodeInfo = await fetch( + "http://127.0.0.1:8646/debug/v1/info" + ).then((r) => r.json()); + const node2Info: NodeInfo = await fetch( + "http://127.0.0.1:8647/debug/v1/info" + ).then((r) => r.json()); + + const peer1: string = node1Info.listenAddresses[0].split("/p2p/")[1]; + const peer2: string = node2Info.listenAddresses[0].split("/p2p/")[1]; + + // Print TypeScript-style config + process.stdout.write( + `${colors.green}✓${colors.reset} Network started successfully!\n\n` + ); + process.stdout.write( + `${colors.gray}Copy this into your application:${colors.reset}\n\n` + ); + + process.stdout.write( + `${colors.blue}import${colors.reset} { createLightNode } ${colors.blue}from${colors.reset} ${colors.yellow}"@waku/sdk"${colors.reset};\n` + ); + process.stdout.write(`\n`); + process.stdout.write( + `${colors.blue}const${colors.reset} waku = ${colors.blue}await${colors.reset} createLightNode({\n` + ); + process.stdout.write( + ` defaultBootstrap: ${colors.cyan}false${colors.reset},\n` + ); + process.stdout.write(` bootstrapPeers: [\n`); + process.stdout.write( + ` ${colors.yellow}"/ip4/127.0.0.1/tcp/${node1Port}/ws/p2p/${peer1}"${colors.reset},\n` + ); + process.stdout.write( + ` ${colors.yellow}"/ip4/127.0.0.1/tcp/${node2Port}/ws/p2p/${peer2}"${colors.reset}\n` + ); + process.stdout.write(` ],\n`); + process.stdout.write(` networkConfig: {\n`); + process.stdout.write( + ` clusterId: ${colors.cyan}${clusterId}${colors.reset},\n` + ); + process.stdout.write( + ` numShardsInCluster: ${colors.cyan}8${colors.reset}\n` + ); + process.stdout.write(` }\n`); + process.stdout.write(`});\n`); + process.stdout.write(`\n`); + process.stdout.write(`${colors.gray}Management:${colors.reset}\n`); + process.stdout.write( + ` ${colors.cyan}npm run logs${colors.reset} - View logs\n` + ); + process.stdout.write( + ` ${colors.cyan}npm run info${colors.reset} - Show config again\n` + ); + process.stdout.write( + ` ${colors.cyan}npm run stop${colors.reset} - Stop network\n` + ); +} catch (error: unknown) { + const err = error as { cause?: { code?: string }; message?: string }; + if (err.cause?.code === "ECONNREFUSED") { + process.stderr.write( + `${colors.yellow}⚠${colors.reset} Nodes are still starting up. Run ${colors.cyan}npm run info${colors.reset} in a few seconds.\n` + ); + } else { + process.stderr.write( + `${colors.yellow}✗${colors.reset} Error: ${err.message || String(error)}\n` + ); + } + process.exit(1); +} diff --git a/packages/run/src/cli.ts b/packages/run/src/cli.ts new file mode 100644 index 0000000000..792eebb5fb --- /dev/null +++ b/packages/run/src/cli.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { spawn } from "child_process"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const command = process.argv[2]; + +const scriptMap: Record = { + start: join(__dirname, "..", "scripts", "start.js"), + info: join(__dirname, "..", "scripts", "info.js") +}; + +if (!command || !scriptMap[command]) { + process.stderr.write("Usage: @waku/run \n"); + process.stderr.write("\n"); + process.stderr.write("Commands:\n"); + process.stderr.write(" start Start the local Waku network\n"); + process.stderr.write(" info Show connection info for running network\n"); + process.exit(1); +} + +const scriptPath = scriptMap[command]; +const child = spawn("node", [scriptPath], { + stdio: "inherit", + env: process.env +}); + +child.on("exit", (code) => { + process.exit(code || 0); +}); diff --git a/packages/run/src/run-tests.js b/packages/run/src/run-tests.js new file mode 100644 index 0000000000..0c4ff82c6e --- /dev/null +++ b/packages/run/src/run-tests.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +import { spawn } from "child_process"; + +const mochaArgs = [ + "mocha", + "--require", + "ts-node/register", + "--project", + "./tsconfig.json", + ...process.argv.slice(2) +]; + +// Run mocha tests +const mocha = spawn("npx", mochaArgs, { + stdio: "inherit", + env: { + ...process.env, + NODE_ENV: "test" + } +}); + +mocha.on("error", (error) => { + console.log(`Error running mocha tests: ${error.message}`); // eslint-disable-line no-console + process.exit(1); +}); + +mocha.on("exit", (code) => { + process.exit(code || 0); +}); diff --git a/packages/run/tests/basic.spec.ts b/packages/run/tests/basic.spec.ts new file mode 100644 index 0000000000..2e7e521cc8 --- /dev/null +++ b/packages/run/tests/basic.spec.ts @@ -0,0 +1,110 @@ +import { execSync } from "child_process"; + +import { createEncoder } from "@waku/core"; +import type { LightNode } from "@waku/interfaces"; +import { createLightNode, Protocols, waitForRemotePeer } from "@waku/sdk"; +import { createRoutingInfo } from "@waku/utils"; +import { expect } from "chai"; + +describe("Waku Run - Basic Test", function () { + this.timeout(90000); + + let waku: LightNode; + + before(async function () { + // Step 1: Start the nodes + execSync("docker compose up -d", { + stdio: "inherit" + }); + + // Wait for nodes to be ready + const maxRetries = 30; + const retryDelay = 2000; + let ready = false; + + for (let i = 0; i < maxRetries; i++) { + try { + await fetch("http://127.0.0.1:8646/debug/v1/info"); + await fetch("http://127.0.0.1:8647/debug/v1/info"); + ready = true; + break; + } catch { + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + } + } + + if (!ready) { + throw new Error("Nodes failed to start within expected time"); + } + + // Connect the two nwaku nodes together + const node1Info = await fetch("http://127.0.0.1:8646/debug/v1/info").then( + (r) => r.json() + ); + const peer1Multiaddr = node1Info.listenAddresses[0]; + + await fetch("http://127.0.0.1:8647/admin/v1/peers", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([peer1Multiaddr]) + }); + + // Wait a bit for the connection to establish + await new Promise((resolve) => setTimeout(resolve, 2000)); + }); + + after(async function () { + // Step 4: Stop the nodes + if (waku) { + await waku.stop(); + } + execSync("docker compose down", { + stdio: "inherit" + }); + }); + + it("should connect to nodes and send lightpush message", async function () { + // Step 2: Connect to nodes via js-waku + const node1Port = process.env.NODE1_WS_PORT || "60000"; + const node2Port = process.env.NODE2_WS_PORT || "60001"; + + // Fetch node info to get peer IDs + const node1Info = await fetch("http://127.0.0.1:8646/debug/v1/info").then( + (r) => r.json() + ); + const node2Info = await fetch("http://127.0.0.1:8647/debug/v1/info").then( + (r) => r.json() + ); + + const peer1 = node1Info.listenAddresses[0].split("/p2p/")[1]; + const peer2 = node2Info.listenAddresses[0].split("/p2p/")[1]; + + const networkConfig = { + clusterId: 0, + numShardsInCluster: 8 + }; + + waku = await createLightNode({ + defaultBootstrap: false, + bootstrapPeers: [ + `/ip4/127.0.0.1/tcp/${node1Port}/ws/p2p/${peer1}`, + `/ip4/127.0.0.1/tcp/${node2Port}/ws/p2p/${peer2}` + ], + networkConfig + }); + + await waku.start(); + await waku.waitForPeers([Protocols.LightPush]); + + // Step 3: Send a lightpush message + const contentTopic = "/test/1/basic/proto"; + const routingInfo = createRoutingInfo(networkConfig, { contentTopic }); + const encoder = createEncoder({ contentTopic, routingInfo }); + + const result = await waku.lightPush.send(encoder, { + payload: new TextEncoder().encode("Hello Waku!") + }); + + expect(result.successes.length).to.be.greaterThan(0); + }); +}); diff --git a/packages/run/tsconfig.dev.json b/packages/run/tsconfig.dev.json new file mode 100644 index 0000000000..2e8879a53e --- /dev/null +++ b/packages/run/tsconfig.dev.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.dev", + "compilerOptions": { + "rootDir": "." + }, + "include": ["src", "scripts", "tests"] +} diff --git a/packages/run/tsconfig.json b/packages/run/tsconfig.json new file mode 100644 index 0000000000..84417ad4b5 --- /dev/null +++ b/packages/run/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "dist/", + "rootDir": ".", + "tsBuildInfoFile": "dist/.tsbuildinfo" + }, + "include": ["src", "scripts"], + "exclude": ["tests", "dist", "node_modules"] +}