From a21d641280c952b5245aefb7b8b09d65bb9dd5f0 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 2 Sep 2021 15:01:52 +1000 Subject: [PATCH] Added `Waku.waitForConnectedPeer` helper To ensure that we are connected to Waku peers when using the bootstrap option. --- CHANGELOG.md | 1 + examples/min-react-js-chat/src/App.js | 15 ++------- examples/store-reactjs-chat/src/App.js | 18 +++-------- examples/web-chat/src/App.tsx | 42 ++++++++---------------- guides/reactjs-relay.md | 35 ++++++-------------- guides/relay-receive-send-messages.md | 32 +++++++++++++------ guides/store-retrieve-messages.md | 28 +++++++--------- src/lib/waku.ts | 44 +++++++++++++++++++++++++- 8 files changed, 110 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d69a3d8a4..ed6a710df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow passing decryption keys in hex string format. - Allow passing decryption keys to `WakuStore` instance to avoid having to pass them at every `queryHistory` call. - Allow passing decryption keys to `Waku` instance to avoid having to pass them to both `WakuRelay` and `WakuStore`. +- `Waku.waitForConnectedPeer` helper to ensure that we are connected to Waku peers when using the bootstrap option. ### Changed - **Breaking**: Moved `startTime` and `endTime` for history queries to a `timeFilter` property as both or neither must be passed; passing only one parameter is not supported. diff --git a/examples/min-react-js-chat/src/App.js b/examples/min-react-js-chat/src/App.js index 2eeddf446a..0110c0920e 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 { getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; +import { Waku, WakuMessage } from 'js-waku'; import * as React from 'react'; import protons from 'protons'; @@ -24,10 +24,10 @@ function App() { setWakuStatus('Starting'); - Waku.create().then((waku) => { + Waku.create({ bootstrap: true }).then((waku) => { setWaku(waku); setWakuStatus('Connecting'); - bootstrapWaku(waku).then(() => { + waku.waitForConnectedPeer().then(() => { setWakuStatus('Ready'); }); }); @@ -95,15 +95,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'); - } -} - async function sendMessage(message, timestamp, waku) { const time = timestamp.getTime(); diff --git a/examples/store-reactjs-chat/src/App.js b/examples/store-reactjs-chat/src/App.js index fbb206b2dc..0143015d44 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 { StoreCodec, Waku } from 'js-waku'; +import { Waku } from 'js-waku'; import * as React from 'react'; import protons from 'protons'; @@ -57,18 +57,10 @@ function App() { // We do not handle disconnection/re-connection in this example if (wakuStatus === 'Connected to Store') return; - const isStoreNode = ({ protocols }) => { - if (protocols.includes(StoreCodec)) { - // We are now connected to a store node - setWakuStatus('Connected to Store'); - } - }; - - waku.libp2p.peerStore.on('change:protocols', isStoreNode); - - return () => { - waku.libp2p.peerStore.removeListener('change:protocols', isStoreNode); - }; + waku.waitForConnectedPeer().then(() => { + // We are now connected to a store node + setWakuStatus('Connected to Store'); + }); }, [waku, wakuStatus]); return ( diff --git a/examples/web-chat/src/App.tsx b/examples/web-chat/src/App.tsx index 27fd4a3b08..bebbe23cc5 100644 --- a/examples/web-chat/src/App.tsx +++ b/examples/web-chat/src/App.tsx @@ -1,12 +1,6 @@ import { useEffect, useReducer, useState } from 'react'; import './App.css'; -import { - Direction, - getBootstrapNodes, - StoreCodec, - Waku, - WakuMessage, -} from 'js-waku'; +import { Direction, getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; import handleCommand from './command'; import Room from './Room'; import { WakuContext } from './WakuContext'; @@ -79,8 +73,8 @@ async function retrieveStoreMessages( }); return res.length; - } catch { - console.log('Failed to retrieve messages'); + } catch (e) { + console.log('Failed to retrieve messages', e); return 0; } } @@ -131,29 +125,21 @@ export default function App() { if (!waku) return; if (historicalMessagesRetrieved) return; - const checkAndRetrieve = ({ protocols }: { protocols: string[] }) => { - if (protocols.includes(StoreCodec)) { - console.log(`Retrieving archived messages}`); - setHistoricalMessagesRetrieved(true); + const retrieveMessages = async () => { + await waku.waitForConnectedPeer(); + console.log(`Retrieving archived messages}`); - try { - retrieveStoreMessages(waku, dispatchMessages).then((length) => - console.log(`Messages retrieved:`, length) - ); - } catch (e) { - console.log(`Error encountered when retrieving archived messages`, e); - } + try { + retrieveStoreMessages(waku, dispatchMessages).then((length) => { + console.log(`Messages retrieved:`, length); + setHistoricalMessagesRetrieved(true); + }); + } catch (e) { + console.log(`Error encountered when retrieving archived messages`, e); } }; - waku.libp2p.peerStore.on('change:protocols', checkAndRetrieve); - - return () => { - waku.libp2p.peerStore.removeListener( - 'change:protocols', - checkAndRetrieve - ); - }; + retrieveMessages(); }, [waku, historicalMessagesRetrieved]); return ( diff --git a/guides/reactjs-relay.md b/guides/reactjs-relay.md index be576608a9..fc5678a6d0 100644 --- a/guides/reactjs-relay.md +++ b/guides/reactjs-relay.md @@ -60,7 +60,7 @@ function App() { setWakuStatus('Starting'); // Create Waku - Waku.create().then((waku) => { + Waku.create({ bootstrap: true }).then((waku) => { // Once done, put it in the state setWaku(waku); // And update the status @@ -69,8 +69,8 @@ function App() { }, [waku, wakuStatus]); return ( -
-
+
+
// Display the status on the web page

{wakuStatus}

@@ -79,25 +79,11 @@ function App() { } ``` -# Connect to Other Peers +# Wait to be connected -The Waku instance needs to connect to other peers to communicate with the network. -First, create `bootstrapWaku` to connect to Waku bootstrap nodes: - -```js -import { getBootstrapNodes } from 'js-waku'; - -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'); - } -} -``` - -Then, bootstrap after Waku is created in the previous `useEffect` block: +When using the `bootstrap` option, it may take some times to connect to other peers. +To ensure that you have relay peers available to send and receive messages, +use the `Waku.waitForConnectedPeer()` async function: ```js React.useEffect(() => { @@ -106,17 +92,16 @@ React.useEffect(() => { setWakuStatus('Starting'); - Waku.create().then((waku) => { + Waku.create({ bootstrap: true }).then((waku) => { setWaku(waku); setWakuStatus('Connecting'); - bootstrapWaku(waku).then(() => { + waku.waitForConnectedPeer().then(() => { setWakuStatus('Ready'); }); }); }, [waku, wakuStatus]); -``` -DappConnect will provide more discovery and bootstrap methods over time, or you can make your own. +``` # Define Message Format diff --git a/guides/relay-receive-send-messages.md b/guides/relay-receive-send-messages.md index 47b6637d5e..60e868f9fc 100644 --- a/guides/relay-receive-send-messages.md +++ b/guides/relay-receive-send-messages.md @@ -23,23 +23,35 @@ 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 any method to bootstrap and DappConnect will ship with new methods in the future. - -For now, the easiest way is to connect to Status' Waku fleet: +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 -import { getBootstrapNodes } from 'js-waku'; +import { Waku } from 'js-waku'; -const nodes = await getBootstrapNodes(); -await Promise.all(nodes.map((addr) => waku.dial(addr))); +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' + ] +}); ``` +# Wait to be connected + +When using the `bootstrap` option, it may take some times to connect to other peers. +To ensure that you have relay peers available to send and receive messages, +use the following function: + +```js +await waku.waitForConnectedPeer(); +``` + +The returned Promise will resolve once you are connected to a Waku Relay peer. + # Receive messages To watch messages for your app, you need to register an observer on relay for your app's content topic: diff --git a/guides/store-retrieve-messages.md b/guides/store-retrieve-messages.md index 822009f0e9..2859c3e4b8 100644 --- a/guides/store-retrieve-messages.md +++ b/guides/store-retrieve-messages.md @@ -55,6 +55,18 @@ const wakuNode = await Waku.create({ }); ``` +# Wait to be connected + +When using the `bootstrap` option, it may take some times to connect to other peers. +To ensure that you have store peers available to retrieve historical messages from, +use the following function: + +```js +await waku.waitForConnectedPeer(); +``` + +The returned Promise will resolve once you are connected to a Waku Store peer. + # Use Protobuf Waku v2 protocols use [protobuf](https://developers.google.com/protocol-buffers/) [by default](https://rfc.vac.dev/spec/10/). @@ -153,19 +165,3 @@ Note that `WakuStore.queryHistory` select an available store node for you. However, it can only select a connected node, which is why the bootstrapping is necessary. It will throw an error if no store node is available. -## Wait to be connected - -Depending on your dApp design, you may want to wait for a store node to be available first. -In this case, you can listen for the [PeerStore's change protocol event](https://github.com/libp2p/js-libp2p/blob/master/doc/API.md#known-protocols-for-a-peer-change) -to know whether any of your connected peers is a store peer: - -```js -import { StoreCodec } from 'js-waku'; - -// Or using a callback -waku.libp2p.peerStore.on('change:protocols', ({ peerId, protocols }) => { - if (protocols.includes(StoreCodec)) { - // A Store node is available! - } -}); -``` diff --git a/src/lib/waku.ts b/src/lib/waku.ts index 729ef77066..55833a6350 100644 --- a/src/lib/waku.ts +++ b/src/lib/waku.ts @@ -13,12 +13,14 @@ import Websockets from 'libp2p-websockets'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: No types available import filters from 'libp2p-websockets/src/filters'; +import { Peer } from 'libp2p/dist/src/peer-store'; 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 { getPeersForProtocol } from './select_peer'; +import { LightPushCodec, WakuLightPush } from './waku_light_push'; import { WakuMessage } from './waku_message'; import { RelayCodecs, WakuRelay } from './waku_relay'; import { RelayPingContentTopic } from './waku_relay/constants'; @@ -310,6 +312,46 @@ export class Waku { return localMultiaddr + '/p2p/' + this.libp2p.peerId.toB58String(); } + /** + * Wait to be connected to a peer. Useful when using the [[CreateOptions.bootstrap]] + * with [[Waku.create]]. The Promise resolves only once we are connected to a + * Store peer, Relay peer and Light Push peer. + */ + async waitForConnectedPeer(): Promise { + const desiredProtocols = [[StoreCodec], [LightPushCodec], RelayCodecs]; + + await Promise.all( + desiredProtocols.map((desiredProtocolVersions) => { + const peers = new Array(); + desiredProtocolVersions.forEach((proto) => { + getPeersForProtocol(this.libp2p, proto).forEach((peer) => + peers.push(peer) + ); + }); + dbg('peers for ', desiredProtocolVersions, peers); + + if (peers.length > 0) { + return Promise.resolve(); + } else { + // No peer available for this protocol, waiting to connect to one. + return new Promise((resolve) => { + this.libp2p.peerStore.on( + 'change:protocols', + ({ protocols: connectedPeerProtocols }) => { + desiredProtocolVersions.forEach((desiredProto) => { + if (connectedPeerProtocols.includes(desiredProto)) { + dbg('Resolving for', desiredProto, connectedPeerProtocols); + resolve(); + } + }); + } + ); + }); + } + }) + ); + } + private startKeepAlive( peerId: PeerId, pingPeriodSecs: number,