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:


-
- -
+
-

Import existing credentials manually:

-
- - - - - - - - - - - -
+

Read from Keystore:

+
+ +
+
+

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()); +}