feat: add rln-js and rln-identity examples (#13)

This commit is contained in:
Sasha 2024-02-14 01:38:05 +01:00 committed by GitHub
parent f25868c2bf
commit 5e1be42b9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 36194 additions and 1 deletions

View File

@ -18,7 +18,9 @@ jobs:
web-chat, web-chat,
noise-js, noise-js,
noise-rtc, noise-rtc,
relay-direct-rtc relay-direct-rtc,
rln-js,
rln-identity
] ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

2
ci/Jenkinsfile vendored
View File

@ -40,6 +40,8 @@ pipeline {
stage('noise-js') { steps { script { buildExample() } } } stage('noise-js') { steps { script { buildExample() } } }
stage('noise-rtc') { steps { script { buildExample() } } } stage('noise-rtc') { steps { script { buildExample() } } }
stage('relay-direct-rtc') { steps { script { buildExample() } } } stage('relay-direct-rtc') { steps { script { buildExample() } } }
stage('rln-js') { steps { script { buildExample() } } }
stage('rln-identity') { steps { script { buildExample() } } }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>RLN Credential management</title>
<link rel="apple-touch-icon" href="./favicon.png" />
<link rel="manifest" href="./manifest.json" />
<link rel="icon" href="./favicon.ico" />
<style>
* {
margin: 0;
padding: 0;
word-wrap: break-word;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
html {
font-size: 16px;
overflow: hidden;
}
body {
display: flex;
align-items: center;
padding: 10px;
flex-direction: column;
justify-content: center;
}
.container {
width: 100%;
min-width: 300px;
max-width: 800px;
height: 100%;
display: flex;
flex-direction: column;
align-content: space-between;
}
h2 {
text-align: center;
margin-bottom: 5px;
}
h3 {
margin-bottom: 10px;
}
h3:last-of-type {
margin-bottom: 20px;
}
h2 span,
h3 span {
font-weight: normal;
}
.progress {
color: #9ea13b;
}
.success {
color: #3ba183;
}
.error {
color: #c84740;
}
input {
padding: 0.5rem;
}
select {
padding: 0.5rem;
max-width: 150px;
}
button {
padding: 0.5rem;
}
button.progress {
color: white;
background-color: #9ea13b;
}
button.success {
color: white;
background-color: #3ba183;
}
button.error {
color: white;
background-color: #c84740;
}
.mb-1 {
margin-bottom: 1rem;
}
.mb-2 {
margin-bottom: 2rem;
}
.mb-3 {
margin-bottom: 3rem;
}
.mt-1 {
margin-top: 1rem;
}
.block {
display: flex;
justify-content: space-between;
align-items: center;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="status">
<h3>
<b>Status:</b>
<span id="status" class="progress">Starting...</span>
</h3>
</div>
<div class="block mb-1">
<h2>Wallet</h2>
<button id="connect">Connect</button>
</div>
<div class="block mb-1">
<h2>Keystore</h2>
<div>
<button id="import">Import</button>
<input id="import-file" class="hidden" type="file" />
<button id="export">Export</button>
</div>
</div>
<hr />
<h3 class="mt-1">Existing credentials</h3>
<div class="block mb-2">
<select id="keystore"></select>
<div>
<input id="password" placeholder="password" />
<button id="read-credential">Read</button>
</div>
</div>
<div class="block mb-3">
<h3>Create new (will use the password)</h3>
<button id="register-new">Register</button>
</div>
<div id="current-credentials">
<div class="block mb-1">
<p>Keystore hash</p>
<code>none</code>
</div>
<div class="block mb-1">
<p>Membership ID</p>
<code>none</code>
</div>
<div class="block mb-1">
<p>Secret Hash</p>
<code>none</code>
</div>
<div class="block mb-1">
<p>Commitment</p>
<code>none</code>
</div>
<div class="block mb-1">
<p>Nullifier</p>
<code>none</code>
</div>
<div class="block mb-1">
<p>Trapdoor</p>
<code>none</code>
</div>
</div>
</div>
<script src="./index.js"></script>
</body>
</html>

View File

@ -0,0 +1,19 @@
{
"name": "Waku RLN",
"description": "Example showing Waku RLN credential management.",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "192x192"
}
],
"display": "standalone",
"theme_color": "#ffffff",
"background_color": "#ffffff"
}

17494
examples/rln-identity/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "rln-chat",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack-dev-server"
},
"dependencies": {
"@waku/rln": "0.1.1-77ba0a6",
"@waku/sdk": "^0.0.22",
"@waku/utils": "^0.0.14",
"ethers": "^5.7.2",
"multiaddr": "^10.0.1"
},
"devDependencies": {
"eslint": "^8",
"eslint-config-next": "13.5.6",
"copy-webpack-plugin": "^11.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}

View File

@ -0,0 +1,2 @@
export const SIGNATURE_MESSAGE =
"The signature of this message will be used to generate your RLN credentials. Anyone accessing it may send messages on your behalf, please only share with the RLN dApp";

View File

@ -0,0 +1,27 @@
import { initUI } from "./ui";
import { initRLN } from "./rln";
async function run() {
const { registerEvents, onStatusChange } = initUI();
const {
connectWallet,
registerCredential,
readKeystoreOptions,
readCredential,
saveLocalKeystore,
importLocalKeystore,
} = await initRLN({
onStatusChange,
});
registerEvents({
connectWallet,
registerCredential,
readKeystoreOptions,
readCredential,
saveLocalKeystore,
importLocalKeystore,
});
}
run();

View File

@ -0,0 +1,89 @@
import { createRLN, Keystore, extractMetaMaskSigner } from "@waku/rln";
import { randomNumber } from "./utils";
import { SIGNATURE_MESSAGE } from "./const";
export async function initRLN({ onStatusChange }) {
onStatusChange("Initializing RLN...");
let rln;
try {
rln = await createRLN();
} catch (err) {
onStatusChange(`Failed to initialize RLN: ${err}`, "error");
throw Error(err);
}
onStatusChange("RLN initialized", "success");
const connectWallet = async () => {
let signer;
try {
onStatusChange("Connecting to wallet...");
signer = await extractMetaMaskSigner();
} catch (err) {
onStatusChange(`Failed to access MetaMask: ${err}`, "error");
throw Error(err);
}
try {
onStatusChange("Connecting to Ethereum...");
const localKeystore = readLocalKeystore();
rln.keystore = Keystore.fromString(localKeystore);
await rln.start({ signer });
} catch (err) {
onStatusChange(`Failed to connect to Ethereum: ${err}`, "error");
throw Error(err);
}
onStatusChange("RLN started", "success");
};
const registerCredential = async (password) => {
if (!rln.signer) {
alert("RLN is not initialized. Try connecting wallet first.");
return;
}
const signature = await rln.signer.signMessage(
`${SIGNATURE_MESSAGE}. Nonce: ${randomNumber()}`
);
const credential = await rln.registerMembership({ signature });
const hash = await rln.keystore.addCredential(credential, password);
return { hash, credential };
};
const readKeystoreOptions = () => {
return rln.keystore.keys();
};
const readCredential = async (hash, password) => {
return rln.keystore.readCredential(hash, password);
};
const saveLocalKeystore = () => {
const keystoreStr = rln.keystore.toString();
localStorage.setItem("keystore", keystoreStr);
return keystoreStr;
};
const importLocalKeystore = (keystoreStr) => {
rln.keystore = Keystore.fromString(keystoreStr) || Keystore.create();
};
return {
rln,
connectWallet,
registerCredential,
readKeystoreOptions,
readCredential,
saveLocalKeystore,
importLocalKeystore,
};
}
function readLocalKeystore() {
return localStorage.getItem("keystore") || "";
}

View File

@ -0,0 +1,149 @@
import { renderBytes } from "./utils";
const status = document.getElementById("status");
const connectWalletButton = document.getElementById("connect");
const importKeystoreButton = document.getElementById("import");
const importFileInput = document.getElementById("import-file");
const exportKeystoreButton = document.getElementById("export");
const keystoreOptions = document.getElementById("keystore");
const keystorePassword = document.getElementById("password");
const readCredentialButton = document.getElementById("read-credential");
const registerNewCredentialButton = document.getElementById("register-new");
const currentCredentials = document.getElementById("current-credentials");
export function initUI() {
const _renderCredential = (hash, credential) => {
currentCredentials.innerHTML = `
<div class="block mb-1">
<p>Keystore hash</p>
<code>${hash || "none"}</code>
</div>
<div class="block mb-1">
<p>Membership ID</p>
<code>${credential.membership.treeIndex || "none"}</code>
</div>
<div class="block mb-1">
<p>Secret Hash</p>
<code>${renderBytes(credential.identity.IDSecretHash)}</code>
</div>
<div class="block mb-1">
<p>Commitment</p>
<code>${renderBytes(credential.identity.IDCommitment)}</code>
</div>
<div class="block mb-1">
<p>Nullifier</p>
<code>${renderBytes(credential.identity.IDNullifier)}</code>
</div>
<div class="block mb-1">
<p>Trapdoor</p>
<code>${renderBytes(credential.identity.IDTrapdoor)}</code>
</div>
`;
};
const _renderKeystoreOptions = (options) => {
keystoreOptions.innerHTML = `
${options.map((v) => `<option value=${v}>${v}</option>`)}
`;
};
const registerEvents = ({
connectWallet,
registerCredential,
readKeystoreOptions,
readCredential,
saveLocalKeystore,
importLocalKeystore,
}) => {
connectWalletButton.addEventListener("click", async () => {
await connectWallet();
const keystoreKeys = readKeystoreOptions();
_renderKeystoreOptions(keystoreKeys);
});
registerNewCredentialButton.addEventListener("click", async () => {
const password = keystorePassword.value;
if (!password) {
alert("Please, input password in order to create new credentials.");
return;
}
const { hash, credential } = await registerCredential(password);
_renderCredential(hash, credential);
const keystoreKeys = readKeystoreOptions();
_renderKeystoreOptions(keystoreKeys);
keystoreOptions.value = hash;
saveLocalKeystore();
});
readCredentialButton.addEventListener("click", async () => {
const password = keystorePassword.value;
if (!password) {
alert(
"Please, input password in order to read credential from Keystore."
);
return;
}
const currentHash = keystoreOptions.value;
if (!currentHash) {
alert(
"Please, select hash of a key in order to read credential from Keystore."
);
return;
}
const credential = await readCredential(currentHash, password);
_renderCredential(currentHash, credential);
});
importFileInput.addEventListener("change", async (event) => {
const file = event.currentTarget.files[0];
if (!file) {
return;
}
const text = await file.text();
importLocalKeystore(text);
const keystoreOptions = readKeystoreOptions();
_renderKeystoreOptions(keystoreOptions);
});
importKeystoreButton.addEventListener("click", async () => {
importFileInput.click();
});
exportKeystoreButton.addEventListener("click", () => {
const filename = "keystore.json";
const text = saveLocalKeystore();
const file = new File([text], filename, {
type: "application/json",
});
const link = document.createElement("a");
link.href = URL.createObjectURL(file);
link.download = filename;
link.click();
});
};
return {
registerEvents,
onStatusChange: (value, category = "progress") => {
status.className = category;
status.innerText = value;
},
};
}

View File

@ -0,0 +1,9 @@
import { bytesToHex } from "@waku/utils/bytes";
export function randomNumber() {
return Math.ceil(Math.random() * 1000);
}
export function renderBytes(bytes) {
return bytes ? bytesToHex(bytes) : "none";
}

View File

@ -0,0 +1,19 @@
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "build"),
filename: "./index.js",
},
experiments: {
asyncWebAssembly: true,
},
mode: "development",
plugins: [
new CopyWebpackPlugin({
patterns: ["index.html", "favicon.ico", "favicon.png", "manifest.json"],
}),
],
};

BIN
examples/rln-js/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
examples/rln-js/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

186
examples/rln-js/index.html Normal file
View File

@ -0,0 +1,186 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>RLN Chat</title>
<link rel="apple-touch-icon" href="./favicon.png" />
<link rel="manifest" href="./manifest.json" />
<link rel="icon" href="./favicon.ico" />
<style>
* {
margin: 0;
padding: 0;
word-wrap: break-word;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
html {
font-size: 16px;
overflow: hidden;
}
body {
display: flex;
align-items: center;
padding: 10px;
flex-direction: column;
justify-content: center;
}
.container {
width: 100%;
min-width: 300px;
max-width: 800px;
height: 100%;
display: flex;
flex-direction: column;
align-content: space-between;
}
h2 {
text-align: center;
margin-bottom: 5px;
}
h3 {
margin-bottom: 10px;
}
h3:last-of-type {
margin-bottom: 20px;
}
h2 span,
h3 span {
font-weight: normal;
}
.progress {
color: #9ea13b;
}
.success {
color: #3ba183;
}
.error {
color: #c84740;
}
button.progress {
color: white;
background-color: #9ea13b;
}
button.success {
color: white;
background-color: #3ba183;
}
button.error {
color: white;
background-color: #c84740;
}
.pairingInfo {
display: flex;
flex-direction: column;
align-items: center;
}
.pairingInfo input {
display: block;
min-width: 250px;
width: 100%;
max-width: 600px;
font-size: 1.1rem;
line-height: 1.5rem;
padding: 5px;
margin-bottom: 10px;
}
.pairingInfo button {
flex-grow: 1;
}
.pairingInfo button + button {
margin-left: 5px;
}
button {
cursor: pointer;
padding: 10px;
}
.chatArea {
}
.chatArea ul {
margin-bottom: 30px;
list-style: none;
}
.chatArea ul li + li {
margin-top: 5px;
}
.chatArea div {
display: flex;
flex-direction: column;
}
.chatArea div > * {
font-size: 1.1rem;
line-height: 1.5rem;
padding: 5px;
margin-bottom: 10px;
}
.mb-1 {
margin-bottom: 1rem;
}
.status {
display: flex;
justify-content: space-between;
}
</style>
</head>
<body>
<div class="container">
<div class="status">
<h3>
<b>Status:</b>
<span id="status" class="progress">Starting...</span>
</h3>
<button id="connect" class="mb-1">Connect wallet</button>
</div>
<div class="chatArea" id="chat-area" style="display: none">
<h2>Chat</h2>
<ul id="messages"></ul>
<div>
<input id="nick" placeholder="Choose a nickname" type="text" />
<textarea
id="text"
placeholder="Type your message here"
type="text"
></textarea>
<button id="send" type="button">Send message</button>
</div>
</div>
</div>
<script src="./index.js"></script>
</body>
</html>

View File

@ -0,0 +1,19 @@
{
"name": "Waku RLN",
"description": "Example showing Waku RLN capabilities.",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "192x192"
}
],
"display": "standalone",
"theme_color": "#ffffff",
"background_color": "#ffffff"
}

17654
examples/rln-js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "rln-chat",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack-dev-server"
},
"dependencies": {
"@waku/rln": "0.1.2-e1679b6",
"@waku/sdk": "^0.0.21",
"@waku/utils": "^0.0.14",
"multiaddr": "^10.0.1",
"protobufjs": "^7.2.5"
},
"devDependencies": {
"eslint": "^8",
"eslint-config-next": "13.5.6",
"copy-webpack-plugin": "^11.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}

View File

@ -0,0 +1,35 @@
import protobuf from "protobufjs";
export const KEYSTORE = `{
"application": "waku-rln-relay",
"appIdentifier": "0.2",
"version": "01234567890abcdef",
"credentials": {
"740AF0B31F2DCC5C09264019BE6910FFB9C3CA575A25CC9E378BF67965AF48B4": {
"crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "2089221b325414adcbbc926cc8cec6dc"
},
"ciphertext": "b3acbe5dc51dc2d67e7e08852577d1d538d5967f632ef097e9887852ea5aac541b4755ed5d4945f61e45437c3f9006836bb4e44d8580524cfb363775c250cb4509f15a0665b16874c9e66fe4a12fd2678002d4bf881806946dd31e510edae962e21a4bc15284442460aa843c630127b9fb46309f403f2480160c56ab9f024e394ba22c5de668e14c2500556a35f8e152d78c18a13faf1dc0542f0ff673f659daf7e8c863339f69e412b22cc83358fea684b61f1c5184ead0004854014f9dbe70b555d91d37b18b12b65ae5860ad67f81dc259f781077b18e226470ce6ae40370db0a32f9d245587dc741212ff636ddbe9fcf0bc68e8c5a9e02bbf35b359b5f5a315cb5a699283121bdde8b2093512aa8d7cef82803a7f2d84d250cb218d34c8a077c566ff7628be42e2ab50d5c115984f54640db0a861917fdaf7391b386c7ff27b48740ce90ede2f025a90fef8f1cf6b02f7c791f83950c53aced5909fef64de2e4c5160880a49b797092abad45acb58986051258628a4c994fe69282aad949d287c4d8efe4cd1ad5863edd71779dffc57ea27b817e8610730223e2b382d3f80f8d51dcb04226a1df0b344de55ff71780b750a0ab58af4360bcbc3b885bb49eb7fa2300464c97d7fda26795f853facdace48251ff6bfdb318747920499c88a68a0a00bb04d39b4357bd543778cf083c1ba38ad43d4c2fb54ee75f8707a02d5fd5b9b36a2298d999d4f008e2b2ae4fc58775d4fb0f7e8db58a6a7cb1feecdda458518916e5c359f953e71a172981efb6dd7ab81aa1b6bb87ebbb81513f19af25525214c15112728e0dd7786459c7c4d9c0ea1df62cd7be42253ab484cc9c8b2b7bfaae4216b26b53b86eb691a4dc91615fcf804f95c50ee8b88ab2ccada1e3633d13cea753258e225e98c3ffbe86ee2327bcf6b540730047da79ffa3661a8783f590470d058454be5d261a77d966105cd8f1560d64635b5ec9882733502a46044d9eabb909acee18a60ade210d26af8eb6007142471174d30b8410df90d667089a28c1ef487dcfe58f14132580fd1a3f771f0e10d19f110c8a54ee5936e395227dcb5d4b895f5d6f9b04118c07671dda3942a9c6836339011e297ef2ca034d0bdc1f44693ea7ee621c532ec3893626f92addafb8518a284873c42a17274da519592a967bf8ad2f646650f39c3e4d4d014f3ff54e4088012b01244cf1dfcd2396d4c36d4f210805f167ec7533254d3a3cdefad7e9d531d6d67fbc497ba0c38036263402489b64d766ea11f4df4b3fe1f4e70b8e8fc708de6fb976fe1fbfae81b1ccb3bc3dc65f61d0b92e8bf39189f5bf2fc727777e3728963cc6163169fc8a0ed72723b506d659f9b2c3048261e295876a88db139c8c15429d9a1e2bb1d3587379e2c504ae384d7563d26dab99fc12069211d6f037e513eb749ad48de58bceb95dcc860abcb0f4c1336e8025a402197434b620d1ec5c8f23230a1d75fff8353259cefc1c837faa335e48be1a49db5a7da89d490cff5f6fbf80c00ecc497ba59cbb69d4ebb57a922e646e66184b41286ceed34ec76be8326f48d66fc79c21512dc847f8b01e0253a469b351beb1288f157348da319856b892448d0b269dafd84c14e1436938e7f322f084207d421d8f5587284c021ac0607524bc29d52be2ce7f3263a237f9b319d5a26349069cf588f8f17e05021bc9bdd6edf90722bf91fb57cc67f9125aa39d09563278bea7acff25bad4968d731501f2ae62a3529d223b88aae57f2670",
"kdf": "pbkdf2",
"kdfparams": {
"dklen": 32,
"c": 262144,
"prf": "hmac-sha256",
"salt": "ff41c018bae51f6cd20e96739a46b4724c854dc6f7d4e741526b2be71f8dc572"
},
"mac": "4b98784366b4dd610e5bc0f672bd91c9410c3d271cd1bd48ca7db2ffe889d9eb"
}
}
}
}`;
export const MEMBERSHIP_HASH =
"740AF0B31F2DCC5C09264019BE6910FFB9C3CA575A25CC9E378BF67965AF48B4";
export const MEMBERSHIP_PASSWORD = "qweqwe";
export const CONTENT_TOPIC = "/toy-chat/2/luzhou/proto";
export 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, "string"));

View File

@ -0,0 +1,16 @@
import { initUI } from "./ui";
import { initRLN } from "./rln";
import { initWaku } from "./waku";
async function run() {
const { onStatusChange, registerEvents } = initUI();
const { rln, connectWallet } = await initRLN(onStatusChange);
const { onSend, onSubscribe, onInitWaku } = await initWaku({
rln,
onStatusChange,
});
registerEvents({ onSend, onSubscribe, connectWallet, onInitWaku });
}
run();

View File

@ -0,0 +1,41 @@
import { createRLN, extractMetaMaskSigner } from "@waku/rln";
export async function initRLN(onStatusChange) {
onStatusChange("Initializing RLN...");
let rln;
try {
rln = await createRLN();
} catch (err) {
onStatusChange(`Failed to initialize RLN: ${err}`, "error");
throw Error(err);
}
onStatusChange("RLN initialized", "success");
const connectWallet = async () => {
let signer;
try {
onStatusChange("Connecting to wallet...");
signer = await extractMetaMaskSigner();
} catch (err) {
onStatusChange(`Failed to access MetaMask: ${err}`, "error");
throw Error(err);
}
try {
onStatusChange("Connecting to Ethereum...");
await rln.start({ signer });
} catch (err) {
onStatusChange(`Failed to connect to Ethereum: ${err}`, "error");
throw Error(err);
}
onStatusChange("RLN started", "success");
};
return {
rln,
connectWallet,
};
}

61
examples/rln-js/src/ui.js Normal file
View File

@ -0,0 +1,61 @@
const status = document.getElementById("status");
const chat = document.getElementById("chat-area");
const messages = document.getElementById("messages");
const nickInput = document.getElementById("nick");
const textInput = document.getElementById("text");
const sendButton = document.getElementById("send");
const connectWalletButton = document.getElementById("connect");
export const initUI = () => {
const onStatusChange = (newStatus, className) => {
status.innerText = newStatus;
status.className = className || "progress";
};
const onLoaded = () => {
chat.style.display = "block";
};
const _renderMessage = (nick, text, time, validation) => {
messages.innerHTML += `
<li>
(${nick})(${validation})
<strong>${text}</strong>
<i>[${new Date(time).toISOString()}]</i>
</li>
`;
};
const registerEvents = (events) => {
connectWalletButton.addEventListener("click", async () => {
await events.connectWallet();
await events.onInitWaku();
onLoaded();
events.onSubscribe((nick, text, time, validation) => {
_renderMessage(nick, text, time, validation);
});
sendButton.addEventListener("click", async () => {
const nick = nickInput.value;
const text = textInput.value;
if (!nick || !text) {
console.log("Not sending message: missing nick or text.");
return;
}
await events.onSend(nick, text);
textInput.value = "";
});
});
};
return {
registerEvents,
onStatusChange,
};
};

View File

@ -0,0 +1,96 @@
import { createLightNode, waitForRemotePeer } from "@waku/sdk";
import {
ProtoChatMessage,
CONTENT_TOPIC,
KEYSTORE,
MEMBERSHIP_HASH,
MEMBERSHIP_PASSWORD,
} from "./const";
export async function initWaku({ rln, onStatusChange }) {
let node;
let encoder, decoder;
let subscription;
const onInitWaku = async () => {
encoder = await rln.createEncoder({
ephemeral: false,
contentTopic: CONTENT_TOPIC,
credentials: {
keystore: KEYSTORE,
id: MEMBERSHIP_HASH,
password: MEMBERSHIP_PASSWORD,
},
});
decoder = rln.createDecoder(CONTENT_TOPIC);
onStatusChange("Initializing Waku...");
node = await createLightNode({
defaultBootstrap: true,
});
onStatusChange("Waiting for peers");
await node.start();
await waitForRemotePeer(node);
};
const onSend = async (nick, text) => {
const timestamp = new Date();
const msg = ProtoChatMessage.create({
text,
nick,
timestamp: Math.floor(timestamp.valueOf() / 1000),
});
const payload = ProtoChatMessage.encode(msg).finish();
console.log("Sending message with proof...");
const res = await node.lightPush.send(encoder, { payload, timestamp });
console.log("Message sent:", res);
};
const onSubscribe = async (cb) => {
onStatusChange("Subscribing to content topic...");
subscription = await node.filter.createSubscription();
await subscription.subscribe(decoder, (message) => {
try {
const { timestamp, nick, text } = ProtoChatMessage.decode(
message.payload
);
let proofStatus = "no proof";
if (message.rateLimitProof) {
console.log("Proof received: ", message.rateLimitProof);
try {
console.time("Proof verification took:");
const res = message.verify(rln.contract.roots());
console.timeEnd("Proof verification took:");
proofStatus = res ? "verified" : "not verified";
} catch (error) {
proofStatus = "invalid";
console.error("Failed to verify proof: ", error);
}
}
console.log({
nick,
text,
proofStatus,
time: new Date(timestamp).toDateString(),
});
cb(nick, text, timestamp, proofStatus);
} catch (error) {
console.error("Failed in subscription listener: ", error);
}
});
onStatusChange("Waku initialized", "success");
};
return {
onSend,
onSubscribe,
onInitWaku,
};
}

View File

@ -0,0 +1,19 @@
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "build"),
filename: "./index.js",
},
experiments: {
asyncWebAssembly: true,
},
mode: "development",
plugins: [
new CopyWebpackPlugin({
patterns: ["index.html", "favicon.ico", "favicon.png", "manifest.json"],
}),
],
};