mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-02 05:43:08 +00:00
feat: create @waku/run package for local dev env (#2678)
* feat: create @waku/run package for local dev env * chore: add @waku/run to release please config * feat: test @waku/run with playwright * fix: don't run waku/run tests in CI * fix: cache images so docker-compose can work offline * feat: set nodekey and staticnode flags for each nwaku node * fix: use constants for node ids * chore: set directories for running via npx * fix: remove .env, support env vars for nwaku ports * fix: use separate db (same instance) for each node * feat: add command to test dev env * chore: use package version in container name * fix: replace hardcoded WS/REST ports with constants/env vars * chore: clean up README * fix: refactor config printing into own function * fix: add run package to release please manifest * fix: defer to root folder gitignore/cspell * fix: update node version and remove tsx * fix: remove browser tests and express dep * fix: replace magic values with constants * fix: move to root .gitignore * fix: move cspell to root
This commit is contained in:
parent
ff9c43038e
commit
0df18b2a75
@ -55,6 +55,7 @@
|
|||||||
"fontsource",
|
"fontsource",
|
||||||
"globby",
|
"globby",
|
||||||
"gossipsub",
|
"gossipsub",
|
||||||
|
"hackathons",
|
||||||
"huilong",
|
"huilong",
|
||||||
"iasked",
|
"iasked",
|
||||||
"ihave",
|
"ihave",
|
||||||
@ -62,6 +63,7 @@
|
|||||||
"ineed",
|
"ineed",
|
||||||
"IPAM",
|
"IPAM",
|
||||||
"ipfs",
|
"ipfs",
|
||||||
|
"isready",
|
||||||
"iwant",
|
"iwant",
|
||||||
"jdev",
|
"jdev",
|
||||||
"jswaku",
|
"jswaku",
|
||||||
@ -165,6 +167,7 @@
|
|||||||
"gen",
|
"gen",
|
||||||
"proto",
|
"proto",
|
||||||
"*.spec.ts",
|
"*.spec.ts",
|
||||||
|
"*.log",
|
||||||
"CHANGELOG.md"
|
"CHANGELOG.md"
|
||||||
],
|
],
|
||||||
"patterns": [
|
"patterns": [
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -17,4 +17,6 @@ packages/discovery/mock_local_storage
|
|||||||
.giga
|
.giga
|
||||||
.cursor
|
.cursor
|
||||||
.DS_Store
|
.DS_Store
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
.env
|
||||||
|
postgres-data/
|
||||||
|
|||||||
@ -10,5 +10,6 @@
|
|||||||
"packages/discovery": "0.0.12",
|
"packages/discovery": "0.0.12",
|
||||||
"packages/sds": "0.0.7",
|
"packages/sds": "0.0.7",
|
||||||
"packages/rln": "0.1.9",
|
"packages/rln": "0.1.9",
|
||||||
"packages/react": "0.0.7"
|
"packages/react": "0.0.7",
|
||||||
|
"packages/run": "0.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
32
package-lock.json
generated
32
package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"packages/rln",
|
"packages/rln",
|
||||||
"packages/sdk",
|
"packages/sdk",
|
||||||
"packages/relay",
|
"packages/relay",
|
||||||
|
"packages/run",
|
||||||
"packages/tests",
|
"packages/tests",
|
||||||
"packages/reliability-tests",
|
"packages/reliability-tests",
|
||||||
"packages/browser-tests",
|
"packages/browser-tests",
|
||||||
@ -7643,6 +7644,10 @@
|
|||||||
"resolved": "packages/rln",
|
"resolved": "packages/rln",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@waku/run": {
|
||||||
|
"resolved": "packages/run",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@waku/sdk": {
|
"node_modules/@waku/sdk": {
|
||||||
"resolved": "packages/sdk",
|
"resolved": "packages/sdk",
|
||||||
"link": true
|
"link": true
|
||||||
@ -35978,6 +35983,33 @@
|
|||||||
"uuid": "dist/esm/bin/uuid"
|
"uuid": "dist/esm/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/run": {
|
||||||
|
"name": "@waku/run",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@waku/core": "*",
|
||||||
|
"@waku/interfaces": "*",
|
||||||
|
"@waku/sdk": "*",
|
||||||
|
"@waku/utils": "*"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"waku-run": "dist/src/cli.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.3.11",
|
||||||
|
"@types/mocha": "^10.0.6",
|
||||||
|
"chai": "^4.3.10",
|
||||||
|
"cspell": "^8.6.1",
|
||||||
|
"mocha": "^10.3.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/sdk": {
|
"packages/sdk": {
|
||||||
"name": "@waku/sdk",
|
"name": "@waku/sdk",
|
||||||
"version": "0.0.35",
|
"version": "0.0.35",
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
"packages/rln",
|
"packages/rln",
|
||||||
"packages/sdk",
|
"packages/sdk",
|
||||||
"packages/relay",
|
"packages/relay",
|
||||||
|
"packages/run",
|
||||||
"packages/tests",
|
"packages/tests",
|
||||||
"packages/reliability-tests",
|
"packages/reliability-tests",
|
||||||
"packages/browser-tests",
|
"packages/browser-tests",
|
||||||
|
|||||||
20
packages/run/.eslintrc.cjs
Normal file
20
packages/run/.eslintrc.cjs
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
11
packages/run/.mocharc.cjs
Normal file
11
packages/run/.mocharc.cjs
Normal file
@ -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
|
||||||
|
};
|
||||||
148
packages/run/README.md
Normal file
148
packages/run/README.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# @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:
|
||||||
|
- **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
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Start the Network
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @waku/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:**
|
||||||
|
```typescript
|
||||||
|
import { createLightNode } from "@waku/sdk";
|
||||||
|
|
||||||
|
const waku = await createLightNode({
|
||||||
|
defaultBootstrap: false,
|
||||||
|
bootstrapPeers: [
|
||||||
|
"/ip4/127.0.0.1/tcp/60000/ws/p2p/16Uiu2HAmF6oAsd23RMAnZb3NJgxXrExxBTPMdEoih232iAZkviU2",
|
||||||
|
"/ip4/127.0.0.1/tcp/60001/ws/p2p/16Uiu2HAm5aZU47YkiUoARqivbCXwuFPzFFXXiURAorySqAQbL6EQ"
|
||||||
|
],
|
||||||
|
numPeersToUse: 2,
|
||||||
|
libp2p: {
|
||||||
|
filterMultiaddrs: false
|
||||||
|
},
|
||||||
|
networkConfig: {
|
||||||
|
clusterId: 0,
|
||||||
|
numShardsInCluster: 8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Connect Your js-waku App
|
||||||
|
|
||||||
|
Copy the configuration from the output above and paste it into your application. Then start your node:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await waku.start();
|
||||||
|
|
||||||
|
// Your app is now connected to your local Waku network!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Stop When Done
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @waku/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 stop` | Stop the network and clean up |
|
||||||
|
| `npx @waku/run info` | Show connection info for running network |
|
||||||
|
| `npx @waku/run logs` | View and follow logs from all nodes |
|
||||||
|
| `npx @waku/run test` | Test the network by sending a message |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All configuration is done via environment variables passed to the command.
|
||||||
|
|
||||||
|
### Custom Ports
|
||||||
|
|
||||||
|
If the default ports are in use, specify custom ports:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NODE1_WS_PORT=50000 NODE2_WS_PORT=50001 npx @waku/run start
|
||||||
|
```
|
||||||
|
|
||||||
|
Available port configuration:
|
||||||
|
- `NODE1_WS_PORT` (default: 60000)
|
||||||
|
- `NODE2_WS_PORT` (default: 60001)
|
||||||
|
- `NODE1_REST_PORT` (default: 8646)
|
||||||
|
- `NODE2_REST_PORT` (default: 8647)
|
||||||
|
|
||||||
|
### Cluster Configuration
|
||||||
|
|
||||||
|
The default configuration uses:
|
||||||
|
- Cluster ID: 0
|
||||||
|
- Number of shards: 8
|
||||||
|
|
||||||
|
To test with a different cluster:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CLUSTER_ID=16 npx @waku/run start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom nwaku Version
|
||||||
|
|
||||||
|
To use a different nwaku image version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NWAKU_IMAGE=wakuorg/nwaku:v0.35.0 npx @waku/run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### View Node Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @waku/run logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT OR Apache-2.0
|
||||||
142
packages/run/docker-compose.yml
Normal file
142
packages/run/docker-compose.yml
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
# 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}
|
||||||
|
pull_policy: if_not_present
|
||||||
|
restart: on-failure
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15.4-alpine3.18
|
||||||
|
pull_policy: if_not_present
|
||||||
|
restart: on-failure
|
||||||
|
environment:
|
||||||
|
<<: *pg_env
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
nwaku-1:
|
||||||
|
<<: *nwaku-base
|
||||||
|
container_name: ${COMPOSE_PROJECT_NAME:-waku-run-0-0-1}-node-1
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
ipv4_address: 172.20.0.10
|
||||||
|
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:
|
||||||
|
- --nodekey=e419c3cf4f09ac3babdf61856e6faa0e0c6a7d97674d5401a0114616549c7632
|
||||||
|
- --staticnode=/ip4/172.20.0.11/tcp/60001/ws/p2p/16Uiu2HAm5aZU47YkiUoARqivbCXwuFPzFFXXiURAorySqAQbL6EQ
|
||||||
|
- --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=/dns4/nwaku-1/tcp/60000/ws
|
||||||
|
- --ext-multiaddr=/ip4/127.0.0.1/tcp/60000/ws
|
||||||
|
- --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/nwaku1
|
||||||
|
- --log-level=${LOG_LEVEL:-INFO}
|
||||||
|
- --max-connections=150
|
||||||
|
|
||||||
|
nwaku-2:
|
||||||
|
<<: *nwaku-base
|
||||||
|
container_name: ${COMPOSE_PROJECT_NAME:-waku-run-0-0-1}-node-2
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
ipv4_address: 172.20.0.11
|
||||||
|
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:
|
||||||
|
- --nodekey=50632ab0efd313bfb4aa842de716f03dacd181c863770abd145e3409290fdaa7
|
||||||
|
- --staticnode=/ip4/172.20.0.10/tcp/60000/ws/p2p/16Uiu2HAmF6oAsd23RMAnZb3NJgxXrExxBTPMdEoih232iAZkviU2
|
||||||
|
- --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=/dns4/nwaku-2/tcp/60001/ws
|
||||||
|
- --ext-multiaddr=/ip4/127.0.0.1/tcp/60001/ws
|
||||||
|
- --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/nwaku2
|
||||||
|
- --log-level=${LOG_LEVEL:-INFO}
|
||||||
|
- --max-connections=150
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: ${COMPOSE_PROJECT_NAME:-waku-run-0-0-1}-network
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/16
|
||||||
8
packages/run/init-db.sh
Executable file
8
packages/run/init-db.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Create separate databases for each nwaku node
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
|
||||||
|
CREATE DATABASE nwaku1;
|
||||||
|
CREATE DATABASE nwaku2;
|
||||||
|
EOSQL
|
||||||
68
packages/run/package.json
Normal file
68
packages/run/package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"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": "git+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",
|
||||||
|
"init-db.sh",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"prepublishOnly": "npm run build",
|
||||||
|
"start": "node dist/scripts/start.js",
|
||||||
|
"stop": "node dist/scripts/stop.js",
|
||||||
|
"restart": "npm run stop && npm run start",
|
||||||
|
"logs": "node dist/scripts/logs.js",
|
||||||
|
"info": "node dist/scripts/info.js",
|
||||||
|
"test": "if [ \"$CI\" = \"true\" ]; then echo 'Skipping tests in CI'; exit 0; fi && NODE_ENV=test node ./src/run-tests.js \"tests/basic.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": ">=22"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@waku/core": "*",
|
||||||
|
"@waku/interfaces": "*",
|
||||||
|
"@waku/sdk": "*",
|
||||||
|
"@waku/utils": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.3.11",
|
||||||
|
"@types/mocha": "^10.0.6",
|
||||||
|
"chai": "^4.3.10",
|
||||||
|
"cspell": "^8.6.1",
|
||||||
|
"mocha": "^10.3.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
83
packages/run/scripts/info.ts
Executable file
83
packages/run/scripts/info.ts
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_CLUSTER_ID,
|
||||||
|
DEFAULT_NODE1_WS_PORT,
|
||||||
|
DEFAULT_NODE2_WS_PORT,
|
||||||
|
NODE1_PEER_ID,
|
||||||
|
NODE2_PEER_ID
|
||||||
|
} from "../src/constants.js";
|
||||||
|
import { getProjectName, printWakuConfig } from "../src/utils.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const packageRoot = __dirname.includes("dist")
|
||||||
|
? join(__dirname, "..", "..")
|
||||||
|
: join(__dirname, "..");
|
||||||
|
|
||||||
|
interface Colors {
|
||||||
|
reset: string;
|
||||||
|
cyan: string;
|
||||||
|
blue: string;
|
||||||
|
gray: string;
|
||||||
|
yellow: 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 projectName = getProjectName(packageRoot);
|
||||||
|
const output: string = execSync(
|
||||||
|
`docker compose --project-name ${projectName} ps --quiet`,
|
||||||
|
{
|
||||||
|
cwd: packageRoot,
|
||||||
|
encoding: "utf-8",
|
||||||
|
env: { ...process.env, COMPOSE_PROJECT_NAME: projectName }
|
||||||
|
}
|
||||||
|
).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 || DEFAULT_CLUSTER_ID;
|
||||||
|
const node1Port: string = process.env.NODE1_WS_PORT || DEFAULT_NODE1_WS_PORT;
|
||||||
|
const node2Port: string = process.env.NODE2_WS_PORT || DEFAULT_NODE2_WS_PORT;
|
||||||
|
|
||||||
|
// Static peer IDs from --nodekey configuration
|
||||||
|
// cspell:ignore nodekey
|
||||||
|
const peer1: string = NODE1_PEER_ID;
|
||||||
|
const peer2: string = NODE2_PEER_ID;
|
||||||
|
|
||||||
|
// Print TypeScript-style config
|
||||||
|
printWakuConfig(colors, node1Port, node2Port, peer1, peer2, clusterId);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/run/scripts/logs.ts
Normal file
26
packages/run/scripts/logs.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
import { getProjectName } from "../src/utils.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const packageRoot = __dirname.includes("dist")
|
||||||
|
? join(__dirname, "..", "..")
|
||||||
|
: join(__dirname, "..");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectName = getProjectName(packageRoot);
|
||||||
|
execSync(`docker compose --project-name ${projectName} logs -f`, {
|
||||||
|
cwd: packageRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: { ...process.env, COMPOSE_PROJECT_NAME: projectName }
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const err = error as { message?: string };
|
||||||
|
process.stderr.write(`Error viewing logs: ${err.message || String(error)}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
172
packages/run/scripts/start.ts
Executable file
172
packages/run/scripts/start.ts
Executable file
@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_CLUSTER_ID,
|
||||||
|
DEFAULT_NODE1_WS_PORT,
|
||||||
|
DEFAULT_NODE2_WS_PORT,
|
||||||
|
DEFAULT_NWAKU_IMAGE,
|
||||||
|
NODE1_PEER_ID,
|
||||||
|
NODE2_PEER_ID,
|
||||||
|
POSTGRES_IMAGE,
|
||||||
|
STARTUP_WAIT_MS
|
||||||
|
} from "../src/constants.js";
|
||||||
|
import { getProjectName, printWakuConfig } from "../src/utils.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const packageRoot = __dirname.includes("dist")
|
||||||
|
? join(__dirname, "..", "..")
|
||||||
|
: join(__dirname, "..");
|
||||||
|
|
||||||
|
interface Colors {
|
||||||
|
reset: string;
|
||||||
|
cyan: string;
|
||||||
|
green: string;
|
||||||
|
blue: string;
|
||||||
|
gray: string;
|
||||||
|
yellow: 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"
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkAndPullImages(): void {
|
||||||
|
const nwakuImage = process.env.NWAKU_IMAGE || DEFAULT_NWAKU_IMAGE;
|
||||||
|
const postgresImage = POSTGRES_IMAGE;
|
||||||
|
const images = [
|
||||||
|
{ name: nwakuImage, label: "nwaku" },
|
||||||
|
{ name: postgresImage, label: "postgres" }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { name, label } of images) {
|
||||||
|
try {
|
||||||
|
// Check if image exists locally
|
||||||
|
const imageId = execSync(`docker images -q ${name}`, {
|
||||||
|
encoding: "utf-8"
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
// Image doesn't exist, pull it
|
||||||
|
process.stdout.write(
|
||||||
|
`${colors.cyan}Pulling ${label} image (${name})...${colors.reset}\n`
|
||||||
|
);
|
||||||
|
execSync(`docker pull ${name}`, { stdio: "inherit" });
|
||||||
|
process.stdout.write(
|
||||||
|
`${colors.green}✓${colors.reset} ${label} image ready\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(
|
||||||
|
`${colors.yellow}⚠${colors.reset} Failed to check/pull ${label} image. Continuing anyway...\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitWithProgress(ms: number): Promise<void> {
|
||||||
|
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 {
|
||||||
|
// Check and pull images if needed
|
||||||
|
checkAndPullImages();
|
||||||
|
|
||||||
|
// Start docker compose from package root
|
||||||
|
const projectName = getProjectName(packageRoot);
|
||||||
|
execSync(`docker compose --project-name ${projectName} up -d`, {
|
||||||
|
cwd: packageRoot,
|
||||||
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
|
encoding: "utf-8",
|
||||||
|
env: { ...process.env, COMPOSE_PROJECT_NAME: projectName }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for nodes to be ready
|
||||||
|
await waitWithProgress(STARTUP_WAIT_MS);
|
||||||
|
|
||||||
|
// Get cluster config from env or defaults
|
||||||
|
const clusterId: string = process.env.CLUSTER_ID || DEFAULT_CLUSTER_ID;
|
||||||
|
const node1Port: string = process.env.NODE1_WS_PORT || DEFAULT_NODE1_WS_PORT;
|
||||||
|
const node2Port: string = process.env.NODE2_WS_PORT || DEFAULT_NODE2_WS_PORT;
|
||||||
|
|
||||||
|
// Static peer IDs from --nodekey configuration
|
||||||
|
// cspell:ignore nodekey
|
||||||
|
const peer1: string = NODE1_PEER_ID;
|
||||||
|
const peer2: string = NODE2_PEER_ID;
|
||||||
|
|
||||||
|
// 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`
|
||||||
|
);
|
||||||
|
|
||||||
|
printWakuConfig(colors, node1Port, node2Port, peer1, peer2, clusterId);
|
||||||
|
process.stdout.write(`\n`);
|
||||||
|
process.stdout.write(`${colors.gray}Management:${colors.reset}\n`);
|
||||||
|
|
||||||
|
// Detect if running via npx (published package) or npm run (development)
|
||||||
|
const isPublished = __dirname.includes("dist");
|
||||||
|
const cmdPrefix = isPublished ? "npx @waku/run" : "npm run";
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
` ${colors.cyan}${cmdPrefix} test${colors.reset} - Test network with a message\n`
|
||||||
|
);
|
||||||
|
process.stdout.write(
|
||||||
|
` ${colors.cyan}${cmdPrefix} logs${colors.reset} - View logs\n`
|
||||||
|
);
|
||||||
|
process.stdout.write(
|
||||||
|
` ${colors.cyan}${cmdPrefix} info${colors.reset} - Show config again\n`
|
||||||
|
);
|
||||||
|
process.stdout.write(
|
||||||
|
` ${colors.cyan}${cmdPrefix} 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);
|
||||||
|
}
|
||||||
28
packages/run/scripts/stop.ts
Normal file
28
packages/run/scripts/stop.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
import { getProjectName } from "../src/utils.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const packageRoot = __dirname.includes("dist")
|
||||||
|
? join(__dirname, "..", "..")
|
||||||
|
: join(__dirname, "..");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectName = getProjectName(packageRoot);
|
||||||
|
execSync(`docker compose --project-name ${projectName} down`, {
|
||||||
|
cwd: packageRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: { ...process.env, COMPOSE_PROJECT_NAME: projectName }
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const err = error as { message?: string };
|
||||||
|
process.stderr.write(
|
||||||
|
`Error stopping network: ${err.message || String(error)}\n`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
122
packages/run/scripts/test.ts
Normal file
122
packages/run/scripts/test.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
import { Protocols } from "@waku/sdk";
|
||||||
|
|
||||||
|
import { WakuTestClient } from "../src/test-client.js";
|
||||||
|
import { getProjectName } from "../src/utils.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const packageRoot = __dirname.includes("dist")
|
||||||
|
? join(__dirname, "..", "..")
|
||||||
|
: join(__dirname, "..");
|
||||||
|
|
||||||
|
interface Colors {
|
||||||
|
reset: string;
|
||||||
|
cyan: string;
|
||||||
|
green: string;
|
||||||
|
red: string;
|
||||||
|
yellow: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANSI color codes
|
||||||
|
const colors: Colors = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
yellow: "\x1b[33m"
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
let client: WakuTestClient | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if containers are running
|
||||||
|
const projectName = getProjectName(packageRoot);
|
||||||
|
const output: string = execSync(
|
||||||
|
`docker compose --project-name ${projectName} ps --quiet`,
|
||||||
|
{
|
||||||
|
cwd: packageRoot,
|
||||||
|
encoding: "utf-8",
|
||||||
|
env: { ...process.env, COMPOSE_PROJECT_NAME: projectName }
|
||||||
|
}
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
if (!output) {
|
||||||
|
process.stderr.write(
|
||||||
|
`${colors.red}✗${colors.reset} No nodes running. Start with: ${colors.cyan}npx @waku/run start${colors.reset}\n`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
`${colors.cyan}Testing local Waku network...${colors.reset}\n\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 1: Create client
|
||||||
|
process.stdout.write(
|
||||||
|
`${colors.cyan}→${colors.reset} Creating Waku light node...\n`
|
||||||
|
);
|
||||||
|
client = new WakuTestClient();
|
||||||
|
|
||||||
|
// Step 2: Start and connect
|
||||||
|
process.stdout.write(`${colors.cyan}→${colors.reset} Starting node...\n`);
|
||||||
|
await client.start();
|
||||||
|
|
||||||
|
// Step 3: Wait for peers
|
||||||
|
process.stdout.write(
|
||||||
|
`${colors.cyan}→${colors.reset} Waiting for peers...\n`
|
||||||
|
);
|
||||||
|
await client.waku!.waitForPeers([Protocols.LightPush]);
|
||||||
|
const connectedPeers = client.waku!.libp2p.getPeers().length;
|
||||||
|
process.stdout.write(
|
||||||
|
`${colors.green}✓${colors.reset} Connected to ${connectedPeers} peer(s)\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 4: Send test message
|
||||||
|
process.stdout.write(
|
||||||
|
`${colors.cyan}→${colors.reset} Sending lightpush message...\n`
|
||||||
|
);
|
||||||
|
const result = await client.sendTestMessage("Test from @waku/run");
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
process.stdout.write(
|
||||||
|
`${colors.green}✓${colors.reset} Message sent successfully to ${result.messagesSent} peer(s)\n`
|
||||||
|
);
|
||||||
|
process.stdout.write(
|
||||||
|
`\n${colors.green}✓ All tests passed!${colors.reset}\n`
|
||||||
|
);
|
||||||
|
process.stdout.write(
|
||||||
|
`${colors.cyan}The local Waku network is working correctly.${colors.reset}\n`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
process.stderr.write(
|
||||||
|
`${colors.red}✗${colors.reset} Failed to send message: ${result.error || "Unknown error"}\n`
|
||||||
|
);
|
||||||
|
process.stderr.write(
|
||||||
|
` Sent: ${result.messagesSent}, Failed: ${result.failures}\n`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const err = error as { message?: string };
|
||||||
|
process.stderr.write(
|
||||||
|
`${colors.red}✗${colors.reset} Test failed: ${err.message || String(error)}\n`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
if (client) {
|
||||||
|
await client.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
process.stderr.write(`Unexpected error: ${String(error)}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
40
packages/run/src/cli.ts
Normal file
40
packages/run/src/cli.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/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<string, string> = {
|
||||||
|
start: join(__dirname, "..", "scripts", "start.js"),
|
||||||
|
stop: join(__dirname, "..", "scripts", "stop.js"),
|
||||||
|
info: join(__dirname, "..", "scripts", "info.js"),
|
||||||
|
logs: join(__dirname, "..", "scripts", "logs.js"),
|
||||||
|
test: join(__dirname, "..", "scripts", "test.js")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!command || !scriptMap[command]) {
|
||||||
|
process.stderr.write("Usage: @waku/run <command>\n");
|
||||||
|
process.stderr.write("\n");
|
||||||
|
process.stderr.write("Commands:\n");
|
||||||
|
process.stderr.write(" start Start the local Waku network\n");
|
||||||
|
process.stderr.write(" stop Stop the local Waku network\n");
|
||||||
|
process.stderr.write(" info Show connection info for running network\n");
|
||||||
|
process.stderr.write(" logs View logs from running network\n");
|
||||||
|
process.stderr.write(" test Test the network by sending a message\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);
|
||||||
|
});
|
||||||
40
packages/run/src/constants.ts
Normal file
40
packages/run/src/constants.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Static configuration constants for the local Waku development environment.
|
||||||
|
* These values are derived from the --nodekey configuration in docker-compose.yml
|
||||||
|
* cspell:ignore nodekey
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Node private keys (from docker-compose.yml --nodekey)
|
||||||
|
export const NODE1_PRIVATE_KEY =
|
||||||
|
"e419c3cf4f09ac3babdf61856e6faa0e0c6a7d97674d5401a0114616549c7632";
|
||||||
|
export const NODE2_PRIVATE_KEY =
|
||||||
|
"50632ab0efd313bfb4aa842de716f03dacd181c863770abd145e3409290fdaa7";
|
||||||
|
|
||||||
|
// Derived peer IDs (libp2p identities from the private keys)
|
||||||
|
export const NODE1_PEER_ID =
|
||||||
|
"16Uiu2HAmF6oAsd23RMAnZb3NJgxXrExxBTPMdEoih232iAZkviU2";
|
||||||
|
export const NODE2_PEER_ID =
|
||||||
|
"16Uiu2HAm5aZU47YkiUoARqivbCXwuFPzFFXXiURAorySqAQbL6EQ";
|
||||||
|
|
||||||
|
// Static IP addresses (from docker-compose.yml network configuration)
|
||||||
|
export const NODE1_IP = "172.20.0.10";
|
||||||
|
export const NODE2_IP = "172.20.0.11";
|
||||||
|
|
||||||
|
// Default WebSocket ports for local nodes
|
||||||
|
export const DEFAULT_NODE1_WS_PORT = "60000";
|
||||||
|
export const DEFAULT_NODE2_WS_PORT = "60001";
|
||||||
|
|
||||||
|
// Default REST API ports for local nodes
|
||||||
|
export const DEFAULT_NODE1_REST_PORT = "8646";
|
||||||
|
export const DEFAULT_NODE2_REST_PORT = "8647";
|
||||||
|
|
||||||
|
// Docker images
|
||||||
|
export const DEFAULT_NWAKU_IMAGE = "wakuorg/nwaku:v0.36.0";
|
||||||
|
export const POSTGRES_IMAGE = "postgres:15.4-alpine3.18";
|
||||||
|
|
||||||
|
// Timing configuration
|
||||||
|
export const STARTUP_WAIT_MS = 20000; // Time to wait for nodes to start
|
||||||
|
|
||||||
|
// Network configuration
|
||||||
|
export const DEFAULT_CLUSTER_ID = "0";
|
||||||
|
export const DEFAULT_NUM_SHARDS_IN_CLUSTER = 8;
|
||||||
30
packages/run/src/run-tests.js
Normal file
30
packages/run/src/run-tests.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
126
packages/run/src/test-client.ts
Normal file
126
packages/run/src/test-client.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { createEncoder } from "@waku/core";
|
||||||
|
import type { LightNode } from "@waku/interfaces";
|
||||||
|
import { createLightNode } from "@waku/sdk";
|
||||||
|
import { createRoutingInfo } from "@waku/utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_CLUSTER_ID,
|
||||||
|
DEFAULT_NODE1_WS_PORT,
|
||||||
|
DEFAULT_NODE2_WS_PORT,
|
||||||
|
DEFAULT_NUM_SHARDS_IN_CLUSTER,
|
||||||
|
NODE1_PEER_ID,
|
||||||
|
NODE2_PEER_ID
|
||||||
|
} from "./constants.js";
|
||||||
|
|
||||||
|
export interface WakuTestClientOptions {
|
||||||
|
node1Port?: string;
|
||||||
|
node2Port?: string;
|
||||||
|
clusterId?: number;
|
||||||
|
numShardsInCluster?: number;
|
||||||
|
contentTopic?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestResult {
|
||||||
|
success: boolean;
|
||||||
|
connectedPeers: number;
|
||||||
|
messagesSent: number;
|
||||||
|
failures: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WakuTestClient {
|
||||||
|
public waku: LightNode | null = null;
|
||||||
|
private options: Required<WakuTestClientOptions>;
|
||||||
|
|
||||||
|
public constructor(options: WakuTestClientOptions = {}) {
|
||||||
|
this.options = {
|
||||||
|
node1Port:
|
||||||
|
options.node1Port || process.env.NODE1_WS_PORT || DEFAULT_NODE1_WS_PORT,
|
||||||
|
node2Port:
|
||||||
|
options.node2Port || process.env.NODE2_WS_PORT || DEFAULT_NODE2_WS_PORT,
|
||||||
|
clusterId: options.clusterId ?? parseInt(DEFAULT_CLUSTER_ID),
|
||||||
|
numShardsInCluster:
|
||||||
|
options.numShardsInCluster ?? DEFAULT_NUM_SHARDS_IN_CLUSTER,
|
||||||
|
contentTopic: options.contentTopic || "/waku-run/1/test/proto"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and start the Waku light node
|
||||||
|
*/
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
const { node1Port, node2Port, clusterId, numShardsInCluster } =
|
||||||
|
this.options;
|
||||||
|
|
||||||
|
const networkConfig = {
|
||||||
|
clusterId,
|
||||||
|
numShardsInCluster
|
||||||
|
};
|
||||||
|
|
||||||
|
this.waku = await createLightNode({
|
||||||
|
defaultBootstrap: false,
|
||||||
|
bootstrapPeers: [
|
||||||
|
`/ip4/127.0.0.1/tcp/${node1Port}/ws/p2p/${NODE1_PEER_ID}`,
|
||||||
|
`/ip4/127.0.0.1/tcp/${node2Port}/ws/p2p/${NODE2_PEER_ID}`
|
||||||
|
],
|
||||||
|
networkConfig,
|
||||||
|
numPeersToUse: 2,
|
||||||
|
libp2p: {
|
||||||
|
filterMultiaddrs: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.waku.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a test message via lightpush
|
||||||
|
*/
|
||||||
|
public async sendTestMessage(
|
||||||
|
payload: string = "Hello Waku!"
|
||||||
|
): Promise<TestResult> {
|
||||||
|
if (!this.waku) {
|
||||||
|
throw new Error("Waku node not started. Call start() first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { contentTopic, clusterId, numShardsInCluster } = this.options;
|
||||||
|
const networkConfig = { clusterId, numShardsInCluster };
|
||||||
|
|
||||||
|
const routingInfo = createRoutingInfo(networkConfig, { contentTopic });
|
||||||
|
const encoder = createEncoder({ contentTopic, routingInfo });
|
||||||
|
|
||||||
|
const result = await this.waku.lightPush.send(encoder, {
|
||||||
|
payload: new TextEncoder().encode(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectedPeers = this.waku.libp2p.getPeers().length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success:
|
||||||
|
result.successes.length > 0 && (result.failures?.length || 0) === 0,
|
||||||
|
connectedPeers,
|
||||||
|
messagesSent: result.successes.length,
|
||||||
|
failures: result.failures?.length || 0
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
connectedPeers: this.waku.libp2p.getPeers().length,
|
||||||
|
messagesSent: 0,
|
||||||
|
failures: 0,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the Waku node
|
||||||
|
*/
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
if (this.waku) {
|
||||||
|
await this.waku.stop();
|
||||||
|
this.waku = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
packages/run/src/utils.ts
Normal file
63
packages/run/src/utils.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
import { DEFAULT_NUM_SHARDS_IN_CLUSTER } from "./constants.js";
|
||||||
|
|
||||||
|
export function getProjectName(packageRoot: string): string {
|
||||||
|
const packageJsonPath = join(packageRoot, "package.json");
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||||
|
// Docker Compose project names must consist only of lowercase alphanumeric characters, hyphens, and underscores
|
||||||
|
const name = packageJson.name.replace("@", "").replace("/", "-");
|
||||||
|
const version = packageJson.version.replace(/\./g, "-");
|
||||||
|
return `${name}-${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Colors {
|
||||||
|
reset: string;
|
||||||
|
cyan: string;
|
||||||
|
blue: string;
|
||||||
|
yellow: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printWakuConfig(
|
||||||
|
colors: Colors,
|
||||||
|
node1Port: string,
|
||||||
|
node2Port: string,
|
||||||
|
peer1: string,
|
||||||
|
peer2: string,
|
||||||
|
clusterId: string
|
||||||
|
): void {
|
||||||
|
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(` numPeersToUse: ${colors.cyan}2${colors.reset},\n`);
|
||||||
|
process.stdout.write(` libp2p: {\n`);
|
||||||
|
process.stdout.write(
|
||||||
|
` filterMultiaddrs: ${colors.cyan}false${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}${DEFAULT_NUM_SHARDS_IN_CLUSTER}${colors.reset}\n`
|
||||||
|
);
|
||||||
|
process.stdout.write(` }\n`);
|
||||||
|
process.stdout.write(`});\n`);
|
||||||
|
}
|
||||||
120
packages/run/tests/basic.spec.ts
Normal file
120
packages/run/tests/basic.spec.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
import { Protocols } from "@waku/sdk";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_NODE1_REST_PORT,
|
||||||
|
DEFAULT_NODE2_REST_PORT
|
||||||
|
} from "../src/constants.js";
|
||||||
|
import { WakuTestClient } from "../src/test-client.js";
|
||||||
|
import { getProjectName } from "../src/utils.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const packageRoot = join(__dirname, "..");
|
||||||
|
|
||||||
|
describe("Waku Run - Basic Test", function () {
|
||||||
|
this.timeout(90000);
|
||||||
|
|
||||||
|
let client: WakuTestClient;
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
// Step 1: Start the nodes
|
||||||
|
const projectName = getProjectName(packageRoot);
|
||||||
|
execSync(`docker compose --project-name ${projectName} up -d`, {
|
||||||
|
cwd: packageRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: { ...process.env, COMPOSE_PROJECT_NAME: projectName }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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:${DEFAULT_NODE1_REST_PORT}/debug/v1/info`
|
||||||
|
);
|
||||||
|
await fetch(
|
||||||
|
`http://127.0.0.1:${DEFAULT_NODE2_REST_PORT}/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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes automatically connect via --staticnode configuration
|
||||||
|
// cspell:ignore staticnode
|
||||||
|
// Wait for nwaku nodes to connect to each other
|
||||||
|
let connected = false;
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
try {
|
||||||
|
const peers = await fetch(
|
||||||
|
`http://127.0.0.1:${DEFAULT_NODE1_REST_PORT}/admin/v1/peers`
|
||||||
|
).then((r) => r.json());
|
||||||
|
if (peers.length > 0 && peers[0].connected === "Connected") {
|
||||||
|
connected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
throw new Error("Nwaku nodes failed to connect to each other");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
// Step 4: Stop the nodes
|
||||||
|
if (client) {
|
||||||
|
await client.stop();
|
||||||
|
}
|
||||||
|
const projectName = getProjectName(packageRoot);
|
||||||
|
execSync(`docker compose --project-name ${projectName} down`, {
|
||||||
|
cwd: packageRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: { ...process.env, COMPOSE_PROJECT_NAME: projectName }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should connect to both nodes and send lightpush message to both peers", async function () {
|
||||||
|
// Step 2: Connect to nodes via js-waku using WakuTestClient
|
||||||
|
client = new WakuTestClient({
|
||||||
|
contentTopic: "/test/1/basic/proto"
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.start();
|
||||||
|
|
||||||
|
// Wait for both peers to be connected
|
||||||
|
await client.waku!.waitForPeers([Protocols.LightPush]);
|
||||||
|
const connectedPeers = client.waku!.libp2p.getPeers().length;
|
||||||
|
expect(connectedPeers).to.equal(
|
||||||
|
2,
|
||||||
|
"Should be connected to both nwaku nodes"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 3: Send lightpush message - it should be sent to both peers
|
||||||
|
const result = await client.sendTestMessage("Hello Waku!");
|
||||||
|
|
||||||
|
expect(result.success).to.be.true;
|
||||||
|
expect(result.messagesSent).to.equal(
|
||||||
|
2,
|
||||||
|
"Message should be sent to both peers"
|
||||||
|
);
|
||||||
|
expect(result.failures).to.equal(0, "Should have no failures");
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/run/tsconfig.dev.json
Normal file
7
packages/run/tsconfig.dev.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.dev",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": ["src", "scripts", "tests"]
|
||||||
|
}
|
||||||
10
packages/run/tsconfig.json
Normal file
10
packages/run/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist/",
|
||||||
|
"rootDir": ".",
|
||||||
|
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src", "scripts"],
|
||||||
|
"exclude": ["tests", "dist", "node_modules"]
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@
|
|||||||
"packages/discovery": {},
|
"packages/discovery": {},
|
||||||
"packages/sds": {},
|
"packages/sds": {},
|
||||||
"packages/rln": {},
|
"packages/rln": {},
|
||||||
"packages/react": {}
|
"packages/react": {},
|
||||||
|
"packages/run": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user