From c3855112d7c2ab2e974fda1b690b0ef021448af0 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Mon, 9 Aug 2021 16:25:10 +1000 Subject: [PATCH 1/5] Rename `getStatusFleetNodes` To make it more generic to allow retrieval of bootstrap nodes from other sources. --- CHANGELOG.md | 3 + README.md | 6 +- examples/eth-dm/src/waku.ts | 4 +- examples/eth-pm-wallet-encryption/src/waku.ts | 4 +- examples/min-react-js-chat/src/App.js | 10 +++- examples/store-reactjs-chat/src/App.js | 10 +++- examples/web-chat/src/App.tsx | 15 ++--- guides/reactjs-relay.md | 14 +++-- guides/relay-receive-send-messages.md | 11 ++-- guides/store-retrieve-messages.md | 13 +++-- src/index.ts | 2 +- src/lib/discover.ts | 33 ----------- src/lib/discovery.ts | 56 +++++++++++++++++++ 13 files changed, 110 insertions(+), 71 deletions(-) delete mode 100644 src/lib/discover.ts create mode 100644 src/lib/discovery.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8969317afa..b439926b7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Renamed `discover.getStatusFleetNodes` to `discovery.getBootstrapNodes` and make it more generic to allow retrieval of bootstrap nodes from other sources. + ### Removed - Examples (cli-chat): The focus of this library is Web environment; Several examples now cover usage of Waku Relay and Waku Store making cli-chat example obsolete; diff --git a/README.md b/README.md index 568881ca25..5d502bdfdf 100644 --- a/README.md +++ b/README.md @@ -48,12 +48,12 @@ waku.addPeerToAddressBook( ); ``` -You can also use `getStatusFleetNodes` to connect to nodes run by Status: +You can also use `getBootstrapNodes` to connect to Waku bootstrap nodes: ```ts -import { getStatusFleetNodes } from 'js-waku'; +import { getBootstrapNodes } from 'js-waku'; -getStatusFleetNodes().then((nodes) => { +getBootstrapNodes().then((nodes) => { nodes.forEach((addr) => { waku.dial(addr); }); diff --git a/examples/eth-dm/src/waku.ts b/examples/eth-dm/src/waku.ts index 1eded8b646..5d24ab60a1 100644 --- a/examples/eth-dm/src/waku.ts +++ b/examples/eth-dm/src/waku.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from 'react'; -import { getStatusFleetNodes, Waku, WakuMessage } from 'js-waku'; +import { getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; import { DirectMessage, PublicKeyMessage } from './messaging/wire'; import { validatePublicKeyMessage } from './crypto'; import { Message } from './messaging/Messages'; @@ -12,7 +12,7 @@ export async function initWaku(): Promise { const waku = await Waku.create({}); // Dial all nodes it can find - getStatusFleetNodes().then((nodes) => { + getBootstrapNodes().then((nodes) => { nodes.forEach((addr) => { waku.dial(addr); }); diff --git a/examples/eth-pm-wallet-encryption/src/waku.ts b/examples/eth-pm-wallet-encryption/src/waku.ts index b1c4b5b274..07c3bd1ffa 100644 --- a/examples/eth-pm-wallet-encryption/src/waku.ts +++ b/examples/eth-pm-wallet-encryption/src/waku.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from 'react'; -import { getStatusFleetNodes, Waku, WakuMessage } from 'js-waku'; +import { getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; import { DirectMessage, PublicKeyMessage } from './messaging/wire'; import { validatePublicKeyMessage } from './crypto'; import { Message } from './messaging/Messages'; @@ -14,7 +14,7 @@ export async function initWaku(): Promise { const waku = await Waku.create({}); // Dial all nodes it can find - getStatusFleetNodes().then((nodes) => { + getBootstrapNodes().then((nodes) => { nodes.forEach((addr) => { waku.dial(addr); }); diff --git a/examples/min-react-js-chat/src/App.js b/examples/min-react-js-chat/src/App.js index ce6e902af1..2eeddf446a 100644 --- a/examples/min-react-js-chat/src/App.js +++ b/examples/min-react-js-chat/src/App.js @@ -1,5 +1,5 @@ import './App.css'; -import { getStatusFleetNodes, Waku, WakuMessage } from 'js-waku'; +import { getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; import * as React from 'react'; import protons from 'protons'; @@ -96,8 +96,12 @@ function App() { export default App; async function bootstrapWaku(waku) { - const nodes = await getStatusFleetNodes(); - await Promise.all(nodes.map((addr) => waku.dial(addr))); + try { + const nodes = await getBootstrapNodes(); + await Promise.all(nodes.map((addr) => waku.dial(addr))); + } catch (e) { + console.error('Failed to bootstrap to Waku network'); + } } async function sendMessage(message, timestamp, waku) { diff --git a/examples/store-reactjs-chat/src/App.js b/examples/store-reactjs-chat/src/App.js index ffd23da3a8..fe9551393d 100644 --- a/examples/store-reactjs-chat/src/App.js +++ b/examples/store-reactjs-chat/src/App.js @@ -1,5 +1,5 @@ import './App.css'; -import { getStatusFleetNodes, StoreCodec, Waku } from 'js-waku'; +import { getBootstrapNodes, StoreCodec, Waku } from 'js-waku'; import * as React from 'react'; import protons from 'protons'; @@ -101,8 +101,12 @@ function App() { export default App; async function bootstrapWaku(waku) { - const nodes = await getStatusFleetNodes(); - await Promise.all(nodes.map((addr) => waku.dial(addr))); + try { + const nodes = await getBootstrapNodes(); + await Promise.all(nodes.map((addr) => waku.dial(addr))); + } catch (e) { + console.error('Failed to bootstrap to Waku network'); + } } function decodeMessage(wakuMessage) { diff --git a/examples/web-chat/src/App.tsx b/examples/web-chat/src/App.tsx index 072eebda9b..3814d09bbd 100644 --- a/examples/web-chat/src/App.tsx +++ b/examples/web-chat/src/App.tsx @@ -2,8 +2,7 @@ import { useEffect, useReducer, useState } from 'react'; import './App.css'; import { Direction, - Environment, - getStatusFleetNodes, + getBootstrapNodes, StoreCodec, Waku, WakuMessage, @@ -83,10 +82,8 @@ export default function App() { const persistedNick = window.localStorage.getItem('nick'); return persistedNick !== null ? persistedNick : generate(); }); - const [ - historicalMessagesRetrieved, - setHistoricalMessagesRetrieved, - ] = useState(false); + const [historicalMessagesRetrieved, setHistoricalMessagesRetrieved] = + useState(false); useEffect(() => { localStorage.setItem('nick', nick); @@ -186,7 +183,7 @@ async function initWaku(setter: (waku: Waku) => void) { setter(waku); - const nodes = await getStatusFleetNodes(selectFleetEnv()); + const nodes = await getBootstrapNodes(selectFleetEnv()); await Promise.all( nodes.map((addr) => { return waku.dial(addr); @@ -200,9 +197,9 @@ async function initWaku(setter: (waku: Waku) => void) { function selectFleetEnv() { // Works with react-scripts if (process?.env?.NODE_ENV === 'development') { - return Environment.Test; + return ['fleets', 'wakuv2.test', 'waku-websocket']; } else { - return Environment.Prod; + return ['fleets', 'wakuv2.prod', 'waku-websocket']; } } diff --git a/guides/reactjs-relay.md b/guides/reactjs-relay.md index a9cb34b4ef..30b20936b9 100644 --- a/guides/reactjs-relay.md +++ b/guides/reactjs-relay.md @@ -82,16 +82,18 @@ function App() { # Connect to Other Peers The Waku instance needs to connect to other peers to communicate with the network. -First, create `bootstrapWaku` to connect to the Status fleet: +First, create `bootstrapWaku` to connect to Waku bootstrap nodes (hosted by Status): ```js -import { getStatusFleetNodes } from 'js-waku'; +import { getBootstrapNodes } from 'js-waku'; async function bootstrapWaku(waku) { - // Retrieve node addresses from https://fleets.status.im/ - const nodes = await getStatusFleetNodes(); - // Connect to the nodes - await Promise.all(nodes.map((addr) => waku.dial(addr))); + try { + const nodes = await getBootstrapNodes(); + await Promise.all(nodes.map((addr) => waku.dial(addr))); + } catch (e) { + console.error('Failed to bootstrap to Waku network'); + } } ``` diff --git a/guides/relay-receive-send-messages.md b/guides/relay-receive-send-messages.md index e6890f93bb..47b6637d5e 100644 --- a/guides/relay-receive-send-messages.md +++ b/guides/relay-receive-send-messages.md @@ -34,9 +34,10 @@ You are free to choose any method to bootstrap and DappConnect will ship with ne For now, the easiest way is to connect to Status' Waku fleet: ```js -import { getStatusFleetNodes } from 'js-waku'; -const nodes = await getStatusFleetNodes(); -await Promise.all(nodes.map((addr) => waku.dial(addr))); +import { getBootstrapNodes } from 'js-waku'; + +const nodes = await getBootstrapNodes(); +await Promise.all(nodes.map((addr) => waku.dial(addr))); ``` # Receive messages @@ -177,7 +178,7 @@ Feel free to check out other [guides](menu.md) or [examples](/examples/examples. Here is the final code: ```js -import { getStatusFleetNodes, Waku, WakuMessage } from 'js-waku'; +import { getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; import protons from 'protons'; const proto = protons(` @@ -189,7 +190,7 @@ message SimpleChatMessage { const wakuNode = await Waku.create(); -const nodes = await getStatusFleetNodes(); +const nodes = await getBootstrapNodes(); await Promise.all(nodes.map((addr) => waku.dial(addr))); const processIncomingMessage = (wakuMessage) => { diff --git a/guides/store-retrieve-messages.md b/guides/store-retrieve-messages.md index b49d7e85d9..acd5f71551 100644 --- a/guides/store-retrieve-messages.md +++ b/guides/store-retrieve-messages.md @@ -46,12 +46,17 @@ const wakuNode = await Waku.create(); The Waku instance needs to connect to other peers to communicate with the network. You are free to choose other methods to bootstrap and DappConnect will ship with new bootstrap mechanisms in the future. -For now, the easiest way is to connect to Status' Waku fleet: +For now, the easiest way is to connect to Waku bootstrap nodes: ```js -import { getStatusFleetNodes } from 'js-waku'; -const nodes = await getStatusFleetNodes(); -await Promise.all(nodes.map((addr) => waku.dial(addr))); +import { getBootstrapNodes } from 'js-waku'; + +try { + const nodes = await getBootstrapNodes(); + await Promise.all(nodes.map((addr) => waku.dial(addr))); +} catch (e) { + console.error('Failed to bootstrap to Waku network'); +} ``` # Use Protobuf diff --git a/src/index.ts b/src/index.ts index e952044319..9fa7f20c8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export { getStatusFleetNodes, Environment, Protocol } from './lib/discover'; +export { getBootstrapNodes } from './lib/discovery'; export * as utils from './lib/utils'; diff --git a/src/lib/discover.ts b/src/lib/discover.ts deleted file mode 100644 index b532630a36..0000000000 --- a/src/lib/discover.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Returns multiaddrs (inc. ip) of nim-waku nodes ran by Status. - * Used as a temporary discovery helper until more parties run their own nodes. - */ -import axios from 'axios'; - -export enum Protocol { - websocket = 'websocket', - tcp = 'tcp', -} - -export enum Environment { - Test = 'test', - Prod = 'prod', -} - -export async function getStatusFleetNodes( - env: Environment = Environment.Prod, - protocol: Protocol = Protocol.websocket -): Promise { - const res = await axios.get('https://fleets.status.im/', { - headers: { 'Content-Type': 'application/json' }, - }); - - const wakuFleet = res.data.fleets[`wakuv2.${env}`]; - - switch (protocol) { - case Protocol.tcp: - return Object.values(wakuFleet['waku']); - default: - return Object.values(wakuFleet['waku-websocket']); - } -} diff --git a/src/lib/discovery.ts b/src/lib/discovery.ts new file mode 100644 index 0000000000..6b75947423 --- /dev/null +++ b/src/lib/discovery.ts @@ -0,0 +1,56 @@ +import axios from 'axios'; +import debug from 'debug'; + +const dbg = debug('waku:discovery'); + +/** + * GET list of nodes from remote HTTP host. + * + * @param path The property path to access the node list. The result should be + * a string, a string array or an object. If the result is an object then the + * values of the objects are used as multiaddresses. + * @param url Remote host containing bootstrap peers in JSON format. + * + * @returns An array of multiaddresses. + * @throws If the remote host is unreachable or the response cannot be parsed + * according to the passed _path_. + */ +export async function getBootstrapNodes( + path: string[] = ['fleets', 'wakuv2.prod', 'waku-websocket'], + url = 'https://fleets.status.im/' +): Promise { + const res = await axios.get(url, { + headers: { 'Content-Type': 'application/json' }, + }); + + let nodes = res.data; + + for (const prop of path) { + if (nodes[prop] === undefined) { + dbg( + `Failed to retrieve bootstrap nodes: ${prop} does not exist on `, + nodes + ); + throw `Failed to retrieve bootstrap nodes: ${prop} does not exist on ${JSON.stringify( + nodes + )}`; + } + nodes = nodes[prop]; + } + + if (Array.isArray(nodes)) { + return nodes; + } + + if (typeof nodes === 'string') { + return [nodes]; + } + + if (typeof nodes === 'object') { + return Object.values(nodes); + } + + throw `Failed to retrieve bootstrap nodes: response format is not supported: ${JSON.stringify( + nodes + )}`; +} From 140791cc91da54ce37a97bbdae0af46cf5e22279 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 10 Aug 2021 11:32:14 +1000 Subject: [PATCH 2/5] Provide easy way to bootstrap when creating Waku node --- .cspell.json | 1 + CHANGELOG.md | 6 +- README.md | 27 +----- examples/eth-dm/src/waku.ts | 11 +-- examples/eth-pm-wallet-encryption/src/waku.ts | 11 +-- examples/store-reactjs-chat/src/App.js | 37 ++------- examples/web-chat/src/App.tsx | 14 ++-- guides/store-retrieve-messages.md | 24 +++--- package-lock.json | 60 ++++++++++++++ package.json | 1 + src/lib/discovery.ts | 2 + src/lib/waku.spec.ts | 82 ++++++++++++++++++- src/lib/waku.ts | 51 ++++++++++++ 13 files changed, 227 insertions(+), 100 deletions(-) diff --git a/.cspell.json b/.cspell.json index 90db492bb2..499e87d912 100644 --- a/.cspell.json +++ b/.cspell.json @@ -47,6 +47,7 @@ "livechat", "mkdir", "multiaddr", + "multiaddresses", "multiaddrs", "multicodecs", "mplex", diff --git a/CHANGELOG.md b/CHANGELOG.md index b439926b7f..cfbc8032bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- New `bootstrap` option for `Waku.create` to easily connect to Waku nodes upon start up. + ### Changed -- Renamed `discover.getStatusFleetNodes` to `discovery.getBootstrapNodes` and make it more generic to allow retrieval of bootstrap nodes from other sources. +- Renamed `discover.getStatusFleetNodes` to `discovery.getBootstrapNodes`; + Changed the API to allow retrieval of bootstrap nodes from other sources. ### Removed - Examples (cli-chat): The focus of this library is Web environment; diff --git a/README.md b/README.md index 5d502bdfdf..91780aef5b 100644 --- a/README.md +++ b/README.md @@ -32,32 +32,7 @@ npm install js-waku ```ts import { Waku } from 'js-waku'; -const waku = await Waku.create(); -``` - -### Connect to a new peer - -```ts -// Directly dial a new peer -await waku.dial('/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ'); - -// Or, add peer to address book so it auto dials in the background -waku.addPeerToAddressBook( - '16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ', - ['/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss'] -); -``` - -You can also use `getBootstrapNodes` to connect to Waku bootstrap nodes: - -```ts -import { getBootstrapNodes } from 'js-waku'; - -getBootstrapNodes().then((nodes) => { - nodes.forEach((addr) => { - waku.dial(addr); - }); -}); +const waku = await Waku.create({ bootstrap: true }); ``` ### Listen for messages diff --git a/examples/eth-dm/src/waku.ts b/examples/eth-dm/src/waku.ts index 5d24ab60a1..83214e1457 100644 --- a/examples/eth-dm/src/waku.ts +++ b/examples/eth-dm/src/waku.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from 'react'; -import { getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; +import { Waku, WakuMessage } from 'js-waku'; import { DirectMessage, PublicKeyMessage } from './messaging/wire'; import { validatePublicKeyMessage } from './crypto'; import { Message } from './messaging/Messages'; @@ -9,14 +9,7 @@ export const PublicKeyContentTopic = '/eth-dm/1/public-key/proto'; export const DirectMessageContentTopic = '/eth-dm/1/direct-message/proto'; export async function initWaku(): Promise { - const waku = await Waku.create({}); - - // Dial all nodes it can find - getBootstrapNodes().then((nodes) => { - nodes.forEach((addr) => { - waku.dial(addr); - }); - }); + const waku = await Waku.create({ bootstrap: true }); // Wait to be connected to at least one peer await new Promise((resolve, reject) => { diff --git a/examples/eth-pm-wallet-encryption/src/waku.ts b/examples/eth-pm-wallet-encryption/src/waku.ts index 07c3bd1ffa..c85649276c 100644 --- a/examples/eth-pm-wallet-encryption/src/waku.ts +++ b/examples/eth-pm-wallet-encryption/src/waku.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from 'react'; -import { getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; +import { Waku, WakuMessage } from 'js-waku'; import { DirectMessage, PublicKeyMessage } from './messaging/wire'; import { validatePublicKeyMessage } from './crypto'; import { Message } from './messaging/Messages'; @@ -11,14 +11,7 @@ export const DirectMessageContentTopic = '/eth-pm-wallet/1/direct-message/proto'; export async function initWaku(): Promise { - const waku = await Waku.create({}); - - // Dial all nodes it can find - getBootstrapNodes().then((nodes) => { - nodes.forEach((addr) => { - waku.dial(addr); - }); - }); + const waku = await Waku.create({ bootstrap: true }); // Wait to be connected to at least one peer await new Promise((resolve, reject) => { diff --git a/examples/store-reactjs-chat/src/App.js b/examples/store-reactjs-chat/src/App.js index fe9551393d..fbb206b2dc 100644 --- a/examples/store-reactjs-chat/src/App.js +++ b/examples/store-reactjs-chat/src/App.js @@ -1,5 +1,5 @@ import './App.css'; -import { getBootstrapNodes, StoreCodec, Waku } from 'js-waku'; +import { StoreCodec, Waku } from 'js-waku'; import * as React from 'react'; import protons from 'protons'; @@ -17,9 +17,6 @@ function App() { const [waku, setWaku] = React.useState(undefined); const [wakuStatus, setWakuStatus] = React.useState('None'); const [messages, setMessages] = React.useState([]); - // Set to true when Waku connects to a store node - // it does not reflect whether we then disconnected from said node. - const [connectedToStore, setConnectedToStore] = React.useState(false); React.useEffect(() => { if (!!waku) return; @@ -27,20 +24,15 @@ function App() { setWakuStatus('Starting'); - Waku.create().then((waku) => { + Waku.create({ bootstrap: true }).then((waku) => { setWaku(waku); setWakuStatus('Connecting'); - bootstrapWaku(waku).then(() => { - setWakuStatus('Ready'); - }); }); }, [waku, wakuStatus]); React.useEffect(() => { if (!waku) return; - // This is superfluous as the try/catch block would catch the failure if - // we are indeed not connected to any store node. - if (!connectedToStore) return; + if (wakuStatus !== 'Connected to Store') return; const interval = setInterval(() => { waku.store @@ -57,33 +49,27 @@ function App() { }, 10000); return () => clearInterval(interval); - }, [waku, connectedToStore]); + }, [waku, wakuStatus]); React.useEffect(() => { if (!waku) return; // We do not handle disconnection/re-connection in this example - if (connectedToStore) return; + if (wakuStatus === 'Connected to Store') return; const isStoreNode = ({ protocols }) => { if (protocols.includes(StoreCodec)) { // We are now connected to a store node - setConnectedToStore(true); + setWakuStatus('Connected to Store'); } }; - // This demonstrates how to wait for a connection to a store node. - // - // This is only for demonstration purposes. It is not really needed in this - // example app as we query the store node every 10s and catch if it fails. - // Meaning if we are not connected to a store node, then it just fails and - // we try again 10s later. waku.libp2p.peerStore.on('change:protocols', isStoreNode); return () => { waku.libp2p.peerStore.removeListener('change:protocols', isStoreNode); }; - }, [waku, connectedToStore]); + }, [waku, wakuStatus]); return (
@@ -100,15 +86,6 @@ function App() { export default App; -async function bootstrapWaku(waku) { - try { - const nodes = await getBootstrapNodes(); - await Promise.all(nodes.map((addr) => waku.dial(addr))); - } catch (e) { - console.error('Failed to bootstrap to Waku network'); - } -} - function decodeMessage(wakuMessage) { if (!wakuMessage.payload) return; diff --git a/examples/web-chat/src/App.tsx b/examples/web-chat/src/App.tsx index 3814d09bbd..fdc292e938 100644 --- a/examples/web-chat/src/App.tsx +++ b/examples/web-chat/src/App.tsx @@ -82,8 +82,10 @@ export default function App() { const persistedNick = window.localStorage.getItem('nick'); return persistedNick !== null ? persistedNick : generate(); }); - const [historicalMessagesRetrieved, setHistoricalMessagesRetrieved] = - useState(false); + const [ + historicalMessagesRetrieved, + setHistoricalMessagesRetrieved, + ] = useState(false); useEffect(() => { localStorage.setItem('nick', nick); @@ -179,16 +181,10 @@ async function initWaku(setter: (waku: Waku) => void) { }, }, }, + bootstrap: getBootstrapNodes.bind({}, selectFleetEnv()), }); setter(waku); - - const nodes = await getBootstrapNodes(selectFleetEnv()); - await Promise.all( - nodes.map((addr) => { - return waku.dial(addr); - }) - ); } catch (e) { console.log('Issue starting waku ', e); } diff --git a/guides/store-retrieve-messages.md b/guides/store-retrieve-messages.md index acd5f71551..fc1672c922 100644 --- a/guides/store-retrieve-messages.md +++ b/guides/store-retrieve-messages.md @@ -38,25 +38,21 @@ In order to interact with the Waku network, you first need a Waku instance: ```js import { Waku } from 'js-waku'; -const wakuNode = await Waku.create(); +const wakuNode = await Waku.create({ bootstrap: true }); ``` -# Connect to Other Peers - -The Waku instance needs to connect to other peers to communicate with the network. -You are free to choose other methods to bootstrap and DappConnect will ship with new bootstrap mechanisms in the future. - -For now, the easiest way is to connect to Waku bootstrap nodes: +Passing the `bootstrap` option will connect your node to predefined Waku nodes hosted by Status. +If you want to bootstrap to your own nodes, you can pass an array of multiaddresses instead: ```js -import { getBootstrapNodes } from 'js-waku'; +import { Waku } from 'js-waku'; -try { - const nodes = await getBootstrapNodes(); - await Promise.all(nodes.map((addr) => waku.dial(addr))); -} catch (e) { - console.error('Failed to bootstrap to Waku network'); -} +const wakuNode = await Waku.create({ + bootstrap: [ + '/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm', + '/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ' + ] +}); ``` # Use Protobuf diff --git a/package-lock.json b/package-lock.json index a2ea0fe611..d50285af27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "it-length-prefixed": "^5.0.2", "js-sha3": "^0.8.0", "libp2p": "^0.32.0", + "libp2p-bootstrap": "^0.13.0", "libp2p-gossipsub": "^0.10.0", "libp2p-mplex": "^0.10.4", "libp2p-noise": "^4.0.0", @@ -15055,6 +15056,39 @@ "node": ">=14.0.0" } }, + "node_modules/libp2p-bootstrap": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/libp2p-bootstrap/-/libp2p-bootstrap-0.13.0.tgz", + "integrity": "sha512-8sXEZrikY+chKvMorkvOi9E/v9GvwsYr9DAEfzQZrOKQZByqhan1aXQKWrSpc4AxEv5/UopRzu1P47bkOi8wdw==", + "dependencies": { + "debug": "^4.3.1", + "mafmt": "^10.0.0", + "multiaddr": "^10.0.0", + "peer-id": "^0.15.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/libp2p-bootstrap/node_modules/peer-id": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/peer-id/-/peer-id-0.15.2.tgz", + "integrity": "sha512-3OMbup76F28gKsQK4rGheEJHwosnJGe2+Obsf1xFaS9DpUaG9/JK0rtguWVLbrkxPclsCceci8g3/ulg8jsORA==", + "dependencies": { + "class-is": "^1.1.0", + "libp2p-crypto": "^0.19.0", + "minimist": "^1.2.5", + "multiformats": "^9.3.0", + "protobufjs": "^6.10.2", + "uint8arrays": "^2.0.5" + }, + "bin": { + "peer-id": "src/bin.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/libp2p-crypto": { "version": "0.19.6", "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.19.6.tgz", @@ -36622,6 +36656,32 @@ } } }, + "libp2p-bootstrap": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/libp2p-bootstrap/-/libp2p-bootstrap-0.13.0.tgz", + "integrity": "sha512-8sXEZrikY+chKvMorkvOi9E/v9GvwsYr9DAEfzQZrOKQZByqhan1aXQKWrSpc4AxEv5/UopRzu1P47bkOi8wdw==", + "requires": { + "debug": "^4.3.1", + "mafmt": "^10.0.0", + "multiaddr": "^10.0.0", + "peer-id": "^0.15.0" + }, + "dependencies": { + "peer-id": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/peer-id/-/peer-id-0.15.2.tgz", + "integrity": "sha512-3OMbup76F28gKsQK4rGheEJHwosnJGe2+Obsf1xFaS9DpUaG9/JK0rtguWVLbrkxPclsCceci8g3/ulg8jsORA==", + "requires": { + "class-is": "^1.1.0", + "libp2p-crypto": "^0.19.0", + "minimist": "^1.2.5", + "multiformats": "^9.3.0", + "protobufjs": "^6.10.2", + "uint8arrays": "^2.0.5" + } + } + } + }, "libp2p-crypto": { "version": "0.19.6", "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.19.6.tgz", diff --git a/package.json b/package.json index b6f3799250..3e45650197 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "it-length-prefixed": "^5.0.2", "js-sha3": "^0.8.0", "libp2p": "^0.32.0", + "libp2p-bootstrap": "^0.13.0", "libp2p-gossipsub": "^0.10.0", "libp2p-mplex": "^0.10.4", "libp2p-noise": "^4.0.0", diff --git a/src/lib/discovery.ts b/src/lib/discovery.ts index 6b75947423..f631909922 100644 --- a/src/lib/discovery.ts +++ b/src/lib/discovery.ts @@ -6,6 +6,8 @@ const dbg = debug('waku:discovery'); /** * GET list of nodes from remote HTTP host. * + * Default behaviour is to return nodes hosted by Status. + * * @param path The property path to access the node list. The result should be * a string, a string array or an object. If the result is an object then the * values of the objects are used as multiaddresses. diff --git a/src/lib/waku.spec.ts b/src/lib/waku.spec.ts index bad7599e33..797700bf3d 100644 --- a/src/lib/waku.spec.ts +++ b/src/lib/waku.spec.ts @@ -2,20 +2,98 @@ import { expect } from 'chai'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: No types available import TCP from 'libp2p-tcp'; +import PeerId from 'peer-id'; -import { makeLogFileName, NimWaku, NOISE_KEY_1 } from '../test_utils/'; +import { + makeLogFileName, + NimWaku, + NOISE_KEY_1, + NOISE_KEY_2, +} from '../test_utils/'; import { Waku } from './waku'; describe('Waku Dial', function () { let waku: Waku; + let waku2: Waku; let nimWaku: NimWaku; afterEach(async function () { this.timeout(10_000); nimWaku ? nimWaku.stop() : null; - waku ? await waku.stop() : null; + + await Promise.all([waku ? waku.stop() : null, waku2 ? waku2.stop() : null]); + }); + + describe('Bootstrap', function () { + it('Passing an array', async function () { + this.timeout(10_000); + + waku = await Waku.create({ + staticNoiseKey: NOISE_KEY_1, + libp2p: { + addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] }, + modules: { transport: [TCP] }, + }, + }); + + const multiAddrWithId = waku.getLocalMultiaddrWithID(); + + waku2 = await Waku.create({ + staticNoiseKey: NOISE_KEY_2, + libp2p: { + modules: { transport: [TCP] }, + }, + bootstrap: [multiAddrWithId], + }); + + const connectedPeerID: PeerId = await new Promise((resolve) => { + waku.libp2p.connectionManager.on('peer:connect', (connection) => { + resolve(connection.remotePeer); + }); + }); + + expect(connectedPeerID.toB58String()).to.eq( + waku2.libp2p.peerId.toB58String() + ); + }); + }); + + describe('Bootstrap', function () { + it('Passing a function', async function () { + this.timeout(10_000); + + waku = await Waku.create({ + staticNoiseKey: NOISE_KEY_1, + libp2p: { + addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] }, + modules: { transport: [TCP] }, + }, + }); + + const multiAddrWithId = waku.getLocalMultiaddrWithID(); + + waku2 = await Waku.create({ + staticNoiseKey: NOISE_KEY_2, + libp2p: { + modules: { transport: [TCP] }, + }, + bootstrap: () => { + return [multiAddrWithId]; + }, + }); + + const connectedPeerID: PeerId = await new Promise((resolve) => { + waku.libp2p.connectionManager.on('peer:connect', (connection) => { + resolve(connection.remotePeer); + }); + }); + + expect(connectedPeerID.toB58String()).to.eq( + waku2.libp2p.peerId.toB58String() + ); + }); }); describe('Interop: Nim', function () { diff --git a/src/lib/waku.ts b/src/lib/waku.ts index 49c813cd5b..1108dd94f4 100644 --- a/src/lib/waku.ts +++ b/src/lib/waku.ts @@ -1,4 +1,6 @@ +import debug from 'debug'; import Libp2p, { Connection, Libp2pModules, Libp2pOptions } from 'libp2p'; +import Bootstrap from 'libp2p-bootstrap'; import { MuxedStream } from 'libp2p-interfaces/dist/src/stream-muxer/types'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: No types available @@ -15,6 +17,7 @@ import Ping from 'libp2p/src/ping'; import { Multiaddr, multiaddr } from 'multiaddr'; import PeerId from 'peer-id'; +import { getBootstrapNodes } from './discovery'; import { WakuLightPush } from './waku_light_push'; import { WakuMessage } from './waku_message'; import { RelayCodecs, WakuRelay } from './waku_relay'; @@ -26,6 +29,8 @@ const websocketsTransportKey = Websockets.prototype[Symbol.toStringTag]; const DefaultPingKeepAliveValueSecs = 0; const DefaultRelayKeepAliveValueSecs = 5 * 60; +const dbg = debug('waku:waku'); + export interface CreateOptions { /** * The PubSub Topic to use. Defaults to {@link DefaultPubsubTopic}. @@ -71,6 +76,18 @@ export interface CreateOptions { * This is only used for test purposes to not run out of entropy during CI runs. */ staticNoiseKey?: bytes; + /** + * Use libp2p-bootstrap to discover and connect to new nodes. + * + * You can pass: + * - `true` to use {@link getBootstrapNodes}, + * - an array of multiaddresses, + * - a function that returns an array of multiaddresses (or Promise of). + * + * Note: It overrides any other peerDiscovery modules that may have been set via + * {@link CreateOptions.libp2p}. + */ + bootstrap?: boolean | string[] | (() => string[] | Promise); } export class Waku { @@ -161,6 +178,40 @@ export class Waku { pubsub: WakuRelay, }); + if (options?.bootstrap) { + let bootstrap: undefined | (() => string[] | Promise); + + if (options.bootstrap === true) { + bootstrap = getBootstrapNodes; + } else if (Array.isArray(options.bootstrap)) { + bootstrap = (): string[] => { + return options.bootstrap as string[]; + }; + } else if (typeof options.bootstrap === 'function') { + bootstrap = options.bootstrap; + } + + if (bootstrap !== undefined) { + // Note: this overrides any other peer discover + libp2pOpts.modules = Object.assign(libp2pOpts.modules, { + peerDiscovery: [Bootstrap], + }); + + try { + const list = await bootstrap(); + + libp2pOpts.config.peerDiscovery = { + [Bootstrap.tag]: { + list, + enabled: true, + }, + }; + } catch (e) { + dbg('Failed to retrieve bootstrap nodes', e); + } + } + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: modules property is correctly set thanks to voodoo const libp2p = await Libp2p.create(libp2pOpts); From bac26ea3da19b12f605f6e667ac723a80068f0fd Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 11 Aug 2021 10:32:15 +1000 Subject: [PATCH 3/5] fixup --- src/lib/discovery.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/discovery.ts b/src/lib/discovery.ts index f631909922..4999bdcb33 100644 --- a/src/lib/discovery.ts +++ b/src/lib/discovery.ts @@ -10,7 +10,9 @@ const dbg = debug('waku:discovery'); * * @param path The property path to access the node list. The result should be * a string, a string array or an object. If the result is an object then the - * values of the objects are used as multiaddresses. + * values of the objects are used as multiaddresses. For example, if the GET + * request returns `{ foo: { bar: [address1, address2] } }` then `path` should be + * `[ "foo", "bar" ]`. * @param url Remote host containing bootstrap peers in JSON format. * * @returns An array of multiaddresses. From 6d42c392986cab864377f089c4e61ca8f859c6c8 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Wed, 11 Aug 2021 10:34:42 +1000 Subject: [PATCH 4/5] export values so they are included in the documentation --- src/lib/waku.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/waku.ts b/src/lib/waku.ts index 1108dd94f4..c3c1fe9c2c 100644 --- a/src/lib/waku.ts +++ b/src/lib/waku.ts @@ -26,8 +26,8 @@ import { StoreCodec, WakuStore } from './waku_store'; const websocketsTransportKey = Websockets.prototype[Symbol.toStringTag]; -const DefaultPingKeepAliveValueSecs = 0; -const DefaultRelayKeepAliveValueSecs = 5 * 60; +export const DefaultPingKeepAliveValueSecs = 0; +export const DefaultRelayKeepAliveValueSecs = 5 * 60; const dbg = debug('waku:waku'); From 6ded9630b286ca5f036b96d152f8fbb127ab2845 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Fri, 13 Aug 2021 16:02:54 +1000 Subject: [PATCH 5/5] Remove mention of Status in guide Keep it in the code doc. --- guides/reactjs-relay.md | 2 +- guides/store-retrieve-messages.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/reactjs-relay.md b/guides/reactjs-relay.md index 30b20936b9..be576608a9 100644 --- a/guides/reactjs-relay.md +++ b/guides/reactjs-relay.md @@ -82,7 +82,7 @@ function App() { # Connect to Other Peers The Waku instance needs to connect to other peers to communicate with the network. -First, create `bootstrapWaku` to connect to Waku bootstrap nodes (hosted by Status): +First, create `bootstrapWaku` to connect to Waku bootstrap nodes: ```js import { getBootstrapNodes } from 'js-waku'; diff --git a/guides/store-retrieve-messages.md b/guides/store-retrieve-messages.md index fc1672c922..822009f0e9 100644 --- a/guides/store-retrieve-messages.md +++ b/guides/store-retrieve-messages.md @@ -41,7 +41,7 @@ import { Waku } from 'js-waku'; const wakuNode = await Waku.create({ bootstrap: true }); ``` -Passing the `bootstrap` option will connect your node to predefined Waku nodes hosted by Status. +Passing the `bootstrap` option will connect your node to predefined Waku nodes. If you want to bootstrap to your own nodes, you can pass an array of multiaddresses instead: ```js