diff --git a/src/index.ts b/src/index.ts index ae89bec..d0eed69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { } from "./constants.js"; import { createRLN } from "./create.js"; import { Keystore } from "./keystore/index.js"; +import { extractMetaMaskSigner } from "./metamask.js"; import { IdentityCredential, Proof, @@ -29,4 +30,5 @@ export { RLN_STORAGE_ABI, RLN_REGISTRY_ABI, SEPOLIA_CONTRACT, + extractMetaMaskSigner, }; diff --git a/src/keystore/index.ts b/src/keystore/index.ts index fc85b09..5dc3b56 100644 --- a/src/keystore/index.ts +++ b/src/keystore/index.ts @@ -1,3 +1,5 @@ import { Keystore } from "./keystore.js"; +import type { KeystoreEntity } from "./types.js"; export { Keystore }; +export type { KeystoreEntity }; diff --git a/src/keystore/keystore.ts b/src/keystore/keystore.ts index 4232aad..accd33f 100644 --- a/src/keystore/keystore.ts +++ b/src/keystore/keystore.ts @@ -14,12 +14,12 @@ import _ from "lodash"; import { v4 as uuidV4 } from "uuid"; import { buildBigIntFromUint8Array } from "../byte_utils.js"; -import type { IdentityCredential } from "../rln.js"; import { decryptEipKeystore, keccak256Checksum } from "./cipher.js"; import { isCredentialValid, isKeystoreValid } from "./schema_validator.js"; import type { Keccak256Hash, + KeystoreEntity, MembershipHash, MembershipInfo, Password, @@ -57,11 +57,6 @@ type KeystoreCreateOptions = { appIdentifier?: string; }; -type IdentityOptions = { - identity: IdentityCredential; - membership: MembershipInfo; -}; - export class Keystore { private data: NwakuKeystore; @@ -105,7 +100,7 @@ export class Keystore { } public async addCredential( - options: IdentityOptions, + options: KeystoreEntity, password: Password ): Promise { const membershipHash: MembershipHash = Keystore.computeMembershipHash( @@ -138,7 +133,7 @@ export class Keystore { public async readCredential( membershipHash: MembershipHash, password: Password - ): Promise { + ): Promise { const nwakuCredential = this.data.credentials[membershipHash]; if (!nwakuCredential) { @@ -235,9 +230,7 @@ export class Keystore { }; } - private static fromBytesToIdentity( - bytes: Uint8Array - ): null | IdentityOptions { + private static fromBytesToIdentity(bytes: Uint8Array): null | KeystoreEntity { try { const str = bytesToUtf8(bytes); const obj = JSON.parse(str); @@ -302,7 +295,7 @@ export class Keystore { // follows nwaku implementation // https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98 - private static fromIdentityToBytes(options: IdentityOptions): Uint8Array { + private static fromIdentityToBytes(options: KeystoreEntity): Uint8Array { return utf8ToBytes( JSON.stringify({ treeIndex: options.membership.treeIndex, diff --git a/src/keystore/types.ts b/src/keystore/types.ts index 4873f97..b792920 100644 --- a/src/keystore/types.ts +++ b/src/keystore/types.ts @@ -1,3 +1,5 @@ +import type { IdentityCredential } from "../rln.js"; + export type MembershipHash = string; export type Sha256Hash = string; export type Keccak256Hash = string; @@ -10,3 +12,8 @@ export type MembershipInfo = { address: string; treeIndex: number; }; + +export type KeystoreEntity = { + identity: IdentityCredential; + membership: MembershipInfo; +}; diff --git a/src/metamask.ts b/src/metamask.ts index e171205..f35423d 100644 --- a/src/metamask.ts +++ b/src/metamask.ts @@ -1,15 +1,16 @@ import { ethers } from "ethers"; -export const extractMetaMaskAccount = - async (): Promise => { - const ethereum = (window as any).ethereum; +export const extractMetaMaskSigner = async (): Promise => { + const ethereum = (window as any).ethereum; - if (!ethereum) { - throw Error( - "Missing or invalid Ethereum provider. Please install a Web3 wallet such as MetaMask." - ); - } + if (!ethereum) { + throw Error( + "Missing or invalid Ethereum provider. Please install a Web3 wallet such as MetaMask." + ); + } - await ethereum.request({ method: "eth_requestAccounts" }); - return new ethers.providers.Web3Provider(ethereum, "any"); - }; + await ethereum.request({ method: "eth_requestAccounts" }); + const provider = new ethers.providers.Web3Provider(ethereum, "any"); + + return provider.getSigner(); +}; diff --git a/src/rln.ts b/src/rln.ts index f96bdc4..4f87a39 100644 --- a/src/rln.ts +++ b/src/rln.ts @@ -1,12 +1,21 @@ +import { createDecoder, createEncoder } from "@waku/core"; import type { IRateLimitProof } from "@waku/interfaces"; +import type { + ContentTopic, + IDecodedMessage, + EncoderOptions as WakuEncoderOptions, +} from "@waku/interfaces"; import init from "@waku/zerokit-rln-wasm"; import * as zerokitRLN from "@waku/zerokit-rln-wasm"; import { ethers } from "ethers"; import { buildBigIntFromUint8Array, writeUIntLE } from "./byte_utils.js"; +import type { RLNDecoder, RLNEncoder } from "./codec.js"; +import { createRLNDecoder, createRLNEncoder } from "./codec.js"; import { SEPOLIA_CONTRACT } from "./constants.js"; import { dateToEpoch, epochIntToBytes } from "./epoch.js"; -import { extractMetaMaskAccount } from "./metamask.js"; +import type { KeystoreEntity } from "./keystore/index.js"; +import { extractMetaMaskSigner } from "./metamask.js"; import verificationKey from "./resources/verification_key.js"; import { RLNContract } from "./rln_contract.js"; import * as wc from "./witness_calculator.js"; @@ -164,34 +173,138 @@ export function sha256(input: Uint8Array): Uint8Array { type StartRLNOptions = { /** - * If not set - will extract MetaMask account and get provider from it. + * If not set - will extract MetaMask account and get signer from it. */ - provider?: ethers.providers.Provider; + signer?: ethers.Signer; /** * If not set - will use default SEPOLIA_CONTRACT address. */ registryAddress?: string; + /** + * Credentials to use for generating proofs and connecting to the contract and network. + * If provided used for validating the network chainId and connecting to registry contract. + */ + credentials?: KeystoreEntity; }; +type RegisterMembershipOptions = + | { signature: string } + | { identity: IdentityCredential }; + export class RLNInstance { - private _contract: null | RLNContract = null; + private started = false; + private starting = false; + + private _contract: undefined | RLNContract; + private _signer: undefined | ethers.Signer; + private _credentials: undefined | KeystoreEntity; constructor( private zkRLN: number, private witnessCalculator: WitnessCalculator ) {} - public get contract(): null | RLNContract { + public get contract(): undefined | RLNContract { return this._contract; } - public async start(options: StartRLNOptions = {}): Promise { - const provider = options.provider || (await extractMetaMaskAccount()); - const registryAddress = options.registryAddress || SEPOLIA_CONTRACT.address; + public get signer(): undefined | ethers.Signer { + return this._signer; + } - this._contract = await RLNContract.init(this, { + public async start(options: StartRLNOptions = {}): Promise { + if (this.started || this.starting) { + return; + } + + this.starting = true; + + try { + const { signer, credentials, registryAddress } = + await this.determineStartOptions(options); + + this._signer = signer!; + this._credentials = credentials; + this._contract = await RLNContract.init(this, { + registryAddress: registryAddress!, + signer: signer!, + }); + this.started = true; + } finally { + this.starting = false; + } + } + + private async determineStartOptions( + options: StartRLNOptions + ): Promise { + let chainId = options.credentials?.membership.chainId; + const registryAddress = + options.credentials?.membership.address || + options.registryAddress || + SEPOLIA_CONTRACT.address; + + if (registryAddress === SEPOLIA_CONTRACT.address) { + chainId = SEPOLIA_CONTRACT.chainId; + } + + const signer = options.signer || (await extractMetaMaskSigner()); + const currentChainId = await signer.getChainId(); + + if (chainId && chainId !== currentChainId) { + throw Error( + `Failed to start RLN contract, chain ID of contract is different from current one: contract-${chainId}, current network-${currentChainId}` + ); + } + + return { + signer, registryAddress, - provider, + credentials: options.credentials, + }; + } + + public async registerMembership( + options: RegisterMembershipOptions + ): Promise { + if (!this.contract) { + throw Error("RLN Contract is not initialized."); + } + + if (!options.identity || !options.signature) { + throw Error("Missing signature or identity to register membership."); + } + + let identity = options.identity; + + if (options.signature) { + identity = await this.generateSeededIdentityCredential(signature); + } + + return this.contract.registerWithIdentity(identity); + } + + public createEncoder(options: WakuEncoderOptions): RLNEncoder { + if (!this._credentials) { + throw Error( + "Failed to create Encoder: missing RLN credentials. Use createRLNEncoder directly." + ); + } + + return createRLNEncoder({ + encoder: createEncoder(options), + rlnInstance: this, + index: this._credentials.membership.treeIndex, + credential: this._credentials.identity, + }); + } + + public createDecoder( + contentTopic: ContentTopic + ): RLNDecoder { + return createRLNDecoder({ + rlnInstance: this, + decoder: createDecoder(contentTopic), }); } diff --git a/src/rln_contract.ts b/src/rln_contract.ts index 6a53fa9..6fc82e5 100644 --- a/src/rln_contract.ts +++ b/src/rln_contract.ts @@ -3,7 +3,8 @@ import { ethers } from "ethers"; import { zeroPadLE } from "./byte_utils.js"; import { RLN_REGISTRY_ABI, RLN_STORAGE_ABI } from "./constants.js"; -import { IdentityCredential, RLNInstance } from "./rln.js"; +import type { KeystoreEntity } from "./keystore/index.js"; +import { type IdentityCredential, RLNInstance } from "./rln.js"; import { MerkleRootTracker } from "./root_tracker.js"; type Member = { @@ -11,10 +12,10 @@ type Member = { index: ethers.BigNumber; }; -type Provider = ethers.Signer | ethers.providers.Provider; +type Signer = ethers.Signer; type RLNContractOptions = { - provider: Provider; + signer: Signer; registryAddress: string; }; @@ -47,7 +48,7 @@ export class RLNContract { ): Promise { const rlnContract = new RLNContract(rlnInstance, options); - await rlnContract.initStorageContract(options.provider); + await rlnContract.initStorageContract(options.signer); await rlnContract.fetchMembers(rlnInstance); rlnContract.subscribeToMembers(rlnInstance); @@ -56,20 +57,20 @@ export class RLNContract { constructor( rlnInstance: RLNInstance, - { registryAddress, provider }: RLNContractOptions + { registryAddress, signer }: RLNContractOptions ) { const initialRoot = rlnInstance.getMerkleRoot(); this.registryContract = new ethers.Contract( registryAddress, RLN_REGISTRY_ABI, - provider + signer ); this.merkleRootTracker = new MerkleRootTracker(5, initialRoot); } private async initStorageContract( - provider: Provider, + signer: Signer, options: RLNStorageOptions = {} ): Promise { const storageIndex = options?.storageIndex @@ -85,7 +86,7 @@ export class RLNContract { this.storageContract = new ethers.Contract( storageAddress, RLN_STORAGE_ABI, - provider + signer ); this._membersFilter = this.storageContract.filters.MemberRegistered(); @@ -207,19 +208,9 @@ export class RLNContract { ); } - public async registerWithSignature( - rlnInstance: RLNInstance, - signature: string - ): Promise { - const identityCredential = - await rlnInstance.generateSeededIdentityCredential(signature); - - return this.registerWithKey(identityCredential); - } - - public async registerWithKey( - credential: IdentityCredential - ): Promise { + public async registerWithIdentity( + identity: IdentityCredential + ): Promise { if (this.storageIndex === undefined) { throw Error( "Cannot register credential, no storage contract index found." @@ -228,7 +219,7 @@ export class RLNContract { const txRegisterResponse: ethers.ContractTransaction = await this.registryContract["register(uint16,uint256)"]( this.storageIndex, - credential.IDCommitmentBigInt, + identity.IDCommitmentBigInt, { gasLimit: 100000 } ); const txRegisterReceipt = await txRegisterResponse.wait(); @@ -245,9 +236,17 @@ export class RLNContract { memberRegistered.data ); + const network = await this.registryContract.provider.getNetwork(); + const address = this.registryContract.address; + const membershipId = decodedData.index.toNumber(); + return { - idCommitment: decodedData.idCommitment, - index: decodedData.index, + identity, + membership: { + address, + treeIndex: membershipId, + chainId: network.chainId, + }, }; }