From 5f3b0e22b0d8b19cd60d84cf86e2fe788363c266 Mon Sep 17 00:00:00 2001
From: Sasha <118575614+weboko@users.noreply.github.com>
Date: Fri, 20 Oct 2023 13:01:40 +0200
Subject: [PATCH] feat: add support for Keystore and new contract (#280)
* add support for Keystore and new contract
* update mock
* up rln version
---
examples/rln-js/index.html | 76 +++++++----
examples/rln-js/index.js | 269 +++++++++++++++++++++++++------------
2 files changed, 236 insertions(+), 109 deletions(-)
diff --git a/examples/rln-js/index.html b/examples/rln-js/index.html
index c6610940..0a9df24c 100644
--- a/examples/rln-js/index.html
+++ b/examples/rln-js/index.html
@@ -42,45 +42,69 @@
Latest membership id on contract
Not loaded yet
- Credentials
+
+
+
Keystore
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
Generate new, or import existing, credentials from wallet:
+
Generate new credentials from wallet:
-
-
-
+
+
Keystore Hash
+ none
+
+
Membership id
none
diff --git a/examples/rln-js/index.js b/examples/rln-js/index.js
index a1b3d4cb..eb9ff032 100644
--- a/examples/rln-js/index.js
+++ b/examples/rln-js/index.js
@@ -8,11 +8,12 @@ import {
import { protobuf } from "https://taisukef.github.io/protobuf-es.js/dist/protobuf-es.js";
import {
create,
- IdentityCredential,
+ Keystore,
RLNDecoder,
RLNEncoder,
RLNContract,
-} from "https://unpkg.com/@waku/rln@0.1.1/bundle/index.js";
+ SEPOLIA_CONTRACT,
+} from "https://unpkg.com/@waku/rln@0.1.1-fa49e29/bundle/index.js";
import { ethers } from "https://unpkg.com/ethers@5.7.2/dist/ethers.esm.min.js";
const ContentTopic = "/toy-chat/2/luzhou/proto";
@@ -23,9 +24,6 @@ const ProtoChatMessage = new protobuf.Type("ChatMessage")
.add(new protobuf.Field("nick", 2, "string"))
.add(new protobuf.Field("text", 3, "bytes"));
-const rlnDeployBlk = 3193048;
-const rlnAddress = "0x9C09146844C1326c2dBC41c451766C7138F88155";
-
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";
@@ -56,14 +54,37 @@ async function initRLN(ui) {
const rlnInstance = await create();
ui.setRlnStatus("WASM Blob download in progress... done!");
- const rlnContract = new RLNContract(
- rlnInstance, {
- address: rlnAddress,
+ const rlnContract = await RLNContract.init(rlnInstance, {
+ registryAddress: SEPOLIA_CONTRACT.address,
provider: provider.getSigner(),
});
result.contract = rlnContract;
+ // Keystore logic
+ let keystore = initKeystore(ui);
+ ui.createKeystoreOptions(keystore);
+
+ ui.onKeystoreImport(async (text) => {
+ try {
+ keystore = Keystore.fromString(text);
+ ui.setKeystoreStatus("Imported keystore from json");
+ } catch (err) {
+ console.error("Failed to import keystore:", err);
+ ui.setKeystoreStatus("Failed to import, fallback to current keystore");
+ }
+ ui.createKeystoreOptions(keystore);
+ saveLocalKeystore(keystore);
+ });
+
+ ui.onKeystoreExport(async () => {
+ return keystore.toString();
+ });
+
+ ui.onKeystoreRead(async (hash, password) => {
+ return keystore.readCredential(hash, password);
+ });
+
// Wallet logic
window.ethereum.on("accountsChanged", ui.setAccount);
window.ethereum.on("chainChanged", (chainId) => {
@@ -85,7 +106,7 @@ async function initRLN(ui) {
const filter = rlnContract.contract.filters.MemberRegistered();
ui.disableRetrieveButton();
- await rlnContract.fetchMembers(rlnInstance, { fromBlock: rlnDeployBlk });
+ await rlnContract.fetchMembers(rlnInstance);
ui.enableRetrieveButton();
rlnContract.subscribeToMembers(rlnInstance);
@@ -93,12 +114,12 @@ async function initRLN(ui) {
const last = rlnContract.members.at(-1);
if (last) {
- ui.setLastMember(last.index, last.pubkey);
+ ui.setLastMember(last.index, last.idCommitment);
}
// make sure we have subscriptions to keep updating last item
- rlnContract.contract.on(filter, (_pubkey, _index, event) => {
- ui.setLastMember(event.args.index, event.args.pubkey);
+ rlnContract.contract.on(filter, (_idCommitment, _index, event) => {
+ ui.setLastMember(event.args.index, event.args.idCommitment);
});
});
@@ -106,31 +127,18 @@ async function initRLN(ui) {
let membershipId;
let credentials;
- ui.onManualImport((membershipId, credentials) => {
- result.encoder = new RLNEncoder(
- createEncoder({
- ephemeral: false,
- contentTopic: ContentTopic,
- }),
- rlnInstance,
- membershipId,
- credentials
- );
-
- ui.setMembershipInfo(membershipId, credentials);
- ui.enableDialButton();
- });
-
ui.onWalletImport(async () => {
const signer = provider.getSigner();
- signature = await signer.signMessage(SIGNATURE_MESSAGE);
+ signature = await signer.signMessage(
+ `${SIGNATURE_MESSAGE}. Nonce: ${Math.ceil(Math.random() * 1000)}`
+ );
credentials = await rlnInstance.generateSeededIdentityCredential(signature);
const idCommitment = ethers.utils.hexlify(credentials.IDCommitment);
rlnContract.members.forEach((m) => {
- if (m.pubkey._hex === idCommitment) {
+ if (m.idCommitment === idCommitment) {
membershipId = m.index.toString();
}
});
@@ -155,20 +163,42 @@ async function initRLN(ui) {
ui.onRegister(async () => {
ui.setRlnStatus("Trying to register...");
- const event = signature
+ const memberInfo = signature
? await rlnContract.registerWithSignature(rlnInstance, signature)
: await rlnContract.registerWithKey(credentials);
- // Update membershipId
- membershipId = event.args.index.toNumber();
+ membershipId = memberInfo.index.toNumber();
console.log(
"Obtained index for current membership credentials",
membershipId
);
+ const password = ui.getKeystorePassword();
+
+ if (!password) {
+ ui.setKeystoreStatus("Cannot add credentials, no password.");
+ }
+
+ const keystoreHash = await keystore.addCredential(
+ {
+ membership: {
+ treeIndex: membershipId,
+ chainId: SEPOLIA_CONTRACT.chainId,
+ address: SEPOLIA_CONTRACT.address,
+ },
+ identity:
+ credentials ||
+ rlnInstance.generateSeededIdentityCredential(signature),
+ },
+ password
+ );
+ saveLocalKeystore(keystore);
+ ui.addKeystoreOption(keystoreHash);
+ ui.setKeystoreStatus(`Added credential to Keystore`);
+
ui.setRlnStatus("Successfully registered.");
- ui.setMembershipInfo(membershipId, credentials);
+ ui.setMembershipInfo(membershipId, credentials, keystoreHash);
ui.enableDialButton();
});
@@ -284,17 +314,11 @@ function initUI() {
"retrieve-rln-details"
);
- // Credentials Elements
- const membershipIdInput = document.getElementById("membership-id");
- const idSecretHashInput = document.getElementById("id-secret-hash");
- const commitmentKeyInput = document.getElementById("commitment-key");
- const idTrapdoorInput = document.getElementById("id-trapdoor");
- const idNullifierInput = document.getElementById("id-nullifier");
- const importManually = document.getElementById("import-manually-button");
const importFromWalletButton = document.getElementById(
"import-from-wallet-button"
);
+ const keystoreHashDiv = document.getElementById("keystoreHash");
const idDiv = document.getElementById("id");
const secretHashDiv = document.getElementById("secret-hash");
const commitmentDiv = document.getElementById("commitment");
@@ -313,36 +337,25 @@ function initUI() {
const sendingStatusSpan = document.getElementById("sending-status");
const messagesList = document.getElementById("messagesList");
+ // Keystore
+ const importKeystoreBtn = document.getElementById("importKeystore");
+ const importKeystoreInput = document.getElementById("importKeystoreInput");
+ const exportKeystore = document.getElementById("exportKeystore");
+ const keystoreStatus = document.getElementById("keystoreStatus");
+ const keystorePassword = document.getElementById("keystorePassword");
+ const keystoreOptions = document.getElementById("keystoreOptions");
+ const readKeystoreButton = document.getElementById("readKeystore");
+
// set initial state
+ keystoreHashDiv.innerText = "not registered yet";
idDiv.innerText = "not registered yet";
registerButton.disabled = true;
- importManually.disabled = true;
textInput.disabled = true;
sendButton.disabled = true;
dialButton.disabled = true;
retrieveRLNDetailsButton.disabled = true;
nicknameInput.disabled = true;
- // monitor & enable buttons if needed
- membershipIdInput.onchange = enableManualImportIfNeeded;
- idSecretHashInput.onchange = enableManualImportIfNeeded;
- commitmentKeyInput.onchange = enableManualImportIfNeeded;
- idNullifierInput.onchange = enableManualImportIfNeeded;
- idTrapdoorInput.onchange = enableManualImportIfNeeded;
-
- function enableManualImportIfNeeded() {
- const isValuesPresent =
- idSecretHashInput.value &&
- commitmentKeyInput.value &&
- idNullifierInput.value &&
- idTrapdoorInput.value &&
- membershipIdInput.value;
-
- if (isValuesPresent) {
- importManually.disabled = false;
- }
- }
-
nicknameInput.onchange = enableChatIfNeeded;
nicknameInput.onblur = enableChatIfNeeded;
@@ -353,22 +366,32 @@ function initUI() {
}
}
+ // Keystore
+ keystorePassword.onchange = enableRegisterIfNeeded;
+ keystorePassword.onblur = enableRegisterIfNeeded;
+ function enableRegisterIfNeeded() {
+ if (keystorePassword.value && commitmentDiv.innerText !== "none") {
+ registerButton.disabled = false;
+ }
+ }
+
return {
// UI for RLN
setRlnStatus(text) {
statusSpan.innerText = text;
},
- setMembershipInfo(id, credential) {
+ setMembershipInfo(id, credential, keystoreHash) {
+ keystoreHashDiv.innerText = keystoreHash || "not registered yet";
idDiv.innerText = id || "not registered yet";
secretHashDiv.innerText = utils.bytesToHex(credential.IDSecretHash);
commitmentDiv.innerText = utils.bytesToHex(credential.IDCommitment);
nullifierDiv.innerText = utils.bytesToHex(credential.IDNullifier);
trapdoorDiv.innerText = utils.bytesToHex(credential.IDTrapdoor);
},
- setLastMember(index, pubkey) {
+ setLastMember(index, _idCommitment) {
try {
const idCommitment = ethers.utils.zeroPad(
- ethers.utils.arrayify(pubkey),
+ ethers.utils.arrayify(_idCommitment),
32
);
const indexInt = index.toNumber();
@@ -399,7 +422,15 @@ function initUI() {
retrieveRLNDetailsButton.disabled = true;
},
enableRegisterButtonForSepolia(chainId) {
- registerButton.disabled = isSepolia(chainId) ? false : true;
+ registerButton.disabled =
+ isSepolia(chainId) &&
+ keystorePassword.value &&
+ commitmentDiv.innerText !== "none"
+ ? false
+ : true;
+ },
+ getKeystorePassword() {
+ return keystorePassword.value;
},
setAccount(accounts) {
addressDiv.innerText = accounts.length ? accounts[0] : "";
@@ -415,24 +446,6 @@ function initUI() {
await fn();
});
},
- onManualImport(fn) {
- importManually.addEventListener("click", () => {
- const idTrapdoor = utils.hexToBytes(idTrapdoorInput.value);
- const idNullifier = utils.hexToBytes(idNullifierInput.value);
- const idCommitment = utils.hexToBytes(commitmentKeyInput.value);
- const idSecretHash = utils.hexToBytes(idSecretHashInput.value);
-
- const membershipId = membershipIdInput.value;
- const credentials = new IdentityCredential(
- idTrapdoor,
- idNullifier,
- idSecretHash,
- idCommitment
- );
-
- fn(membershipId, credentials);
- });
- },
onWalletImport(fn) {
importFromWalletButton.addEventListener("click", async () => {
await fn();
@@ -450,6 +463,68 @@ function initUI() {
}
});
},
+ // Keystore
+ addKeystoreOption(id) {
+ const option = document.createElement("option");
+ option.innerText = id;
+ option.setAttribute("value", id);
+ keystoreOptions.appendChild(option);
+ },
+ createKeystoreOptions(keystore) {
+ const ids = Object.keys(keystore.toObject().credentials || {});
+ keystoreOptions.innerHTML = "";
+ ids.forEach((v) => this.addKeystoreOption(v));
+ },
+ onKeystoreRead(fn) {
+ readKeystoreButton.addEventListener("click", async (event) => {
+ event.preventDefault();
+ if (!keystoreOptions.value) {
+ throw Error("No value selected to read from Keystore");
+ }
+ const credentials = await fn(
+ keystoreOptions.value,
+ keystorePassword.value
+ );
+ this.setMembershipInfo(
+ credentials.membership.treeIndex,
+ credentials.identity,
+ keystoreOptions.value
+ );
+ });
+ },
+ setKeystoreStatus(text) {
+ keystoreStatus.innerText = text;
+ },
+ onKeystoreImport(fn) {
+ importKeystoreBtn.addEventListener("click", (event) => {
+ event.preventDefault();
+ importKeystoreInput.click();
+ });
+ importKeystoreInput.addEventListener("change", async (event) => {
+ const file = event.target.files[0];
+ if (!file) {
+ console.error("No file selected");
+ return;
+ }
+ const text = await file.text();
+ fn(text);
+ });
+ },
+ onKeystoreExport(fn) {
+ exportKeystore.addEventListener("click", async (event) => {
+ event.preventDefault();
+ const filename = "keystore.json";
+ const text = await fn();
+ const file = new File([text], filename, {
+ type: "application/json",
+ });
+
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(file);
+ link.download = filename;
+ link.click();
+ });
+ },
// UI for Waku
setWakuStatus(text) {
statusDiv.innerText = text;
@@ -501,3 +576,31 @@ function initUI() {
function isSepolia(id) {
return id === 11155111;
}
+
+function initKeystore(ui) {
+ try {
+ const text = readLocalKeystore();
+ if (!text) {
+ ui.setKeystoreStatus("Initialized empty keystore");
+ return Keystore.create();
+ }
+ const keystore = Keystore.fromString(text);
+ if (!keystore) {
+ throw Error("Failed to create from string");
+ }
+ ui.setKeystoreStatus("Loaded from localStorage");
+ return keystore;
+ } catch (err) {
+ console.error("Failed to init keystore:", err);
+ ui.setKeystoreStatus("Initialized empty keystore");
+ return Keystore.create();
+ }
+}
+
+function readLocalKeystore() {
+ return localStorage.getItem("keystore");
+}
+
+function saveLocalKeystore(keystore) {
+ localStorage.setItem("keystore", keystore.toString());
+}