move example from js-noise repo
This commit is contained in:
parent
266916be74
commit
465fe17e34
|
@ -0,0 +1,7 @@
|
|||
# Waku Noise Pairing example app
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
Browse http://localhost:8080 and open the developer tools to see console logs
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title>JS-Waku light node example</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Press F12 to open the console</p>
|
||||
<p><b>Waku Status:</b> <span id="waku-status">connecting...</span></p>
|
||||
<p id="handshake-span">
|
||||
<b>Handshake Status:</b> <span id="handshake-status">-</span>
|
||||
</p>
|
||||
<canvas id="qr-canvas"></canvas>
|
||||
<a href="#" id="qr-url" style="display: none" target="_blank"
|
||||
>Open QR code link in new window instead of scanning it.</a
|
||||
>
|
||||
|
||||
<div id="chat-area" style="display: none">
|
||||
<label for="nick-input">Your nickname</label>
|
||||
<input id="nick-input" placeholder="Choose a nickname" type="text" />
|
||||
<label for="text-input">Message text</label>
|
||||
<input id="text-input" placeholder="Type your message here" type="text" />
|
||||
<button id="send-btn" type="button" disabled>
|
||||
Send message using Light Push
|
||||
</button>
|
||||
<span id="sending-status"></span>
|
||||
|
||||
<h4 class="mu1">Messages</h4>
|
||||
<div id="messages"></div>
|
||||
</div>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,320 @@
|
|||
import { createLightNode } from "js-waku/lib/create_waku";
|
||||
import { utils } from "js-waku";
|
||||
import { waitForRemotePeer } from "js-waku/lib/wait_for_remote_peer";
|
||||
import {
|
||||
Fleet,
|
||||
getPredefinedBootstrapNodes,
|
||||
} from "js-waku/lib/predefined_bootstrap_nodes";
|
||||
import { PeerDiscoveryStaticPeers } from "js-waku/lib/peer_discovery_static_list";
|
||||
import { Protocols } from "js-waku";
|
||||
import * as noise from "@waku/noise";
|
||||
import protobuf from "protobufjs";
|
||||
import QRCode from "qrcode";
|
||||
// TODO: Get rid of these
|
||||
import hexToArrayBuffer from "hex-to-array-buffer";
|
||||
import arrayBufferToHex from "array-buffer-to-hex";
|
||||
|
||||
const messagesDiv = document.getElementById("messages");
|
||||
const nicknameInput = document.getElementById("nick-input");
|
||||
const textInput = document.getElementById("text-input");
|
||||
const sendButton = document.getElementById("send-btn");
|
||||
const sendingStatusSpan = document.getElementById("sending-status");
|
||||
const chatArea = document.getElementById("chat-area");
|
||||
const qrCanvas = document.getElementById("qr-canvas");
|
||||
const qrUrl = document.getElementById("qr-url");
|
||||
const wakuStatusSpan = document.getElementById("waku-status");
|
||||
const handshakeStatusSpan = document.getElementById("handshake-status");
|
||||
|
||||
function getPairingInfofromUrl() {
|
||||
const urlParts = window.location.href.split("?");
|
||||
if (urlParts.length < 2) return undefined;
|
||||
|
||||
const pairingParts = decodeURIComponent(urlParts[1]).split(":");
|
||||
if (pairingParts.length < 6)
|
||||
throw new Error("invalid pairing information format");
|
||||
|
||||
const qrMessageNameTag = new Uint8Array(
|
||||
hexToArrayBuffer(pairingParts.shift())
|
||||
);
|
||||
|
||||
return new noise.InitiatorParameters(
|
||||
pairingParts.join(":"),
|
||||
qrMessageNameTag
|
||||
);
|
||||
}
|
||||
|
||||
function getSenderAndResponder(node) {
|
||||
const sender = {
|
||||
async publish(encoder, msg) {
|
||||
await node.lightPush.push(encoder, msg);
|
||||
},
|
||||
};
|
||||
|
||||
const msgQueue = new Array();
|
||||
const subscriptions = new Map();
|
||||
const intervals = new Map();
|
||||
|
||||
const responder = {
|
||||
async subscribe(decoder) {
|
||||
const subscription = await node.filter.subscribe(
|
||||
[decoder],
|
||||
(wakuMessage) => {
|
||||
msgQueue.push(wakuMessage);
|
||||
}
|
||||
);
|
||||
subscriptions.set(decoder.contentTopic, subscription);
|
||||
},
|
||||
async nextMessage(contentTopic) {
|
||||
if (msgQueue.length != 0) {
|
||||
const oldestMsg = msgQueue.shift();
|
||||
if (oldestMsg.contentTopic === contentTopic) {
|
||||
return oldestMsg;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
if (msgQueue.length != 0) {
|
||||
clearInterval(interval);
|
||||
const oldestMsg = msgQueue.shift();
|
||||
if (oldestMsg.contentTopic === contentTopic) {
|
||||
resolve(oldestMsg);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
intervals.set(contentTopic, interval);
|
||||
});
|
||||
},
|
||||
async stop(contentTopic) {
|
||||
if (intervals.has(contentTopic)) {
|
||||
clearInterval(intervals.get(contentTopic));
|
||||
intervals.delete(contentTopic);
|
||||
}
|
||||
if (subscriptions.has(contentTopic)) {
|
||||
await subscriptions.get(contentTopic)();
|
||||
subscriptions.delete(contentTopic);
|
||||
} else {
|
||||
console.log("Subscriptipon doesnt exist");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return [sender, responder];
|
||||
}
|
||||
|
||||
async function confirmAuthCodeFlow(pairingObj) {
|
||||
const authCode = await pairingObj.getAuthCode();
|
||||
pairingObj.validateAuthCode(confirm("Confirm that authcode is: " + authCode));
|
||||
}
|
||||
|
||||
async function hideQR() {
|
||||
qrCanvas.remove();
|
||||
qrUrl.remove();
|
||||
}
|
||||
|
||||
async function disableUI() {
|
||||
hideQR();
|
||||
chatArea.remove();
|
||||
}
|
||||
|
||||
// Function to update the fields to guide the user by disabling buttons.
|
||||
const updateFields = () => {
|
||||
const readyToSend = nicknameInput.value !== "";
|
||||
textInput.disabled = !readyToSend;
|
||||
sendButton.disabled = !readyToSend;
|
||||
};
|
||||
|
||||
// Protobuf
|
||||
const ProtoChatMessage = new protobuf.Type("ChatMessage")
|
||||
.add(new protobuf.Field("timestamp", 1, "uint64"))
|
||||
.add(new protobuf.Field("nick", 2, "string"))
|
||||
.add(new protobuf.Field("text", 3, "bytes"));
|
||||
|
||||
let messages = [];
|
||||
|
||||
const updateMessages = () => {
|
||||
messagesDiv.innerHTML = "<ul>";
|
||||
messages.forEach((msg) => {
|
||||
messagesDiv.innerHTML += `<li>${msg}</li>`;
|
||||
});
|
||||
messagesDiv.innerHTML += "</ul>";
|
||||
};
|
||||
|
||||
const onMessage = (wakuMessage) => {
|
||||
const { timestamp, nick, text } = ProtoChatMessage.decode(
|
||||
wakuMessage.payload
|
||||
);
|
||||
const time = new Date();
|
||||
time.setTime(Number(timestamp) * 1000);
|
||||
|
||||
messages.push(
|
||||
`(${nick}) <strong>${utils.bytesToUtf8(
|
||||
text
|
||||
)}</strong> <i>[${time.toISOString()}]</i>`
|
||||
);
|
||||
updateMessages();
|
||||
};
|
||||
|
||||
async function main() {
|
||||
// Starting the node
|
||||
const node = await createLightNode({
|
||||
libp2p: {
|
||||
peerDiscovery: [
|
||||
new PeerDiscoveryStaticPeers(getPredefinedBootstrapNodes(Fleet.Test)),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await node.start();
|
||||
|
||||
await waitForRemotePeer(node, [Protocols.Filter, Protocols.LightPush]);
|
||||
|
||||
wakuStatusSpan.innerHTML = "connected";
|
||||
|
||||
const [sender, responder] = getSenderAndResponder(node);
|
||||
|
||||
const myStaticKey = noise.generateX25519KeyPair();
|
||||
|
||||
const pairingParameters = getPairingInfofromUrl();
|
||||
|
||||
const initiator = pairingParameters ? true : false;
|
||||
|
||||
let encoder;
|
||||
let decoder;
|
||||
|
||||
if (initiator) {
|
||||
console.log("Initiator");
|
||||
qrCanvas.remove(); // Initiator does not require a QR code
|
||||
|
||||
const pairingObj = new noise.WakuPairing(
|
||||
sender,
|
||||
responder,
|
||||
myStaticKey,
|
||||
pairingParameters
|
||||
);
|
||||
const pExecute = pairingObj.execute(120000); // timeout after 2m
|
||||
|
||||
confirmAuthCodeFlow(pairingObj);
|
||||
|
||||
try {
|
||||
handshakeStatusSpan.innerHTML = "executing handshake...";
|
||||
|
||||
[encoder, decoder] = await pExecute;
|
||||
|
||||
handshakeStatusSpan.innerHTML = "handshake completed!";
|
||||
} catch (err) {
|
||||
handshakeStatusSpan.innerHTML = err.message;
|
||||
disableUI();
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
/*
|
||||
// The information needs to be backed up to decrypt messages sent with
|
||||
// codecs generated with the handshake. The `handshakeResult` variable
|
||||
// contains private information that needs to be stored safely
|
||||
const contentTopic = pairingObj.contentTopic;
|
||||
const handshakeResult = pairingObj.getHandshakeResult();
|
||||
|
||||
// To restore the codecs for decrypting older messages, or continuing an existing
|
||||
// session, use this:
|
||||
[encoder, decoder] = WakuPairing.getSecureCodec(contentTopic, handshakeResult);
|
||||
*/
|
||||
} else {
|
||||
console.log("Responder");
|
||||
|
||||
const pairingObj = new noise.WakuPairing(
|
||||
sender,
|
||||
responder,
|
||||
myStaticKey,
|
||||
new noise.ResponderParameters()
|
||||
);
|
||||
const pExecute = pairingObj.execute(120000); // timeout after 2m
|
||||
|
||||
confirmAuthCodeFlow(pairingObj);
|
||||
|
||||
const pInfo = pairingObj.getPairingInfo();
|
||||
|
||||
// Data to encode in the QR code. The qrMessageNametag too to the QR string (separated by )
|
||||
const qrString =
|
||||
arrayBufferToHex(pInfo.qrMessageNameTag) + ":" + pInfo.qrCode;
|
||||
const qrURLString =
|
||||
window.location.href + "?" + encodeURIComponent(qrString);
|
||||
|
||||
handshakeStatusSpan.innerHTML = "generating QR code...";
|
||||
|
||||
console.log("Generating QR...");
|
||||
|
||||
QRCode.toCanvas(qrCanvas, qrURLString, (err) => {
|
||||
if (err) {
|
||||
handshakeStatusSpan.innerHTML = err.message;
|
||||
disableUI();
|
||||
console.error(err);
|
||||
} else {
|
||||
handshakeStatusSpan.innerHTML = "waiting for handshake to start";
|
||||
qrUrl.href = qrURLString;
|
||||
qrUrl.style.display = "block";
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
handshakeStatusSpan.innerHTML = "executing handshake...";
|
||||
|
||||
[encoder, decoder] = await pExecute;
|
||||
|
||||
handshakeStatusSpan.innerHTML = "handshake completed!";
|
||||
|
||||
hideQR();
|
||||
} catch (err) {
|
||||
handshakeStatusSpan.innerHTML = err.message;
|
||||
disableUI();
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
/*
|
||||
// The information needs to be backed up to decrypt messages sent with
|
||||
// codecs generated with the handshake. The `handshakeResult` variable
|
||||
// contains private information that needs to be stored safely
|
||||
const contentTopic = pairingObj.contentTopic;
|
||||
const handshakeResult = pairingObj.getHandshakeResult();
|
||||
|
||||
// To restore the codecs for decrypting older messages, or continuing an existing
|
||||
// session, use this:
|
||||
[encoder, decoder] = WakuPairing.getSecureCodec(contentTopic, handshakeResult);
|
||||
*/
|
||||
}
|
||||
|
||||
nicknameInput.onchange = updateFields;
|
||||
nicknameInput.onblur = updateFields;
|
||||
|
||||
sendButton.onclick = async () => {
|
||||
const text = utils.utf8ToBytes(textInput.value);
|
||||
const timestamp = new Date();
|
||||
const msg = ProtoChatMessage.create({
|
||||
text,
|
||||
nick: nicknameInput.value,
|
||||
timestamp: Math.floor(timestamp.valueOf() / 1000),
|
||||
});
|
||||
const payload = ProtoChatMessage.encode(msg).finish();
|
||||
sendingStatusSpan.innerText = "sending...";
|
||||
await node.lightPush.push(encoder, { payload, timestamp });
|
||||
sendingStatusSpan.innerText = "sent!";
|
||||
|
||||
onMessage({ payload });
|
||||
|
||||
textInput.value = null;
|
||||
setTimeout(() => {
|
||||
sendingStatusSpan.innerText = "";
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
await node.filter.subscribe([decoder], onMessage);
|
||||
|
||||
chatArea.style.display = "block";
|
||||
} catch (err) {
|
||||
wakuStatusSpan.innerHTML = err.message;
|
||||
disableUI();
|
||||
return;
|
||||
}
|
||||
}
|
||||
main();
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "@waku/noise-example",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"start": "webpack-dev-server"
|
||||
},
|
||||
"dependencies": {
|
||||
"@waku/noise": "https://github.com/waku-org/js-noise.git",
|
||||
"array-buffer-to-hex": "^1.0.0",
|
||||
"hex-to-array-buffer": "^2.0.0",
|
||||
"js-waku": "^0.29.0-29436ea",
|
||||
"protobufjs": "^7.1.2",
|
||||
"qrcode": "^1.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: "./index.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "index.js",
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
mode: "development",
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: ["index.html"],
|
||||
}),
|
||||
],
|
||||
};
|
Loading…
Reference in New Issue