feat: add rln-js and rln-identity examples (#13)
This commit is contained in:
parent
f25868c2bf
commit
5e1be42b9d
|
@ -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:
|
||||||
|
|
|
@ -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 |
|
@ -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>
|
|
@ -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"
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
|
@ -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();
|
|
@ -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") || "";
|
||||||
|
}
|
|
@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
|
@ -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"],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -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>
|
|
@ -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"
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"));
|
|
@ -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();
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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"],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
Loading…
Reference in New Issue