2023-08-07 11:16:49 +00:00
import * as utils from "https://unpkg.com/@waku/utils@0.0.10/bundle/bytes.js" ;
2023-04-27 23:35:12 +00:00
import {
createEncoder ,
createDecoder ,
waitForRemotePeer ,
2023-06-20 22:27:04 +00:00
createLightNode ,
2023-08-07 11:16:49 +00:00
} from "https://unpkg.com/@waku/sdk@0.0.18/bundle/index.js" ;
2023-04-27 23:35:12 +00:00
import { protobuf } from "https://taisukef.github.io/protobuf-es.js/dist/protobuf-es.js" ;
import {
create ,
2023-10-20 11:03:39 +00:00
Keystore ,
2023-04-27 23:35:12 +00:00
RLNDecoder ,
RLNEncoder ,
RLNContract ,
2023-10-20 11:03:39 +00:00
SEPOLIA _CONTRACT ,
} from "https://unpkg.com/@waku/rln@0.1.1-fa49e29/bundle/index.js" ;
2023-04-27 23:35:12 +00:00
import { ethers } from "https://unpkg.com/ethers@5.7.2/dist/ethers.esm.min.js" ;
const ContentTopic = "/toy-chat/2/luzhou/proto" ;
// Protobuf
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 , "bytes" ) ) ;
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" ;
run ( )
. then ( ( ) => {
console . log ( "Successfully started application." ) ;
} )
. catch ( ( err ) => {
console . error ( "Failed at starting application with " , err . message ) ;
} ) ;
async function run ( ) {
const ui = initUI ( ) ;
const rln = await initRLN ( ui ) ;
await initWaku ( ui , rln ) ;
}
async function initRLN ( ui ) {
const result = {
encoder : undefined ,
rlnInstance : undefined ,
2023-08-03 07:44:59 +00:00
contract : undefined ,
2023-04-27 23:35:12 +00:00
} ;
const provider = new ethers . providers . Web3Provider ( window . ethereum , "any" ) ;
ui . setRlnStatus ( "WASM Blob download in progress..." ) ;
const rlnInstance = await create ( ) ;
ui . setRlnStatus ( "WASM Blob download in progress... done!" ) ;
2023-10-20 11:03:39 +00:00
const rlnContract = await RLNContract . init ( rlnInstance , {
registryAddress : SEPOLIA _CONTRACT . address ,
2023-04-27 23:35:12 +00:00
provider : provider . getSigner ( ) ,
} ) ;
2023-08-03 07:44:59 +00:00
result . contract = rlnContract ;
2023-10-20 11:03:39 +00:00
// 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 ) ;
} ) ;
2023-04-27 23:35:12 +00:00
// Wallet logic
window . ethereum . on ( "accountsChanged" , ui . setAccount ) ;
window . ethereum . on ( "chainChanged" , ( chainId ) => {
const id = parseInt ( chainId , 16 ) ;
2023-05-17 20:09:49 +00:00
ui . disableIfNotSepolia ( id ) ;
2023-04-27 23:35:12 +00:00
} ) ;
ui . onConnectWallet ( async ( ) => {
try {
const accounts = await provider . send ( "eth_requestAccounts" , [ ] ) ;
ui . setAccount ( accounts ) ;
const network = await provider . getNetwork ( ) ;
2023-05-17 20:09:49 +00:00
ui . disableIfNotSepolia ( network . chainId ) ;
2023-04-27 23:35:12 +00:00
} catch ( e ) {
console . log ( "No web3 provider available" , e ) ;
}
} ) ;
ui . onRetrieveDetails ( async ( ) => {
const filter = rlnContract . contract . filters . MemberRegistered ( ) ;
ui . disableRetrieveButton ( ) ;
2023-10-20 11:03:39 +00:00
await rlnContract . fetchMembers ( rlnInstance ) ;
2023-04-27 23:35:12 +00:00
ui . enableRetrieveButton ( ) ;
rlnContract . subscribeToMembers ( rlnInstance ) ;
const last = rlnContract . members . at ( - 1 ) ;
if ( last ) {
2023-10-20 11:03:39 +00:00
ui . setLastMember ( last . index , last . idCommitment ) ;
2023-04-27 23:35:12 +00:00
}
// make sure we have subscriptions to keep updating last item
2023-10-20 11:03:39 +00:00
rlnContract . contract . on ( filter , ( _idCommitment , _index , event ) => {
ui . setLastMember ( event . args . index , event . args . idCommitment ) ;
2023-04-27 23:35:12 +00:00
} ) ;
} ) ;
let signature ;
let membershipId ;
2023-05-17 20:09:49 +00:00
let credentials ;
2023-04-27 23:35:12 +00:00
ui . onWalletImport ( async ( ) => {
const signer = provider . getSigner ( ) ;
2023-10-20 11:03:39 +00:00
signature = await signer . signMessage (
` ${ SIGNATURE _MESSAGE } . Nonce: ${ Math . ceil ( Math . random ( ) * 1000 ) } `
) ;
2023-05-17 20:09:49 +00:00
credentials = await rlnInstance . generateSeededIdentityCredential ( signature ) ;
2023-04-27 23:35:12 +00:00
2023-05-17 20:09:49 +00:00
const idCommitment = ethers . utils . hexlify ( credentials . IDCommitment ) ;
2023-04-27 23:35:12 +00:00
rlnContract . members . forEach ( ( m ) => {
2023-10-20 11:03:39 +00:00
if ( m . idCommitment === idCommitment ) {
2023-04-27 23:35:12 +00:00
membershipId = m . index . toString ( ) ;
}
} ) ;
if ( membershipId ) {
result . encoder = new RLNEncoder (
createEncoder ( {
ephemeral : false ,
contentTopic : ContentTopic ,
} ) ,
rlnInstance ,
membershipId ,
2023-05-17 20:09:49 +00:00
credentials
2023-04-27 23:35:12 +00:00
) ;
}
2023-05-17 20:09:49 +00:00
ui . setMembershipInfo ( membershipId , credentials ) ;
2023-04-27 23:35:12 +00:00
const network = await provider . getNetwork ( ) ;
2023-05-17 20:09:49 +00:00
ui . enableRegisterButtonForSepolia ( network . chainId ) ;
2023-04-27 23:35:12 +00:00
} ) ;
ui . onRegister ( async ( ) => {
ui . setRlnStatus ( "Trying to register..." ) ;
2023-10-20 11:03:39 +00:00
const memberInfo = signature
2023-05-17 20:09:49 +00:00
? await rlnContract . registerWithSignature ( rlnInstance , signature )
: await rlnContract . registerWithKey ( credentials ) ;
2023-04-27 23:35:12 +00:00
2023-10-20 11:03:39 +00:00
membershipId = memberInfo . index . toNumber ( ) ;
2023-04-27 23:35:12 +00:00
console . log (
"Obtained index for current membership credentials" ,
membershipId
) ;
2023-10-20 11:03:39 +00:00
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 ` ) ;
2023-04-27 23:35:12 +00:00
ui . setRlnStatus ( "Successfully registered." ) ;
2023-10-20 11:03:39 +00:00
ui . setMembershipInfo ( membershipId , credentials , keystoreHash ) ;
2023-04-27 23:35:12 +00:00
ui . enableDialButton ( ) ;
} ) ;
return result ;
}
async function initWaku ( ui , rln ) {
ui . setWakuStatus ( "Creating Waku node." ) ;
const node = await createLightNode ( ) ;
ui . setWakuStatus ( "Starting Waku node." ) ;
await node . start ( ) ;
ui . setWakuStatus ( "Waku node started." ) ;
const verifyMessage = ( message ) => {
if ( message . proofState === "verifying..." ) {
try {
console . log ( "Verifying proof without roots" ) ;
console . time ( "proof_verify_timer" ) ;
2023-08-03 07:44:59 +00:00
const res = message . verify ( rln . contract . roots ( ) ) ;
2023-04-27 23:35:12 +00:00
console . timeEnd ( "proof_verify_timer" ) ;
console . log ( "proof verified without roots" , res ) ;
if ( res === undefined ) {
message . proofState = "no proof attached" ;
} else if ( res ) {
message . proofState = "verified." ;
} else {
message . proofState = "invalid!" ;
}
} catch ( e ) {
message . proofState = "Error encountered, check console" ;
console . error ( "Error verifying proof:" , e ) ;
}
console . log ( "Verifying proof with roots" , message . verify ( ) ) ;
}
} ;
const onFilterMessage = ( wakuMessage ) => {
const { timestamp , nick , text } = ProtoChatMessage . decode (
wakuMessage . payload
) ;
const time = new Date ( ) ;
time . setTime ( Number ( timestamp ) * 1000 ) ;
if ( wakuMessage . rateLimitProof ) {
console . log ( "Proof received:" , wakuMessage . rateLimitProof ) ;
}
wakuMessage . proofState = ! ! wakuMessage . rateLimitProof
? "verifying..."
: "no proof attached" ;
wakuMessage . msg = `
( $ { nick } )
< strong > $ { utils . bytesToUtf8 ( text ) } < / s t r o n g >
< i > [ $ { time . toISOString ( ) } ] < / i >
` ;
verifyMessage ( wakuMessage ) ;
ui . renderMessage ( wakuMessage ) ;
} ;
ui . onDial ( async ( ma ) => {
ui . setWakuStatus ( "Dialing peer." ) ;
// TODO(@weboko): move this fix into Waku.dial
const multiaddr = MultiformatsMultiaddr . multiaddr ( ma ) ;
await node . dial ( multiaddr , [ "filter" , "lightpush" ] ) ;
await waitForRemotePeer ( node , [ "filter" , "lightpush" ] ) ;
ui . setWakuStatus ( "Waku node connected." ) ;
const decoder = new RLNDecoder (
rln . rlnInstance ,
createDecoder ( ContentTopic )
) ;
await node . filter . subscribe ( decoder , onFilterMessage ) ;
ui . setWakuStatus ( "Waku node subscribed." ) ;
ui . enableChatButtonsIfNickSet ( ) ;
} ) ;
ui . onSendMessage ( 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..." ) ;
ui . setSendingStatus ( "sending..." ) ;
await node . lightPush . send ( rln . encoder , { payload , timestamp } ) ;
ui . setSendingStatus ( "sent!" ) ;
console . log ( "Message sent!" ) ;
ui . clearMessageArea ( ) ;
} ) ;
}
function initUI ( ) {
const statusSpan = document . getElementById ( "status" ) ;
// Blockchain Elements
const addressDiv = document . getElementById ( "address" ) ;
const connectWalletButton = document . getElementById ( "connect-wallet" ) ;
const latestMembershipSpan = document . getElementById ( "latest-membership-id" ) ;
const retrieveRLNDetailsButton = document . getElementById (
"retrieve-rln-details"
) ;
const importFromWalletButton = document . getElementById (
"import-from-wallet-button"
) ;
2023-10-20 11:03:39 +00:00
const keystoreHashDiv = document . getElementById ( "keystoreHash" ) ;
2023-04-27 23:35:12 +00:00
const idDiv = document . getElementById ( "id" ) ;
2023-05-17 20:09:49 +00:00
const secretHashDiv = document . getElementById ( "secret-hash" ) ;
2023-04-27 23:35:12 +00:00
const commitmentDiv = document . getElementById ( "commitment" ) ;
2023-05-17 20:09:49 +00:00
const trapdoorDiv = document . getElementById ( "trapdoor" ) ;
const nullifierDiv = document . getElementById ( "nullifier" ) ;
2023-04-27 23:35:12 +00:00
const registerButton = document . getElementById ( "register-button" ) ;
// Waku Elements
const statusDiv = document . getElementById ( "waku-status" ) ;
const remoteMultiAddrInput = document . getElementById ( "remote-multiaddr" ) ;
const dialButton = document . getElementById ( "dial" ) ;
const nicknameInput = document . getElementById ( "nick-input" ) ;
const textInput = document . getElementById ( "textInput" ) ;
const sendButton = document . getElementById ( "sendButton" ) ;
const sendingStatusSpan = document . getElementById ( "sending-status" ) ;
const messagesList = document . getElementById ( "messagesList" ) ;
2023-10-20 11:03:39 +00:00
// 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" ) ;
2023-04-27 23:35:12 +00:00
// set initial state
2023-10-20 11:03:39 +00:00
keystoreHashDiv . innerText = "not registered yet" ;
2023-04-27 23:35:12 +00:00
idDiv . innerText = "not registered yet" ;
registerButton . disabled = true ;
textInput . disabled = true ;
sendButton . disabled = true ;
dialButton . disabled = true ;
retrieveRLNDetailsButton . disabled = true ;
nicknameInput . disabled = true ;
nicknameInput . onchange = enableChatIfNeeded ;
nicknameInput . onblur = enableChatIfNeeded ;
function enableChatIfNeeded ( ) {
if ( nicknameInput . value ) {
textInput . disabled = false ;
sendButton . disabled = false ;
}
}
2023-10-20 11:03:39 +00:00
// Keystore
keystorePassword . onchange = enableRegisterIfNeeded ;
keystorePassword . onblur = enableRegisterIfNeeded ;
function enableRegisterIfNeeded ( ) {
if ( keystorePassword . value && commitmentDiv . innerText !== "none" ) {
registerButton . disabled = false ;
}
}
2023-04-27 23:35:12 +00:00
return {
// UI for RLN
setRlnStatus ( text ) {
statusSpan . innerText = text ;
} ,
2023-10-20 11:03:39 +00:00
setMembershipInfo ( id , credential , keystoreHash ) {
keystoreHashDiv . innerText = keystoreHash || "not registered yet" ;
2023-04-27 23:35:12 +00:00
idDiv . innerText = id || "not registered yet" ;
2023-05-17 20:09:49 +00:00
secretHashDiv . innerText = utils . bytesToHex ( credential . IDSecretHash ) ;
commitmentDiv . innerText = utils . bytesToHex ( credential . IDCommitment ) ;
nullifierDiv . innerText = utils . bytesToHex ( credential . IDNullifier ) ;
trapdoorDiv . innerText = utils . bytesToHex ( credential . IDTrapdoor ) ;
2023-04-27 23:35:12 +00:00
} ,
2023-10-20 11:03:39 +00:00
setLastMember ( index , _idCommitment ) {
2023-04-27 23:35:12 +00:00
try {
const idCommitment = ethers . utils . zeroPad (
2023-10-20 11:03:39 +00:00
ethers . utils . arrayify ( _idCommitment ) ,
2023-04-27 23:35:12 +00:00
32
) ;
const indexInt = index . toNumber ( ) ;
console . debug (
"IDCommitment registered in tree" ,
idCommitment ,
indexInt
) ;
latestMembershipSpan . innerHTML = indexInt ;
} catch ( err ) {
console . error ( err ) ; // TODO: the merkle tree can be in a wrong state. The app should be disabled
}
} ,
2023-05-17 20:09:49 +00:00
disableIfNotSepolia ( chainId ) {
if ( ! isSepolia ( chainId ) ) {
window . alert ( "Switch to Sepolia" ) ;
2023-04-27 23:35:12 +00:00
registerButton . disabled = true ;
this . disableRetrieveButton ( ) ;
} else {
this . enableRetrieveButton ( ) ;
}
} ,
enableRetrieveButton ( ) {
retrieveRLNDetailsButton . disabled = false ;
} ,
disableRetrieveButton ( ) {
retrieveRLNDetailsButton . disabled = true ;
} ,
2023-05-17 20:09:49 +00:00
enableRegisterButtonForSepolia ( chainId ) {
2023-10-20 11:03:39 +00:00
registerButton . disabled =
isSepolia ( chainId ) &&
keystorePassword . value &&
commitmentDiv . innerText !== "none"
? false
: true ;
} ,
getKeystorePassword ( ) {
return keystorePassword . value ;
2023-04-27 23:35:12 +00:00
} ,
setAccount ( accounts ) {
addressDiv . innerText = accounts . length ? accounts [ 0 ] : "" ;
} ,
onConnectWallet ( fn ) {
connectWalletButton . addEventListener ( "click" , async ( ) => {
await fn ( ) ;
importFromWalletButton . disabled = false ;
} ) ;
} ,
onRetrieveDetails ( fn ) {
retrieveRLNDetailsButton . addEventListener ( "click" , async ( ) => {
await fn ( ) ;
} ) ;
} ,
onWalletImport ( fn ) {
importFromWalletButton . addEventListener ( "click" , async ( ) => {
await fn ( ) ;
} ) ;
} ,
onRegister ( fn ) {
registerButton . addEventListener ( "click" , async ( ) => {
try {
registerButton . disabled = true ;
await fn ( ) ;
registerButton . disabled = false ;
} catch ( err ) {
alert ( err ) ;
registerButton . disabled = false ;
}
} ) ;
} ,
2023-10-20 11:03:39 +00:00
// 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 ( ) ;
} ) ;
} ,
2023-04-27 23:35:12 +00:00
// UI for Waku
setWakuStatus ( text ) {
statusDiv . innerText = text ;
} ,
setSendingStatus ( text ) {
sendingStatusSpan . innerText = text ;
} ,
renderMessage ( message ) {
messagesList . innerHTML += ` <li> ${ message . msg } - [epoch: ${ message . epoch } , proof: ${ message . proofState } ]</li> ` ;
} ,
enableDialButton ( ) {
dialButton . disabled = false ;
} ,
enableChatButtonsIfNickSet ( ) {
if ( nicknameInput . value ) {
textInput . disabled = false ;
sendButton . disabled = false ;
}
} ,
onDial ( fn ) {
dialButton . addEventListener ( "click" , async ( ) => {
const multiaddr = remoteMultiAddrInput . value ;
if ( ! multiaddr ) {
this . setWakuStatus ( "Error: No multiaddr provided." ) ;
return ;
}
await fn ( multiaddr ) ;
nicknameInput . disabled = false ;
} ) ;
} ,
clearMessageArea ( ) {
textInput . value = null ;
setTimeout ( ( ) => {
this . setSendingStatus ( "" ) ;
} , 5000 ) ;
} ,
onSendMessage ( fn ) {
sendButton . addEventListener ( "click" , async ( ) => {
const nick = nicknameInput . value ;
const text = utils . utf8ToBytes ( textInput . value ) ;
await fn ( nick , text ) ;
} ) ;
} ,
} ;
}
2023-05-17 20:09:49 +00:00
function isSepolia ( id ) {
return id === 11155111 ;
2023-04-27 23:35:12 +00:00
}
2023-10-20 11:03:39 +00:00
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 ( ) ) ;
}