diff --git a/.size-limit.cjs b/.size-limit.cjs index d7b648f668..370ec75ddc 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -2,13 +2,13 @@ module.exports = [ { name: "Waku core", path: "bundle/index.js", - import: "{ Waku }", + import: "{ WakuNode }", }, { name: "Waku default setup", path: ["bundle/index.js", "bundle/lib/create_waku.js"], import: { - "./bundle/lib/create_waku.js": "{ createWaku }", + "./bundle/lib/create_waku.js": "{ createLightNode }", "./bundle/lib/wait_for_remote_peer.js": "{ waitForRemotePeer }", }, }, diff --git a/CHANGELOG.md b/CHANGELOG.md index d003dcae7d..f2b4835de2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Simple connection management that selects the most recent connection for store, light push and filter requests. +- `createLightNode` to create a Waku node for resource restricted environment with Light Push, Filter and Relay. +- `createPrivacyNode` to create a Waku node for privacy preserving usage with Relay only. +- `createFullNode` to create a Waku node for with all protocols, for **testing purposes only**. ### Changed - **breaking**: `DecryptionParams` may be passed when using `queryHistory` instead of just keys. - Examples have been moved to https://github.com/waku-org/js-waku-examples. +- `Waku` is now defined as an interface with `WakuNode` an implementation of it. +- `createWaku` is deprecated in favour of `createLightNode` and `createPrivacyNode`. +- `waitForRemotePeer` can throw, default behaviour has changed. ### Fixed diff --git a/package-lock.json b/package-lock.json index 6e01acfe5c..4a99a45e9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@libp2p/interface-peer-id": "^1.0.2", "@libp2p/interface-peer-info": "^1.0.1", "@libp2p/interface-peer-store": "^1.0.0", + "@libp2p/interface-pubsub": "^2.0.1", "@libp2p/interfaces": "^3.0.2", "@libp2p/mplex": "^5.1.1", "@libp2p/peer-id": "^1.1.10", diff --git a/package.json b/package.json index 4eaa56393b..2fc3f7f4cf 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "types": "./dist/lib/create_waku.d.ts", "import": "./dist/lib/create_waku.js" }, + "./lib/interfaces": { + "types": "./dist/lib/interfaces.d.ts" + }, "./lib/peer_discovery_dns": { "types": "./dist/lib/peer_discovery_dns/index.d.ts", "import": "./dist/lib/peer_discovery_dns/index.js" @@ -60,7 +63,7 @@ "pretest": "run-s pretest:*", "pretest:1-init-git-submodules": "[ -f './nwaku/build/wakunode2' ] || git submodule update --init --recursive", "pretest:2-build-nwaku": "[ -f './nwaku/build/wakunode2' ] || run-s nwaku:build", - "nwaku:build": "(cd nwaku; NIMFLAGS=\"-d:chronicles_colors=off -d:chronicles_sinks=textlines -d:chronicles_log_level=TRACE\" make -j$(nproc --all 2>/dev/null || echo 2) wakunode2)", + "nwaku:build": "(cd nwaku; make update; NIMFLAGS=\"-d:chronicles_colors=off -d:chronicles_sinks=textlines -d:chronicles_log_level=TRACE\" make -j$(nproc --all 2>/dev/null || echo 2) wakunode2)", "nwaku:force-build": "(cd nwaku && rm -rf ./build/ ./vendor && make -j$(nproc --all 2>/dev/null || echo 2) update) && run-s nwaku:build", "test": "run-s test:*", "test:lint": "eslint src --ext .ts", @@ -95,6 +98,7 @@ "@libp2p/interface-peer-id": "^1.0.2", "@libp2p/interface-peer-info": "^1.0.1", "@libp2p/interface-peer-store": "^1.0.0", + "@libp2p/interface-pubsub": "^2.0.1", "@libp2p/interfaces": "^3.0.2", "@libp2p/mplex": "^5.1.1", "@libp2p/peer-id": "^1.1.10", diff --git a/src/index.ts b/src/index.ts index 516a1e7cae..60078fbb9b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ export * as utils from "./lib/utils"; export * as proto_message from "./proto/message"; export * as waku from "./lib/waku"; -export { Waku, Protocols } from "./lib/waku"; +export { WakuNode, Protocols } from "./lib/waku"; export * as waku_message from "./lib/waku_message"; export { WakuMessage } from "./lib/waku_message"; diff --git a/src/lib/create_waku.ts b/src/lib/create_waku.ts index 3605273b56..c11476c41b 100644 --- a/src/lib/create_waku.ts +++ b/src/lib/create_waku.ts @@ -6,9 +6,10 @@ import { all as filterAll } from "@libp2p/websockets/filters"; import { createLibp2p, Libp2pOptions } from "libp2p"; import type { Libp2p } from "libp2p"; +import type { Waku, WakuFull, WakuLight, WakuPrivacy } from "./interfaces"; import { PeerDiscoveryStaticPeers } from "./peer_discovery_static_list"; import { getPredefinedBootstrapNodes } from "./predefined_bootstrap_nodes"; -import { Waku, WakuOptions } from "./waku"; +import { WakuNode, WakuOptions } from "./waku"; import { WakuFilter } from "./waku_filter"; import { WakuLightPush } from "./waku_light_push"; import { WakuRelay } from "./waku_relay"; @@ -30,11 +31,11 @@ export interface CreateOptions { */ pubSubTopic?: string; /** - * You can pass options to the `Libp2p` instance used by {@link index.waku.Waku} using the {@link CreateOptions.libp2p} property. + * You can pass options to the `Libp2p` instance used by {@link index.waku.WakuNode} using the {@link CreateOptions.libp2p} property. * This property is the same type than the one passed to [`Libp2p.create`](https://github.com/libp2p/js-libp2p/blob/master/doc/API.md#create) * apart that we made the `modules` property optional and partial, * allowing its omission and letting Waku set good defaults. - * Notes that some values are overridden by {@link index.waku.Waku} to ensure it implements the Waku protocol. + * Notes that some values are overridden by {@link index.waku.WakuNode} to ensure it implements the Waku protocol. */ libp2p?: Partial; /** @@ -49,6 +50,99 @@ export interface CreateOptions { defaultBootstrap?: boolean; } +/** + * Create a Waku node that uses Waku Light Push, Filter and Store to send and + * receive messages, enabling low resource consumption. + * **Note: This is NOT compatible with nwaku v0.11** + * + * @see https://github.com/status-im/nwaku/issues/1085 + */ +export async function createLightNode( + options?: CreateOptions & WakuOptions +): Promise { + const libp2pOptions = options?.libp2p ?? {}; + const peerDiscovery = libp2pOptions.peerDiscovery ?? []; + if (options?.defaultBootstrap) { + peerDiscovery.push(defaultPeerDiscovery()); + Object.assign(libp2pOptions, { peerDiscovery }); + } + + const libp2p = await defaultLibp2p(undefined, libp2pOptions); + + const wakuStore = new WakuStore(libp2p, options); + const wakuLightPush = new WakuLightPush(libp2p, options); + const wakuFilter = new WakuFilter(libp2p, options); + + return new WakuNode( + options ?? {}, + libp2p, + wakuStore, + wakuLightPush, + wakuFilter + ) as WakuLight; +} + +/** + * Create a Waku node that uses Waku Relay to send and receive messages, + * enabling some privacy preserving properties. + */ +export async function createPrivacyNode( + options?: CreateOptions & WakuOptions +): Promise { + const libp2pOptions = options?.libp2p ?? {}; + const peerDiscovery = libp2pOptions.peerDiscovery ?? []; + if (options?.defaultBootstrap) { + peerDiscovery.push(defaultPeerDiscovery()); + Object.assign(libp2pOptions, { peerDiscovery }); + } + + const libp2p = await defaultLibp2p(new WakuRelay(options), libp2pOptions); + + return new WakuNode(options ?? {}, libp2p) as WakuPrivacy; +} + +/** + * Create a Waku node that uses all Waku protocols. + * + * This helper is not recommended except if: + * - you are interfacing with nwaku v0.11 or below + * - you are doing some form of testing + * + * If you are building a full node, it is recommended to use + * [nwaku](github.com/status-im/nwaku) and its JSON RPC API or wip REST API. + * + * @see https://github.com/status-im/nwaku/issues/1085 + * @internal + */ +export async function createFullNode( + options?: CreateOptions & WakuOptions +): Promise { + const libp2pOptions = options?.libp2p ?? {}; + const peerDiscovery = libp2pOptions.peerDiscovery ?? []; + if (options?.defaultBootstrap) { + peerDiscovery.push(defaultPeerDiscovery()); + Object.assign(libp2pOptions, { peerDiscovery }); + } + + const libp2p = await defaultLibp2p(new WakuRelay(options), libp2pOptions); + + const wakuStore = new WakuStore(libp2p, options); + const wakuLightPush = new WakuLightPush(libp2p, options); + const wakuFilter = new WakuFilter(libp2p, options); + + return new WakuNode( + options ?? {}, + libp2p, + wakuStore, + wakuLightPush, + wakuFilter + ) as WakuFull; +} + +/** + * @deprecated use { @link createLightNode }, { @link createPrivacyNode } or + * { @link index.waku.WakuNode.constructor } instead. + */ export async function createWaku( options?: CreateOptions & WakuOptions ): Promise { @@ -65,7 +159,13 @@ export async function createWaku( const wakuLightPush = new WakuLightPush(libp2p, options); const wakuFilter = new WakuFilter(libp2p, options); - return new Waku(options ?? {}, libp2p, wakuStore, wakuLightPush, wakuFilter); + return new WakuNode( + options ?? {}, + libp2p, + wakuStore, + wakuLightPush, + wakuFilter + ); } export function defaultPeerDiscovery(): PeerDiscovery { @@ -73,7 +173,7 @@ export function defaultPeerDiscovery(): PeerDiscovery { } export async function defaultLibp2p( - wakuRelay: WakuRelay, + wakuRelay?: WakuRelay, options?: Partial ): Promise { const libp2pOpts = Object.assign( @@ -82,7 +182,7 @@ export async function defaultLibp2p( streamMuxers: [new Mplex()], connectionEncryption: [new Noise()], }, - { pubsub: wakuRelay }, + wakuRelay ? { pubsub: wakuRelay } : {}, options ?? {} ); diff --git a/src/lib/enr/enr.node.spec.ts b/src/lib/enr/enr.node.spec.ts index 3c6415249e..f8daa69747 100644 --- a/src/lib/enr/enr.node.spec.ts +++ b/src/lib/enr/enr.node.spec.ts @@ -2,8 +2,9 @@ import { expect } from "chai"; import { makeLogFileName, NOISE_KEY_1, Nwaku } from "../../test_utils"; import { createWaku } from "../create_waku"; +import type { Waku } from "../interfaces"; import { waitForRemotePeer } from "../wait_for_remote_peer"; -import { Protocols, Waku } from "../waku"; +import { Protocols } from "../waku"; import { ENR } from "./enr"; diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts new file mode 100644 index 0000000000..1f315dc0fe --- /dev/null +++ b/src/lib/interfaces.ts @@ -0,0 +1,60 @@ +import type { Stream } from "@libp2p/interface-connection"; +import type { PeerId } from "@libp2p/interface-peer-id"; +import type { Multiaddr } from "@multiformats/multiaddr"; +import type { Libp2p } from "libp2p"; + +import type { Protocols } from "./waku"; +import type { WakuFilter } from "./waku_filter"; +import type { WakuLightPush } from "./waku_light_push"; +import type { DecryptionMethod } from "./waku_message"; +import type { WakuRelay } from "./waku_relay"; +import type { WakuStore } from "./waku_store"; + +export interface Waku { + libp2p: Libp2p; + relay?: WakuRelay; + store?: WakuStore; + filter?: WakuFilter; + lightPush?: WakuLightPush; + + dial(peer: PeerId | Multiaddr, protocols?: Protocols[]): Promise; + + addPeerToAddressBook( + peerId: PeerId | string, + multiaddrs: Multiaddr[] | string[] + ): void; + + start(): Promise; + + stop(): Promise; + + isStarted(): boolean; + + addDecryptionKey( + key: Uint8Array | string, + options?: { method?: DecryptionMethod; contentTopics?: string[] } + ): void; + + deleteDecryptionKey(key: Uint8Array | string): void; +} + +export interface WakuLight extends Waku { + relay: undefined; + store: WakuStore; + filter: WakuFilter; + lightPush: WakuLightPush; +} + +export interface WakuPrivacy extends Waku { + relay: WakuRelay; + store: undefined; + filter: undefined; + lightPush: undefined; +} + +export interface WakuFull extends Waku { + relay: WakuRelay; + store: WakuStore; + filter: WakuFilter; + lightPush: WakuLightPush; +} diff --git a/src/lib/wait_for_remote_peer.node.spec.ts b/src/lib/wait_for_remote_peer.node.spec.ts index c48bef1fbb..67793a890b 100644 --- a/src/lib/wait_for_remote_peer.node.spec.ts +++ b/src/lib/wait_for_remote_peer.node.spec.ts @@ -3,12 +3,14 @@ import { expect } from "chai"; import { makeLogFileName, NOISE_KEY_1, Nwaku } from "../test_utils"; import { delay } from "../test_utils/delay"; -import { createWaku } from "./create_waku"; +import { createLightNode, createPrivacyNode } from "./create_waku"; +import type { WakuLight, WakuPrivacy } from "./interfaces"; import { waitForRemotePeer } from "./wait_for_remote_peer"; -import { Protocols, Waku } from "./waku"; +import { Protocols } from "./waku"; describe("Wait for remote peer", function () { - let waku: Waku; + let waku1: WakuPrivacy; + let waku2: WakuLight; let nwaku: Nwaku | undefined; afterEach(async function () { @@ -16,7 +18,8 @@ describe("Wait for remote peer", function () { nwaku.stop(); nwaku = undefined; } - !!waku && waku.stop().catch((e) => console.log("Waku failed to stop", e)); + waku1?.stop().catch((e) => console.log("Waku failed to stop", e)); + waku2?.stop().catch((e) => console.log("Waku failed to stop", e)); }); it("Relay - dialed first", async function () { @@ -30,14 +33,14 @@ describe("Wait for remote peer", function () { }); const multiAddrWithId = await nwaku.getMultiaddrWithId(); - waku = await createWaku({ + waku1 = await createPrivacyNode({ staticNoiseKey: NOISE_KEY_1, }); - await waku.start(); - await waku.dial(multiAddrWithId); + await waku1.start(); + await waku1.dial(multiAddrWithId); await delay(1000); - await waitForRemotePeer(waku, [Protocols.Relay]); - const peers = waku.relay.getMeshPeers(); + await waitForRemotePeer(waku1, [Protocols.Relay]); + const peers = waku1.relay.getMeshPeers(); const nimPeerId = multiAddrWithId.getPeerId(); expect(nimPeerId).to.not.be.undefined; @@ -55,17 +58,17 @@ describe("Wait for remote peer", function () { }); const multiAddrWithId = await nwaku.getMultiaddrWithId(); - waku = await createWaku({ + waku1 = await createPrivacyNode({ staticNoiseKey: NOISE_KEY_1, }); - await waku.start(); + await waku1.start(); - const waitPromise = waitForRemotePeer(waku, [Protocols.Relay]); + const waitPromise = waitForRemotePeer(waku1, [Protocols.Relay]); await delay(1000); - await waku.dial(multiAddrWithId); + await waku1.dial(multiAddrWithId); await waitPromise; - const peers = waku.relay.getMeshPeers(); + const peers = waku1.relay.getMeshPeers(); const nimPeerId = multiAddrWithId.getPeerId(); expect(nimPeerId).to.not.be.undefined; @@ -74,12 +77,12 @@ describe("Wait for remote peer", function () { it("Relay - times out", function (done) { this.timeout(5000); - createWaku({ + createPrivacyNode({ staticNoiseKey: NOISE_KEY_1, }) - .then((waku) => waku.start().then(() => waku)) - .then((waku) => { - waitForRemotePeer(waku, [Protocols.Relay], 200).then( + .then((waku1) => waku1.start().then(() => waku1)) + .then((waku1) => { + waitForRemotePeer(waku1, [Protocols.Relay], 200).then( () => { throw "Promise expected to reject on time out"; }, @@ -103,15 +106,15 @@ describe("Wait for remote peer", function () { }); const multiAddrWithId = await nwaku.getMultiaddrWithId(); - waku = await createWaku({ + waku2 = await createLightNode({ staticNoiseKey: NOISE_KEY_1, }); - await waku.start(); - await waku.dial(multiAddrWithId); + await waku2.start(); + await waku2.dial(multiAddrWithId); await delay(1000); - await waitForRemotePeer(waku, [Protocols.Store]); + await waitForRemotePeer(waku2, [Protocols.Store]); - const peers = (await waku.store.peers()).map((peer) => peer.id.toString()); + const peers = (await waku2.store.peers()).map((peer) => peer.id.toString()); const nimPeerId = multiAddrWithId.getPeerId(); expect(nimPeerId).to.not.be.undefined; @@ -130,16 +133,16 @@ describe("Wait for remote peer", function () { }); const multiAddrWithId = await nwaku.getMultiaddrWithId(); - waku = await createWaku({ + waku2 = await createLightNode({ staticNoiseKey: NOISE_KEY_1, }); - await waku.start(); - const waitPromise = waitForRemotePeer(waku, [Protocols.Store], 2000); + await waku2.start(); + const waitPromise = waitForRemotePeer(waku2, [Protocols.Store], 2000); await delay(1000); - await waku.dial(multiAddrWithId); + await waku2.dial(multiAddrWithId); await waitPromise; - const peers = (await waku.store.peers()).map((peer) => peer.id.toString()); + const peers = (await waku2.store.peers()).map((peer) => peer.id.toString()); const nimPeerId = multiAddrWithId.getPeerId(); @@ -158,14 +161,14 @@ describe("Wait for remote peer", function () { }); const multiAddrWithId = await nwaku.getMultiaddrWithId(); - waku = await createWaku({ + waku2 = await createLightNode({ staticNoiseKey: NOISE_KEY_1, }); - await waku.start(); - await waku.dial(multiAddrWithId); - await waitForRemotePeer(waku, [Protocols.LightPush]); + await waku2.start(); + await waku2.dial(multiAddrWithId); + await waitForRemotePeer(waku2, [Protocols.LightPush]); - const peers = (await waku.lightPush.peers()).map((peer) => + const peers = (await waku2.lightPush.peers()).map((peer) => peer.id.toString() ); @@ -186,14 +189,79 @@ describe("Wait for remote peer", function () { }); const multiAddrWithId = await nwaku.getMultiaddrWithId(); - waku = await createWaku({ + waku2 = await createLightNode({ staticNoiseKey: NOISE_KEY_1, }); - await waku.start(); - await waku.dial(multiAddrWithId); - await waitForRemotePeer(waku, [Protocols.Filter]); + await waku2.start(); + await waku2.dial(multiAddrWithId); + await waitForRemotePeer(waku2, [Protocols.Filter]); - const peers = (await waku.filter.peers()).map((peer) => peer.id.toString()); + const peers = (await waku2.filter.peers()).map((peer) => + peer.id.toString() + ); + + const nimPeerId = multiAddrWithId.getPeerId(); + + expect(nimPeerId).to.not.be.undefined; + expect(peers.includes(nimPeerId as string)).to.be.true; + }); + + it("Light Node - default protocols", async function () { + this.timeout(20_000); + nwaku = new Nwaku(makeLogFileName(this)); + await nwaku.start({ + filter: true, + lightpush: true, + relay: false, + store: true, + persistMessages: true, + }); + const multiAddrWithId = await nwaku.getMultiaddrWithId(); + + waku2 = await createLightNode({ + staticNoiseKey: NOISE_KEY_1, + }); + await waku2.start(); + await waku2.dial(multiAddrWithId); + await waitForRemotePeer(waku2); + + const filterPeers = (await waku2.filter.peers()).map((peer) => + peer.id.toString() + ); + const storePeers = (await waku2.store.peers()).map((peer) => + peer.id.toString() + ); + const lightPushPeers = (await waku2.lightPush.peers()).map((peer) => + peer.id.toString() + ); + + const nimPeerId = multiAddrWithId.getPeerId(); + + expect(nimPeerId).to.not.be.undefined; + expect(filterPeers.includes(nimPeerId as string)).to.be.true; + expect(storePeers.includes(nimPeerId as string)).to.be.true; + expect(lightPushPeers.includes(nimPeerId as string)).to.be.true; + }); + + it("Privacy Node - default protocol", async function () { + this.timeout(20_000); + nwaku = new Nwaku(makeLogFileName(this)); + await nwaku.start({ + filter: false, + lightpush: false, + relay: true, + store: false, + }); + const multiAddrWithId = await nwaku.getMultiaddrWithId(); + + waku1 = await createPrivacyNode({ + staticNoiseKey: NOISE_KEY_1, + }); + await waku1.start(); + await waku1.dial(multiAddrWithId); + await waitForRemotePeer(waku1); + + const peers = await waku1.relay.getMeshPeers(); const nimPeerId = multiAddrWithId.getPeerId(); diff --git a/src/lib/wait_for_remote_peer.ts b/src/lib/wait_for_remote_peer.ts index 85220e8dc1..ec6cec2437 100644 --- a/src/lib/wait_for_remote_peer.ts +++ b/src/lib/wait_for_remote_peer.ts @@ -5,7 +5,8 @@ import type { Libp2p } from "libp2p"; import { pEvent } from "p-event"; import { StoreCodecs } from "./constants"; -import { Protocols, Waku } from "./waku"; +import type { Waku } from "./interfaces"; +import { Protocols } from "./waku"; import { FilterCodec } from "./waku_filter"; import { LightPushCodec } from "./waku_light_push"; @@ -22,8 +23,9 @@ interface WakuGossipSubProtocol extends GossipSub { /** * Wait for a remote peer to be ready given the passed protocols. - * Must be used after attempting to connect to nodes, using {@link index.waku.Waku.dial} or - * a bootstrap method with {@link index.waku.Waku.constructor}. + * Must be used after attempting to connect to nodes, using + * {@link index.waku.WakuNode.dial} or a bootstrap method with + * {@link lib/create_waku.createLightNode}. * * If the passed protocols is a GossipSub protocol, then it resolves only once * a peer is in a mesh, to help ensure that other peers will send and receive @@ -35,33 +37,41 @@ interface WakuGossipSubProtocol extends GossipSub { * * @returns A promise that **resolves** if all desired protocols are fulfilled by * remote nodes, **rejects** if the timeoutMs is reached. - * - * @default Remote peer must have Waku Relay enabled and no time out is applied. + * @throws If passing a protocol that is not mounted + * @default Wait for remote peers with protocols enabled locally and no time out is applied. */ export async function waitForRemotePeer( waku: Waku, protocols?: Protocols[], timeoutMs?: number ): Promise { - protocols = protocols ?? [Protocols.Relay]; + protocols = protocols ?? getEnabledProtocols(waku); if (!waku.isStarted()) return Promise.reject("Waku node is not started"); const promises = []; if (protocols.includes(Protocols.Relay)) { + if (!waku.relay) + throw new Error("Cannot wait for Relay peer: protocol not mounted"); promises.push(waitForGossipSubPeerInMesh(waku.relay)); } if (protocols.includes(Protocols.Store)) { + if (!waku.store) + throw new Error("Cannot wait for Store peer: protocol not mounted"); promises.push(waitForConnectedPeer(waku.store, Object.values(StoreCodecs))); } if (protocols.includes(Protocols.LightPush)) { + if (!waku.lightPush) + throw new Error("Cannot wait for LightPush peer: protocol not mounted"); promises.push(waitForConnectedPeer(waku.lightPush, [LightPushCodec])); } if (protocols.includes(Protocols.Filter)) { + if (!waku.filter) + throw new Error("Cannot wait for Filter peer: protocol not mounted"); promises.push(waitForConnectedPeer(waku.filter, [FilterCodec])); } @@ -130,3 +140,25 @@ async function rejectOnTimeout( ): Promise { await Promise.race([promise, awaitTimeout(timeoutMs, rejectReason)]); } + +function getEnabledProtocols(waku: Waku): Protocols[] { + const protocols = []; + + if (waku.relay) { + protocols.push(Protocols.Relay); + } + + if (waku.filter) { + protocols.push(Protocols.Filter); + } + + if (waku.store) { + protocols.push(Protocols.Store); + } + + if (waku.lightPush) { + protocols.push(Protocols.LightPush); + } + + return protocols; +} diff --git a/src/lib/waku.node.spec.ts b/src/lib/waku.node.spec.ts index c61d6b31f1..9913cfc3fb 100644 --- a/src/lib/waku.node.spec.ts +++ b/src/lib/waku.node.spec.ts @@ -8,11 +8,12 @@ import { Nwaku, } from "../test_utils/"; -import { createWaku } from "./create_waku"; +import { createLightNode, createPrivacyNode } from "./create_waku"; import { generateSymmetricKey } from "./crypto"; +import type { Waku, WakuLight, WakuPrivacy } from "./interfaces"; import { PeerDiscoveryStaticPeers } from "./peer_discovery_static_list"; import { waitForRemotePeer } from "./wait_for_remote_peer"; -import { Protocols, Waku } from "./waku"; +import { Protocols } from "./waku"; import { WakuMessage } from "./waku_message"; const TestContentTopic = "/test/1/waku/utf8"; @@ -30,15 +31,20 @@ describe("Waku Dial [node only]", function () { it("connects to nwaku", async function () { this.timeout(20_000); nwaku = new Nwaku(makeLogFileName(this)); - await nwaku.start(); + await nwaku.start({ + filter: true, + store: true, + lightpush: true, + persistMessages: true, + }); const multiAddrWithId = await nwaku.getMultiaddrWithId(); - waku = await createWaku({ + waku = await createLightNode({ staticNoiseKey: NOISE_KEY_1, }); await waku.start(); await waku.dial(multiAddrWithId); - await waitForRemotePeer(waku, [Protocols.Relay]); + await waitForRemotePeer(waku); const nimPeerId = await nwaku.getPeerId(); expect(await waku.libp2p.peerStore.has(nimPeerId)).to.be.true; @@ -46,7 +52,7 @@ describe("Waku Dial [node only]", function () { }); describe("Bootstrap", function () { - let waku: Waku; + let waku: WakuLight; let nwaku: Nwaku; afterEach(async function () { @@ -60,7 +66,7 @@ describe("Waku Dial [node only]", function () { nwaku = new Nwaku(makeLogFileName(this)); await nwaku.start(); const multiAddrWithId = await nwaku.getMultiaddrWithId(); - waku = await createWaku({ + waku = await createLightNode({ staticNoiseKey: NOISE_KEY_1, libp2p: { peerDiscovery: [new PeerDiscoveryStaticPeers([multiAddrWithId])], @@ -86,7 +92,7 @@ describe("Waku Dial [node only]", function () { nwaku = new Nwaku(makeLogFileName(this)); await nwaku.start(); - waku = await createWaku({ + waku = await createLightNode({ staticNoiseKey: NOISE_KEY_1, libp2p: { peerDiscovery: [ @@ -118,15 +124,15 @@ describe("Decryption Keys", () => { } }); - let waku1: Waku; - let waku2: Waku; + let waku1: WakuPrivacy; + let waku2: WakuPrivacy; beforeEach(async function () { this.timeout(5000); [waku1, waku2] = await Promise.all([ - createWaku({ staticNoiseKey: NOISE_KEY_1 }).then((waku) => + createPrivacyNode({ staticNoiseKey: NOISE_KEY_1 }).then((waku) => waku.start().then(() => waku) ), - createWaku({ + createPrivacyNode({ staticNoiseKey: NOISE_KEY_2, libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } }, }).then((waku) => waku.start().then(() => waku)), diff --git a/src/lib/waku.spec.ts b/src/lib/waku.spec.ts index 9ac244d2c9..efde19c331 100644 --- a/src/lib/waku.spec.ts +++ b/src/lib/waku.spec.ts @@ -2,7 +2,7 @@ import type { PeerId } from "@libp2p/interface-peer-id"; import { expect } from "chai"; import { createWaku } from "./create_waku"; -import { Waku } from "./waku"; +import type { Waku } from "./interfaces"; describe("Waku Dial", function () { describe("Bootstrap [live data]", function () { diff --git a/src/lib/waku.ts b/src/lib/waku.ts index fc9428bf49..aa65878f16 100644 --- a/src/lib/waku.ts +++ b/src/lib/waku.ts @@ -1,16 +1,19 @@ import type { Stream } from "@libp2p/interface-connection"; import type { PeerId } from "@libp2p/interface-peer-id"; +import type { PubSub } from "@libp2p/interface-pubsub"; import { peerIdFromString } from "@libp2p/peer-id"; import type { Multiaddr } from "@multiformats/multiaddr"; import { multiaddr } from "@multiformats/multiaddr"; import debug from "debug"; import type { Libp2p } from "libp2p"; +import { Waku } from "./interfaces"; import { FilterCodec, WakuFilter } from "./waku_filter"; import { LightPushCodec, WakuLightPush } from "./waku_light_push"; import { DecryptionMethod, WakuMessage } from "./waku_message"; import { WakuRelay } from "./waku_relay"; import { RelayCodecs, RelayPingContentTopic } from "./waku_relay/constants"; +import * as relayConstants from "./waku_relay/constants"; import { StoreCodecs, WakuStore } from "./waku_store"; export const DefaultPingKeepAliveValueSecs = 0; @@ -43,12 +46,12 @@ export interface WakuOptions { decryptionKeys?: Array; } -export class Waku { +export class WakuNode implements Waku { public libp2p: Libp2p; - public relay: WakuRelay; - public store: WakuStore; - public filter: WakuFilter; - public lightPush: WakuLightPush; + public relay?: WakuRelay; + public store?: WakuStore; + public filter?: WakuFilter; + public lightPush?: WakuLightPush; private pingKeepAliveTimers: { [peer: string]: ReturnType; @@ -60,22 +63,27 @@ export class Waku { constructor( options: WakuOptions, libp2p: Libp2p, - store: WakuStore, - lightPush: WakuLightPush, - filter: WakuFilter + store?: WakuStore, + lightPush?: WakuLightPush, + filter?: WakuFilter ) { this.libp2p = libp2p; - this.relay = libp2p.pubsub as unknown as WakuRelay; this.store = store; this.filter = filter; this.lightPush = lightPush; + + if (isWakuRelay(libp2p.pubsub)) { + this.relay = libp2p.pubsub; + } + this.pingKeepAliveTimers = {}; this.relayKeepAliveTimers = {}; const pingKeepAlive = options.pingKeepAlive || DefaultPingKeepAliveValueSecs; - const relayKeepAlive = - options.relayKeepAlive || DefaultRelayKeepAliveValueSecs; + const relayKeepAlive = this.relay + ? options.relayKeepAlive || DefaultRelayKeepAliveValueSecs + : 0; libp2p.connectionManager.addEventListener("peer:connect", (evt) => { this.startKeepAlive(evt.detail.remotePeer, pingKeepAlive, relayKeepAlive); @@ -179,9 +187,9 @@ export class Waku { key: Uint8Array | string, options?: { method?: DecryptionMethod; contentTopics?: string[] } ): void { - this.relay.addDecryptionKey(key, options); - this.store.addDecryptionKey(key, options); - this.filter.addDecryptionKey(key, options); + if (this.relay) this.relay.addDecryptionKey(key, options); + if (this.store) this.store.addDecryptionKey(key, options); + if (this.filter) this.filter.addDecryptionKey(key, options); } /** @@ -191,9 +199,9 @@ export class Waku { * Strings must be in hex format. */ deleteDecryptionKey(key: Uint8Array | string): void { - this.relay.deleteDecryptionKey(key); - this.store.deleteDecryptionKey(key); - this.filter.deleteDecryptionKey(key); + if (this.relay) this.relay.deleteDecryptionKey(key); + if (this.store) this.store.deleteDecryptionKey(key); + if (this.filter) this.filter.deleteDecryptionKey(key); } /** @@ -229,11 +237,12 @@ export class Waku { }, pingPeriodSecs * 1000); } - if (relayPeriodSecs !== 0) { + const relay = this.relay; + if (relay && relayPeriodSecs !== 0) { this.relayKeepAliveTimers[peerIdStr] = setInterval(() => { log("Sending Waku Relay ping message"); WakuMessage.fromBytes(new Uint8Array(), RelayPingContentTopic).then( - (wakuMsg) => this.relay.send(wakuMsg) + (wakuMsg) => relay.send(wakuMsg) ); }, relayPeriodSecs * 1000); } @@ -265,3 +274,16 @@ export class Waku { this.relayKeepAliveTimers = {}; } } + +function isWakuRelay(pubsub: PubSub): pubsub is WakuRelay { + if (pubsub) { + try { + return pubsub.multicodecs.includes( + relayConstants.RelayCodecs[relayConstants.RelayCodecs.length - 1] + ); + // Exception is expected if `libp2p` was not instantiated with pubsub + // eslint-disable-next-line no-empty + } catch (e) {} + } + return false; +} diff --git a/src/lib/waku_filter/index.node.spec.ts b/src/lib/waku_filter/index.node.spec.ts index f02949e018..da0dfdefdf 100644 --- a/src/lib/waku_filter/index.node.spec.ts +++ b/src/lib/waku_filter/index.node.spec.ts @@ -3,9 +3,10 @@ import debug from "debug"; import { makeLogFileName, NOISE_KEY_1, Nwaku } from "../../test_utils"; import { delay } from "../../test_utils/delay"; -import { createWaku } from "../create_waku"; +import { createFullNode } from "../create_waku"; +import type { WakuFull } from "../interfaces"; import { waitForRemotePeer } from "../wait_for_remote_peer"; -import { Protocols, Waku } from "../waku"; +import { Protocols } from "../waku"; import { WakuMessage } from "../waku_message"; const log = debug("waku:test"); @@ -13,7 +14,7 @@ const log = debug("waku:test"); const TestContentTopic = "/test/1/waku-filter"; describe("Waku Filter", () => { - let waku: Waku; + let waku: WakuFull; let nwaku: Nwaku; afterEach(async function () { @@ -25,7 +26,7 @@ describe("Waku Filter", () => { this.timeout(15000); nwaku = new Nwaku(makeLogFileName(this)); await nwaku.start({ filter: true, lightpush: true }); - waku = await createWaku({ + waku = await createFullNode({ staticNoiseKey: NOISE_KEY_1, libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } }, }); @@ -46,7 +47,7 @@ describe("Waku Filter", () => { expect(msg.payloadAsUtf8).to.eq(messageText); }; await waku.filter.subscribe(callback, [TestContentTopic]); - // As the filter protocol does not cater for a ack of subscription + // As the filter protocol does not cater for an ack of subscription // we cannot know whether the subscription happened. Something we want to // correct in future versions of the protocol. await delay(200); diff --git a/src/lib/waku_light_push/index.node.spec.ts b/src/lib/waku_light_push/index.node.spec.ts index 2a66d0af34..5a435e5b4c 100644 --- a/src/lib/waku_light_push/index.node.spec.ts +++ b/src/lib/waku_light_push/index.node.spec.ts @@ -3,9 +3,10 @@ import debug from "debug"; import { makeLogFileName, NOISE_KEY_1, Nwaku } from "../../test_utils"; import { delay } from "../../test_utils/delay"; -import { createWaku } from "../create_waku"; +import { createFullNode } from "../create_waku"; +import type { WakuFull } from "../interfaces"; import { waitForRemotePeer } from "../wait_for_remote_peer"; -import { Protocols, Waku } from "../waku"; +import { Protocols } from "../waku"; import { WakuMessage } from "../waku_message"; const log = debug("waku:test:lightpush"); @@ -13,7 +14,7 @@ const log = debug("waku:test:lightpush"); const TestContentTopic = "/test/1/waku-light-push/utf8"; describe("Waku Light Push [node only]", () => { - let waku: Waku; + let waku: WakuFull; let nwaku: Nwaku; afterEach(async function () { @@ -27,7 +28,7 @@ describe("Waku Light Push [node only]", () => { nwaku = new Nwaku(makeLogFileName(this)); await nwaku.start({ lightpush: true }); - waku = await createWaku({ + waku = await createFullNode({ staticNoiseKey: NOISE_KEY_1, }); await waku.start(); @@ -63,7 +64,7 @@ describe("Waku Light Push [node only]", () => { nwaku = new Nwaku(makeLogFileName(this)); await nwaku.start({ lightpush: true, topics: customPubSubTopic }); - waku = await createWaku({ + waku = await createFullNode({ pubSubTopic: customPubSubTopic, staticNoiseKey: NOISE_KEY_1, }); diff --git a/src/lib/waku_message/index.node.spec.ts b/src/lib/waku_message/index.node.spec.ts index 80277f1a95..6fb5f705db 100644 --- a/src/lib/waku_message/index.node.spec.ts +++ b/src/lib/waku_message/index.node.spec.ts @@ -8,15 +8,16 @@ import { WakuRelayMessage, } from "../../test_utils"; import { delay } from "../../test_utils/delay"; -import { createWaku } from "../create_waku"; +import { createPrivacyNode } from "../create_waku"; import { generatePrivateKey, generateSymmetricKey, getPublicKey, } from "../crypto"; +import type { WakuPrivacy } from "../interfaces"; import { bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes } from "../utils"; import { waitForRemotePeer } from "../wait_for_remote_peer"; -import { Protocols, Waku } from "../waku"; +import { Protocols } from "../waku"; import { DecryptionMethod, WakuMessage } from "./index"; @@ -26,12 +27,12 @@ const TestContentTopic = "/test/1/waku-message/utf8"; describe("Waku Message [node only]", function () { describe("Interop: nwaku", function () { - let waku: Waku; + let waku: WakuPrivacy; let nwaku: Nwaku; beforeEach(async function () { this.timeout(30_000); - waku = await createWaku({ + waku = await createPrivacyNode({ staticNoiseKey: NOISE_KEY_1, }); await waku.start(); diff --git a/src/lib/waku_relay/index.node.spec.ts b/src/lib/waku_relay/index.node.spec.ts index 472c3d363b..905a7a33c0 100644 --- a/src/lib/waku_relay/index.node.spec.ts +++ b/src/lib/waku_relay/index.node.spec.ts @@ -11,14 +11,15 @@ import { } from "../../test_utils"; import { delay } from "../../test_utils/delay"; import { DefaultPubSubTopic } from "../constants"; -import { createWaku } from "../create_waku"; +import { createPrivacyNode } from "../create_waku"; import { generatePrivateKey, generateSymmetricKey, getPublicKey, } from "../crypto"; +import type { WakuPrivacy } from "../interfaces"; import { waitForRemotePeer } from "../wait_for_remote_peer"; -import { Protocols, Waku } from "../waku"; +import { Protocols } from "../waku"; import { DecryptionMethod, WakuMessage } from "../waku_message"; const log = debug("waku:test"); @@ -35,17 +36,17 @@ describe("Waku Relay [node only]", () => { } }); - let waku1: Waku; - let waku2: Waku; + let waku1: WakuPrivacy; + let waku2: WakuPrivacy; beforeEach(async function () { this.timeout(10000); log("Starting JS Waku instances"); [waku1, waku2] = await Promise.all([ - createWaku({ staticNoiseKey: NOISE_KEY_1 }).then((waku) => + createPrivacyNode({ staticNoiseKey: NOISE_KEY_1 }).then((waku) => waku.start().then(() => waku) ), - createWaku({ + createPrivacyNode({ staticNoiseKey: NOISE_KEY_2, libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } }, }).then((waku) => waku.start().then(() => waku)), @@ -258,9 +259,9 @@ describe("Waku Relay [node only]", () => { }); describe("Custom pubsub topic", () => { - let waku1: Waku; - let waku2: Waku; - let waku3: Waku; + let waku1: WakuPrivacy; + let waku2: WakuPrivacy; + let waku3: WakuPrivacy; afterEach(async function () { !!waku1 && waku1.stop().catch((e) => console.log("Waku failed to stop", e)); @@ -278,16 +279,16 @@ describe("Waku Relay [node only]", () => { // 1 and 2 uses a custom pubsub // 3 uses the default pubsub [waku1, waku2, waku3] = await Promise.all([ - createWaku({ + createPrivacyNode({ pubSubTopic: pubSubTopic, staticNoiseKey: NOISE_KEY_1, }).then((waku) => waku.start().then(() => waku)), - createWaku({ + createPrivacyNode({ pubSubTopic: pubSubTopic, staticNoiseKey: NOISE_KEY_2, libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } }, }).then((waku) => waku.start().then(() => waku)), - createWaku({ + createPrivacyNode({ staticNoiseKey: NOISE_KEY_3, }).then((waku) => waku.start().then(() => waku)), ]); @@ -337,12 +338,12 @@ describe("Waku Relay [node only]", () => { }); describe("Interop: nwaku", function () { - let waku: Waku; + let waku: WakuPrivacy; let nwaku: Nwaku; beforeEach(async function () { this.timeout(30_000); - waku = await createWaku({ + waku = await createPrivacyNode({ staticNoiseKey: NOISE_KEY_1, }); await waku.start(); @@ -422,8 +423,8 @@ describe("Waku Relay [node only]", () => { }); describe.skip("Two nodes connected to nwaku", function () { - let waku1: Waku; - let waku2: Waku; + let waku1: WakuPrivacy; + let waku2: WakuPrivacy; let nwaku: Nwaku; afterEach(async function () { @@ -437,10 +438,10 @@ describe("Waku Relay [node only]", () => { it("Js publishes, other Js receives", async function () { this.timeout(60_000); [waku1, waku2] = await Promise.all([ - createWaku({ + createPrivacyNode({ staticNoiseKey: NOISE_KEY_1, }).then((waku) => waku.start().then(() => waku)), - createWaku({ + createPrivacyNode({ staticNoiseKey: NOISE_KEY_2, }).then((waku) => waku.start().then(() => waku)), ]); diff --git a/src/lib/waku_relay/index.ts b/src/lib/waku_relay/index.ts index 4d710deaf5..b53cf9519e 100644 --- a/src/lib/waku_relay/index.ts +++ b/src/lib/waku_relay/index.ts @@ -18,7 +18,7 @@ import * as constants from "./constants"; const log = debug("waku:relay"); -export interface CreateOptions { +export type CreateOptions = { /** * The PubSub Topic to use. Defaults to {@link DefaultPubSubTopic}. * @@ -34,7 +34,7 @@ export interface CreateOptions { */ pubSubTopic?: string; decryptionKeys?: Array; -} +} & GossipsubOpts; /** * Implements the [Waku v2 Relay protocol](https://rfc.vac.dev/spec/11/). @@ -59,7 +59,7 @@ export class WakuRelay extends GossipSub { [contentTopic: string]: Set<(message: WakuMessage) => void>; }; - constructor(options?: Partial) { + constructor(options?: Partial) { options = Object.assign(options ?? {}, { // Ensure that no signature is included nor expected in the messages. globalSignaturePolicy: SignaturePolicy.StrictNoSign, @@ -229,3 +229,5 @@ export class WakuRelay extends GossipSub { return super.getMeshPeers(topic ?? this.pubSubTopic); } } + +WakuRelay.multicodec = constants.RelayCodecs[constants.RelayCodecs.length - 1]; diff --git a/src/lib/waku_store/index.node.spec.ts b/src/lib/waku_store/index.node.spec.ts index 47f79fe6b3..6ba5841977 100644 --- a/src/lib/waku_store/index.node.spec.ts +++ b/src/lib/waku_store/index.node.spec.ts @@ -7,14 +7,15 @@ import { NOISE_KEY_2, Nwaku, } from "../../test_utils"; -import { createWaku } from "../create_waku"; +import { createFullNode } from "../create_waku"; import { generatePrivateKey, generateSymmetricKey, getPublicKey, } from "../crypto"; +import type { WakuFull } from "../interfaces"; import { waitForRemotePeer } from "../wait_for_remote_peer"; -import { Protocols, Waku } from "../waku"; +import { Protocols } from "../waku"; import { DecryptionMethod, WakuMessage } from "../waku_message"; import { PageDirection } from "./history_rpc"; @@ -24,7 +25,7 @@ const log = debug("waku:test:store"); const TestContentTopic = "/test/1/waku-store/utf8"; describe("Waku Store", () => { - let waku: Waku; + let waku: WakuFull; let nwaku: Nwaku; afterEach(async function () { @@ -48,7 +49,7 @@ describe("Waku Store", () => { ).to.be.true; } - waku = await createWaku({ + waku = await createFullNode({ staticNoiseKey: NOISE_KEY_1, }); await waku.start(); @@ -81,7 +82,7 @@ describe("Waku Store", () => { ).to.be.true; } - waku = await createWaku({ + waku = await createFullNode({ staticNoiseKey: NOISE_KEY_1, }); await waku.start(); @@ -121,7 +122,7 @@ describe("Waku Store", () => { ).to.be.true; } - waku = await createWaku({ + waku = await createFullNode({ staticNoiseKey: NOISE_KEY_1, }); await waku.start(); @@ -158,7 +159,7 @@ describe("Waku Store", () => { ).to.be.true; } - waku = await createWaku({ + waku = await createFullNode({ staticNoiseKey: NOISE_KEY_1, }); await waku.start(); @@ -201,7 +202,7 @@ describe("Waku Store", () => { ).to.be.true; } - waku = await createWaku({ + waku = await createFullNode({ pubSubTopic: customPubSubTopic, staticNoiseKey: NOISE_KEY_1, }); @@ -268,10 +269,10 @@ describe("Waku Store", () => { log("Messages have been encrypted"); const [waku1, waku2, nimWakuMultiaddr] = await Promise.all([ - createWaku({ + createFullNode({ staticNoiseKey: NOISE_KEY_1, }).then((waku) => waku.start().then(() => waku)), - createWaku({ + createFullNode({ staticNoiseKey: NOISE_KEY_2, }).then((waku) => waku.start().then(() => waku)), nwaku.getMultiaddrWithId(), @@ -370,10 +371,10 @@ describe("Waku Store", () => { log("Messages have been encrypted"); const [waku1, waku2, nimWakuMultiaddr] = await Promise.all([ - createWaku({ + createFullNode({ staticNoiseKey: NOISE_KEY_1, }).then((waku) => waku.start().then(() => waku)), - createWaku({ + createFullNode({ staticNoiseKey: NOISE_KEY_2, }).then((waku) => waku.start().then(() => waku)), nwaku.getMultiaddrWithId(), @@ -457,7 +458,7 @@ describe("Waku Store", () => { ).to.be.true; } - waku = await createWaku({ + waku = await createFullNode({ staticNoiseKey: NOISE_KEY_1, }); await waku.start(); diff --git a/typedoc.json b/typedoc.json index 54badcf026..948a5a0b7e 100644 --- a/typedoc.json +++ b/typedoc.json @@ -2,6 +2,7 @@ "entryPoints": [ "./src/index.ts", "./src/lib/create_waku.ts", + "./src/lib/interfaces.ts", "./src/lib/peer_discovery_dns.ts", "./src/lib/peer_discovery_static_list.ts", "./src/lib/predefined_bootstrap_nodes.ts",