feat: add support for Keystore and new contract (#280)
* add support for Keystore and new contract * update mock * up rln version
This commit is contained in:
parent
736f3209be
commit
5f3b0e22b0
|
@ -42,45 +42,69 @@
|
|||
<code class="value" id="latest-membership-id">Not loaded yet</code>
|
||||
</div>
|
||||
|
||||
<h2 class="mu1">Credentials</h2>
|
||||
<div class="row rcenter mu1">
|
||||
<div>
|
||||
<h2>Keystore</h2>
|
||||
<span id="keystoreStatus"></span>
|
||||
</div>
|
||||
<div>
|
||||
<button id="importKeystore" type="button">Import json</button>
|
||||
<input type="file" id="importKeystoreInput" style="display: none" />
|
||||
|
||||
<button id="exportKeystore" type="button">Download json</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="row mu1">
|
||||
<label for="keystorePassword"
|
||||
>Password(used for reading/saving into Keystore):</label
|
||||
>
|
||||
<input
|
||||
id="keystorePassword"
|
||||
name="keystorePassword"
|
||||
type="text"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="w50">
|
||||
<h4>Generate new, or import existing, credentials from wallet:</h4>
|
||||
<h4>Generate new credentials from wallet:</h4>
|
||||
<br />
|
||||
<div id="import-from-wallet">
|
||||
<button id="import-from-wallet-button" type="button">
|
||||
Generate RLN Credentials
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
id="import-from-wallet-button"
|
||||
type="button"
|
||||
style="width: 100%"
|
||||
>
|
||||
Generate new credentials
|
||||
</button>
|
||||
<br />
|
||||
<button disabled id="register-button" type="button">
|
||||
Register Credentials in Contract
|
||||
Register credentials
|
||||
</button>
|
||||
</div>
|
||||
<div class="w50">
|
||||
<h4>Import existing credentials manually:</h4>
|
||||
<div>
|
||||
<label for="membership-id"
|
||||
>Membership ID (your index in the RLN smart contract):</label
|
||||
>
|
||||
<input id="membership-id" name="membership-id" type="text" />
|
||||
<label for="id-secret-hash">RLN Secret Hash (hex string):</label>
|
||||
<input id="id-secret-hash" name="id-secret-hash" type="text" />
|
||||
<label for="commitment-key">RLN Commitment Key (hex string):</label>
|
||||
<input id="commitment-key" name="commitment-key" type="text" />
|
||||
<label for="id-trapdoor">RLN ID Trapdoor (hex string):</label>
|
||||
<input id="id-trapdoor" name="id-trapdoor" type="text" />
|
||||
<label for="id-nullifier">RLN ID Nullifier (hex string):</label>
|
||||
<input id="id-nullifier" name="id-nullifier" type="text" />
|
||||
<button disabled id="import-manually-button" type="button">
|
||||
Import RLN Credentials
|
||||
</button>
|
||||
</div>
|
||||
<h4>Read from Keystore:</h4>
|
||||
<br />
|
||||
<select
|
||||
id="keystoreOptions"
|
||||
style="
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
></select>
|
||||
<br />
|
||||
<button id="readKeystore" type="button">Read credentials</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row rcenter mu1">
|
||||
<h4>Keystore Hash</h4>
|
||||
<code class="value" id="keystoreHash">none</code>
|
||||
</div>
|
||||
<div class="row rcenter">
|
||||
<h4>Membership id</h4>
|
||||
<code class="value" id="id">none</code>
|
||||
</div>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue