Compare commits

..

No commits in common. "master" and "v0.2.0" have entirely different histories.

649 changed files with 86147 additions and 98447 deletions

View File

@ -1,165 +1,73 @@
{ {
"version": "0.2", "version": "0.1",
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json", "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
"language": "en", "language": "en",
"words": [ "words": [
"abortable",
"acks",
"Addrs",
"ahadns",
"Alives",
"alphabeta",
"arrayify",
"Arraylike",
"asym",
"autoshard",
"autosharding",
"backoff", "backoff",
"backoffs", "backoffs",
"bitauth",
"bitjson", "bitjson",
"bitauth",
"bufbuild", "bufbuild",
"chainsafe",
"cimg", "cimg",
"cipherparams",
"ciphertext",
"circleci", "circleci",
"circom",
"codecov", "codecov",
"codegen",
"commitlint", "commitlint",
"cooldown",
"dependabot", "dependabot",
"dialable",
"dingpu", "dingpu",
"discv",
"Dlazy", "Dlazy",
"dnsaddr",
"Dockerode",
"Dout", "Dout",
"Dscore", "Dscore",
"ecies",
"editorconfig", "editorconfig",
"Encrypters",
"enr",
"enrs",
"enrtree",
"ephem",
"esnext", "esnext",
"ethersproject",
"execa", "execa",
"exponentiate", "exponentiate",
"extip",
"fanout", "fanout",
"floodsub", "floodsub",
"fontsource",
"globby", "globby",
"gossipsub", "gossipsub",
"hackathons",
"huilong",
"iasked", "iasked",
"ihave", "ihave",
"ihaves", "ihaves",
"ineed", "ineed",
"IPAM",
"ipfs",
"isready",
"iwant", "iwant",
"jdev", "jdev",
"jswaku",
"kdfparams",
"keccak",
"keypair",
"lamport",
"lastpub", "lastpub",
"libauth", "libauth",
"libp", "libp",
"lightpush",
"LINEA",
"livechat", "livechat",
"Merkle",
"mkdir", "mkdir",
"mplex",
"multiaddr", "multiaddr",
"multiaddresses",
"multiaddrs", "multiaddrs",
"multicodec",
"multicodecs", "multicodecs",
"multiformats", "mplex",
"multihashes",
"muxed", "muxed",
"muxer", "muxer",
"muxers",
"mvps", "mvps",
"nodekey", "nodekey",
"nwaku",
"opendns",
"peerhave", "peerhave",
"portfinder",
"prettierignore", "prettierignore",
"proto", "proto",
"protobuf", "protobuf",
"protoc", "protoc",
"proxiable",
"reactjs", "reactjs",
"recid",
"rlnrelay", "rlnrelay",
"rlnv",
"roadmap",
"sandboxed", "sandboxed",
"scanf",
"secio", "secio",
"seckey",
"secp",
"sharded",
"sscanf",
"Startable",
"staticnode", "staticnode",
"statusim", "statusim",
"statusteam",
"submodule", "submodule",
"submodules", "submodules",
"supercrypto",
"transpiled", "transpiled",
"typedoc", "typedoc",
"undialable",
"unencrypted",
"unhandle",
"unmarshal",
"unmount",
"unmounts", "unmounts",
"unsubscription",
"untracked", "untracked",
"upgrader", "upgrader",
"vacp",
"varint",
"viem",
"vkey",
"wagmi",
"waku", "waku",
"wakuconnect",
"wakunode", "wakunode",
"wakuorg",
"wakuv",
"webfonts", "webfonts",
"weboko", "websockets"
"websockets",
"wifi",
"WTNS",
"xsalsa20",
"zerokit",
"Привет",
"مرحبا"
],
"flagWords": [
"pubSub: pubsub",
"pubSubTopics: pubsubTopics",
"pubSubTopic: pubsubTopic",
"PubSub: Pubsub",
"PubSubTopics: PubsubTopics",
"PubSubTopic: PubsubTopic",
"DefaultPubSubTopic: DefaultPubsubTopic"
], ],
"flagWords": [],
"ignorePaths": [ "ignorePaths": [
"package.json", "package.json",
"package-lock.json", "package-lock.json",
@ -168,23 +76,6 @@
"node_modules/**", "node_modules/**",
"build", "build",
"gen", "gen",
"proto", "proto"
"*.spec.ts",
"*.log",
"CHANGELOG.md"
],
"patterns": [
{
"name": "import",
"pattern": "/import .*/"
},
{
"name": "multiaddr",
"pattern": "//dns4/.*/"
}
],
"ignoreRegExpList": [
"import",
"multiaddr"
] ]
} }

View File

@ -1,24 +0,0 @@
**/node_modules
**/.git
**/.vscode
**/dist
**/build
**/.DS_Store
**/.env*
**/*.log
# Exclude all packages except browser-tests and browser-container
packages/discovery/
packages/tests/
packages/utils/
packages/sds/
packages/sdk/
packages/relay/
packages/rln/
packages/message-hash/
packages/proto/
packages/enr/
packages/interfaces/
packages/message-encryption/
packages/core/
packages/build-utils/

View File

@ -1,130 +1,42 @@
{ {
"root": true, "root": true,
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": { "project": "./tsconfig.dev.json" },
"project": ["./tsconfig.json"]
},
"env": { "es6": true }, "env": { "es6": true },
"ignorePatterns": [ "ignorePatterns": ["node_modules", "build", "coverage", "proto"],
"node_modules",
"build",
"coverage",
"proto"
],
"plugins": ["import", "eslint-comments", "functional"], "plugins": ["import", "eslint-comments", "functional"],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:eslint-comments/recommended", "plugin:eslint-comments/recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:import/typescript", "plugin:import/typescript",
"plugin:prettier/recommended" "prettier",
"prettier/@typescript-eslint"
], ],
"globals": { "BigInt": true, "console": true, "WebAssembly": true }, "globals": { "BigInt": true, "console": true, "WebAssembly": true },
"rules": { "rules": {
"no-restricted-imports": [ "@typescript-eslint/explicit-function-return-type": ["error"],
"error",
{
"paths": [{
"name": "debug",
"message": "The usage of 'debug' package directly is disallowed. Please use the custom logger from @waku/utils instead."
}]
}
],
"@typescript-eslint/explicit-member-accessibility": "error",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"prettier/prettier": [
"error",
{
"trailingComma": "none"
}
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"eslint-comments/disable-enable-pair": [ "eslint-comments/disable-enable-pair": [
"error", "error",
{ { "allowWholeFile": true }
"allowWholeFile": true
}
], ],
"eslint-comments/no-unused-disable": "error", "eslint-comments/no-unused-disable": "error",
"import/order": [ "import/order": [
"error", "error",
{ { "newlines-between": "always", "alphabetize": { "order": "asc" } }
"newlines-between": "always",
"alphabetize": {
"order": "asc"
}
}
], ],
"no-constant-condition": ["error", { "checkLoops": false }], "no-constant-condition": ["error", { "checkLoops": false }],
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*.test.ts",
"**/*.spec.ts",
"**/tests/**",
"**/rollup.config.js",
"**/playwright.config.ts",
"**/.eslintrc.cjs",
"**/karma.conf.cjs"
]
}
],
"sort-imports": [ "sort-imports": [
"error", "error",
{ "ignoreDeclarationSort": true, "ignoreCase": true } { "ignoreDeclarationSort": true, "ignoreCase": true }
],
"no-console": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-explicit-any": "warn",
"id-match": ["error", "^(?!.*[pP]ubSub)"],
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "always",
"tsx": "never"
}
] ]
}, },
"overrides": [ "overrides": [
{ {
"files": ["*.spec.ts", "**/test_utils/*.ts", "*.js", "*.cjs"], "files": ["*.spec.ts", "**/test_utils/*.ts"],
"rules": { "rules": {
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-explicit-any": "off",
"no-console": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true
}
]
}
},
{
"files": ["*.ts", "*.mts", "*.cts", "*.tsx"],
"rules": {
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": true
}
]
}
},
{
"files": ["**/ci/*.js"],
"rules": {
"no-undef": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-floating-promises": "off",
"import/no-extraneous-dependencies": "off"
} }
} }
] ]

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @waku-org/js-waku

3
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,3 @@
# Example Contributing Guidelines
This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information.

9
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,9 @@
- **I'm submitting a ...**
[ ] bug report
[ ] feature request
[ ] question about the decisions made in the repository
[ ] question about how to use this project
- **Summary**
- **Other information** (e.g. detailed explanation, stack traces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)

View File

@ -1,53 +0,0 @@
---
name: "\U0001F680 Feature request"
about: Suggest an idea for this project
title: 'feat:'
labels: ''
assignees: ''
---
<!--
Delete not needed sections below.
-->
### Description
<!--
What problem are you facing, or what improvement are you suggesting?
- Clearly describe the problem or need.
- Explain why this feature would be useful.
-->
### User Story
<!--
Describe the feature from the perspective of the user. Use the following format:
- As a [type of user], I want to [do something], so that [reason/benefit].
Examples:
- As a developer, I want to see detailed error logs, so that I can debug issues more effectively.
- As a user, I want to be able to subscribe to a content topic without manually specifying a peer.
-->
### Proposed Solution / Feature Design
<!--
Describe how the feature should work.
- Provide a high-level summary of the solution.
- Include details on the behavior, user interaction, and functionality.
-->
#### Optional: Diagram or Draft of Design
<!--
If applicable, include visual aids or drafts to clarify your proposal.
- Attach diagrams or design drafts to illustrate your idea.
Use Mermaid syntax - https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams
-->
### Notes
<!--
Anything relevant:
- issues or previous features;
- discussion threads;
- docs;
- features in other projects;
-->

View File

@ -1,58 +0,0 @@
---
name: "\U0001FAB3 Bug report"
about: Create a report about a problem, observation or feedback.
title: 'bug: '
labels: ''
assignees: ''
---
<!--
Delete not needed sections below.
-->
### Description
<!--
Provide a clear and concise description of the bug.
- What is happening?
- What did you expect to happen?
-->
### Expected Behavior
<!--
Describe what you expected to happen.
-->
### Steps to Reproduce
<!--
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
-->
### Environment Details
<!--
Include details about your environment, such as:
- Version of js-waku packages
- Browser/Node.js version
- Operating System
- Any other context that might be relevant
-->
<details>
<summary>Logs</summary>
<!--
Include any relevant logs or error messages here.
Ensure sensitive information is redacted.
Add following flag: `debug=waku:*`
- In browser to `localStorage`.
- In NodeJS as ENV.
-->
[Paste logs here]
</details>

View File

@ -1,30 +1,7 @@
### Problem / Description - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...)
<!--
What problem does this PR address?
Clearly describe the issue or feature the PR aims to solve.
-->
### Solution - **What is the current behavior?** (You can also link to an open issue here)
<!--
Describe how the problem is solved in this PR.
- Provide an overview of the changes made.
- Highlight any significant design decisions or architectural changes.
-->
### Notes - **What is the new behavior (if this is a feature change)?**
<!--
Additional context, considerations, or information relevant to this PR.
- Are there known limitations or trade-offs in the solution?
- Include links to related discussions, documents, or references.
-->
- Resolves
- Related to
--- - **Other information**:
#### Checklist
- [ ] Code changes are **covered by unit tests**.
- [ ] Code changes are **covered by e2e tests**, if applicable.
- [ ] **Dogfooding has been performed**, if feasible.
- [ ] A **test version has been published**, if required.
- [ ] All **CI checks** pass successfully.

View File

@ -1,10 +0,0 @@
name: npm i
runs:
using: "composite"
steps:
- if: ${{ github.ref == 'refs/heads/master' || github.head_ref == 'release-please--branches--master' }}
run: npm i
shell: bash
- if: ${{ github.ref != 'refs/heads/master' && github.head_ref != 'release-please--branches--master' }}
uses: bahmutov/npm-install@v1

View File

@ -1,27 +0,0 @@
# Inspired by https://github.com/AdityaGarg8/remove-unwanted-software
# to free up disk space. Currently removes Dotnet, Android and Haskell.
name: Remove unwanted software
description: Default GitHub runners come with a lot of unnecessary software
runs:
using: "composite"
steps:
- name: Disk space report before modification
shell: bash
run: |
echo "==> Available space before cleanup"
echo
df -h
- name: Maximize build disk space
shell: bash
run: |
set -euo pipefail
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/.ghcup
- name: Disk space report after modification
shell: bash
run: |
echo "==> Available space after cleanup"
echo
df -h

View File

@ -1,11 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
open-pull-requests-limit: 2
schedule:
interval: "daily"
versioning-strategy: increase
commit-message:
prefix: "chore(deps)"
include: "scope"

View File

@ -1,15 +1,15 @@
name: Add new issues to Waku project board name: Add new issues to js-waku project board
on: on:
issues: issues:
types: [opened] types: [opened]
jobs: jobs:
add-to-project: add-new-issue-to-new-column:
name: Add issue to project
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/add-to-project@v0.5.0 - uses: alex-page/github-project-automation-plus@v0.6.0
with: with:
project-url: https://github.com/orgs/waku-org/projects/2 project: js-waku
github-token: ${{ secrets.ADD_TO_PROJECT_20240815 }} column: New
repo-token: ${{ secrets.GH_ACTION_PROJECT_MGMT }}

View File

@ -3,155 +3,107 @@ name: CI
on: on:
push: push:
branches: branches:
- "master" - 'main'
- "staging" - 'staging'
- "trying" - 'trying'
pull_request: pull_request:
workflow_dispatch:
inputs:
nim_wakunode_image:
description: "Docker hub image name taken from https://hub.docker.com/r/wakuorg/nwaku/tags. Format: wakuorg/nwaku:v0.20.0"
required: false
type: string
env:
NODE_JS: "24"
jobs: jobs:
check: build_and_test:
runs-on: ubuntu-latest env:
BUF_VERSION: '0.41.0'
strategy:
matrix:
node: [14]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - name: Checkout code
uses: actions/checkout@v2
with: with:
repository: waku-org/js-waku submodules: 'recursive'
- uses: actions/setup-node@v3 - name: Get nim-waku HEAD
with: id: nim-waku-head
node-version: ${{ env.NODE_JS }} shell: bash
- uses: ./.github/actions/npm run: cd nim-waku && echo "::set-output name=ref::$(git rev-parse HEAD)"
- run: npm run build
- run: npm run check
- run: npm run doc
proto: - name: Cache nim-waku binary
runs-on: ubuntu-latest id: cache-nim-waku
steps: uses: actions/cache@v2
- uses: actions/checkout@v3
with: with:
repository: waku-org/js-waku path: |
- uses: actions/setup-node@v3 ./nim-waku/build/wakunode2
./nim-waku/vendor/rln/target/debug
key: nim-waku-build-${{ matrix.os }}-v3-${{ steps.nim-waku-head.outputs.ref }}
- name: Install NodeJS
uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_JS }} node-version: ${{ matrix.node }}
- uses: ./.github/actions/npm
# This would have been done part of npm pretest but it gives better
# visibility in the CI if done as a separate step
- name: Build wakunode2
shell: bash
run: (cd nim-waku && ./build/wakunode2 --help) || npm run nim-waku:build
- name: Ensure wakunode2 is ready
shell: bash
run: cd nim-waku && ./build/wakunode2 --help
- name: Cache buf binary
id: cache-buf-bin
uses: actions/cache@v2
with:
path: /opt/hostedtoolcache/buf/
key: buf-bin-v${{ env.BUF_VERSION }}-ubuntu-latest-v1
- name: Install bufbuild
uses: mu-io/setup-buf@v2beta
with:
buf-version: ${{ env.BUF_VERSION }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
version: '3.x'
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Cache npm cache
uses: actions/cache@v2
with:
path: ~/.npm
key: node-${{ matrix.os }}-${{ matrix.node }}-v1-${{ hashFiles('**/package-lock.json') }}
- name: install using npm ci
uses: bahmutov/npm-install@v1
- name: Generate protobuf code - name: Generate protobuf code
run: | run: npm run proto
npm run proto
npm run fix
- name: Check all protobuf code was committed - name: Check all protobuf code was committed
shell: bash shell: bash
run: | run: |
res=$(git status --short --ignore-submodules) [ $(git status --short --ignore-submodules|wc -l) -eq 0 ]
echo -n "'$res'" # For debug purposes
[ $(echo -n "$res"|wc -l) -eq 0 ]
browser: - name: build
runs-on: ubuntu-latest run: npm run build
container:
image: mcr.microsoft.com/playwright:v1.56.1-jammy
env:
HOME: "/root"
steps:
- uses: actions/checkout@v3
with:
repository: waku-org/js-waku
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_JS }}
- uses: ./.github/actions/npm
- run: npm run build:esm
- run: npm run test:browser
node: - name: Check no proto files changed
uses: ./.github/workflows/test-node.yml shell: bash
secrets: inherit
with:
nim_wakunode_image: ${{ inputs.nim_wakunode_image || 'wakuorg/nwaku:v0.36.0' }}
test_type: node
allure_reports: true
node_optional:
uses: ./.github/workflows/test-node.yml
with:
nim_wakunode_image: ${{ inputs.nim_wakunode_image || 'wakuorg/nwaku:v0.36.0' }}
test_type: node-optional
node_with_nwaku_master:
uses: ./.github/workflows/test-node.yml
with:
nim_wakunode_image: harbor.status.im/wakuorg/nwaku:latest
test_type: nwaku-master
maybe-release:
name: release
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
needs: [check, proto, browser, node]
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.CI_TOKEN }}
- uses: actions/checkout@v3
with:
repository: waku-org/js-waku
if: ${{ steps.release.outputs.releases_created }}
- uses: actions/setup-node@v3
if: ${{ steps.release.outputs.releases_created }}
with:
node-version: ${{ env.NODE_JS }}
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v4
if: ${{ steps.release.outputs.releases_created }}
with:
version: 9
- run: npm install
if: ${{ steps.release.outputs.releases_created }}
- run: npm run build
if: ${{ steps.release.outputs.releases_created }}
- name: Setup Foundry
if: ${{ steps.release.outputs.releases_created }}
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Generate RLN contract ABIs
id: rln-abi
if: ${{ steps.release.outputs.releases_created }}
run: | run: |
npm run setup:contract-abi -w @waku/rln || { [ $(git status --short --ignore-submodules|wc -l) -eq 0 ]
echo "::warning::Failed to generate contract ABIs, marking @waku/rln as private to skip publishing"
cd packages/rln
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.private = true; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));"
echo "failed=true" >> $GITHUB_OUTPUT
}
- name: Rebuild with new ABIs - name: test
if: ${{ steps.release.outputs.releases_created && steps.rln-abi.outputs.failed != 'true' }}
run: |
npm install -w packages/rln
npm run build -w @waku/rln || {
echo "::warning::Failed to build @waku/rln, marking as private to skip publishing"
cd packages/rln
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.private = true; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));"
}
- run: npm run publish
if: ${{ steps.release.outputs.releases_created }}
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_JS_WAKU_PUBLISH }} DEBUG: "waku:nim-waku*,waku:test*"
run: npm run test
- name: Upload logs on failure
uses: actions/upload-artifact@v2
if: failure()
with:
name: nim-waku-logs
path: log/

View File

@ -1,29 +0,0 @@
name: "Conventional PR"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
jobs:
main:
name: Validate Pull Request Title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# fix: bug fix on prod code
# feat: new feature on prod code
# test: only modify test or test utils
# doc: only modify docs/comments
# chore: anything else
types: |
fix
feat
test
doc
chore

61
.github/workflows/deploy-gh-pages.yml vendored Normal file
View File

@ -0,0 +1,61 @@
name: Webchat Deploy GH Pages
on:
push:
branches:
- 'main'
jobs:
deploy_gh_pages:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set git author identity
run: |
git config user.name "GitHub Action On js-waku Repo"
git config user.email "franck+ghpages@status.im"
- name: Install NodeJS
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Cache npm cache
uses: actions/cache@v2
with:
path: ~/.npm
key: node-v1-${{ hashFiles('**/package-lock.json') }}
- name: "[js-waku] install using npm ci"
uses: bahmutov/npm-install@v1
- name: "[js-waku] build"
run: npm run build
- name: install using npm i
run: npm install
working-directory: examples/web-chat
- name: build web app
run: npm run build
working-directory: examples/web-chat
- name: Deploy web chat app on gh pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./examples/web-chat/build
- name: Generate docs
run: npm run doc:html
- name: Deploy documentation on gh pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
keep_files: true # Do not delete web chat app
publish_dir: ./build/docs
destination_dir: docs

45
.github/workflows/examples-ci.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Examples CI
on:
push:
branches:
- 'main'
- 'staging'
- 'trying'
pull_request:
jobs:
examples_build_and_test:
strategy:
matrix:
example: [ cli-chat, web-chat ]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install NodeJS
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Cache npm cache
uses: actions/cache@v2
with:
path: ~/.npm
key: examples-node-v1-${{ hashFiles('**/package-lock.json') }}
- name: "[js-waku] install using npm ci"
uses: bahmutov/npm-install@v1
- name: "[js-waku] build"
run: npm run build
- name: ${{ matrix.example }} install using npm i
run: npm install
working-directory: examples/${{ matrix.example }}
- name: ${{ matrix.example }} test
run: npm run test
working-directory: examples/${{ matrix.example }}

View File

@ -1,26 +0,0 @@
on:
workflow_dispatch:
env:
NODE_JS: "22"
jobs:
pre-release:
name: fleet-checker
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v3
with:
repository: waku-org/js-waku
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_JS }}
registry-url: "https://registry.npmjs.org"
- run: npm install
- run: npm run build
- run: node --unhandled-rejections=none ./ci/wss-checker.js

View File

@ -1,40 +0,0 @@
name: Playwright tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
NODE_JS: "22"
# Firefox in container fails due to $HOME not being owned by user running commands
# more details https://github.com/microsoft/playwright/issues/6500
HOME: "/root"
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.56.1-jammy
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_JS }}
- uses: ./.github/actions/npm
- name: Build entire monorepo
run: npm run build
- name: Run Playwright tests
run: npm run test --workspace=@waku/browser-tests
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

View File

@ -1,62 +0,0 @@
on:
workflow_dispatch:
env:
NODE_JS: "24"
permissions:
id-token: write
contents: read
jobs:
pre-release:
name: pre-release
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
with:
repository: waku-org/js-waku
ref: ${{ github.ref }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_JS }}
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v4
with:
version: 9
- run: npm install
- run: npm run build
- name: Setup Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Generate RLN contract ABIs
id: rln-abi
run: |
npm run setup:contract-abi -w @waku/rln || {
echo "::warning::Failed to generate contract ABIs, marking @waku/rln as private to skip publishing"
cd packages/rln
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.private = true; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));"
echo "failed=true" >> $GITHUB_OUTPUT
}
- name: Rebuild with new ABIs
if: steps.rln-abi.outputs.failed != 'true'
run: |
npm install -w packages/rln
npm run build -w @waku/rln || {
echo "::warning::Failed to build @waku/rln, marking as private to skip publishing"
cd packages/rln
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.private = true; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));"
}
- run: npm run publish -- --tag next
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_JS_WAKU_PUBLISH }}

View File

@ -1,14 +0,0 @@
name: "size"
on:
pull_request:
jobs:
size:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v1
- uses: fryorcraken/size-limit-action@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,128 +0,0 @@
# WARNING: This workflow is used by upstream workflows (jswaku, nwaku, gowaku) via workflow_call.
# DO NOT modify the name, inputs, or other parts of this workflow that might break upstream CI.
name: Run Test
on:
workflow_call:
# IMPORTANT: Do not change the name or properties of these inputs.
# If you add new required inputs make sure that they have default value or you make the change upstream as well
inputs:
nim_wakunode_image:
required: true
type: string
test_type:
required: true
type: string
debug:
required: false
type: string
default: ''
allure_reports:
required: false
type: boolean
default: false
env:
NODE_JS: "24"
# Ensure test type conditions remain consistent.
WAKU_SERVICE_NODE_PARAMS: ${{ (inputs.test_type == 'go-waku-master') && '--min-relay-peers-to-publish=0' || '' }}
DEBUG: ${{ inputs.debug }}
GITHUB_TOKEN: ${{ secrets.DEPLOY_TEST_REPORTS_PAT }}
jobs:
node:
runs-on: ubuntu-latest
env:
WAKUNODE_IMAGE: ${{ inputs.nim_wakunode_image }}
ALLURE_REPORTS: ${{ inputs.allure_reports }}
permissions:
contents: read
actions: read
checks: write
steps:
- uses: actions/checkout@v3
with:
repository: waku-org/js-waku
- name: Remove unwanted software
uses: ./.github/actions/prune-vm
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_JS }}
- uses: ./.github/actions/npm
- run: npm run build:esm
- name: Run tests
timeout-minutes: 30
run: ${{ (inputs.test_type == 'node-optional') && 'npm run test:optional --workspace=@waku/tests' || 'npm run test:node' }}
- name: Merge allure reports
if: always() && env.ALLURE_REPORTS == 'true'
run: node ci/mergeAllureResults.cjs
- name: Get allure history
if: always() && env.ALLURE_REPORTS == 'true'
uses: actions/checkout@v3
continue-on-error: true
with:
repository: waku-org/allure-jswaku
ref: gh-pages
path: gh-pages
token: ${{ env.GITHUB_TOKEN }}
- name: Setup allure report
if: always() && env.ALLURE_REPORTS == 'true'
uses: simple-elf/allure-report-action@master
id: allure-report
with:
allure_results: allure-results
gh_pages: gh-pages
allure_history: allure-history
keep_reports: 30
- name: Deploy report to Github Pages
if: always() && env.ALLURE_REPORTS == 'true'
uses: peaceiris/actions-gh-pages@v3
with:
personal_token: ${{ env.GITHUB_TOKEN }}
external_repository: waku-org/allure-jswaku
publish_branch: gh-pages
publish_dir: allure-history
- name: Upload debug logs on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: ${{ inputs.test_type }}-debug.log
path: debug.log
- name: Sanitize log filenames
if: failure()
run: |
find packages/tests/log/ -type f | while read fname; do
dir=$(dirname "$fname")
base=$(basename "$fname")
sanitized_base=$(echo $base | tr -d '\"*:<>?|' | sed 's/[\\/\r\n]/_/g' | sed 's/_$//')
if [ "$base" != "$sanitized_base" ]; then
mv "$fname" "$dir/$sanitized_base"
fi
done
- name: Upload logs on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: ${{ inputs.test_type }}-logs
path: packages/tests/log/
- name: Create test summary
if: always() && env.ALLURE_REPORTS == 'true'
run: |
echo "## Run Information" >> $GITHUB_STEP_SUMMARY
echo "- **NWAKU**: ${{ env.WAKUNODE_IMAGE }}" >> $GITHUB_STEP_SUMMARY
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
echo "Allure report will be available at: https://waku-org.github.io/allure-jswaku/${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY

View File

@ -1,106 +0,0 @@
name: Run Reliability Test
on:
workflow_dispatch:
inputs:
test_type:
description: 'Type of reliability test to run'
required: true
default: 'longevity'
type: choice
options:
- longevity
- high-throughput
- throughput-sizes
- network-latency
- low-bandwidth
- packet-loss
- all
env:
NODE_JS: "24"
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
checks: write
strategy:
matrix:
test_type: [longevity, high-throughput, throughput-sizes, network-latency, low-bandwidth, packet-loss]
fail-fast: false
if: ${{ github.event.inputs.test_type == 'all' }}
steps:
- uses: actions/checkout@v3
with:
repository: waku-org/js-waku
- name: Remove unwanted software
uses: ./.github/actions/prune-vm
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_JS }}
- uses: ./.github/actions/npm
- run: npm run build:esm
- name: Run tests
timeout-minutes: 150
run: |
if [ "${{ matrix.test_type }}" = "high-throughput" ]; then
npm run test:high-throughput
elif [ "${{ matrix.test_type }}" = "throughput-sizes" ]; then
npm run test:throughput-sizes
elif [ "${{ matrix.test_type }}" = "network-latency" ]; then
npm run test:network-latency
elif [ "${{ matrix.test_type }}" = "low-bandwidth" ]; then
npm run test:low-bandwidth
elif [ "${{ matrix.test_type }}" = "packet-loss" ]; then
npm run test:packet-loss
else
npm run test:longevity
fi
single-test:
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
checks: write
if: ${{ github.event.inputs.test_type != 'all' }}
steps:
- uses: actions/checkout@v3
with:
repository: waku-org/js-waku
- name: Remove unwanted software
uses: ./.github/actions/prune-vm
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_JS }}
- uses: ./.github/actions/npm
- run: npm run build:esm
- name: Run tests
timeout-minutes: 150
run: |
if [ "${{ github.event.inputs.test_type }}" = "high-throughput" ]; then
npm run test:high-throughput
elif [ "${{ github.event.inputs.test_type }}" = "throughput-sizes" ]; then
npm run test:throughput-sizes
elif [ "${{ github.event.inputs.test_type }}" = "network-latency" ]; then
npm run test:network-latency
elif [ "${{ github.event.inputs.test_type }}" = "low-bandwidth" ]; then
npm run test:low-bandwidth
elif [ "${{ github.event.inputs.test_type }}" = "packet-loss" ]; then
npm run test:packet-loss
else
npm run test:longevity
fi

20
.gitignore vendored
View File

@ -1,23 +1,9 @@
.idea/* .idea/*
.angular .nyc_output
build build
bundle
dist
node_modules node_modules
test
src/**.js src/**.js
coverage coverage
*.log *.log
*.tsbuildinfo yarn.lock
docs
test-results
playwright-report
example
packages/discovery/mock_local_storage
.cursorrules
.giga
.cursor
.DS_Store
CLAUDE.md
.env
postgres-data/
packages/rln/waku-rlnv2-contract/

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "nim-waku"]
path = nim-waku
url = https://github.com/status-im/nim-waku.git

View File

@ -1 +0,0 @@
npx lint-staged

6
.mocharc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"require": "ts-node/register",
"exit": true
}

View File

@ -1,9 +1,3 @@
.github # package.json is formatted by package managers, so we ignore it here
.husky package.json
.vscode gen
nwaku
*/**/build
*/**/bundle
*/**/dist
*/**/node_modules
*/**/CHANGELOG.md

View File

@ -1,15 +0,0 @@
{
"packages/utils": "0.0.27",
"packages/proto": "0.0.15",
"packages/interfaces": "0.0.34",
"packages/enr": "0.0.33",
"packages/core": "0.0.40",
"packages/message-encryption": "0.0.38",
"packages/relay": "0.0.23",
"packages/sdk": "0.0.36",
"packages/discovery": "0.0.13",
"packages/sds": "0.0.8",
"packages/rln": "0.1.10",
"packages/react": "0.0.8",
"packages/run": "0.0.2"
}

View File

@ -1,65 +0,0 @@
module.exports = [
{
name: "Waku node",
path: "packages/sdk/bundle/index.js",
import: "{ WakuNode }",
},
{
name: "Waku Simple Light Node",
path: ["packages/sdk/bundle/index.js", "packages/core/bundle/index.js"],
import: {
"packages/sdk/bundle/index.js":
"{ createLightNode, createEncoder, createDecoder, bytesToUtf8, utf8ToBytes, Decoder, Encoder, DecodedMessage, WakuNode }",
},
},
{
name: "ECIES encryption",
path: "packages/message-encryption/bundle/ecies.js",
import: "{ generatePrivateKey, createEncoder, createDecoder }",
},
{
name: "Symmetric encryption",
path: "packages/message-encryption/bundle/symmetric.js",
import: "{ generateSymmetricKey, createEncoder, createDecoder }",
},
{
name: "DNS discovery",
path: "packages/discovery/bundle/index.js",
import: "{ PeerDiscoveryDns }",
},
{
name: "Peer Exchange discovery",
path: "packages/discovery/bundle/index.js",
import: "{ wakuPeerExchangeDiscovery }",
},
{
name: "Peer Cache Discovery",
path: "packages/discovery/bundle/index.js",
import: "{ wakuPeerCacheDiscovery }",
},
{
name: "Privacy preserving protocols",
path: "packages/relay/bundle/index.js",
import: "{ Relay }",
},
{
name: "Waku Filter",
path: "packages/sdk/bundle/index.js",
import: "{ Filter }",
},
{
name: "Waku LightPush",
path: "packages/sdk/bundle/index.js",
import: "{ LightPush }",
},
{
name: "History retrieval protocols",
path: "packages/sdk/bundle/index.js",
import: "{ Store }",
},
{
name: "Deterministic Message Hashing",
path: ["packages/core/bundle/index.js"],
import: "{ messageHash }",
},
];

View File

@ -1,11 +1,7 @@
{ {
"cSpell.userWords": [], // only use words from .cspell.json "cSpell.userWords": [], // only use words from .cspell.json
"cSpell.enabled": true, "cSpell.enabled": true,
"editor.defaultFormatter": "dbaeumer.vscode-eslint", "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.formatOnSave": false, // Disable general format on save
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true "typescript.enablePromptUseWorkspaceTsdk": true
} }

34
CHANGELOG.md Normal file
View File

@ -0,0 +1,34 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.2.0] - 2021-05-14
### Added
- `WakuRelay.getPeers` method.
- Use `WakuRelay.getPeers` in web chat app example to disable send button.
### Changed
- Enable passing `string`s to `addPeerToAddressBook`.
- Use `addPeerToAddressBook` in examples and usage doc.
- Settle on `js-waku` name across the board.
- **Breaking**: `RelayDefaultTopic` renamed to `DefaultPubsubTopic`.
## [0.1.0] - 2021-05-12
### Added
- Add usage section to the README.
- Support of [Waku v2 Relay](https://rfc.vac.dev/spec/11/).
- Support of [Waku v2 Store](https://rfc.vac.dev/spec/13/).
- [Node Chat App example](./examples/cli-chat).
- [ReactJS Chat App example](./examples/web-chat).
- [Typedoc Documentation](https://status-im.github.io/js-waku/docs).
[Unreleased]: https://github.com/status-im/js-waku/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/status-im/js-waku/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/status-im/js-waku/compare/f46ce77f57c08866873b5c80acd052e0ddba8bc9...v0.1.0

View File

@ -11,35 +11,28 @@ This project board is to prioritize the work of core contributors so do not be d
Do note that we have a [CI](./.github/workflows/ci.yml) powered by GitHub Action. Do note that we have a [CI](./.github/workflows/ci.yml) powered by GitHub Action.
To help ensure your PR passes, just run before committing: To help ensure your PR passes, just run before committing:
- `npm run fix`: To format your code, - `npm run fix`: To format your code,
- `npm run check`: To check your code for linting errors, - `npm run test`: To run all tests, including lint checks.
- `npm run test`: To run all tests
## Build & Test ## Build & Test
To build and test this repository, you need: To build and test this repository, you need:
- [Node.js & npm](https://nodejs.org/en/). - [Node.js & npm](https://nodejs.org/en/)
- Chrome (for browser testing). - [bufbuild](https://github.com/bufbuild/buf) (only if changing protobuf files)
- [protoc](https://grpc.io/docs/protoc-installation/) (only if changing protobuf files)
Run `npm run build` at least once so that intra-dependencies are resolved.
To ensure interoperability with [nim-waku](https://github.com/status-im/nim-waku/), some tests are run against a nim-waku node. To ensure interoperability with [nim-waku](https://github.com/status-im/nim-waku/), some tests are run against a nim-waku node.
This is why the relevant docker images for the node is pulled as part of the `pretest` script that runs before `npm run test`. This is why `nim-waku` is present as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules), which itself contain several submodules.
At this stage, it is not possible to exclude nim-waku tests, hence `git submodule update --init --recursive` is run before testing (see [`pretest` script](https://github.com/status-im/js-waku/blob/main/package.json)).
If you do not want to run `npm run test`, you can still pull the relevant nim-waku docker image by running `npm run pretest`. To build nim-waku, you also need [Rust](https://www.rust-lang.org/tools/install).
Note that we run tests in both NodeJS and browser environments (using [karma](https://karma-runner.github.io/)).
Files named `*.node.spec.ts` are only run in NodeJS environment;
Files named `*.spec.ts` are run in both NodeJS and browser environment.
## Guidelines ## Guidelines
- Please follow [Chris Beam's commit message guide](https://chris.beams.io/posts/git-commit/) for commit patches, - Please follow [Chris Beam's commit message guide](https://chris.beams.io/posts/git-commit/),
- Please test new code, we use [mocha](https://mochajs.org/), - Usually best to test new code,
[chai](https://www.chaijs.com/),
[fast-check](https://github.com/dubzzz/fast-check)
and [karma](https://karma-runner.github.io/).
### Committing Patches ### Committing Patches
@ -63,11 +56,3 @@ Commit messages should never contain any `@` mentions (usernames prefixed with "
Please refer to the [Git manual](https://git-scm.com/doc) for more information Please refer to the [Git manual](https://git-scm.com/doc) for more information
about Git. about Git.
### Releasing
`js-waku` has two types of releases:
- public releases;
- pre releases;
Public releases happen by merging PRs opened by `release-please` action.
Pre releases happen manually by triggering [this workflow](https://github.com/waku-org/js-waku/actions/workflows/pre-release.yml)

186
README.md
View File

@ -1,59 +1,183 @@
![GitHub Action](https://img.shields.io/github/actions/workflow/status/waku-org/js-waku/ci.yml?branch=master)
![Code Climate](https://img.shields.io/codeclimate/maintainability/waku-org/js-waku)
[![Discord chat](https://img.shields.io/discord/1110799176264056863.svg?logo=discord&colorB=7289DA)](https://discord.waku.org)
# js-waku # js-waku
A TypeScript implementation of the [Waku v2 protocol](https://rfc.vac.dev/spec/10/). A JavaScript implementation of the [Waku v2 protocol](https://rfc.vac.dev/spec/10/).
## Documentation ## Usage
- [Quick start](https://docs.waku.org/guides/js-waku/#getting-started) Install `js-waku` package:
- [Full documentation](https://docs.waku.org/guides/js-waku)
- [API documentation (`master` branch)](https://js.waku.org/) ```shell
- [Waku](https://waku.org/) npm install js-waku
- [Vac](https://vac.dev/) ```
API Documentation can also be generated locally: Start a waku node:
```javascript
import { Waku } from 'js-waku';
const waku = await Waku.create();
```
Connect to a new peer:
```javascript
import { multiaddr } from 'multiaddr';
import PeerId from 'peer-id';
// Directly dial a new peer
await waku.dial('/dns4/node-01.do-ams3.jdev.misc.statusim.net/tcp/7010/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ');
// Or, add peer to address book so it auto dials in the background
waku.addPeerToAddressBook(
'16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ',
['/dns4/node-01.do-ams3.jdev.misc.statusim.net/tcp/7010/wss']
);
```
The `contentTopic` is a metadata `string` that allows categorization of messages on the waku network.
Depending on your use case, you can either create a new `contentTopic` or look at the [RFCs](https://rfc.vac.dev/) and use an existing `contentTopic`.
See the [Waku v2 Message spec](https://rfc.vac.dev/spec/14/) for more details.
Listen to new messages received via [Waku v2 Relay](https://rfc.vac.dev/spec/11/), filtering the `contentTopic` to `waku/2/my-cool-app/proto`:
```javascript
waku.relay.addObserver((msg) => {
console.log("Message received:", msg.payloadAsUtf8)
}, ["waku/2/my-cool-app/proto"]);
```
Send a message on the waku relay network:
```javascript
import { WakuMessage } from 'js-waku';
const msg = WakuMessage.fromUtf8String("Here is a message!", "waku/2/my-cool-app/proto")
await waku.relay.send(msg);
```
The [Waku v2 Store protocol](https://rfc.vac.dev/spec/13/) enables full nodes to store messages received via relay
and clients to retrieve them (e.g. after resuming connectivity).
The protocol implements pagination meaning that it may take several queries to retrieve all messages.
Query a waku store peer to check historical messages:
```javascript
// Process messages once they are all retrieved:
const messages = await waku.store.queryHistory(storePeerId, ["waku/2/my-cool-app/proto"]);
messages.forEach((msg) => {
console.log("Message retrieved:", msg.payloadAsUtf8)
})
// Or, pass a callback function to be executed as pages are received:
waku.store.queryHistory(storePeerId, ["waku/2/my-cool-app/proto"],
(messages) => {
messages.forEach((msg) => {
console.log("Message retrieved:", msg.payloadAsUtf8)
})
});
```
Find more [examples](#examples) below
or checkout the latest `main` branch documentation at [https://status-im.github.io/js-waku/docs/](https://status-im.github.io/js-waku/docs/).
Docs can also be generated locally using:
```shell ```shell
git clone https://github.com/waku-org/js-waku.git
cd js-waku
npm install npm install
npm run doc npm run doc
``` ```
# Using Nix shell ## Waku Protocol Support
```shell
git clone https://github.com/waku-org/js-waku.git You can track progress on the [project board](https://github.com/status-im/js-waku/projects/1).
cd js-waku
nix develop - ✔: Supported
npm install - 🚧: Implementation in progress
npm run doc - ⛔: Support is not planned
```
| Spec | Implementation Status |
| ---- | -------------- |
|[6/WAKU1](https://rfc.vac.dev/spec/6)|⛔|
|[7/WAKU-DATA](https://rfc.vac.dev/spec/7)|⛔|
|[8/WAKU-MAIL](https://rfc.vac.dev/spec/8)|⛔|
|[9/WAKU-RPC](https://rfc.vac.dev/spec/9)|⛔|
|[10/WAKU2](https://rfc.vac.dev/spec/10)|🚧|
|[11/WAKU2-RELAY](https://rfc.vac.dev/spec/11)|✔|
|[12/WAKU2-FILTER](https://rfc.vac.dev/spec/12)||
|[13/WAKU2-STORE](https://rfc.vac.dev/spec/13)|✔ (querying node only)|
|[14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14)|✔|
|[15/WAKU2-BRIDGE](https://rfc.vac.dev/spec/15)||
|[16/WAKU2-RPC](https://rfc.vac.dev/spec/16)|⛔|
|[17/WAKU2-RLNRELAY](https://rfc.vac.dev/spec/17)||
|[18/WAKU2-SWAP](https://rfc.vac.dev/spec/18)||
## Bugs, Questions & Features ## Bugs, Questions & Features
If you encounter any bug or would like to propose new features, feel free to [open an issue](https://github.com/waku-org/js-waku/issues/new/). If you encounter any bug or would like to propose new features, feel free to [open an issue](https://github.com/status-im/js-waku/issues/new/).
For general discussion, get help or latest news, join us on [Vac Discord](https://discord.gg/Nrac59MfSX) or the [Waku Telegram Group](https://t.me/waku_org). For support, questions & more general topics, please join the discussion on the [Vac forum](https://forum.vac.dev/tag/js-waku) (use _\#js-waku_ tag).
## Roadmap ## Examples
You can track progress on the [project board](https://github.com/orgs/waku-org/projects/2/views/1). ## Web Chat App (ReactJS)
A ReactJS chat app is provided as a showcase of the library used in the browser.
A deployed version is available at https://status-im.github.io/js-waku/
Find the code in the [examples folder](https://github.com/status-im/js-waku/tree/main/examples/web-chat).
To run a development version locally, do:
```shell
git clone https://github.com/status-im/js-waku/ ; cd js-waku
npm install # Install dependencies for js-waku
npm run build # Build js-waku
cd examples/web-chat
npm install # Install dependencies for the web app
npm run start # Start development server to serve the web app on http://localhost:3000/js-waku
```
Use `/help` to see the available commands.
## CLI Chat App (NodeJS)
A node chat app is provided as a working example of the library.
It is interoperable with the [nim-waku chat app example](https://github.com/status-im/nim-waku/blob/master/examples/v2/chat2.nim).
Find the code in the [examples folder](https://github.com/status-im/js-waku/tree/main/examples/cli-chat).
To run the chat app, first ensure you have [Node.js](https://nodejs.org/en/) v14 or above:
```shell
node --version
```
Then, install and run:
```shell
git clone https://github.com/status-im/js-waku/ ; cd js-waku
npm install # Install dependencies for js-waku
npm run build # Build js-waku
cd examples/cli-chat
npm install # Install dependencies for the cli app
npm run start -- --staticNode /ip4/134.209.139.210/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ
```
You can also specify an optional `listenAddr` parameter (.e.g `--listenAddr /ip4/0.0.0.0/tcp/7777/ws`).
This is only useful if you want a remote node to dial to your chat app,
it is not necessary in normal usage when you just connect to the fleet.
## Contributing ## Contributing
See [CONTRIBUTING.md](https://github.com/waku-org/js-waku/blob/master/CONTRIBUTING.md). See [CONTRIBUTING.md](./CONTRIBUTING.md).
## License ## License
Licensed and distributed under either of Licensed and distributed under either of
- MIT license: [LICENSE-MIT](https://github.com/waku-org/js-waku/blob/master/LICENSE-MIT) or http://opensource.org/licenses/MIT * MIT license: [LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT
or or
- Apache License, Version 2.0, ([LICENSE-APACHE-v2](https://github.com/waku-org/js-waku/blob/master/LICENSE-APACHE-v2) or http://www.apache.org/licenses/LICENSE-2.0) * Apache License, Version 2.0, ([LICENSE-APACHE-v2](LICENSE-APACHE-v2) or http://www.apache.org/licenses/LICENSE-2.0)
at your option. These files may not be copied, modified, or distributed except according to those terms. at your option. These files may not be copied, modified, or distributed except according to those terms.

View File

@ -1,8 +1,8 @@
status = [ status = [
"check", "build_and_test (14, ubuntu-latest)",
"proto", "build_and_test (14, macos-latest)",
"browser", "examples_build_and_test (web-chat)",
"node", "examples_build_and_test (cli-chat)"
] ]
block_labels = ["work-in-progress"] block_labels = ["work-in-progress"]
delete_merged_branches = true delete_merged_branches = true

6
buf.gen.yaml Normal file
View File

@ -0,0 +1,6 @@
version: v1beta1
plugins:
- name: ts_proto
out: ./src/proto
opt: grpc_js,esModuleInterop=true

5
buf.yaml Normal file
View File

@ -0,0 +1,5 @@
version: v1beta1
build:
roots:
- ./proto

71
ci/Jenkinsfile vendored
View File

@ -1,71 +0,0 @@
#!/usr/bin/env groovy
library 'status-jenkins-lib@v1.9.27'
pipeline {
agent {
docker {
label 'linuxcontainer'
image 'harbor.status.im/infra/ci-build-containers:linux-base-1.0.0'
args '--volume=/nix:/nix ' +
'--volume=/etc/nix:/etc/nix ' +
'--user jenkins'
}
}
options {
disableConcurrentBuilds()
disableRestartFromStage()
/* manage how many builds we keep */
buildDiscarder(logRotator(
numToKeepStr: '20',
daysToKeepStr: '30',
))
}
environment {
GIT_AUTHOR_NAME = 'status-im-auto'
GIT_AUTHOR_EMAIL = 'auto@status.im'
PUPPETEER_SKIP_DOWNLOAD = 'true'
NO_COLOR = 'true'
}
stages {
stage('Deps') {
steps {
script {
nix.develop('npm install', pure: true)
}
}
}
stage('Packages') {
steps {
script {
nix.develop('npm run build', pure: true)
}
}
}
stage('Build') {
steps {
script {
nix.develop('npm run doc', pure: true)
}
}
}
stage('Publish') {
when { expression { GIT_BRANCH.endsWith('master') } }
steps {
sshagent(credentials: ['status-im-auto-ssh']) {
script {
nix.develop('npm run deploy', pure: false)
}
}
}
}
}
post {
always { cleanWs() }
}
}

View File

@ -1,11 +0,0 @@
# Description
Configuration of CI builds executed under a Jenkins instance at https://ci.status.im/.
# Website
The `Jenkinsfile.gh-pages` file builds the documentation site with this job:
https://ci.infra.status.im/job/website/job/js.waku.org/
And deploys it via `gh-pages` branch and [GitHub Pages](https://pages.github.com/) to:
https://js.waku.org/

View File

@ -1,45 +0,0 @@
import { promisify } from "util";
import { publish } from "gh-pages";
/* fix for "Unhandled promise rejections" */
process.on("unhandledRejection", (err) => {
throw err;
});
const ghpublish = promisify(publish);
const Args = process.argv.slice(2);
const USE_HTTPS = Args[0] && Args[0].toUpperCase() === "HTTPS";
const branch = "gh-pages";
const org = "waku-org";
const repo = "js-waku";
/* use SSH auth by default */
let repoUrl = USE_HTTPS
? `https://github.com/${org}/${repo}.git`
: `git@github.com:${org}/${repo}.git`;
/* alternative auth using GitHub user and API token */
if (typeof process.env.GH_USER !== "undefined") {
repoUrl =
"https://" +
process.env.GH_USER +
":" +
process.env.GH_TOKEN +
"@" +
`github.com/${org}/${repo}.git`;
}
const main = async (url, branch) => {
console.log(`Pushing to: ${url}`);
console.log(`On branch: ${branch}`);
await ghpublish("docs", {
repo: url,
branch: branch,
dotfiles: true,
silent: false
});
};
void main(repoUrl, branch);

View File

@ -1,19 +0,0 @@
const fs = require("fs-extra");
const glob = require("glob");
const ROOT_ALLURE_RESULTS = "./allure-results"; // Target directory in the root
fs.ensureDirSync(ROOT_ALLURE_RESULTS);
const directories = glob.sync("packages/**/allure-results");
directories.forEach((dir) => {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const sourcePath = `${dir}/${file}`;
const targetPath = `${ROOT_ALLURE_RESULTS}/${file}`;
fs.copyFileSync(sourcePath, targetPath);
});
});
console.log("All allure-results directories merged successfully!");

View File

@ -1,178 +0,0 @@
import cp from "child_process";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";
const PACKAGE_JSON = "package.json";
// hack to get __dirname
const DIR = path.dirname(fileURLToPath(import.meta.url));
const NEXT_TAG = "next";
const LATEST_TAG = "latest";
const CURRENT_TAG = readPublishTag();
const exec = promisify(cp.exec);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
run()
.then(() => {
console.info("Successfully published packages.");
})
.catch((err) => {
console.error("Failed at publishing packages with ", err.message);
});
async function run() {
const rootPackage = await readJSON(path.resolve(DIR, "../", PACKAGE_JSON));
const workspacePaths = rootPackage.workspaces;
if (CURRENT_TAG === NEXT_TAG) {
await makeReleaseCandidate();
}
const workspaces = await Promise.all(workspacePaths.map(readWorkspace));
if (CURRENT_TAG === NEXT_TAG) {
await upgradeWakuDependencies(workspaces);
}
await Promise.all(
workspaces
.filter(async (info) => {
const allowPublishing = await shouldBePublished(info);
if (allowPublishing) {
return true;
}
return false;
})
.map(async (info) => {
try {
await exec(
`npm publish --workspace ${info.workspace} --tag ${CURRENT_TAG} --access public`
);
console.info(
`Successfully published ${info.workspace} with version ${info.version}.`
);
} catch (err) {
console.error(
`Cannot publish ${info.workspace} with version ${info.version}. Error: ${err.message}`
);
}
})
);
}
async function readJSON(path) {
const rawJSON = await readFile(path, "utf-8");
return JSON.parse(rawJSON);
}
async function writeWorkspace(packagePath, text) {
const resolvedPath = path.resolve(DIR, "../", packagePath, PACKAGE_JSON);
await writeFile(resolvedPath, text);
}
async function readWorkspace(packagePath) {
const json = await readJSON(
path.resolve(DIR, "../", packagePath, PACKAGE_JSON)
);
return {
name: json.name,
private: !!json.private,
version: json.version,
workspace: packagePath,
rawPackageJson: json
};
}
async function shouldBePublished(info) {
if (info.private) {
console.info(`Skipping ${info.name} because it is private.`);
return false;
}
try {
const npmTag = `${info.name}@${info.version}`;
const { stdout } = await exec(`npm view ${npmTag} version`);
if (stdout.trim() !== info.version.trim()) {
return true;
}
console.info(`Workspace ${info.path} is already published.`);
} catch (err) {
if (err.message.includes("code E404")) {
return true;
}
console.error(
`Cannot check published version of ${info.path}. Received error: ${err.message}`
);
}
}
async function makeReleaseCandidate() {
try {
console.info("Marking workspace versions as release candidates.");
await exec(
`npm version prerelease --preid $(git rev-parse --short HEAD) --workspaces true`
);
} catch (e) {
console.error("Failed to mark release candidate versions.");
}
}
function readPublishTag() {
const args = process.argv.slice(2);
const tagIndex = args.indexOf("--tag");
if (tagIndex !== -1 && args[tagIndex + 1]) {
return args[tagIndex + 1];
}
return LATEST_TAG;
}
async function upgradeWakuDependencies(workspaces) {
console.log("Upgrading Waku dependencies in workspaces.");
const map = workspaces.reduce((acc, item) => {
if (!item.private) {
acc[item.name] = item;
}
return acc;
}, {});
const packageNames = Object.keys(map);
const promises = workspaces.map(async (info) => {
if (info.private) {
return;
}
["dependencies", "devDependencies", "peerDependencies"].forEach((type) => {
const deps = info.rawPackageJson[type];
if (!deps) {
return;
}
packageNames.forEach((name) => {
if (deps[name]) {
deps[name] = map[name].version;
}
});
});
try {
await writeWorkspace(info.workspace, JSON.stringify(info.rawPackageJson));
} catch (error) {
console.error(
`Failed to update package.json for ${info.name} with: `,
error
);
}
});
for (const promise of promises) {
await promise;
}
}

View File

@ -1,188 +0,0 @@
import cp from "child_process";
import { promisify } from "util";
import { createLightNode } from "@waku/sdk";
const exec = promisify(cp.exec);
class Fleet {
static async create() {
const url = "https://fleets.status.im";
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const fleet = await response.json();
if (!Fleet.isRecordValid(fleet)) {
throw Error("invalid_fleet_record");
}
return new Fleet(fleet);
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
throw error;
}
}
static isRecordValid(fleet) {
let isValid = true;
if (!fleet.fleets) {
console.error("No fleet records are present.");
isValid = false;
}
if (!fleet.fleets["waku.sandbox"]) {
console.error("No waku.sandbox records are present.");
isValid = false;
} else if (!fleet.fleets["waku.sandbox"]["wss/p2p/waku"]) {
console.error("No waku.sandbox WSS multi-addresses are present.");
isValid = false;
}
if (!fleet.fleets["waku.test"]) {
console.error("No waku.test records are present.");
isValid = false;
} else if (!fleet.fleets["waku.test"]["wss/p2p/waku"]) {
console.error("No waku.test WSS multi-addresses are present.");
isValid = false;
}
if (!isValid) {
console.error(`Got ${JSON.stringify(fleet)}`);
}
return isValid;
}
constructor(fleet) {
this.fleet = fleet;
}
get sandbox() {
return this.fleet.fleets["waku.sandbox"]["wss/p2p/waku"];
}
get test() {
return this.fleet.fleets["waku.test"]["wss/p2p/waku"];
}
}
class ConnectionChecker {
static waku;
static lock = false;
static async checkPlainWss(maddrs) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "1";
const results = await Promise.all(
maddrs.map((v) => ConnectionChecker.dialPlainWss(v))
);
console.log(
"Raw WSS connection:\n",
results.map(([addr, result]) => `${addr}:\t${result}`).join("\n")
);
return results;
}
static async dialPlainWss(maddr) {
const { domain, port } = ConnectionChecker.parseMaddr(maddr);
return [
maddr,
await ConnectionChecker.spawn(`npx wscat -c wss://${domain}:${port}`)
];
}
static async checkWakuWss(maddrs) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const waku = await createLightNode({
defaultBootstrap: false,
libp2p: {
hideWebSocketInfo: true
}
});
const results = await Promise.all(
maddrs.map((v) => ConnectionChecker.dialWaku(waku, v))
);
console.log(
"Libp2p WSS connection:\n",
results.map(([addr, result]) => `${addr}:\t${result}`).join("\n")
);
return results;
}
static async dialWaku(waku, maddr) {
try {
await waku.dial(maddr);
return [maddr, "OK"];
} catch (e) {
return [maddr, "FAIL"];
}
}
static parseMaddr(multiaddr) {
const regex = /\/dns4\/([^/]+)\/tcp\/(\d+)/;
const match = multiaddr.match(regex);
if (!match) {
throw new Error(
"Invalid multiaddress format. Expected /dns4/domain/tcp/port pattern."
);
}
return {
domain: match[1],
port: parseInt(match[2], 10)
};
}
static async spawn(command) {
try {
console.info(`Spawning command: ${command}`);
const { stderr } = await exec(command);
return stderr || "OK";
} catch (e) {
return "FAIL";
}
}
}
async function run() {
const fleet = await Fleet.create();
const sandbox = Object.values(fleet.sandbox);
const test = Object.values(fleet.test);
let maddrs = [...sandbox, ...test];
const plainWssResult = await ConnectionChecker.checkPlainWss(maddrs);
const wakuWssResult = await ConnectionChecker.checkWakuWss(maddrs);
const plainWssFail = plainWssResult.some(([_, status]) => status === "FAIL");
const wakuWssFail = wakuWssResult.some(([_, status]) => status === "FAIL");
if (plainWssFail || wakuWssFail) {
process.exit(1);
}
process.exit(0);
}
(async () => {
try {
await run();
} catch (error) {
console.error("Unhandled error:", error);
process.exit(1);
}
})();

View File

@ -0,0 +1,35 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": "./tsconfig.json" },
"env": { "es6": true },
"ignorePatterns": ["node_modules"],
"plugins": ["import", "eslint-comments", "functional"],
"extends": [
"eslint:recommended",
"plugin:eslint-comments/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript",
"prettier",
"prettier/@typescript-eslint"
],
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"eslint-comments/disable-enable-pair": [
"error",
{ "allowWholeFile": true }
],
"eslint-comments/no-unused-disable": "error",
"import/order": [
"error",
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
],
"no-constant-condition": ["error", { "checkLoops": false }],
"sort-imports": [
"error",
{ "ignoreDeclarationSort": true, "ignoreCase": true }
]
}
}

4
examples/cli-chat/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Generated directories:
.nyc_output/
node_modules/
/tsconfig.tsbuildinfo

View File

@ -0,0 +1,5 @@
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"require": "ts-node/register"
}

View File

@ -0,0 +1,3 @@
# A NodeJS CLI Chat App powered by js-waku
See js-waku [README](../../README.md#cli-chat-app-nodejs) for details.

13776
examples/cli-chat/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,76 @@
{
"name": "js-waku-cli-chat",
"version": "0.1.0",
"description": "A NodeJS CLI Chat App powered by js-waku",
"main": "./index.ts",
"repository": "https://github.com/status-im/js-waku",
"license": "MIT OR Apache-2.0",
"keywords": [
"waku",
"decentralised",
"communication"
],
"scripts": {
"build": "run-s build:*",
"build:main": "tsc -p tsconfig.json",
"fix": "run-s fix:*",
"fix:prettier": "prettier \"src/**/*.ts\" \"./*.json\" --write",
"fix:lint": "eslint src --ext .ts --fix",
"start": "ts-node src/index.ts",
"test": "run-s build test:*",
"test:lint": "eslint src --ext .ts",
"test:prettier": "prettier \"src/**/*.ts\" \"./*.json\" --list-different",
"test:spelling": "cspell \"{README.md,src/**/*.ts}\" -c ../../.cspell.json",
"test:unit": "nyc --silent mocha",
"watch:build": "tsc -p tsconfig.json -w",
"watch:test": "nyc --silent mocha --watch",
"version": "standard-version",
"reset-hard": "git clean -dfx && git reset --hard && npm i"
},
"engines": {
"node": ">=14"
},
"dependencies": {
"js-waku": "../../build/main",
"libp2p-tcp": "^0.15.4",
"prompt-sync": "^4.2.0"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/app-root-path": "^1.2.4",
"@types/chai": "^4.2.15",
"@types/mocha": "^8.2.2",
"@types/node": "^14.14.31",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"chai": "^4.3.4",
"cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"mocha": "^8.3.2",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"prettier": "^2.1.1",
"standard-version": "^9.0.0",
"ts-node": "^9.1.1",
"typedoc": "^0.20.29",
"typescript": "^4.0.2"
},
"prettier": {
"singleQuote": true
},
"files": [
"!**/*.spec.*",
"!**/*.json",
"README.md"
],
"nyc": {
"extends": "@istanbuljs/nyc-config-typescript",
"exclude": [
"**/*.spec.js"
]
}
}

View File

@ -0,0 +1,17 @@
import { expect } from 'chai';
import { ChatMessage } from 'js-waku';
import { formatMessage } from './chat';
describe('CLI Chat app', () => {
it('Format message', () => {
const date = new Date(234325324);
const chatMessage = ChatMessage.fromUtf8String(
date,
'alice',
'Hello world!'
);
expect(formatMessage(chatMessage)).to.match(/^<.*> alice: Hello world!$/);
});
});

View File

@ -0,0 +1,130 @@
import readline from 'readline';
import util from 'util';
import { ChatMessage, StoreCodec, Waku, WakuMessage } from 'js-waku';
import TCP from 'libp2p-tcp';
import { multiaddr, Multiaddr } from 'multiaddr';
const ChatContentTopic = 'dingpu';
export default async function startChat(): Promise<void> {
const opts = processArguments();
const waku = await Waku.create({
listenAddresses: [opts.listenAddr],
modules: { transport: [TCP] },
});
console.log('PeerId: ', waku.libp2p.peerId.toB58String());
console.log('Listening on ');
waku.libp2p.multiaddrs.forEach((address) => {
console.log(`\t- ${address}`);
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
let nick = 'js-waku';
try {
const question = util.promisify(rl.question).bind(rl);
// Looks like wrong type definition of promisify is picked.
// May be related to https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20497
nick = ((await question(
'Please choose a nickname: '
)) as unknown) as string;
} catch (e) {
console.log('Using default nick. Due to ', e);
}
console.log(`Hi, ${nick}!`);
waku.relay.addObserver(
(message) => {
if (message.payload) {
const chatMsg = ChatMessage.decode(message.payload);
console.log(formatMessage(chatMsg));
}
},
[ChatContentTopic]
);
if (opts.staticNode) {
console.log(`Dialing ${opts.staticNode}`);
await waku.dial(opts.staticNode);
}
// If we connect to a peer with WakuStore, we run the protocol
// TODO: Instead of doing it `once` it should always be done but
// only new messages should be printed
waku.libp2p.peerStore.once(
'change:protocols',
async ({ peerId, protocols }) => {
if (protocols.includes(StoreCodec)) {
console.log(
`Retrieving archived messages from ${peerId.toB58String()}`
);
const messages = await waku.store.queryHistory(peerId, [
ChatContentTopic,
]);
messages?.map((msg) => {
if (msg.payload) {
const chatMsg = ChatMessage.decode(msg.payload);
console.log(formatMessage(chatMsg));
}
});
}
}
);
console.log('Ready to chat!');
rl.prompt();
for await (const line of rl) {
rl.prompt();
const chatMessage = ChatMessage.fromUtf8String(new Date(), nick, line);
const msg = WakuMessage.fromBytes(chatMessage.encode(), ChatContentTopic);
await waku.relay.send(msg);
}
}
interface Options {
staticNode?: Multiaddr;
listenAddr: string;
}
function processArguments(): Options {
const passedArgs = process.argv.slice(2);
let opts: Options = { listenAddr: '/ip4/0.0.0.0/tcp/0' };
while (passedArgs.length) {
const arg = passedArgs.shift();
switch (arg) {
case '--staticNode':
opts = Object.assign(opts, {
staticNode: multiaddr(passedArgs.shift()!),
});
break;
case '--listenAddr':
opts = Object.assign(opts, { listenAddr: passedArgs.shift() });
break;
default:
console.log(`Unsupported argument: ${arg}`);
process.exit(1);
}
}
return opts;
}
export function formatMessage(chatMsg: ChatMessage): string {
const timestamp = chatMsg.timestamp.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: false,
});
return `<${timestamp}> ${chatMsg.nick}: ${chatMsg.payloadAsUtf8}`;
}

View File

@ -0,0 +1,5 @@
import startChat from './chat';
(async () => {
await startChat();
})();

View File

@ -0,0 +1,5 @@
declare module 'libp2p-tcp' {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const TCP: any;
export = TCP;
}

View File

@ -0,0 +1,54 @@
{
"compilerOptions": {
"incremental": true,
"target": "es2017",
"rootDir": "src",
"noEmit": true,
"moduleResolution": "node",
"module": "commonjs",
"declaration": true,
"inlineSourceMap": true,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"resolveJsonModule": true /* Include modules imported with .json extension. */,
"strict": true /* Enable all strict type-checking options. */,
/* Strict Type-Checking Options */
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
"strictFunctionTypes": true /* Enable strict checking of function types. */,
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"forceConsistentCasingInFileNames": true,
/* Debugging Options */
"traceResolution": false /* Report module resolution log messages. */,
"listEmittedFiles": false /* Print names of generated files part of the compilation. */,
"listFiles": false /* Print names of files part of the compilation. */,
"pretty": true /* Stylize errors and messages using color and context. */,
// Due to broken types in indirect dependencies
"skipLibCheck": true,
/* Experimental Options */
// "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
// "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
"lib": ["es2017"],
"types": ["node", "mocha"],
"typeRoots": ["node_modules/@types", "src/types"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**"],
"compileOnSave": false,
"ts-node": {
"files": true
}
}

23
examples/web-chat/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,3 @@
# package.json is formatted by package managers, so we ignore it here
package.json
gen

View File

@ -0,0 +1,3 @@
# A React Web Chat App powered by js-waku
See js-waku [README](../../README.md#web-chat-app-reactjs) for details.

44966
examples/web-chat/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
{
"name": "web-chat",
"version": "0.1.0",
"private": true,
"homepage": "/js-waku",
"dependencies": {
"@livechat/ui-kit": "*",
"js-waku": "../../build/main",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"server-name-generator": "^1.0.5",
"web-vitals": "^1.1.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.22",
"@types/node": "^12.20.7",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"cspell": "^5.3.12",
"gh-pages": "^3.1.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.2.1",
"react-scripts": "4.0.3",
"typescript": "^4.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test:unit": "react-scripts test",
"eject": "react-scripts eject",
"fix": "run-s fix:*",
"test": "run-s build test:*",
"test:lint": "eslint src --ext .ts --ext .tsx",
"test:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" --list-different",
"test:spelling": "cspell \"{README.md,.github/*.md,src/**/*.{ts,tsx},public/**/*.html}\" -c ../../.cspell.json",
"fix:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" --write",
"fix:lint": "eslint src --ext .ts --ext .tsx --fix",
"js-waku:build": "cd ../; npm run build",
"predeploy": "run-s js-waku:build build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<meta
name="description"
content="Chat app powered by js-waku"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Waku v2 chat app</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,25 @@
{
"short_name": "Waku v2 chat app",
"name": "Chat app powered by js-waku",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,160 @@
import PeerId from 'peer-id';
import { useEffect, useState } from 'react';
import './App.css';
import { ChatMessage, WakuMessage, StoreCodec, Waku } from 'js-waku';
import handleCommand from './command';
import Room from './Room';
import { WakuContext } from './WakuContext';
import { ThemeProvider } from '@livechat/ui-kit';
import { generate } from 'server-name-generator';
const themes = {
AuthorName: {
css: {
fontSize: '1.1em',
},
},
Message: {
css: {
margin: '0em',
padding: '0em',
fontSize: '0.83em',
},
},
MessageText: {
css: {
margin: '0em',
padding: '0.1em',
paddingLeft: '1em',
fontSize: '1.1em',
},
},
MessageGroup: {
css: {
margin: '0em',
padding: '0.2em',
},
},
};
export const ChatContentTopic = 'dingpu';
export default function App() {
let [newMessages, setNewMessages] = useState<ChatMessage[]>([]);
let [archivedMessages, setArchivedMessages] = useState<ChatMessage[]>([]);
let [stateWaku, setWaku] = useState<Waku | undefined>(undefined);
let [nick, setNick] = useState<string>(generate());
useEffect(() => {
const handleRelayMessage = (wakuMsg: WakuMessage) => {
if (wakuMsg.payload) {
const chatMsg = ChatMessage.decode(wakuMsg.payload);
if (chatMsg) {
setNewMessages([chatMsg]);
}
}
};
const handleProtocolChange = async (
waku: Waku,
{ peerId, protocols }: { peerId: PeerId; protocols: string[] }
) => {
if (protocols.includes(StoreCodec)) {
console.log(`${peerId.toB58String()}: retrieving archived messages}`);
try {
const response = await waku.store.queryHistory(peerId, [
ChatContentTopic,
]);
console.log(`${peerId.toB58String()}: messages retrieved:`, response);
if (response) {
const messages = response
.map((wakuMsg) => wakuMsg.payload)
.filter((payload) => !!payload)
.map((payload) => ChatMessage.decode(payload as Uint8Array));
setArchivedMessages(messages);
}
} catch (e) {
console.log(
`${peerId.toB58String()}: error encountered when retrieving archived messages`,
e
);
}
}
};
if (!stateWaku) {
initWaku(setWaku)
.then(() => console.log('Waku init done'))
.catch((e) => console.log('Waku init failed ', e));
} else {
stateWaku.relay.addObserver(handleRelayMessage, [ChatContentTopic]);
stateWaku.libp2p.peerStore.on(
'change:protocols',
handleProtocolChange.bind({}, stateWaku)
);
// To clean up listener when component unmounts
return () => {
stateWaku?.libp2p.peerStore.removeListener(
'change:protocols',
handleProtocolChange.bind({}, stateWaku)
);
};
}
}, [stateWaku]);
return (
<div
className="chat-app"
style={{ height: '100vh', width: '100vw', overflow: 'hidden' }}
>
<WakuContext.Provider value={{ waku: stateWaku }}>
<ThemeProvider theme={themes}>
<Room
nick={nick}
newMessages={newMessages}
archivedMessages={archivedMessages}
commandHandler={(input: string) => {
const { command, response } = handleCommand(
input,
stateWaku,
setNick
);
const commandMessages = response.map((msg) => {
return ChatMessage.fromUtf8String(new Date(), command, msg);
});
setNewMessages(commandMessages);
}}
/>
</ThemeProvider>
</WakuContext.Provider>
</div>
);
}
async function initWaku(setter: (waku: Waku) => void) {
try {
const waku = await Waku.create({
config: {
pubsub: {
enabled: true,
emitSelf: true,
},
},
});
setter(waku);
waku.addPeerToAddressBook(
'16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ',
['/dns4/node-01.do-ams3.jdev.misc.statusim.net/tcp/7010/wss']
);
waku.addPeerToAddressBook(
'16Uiu2HAmSyrYVycqBCWcHyNVQS6zYQcdQbwyov1CDijboVRsQS37',
['/dns4/node-01.do-ams3.jdev.misc.statusim.net/tcp/7009/wss']
);
} catch (e) {
console.log('Issue starting waku ', e);
}
}

View File

@ -0,0 +1,140 @@
import { useEffect, useRef, useState } from 'react';
import { ChatMessage } from 'js-waku';
import {
Message,
MessageText,
MessageGroup,
MessageList,
} from '@livechat/ui-kit';
interface Props {
archivedMessages: ChatMessage[];
newMessages: ChatMessage[];
}
export default function ChatList(props: Props) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
let updatedMessages;
if (IsThereNewMessages(props.newMessages, messages)) {
updatedMessages = messages.slice().concat(props.newMessages);
if (IsThereNewMessages(props.archivedMessages, updatedMessages)) {
updatedMessages = copyMergeUniqueReplace(
props.archivedMessages,
updatedMessages
);
}
} else {
if (IsThereNewMessages(props.archivedMessages, messages)) {
updatedMessages = copyMergeUniqueReplace(
props.archivedMessages,
messages
);
}
}
if (updatedMessages) {
setMessages(updatedMessages);
}
const messagesGroupedBySender = groupMessagesBySender(messages).map(
(currentMessageGroup) => (
<MessageGroup onlyFirstWithMeta>
{currentMessageGroup.map((currentMessage) => (
<Message
key={
currentMessage.timestamp.valueOf() +
currentMessage.nick +
currentMessage.payloadAsUtf8
}
authorName={currentMessage.nick}
date={formatDisplayDate(currentMessage)}
>
<MessageText>{currentMessage.payloadAsUtf8}</MessageText>
</Message>
))}
</MessageGroup>
)
);
return (
<MessageList active containScrollInSubtree>
{messagesGroupedBySender}
<AlwaysScrollToBottom newMessages={props.newMessages} />
</MessageList>
);
}
function groupMessagesBySender(messageArray: ChatMessage[]): ChatMessage[][] {
let currentSender = -1;
let lastNick = '';
let messagesBySender: ChatMessage[][] = [];
let currentSenderMessage = 0;
for (let currentMessage of messageArray) {
if (lastNick !== currentMessage.nick) {
currentSender++;
messagesBySender[currentSender] = [];
currentSenderMessage = 0;
lastNick = currentMessage.nick;
}
messagesBySender[currentSender][currentSenderMessage++] = currentMessage;
}
return messagesBySender;
}
function formatDisplayDate(message: ChatMessage): string {
return message.timestamp.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: false,
});
}
const AlwaysScrollToBottom = (props: { newMessages: ChatMessage[] }) => {
const elementRef = useRef<HTMLDivElement>();
useEffect(() => {
// @ts-ignore
elementRef.current.scrollIntoView();
}, [props.newMessages]);
// @ts-ignore
return <div ref={elementRef} />;
};
function IsThereNewMessages(
newValues: ChatMessage[],
currentValues: ChatMessage[]
): boolean {
if (newValues.length === 0) return false;
if (currentValues.length === 0) return true;
return !newValues.find((newMsg) =>
currentValues.find(isEqual.bind({}, newMsg))
);
}
function copyMergeUniqueReplace(
newValues: ChatMessage[],
currentValues: ChatMessage[]
) {
const copy = currentValues.slice();
newValues.forEach((msg) => {
if (!copy.find(isEqual.bind({}, msg))) {
copy.push(msg);
}
});
copy.sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf());
return copy;
}
function isEqual(lhs: ChatMessage, rhs: ChatMessage): boolean {
return (
lhs.nick === rhs.nick &&
lhs.payloadAsUtf8 === rhs.payloadAsUtf8 &&
lhs.timestamp.toString() === rhs.timestamp.toString()
);
}

View File

@ -0,0 +1,58 @@
import { ChangeEvent, KeyboardEvent, useState } from 'react';
import { useWaku } from './WakuContext';
import {
TextInput,
TextComposer,
Row,
Fill,
Fit,
SendButton,
} from '@livechat/ui-kit';
interface Props {
sendMessage: ((msg: string) => Promise<void>) | undefined;
}
export default function MessageInput(props: Props) {
const [inputText, setInputText] = useState<string>('');
const { waku } = useWaku();
const sendMessage = async () => {
if (props.sendMessage) {
await props.sendMessage(inputText);
setInputText('');
}
};
const messageHandler = (event: ChangeEvent<HTMLInputElement>) => {
setInputText(event.target.value);
};
const keyPressHandler = async (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
await sendMessage();
}
};
// Enable the button if there are relay peers available or the user is sending a command
const activeButton =
(waku && waku.relay.getPeers().size !== 0) || inputText.startsWith('/');
return (
<TextComposer
onKeyDown={keyPressHandler}
onChange={messageHandler}
active={activeButton}
onButtonClick={sendMessage}
>
<Row align="center">
<Fill>
<TextInput value={inputText} />
</Fill>
<Fit>
<SendButton />
</Fit>
</Row>
</TextComposer>
);
}

View File

@ -0,0 +1,62 @@
import { ChatMessage, WakuMessage } from 'js-waku';
import { ChatContentTopic } from './App';
import ChatList from './ChatList';
import MessageInput from './MessageInput';
import { useWaku } from './WakuContext';
import { TitleBar } from '@livechat/ui-kit';
interface Props {
newMessages: ChatMessage[];
archivedMessages: ChatMessage[];
commandHandler: (cmd: string) => void;
nick: string;
}
export default function Room(props: Props) {
const { waku } = useWaku();
return (
<div
className="chat-container"
style={{ height: '98vh', display: 'flex', flexDirection: 'column' }}
>
<TitleBar title="Waku v2 chat app" />
<ChatList
newMessages={props.newMessages}
archivedMessages={props.archivedMessages}
/>
<MessageInput
sendMessage={
waku
? async (messageToSend) => {
return handleMessage(
messageToSend,
props.nick,
props.commandHandler,
waku.relay.send.bind(waku.relay)
);
}
: undefined
}
/>
</div>
);
}
async function handleMessage(
message: string,
nick: string,
commandHandler: (cmd: string) => void,
messageSender: (msg: WakuMessage) => Promise<void>
) {
if (message.startsWith('/')) {
commandHandler(message);
} else {
const chatMessage = ChatMessage.fromUtf8String(new Date(), nick, message);
const wakuMsg = WakuMessage.fromBytes(
chatMessage.encode(),
ChatContentTopic
);
return messageSender(wakuMsg);
}
}

View File

@ -0,0 +1,9 @@
import { createContext, useContext } from 'react';
import { Waku } from 'js-waku';
export type WakuContextType = {
waku?: Waku;
};
export const WakuContext = createContext<WakuContextType>({ waku: undefined });
export const useWaku = () => useContext(WakuContext);

View File

@ -0,0 +1,30 @@
import WakuMock, { Message } from './WakuMock';
test('Messages are emitted', async () => {
const wakuMock = await WakuMock.create();
let message: Message;
wakuMock.on('message', (msg) => {
message = msg;
});
await new Promise((resolve) => setTimeout(resolve, 2000));
// @ts-ignore
expect(message.message).toBeDefined();
});
test('Messages are sent', async () => {
const wakuMock = await WakuMock.create();
const text = 'This is a message.';
let message: Message;
wakuMock.on('message', (msg) => {
message = msg;
});
await wakuMock.send(text);
// @ts-ignore
expect(message.message).toEqual(text);
});

View File

@ -0,0 +1,69 @@
class EventEmitter<T> {
public callbacks: { [key: string]: Array<(data: T) => void> };
constructor() {
this.callbacks = {};
}
on(event: string, cb: (data: T) => void) {
if (!this.callbacks[event]) this.callbacks[event] = [];
this.callbacks[event].push(cb);
}
emit(event: string, data: T) {
let cbs = this.callbacks[event];
if (cbs) {
cbs.forEach((cb) => cb(data));
}
}
}
export interface Message {
timestamp: Date;
handle: string;
message: string;
}
export default class WakuMock extends EventEmitter<Message> {
index: number;
intervalId?: number | NodeJS.Timeout;
private constructor() {
super();
this.index = 0;
}
public static async create(): Promise<WakuMock> {
await new Promise((resolve) => setTimeout(resolve, 500));
const wakuMock = new WakuMock();
wakuMock.startInterval();
return wakuMock;
}
public async send(message: string): Promise<void> {
const timestamp = new Date();
const handle = 'me';
this.emit('message', {
timestamp,
handle,
message,
});
}
private startInterval() {
if (this.intervalId === undefined) {
this.intervalId = setInterval(this.emitMessage.bind(this), 1000);
}
}
private emitMessage() {
const handle = 'you';
const timestamp = new Date();
this.emit('message', {
timestamp,
handle,
message: `This is message #${this.index++}.`,
});
}
}

View File

@ -0,0 +1,143 @@
import { multiaddr } from 'multiaddr';
import PeerId from 'peer-id';
import { Waku } from 'js-waku';
function help(): string[] {
return [
'/nick <nickname>: set a new nickname',
'/info: some information about the node',
'/connect <Multiaddr>: connect to the given peer',
'/help: Display this help',
];
}
function nick(
nick: string | undefined,
setNick: (nick: string) => void
): string[] {
if (!nick) {
return ['No nick provided'];
}
setNick(nick);
return [`New nick: ${nick}`];
}
function info(waku: Waku | undefined): string[] {
if (!waku) {
return ['Waku node is starting'];
}
return [`PeerId: ${waku.libp2p.peerId.toB58String()}`];
}
function connect(peer: string | undefined, waku: Waku | undefined): string[] {
if (!waku) {
return ['Waku node is starting'];
}
if (!peer) {
return ['No peer provided'];
}
try {
const peerMultiaddr = multiaddr(peer);
const peerId = peerMultiaddr.getPeerId();
if (!peerId) {
return ['Peer Id needed to dial'];
}
waku.addPeerToAddressBook(PeerId.createFromB58String(peerId), [
peerMultiaddr,
]);
return [
`${peerId}: ${peerMultiaddr.toString()} added to address book, autodial in progress`,
];
} catch (e) {
return ['Invalid multiaddr: ' + e];
}
}
function peers(waku: Waku | undefined): string[] {
if (!waku) {
return ['Waku node is starting'];
}
let response: string[] = [];
waku.libp2p.peerStore.peers.forEach((peer, peerId) => {
response.push(peerId + ':');
let addresses = ' addresses: [';
peer.addresses.forEach(({ multiaddr }) => {
addresses += ' ' + multiaddr.toString() + ',';
});
addresses = addresses.replace(/,$/, '');
addresses += ']';
response.push(addresses);
let protocols = ' protocols: [';
protocols += peer.protocols;
protocols += ']';
response.push(protocols);
});
if (response.length === 0) {
response.push('Not connected to any peer.');
}
return response;
}
function connections(waku: Waku | undefined): string[] {
if (!waku) {
return ['Waku node is starting'];
}
let response: string[] = [];
waku.libp2p.connections.forEach(
(
connections: import('libp2p-interfaces/src/connection/connection')[],
peerId
) => {
response.push(peerId + ':');
let strConnections = ' connections: [';
connections.forEach((connection) => {
strConnections += JSON.stringify(connection.stat);
strConnections += '; ' + JSON.stringify(connection.streams);
});
strConnections += ']';
response.push(strConnections);
}
);
if (response.length === 0) {
response.push('Not connected to any peer.');
}
return response;
}
export default function handleCommand(
input: string,
waku: Waku | undefined,
setNick: (nick: string) => void
): { command: string; response: string[] } {
let response: string[] = [];
const args = parseInput(input);
const command = args.shift()!;
switch (command) {
case '/help':
help().map((str) => response.push(str));
break;
case '/nick':
nick(args.shift(), setNick).map((str) => response.push(str));
break;
case '/info':
info(waku).map((str) => response.push(str));
break;
case '/connect':
connect(args.shift(), waku).map((str) => response.push(str));
break;
case '/peers':
peers(waku).map((str) => response.push(str));
break;
case '/connections':
connections(waku).map((str) => response.push(str));
break;
default:
response.push(`Unknown Command '${command}'`);
}
return { command, response };
}
export function parseInput(input: string): string[] {
const clean = input.trim().replaceAll(/\s\s+/g, ' ');
return clean.split(' ');
}

View File

@ -0,0 +1,30 @@
@import-normalize; /* bring in normalize.css styles */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.room-row {
text-align: left;
margin-left: 20px;
}
.room-row:after {
clear: both;
content: "";
display: table;
}
.chat-room{
margin: 2px;
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -0,0 +1 @@
declare module '@livechat/ui-kit';

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"incremental": true,
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"typeRoots": ["node_modules/@types", "src/types"]
},
"include": ["src"]
}

26
flake.lock generated
View File

@ -1,26 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1761016216,
"narHash": "sha256-G/iC4t/9j/52i/nm+0/4ybBmAF4hzR8CNHC75qEhjHo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "481cf557888e05d3128a76f14c76397b7d7cc869",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-25.05",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,33 +0,0 @@
{
description = "Nix flake development shell.";
inputs = {
nixpkgs.url = "nixpkgs/nixos-25.05";
};
outputs =
{ self, nixpkgs }:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forEachSystem = nixpkgs.lib.genAttrs supportedSystems;
pkgsFor = forEachSystem (system: import nixpkgs { inherit system; });
in
rec {
formatter = forEachSystem (system: pkgsFor.${system}.nixpkgs-fmt);
devShells = forEachSystem (system: {
default = pkgsFor.${system}.mkShellNoCC {
packages = with pkgsFor.${system}.buildPackages; [
git # 2.44.1
openssh # 9.7p1
nodejs_20 # v20.15.1
];
};
});
};
}

View File

@ -1,72 +0,0 @@
/* eslint-env node */
const playwright = require("playwright");
const webpack = require("webpack");
if (!process.env.CHROME_BIN) {
process.env.CHROME_BIN = playwright.chromium.executablePath();
}
console.log("Using CHROME_BIN:", process.env.CHROME_BIN);
if (!process.env.FIREFOX_BIN) {
process.env.FIREFOX_BIN = playwright.firefox.executablePath();
}
console.log("Using FIREFOX_BIN:", process.env.FIREFOX_BIN);
module.exports = function (config) {
const configuration = {
frameworks: ["webpack", "mocha"],
files: ["src/**/!(node).spec.ts"],
preprocessors: {
"src/**/!(node).spec.ts": ["webpack"]
},
envPreprocessor: ["CI"],
reporters: ["progress"],
browsers: process.env.CI
? ["ChromeHeadlessCI", "FirefoxHeadless"]
: ["ChromeHeadless", "FirefoxHeadless"],
customLaunchers: {
ChromeHeadlessCI: {
base: "ChromeHeadless",
flags: [
"--no-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
"--disable-software-rasterizer",
"--disable-extensions"
]
}
},
singleRun: true,
client: {
mocha: {
timeout: 6000 // Default is 2s
}
},
webpack: {
mode: "development",
module: {
rules: [{ test: /\.([cm]?ts|tsx)$/, loader: "ts-loader" }]
},
plugins: [
new webpack.DefinePlugin({
"process.env.CI": process.env.CI || false,
"process.env.DISPLAY": "Browser"
}),
new webpack.ProvidePlugin({
process: "process/browser.js"
})
],
resolve: {
extensions: [".ts", ".tsx", ".js"],
extensionAlias: {
".js": [".js", ".ts"],
".cjs": [".cjs", ".cts"],
".mjs": [".mjs", ".mts"]
}
},
stats: { warnings: false },
devtool: "inline-source-map"
}
};
config.set(configuration);
};

21
netlify.toml Normal file
View File

@ -0,0 +1,21 @@
[build]
publish = "build/html/"
# Default build command.
command = '''
npm install
npm run build
npm run doc:html
cd examples/web-chat
npm install
npm run build
cd ../../
mkdir -p ./build/html
mv -v examples/web-chat/build ./build/html/js-waku
mv -v build/docs ./build/html/
'''
[[redirects]]
from = "/"
to = "/js-waku"
status = 200

1
nim-waku Submodule

@ -0,0 +1 @@
Subproject commit 7c5df3379b42f1e5ddad66e84e152aec6ebdf10d

54535
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,83 +1,134 @@
{ {
"name": "@waku/root", "name": "js-waku",
"private": true, "version": "0.2.0",
"type": "module", "description": "TypeScript implementation of the Waku v2 protocol",
"workspaces": [ "main": "build/main/index.js",
"packages/proto", "typings": "build/main/index.d.ts",
"packages/interfaces", "module": "build/module/index.js",
"packages/utils", "repository": "https://github.com/status-im/js-waku",
"packages/enr", "license": "MIT OR Apache-2.0",
"packages/core", "keywords": [
"packages/discovery", "waku",
"packages/message-encryption", "decentralised",
"packages/sds", "communication"
"packages/rln",
"packages/sdk",
"packages/relay",
"packages/run",
"packages/tests",
"packages/reliability-tests",
"packages/browser-tests",
"packages/build-utils",
"packages/react"
], ],
"scripts": { "scripts": {
"prepare": "husky", "build": "run-s build:*",
"build": "npm run build --workspaces --if-present", "build:main": "tsc -p tsconfig.json",
"build:esm": "npm run build:esm --workspaces --if-present", "build:module": "tsc -p tsconfig.module.json",
"size": "npm run build && size-limit", "build:dev": "tsc -p tsconfig.dev.json",
"fix": "run-s fix:*", "fix": "run-s fix:*",
"fix:workspaces": "npm run fix --workspaces --if-present", "fix:prettier": "prettier \"src/**/*.ts\" \"./*.json\" --write",
"check": "run-s check:*", "fix:lint": "eslint src --ext .ts --fix",
"check:workspaces": "npm run check --workspaces --if-present", "pretest": "run-s pretest:*",
"check:ws": "[ $(ls -1 ./packages|wc -l) -eq $(cat package.json | jq '.workspaces | length') ] || exit 1 # check no packages left behind", "pretest:1-init-git-submodules": "[ -f './nim-waku/build/wakunode2' ] || git submodule update --init --recursive",
"test": "NODE_ENV=test npm run test --workspaces --if-present", "pretest:2-build-nim-waku": "[ -f './nim-waku/build/wakunode2' ] || run-s nim-waku:build",
"test:browser": "NODE_ENV=test npm run test:browser --workspaces --if-present", "nim-waku:build": "(cd nim-waku; NIMFLAGS=\"-d:chronicles_colors=off -d:chronicles_sinks=textlines -d:chronicles_log_level=TRACE\" make -j$(nproc --all 2>/dev/null || echo 2) wakunode2)",
"test:node": "NODE_ENV=test npm run test:node --workspaces --if-present", "nim-waku:upgrade": "(cd nim-waku && git pull origin master && rm -rf ./build/ ./vendor && make -j$(nproc --all 2>/dev/null || echo 2) update) && run-s nim-waku:build",
"test:longevity": "npm --prefix packages/reliability-tests run test:longevity", "test": "run-s build test:*",
"test:high-throughput": "npm --prefix packages/reliability-tests run test:high-throughput", "test:lint": "eslint src --ext .ts",
"test:throughput-sizes": "npm --prefix packages/reliability-tests run test:throughput-sizes", "test:prettier": "prettier \"src/**/*.ts\" \"./*.json\" --list-different",
"test:network-latency": "npm --prefix packages/reliability-tests run test:network-latency", "test:spelling": "cspell \"{README.md,.github/*.md,src/**/*.ts}\"",
"test:low-bandwidth": "npm --prefix packages/reliability-tests run test:low-bandwidth", "test:unit": "nyc --silent mocha",
"test:packet-loss": "npm --prefix packages/reliability-tests run test:packet-loss", "proto": "run-s proto:*",
"proto": "npm run proto --workspaces --if-present", "proto:lint": "buf lint",
"deploy": "node ci/deploy.js", "proto:build": "buf generate",
"doc": "run-s doc:*", "check-cli": "run-s test diff-integration-tests check-integration-tests",
"doc:html": "typedoc --options typedoc.cjs", "check-integration-tests": "run-s check-integration-test:*",
"doc:cname": "echo 'js.waku.org' > docs/CNAME", "diff-integration-tests": "mkdir -p diff && rm -rf diff/test && cp -r test diff/test && rm -rf diff/test/test-*/.git && cd diff && git init --quiet && git add -A && git commit --quiet --no-verify --allow-empty -m 'WIP' && echo '\\n\\nCommitted most recent integration test output in the \"diff\" directory. Review the changes with \"cd diff && git diff HEAD\" or your preferred git diff viewer.'",
"publish": "node ./ci/publish.js" "watch:build": "tsc -p tsconfig.json -w",
"watch:test": "nyc --silent mocha --watch",
"cov": "run-s build test:unit cov:html cov:lcov && open-cli coverage/index.html",
"cov:html": "nyc report --reporter=html",
"cov:lcov": "nyc report --reporter=lcov",
"cov:send": "run-s cov:lcov && codecov",
"cov:check": "nyc report && nyc check-coverage --lines 100 --functions 100 --branches 100",
"doc": "run-s doc:html && open-cli build/docs/index.html",
"doc:html": "typedoc --exclude **/*.spec.ts --out build/docs src/",
"doc:json": "typedoc src/ --exclude **/*.spec.ts --json build/docs/typedoc.json",
"doc:publish": "gh-pages -m \"[ci skip] Updates\" -d build/docs",
"version": "standard-version",
"reset-hard": "git clean -dfx && git reset --hard && npm i && npm run build && for d in examples/*; do (cd $d; npm i); done",
"prepare-release": "run-s reset-hard test cov:check doc:html version doc:publish"
},
"engines": {
"node": ">=14"
},
"dependencies": {
"@bitauth/libauth": "^1.17.1",
"debug": "^4.3.1",
"it-concat": "^2.0.0",
"it-length-prefixed": "^5.0.2",
"libp2p": "^0.31.0",
"libp2p-gossipsub": "^0.9.0",
"libp2p-mplex": "^0.10.3",
"libp2p-noise": "^3.0.0",
"libp2p-tcp": "^0.15.4",
"libp2p-websockets": "^0.15.6",
"multiaddr": "^9.0.1",
"prompt-sync": "^4.2.0",
"ts-proto": "^1.79.7",
"uuid": "^8.3.2",
"yarg": "^1.0.8"
}, },
"devDependencies": { "devDependencies": {
"@size-limit/preset-big-lib": "^11.0.2", "@istanbuljs/nyc-config-typescript": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^6.6.0", "@types/app-root-path": "^1.2.4",
"@typescript-eslint/parser": "^6.21.0", "@types/axios": "^0.14.0",
"eslint": "^8.56.0", "@types/chai": "^4.2.15",
"eslint-config-prettier": "^9.0.0", "@types/google-protobuf": "^3.7.4",
"@types/mocha": "^8.2.2",
"@types/node": "^14.14.31",
"@types/tail": "^2.0.0",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"app-root-path": "^3.0.0",
"axios": "^0.21.1",
"chai": "^4.3.4",
"codecov": "^3.5.0",
"cspell": "^4.1.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^6.0.1", "eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^5.1.3", "fast-check": "^2.14.0",
"gh-pages": "^6.1.1", "gh-pages": "^3.1.0",
"husky": "^9.0.11", "mocha": "^8.3.2",
"karma": "^6.4.2", "npm-run-all": "^4.1.5",
"karma-chrome-launcher": "^3.2.0", "nyc": "^15.1.0",
"karma-firefox-launcher": "^2.1.3", "open-cli": "^6.0.1",
"karma-mocha": "^2.0.1", "p-timeout": "^4.1.0",
"karma-webkit-launcher": "^2.4.0", "prettier": "^2.1.1",
"karma-webpack": "github:codymikol/karma-webpack#2337a82beb078c0d8e25ae8333a06249b8e72828", "standard-version": "^9.0.0",
"lint-staged": "^15.4.3", "tail": "^2.2.0",
"playwright": "^1.40.1", "ts-node": "^9.1.1",
"size-limit": "^11.0.1", "typedoc": "^0.20.29",
"ts-loader": "9.5.2", "typescript": "^4.0.2"
"ts-node": "10.9.2",
"typedoc": "0.28.5",
"typescript": "5.8.3",
"wscat": "^6.0.1"
}, },
"lint-staged": { "files": [
"*.{ts,js}": [ "build/main",
"eslint --fix" "build/module",
"!**/*.spec.*",
"!**/*.json",
"CHANGELOG.md",
"LICENSE",
"README.md"
],
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"prettier": {
"singleQuote": true
},
"nyc": {
"extends": "@istanbuljs/nyc-config-typescript",
"exclude": [
"**/*.spec.js"
] ]
}, }
"version": ""
} }

View File

@ -1,4 +0,0 @@
node_modules
build
.DS_Store
*.log

View File

@ -1,3 +0,0 @@
EXAMPLE_TEMPLATE="headless"
EXAMPLE_NAME="headless"
EXAMPLE_PORT="8080"

View File

@ -1,45 +0,0 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 2022,
sourceType: "module"
},
env: {
node: true,
browser: true,
es2021: true
},
plugins: ["import"],
extends: ["eslint:recommended"],
rules: {
"no-unused-vars": ["error", { "argsIgnorePattern": "^_", "ignoreRestSiblings": true }]
},
globals: {
process: true
},
overrides: [
{
files: ["*.spec.ts", "**/test_utils/*.ts"],
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"no-console": "off",
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
}
},
{
files: ["*.ts"],
parser: "@typescript-eslint/parser",
parserOptions: {
tsconfigRootDir: __dirname,
project: "./tsconfig.dev.json"
}
},
{
files: ["*.d.ts"],
rules: {
"no-unused-vars": "off"
}
}
]
};

View File

@ -1,72 +0,0 @@
# syntax=docker/dockerfile:1
# Build stage - install all dependencies and build
FROM node:22-bullseye AS builder
WORKDIR /app
# Copy package.json and temporarily remove workspace dependencies that can't be resolved
COPY package.json package.json.orig
RUN sed '/"@waku\/tests": "\*",/d' package.json.orig > package.json
RUN npm install --no-audit --no-fund
COPY src ./src
COPY types ./types
COPY tsconfig.json ./
COPY web ./web
RUN npm run build
# Production stage - only runtime dependencies
FROM node:22-bullseye
# Install required system deps for Playwright Chromium
RUN apt-get update && apt-get install -y \
wget \
gnupg \
ca-certificates \
fonts-liberation \
libatk-bridge2.0-0 \
libatk1.0-0 \
libatspi2.0-0 \
libcups2 \
libdbus-1-3 \
libdrm2 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxkbcommon0 \
libxrandr2 \
xdg-utils \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy package files and install only production dependencies
COPY package.json package.json.orig
RUN sed '/"@waku\/tests": "\*",/d' package.json.orig > package.json
RUN npm install --only=production --no-audit --no-fund
# Copy built application from builder stage
COPY --from=builder /app/dist ./dist
# Install Playwright browsers (Chromium only) at runtime layer
RUN npx playwright install --with-deps chromium
ENV PORT=8080 \
NODE_ENV=production
EXPOSE 8080
# Use a script to handle CLI arguments and environment variables
COPY scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["npm", "run", "start:server"]

View File

@ -1,174 +0,0 @@
# Waku Browser Tests
This package provides a containerized Waku light node simulation server for testing and development. The server runs a headless browser using Playwright and exposes a REST API similar to the nwaku REST API. A Dockerfile is provided to allow programmatic simulation and "deployment" of js-waku nodes in any Waku orchestration environment that uses Docker (e.g. [10ksim](https://github.com/vacp2p/10ksim) ).
## Quick Start
### Build and Run
```bash
# Build the application
npm run build
# Start the server (port 8080)
npm run start:server
# Build and run Docker container
npm run docker:build
docker run -p 8080:8080 waku-browser-tests:local
```
## Configuration
Configure the Waku node using environment variables:
### Network Configuration
- `WAKU_CLUSTER_ID`: Cluster ID (default: 1)
- `WAKU_SHARD`: Specific shard number - enables static sharding mode (optional)
**Sharding Behavior:**
- **Auto-sharding** (default): Uses `numShardsInCluster: 8` across cluster 1
- **Static sharding**: When `WAKU_SHARD` is set, uses only that specific shard
### Bootstrap Configuration
- `WAKU_ENR_BOOTSTRAP`: Enable ENR bootstrap mode with custom bootstrap peers (comma-separated)
- `WAKU_LIGHTPUSH_NODE`: Preferred lightpush node multiaddr (Docker only)
### ENR Bootstrap Mode
When `WAKU_ENR_BOOTSTRAP` is set:
- Disables default bootstrap (`defaultBootstrap: false`)
- Enables DNS discovery using production ENR trees
- Enables peer exchange and peer cache
- Uses the specified ENR for additional bootstrap peers
```bash
# Example: ENR bootstrap mode
WAKU_ENR_BOOTSTRAP="enr:-QEnuEBEAyErHEfhiQxAVQoWowGTCuEF9fKZtXSd7H_PymHFhGJA3rGAYDVSHKCyJDGRLBGsloNbS8AZF33IVuefjOO6BIJpZIJ2NIJpcIQS39tkim11bHRpYWRkcnO4lgAvNihub2RlLTAxLmRvLWFtczMud2FrdXYyLnRlc3Quc3RhdHVzaW0ubmV0BgG73gMAODcxbm9kZS0wMS5hYy1jbi1ob25na29uZy1jLndha3V2Mi50ZXN0LnN0YXR1c2ltLm5ldAYBu94DACm9A62t7AQL4Ef5ZYZosRpQTzFVAB8jGjf1TER2wH-0zBOe1-MDBNLeA4lzZWNwMjU2azGhAzfsxbxyCkgCqq8WwYsVWH7YkpMLnU2Bw5xJSimxKav-g3VkcIIjKA" npm run start:server
```
## API Endpoints
The server exposes the following HTTP endpoints:
### Node Management
- `GET /`: Health check - returns server status
- `GET /waku/v1/peer-info`: Get node peer information
- `POST /waku/v1/wait-for-peers`: Wait for peers with specific protocols
### Messaging
- `POST /lightpush/v3/message`: Send message via lightpush
### Static Files
- `GET /app/index.html`: Web application entry point
- `GET /app/*`: Static web application files
### Examples
#### Send a Message (Auto-sharding)
```bash
curl -X POST http://localhost:8080/lightpush/v3/message \
-H "Content-Type: application/json" \
-d '{
"pubsubTopic": "",
"message": {
"contentTopic": "/test/1/example/proto",
"payload": "SGVsbG8gV2FrdQ==",
"version": 1
}
}'
```
#### Send a Message (Explicit pubsub topic)
```bash
curl -X POST http://localhost:8080/lightpush/v3/message \
-H "Content-Type: application/json" \
-d '{
"pubsubTopic": "/waku/2/rs/1/4",
"message": {
"contentTopic": "/test/1/example/proto",
"payload": "SGVsbG8gV2FrdQ==",
"version": 1
}
}'
```
#### Wait for Peers
```bash
curl -X POST http://localhost:8080/waku/v1/wait-for-peers \
-H "Content-Type: application/json" \
-d '{
"timeoutMs": 30000,
"protocols": ["lightpush", "filter"]
}'
```
#### Get Peer Info
```bash
curl -X GET http://localhost:8080/waku/v1/peer-info
```
## CLI Usage
Run with CLI arguments:
```bash
# Custom cluster and shard
node dist/src/server.js --cluster-id=2 --shard=0
```
## Testing
The package includes several test suites:
```bash
# Basic server functionality tests (default)
npm test
# Docker testing workflow
npm run docker:build
npm run test:integration
# All tests
npm run test:all
# Individual test suites:
npm run test:server # Server-only tests
npm run test:e2e # End-to-end tests
```
**Test Types:**
- `server.spec.ts` - Tests basic server functionality and static file serving
- `integration.spec.ts` - Tests Docker container integration with external services
- `e2e.spec.ts` - Full end-to-end tests using nwaku nodes
## Docker Usage
The package includes Docker support for containerized testing:
```bash
# Build image
docker build -t waku-browser-tests:local .
# Run with ENR bootstrap
docker run -p 8080:8080 \
-e WAKU_ENR_BOOTSTRAP="enr:-QEnuE..." \
-e WAKU_CLUSTER_ID="1" \
waku-browser-tests:local
# Run with specific configuration
docker run -p 8080:8080 \
-e WAKU_CLUSTER_ID="2" \
-e WAKU_SHARD="0" \
waku-browser-tests:local
```
## Development
The server automatically:
- Creates a Waku light node on startup
- Configures network settings from environment variables
- Enables appropriate protocols (lightpush, filter)
- Handles peer discovery and connection management
All endpoints are CORS-enabled for cross-origin requests.

View File

@ -1,42 +0,0 @@
{
"name": "@waku/browser-tests",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "npm run start:server",
"start:server": "PORT=8080 node ./dist/src/server.js",
"test": "npx playwright test tests/server.spec.ts --reporter=line",
"test:all": "npx playwright test --reporter=line",
"test:server": "npx playwright test tests/server.spec.ts --reporter=line",
"test:integration": "npx playwright test tests/integration.spec.ts --reporter=line",
"test:e2e": "npx playwright test tests/e2e.spec.ts --reporter=line",
"build:server": "tsc -p tsconfig.json",
"build:web": "esbuild web/index.ts --bundle --format=esm --platform=browser --outdir=dist/web && cp web/index.html dist/web/index.html",
"build": "npm-run-all -s build:server build:web",
"docker:build": "docker build -t waku-browser-tests:local . && docker tag waku-browser-tests:local waku-browser-tests:latest"
},
"dependencies": {
"@playwright/test": "^1.51.1",
"@waku/discovery": "^0.0.11",
"@waku/interfaces": "^0.0.33",
"@waku/sdk": "^0.0.34",
"@waku/utils": "0.0.27",
"cors": "^2.8.5",
"dotenv-flow": "^0.4.0",
"express": "^4.21.2",
"filter-obj": "^2.0.2",
"it-first": "^3.0.9"
},
"devDependencies": {
"@types/cors": "^2.8.15",
"@types/express": "^4.17.21",
"@types/node": "^20.10.0",
"@waku/tests": "*",
"axios": "^1.8.4",
"esbuild": "^0.21.5",
"npm-run-all": "^4.1.5",
"testcontainers": "^10.9.0",
"typescript": "5.8.3"
}
}

View File

@ -1,39 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
import { Logger } from "@waku/utils";
const log = new Logger("playwright-config");
if (!process.env.CI) {
try {
await import("dotenv-flow/config.js");
} catch (e) {
log.warn("dotenv-flow not found; skipping env loading");
}
}
const EXAMPLE_PORT = process.env.EXAMPLE_PORT || "8080";
const BASE_URL = `http://127.0.0.1:${EXAMPLE_PORT}`;
const TEST_IGNORE = process.env.CI ? ["tests/e2e.spec.ts"] : [];
export default defineConfig({
testDir: "./tests",
testIgnore: TEST_IGNORE,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: "html",
use: {
baseURL: BASE_URL,
trace: "on-first-retry"
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] }
}
]
});

View File

@ -1,54 +0,0 @@
#!/bin/bash
# Docker entrypoint script for waku-browser-tests
# Handles CLI arguments and converts them to environment variables
# Supports reading discovered addresses from /etc/addrs/addrs.env (10k sim pattern)
echo "docker-entrypoint.sh"
echo "Using address: $addrs1"
# Only set WAKU_LIGHTPUSH_NODE if it's not already set and addrs1 is available
if [ -z "$WAKU_LIGHTPUSH_NODE" ] && [ -n "$addrs1" ]; then
export WAKU_LIGHTPUSH_NODE="$addrs1"
fi
echo "Num Args: $#"
echo "Args: $@"
echo "WAKU_LIGHTPUSH_NODE=$WAKU_LIGHTPUSH_NODE"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--cluster-id=*)
export WAKU_CLUSTER_ID="${1#*=}"
echo "Setting WAKU_CLUSTER_ID=${WAKU_CLUSTER_ID}"
shift
;;
--shard=*)
export WAKU_SHARD="${1#*=}"
echo "Setting WAKU_SHARD=${WAKU_SHARD}"
shift
;;
--lightpushnode=*)
export WAKU_LIGHTPUSH_NODE="${1#*=}"
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, notify user and keep it for the main command
echo "Warning: Unknown argument '$1' will be passed to the main command"
break
;;
esac
done
# If no specific command is provided, use the default CMD
if [ $# -eq 0 ]; then
set -- "npm" "run" "start:server"
fi
# Execute the main command
exec "$@"

View File

@ -1,67 +0,0 @@
import { Browser, chromium, Page } from "@playwright/test";
import { Logger } from "@waku/utils";
const log = new Logger("browser-test");
let browser: Browser | undefined;
let page: Page | undefined;
export async function initBrowser(appPort: number): Promise<void> {
try {
const launchArgs = ["--no-sandbox", "--disable-setuid-sandbox"];
browser = await chromium.launch({
headless: true,
args: launchArgs
});
if (!browser) {
throw new Error("Failed to initialize browser");
}
page = await browser.newPage();
// Forward browser console to server logs
page.on('console', msg => {
const type = msg.type();
const text = msg.text();
log.info(`[Browser Console ${type.toUpperCase()}] ${text}`);
});
page.on('pageerror', error => {
log.error('[Browser Page Error]', error.message);
});
await page.goto(`http://localhost:${appPort}/app/index.html`, {
waitUntil: "networkidle",
});
await page.waitForFunction(
() => {
return window.wakuApi && typeof window.wakuApi.createWakuNode === "function";
},
{ timeout: 30000 }
);
log.info("Browser initialized successfully with wakuApi");
} catch (error) {
log.error("Error initializing browser:", error);
throw error;
}
}
export function getPage(): Page | undefined {
return page;
}
export function setPage(pageInstance: Page | undefined): void {
page = pageInstance;
}
export async function closeBrowser(): Promise<void> {
if (browser) {
await browser.close();
browser = undefined;
page = undefined;
}
}

View File

@ -1,87 +0,0 @@
import { Router } from "express";
import { Logger } from "@waku/utils";
import {
createEndpointHandler,
validators,
errorHandlers,
} from "../utils/endpoint-handler.js";
interface LightPushResult {
successes: string[];
failures: Array<{ error: string; peerId?: string }>;
}
const log = new Logger("routes:waku");
const router = Router();
const corsEndpoints = [
"/waku/v1/wait-for-peers",
"/waku/v1/peer-info",
"/lightpush/v3/message",
];
corsEndpoints.forEach((endpoint) => {
router.head(endpoint, (_req, res) => {
res.status(200).end();
});
});
router.post(
"/waku/v1/wait-for-peers",
createEndpointHandler({
methodName: "waitForPeers",
validateInput: (body: unknown) => {
const bodyObj = body as { timeoutMs?: number; protocols?: string[] };
return [
bodyObj.timeoutMs || 10000,
bodyObj.protocols || ["lightpush", "filter"],
];
},
transformResult: () => ({
success: true,
message: "Successfully connected to peers",
}),
}),
);
router.get(
"/waku/v1/peer-info",
createEndpointHandler({
methodName: "getPeerInfo",
validateInput: validators.noInput,
}),
);
router.post(
"/lightpush/v3/message",
createEndpointHandler({
methodName: "pushMessageV3",
validateInput: (body: unknown): [string, string, string] => {
const validatedRequest = validators.requireLightpushV3(body);
return [
validatedRequest.message.contentTopic,
validatedRequest.message.payload,
validatedRequest.pubsubTopic,
];
},
handleError: errorHandlers.lightpushError,
transformResult: (result: unknown) => {
const lightPushResult = result as LightPushResult;
if (lightPushResult && lightPushResult.successes && lightPushResult.successes.length > 0) {
log.info("[Server] Message successfully sent via v3 lightpush!");
return {
success: true,
result: lightPushResult,
};
} else {
return {
success: false,
error: "Could not publish message: no suitable peers",
};
}
},
}),
);
export default router;

View File

@ -1,244 +0,0 @@
import { fileURLToPath } from "url";
import * as path from "path";
import cors from "cors";
import express, { Request, Response } from "express";
import { Logger } from "@waku/utils";
import wakuRouter from "./routes/waku.js";
import { initBrowser, getPage, closeBrowser } from "./browser/index.js";
import {
DEFAULT_CLUSTER_ID,
DEFAULT_NUM_SHARDS,
Protocols,
AutoSharding,
StaticSharding,
} from "@waku/interfaces";
import { CreateNodeOptions } from "@waku/sdk";
import type { WindowNetworkConfig } from "../types/global.js";
interface NodeError extends Error {
code?: string;
}
const log = new Logger("server");
const app = express();
app.use(cors());
app.use(express.json());
import * as fs from "fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const distRoot = path.resolve(__dirname, "..");
const webDir = path.resolve(distRoot, "web");
app.get("/app/index.html", (_req: Request, res: Response) => {
try {
const htmlPath = path.join(webDir, "index.html");
let htmlContent = fs.readFileSync(htmlPath, "utf8");
const networkConfig: WindowNetworkConfig = {};
if (process.env.WAKU_CLUSTER_ID) {
networkConfig.clusterId = parseInt(process.env.WAKU_CLUSTER_ID, 10);
}
if (process.env.WAKU_SHARD) {
networkConfig.shards = [parseInt(process.env.WAKU_SHARD, 10)];
log.info("Using static shard:", networkConfig.shards);
}
const lightpushNode = process.env.WAKU_LIGHTPUSH_NODE || null;
const enrBootstrap = process.env.WAKU_ENR_BOOTSTRAP || null;
log.info("Network config on server start, pre headless:", networkConfig);
const configScript = ` <script>
window.__WAKU_NETWORK_CONFIG = ${JSON.stringify(networkConfig)};
window.__WAKU_LIGHTPUSH_NODE = ${JSON.stringify(lightpushNode)};
window.__WAKU_ENR_BOOTSTRAP = ${JSON.stringify(enrBootstrap)};
</script>`;
const originalPattern =
' <script type="module" src="./index.js"></script>';
const replacement = `${configScript}\n <script type="module" src="./index.js"></script>`;
htmlContent = htmlContent.replace(originalPattern, replacement);
res.setHeader("Content-Type", "text/html");
res.send(htmlContent);
} catch (error) {
log.error("Error serving dynamic index.html:", error);
res.status(500).send("Error loading page");
}
});
app.use("/app", express.static(webDir, { index: false }));
app.use(wakuRouter);
async function startAPI(requestedPort: number): Promise<number> {
try {
app.get("/", (_req: Request, res: Response) => {
res.json({ status: "Waku simulation server is running" });
});
app
.listen(requestedPort, () => {
log.info(`API server running on http://localhost:${requestedPort}`);
})
.on("error", (error: NodeError) => {
if (error.code === "EADDRINUSE") {
log.error(
`Port ${requestedPort} is already in use. Please close the application using this port and try again.`,
);
} else {
log.error("Error starting server:", error);
}
throw error;
});
return requestedPort;
} catch (error) {
log.error("Error starting server:", error);
throw error;
}
}
async function startServer(port: number = 3000): Promise<void> {
try {
const actualPort = await startAPI(port);
await initBrowser(actualPort);
try {
log.info("Auto-starting node with CLI configuration...");
const hasEnrBootstrap = Boolean(process.env.WAKU_ENR_BOOTSTRAP);
const networkConfig: AutoSharding | StaticSharding = process.env.WAKU_SHARD
? ({
clusterId: process.env.WAKU_CLUSTER_ID
? parseInt(process.env.WAKU_CLUSTER_ID, 10)
: DEFAULT_CLUSTER_ID,
shards: [parseInt(process.env.WAKU_SHARD, 10)],
} as StaticSharding)
: ({
clusterId: process.env.WAKU_CLUSTER_ID
? parseInt(process.env.WAKU_CLUSTER_ID, 10)
: DEFAULT_CLUSTER_ID,
numShardsInCluster: DEFAULT_NUM_SHARDS,
} as AutoSharding);
const createOptions: CreateNodeOptions = {
defaultBootstrap: false,
...(hasEnrBootstrap && {
discovery: {
dns: true,
peerExchange: true,
peerCache: true,
},
}),
networkConfig,
};
log.info(
`Bootstrap mode: ${hasEnrBootstrap ? "ENR-only (defaultBootstrap=false)" : "default bootstrap (defaultBootstrap=true)"}`,
);
if (hasEnrBootstrap) {
log.info(`ENR bootstrap peers: ${process.env.WAKU_ENR_BOOTSTRAP}`);
}
log.info(
`Network config: ${JSON.stringify(networkConfig)}`,
);
await getPage()?.evaluate((config) => {
return window.wakuApi.createWakuNode(config);
}, createOptions);
await getPage()?.evaluate(() => window.wakuApi.startNode());
try {
await getPage()?.evaluate(() =>
window.wakuApi.waitForPeers?.(5000, [Protocols.LightPush]),
);
log.info("Auto-start completed with bootstrap peers");
} catch (peerError) {
log.info(
"Auto-start completed (no bootstrap peers found - may be expected with test ENRs)",
);
}
} catch (e) {
log.warn("Auto-start failed:", e);
}
} catch (error) {
log.error("Error starting server:", error);
}
}
process.on("uncaughtException", (error) => {
log.error("Uncaught Exception:", error);
if (process.env.NODE_ENV !== "production") {
process.exit(1);
}
});
process.on("unhandledRejection", (reason, promise) => {
log.error("Unhandled Rejection at:", promise, "reason:", reason);
if (process.env.NODE_ENV !== "production") {
process.exit(1);
}
});
const gracefulShutdown = async (signal: string) => {
log.info(`Received ${signal}, gracefully shutting down...`);
try {
await closeBrowser();
} catch (e) {
log.warn("Error closing browser:", e);
}
process.exit(0);
};
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
function parseCliArgs() {
const args = process.argv.slice(2);
let clusterId: number | undefined;
let shard: number | undefined;
for (const arg of args) {
if (arg.startsWith("--cluster-id=")) {
clusterId = parseInt(arg.split("=")[1], 10);
if (isNaN(clusterId)) {
log.error("Invalid cluster-id value. Must be a number.");
process.exit(1);
}
} else if (arg.startsWith("--shard=")) {
shard = parseInt(arg.split("=")[1], 10);
if (isNaN(shard)) {
log.error("Invalid shard value. Must be a number.");
process.exit(1);
}
}
}
return { clusterId, shard };
}
const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
if (isMainModule) {
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
const cliArgs = parseCliArgs();
if (cliArgs.clusterId !== undefined) {
process.env.WAKU_CLUSTER_ID = cliArgs.clusterId.toString();
log.info(`Using CLI cluster ID: ${cliArgs.clusterId}`);
}
if (cliArgs.shard !== undefined) {
process.env.WAKU_SHARD = cliArgs.shard.toString();
log.info(`Using CLI shard: ${cliArgs.shard}`);
}
void startServer(port);
}

View File

@ -1,197 +0,0 @@
import { Request, Response } from "express";
import { Logger } from "@waku/utils";
import { getPage } from "../browser/index.js";
import type { ITestBrowser } from "../../types/global.js";
const log = new Logger("endpoint-handler");
export interface LightpushV3Request {
pubsubTopic: string;
message: {
payload: string;
contentTopic: string;
version: number;
};
}
export interface LightpushV3Response {
success?: boolean;
error?: string;
result?: {
successes: string[];
failures: Array<{
error: string;
peerId?: string;
}>;
};
}
export interface EndpointConfig<TInput = unknown, TOutput = unknown> {
methodName: string;
validateInput?: (_requestBody: unknown) => TInput;
transformResult?: (_sdkResult: unknown) => TOutput;
handleError?: (_caughtError: Error) => { code: number; message: string };
preCheck?: () => Promise<void> | void;
logResult?: boolean;
}
export function createEndpointHandler<TInput = unknown, TOutput = unknown>(
config: EndpointConfig<TInput, TOutput>,
) {
return async (req: Request, res: Response) => {
try {
let input: TInput;
try {
input = config.validateInput
? config.validateInput(req.body)
: req.body;
} catch (validationError) {
return res.status(400).json({
code: 400,
message: `Invalid input: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
});
}
if (config.preCheck) {
try {
await config.preCheck();
} catch (checkError) {
return res.status(503).json({
code: 503,
message: checkError instanceof Error ? checkError.message : String(checkError),
});
}
}
const page = getPage();
if (!page) {
return res.status(503).json({
code: 503,
message: "Browser not initialized",
});
}
const result = await page.evaluate(
({ methodName, params }) => {
const testWindow = window as ITestBrowser;
if (!testWindow.wakuApi) {
throw new Error("window.wakuApi is not available");
}
const wakuApi = testWindow.wakuApi as unknown as Record<string, unknown>;
const method = wakuApi[methodName];
if (typeof method !== "function") {
throw new Error(`window.wakuApi.${methodName} is not a function`);
}
if (params === null || params === undefined) {
return method.call(testWindow.wakuApi);
} else if (Array.isArray(params)) {
return method.apply(testWindow.wakuApi, params);
} else {
return method.call(testWindow.wakuApi, params);
}
},
{ methodName: config.methodName, params: input },
);
if (config.logResult !== false) {
log.info(
`[${config.methodName}] Result:`,
JSON.stringify(result, null, 2),
);
}
const finalResult = config.transformResult
? config.transformResult(result)
: result;
res.status(200).json(finalResult);
} catch (error) {
if (config.handleError) {
const errorResponse = config.handleError(error as Error);
return res.status(errorResponse.code).json({
code: errorResponse.code,
message: errorResponse.message,
});
}
log.error(`[${config.methodName}] Error:`, error);
res.status(500).json({
code: 500,
message: `Could not execute ${config.methodName}: ${error instanceof Error ? error.message : String(error)}`,
});
}
};
}
export const validators = {
requireLightpushV3: (body: unknown): LightpushV3Request => {
// Type guard to check if body is an object
if (!body || typeof body !== "object") {
throw new Error("Request body must be an object");
}
const bodyObj = body as Record<string, unknown>;
if (
bodyObj.pubsubTopic !== undefined &&
typeof bodyObj.pubsubTopic !== "string"
) {
throw new Error("pubsubTopic must be a string if provided");
}
if (!bodyObj.message || typeof bodyObj.message !== "object") {
throw new Error("message is required and must be an object");
}
const message = bodyObj.message as Record<string, unknown>;
if (
!message.contentTopic ||
typeof message.contentTopic !== "string"
) {
throw new Error("message.contentTopic is required and must be a string");
}
if (!message.payload || typeof message.payload !== "string") {
throw new Error(
"message.payload is required and must be a string (base64 encoded)",
);
}
if (
message.version !== undefined &&
typeof message.version !== "number"
) {
throw new Error("message.version must be a number if provided");
}
return {
pubsubTopic: (bodyObj.pubsubTopic as string) || "",
message: {
payload: message.payload as string,
contentTopic: message.contentTopic as string,
version: (message.version as number) || 1,
},
};
},
noInput: () => null,
};
export const errorHandlers = {
lightpushError: (error: Error) => {
if (
error.message.includes("size exceeds") ||
error.message.includes("stream reset")
) {
return {
code: 503,
message:
"Could not publish message: message size exceeds gossipsub max message size",
};
}
return {
code: 500,
message: `Could not publish message: ${error.message}`,
};
},
};

View File

@ -1,117 +0,0 @@
import { test, expect } from "@playwright/test";
import axios from "axios";
import { StartedTestContainer } from "testcontainers";
import { DefaultTestRoutingInfo } from "@waku/tests";
import {
startBrowserTestsContainer,
stopContainer
} from "./utils/container-helpers.js";
import {
createTwoNodeNetwork,
getDockerAccessibleMultiaddr,
stopNwakuNodes,
TwoNodeNetwork
} from "./utils/nwaku-helpers.js";
import {
ENV_BUILDERS,
TEST_CONFIG,
ASSERTIONS
} from "./utils/test-config.js";
test.describe.configure({ mode: "serial" });
let container: StartedTestContainer;
let nwakuNodes: TwoNodeNetwork;
let baseUrl: string;
test.beforeAll(async () => {
nwakuNodes = await createTwoNodeNetwork();
const lightPushPeerAddr = await getDockerAccessibleMultiaddr(nwakuNodes.nodes[0]);
const result = await startBrowserTestsContainer({
environment: {
...ENV_BUILDERS.withLocalLightPush(lightPushPeerAddr),
DEBUG: "waku:*",
WAKU_LIGHTPUSH_NODE: lightPushPeerAddr,
},
networkMode: "waku",
});
container = result.container;
baseUrl = result.baseUrl;
});
test.afterAll(async () => {
await Promise.all([
stopContainer(container),
stopNwakuNodes(nwakuNodes?.nodes || []),
]);
});
test("WakuHeadless can discover nwaku peer and use it for light push", async () => {
test.setTimeout(TEST_CONFIG.DEFAULT_TEST_TIMEOUT);
const contentTopic = TEST_CONFIG.DEFAULT_CONTENT_TOPIC;
const testMessage = TEST_CONFIG.DEFAULT_TEST_MESSAGE;
await new Promise((r) => setTimeout(r, TEST_CONFIG.WAKU_INIT_DELAY));
const healthResponse = await axios.get(`${baseUrl}/`, { timeout: 5000 });
ASSERTIONS.serverHealth(healthResponse);
try {
await axios.post(`${baseUrl}/waku/v1/wait-for-peers`, {
timeoutMs: 10000,
protocols: ["lightpush"],
}, { timeout: 15000 });
} catch {
// Ignore errors
}
const peerInfoResponse = await axios.get(`${baseUrl}/waku/v1/peer-info`);
ASSERTIONS.peerInfo(peerInfoResponse);
const routingInfo = DefaultTestRoutingInfo;
const subscriptionResults = await Promise.all([
nwakuNodes.nodes[0].ensureSubscriptions([routingInfo.pubsubTopic]),
nwakuNodes.nodes[1].ensureSubscriptions([routingInfo.pubsubTopic])
]);
expect(subscriptionResults[0]).toBe(true);
expect(subscriptionResults[1]).toBe(true);
await new Promise((r) => setTimeout(r, TEST_CONFIG.SUBSCRIPTION_DELAY));
const base64Payload = btoa(testMessage);
const pushResponse = await axios.post(`${baseUrl}/lightpush/v3/message`, {
pubsubTopic: routingInfo.pubsubTopic,
message: {
contentTopic,
payload: base64Payload,
version: 1,
},
});
ASSERTIONS.lightPushV3Success(pushResponse);
await new Promise((r) => setTimeout(r, TEST_CONFIG.MESSAGE_PROPAGATION_DELAY));
const [node1Messages, node2Messages] = await Promise.all([
nwakuNodes.nodes[0].messages(contentTopic),
nwakuNodes.nodes[1].messages(contentTopic)
]);
const totalMessages = node1Messages.length + node2Messages.length;
expect(totalMessages).toBeGreaterThanOrEqual(1);
const receivedMessages = [...node1Messages, ...node2Messages];
expect(receivedMessages.length).toBeGreaterThan(0);
const receivedMessage = receivedMessages[0];
ASSERTIONS.messageContent(receivedMessage, testMessage, contentTopic);
});

View File

@ -1,134 +0,0 @@
import { test, expect } from "@playwright/test";
import axios from "axios";
import { StartedTestContainer } from "testcontainers";
import {
createLightNode,
LightNode,
Protocols,
IDecodedMessage,
} from "@waku/sdk";
import { DEFAULT_CLUSTER_ID, DEFAULT_NUM_SHARDS } from "@waku/interfaces";
import { startBrowserTestsContainer, stopContainer } from "./utils/container-helpers.js";
import { ENV_BUILDERS, TEST_CONFIG } from "./utils/test-config.js";
test.describe.configure({ mode: "serial" });
let container: StartedTestContainer;
let baseUrl: string;
let wakuNode: LightNode;
test.beforeAll(async () => {
const result = await startBrowserTestsContainer({
environment: {
...ENV_BUILDERS.withProductionEnr(),
DEBUG: "waku:*",
},
});
container = result.container;
baseUrl = result.baseUrl;
});
test.afterAll(async () => {
if (wakuNode) {
try {
await wakuNode.stop();
} catch {
// Ignore errors
}
}
await stopContainer(container);
});
test("cross-network message delivery: SDK light node receives server lightpush", async () => {
test.setTimeout(TEST_CONFIG.DEFAULT_TEST_TIMEOUT);
const contentTopic = TEST_CONFIG.DEFAULT_CONTENT_TOPIC;
const testMessage = TEST_CONFIG.DEFAULT_TEST_MESSAGE;
wakuNode = await createLightNode({
defaultBootstrap: true,
discovery: {
dns: true,
peerExchange: true,
peerCache: true,
},
networkConfig: {
clusterId: DEFAULT_CLUSTER_ID,
numShardsInCluster: DEFAULT_NUM_SHARDS,
},
libp2p: {
filterMultiaddrs: false,
},
});
await wakuNode.start();
await wakuNode.waitForPeers(
[Protocols.Filter, Protocols.LightPush],
30000,
);
const messages: IDecodedMessage[] = [];
const decoder = wakuNode.createDecoder({ contentTopic });
if (
!(await wakuNode.filter.subscribe([decoder], (message) => {
messages.push(message);
}))
) {
throw new Error("Failed to subscribe to Filter");
}
await new Promise((r) => setTimeout(r, 2000));
const messagePromise = new Promise<void>((resolve) => {
const originalLength = messages.length;
const checkForMessage = () => {
if (messages.length > originalLength) {
resolve();
} else {
setTimeout(checkForMessage, 100);
}
};
checkForMessage();
});
await axios.post(`${baseUrl}/waku/v1/wait-for-peers`, {
timeoutMs: 30000, // Increased timeout
protocols: ["lightpush", "filter"],
});
await new Promise((r) => setTimeout(r, 10000));
const base64Payload = btoa(testMessage);
const pushResponse = await axios.post(`${baseUrl}/lightpush/v3/message`, {
pubsubTopic: decoder.pubsubTopic,
message: {
contentTopic,
payload: base64Payload,
version: 1,
},
});
expect(pushResponse.status).toBe(200);
expect(pushResponse.data.success).toBe(true);
await Promise.race([
messagePromise,
new Promise((_, reject) =>
setTimeout(() => {
reject(new Error("Timeout waiting for message"));
}, 45000),
),
]);
expect(messages).toHaveLength(1);
const receivedMessage = messages[0];
expect(receivedMessage.contentTopic).toBe(contentTopic);
const receivedPayload = new TextDecoder().decode(receivedMessage.payload);
expect(receivedPayload).toBe(testMessage);
});

View File

@ -1,82 +0,0 @@
import { test, expect } from "@playwright/test";
import axios from "axios";
import { spawn, ChildProcess } from "child_process";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
test.describe.configure({ mode: "serial" });
test.describe("Server Tests", () => {
let serverProcess: ChildProcess;
let baseUrl = "http://localhost:3000";
test.beforeAll(async () => {
const serverPath = join(__dirname, "..", "dist", "src", "server.js");
serverProcess = spawn("node", [serverPath], {
stdio: "pipe",
env: { ...process.env, PORT: "3000" }
});
serverProcess.stdout?.on("data", (_data: Buffer) => {
});
serverProcess.stderr?.on("data", (_data: Buffer) => {
});
await new Promise((resolve) => setTimeout(resolve, 3000));
let serverReady = false;
for (let i = 0; i < 30; i++) {
try {
const res = await axios.get(`${baseUrl}/`, { timeout: 2000 });
if (res.status === 200) {
serverReady = true;
break;
}
} catch {
// Ignore errors
}
await new Promise((r) => setTimeout(r, 1000));
}
expect(serverReady).toBe(true);
});
test.afterAll(async () => {
if (serverProcess) {
serverProcess.kill("SIGTERM");
await new Promise((resolve) => setTimeout(resolve, 1000));
}
});
test("server 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("static files are served", async () => {
const htmlRes = await axios.get(`${baseUrl}/app/index.html`);
expect(htmlRes.status).toBe(200);
expect(htmlRes.data).toContain("Waku Test Environment");
const jsRes = await axios.get(`${baseUrl}/app/index.js`);
expect(jsRes.status).toBe(200);
expect(jsRes.data).toContain("WakuHeadless");
});
test("Waku node auto-started", async () => {
try {
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) {
expect(error.response?.status).toBe(400);
}
});
});

View File

@ -1,128 +0,0 @@
import axios from "axios";
import { GenericContainer, StartedTestContainer } from "testcontainers";
import { Logger } from "@waku/utils";
const log = new Logger("container-helpers");
export interface ContainerSetupOptions {
environment?: Record<string, string>;
networkMode?: string;
timeout?: number;
maxAttempts?: number;
}
export interface ContainerSetupResult {
container: StartedTestContainer;
baseUrl: string;
}
/**
* Starts a waku-browser-tests Docker container with proper health checking.
* Follows patterns from @waku/tests package for retry logic and cleanup.
*/
export async function startBrowserTestsContainer(
options: ContainerSetupOptions = {}
): Promise<ContainerSetupResult> {
const {
environment = {},
networkMode = "bridge",
timeout = 2000,
maxAttempts = 60
} = options;
log.info("Starting waku-browser-tests container...");
let generic = new GenericContainer("waku-browser-tests:local")
.withExposedPorts(8080)
.withNetworkMode(networkMode);
// Apply environment variables
for (const [key, value] of Object.entries(environment)) {
generic = generic.withEnvironment({ [key]: value });
}
const container = await generic.start();
// Set up container logging - stream all output from the start
const logs = await container.logs();
logs.on("data", (b) => process.stdout.write("[container] " + b.toString()));
logs.on("error", (err) => log.error("[container log error]", err));
// Give container time to initialize
await new Promise((r) => setTimeout(r, 5000));
const mappedPort = container.getMappedPort(8080);
const baseUrl = `http://127.0.0.1:${mappedPort}`;
// Wait for server readiness with retry logic (following waku/tests patterns)
const serverReady = await waitForServerReady(baseUrl, maxAttempts, timeout);
if (!serverReady) {
await logFinalContainerState(container);
throw new Error("Container failed to become ready");
}
log.info("✅ Browser tests container ready");
await new Promise((r) => setTimeout(r, 500)); // Final settling time
return { container, baseUrl };
}
/**
* Waits for server to become ready with exponential backoff and detailed logging.
* Follows retry patterns from @waku/tests ServiceNode.
*/
async function waitForServerReady(
baseUrl: string,
maxAttempts: number,
timeout: number
): Promise<boolean> {
for (let i = 0; i < maxAttempts; i++) {
try {
const res = await axios.get(`${baseUrl}/`, { timeout });
if (res.status === 200) {
log.info(`Server is ready after ${i + 1} attempts`);
return true;
}
} catch (error) {
if (i % 10 === 0) {
log.info(`Attempt ${i + 1}/${maxAttempts} failed:`, error.code || error.message);
}
}
await new Promise((r) => setTimeout(r, 1000));
}
return false;
}
/**
* Logs final container state for debugging, following waku/tests error handling patterns.
*/
async function logFinalContainerState(container: StartedTestContainer): Promise<void> {
try {
const finalLogs = await container.logs({ tail: 50 });
log.info("=== Final Container Logs ===");
finalLogs.on("data", (b) => log.info(b.toString()));
await new Promise((r) => setTimeout(r, 1000));
} catch (logError) {
log.error("Failed to get container logs:", logError);
}
}
/**
* Gracefully stops containers with retry logic, following teardown patterns from waku/tests.
*/
export async function stopContainer(container: StartedTestContainer): Promise<void> {
if (!container) return;
log.info("Stopping container gracefully...");
try {
await container.stop({ timeout: 10000 });
log.info("Container stopped successfully");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.warn(
"Container stop had issues (expected):",
message
);
}
}

Some files were not shown because too many files have changed in this diff Show More