mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-02 13:53:12 +00:00
feat(rln): create CredentialsManager without Zerokit (#2295)
* chore: update ABI * chore: update contract address and chain ID to Linea Sepolia * chore: improve error handling * fix: bigint conversion * chore: update tests * chore: clean comments * chore: export keystore types * chore: update README with contract address * tests: add reusable mock functions * chore: LINEA_CONTRACT instead of SEPOLIA_CONTRACT * chore: add linea to cspell * chore: add rateLimit to MembershipInfo * fix: determine start options * feat: RLNLight * chore: fix * chore: export * fix: returns for ratelimit * chore: big number conversions * chore: rename RLNLight to CredentialsManager, add logs * chore: setup and use rln_base_contract * chore: use CredentialsManager for rln.ts * chore: public methods written above private methods * fix: rate limit getter * chore: simplify getters/setters * chore: insert empty line
This commit is contained in:
parent
a8ff776962
commit
4adf8706c3
34
package-lock.json
generated
34
package-lock.json
generated
@ -6915,6 +6915,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz",
|
||||
"integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
@ -14138,6 +14139,7 @@
|
||||
"version": "18.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz",
|
||||
"integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@npmcli/fs": "^3.1.0",
|
||||
@ -14161,6 +14163,7 @@
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
@ -14181,12 +14184,14 @@
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cacache/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@ -14202,6 +14207,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
||||
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aggregate-error": "^3.0.0"
|
||||
@ -14677,6 +14683,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -20169,6 +20176,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
|
||||
"integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.3"
|
||||
@ -21068,6 +21076,7 @@
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz",
|
||||
"integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@ -21080,6 +21089,7 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
@ -27183,6 +27193,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
|
||||
"integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.3"
|
||||
@ -27195,6 +27206,7 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
|
||||
"integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
@ -27207,6 +27219,7 @@
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
@ -27219,6 +27232,7 @@
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
|
||||
"integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
@ -27231,6 +27245,7 @@
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
@ -27243,6 +27258,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
@ -27256,6 +27272,7 @@
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
@ -28103,6 +28120,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-7.0.0.tgz",
|
||||
"integrity": "sha512-xXxr8y5U0kl8dVkz2oK7yZjPBvqM2fwaO5l3Yg13p03v8+E3qQcD0JNhHzjL1vyGgxcKkD0cco+NLR72iuPk3g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"hosted-git-info": "^3.0.2",
|
||||
@ -28115,12 +28133,14 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
|
||||
"integrity": "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/npm-package-arg/node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
@ -28130,6 +28150,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
|
||||
"integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"builtins": "^1.0.3"
|
||||
@ -31815,6 +31836,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -31834,6 +31856,7 @@
|
||||
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
|
||||
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"os-homedir": "^1.0.0",
|
||||
@ -34050,6 +34073,7 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz",
|
||||
"integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
@ -34649,6 +34673,7 @@
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz",
|
||||
"integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"qrcode-terminal": "bin/qrcode-terminal.js"
|
||||
}
|
||||
@ -38569,6 +38594,7 @@
|
||||
"version": "10.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz",
|
||||
"integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.3"
|
||||
@ -39298,6 +39324,7 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
@ -39342,6 +39369,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
@ -39354,6 +39382,7 @@
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
@ -39366,6 +39395,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -39375,6 +39405,7 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
@ -40697,6 +40728,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
|
||||
"integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"unique-slug": "^4.0.0"
|
||||
@ -40709,6 +40741,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz",
|
||||
"integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"imurmurhash": "^0.1.4"
|
||||
@ -42292,6 +42325,7 @@
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chainsafe/bls-keystore": "3.0.0",
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"@waku/core": "^0.0.34",
|
||||
"@waku/utils": "^0.0.22",
|
||||
"@waku/zerokit-rln-wasm": "^0.0.13",
|
||||
|
||||
@ -77,6 +77,7 @@
|
||||
"@chainsafe/bls-keystore": "3.0.0",
|
||||
"@waku/core": "^0.0.34",
|
||||
"@waku/utils": "^0.0.22",
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"@waku/zerokit-rln-wasm": "^0.0.13",
|
||||
"ethereum-cryptography": "^3.1.0",
|
||||
"ethers": "^5.7.2",
|
||||
|
||||
707
packages/rln/src/contract/rln_base_contract.ts
Normal file
707
packages/rln/src/contract/rln_base_contract.ts
Normal file
@ -0,0 +1,707 @@
|
||||
import { Logger } from "@waku/utils";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import { IdentityCredential } from "../identity.js";
|
||||
import { DecryptedCredentials } from "../keystore/types.js";
|
||||
|
||||
import { RLN_ABI } from "./abi.js";
|
||||
import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./constants.js";
|
||||
import {
|
||||
CustomQueryOptions,
|
||||
FetchMembersOptions,
|
||||
Member,
|
||||
MembershipInfo,
|
||||
MembershipRegisteredEvent,
|
||||
MembershipState,
|
||||
RLNContractInitOptions
|
||||
} from "./types.js";
|
||||
|
||||
const log = new Logger("waku:rln:contract:base");
|
||||
|
||||
export class RLNBaseContract {
|
||||
public contract: ethers.Contract;
|
||||
private deployBlock: undefined | number;
|
||||
private rateLimit: number;
|
||||
|
||||
protected _members: Map<number, Member> = new Map();
|
||||
private _membersFilter: ethers.EventFilter;
|
||||
private _membershipErasedFilter: ethers.EventFilter;
|
||||
private _membersExpiredFilter: ethers.EventFilter;
|
||||
|
||||
/**
|
||||
* Constructor for RLNBaseContract.
|
||||
* Allows injecting a mocked contract for testing purposes.
|
||||
*/
|
||||
public constructor(options: RLNContractInitOptions) {
|
||||
// Initialize members and subscriptions
|
||||
this.fetchMembers()
|
||||
.then(() => {
|
||||
this.subscribeToMembers();
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("Failed to initialize members", { error });
|
||||
});
|
||||
|
||||
const {
|
||||
address,
|
||||
signer,
|
||||
rateLimit = DEFAULT_RATE_LIMIT,
|
||||
contract
|
||||
} = options;
|
||||
|
||||
this.validateRateLimit(rateLimit);
|
||||
|
||||
this.contract = contract || new ethers.Contract(address, RLN_ABI, signer);
|
||||
this.rateLimit = rateLimit;
|
||||
|
||||
// Initialize event filters
|
||||
this._membersFilter = this.contract.filters.MembershipRegistered();
|
||||
this._membershipErasedFilter = this.contract.filters.MembershipErased();
|
||||
this._membersExpiredFilter = this.contract.filters.MembershipExpired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current rate limit for this contract instance
|
||||
*/
|
||||
public getRateLimit(): number {
|
||||
return this.rateLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contract address
|
||||
*/
|
||||
public get address(): string {
|
||||
return this.contract.address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contract provider
|
||||
*/
|
||||
public get provider(): ethers.providers.Provider {
|
||||
return this.contract.provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum allowed rate limit from the contract
|
||||
* @returns Promise<number> The minimum rate limit in messages per epoch
|
||||
*/
|
||||
public async getMinRateLimit(): Promise<number> {
|
||||
const minRate = await this.contract.minMembershipRateLimit();
|
||||
return ethers.BigNumber.from(minRate).toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum allowed rate limit from the contract
|
||||
* @returns Promise<number> The maximum rate limit in messages per epoch
|
||||
*/
|
||||
public async getMaxRateLimit(): Promise<number> {
|
||||
const maxRate = await this.contract.maxMembershipRateLimit();
|
||||
return ethers.BigNumber.from(maxRate).toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum total rate limit across all memberships
|
||||
* @returns Promise<number> The maximum total rate limit in messages per epoch
|
||||
*/
|
||||
public async getMaxTotalRateLimit(): Promise<number> {
|
||||
const maxTotalRate = await this.contract.maxTotalRateLimit();
|
||||
return maxTotalRate.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current total rate limit usage across all memberships
|
||||
* @returns Promise<number> The current total rate limit usage in messages per epoch
|
||||
*/
|
||||
public async getCurrentTotalRateLimit(): Promise<number> {
|
||||
const currentTotal = await this.contract.currentTotalRateLimit();
|
||||
return currentTotal.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the remaining available total rate limit that can be allocated
|
||||
* @returns Promise<number> The remaining rate limit that can be allocated
|
||||
*/
|
||||
public async getRemainingTotalRateLimit(): Promise<number> {
|
||||
const [maxTotal, currentTotal] = await Promise.all([
|
||||
this.contract.maxTotalRateLimit(),
|
||||
this.contract.currentTotalRateLimit()
|
||||
]);
|
||||
return Number(maxTotal) - Number(currentTotal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the rate limit for future registrations
|
||||
* @param newRateLimit The new rate limit to use
|
||||
*/
|
||||
public async setRateLimit(newRateLimit: number): Promise<void> {
|
||||
this.validateRateLimit(newRateLimit);
|
||||
this.rateLimit = newRateLimit;
|
||||
}
|
||||
|
||||
public get members(): Member[] {
|
||||
const sortedMembers = Array.from(this._members.values()).sort(
|
||||
(left, right) => left.index.toNumber() - right.index.toNumber()
|
||||
);
|
||||
return sortedMembers;
|
||||
}
|
||||
|
||||
public async fetchMembers(options: FetchMembersOptions = {}): Promise<void> {
|
||||
const registeredMemberEvents = await RLNBaseContract.queryFilter(
|
||||
this.contract,
|
||||
{
|
||||
fromBlock: this.deployBlock,
|
||||
...options,
|
||||
membersFilter: this.membersFilter
|
||||
}
|
||||
);
|
||||
const removedMemberEvents = await RLNBaseContract.queryFilter(
|
||||
this.contract,
|
||||
{
|
||||
fromBlock: this.deployBlock,
|
||||
...options,
|
||||
membersFilter: this.membershipErasedFilter
|
||||
}
|
||||
);
|
||||
const expiredMemberEvents = await RLNBaseContract.queryFilter(
|
||||
this.contract,
|
||||
{
|
||||
fromBlock: this.deployBlock,
|
||||
...options,
|
||||
membersFilter: this.membersExpiredFilter
|
||||
}
|
||||
);
|
||||
|
||||
const events = [
|
||||
...registeredMemberEvents,
|
||||
...removedMemberEvents,
|
||||
...expiredMemberEvents
|
||||
];
|
||||
this.processEvents(events);
|
||||
}
|
||||
|
||||
public static async queryFilter(
|
||||
contract: ethers.Contract,
|
||||
options: CustomQueryOptions
|
||||
): Promise<ethers.Event[]> {
|
||||
const FETCH_CHUNK = 5;
|
||||
const BLOCK_RANGE = 3000;
|
||||
|
||||
const {
|
||||
fromBlock,
|
||||
membersFilter,
|
||||
fetchRange = BLOCK_RANGE,
|
||||
fetchChunks = FETCH_CHUNK
|
||||
} = options;
|
||||
|
||||
if (fromBlock === undefined) {
|
||||
return contract.queryFilter(membersFilter);
|
||||
}
|
||||
|
||||
if (!contract.provider) {
|
||||
throw Error("No provider found on the contract.");
|
||||
}
|
||||
|
||||
const toBlock = await contract.provider.getBlockNumber();
|
||||
|
||||
if (toBlock - fromBlock < fetchRange) {
|
||||
return contract.queryFilter(membersFilter, fromBlock, toBlock);
|
||||
}
|
||||
|
||||
const events: ethers.Event[][] = [];
|
||||
const chunks = RLNBaseContract.splitToChunks(
|
||||
fromBlock,
|
||||
toBlock,
|
||||
fetchRange
|
||||
);
|
||||
|
||||
for (const portion of RLNBaseContract.takeN<[number, number]>(
|
||||
chunks,
|
||||
fetchChunks
|
||||
)) {
|
||||
const promises = portion.map(([left, right]) =>
|
||||
RLNBaseContract.ignoreErrors(
|
||||
contract.queryFilter(membersFilter, left, right),
|
||||
[]
|
||||
)
|
||||
);
|
||||
const fetchedEvents = await Promise.all(promises);
|
||||
events.push(fetchedEvents.flatMap((v) => v));
|
||||
}
|
||||
|
||||
return events.flatMap((v) => v);
|
||||
}
|
||||
|
||||
public processEvents(events: ethers.Event[]): void {
|
||||
const toRemoveTable = new Map<number, number[]>();
|
||||
const toInsertTable = new Map<number, ethers.Event[]>();
|
||||
|
||||
events.forEach((evt) => {
|
||||
if (!evt.args) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
evt.event === "MembershipErased" ||
|
||||
evt.event === "MembershipExpired"
|
||||
) {
|
||||
let index = evt.args.index;
|
||||
|
||||
if (!index) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof index === "number" || typeof index === "string") {
|
||||
index = ethers.BigNumber.from(index);
|
||||
}
|
||||
|
||||
const toRemoveVal = toRemoveTable.get(evt.blockNumber);
|
||||
if (toRemoveVal != undefined) {
|
||||
toRemoveVal.push(index.toNumber());
|
||||
toRemoveTable.set(evt.blockNumber, toRemoveVal);
|
||||
} else {
|
||||
toRemoveTable.set(evt.blockNumber, [index.toNumber()]);
|
||||
}
|
||||
} else if (evt.event === "MembershipRegistered") {
|
||||
let eventsPerBlock = toInsertTable.get(evt.blockNumber);
|
||||
if (eventsPerBlock == undefined) {
|
||||
eventsPerBlock = [];
|
||||
}
|
||||
|
||||
eventsPerBlock.push(evt);
|
||||
toInsertTable.set(evt.blockNumber, eventsPerBlock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static splitToChunks(
|
||||
from: number,
|
||||
to: number,
|
||||
step: number
|
||||
): Array<[number, number]> {
|
||||
const chunks: Array<[number, number]> = [];
|
||||
|
||||
let left = from;
|
||||
while (left < to) {
|
||||
const right = left + step < to ? left + step : to;
|
||||
|
||||
chunks.push([left, right] as [number, number]);
|
||||
|
||||
left = right;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
public static *takeN<T>(array: T[], size: number): Iterable<T[]> {
|
||||
let start = 0;
|
||||
|
||||
while (start < array.length) {
|
||||
const portion = array.slice(start, start + size);
|
||||
|
||||
yield portion;
|
||||
|
||||
start += size;
|
||||
}
|
||||
}
|
||||
|
||||
public static async ignoreErrors<T>(
|
||||
promise: Promise<T>,
|
||||
defaultValue: T
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
log.info(`Ignoring an error during query: ${err.message}`);
|
||||
} else {
|
||||
log.info(`Ignoring an unknown error during query`);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public subscribeToMembers(): void {
|
||||
this.contract.on(
|
||||
this.membersFilter,
|
||||
(
|
||||
_idCommitment: string,
|
||||
_membershipRateLimit: ethers.BigNumber,
|
||||
_index: ethers.BigNumber,
|
||||
event: ethers.Event
|
||||
) => {
|
||||
this.processEvents([event]);
|
||||
}
|
||||
);
|
||||
|
||||
this.contract.on(
|
||||
this.membershipErasedFilter,
|
||||
(
|
||||
_idCommitment: string,
|
||||
_membershipRateLimit: ethers.BigNumber,
|
||||
_index: ethers.BigNumber,
|
||||
event: ethers.Event
|
||||
) => {
|
||||
this.processEvents([event]);
|
||||
}
|
||||
);
|
||||
|
||||
this.contract.on(
|
||||
this.membersExpiredFilter,
|
||||
(
|
||||
_idCommitment: string,
|
||||
_membershipRateLimit: ethers.BigNumber,
|
||||
_index: ethers.BigNumber,
|
||||
event: ethers.Event
|
||||
) => {
|
||||
this.processEvents([event]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get remaining messages in current epoch
|
||||
* @param membershipId The ID of the membership to check
|
||||
* @returns number of remaining messages allowed in current epoch
|
||||
*/
|
||||
public async getRemainingMessages(membershipId: number): Promise<number> {
|
||||
try {
|
||||
const [startTime, , rateLimit] =
|
||||
await this.contract.getMembershipInfo(membershipId);
|
||||
|
||||
// Calculate current epoch
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const epochsPassed = Math.floor(
|
||||
(currentTime - startTime) / RATE_LIMIT_PARAMS.EPOCH_LENGTH
|
||||
);
|
||||
const currentEpochStart =
|
||||
startTime + epochsPassed * RATE_LIMIT_PARAMS.EPOCH_LENGTH;
|
||||
|
||||
// Get message count in current epoch using contract's function
|
||||
const messageCount = await this.contract.getMessageCount(
|
||||
membershipId,
|
||||
currentEpochStart
|
||||
);
|
||||
return Math.max(
|
||||
0,
|
||||
ethers.BigNumber.from(rateLimit)
|
||||
.sub(ethers.BigNumber.from(messageCount))
|
||||
.toNumber()
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error getting remaining messages: ${(error as Error).message}`
|
||||
);
|
||||
return 0; // Fail safe: assume no messages remaining on error
|
||||
}
|
||||
}
|
||||
|
||||
public async getMembershipInfo(
|
||||
idCommitment: string
|
||||
): Promise<MembershipInfo | undefined> {
|
||||
try {
|
||||
const [startBlock, endBlock, rateLimit] =
|
||||
await this.contract.getMembershipInfo(idCommitment);
|
||||
const currentBlock = await this.contract.provider.getBlockNumber();
|
||||
|
||||
let state: MembershipState;
|
||||
if (currentBlock < startBlock) {
|
||||
state = MembershipState.Active;
|
||||
} else if (currentBlock < endBlock) {
|
||||
state = MembershipState.GracePeriod;
|
||||
} else {
|
||||
state = MembershipState.Expired;
|
||||
}
|
||||
|
||||
const index = await this.getMemberIndex(idCommitment);
|
||||
if (!index) return undefined;
|
||||
|
||||
return {
|
||||
index,
|
||||
idCommitment,
|
||||
rateLimit: rateLimit.toNumber(),
|
||||
startBlock: startBlock.toNumber(),
|
||||
endBlock: endBlock.toNumber(),
|
||||
state
|
||||
};
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async extendMembership(
|
||||
idCommitment: string
|
||||
): Promise<ethers.ContractTransaction> {
|
||||
return this.contract.extendMemberships([idCommitment]);
|
||||
}
|
||||
|
||||
public async eraseMembership(
|
||||
idCommitment: string,
|
||||
eraseFromMembershipSet: boolean = true
|
||||
): Promise<ethers.ContractTransaction> {
|
||||
return this.contract.eraseMemberships(
|
||||
[idCommitment],
|
||||
eraseFromMembershipSet
|
||||
);
|
||||
}
|
||||
|
||||
public async registerMembership(
|
||||
idCommitment: string,
|
||||
rateLimit: number = DEFAULT_RATE_LIMIT
|
||||
): Promise<ethers.ContractTransaction> {
|
||||
if (
|
||||
rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
|
||||
rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
|
||||
) {
|
||||
throw new Error(
|
||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
|
||||
);
|
||||
}
|
||||
return this.contract.register(idCommitment, rateLimit, []);
|
||||
}
|
||||
|
||||
public async withdraw(token: string, holder: string): Promise<void> {
|
||||
try {
|
||||
const tx = await this.contract.withdraw(token, { from: holder });
|
||||
await tx.wait();
|
||||
} catch (error) {
|
||||
log.error(`Error in withdraw: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async registerWithIdentity(
|
||||
identity: IdentityCredential
|
||||
): Promise<DecryptedCredentials | undefined> {
|
||||
try {
|
||||
log.info(
|
||||
`Registering identity with rate limit: ${this.rateLimit} messages/epoch`
|
||||
);
|
||||
|
||||
// Check if the ID commitment is already registered
|
||||
const existingIndex = await this.getMemberIndex(
|
||||
identity.IDCommitmentBigInt.toString()
|
||||
);
|
||||
if (existingIndex) {
|
||||
throw new Error(
|
||||
`ID commitment is already registered with index ${existingIndex}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if there's enough remaining rate limit
|
||||
const remainingRateLimit = await this.getRemainingTotalRateLimit();
|
||||
if (remainingRateLimit < this.rateLimit) {
|
||||
throw new Error(
|
||||
`Not enough remaining rate limit. Requested: ${this.rateLimit}, Available: ${remainingRateLimit}`
|
||||
);
|
||||
}
|
||||
|
||||
const estimatedGas = await this.contract.estimateGas.register(
|
||||
identity.IDCommitmentBigInt,
|
||||
this.rateLimit,
|
||||
[]
|
||||
);
|
||||
const gasLimit = estimatedGas.add(10000);
|
||||
|
||||
const txRegisterResponse: ethers.ContractTransaction =
|
||||
await this.contract.register(
|
||||
identity.IDCommitmentBigInt,
|
||||
this.rateLimit,
|
||||
[],
|
||||
{ gasLimit }
|
||||
);
|
||||
|
||||
const txRegisterReceipt = await txRegisterResponse.wait();
|
||||
|
||||
if (txRegisterReceipt.status === 0) {
|
||||
throw new Error("Transaction failed on-chain");
|
||||
}
|
||||
|
||||
const memberRegistered = txRegisterReceipt.events?.find(
|
||||
(event) => event.event === "MembershipRegistered"
|
||||
);
|
||||
|
||||
if (!memberRegistered || !memberRegistered.args) {
|
||||
log.error(
|
||||
"Failed to register membership: No MembershipRegistered event found"
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const decodedData: MembershipRegisteredEvent = {
|
||||
idCommitment: memberRegistered.args.idCommitment,
|
||||
membershipRateLimit: memberRegistered.args.membershipRateLimit,
|
||||
index: memberRegistered.args.index
|
||||
};
|
||||
|
||||
log.info(
|
||||
`Successfully registered membership with index ${decodedData.index} ` +
|
||||
`and rate limit ${decodedData.membershipRateLimit}`
|
||||
);
|
||||
|
||||
const network = await this.contract.provider.getNetwork();
|
||||
const address = this.contract.address;
|
||||
const membershipId = Number(decodedData.index);
|
||||
|
||||
return {
|
||||
identity,
|
||||
membership: {
|
||||
address,
|
||||
treeIndex: membershipId,
|
||||
chainId: network.chainId,
|
||||
rateLimit: decodedData.membershipRateLimit.toNumber()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
const errorMessage = error.message;
|
||||
log.error("registerWithIdentity - error message:", errorMessage);
|
||||
log.error("registerWithIdentity - error stack:", error.stack);
|
||||
|
||||
// Try to extract more specific error information
|
||||
if (errorMessage.includes("CannotExceedMaxTotalRateLimit")) {
|
||||
throw new Error(
|
||||
"Registration failed: Cannot exceed maximum total rate limit"
|
||||
);
|
||||
} else if (errorMessage.includes("InvalidIdCommitment")) {
|
||||
throw new Error("Registration failed: Invalid ID commitment");
|
||||
} else if (errorMessage.includes("InvalidMembershipRateLimit")) {
|
||||
throw new Error("Registration failed: Invalid membership rate limit");
|
||||
} else if (errorMessage.includes("execution reverted")) {
|
||||
throw new Error(
|
||||
"Contract execution reverted. Check contract requirements."
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Error in registerWithIdentity: ${errorMessage}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unknown error in registerWithIdentity", {
|
||||
cause: error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async registerWithPermitAndErase(
|
||||
identity: IdentityCredential,
|
||||
permit: {
|
||||
owner: string;
|
||||
deadline: number;
|
||||
v: number;
|
||||
r: string;
|
||||
s: string;
|
||||
},
|
||||
idCommitmentsToErase: string[]
|
||||
): Promise<DecryptedCredentials | undefined> {
|
||||
try {
|
||||
log.info(
|
||||
`Registering identity with permit and rate limit: ${this.rateLimit} messages/epoch`
|
||||
);
|
||||
|
||||
const txRegisterResponse: ethers.ContractTransaction =
|
||||
await this.contract.registerWithPermit(
|
||||
permit.owner,
|
||||
permit.deadline,
|
||||
permit.v,
|
||||
permit.r,
|
||||
permit.s,
|
||||
identity.IDCommitmentBigInt,
|
||||
this.rateLimit,
|
||||
idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
|
||||
);
|
||||
const txRegisterReceipt = await txRegisterResponse.wait();
|
||||
|
||||
const memberRegistered = txRegisterReceipt.events?.find(
|
||||
(event) => event.event === "MembershipRegistered"
|
||||
);
|
||||
|
||||
if (!memberRegistered || !memberRegistered.args) {
|
||||
log.error(
|
||||
"Failed to register membership with permit: No MembershipRegistered event found"
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const decodedData: MembershipRegisteredEvent = {
|
||||
idCommitment: memberRegistered.args.idCommitment,
|
||||
membershipRateLimit: memberRegistered.args.membershipRateLimit,
|
||||
index: memberRegistered.args.index
|
||||
};
|
||||
|
||||
log.info(
|
||||
`Successfully registered membership with permit. Index: ${decodedData.index}, ` +
|
||||
`Rate limit: ${decodedData.membershipRateLimit}, Erased ${idCommitmentsToErase.length} commitments`
|
||||
);
|
||||
|
||||
const network = await this.contract.provider.getNetwork();
|
||||
const address = this.contract.address;
|
||||
const membershipId = Number(decodedData.index);
|
||||
|
||||
return {
|
||||
identity,
|
||||
membership: {
|
||||
address,
|
||||
treeIndex: membershipId,
|
||||
chainId: network.chainId,
|
||||
rateLimit: decodedData.membershipRateLimit.toNumber()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error in registerWithPermitAndErase: ${(error as Error).message}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the rate limit is within the allowed range
|
||||
* @throws Error if the rate limit is outside the allowed range
|
||||
*/
|
||||
private validateRateLimit(rateLimit: number): void {
|
||||
if (
|
||||
rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
|
||||
rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
|
||||
) {
|
||||
throw new Error(
|
||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE} messages per epoch`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private get membersFilter(): ethers.EventFilter {
|
||||
if (!this._membersFilter) {
|
||||
throw Error("Members filter was not initialized.");
|
||||
}
|
||||
return this._membersFilter;
|
||||
}
|
||||
|
||||
private get membershipErasedFilter(): ethers.EventFilter {
|
||||
if (!this._membershipErasedFilter) {
|
||||
throw Error("MembershipErased filter was not initialized.");
|
||||
}
|
||||
return this._membershipErasedFilter;
|
||||
}
|
||||
|
||||
private get membersExpiredFilter(): ethers.EventFilter {
|
||||
if (!this._membersExpiredFilter) {
|
||||
throw Error("MembersExpired filter was not initialized.");
|
||||
}
|
||||
return this._membersExpiredFilter;
|
||||
}
|
||||
|
||||
private async getMemberIndex(
|
||||
idCommitment: string
|
||||
): Promise<ethers.BigNumber | undefined> {
|
||||
try {
|
||||
const events = await this.contract.queryFilter(
|
||||
this.contract.filters.MembershipRegistered(idCommitment)
|
||||
);
|
||||
if (events.length === 0) return undefined;
|
||||
|
||||
// Get the most recent registration event
|
||||
const event = events[events.length - 1];
|
||||
return event.args?.index;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ describe("RLN Contract abstraction - RLN", () => {
|
||||
mockedRegistryContract
|
||||
);
|
||||
|
||||
await rlnContract.fetchMembers(rlnInstance, {
|
||||
await rlnContract.fetchMembers({
|
||||
fromBlock: 0,
|
||||
fetchRange: 1000,
|
||||
fetchChunks: 2
|
||||
|
||||
@ -2,72 +2,19 @@ import { Logger } from "@waku/utils";
|
||||
import { hexToBytes } from "@waku/utils/bytes";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import type { IdentityCredential } from "../identity.js";
|
||||
import type { DecryptedCredentials } from "../keystore/index.js";
|
||||
import type { RLNInstance } from "../rln.js";
|
||||
import { MerkleRootTracker } from "../root_tracker.js";
|
||||
import { zeroPadLE } from "../utils/bytes.js";
|
||||
|
||||
import { RLN_ABI } from "./abi.js";
|
||||
import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./constants.js";
|
||||
import { RLNBaseContract } from "./rln_base_contract.js";
|
||||
import { RLNContractInitOptions } from "./types.js";
|
||||
|
||||
const log = new Logger("waku:rln:contract");
|
||||
|
||||
type Member = {
|
||||
idCommitment: string;
|
||||
index: ethers.BigNumber;
|
||||
};
|
||||
|
||||
interface RLNContractOptions {
|
||||
signer: ethers.Signer;
|
||||
address: string;
|
||||
rateLimit?: number;
|
||||
}
|
||||
|
||||
interface RLNContractInitOptions extends RLNContractOptions {
|
||||
contract?: ethers.Contract;
|
||||
}
|
||||
|
||||
export interface MembershipRegisteredEvent {
|
||||
idCommitment: string;
|
||||
membershipRateLimit: ethers.BigNumber;
|
||||
index: ethers.BigNumber;
|
||||
}
|
||||
|
||||
type FetchMembersOptions = {
|
||||
fromBlock?: number;
|
||||
fetchRange?: number;
|
||||
fetchChunks?: number;
|
||||
};
|
||||
|
||||
export interface MembershipInfo {
|
||||
index: ethers.BigNumber;
|
||||
idCommitment: string;
|
||||
rateLimit: number;
|
||||
startBlock: number;
|
||||
endBlock: number;
|
||||
state: MembershipState;
|
||||
}
|
||||
|
||||
export enum MembershipState {
|
||||
Active = "Active",
|
||||
GracePeriod = "GracePeriod",
|
||||
Expired = "Expired",
|
||||
ErasedAwaitsWithdrawal = "ErasedAwaitsWithdrawal"
|
||||
}
|
||||
|
||||
export class RLNContract {
|
||||
public contract: ethers.Contract;
|
||||
export class RLNContract extends RLNBaseContract {
|
||||
private instance: RLNInstance;
|
||||
private merkleRootTracker: MerkleRootTracker;
|
||||
|
||||
private deployBlock: undefined | number;
|
||||
private rateLimit: number;
|
||||
|
||||
private _members: Map<number, Member> = new Map();
|
||||
private _membersFilter: ethers.EventFilter;
|
||||
private _membershipErasedFilter: ethers.EventFilter;
|
||||
private _membersExpiredFilter: ethers.EventFilter;
|
||||
|
||||
/**
|
||||
* Asynchronous initializer for RLNContract.
|
||||
* Allows injecting a mocked contract for testing purposes.
|
||||
@ -78,9 +25,6 @@ export class RLNContract {
|
||||
): Promise<RLNContract> {
|
||||
const rlnContract = new RLNContract(rlnInstance, options);
|
||||
|
||||
await rlnContract.fetchMembers(rlnInstance);
|
||||
rlnContract.subscribeToMembers(rlnInstance);
|
||||
|
||||
return rlnContract;
|
||||
}
|
||||
|
||||
@ -88,178 +32,15 @@ export class RLNContract {
|
||||
rlnInstance: RLNInstance,
|
||||
options: RLNContractInitOptions
|
||||
) {
|
||||
const {
|
||||
address,
|
||||
signer,
|
||||
rateLimit = DEFAULT_RATE_LIMIT,
|
||||
contract
|
||||
} = options;
|
||||
super(options);
|
||||
|
||||
this.validateRateLimit(rateLimit);
|
||||
this.rateLimit = rateLimit;
|
||||
this.instance = rlnInstance;
|
||||
|
||||
const initialRoot = rlnInstance.zerokit.getMerkleRoot();
|
||||
|
||||
// Use the injected contract if provided; otherwise, instantiate a new one.
|
||||
this.contract = contract || new ethers.Contract(address, RLN_ABI, signer);
|
||||
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
|
||||
|
||||
// Initialize event filters
|
||||
this._membersFilter = this.contract.filters.MembershipRegistered();
|
||||
this._membershipErasedFilter = this.contract.filters.MembershipErased();
|
||||
this._membersExpiredFilter = this.contract.filters.MembershipExpired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the rate limit is within the allowed range
|
||||
* @throws Error if the rate limit is outside the allowed range
|
||||
*/
|
||||
private validateRateLimit(rateLimit: number): void {
|
||||
if (
|
||||
rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
|
||||
rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
|
||||
) {
|
||||
throw new Error(
|
||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE} messages per epoch`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current rate limit for this contract instance
|
||||
*/
|
||||
public getRateLimit(): number {
|
||||
return this.rateLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contract address
|
||||
*/
|
||||
public get address(): string {
|
||||
return this.contract.address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contract provider
|
||||
*/
|
||||
public get provider(): ethers.providers.Provider {
|
||||
return this.contract.provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum allowed rate limit from the contract
|
||||
* @returns Promise<number> The minimum rate limit in messages per epoch
|
||||
*/
|
||||
public async getMinRateLimit(): Promise<number> {
|
||||
const minRate = await this.contract.minMembershipRateLimit();
|
||||
return minRate.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum allowed rate limit from the contract
|
||||
* @returns Promise<number> The maximum rate limit in messages per epoch
|
||||
*/
|
||||
public async getMaxRateLimit(): Promise<number> {
|
||||
const maxRate = await this.contract.maxMembershipRateLimit();
|
||||
return maxRate.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum total rate limit across all memberships
|
||||
* @returns Promise<number> The maximum total rate limit in messages per epoch
|
||||
*/
|
||||
public async getMaxTotalRateLimit(): Promise<number> {
|
||||
const maxTotalRate = await this.contract.maxTotalRateLimit();
|
||||
return maxTotalRate.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current total rate limit usage across all memberships
|
||||
* @returns Promise<number> The current total rate limit usage in messages per epoch
|
||||
*/
|
||||
public async getCurrentTotalRateLimit(): Promise<number> {
|
||||
const currentTotal = await this.contract.currentTotalRateLimit();
|
||||
return currentTotal.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the remaining available total rate limit that can be allocated
|
||||
* @returns Promise<number> The remaining rate limit that can be allocated
|
||||
*/
|
||||
public async getRemainingTotalRateLimit(): Promise<number> {
|
||||
const [maxTotal, currentTotal] = await Promise.all([
|
||||
this.contract.maxTotalRateLimit(),
|
||||
this.contract.currentTotalRateLimit()
|
||||
]);
|
||||
return Number(maxTotal) - Number(currentTotal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the rate limit for future registrations
|
||||
* @param newRateLimit The new rate limit to use
|
||||
*/
|
||||
public async setRateLimit(newRateLimit: number): Promise<void> {
|
||||
this.validateRateLimit(newRateLimit);
|
||||
this.rateLimit = newRateLimit;
|
||||
}
|
||||
|
||||
public get members(): Member[] {
|
||||
const sortedMembers = Array.from(this._members.values()).sort(
|
||||
(left, right) => left.index.toNumber() - right.index.toNumber()
|
||||
);
|
||||
return sortedMembers;
|
||||
}
|
||||
|
||||
private get membersFilter(): ethers.EventFilter {
|
||||
if (!this._membersFilter) {
|
||||
throw Error("Members filter was not initialized.");
|
||||
}
|
||||
return this._membersFilter;
|
||||
}
|
||||
|
||||
private get membershipErasedFilter(): ethers.EventFilter {
|
||||
if (!this._membershipErasedFilter) {
|
||||
throw Error("MembershipErased filter was not initialized.");
|
||||
}
|
||||
return this._membershipErasedFilter;
|
||||
}
|
||||
|
||||
private get membersExpiredFilter(): ethers.EventFilter {
|
||||
if (!this._membersExpiredFilter) {
|
||||
throw Error("MembersExpired filter was not initialized.");
|
||||
}
|
||||
return this._membersExpiredFilter;
|
||||
}
|
||||
|
||||
public async fetchMembers(
|
||||
rlnInstance: RLNInstance,
|
||||
options: FetchMembersOptions = {}
|
||||
): Promise<void> {
|
||||
const registeredMemberEvents = await queryFilter(this.contract, {
|
||||
fromBlock: this.deployBlock,
|
||||
...options,
|
||||
membersFilter: this.membersFilter
|
||||
});
|
||||
const removedMemberEvents = await queryFilter(this.contract, {
|
||||
fromBlock: this.deployBlock,
|
||||
...options,
|
||||
membersFilter: this.membershipErasedFilter
|
||||
});
|
||||
const expiredMemberEvents = await queryFilter(this.contract, {
|
||||
fromBlock: this.deployBlock,
|
||||
...options,
|
||||
membersFilter: this.membersExpiredFilter
|
||||
});
|
||||
|
||||
const events = [
|
||||
...registeredMemberEvents,
|
||||
...removedMemberEvents,
|
||||
...expiredMemberEvents
|
||||
];
|
||||
this.processEvents(rlnInstance, events);
|
||||
}
|
||||
|
||||
public processEvents(rlnInstance: RLNInstance, events: ethers.Event[]): void {
|
||||
public override processEvents(events: ethers.Event[]): void {
|
||||
const toRemoveTable = new Map<number, number[]>();
|
||||
const toInsertTable = new Map<number, ethers.Event[]>();
|
||||
|
||||
@ -306,8 +87,8 @@ export class RLNContract {
|
||||
}
|
||||
});
|
||||
|
||||
this.removeMembers(rlnInstance, toRemoveTable);
|
||||
this.insertMembers(rlnInstance, toInsertTable);
|
||||
this.removeMembers(this.instance, toRemoveTable);
|
||||
this.insertMembers(this.instance, toInsertTable);
|
||||
}
|
||||
|
||||
private insertMembers(
|
||||
@ -360,439 +141,4 @@ export class RLNContract {
|
||||
this.merkleRootTracker.backFill(blockNumber);
|
||||
});
|
||||
}
|
||||
|
||||
public subscribeToMembers(rlnInstance: RLNInstance): void {
|
||||
this.contract.on(
|
||||
this.membersFilter,
|
||||
(
|
||||
_idCommitment: string,
|
||||
_membershipRateLimit: ethers.BigNumber,
|
||||
_index: ethers.BigNumber,
|
||||
event: ethers.Event
|
||||
) => {
|
||||
this.processEvents(rlnInstance, [event]);
|
||||
}
|
||||
);
|
||||
|
||||
this.contract.on(
|
||||
this.membershipErasedFilter,
|
||||
(
|
||||
_idCommitment: string,
|
||||
_membershipRateLimit: ethers.BigNumber,
|
||||
_index: ethers.BigNumber,
|
||||
event: ethers.Event
|
||||
) => {
|
||||
this.processEvents(rlnInstance, [event]);
|
||||
}
|
||||
);
|
||||
|
||||
this.contract.on(
|
||||
this.membersExpiredFilter,
|
||||
(
|
||||
_idCommitment: string,
|
||||
_membershipRateLimit: ethers.BigNumber,
|
||||
_index: ethers.BigNumber,
|
||||
event: ethers.Event
|
||||
) => {
|
||||
this.processEvents(rlnInstance, [event]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async registerWithIdentity(
|
||||
identity: IdentityCredential
|
||||
): Promise<DecryptedCredentials | undefined> {
|
||||
try {
|
||||
log.info(
|
||||
`Registering identity with rate limit: ${this.rateLimit} messages/epoch`
|
||||
);
|
||||
|
||||
// Check if the ID commitment is already registered
|
||||
const existingIndex = await this.getMemberIndex(
|
||||
identity.IDCommitmentBigInt.toString()
|
||||
);
|
||||
if (existingIndex) {
|
||||
throw new Error(
|
||||
`ID commitment is already registered with index ${existingIndex}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if there's enough remaining rate limit
|
||||
const remainingRateLimit = await this.getRemainingTotalRateLimit();
|
||||
if (remainingRateLimit < this.rateLimit) {
|
||||
throw new Error(
|
||||
`Not enough remaining rate limit. Requested: ${this.rateLimit}, Available: ${remainingRateLimit}`
|
||||
);
|
||||
}
|
||||
|
||||
const estimatedGas = await this.contract.estimateGas.register(
|
||||
identity.IDCommitmentBigInt,
|
||||
this.rateLimit,
|
||||
[]
|
||||
);
|
||||
const gasLimit = estimatedGas.add(10000);
|
||||
|
||||
const txRegisterResponse: ethers.ContractTransaction =
|
||||
await this.contract.register(
|
||||
identity.IDCommitmentBigInt,
|
||||
this.rateLimit,
|
||||
[],
|
||||
{ gasLimit }
|
||||
);
|
||||
|
||||
const txRegisterReceipt = await txRegisterResponse.wait();
|
||||
|
||||
if (txRegisterReceipt.status === 0) {
|
||||
throw new Error("Transaction failed on-chain");
|
||||
}
|
||||
|
||||
const memberRegistered = txRegisterReceipt.events?.find(
|
||||
(event) => event.event === "MembershipRegistered"
|
||||
);
|
||||
|
||||
if (!memberRegistered || !memberRegistered.args) {
|
||||
log.error(
|
||||
"Failed to register membership: No MembershipRegistered event found"
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const decodedData: MembershipRegisteredEvent = {
|
||||
idCommitment: memberRegistered.args.idCommitment,
|
||||
membershipRateLimit: memberRegistered.args.membershipRateLimit,
|
||||
index: memberRegistered.args.index
|
||||
};
|
||||
|
||||
log.info(
|
||||
`Successfully registered membership with index ${decodedData.index} ` +
|
||||
`and rate limit ${decodedData.membershipRateLimit}`
|
||||
);
|
||||
|
||||
const network = await this.contract.provider.getNetwork();
|
||||
const address = this.contract.address;
|
||||
const membershipId = Number(decodedData.index);
|
||||
|
||||
return {
|
||||
identity,
|
||||
membership: {
|
||||
address,
|
||||
treeIndex: membershipId,
|
||||
chainId: network.chainId,
|
||||
rateLimit: decodedData.membershipRateLimit.toNumber()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
const errorMessage = error.message;
|
||||
log.error("registerWithIdentity - error message:", errorMessage);
|
||||
log.error("registerWithIdentity - error stack:", error.stack);
|
||||
|
||||
// Try to extract more specific error information
|
||||
if (errorMessage.includes("CannotExceedMaxTotalRateLimit")) {
|
||||
throw new Error(
|
||||
"Registration failed: Cannot exceed maximum total rate limit"
|
||||
);
|
||||
} else if (errorMessage.includes("InvalidIdCommitment")) {
|
||||
throw new Error("Registration failed: Invalid ID commitment");
|
||||
} else if (errorMessage.includes("InvalidMembershipRateLimit")) {
|
||||
throw new Error("Registration failed: Invalid membership rate limit");
|
||||
} else if (errorMessage.includes("execution reverted")) {
|
||||
throw new Error(
|
||||
"Contract execution reverted. Check contract requirements."
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Error in registerWithIdentity: ${errorMessage}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unknown error in registerWithIdentity", {
|
||||
cause: error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get remaining messages in current epoch
|
||||
* @param membershipId The ID of the membership to check
|
||||
* @returns number of remaining messages allowed in current epoch
|
||||
*/
|
||||
public async getRemainingMessages(membershipId: number): Promise<number> {
|
||||
try {
|
||||
const [startTime, , rateLimit] =
|
||||
await this.contract.getMembershipInfo(membershipId);
|
||||
|
||||
// Calculate current epoch
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const epochsPassed = Math.floor(
|
||||
(currentTime - startTime) / RATE_LIMIT_PARAMS.EPOCH_LENGTH
|
||||
);
|
||||
const currentEpochStart =
|
||||
startTime + epochsPassed * RATE_LIMIT_PARAMS.EPOCH_LENGTH;
|
||||
|
||||
// Get message count in current epoch using contract's function
|
||||
const messageCount = await this.contract.getMessageCount(
|
||||
membershipId,
|
||||
currentEpochStart
|
||||
);
|
||||
return Math.max(0, rateLimit.sub(messageCount).toNumber());
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error getting remaining messages: ${(error as Error).message}`
|
||||
);
|
||||
return 0; // Fail safe: assume no messages remaining on error
|
||||
}
|
||||
}
|
||||
|
||||
public async registerWithPermitAndErase(
|
||||
identity: IdentityCredential,
|
||||
permit: {
|
||||
owner: string;
|
||||
deadline: number;
|
||||
v: number;
|
||||
r: string;
|
||||
s: string;
|
||||
},
|
||||
idCommitmentsToErase: string[]
|
||||
): Promise<DecryptedCredentials | undefined> {
|
||||
try {
|
||||
log.info(
|
||||
`Registering identity with permit and rate limit: ${this.rateLimit} messages/epoch`
|
||||
);
|
||||
|
||||
const txRegisterResponse: ethers.ContractTransaction =
|
||||
await this.contract.registerWithPermit(
|
||||
permit.owner,
|
||||
permit.deadline,
|
||||
permit.v,
|
||||
permit.r,
|
||||
permit.s,
|
||||
identity.IDCommitmentBigInt,
|
||||
this.rateLimit,
|
||||
idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
|
||||
);
|
||||
const txRegisterReceipt = await txRegisterResponse.wait();
|
||||
|
||||
const memberRegistered = txRegisterReceipt.events?.find(
|
||||
(event) => event.event === "MembershipRegistered"
|
||||
);
|
||||
|
||||
if (!memberRegistered || !memberRegistered.args) {
|
||||
log.error(
|
||||
"Failed to register membership with permit: No MembershipRegistered event found"
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const decodedData: MembershipRegisteredEvent = {
|
||||
idCommitment: memberRegistered.args.idCommitment,
|
||||
membershipRateLimit: memberRegistered.args.membershipRateLimit,
|
||||
index: memberRegistered.args.index
|
||||
};
|
||||
|
||||
log.info(
|
||||
`Successfully registered membership with permit. Index: ${decodedData.index}, ` +
|
||||
`Rate limit: ${decodedData.membershipRateLimit}, Erased ${idCommitmentsToErase.length} commitments`
|
||||
);
|
||||
|
||||
const network = await this.contract.provider.getNetwork();
|
||||
const address = this.contract.address;
|
||||
const membershipId = ethers.BigNumber.from(decodedData.index).toNumber();
|
||||
|
||||
return {
|
||||
identity,
|
||||
membership: {
|
||||
address,
|
||||
treeIndex: membershipId,
|
||||
chainId: network.chainId,
|
||||
rateLimit: decodedData.membershipRateLimit.toNumber()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error in registerWithPermitAndErase: ${(error as Error).message}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public roots(): Uint8Array[] {
|
||||
return this.merkleRootTracker.roots();
|
||||
}
|
||||
|
||||
public async withdraw(token: string, holder: string): Promise<void> {
|
||||
try {
|
||||
const tx = await this.contract.withdraw(token, { from: holder });
|
||||
await tx.wait();
|
||||
} catch (error) {
|
||||
log.error(`Error in withdraw: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMembershipInfo(
|
||||
idCommitment: string
|
||||
): Promise<MembershipInfo | undefined> {
|
||||
try {
|
||||
const [startBlock, endBlock, rateLimit] =
|
||||
await this.contract.getMembershipInfo(idCommitment);
|
||||
const currentBlock = await this.contract.provider.getBlockNumber();
|
||||
|
||||
let state: MembershipState;
|
||||
if (currentBlock < startBlock) {
|
||||
state = MembershipState.Active;
|
||||
} else if (currentBlock < endBlock) {
|
||||
state = MembershipState.GracePeriod;
|
||||
} else {
|
||||
state = MembershipState.Expired;
|
||||
}
|
||||
|
||||
const index = await this.getMemberIndex(idCommitment);
|
||||
if (!index) return undefined;
|
||||
|
||||
return {
|
||||
index,
|
||||
idCommitment,
|
||||
rateLimit: rateLimit.toNumber(),
|
||||
startBlock: startBlock.toNumber(),
|
||||
endBlock: endBlock.toNumber(),
|
||||
state
|
||||
};
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async extendMembership(
|
||||
idCommitment: string
|
||||
): Promise<ethers.ContractTransaction> {
|
||||
return this.contract.extendMemberships([idCommitment]);
|
||||
}
|
||||
|
||||
public async eraseMembership(
|
||||
idCommitment: string,
|
||||
eraseFromMembershipSet: boolean = true
|
||||
): Promise<ethers.ContractTransaction> {
|
||||
return this.contract.eraseMemberships(
|
||||
[idCommitment],
|
||||
eraseFromMembershipSet
|
||||
);
|
||||
}
|
||||
|
||||
public async registerMembership(
|
||||
idCommitment: string,
|
||||
rateLimit: number = this.rateLimit
|
||||
): Promise<ethers.ContractTransaction> {
|
||||
this.validateRateLimit(rateLimit);
|
||||
return this.contract.register(idCommitment, rateLimit, []);
|
||||
}
|
||||
|
||||
private async getMemberIndex(
|
||||
idCommitment: string
|
||||
): Promise<ethers.BigNumber | undefined> {
|
||||
try {
|
||||
const events = await this.contract.queryFilter(
|
||||
this.contract.filters.MembershipRegistered(idCommitment)
|
||||
);
|
||||
if (events.length === 0) return undefined;
|
||||
|
||||
// Get the most recent registration event
|
||||
const event = events[events.length - 1];
|
||||
return event.args?.index;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CustomQueryOptions extends FetchMembersOptions {
|
||||
membersFilter: ethers.EventFilter;
|
||||
}
|
||||
|
||||
// These values should be tested on other networks
|
||||
const FETCH_CHUNK = 5;
|
||||
const BLOCK_RANGE = 3000;
|
||||
|
||||
async function queryFilter(
|
||||
contract: ethers.Contract,
|
||||
options: CustomQueryOptions
|
||||
): Promise<ethers.Event[]> {
|
||||
const {
|
||||
fromBlock,
|
||||
membersFilter,
|
||||
fetchRange = BLOCK_RANGE,
|
||||
fetchChunks = FETCH_CHUNK
|
||||
} = options;
|
||||
|
||||
if (fromBlock === undefined) {
|
||||
return contract.queryFilter(membersFilter);
|
||||
}
|
||||
|
||||
if (!contract.provider) {
|
||||
throw Error("No provider found on the contract.");
|
||||
}
|
||||
|
||||
const toBlock = await contract.provider.getBlockNumber();
|
||||
|
||||
if (toBlock - fromBlock < fetchRange) {
|
||||
return contract.queryFilter(membersFilter, fromBlock, toBlock);
|
||||
}
|
||||
|
||||
const events: ethers.Event[][] = [];
|
||||
const chunks = splitToChunks(fromBlock, toBlock, fetchRange);
|
||||
|
||||
for (const portion of takeN<[number, number]>(chunks, fetchChunks)) {
|
||||
const promises = portion.map(([left, right]) =>
|
||||
ignoreErrors(contract.queryFilter(membersFilter, left, right), [])
|
||||
);
|
||||
const fetchedEvents = await Promise.all(promises);
|
||||
events.push(fetchedEvents.flatMap((v) => v));
|
||||
}
|
||||
|
||||
return events.flatMap((v) => v);
|
||||
}
|
||||
|
||||
function splitToChunks(
|
||||
from: number,
|
||||
to: number,
|
||||
step: number
|
||||
): Array<[number, number]> {
|
||||
const chunks: Array<[number, number]> = [];
|
||||
|
||||
let left = from;
|
||||
while (left < to) {
|
||||
const right = left + step < to ? left + step : to;
|
||||
|
||||
chunks.push([left, right] as [number, number]);
|
||||
|
||||
left = right;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function* takeN<T>(array: T[], size: number): Iterable<T[]> {
|
||||
let start = 0;
|
||||
|
||||
while (start < array.length) {
|
||||
const portion = array.slice(start, start + size);
|
||||
|
||||
yield portion;
|
||||
|
||||
start += size;
|
||||
}
|
||||
}
|
||||
|
||||
async function ignoreErrors<T>(
|
||||
promise: Promise<T>,
|
||||
defaultValue: T
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
log.info(`Ignoring an error during query: ${err.message}`);
|
||||
} else {
|
||||
log.info(`Ignoring an unknown error during query`);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
48
packages/rln/src/contract/types.ts
Normal file
48
packages/rln/src/contract/types.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { ethers } from "ethers";
|
||||
|
||||
export interface CustomQueryOptions extends FetchMembersOptions {
|
||||
membersFilter: ethers.EventFilter;
|
||||
}
|
||||
|
||||
export type Member = {
|
||||
idCommitment: string;
|
||||
index: ethers.BigNumber;
|
||||
};
|
||||
|
||||
export interface RLNContractOptions {
|
||||
signer: ethers.Signer;
|
||||
address: string;
|
||||
rateLimit?: number;
|
||||
}
|
||||
|
||||
export interface RLNContractInitOptions extends RLNContractOptions {
|
||||
contract?: ethers.Contract;
|
||||
}
|
||||
|
||||
export interface MembershipRegisteredEvent {
|
||||
idCommitment: string;
|
||||
membershipRateLimit: ethers.BigNumber;
|
||||
index: ethers.BigNumber;
|
||||
}
|
||||
|
||||
export type FetchMembersOptions = {
|
||||
fromBlock?: number;
|
||||
fetchRange?: number;
|
||||
fetchChunks?: number;
|
||||
};
|
||||
|
||||
export interface MembershipInfo {
|
||||
index: ethers.BigNumber;
|
||||
idCommitment: string;
|
||||
rateLimit: number;
|
||||
startBlock: number;
|
||||
endBlock: number;
|
||||
state: MembershipState;
|
||||
}
|
||||
|
||||
export enum MembershipState {
|
||||
Active = "Active",
|
||||
GracePeriod = "GracePeriod",
|
||||
Expired = "Expired",
|
||||
ErasedAwaitsWithdrawal = "ErasedAwaitsWithdrawal"
|
||||
}
|
||||
@ -5,5 +5,5 @@ export async function createRLN(): Promise<RLNInstance> {
|
||||
// asynchronously. This file does the single async import, so
|
||||
// that no one else needs to worry about it again.
|
||||
const rlnModule = await import("./rln.js");
|
||||
return rlnModule.create();
|
||||
return rlnModule.RLNInstance.create();
|
||||
}
|
||||
|
||||
282
packages/rln/src/credentials_manager.ts
Normal file
282
packages/rln/src/credentials_manager.ts
Normal file
@ -0,0 +1,282 @@
|
||||
import { hmac } from "@noble/hashes/hmac";
|
||||
import { sha256 } from "@noble/hashes/sha256";
|
||||
import { Logger } from "@waku/utils";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import { LINEA_CONTRACT } from "./contract/constants.js";
|
||||
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
||||
import { IdentityCredential } from "./identity.js";
|
||||
import { Keystore } from "./keystore/index.js";
|
||||
import type {
|
||||
DecryptedCredentials,
|
||||
EncryptedCredentials
|
||||
} from "./keystore/index.js";
|
||||
import { KeystoreEntity, Password } from "./keystore/types.js";
|
||||
import { RegisterMembershipOptions, StartRLNOptions } from "./types.js";
|
||||
import {
|
||||
buildBigIntFromUint8Array,
|
||||
extractMetaMaskSigner
|
||||
} from "./utils/index.js";
|
||||
import { Zerokit } from "./zerokit.js";
|
||||
|
||||
const log = new Logger("waku:credentials");
|
||||
|
||||
/**
|
||||
* Manages credentials for RLN
|
||||
* This is a lightweight implementation of the RLN contract that doesn't require Zerokit
|
||||
* It is used to register membership and generate identity credentials
|
||||
*/
|
||||
export class RLNCredentialsManager {
|
||||
protected started = false;
|
||||
protected starting = false;
|
||||
|
||||
public contract: undefined | RLNBaseContract;
|
||||
public signer: undefined | ethers.Signer;
|
||||
|
||||
protected keystore = Keystore.create();
|
||||
public credentials: undefined | DecryptedCredentials;
|
||||
|
||||
public zerokit: undefined | Zerokit;
|
||||
|
||||
public constructor(zerokit?: Zerokit) {
|
||||
log.info("RLNCredentialsManager initialized");
|
||||
this.zerokit = zerokit;
|
||||
}
|
||||
|
||||
public get provider(): undefined | ethers.providers.Provider {
|
||||
return this.contract?.provider;
|
||||
}
|
||||
|
||||
public async start(options: StartRLNOptions = {}): Promise<void> {
|
||||
if (this.started || this.starting) {
|
||||
log.info("RLNCredentialsManager already started or starting");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Starting RLNCredentialsManager");
|
||||
this.starting = true;
|
||||
|
||||
try {
|
||||
const { credentials, keystore } =
|
||||
await RLNCredentialsManager.decryptCredentialsIfNeeded(
|
||||
options.credentials
|
||||
);
|
||||
|
||||
if (credentials) {
|
||||
log.info("Credentials successfully decrypted");
|
||||
}
|
||||
|
||||
const { signer, address, rateLimit } = await this.determineStartOptions(
|
||||
options,
|
||||
credentials
|
||||
);
|
||||
|
||||
log.info(`Using contract address: ${address}`);
|
||||
|
||||
if (keystore) {
|
||||
this.keystore = keystore;
|
||||
log.info("Using provided keystore");
|
||||
}
|
||||
|
||||
this.credentials = credentials;
|
||||
this.signer = signer!;
|
||||
this.contract = new RLNBaseContract({
|
||||
address: address!,
|
||||
signer: signer!,
|
||||
rateLimit: rateLimit ?? this.zerokit?.rateLimit
|
||||
});
|
||||
|
||||
log.info("RLNCredentialsManager successfully started");
|
||||
this.started = true;
|
||||
} catch (error) {
|
||||
log.error("Failed to start RLNCredentialsManager", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.starting = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async registerMembership(
|
||||
options: RegisterMembershipOptions
|
||||
): Promise<undefined | DecryptedCredentials> {
|
||||
if (!this.contract) {
|
||||
log.error("RLN Contract is not initialized");
|
||||
throw Error("RLN Contract is not initialized.");
|
||||
}
|
||||
|
||||
log.info("Registering membership");
|
||||
let identity = "identity" in options && options.identity;
|
||||
|
||||
if ("signature" in options) {
|
||||
log.info("Generating identity from signature");
|
||||
if (this.zerokit) {
|
||||
log.info("Using Zerokit to generate identity");
|
||||
identity = this.zerokit.generateSeededIdentityCredential(
|
||||
options.signature
|
||||
);
|
||||
} else {
|
||||
log.info("Using local implementation to generate identity");
|
||||
identity = this.generateSeededIdentityCredential(options.signature);
|
||||
}
|
||||
}
|
||||
|
||||
if (!identity) {
|
||||
log.error("Missing signature or identity to register membership");
|
||||
throw Error("Missing signature or identity to register membership.");
|
||||
}
|
||||
|
||||
log.info("Registering identity with contract");
|
||||
return this.contract.registerWithIdentity(identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes credentials in use by relying on provided Keystore earlier in rln.start
|
||||
* @param id: string, hash of credentials to select from Keystore
|
||||
* @param password: string or bytes to use to decrypt credentials from Keystore
|
||||
*/
|
||||
public async useCredentials(id: string, password: Password): Promise<void> {
|
||||
log.info(`Attempting to use credentials with ID: ${id}`);
|
||||
this.credentials = await this.keystore?.readCredential(id, password);
|
||||
if (this.credentials) {
|
||||
log.info("Successfully loaded credentials");
|
||||
} else {
|
||||
log.warn("Failed to load credentials");
|
||||
}
|
||||
}
|
||||
|
||||
protected async determineStartOptions(
|
||||
options: StartRLNOptions,
|
||||
credentials: KeystoreEntity | undefined
|
||||
): Promise<StartRLNOptions> {
|
||||
let chainId = credentials?.membership.chainId;
|
||||
const address =
|
||||
credentials?.membership.address ||
|
||||
options.address ||
|
||||
LINEA_CONTRACT.address;
|
||||
|
||||
if (address === LINEA_CONTRACT.address) {
|
||||
chainId = LINEA_CONTRACT.chainId;
|
||||
log.info(`Using Linea contract with chainId: ${chainId}`);
|
||||
}
|
||||
|
||||
const signer = options.signer || (await extractMetaMaskSigner());
|
||||
const currentChainId = await signer.getChainId();
|
||||
log.info(`Current chain ID: ${currentChainId}`);
|
||||
|
||||
if (chainId && chainId !== currentChainId) {
|
||||
log.error(
|
||||
`Chain ID mismatch: contract=${chainId}, current=${currentChainId}`
|
||||
);
|
||||
throw Error(
|
||||
`Failed to start RLN contract, chain ID of contract is different from current one: contract-${chainId}, current network-${currentChainId}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
signer,
|
||||
address
|
||||
};
|
||||
}
|
||||
|
||||
protected static async decryptCredentialsIfNeeded(
|
||||
credentials?: EncryptedCredentials | DecryptedCredentials
|
||||
): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> {
|
||||
if (!credentials) {
|
||||
log.info("No credentials provided");
|
||||
return {};
|
||||
}
|
||||
|
||||
if ("identity" in credentials) {
|
||||
log.info("Using already decrypted credentials");
|
||||
return { credentials };
|
||||
}
|
||||
|
||||
log.info("Attempting to decrypt credentials");
|
||||
const keystore = Keystore.fromString(credentials.keystore);
|
||||
|
||||
if (!keystore) {
|
||||
log.warn("Failed to create keystore from string");
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedCredentials = await keystore.readCredential(
|
||||
credentials.id,
|
||||
credentials.password
|
||||
);
|
||||
log.info(`Successfully decrypted credentials with ID: ${credentials.id}`);
|
||||
|
||||
return {
|
||||
keystore,
|
||||
credentials: decryptedCredentials
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("Failed to decrypt credentials", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected async verifyCredentialsAgainstContract(
|
||||
credentials: KeystoreEntity
|
||||
): Promise<void> {
|
||||
if (!this.contract) {
|
||||
throw Error(
|
||||
"Failed to verify chain coordinates: no contract initialized."
|
||||
);
|
||||
}
|
||||
|
||||
const registryAddress = credentials.membership.address;
|
||||
const currentRegistryAddress = this.contract.address;
|
||||
if (registryAddress !== currentRegistryAddress) {
|
||||
throw Error(
|
||||
`Failed to verify chain coordinates: credentials contract address=${registryAddress} is not equal to registryContract address=${currentRegistryAddress}`
|
||||
);
|
||||
}
|
||||
|
||||
const chainId = credentials.membership.chainId;
|
||||
const network = await this.contract.provider.getNetwork();
|
||||
const currentChainId = network.chainId;
|
||||
if (chainId !== currentChainId) {
|
||||
throw Error(
|
||||
`Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an identity credential from a seed string
|
||||
* This is a pure implementation that doesn't rely on Zerokit
|
||||
* @param seed A string seed to generate the identity from
|
||||
* @returns IdentityCredential
|
||||
*/
|
||||
private generateSeededIdentityCredential(seed: string): IdentityCredential {
|
||||
log.info("Generating seeded identity credential");
|
||||
// Convert the seed to bytes
|
||||
const encoder = new TextEncoder();
|
||||
const seedBytes = encoder.encode(seed);
|
||||
|
||||
// Generate deterministic values using HMAC-SHA256
|
||||
// We use different context strings for each component to ensure they're different
|
||||
const idTrapdoor = hmac(sha256, seedBytes, encoder.encode("IDTrapdoor"));
|
||||
const idNullifier = hmac(sha256, seedBytes, encoder.encode("IDNullifier"));
|
||||
|
||||
// Generate IDSecretHash as a hash of IDTrapdoor and IDNullifier
|
||||
const combinedBytes = new Uint8Array([...idTrapdoor, ...idNullifier]);
|
||||
const idSecretHash = sha256(combinedBytes);
|
||||
|
||||
// Generate IDCommitment as a hash of IDSecretHash
|
||||
const idCommitment = sha256(idSecretHash);
|
||||
|
||||
// Convert IDCommitment to BigInt
|
||||
const idCommitmentBigInt = buildBigIntFromUint8Array(idCommitment);
|
||||
|
||||
log.info("Successfully generated identity credential");
|
||||
return new IdentityCredential(
|
||||
idTrapdoor,
|
||||
idNullifier,
|
||||
idSecretHash,
|
||||
idCommitment,
|
||||
idCommitmentBigInt
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
import { RLNDecoder, RLNEncoder } from "./codec.js";
|
||||
import { RLN_ABI } from "./contract/abi.js";
|
||||
import { LINEA_CONTRACT, RLNContract } from "./contract/index.js";
|
||||
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
||||
import { createRLN } from "./create.js";
|
||||
import { RLNCredentialsManager } from "./credentials_manager.js";
|
||||
import { IdentityCredential } from "./identity.js";
|
||||
import { Keystore } from "./keystore/index.js";
|
||||
import { Proof } from "./proof.js";
|
||||
@ -10,6 +12,8 @@ import { MerkleRootTracker } from "./root_tracker.js";
|
||||
import { extractMetaMaskSigner } from "./utils/index.js";
|
||||
|
||||
export {
|
||||
RLNCredentialsManager,
|
||||
RLNBaseContract,
|
||||
createRLN,
|
||||
Keystore,
|
||||
RLNInstance,
|
||||
|
||||
@ -7,7 +7,6 @@ import type {
|
||||
import { Logger } from "@waku/utils";
|
||||
import init from "@waku/zerokit-rln-wasm";
|
||||
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import {
|
||||
createRLNDecoder,
|
||||
@ -16,258 +15,52 @@ import {
|
||||
type RLNEncoder
|
||||
} from "./codec.js";
|
||||
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
|
||||
import { LINEA_CONTRACT, RLNContract } from "./contract/index.js";
|
||||
import { IdentityCredential } from "./identity.js";
|
||||
import { Keystore } from "./keystore/index.js";
|
||||
import { RLNCredentialsManager } from "./credentials_manager.js";
|
||||
import type {
|
||||
DecryptedCredentials,
|
||||
EncryptedCredentials
|
||||
} from "./keystore/index.js";
|
||||
import { KeystoreEntity, Password } from "./keystore/types.js";
|
||||
import verificationKey from "./resources/verification_key";
|
||||
import * as wc from "./resources/witness_calculator";
|
||||
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||
import { extractMetaMaskSigner } from "./utils/index.js";
|
||||
import { Zerokit } from "./zerokit.js";
|
||||
|
||||
const log = new Logger("waku:rln");
|
||||
|
||||
async function loadWitnessCalculator(): Promise<WitnessCalculator> {
|
||||
try {
|
||||
const url = new URL("./resources/rln.wasm", import.meta.url);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch witness calculator: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return await wc.builder(
|
||||
new Uint8Array(await response.arrayBuffer()),
|
||||
false
|
||||
);
|
||||
} catch (error) {
|
||||
log.error("Error loading witness calculator:", error);
|
||||
throw new Error(
|
||||
`Failed to load witness calculator: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadZkey(): Promise<Uint8Array> {
|
||||
try {
|
||||
const url = new URL("./resources/rln_final.zkey", import.meta.url);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch zkey: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return new Uint8Array(await response.arrayBuffer());
|
||||
} catch (error) {
|
||||
log.error("Error loading zkey:", error);
|
||||
throw new Error(
|
||||
`Failed to load zkey: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of RLN
|
||||
* @returns RLNInstance
|
||||
*/
|
||||
export async function create(): Promise<RLNInstance> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await (init as any)?.();
|
||||
zerokitRLN.init_panic_hook();
|
||||
|
||||
const witnessCalculator = await loadWitnessCalculator();
|
||||
const zkey = await loadZkey();
|
||||
|
||||
const stringEncoder = new TextEncoder();
|
||||
const vkey = stringEncoder.encode(JSON.stringify(verificationKey));
|
||||
|
||||
const DEPTH = 20;
|
||||
const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey);
|
||||
const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT);
|
||||
|
||||
return new RLNInstance(zerokit);
|
||||
} catch (error) {
|
||||
log.error("Failed to initialize RLN:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
type StartRLNOptions = {
|
||||
/**
|
||||
* If not set - will extract MetaMask account and get signer from it.
|
||||
*/
|
||||
signer?: ethers.Signer;
|
||||
/**
|
||||
* If not set - will use default LINEA_CONTRACT address.
|
||||
*/
|
||||
address?: 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?: EncryptedCredentials | DecryptedCredentials;
|
||||
/**
|
||||
* Rate limit for the member.
|
||||
*/
|
||||
rateLimit?: number;
|
||||
};
|
||||
|
||||
type RegisterMembershipOptions =
|
||||
| { signature: string }
|
||||
| { identity: IdentityCredential };
|
||||
|
||||
type WakuRLNEncoderOptions = WakuEncoderOptions & {
|
||||
credentials: EncryptedCredentials | DecryptedCredentials;
|
||||
};
|
||||
|
||||
export class RLNInstance {
|
||||
private started = false;
|
||||
private starting = false;
|
||||
|
||||
private _contract: undefined | RLNContract;
|
||||
private _signer: undefined | ethers.Signer;
|
||||
|
||||
private keystore = Keystore.create();
|
||||
private _credentials: undefined | DecryptedCredentials;
|
||||
|
||||
public constructor(public zerokit: Zerokit) {}
|
||||
|
||||
public get contract(): undefined | RLNContract {
|
||||
return this._contract;
|
||||
}
|
||||
|
||||
public get signer(): undefined | ethers.Signer {
|
||||
return this._signer;
|
||||
}
|
||||
|
||||
public async start(options: StartRLNOptions = {}): Promise<void> {
|
||||
if (this.started || this.starting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.starting = true;
|
||||
|
||||
try {
|
||||
const { credentials, keystore } =
|
||||
await RLNInstance.decryptCredentialsIfNeeded(options.credentials);
|
||||
const { signer, address } = await this.determineStartOptions(
|
||||
options,
|
||||
credentials
|
||||
);
|
||||
|
||||
if (keystore) {
|
||||
this.keystore = keystore;
|
||||
}
|
||||
|
||||
this._credentials = credentials;
|
||||
this._signer = signer!;
|
||||
this._contract = await RLNContract.init(this, {
|
||||
address: address!,
|
||||
signer: signer!,
|
||||
rateLimit: options.rateLimit ?? this.zerokit.getRateLimit
|
||||
});
|
||||
this.started = true;
|
||||
} finally {
|
||||
this.starting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async determineStartOptions(
|
||||
options: StartRLNOptions,
|
||||
credentials: KeystoreEntity | undefined
|
||||
): Promise<StartRLNOptions> {
|
||||
let chainId = credentials?.membership.chainId;
|
||||
const address =
|
||||
credentials?.membership.address ||
|
||||
options.address ||
|
||||
LINEA_CONTRACT.address;
|
||||
|
||||
if (address === LINEA_CONTRACT.address) {
|
||||
chainId = LINEA_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,
|
||||
address
|
||||
};
|
||||
}
|
||||
|
||||
private static async decryptCredentialsIfNeeded(
|
||||
credentials?: EncryptedCredentials | DecryptedCredentials
|
||||
): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> {
|
||||
if (!credentials) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if ("identity" in credentials) {
|
||||
return { credentials };
|
||||
}
|
||||
|
||||
const keystore = Keystore.fromString(credentials.keystore);
|
||||
|
||||
if (!keystore) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const decryptedCredentials = await keystore.readCredential(
|
||||
credentials.id,
|
||||
credentials.password
|
||||
);
|
||||
|
||||
return {
|
||||
keystore,
|
||||
credentials: decryptedCredentials
|
||||
};
|
||||
}
|
||||
|
||||
public async registerMembership(
|
||||
options: RegisterMembershipOptions
|
||||
): Promise<undefined | DecryptedCredentials> {
|
||||
if (!this.contract) {
|
||||
throw Error("RLN Contract is not initialized.");
|
||||
}
|
||||
|
||||
let identity = "identity" in options && options.identity;
|
||||
|
||||
if ("signature" in options) {
|
||||
identity = this.zerokit.generateSeededIdentityCredential(
|
||||
options.signature
|
||||
);
|
||||
}
|
||||
|
||||
if (!identity) {
|
||||
throw Error("Missing signature or identity to register membership.");
|
||||
}
|
||||
|
||||
return this.contract.registerWithIdentity(identity);
|
||||
}
|
||||
|
||||
export class RLNInstance extends RLNCredentialsManager {
|
||||
/**
|
||||
* Changes credentials in use by relying on provided Keystore earlier in rln.start
|
||||
* @param id: string, hash of credentials to select from Keystore
|
||||
* @param password: string or bytes to use to decrypt credentials from Keystore
|
||||
* Create an instance of RLN
|
||||
* @returns RLNInstance
|
||||
*/
|
||||
public async useCredentials(id: string, password: Password): Promise<void> {
|
||||
this._credentials = await this.keystore?.readCredential(id, password);
|
||||
public static async create(): Promise<RLNInstance> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await (init as any)?.();
|
||||
zerokitRLN.init_panic_hook();
|
||||
|
||||
const witnessCalculator = await RLNInstance.loadWitnessCalculator();
|
||||
const zkey = await RLNInstance.loadZkey();
|
||||
|
||||
const stringEncoder = new TextEncoder();
|
||||
const vkey = stringEncoder.encode(JSON.stringify(verificationKey));
|
||||
|
||||
const DEPTH = 20;
|
||||
const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey);
|
||||
const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT);
|
||||
|
||||
return new RLNInstance(zerokit);
|
||||
} catch (error) {
|
||||
log.error("Failed to initialize RLN:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(public zerokit: Zerokit) {
|
||||
super(zerokit);
|
||||
}
|
||||
|
||||
public async createEncoder(
|
||||
@ -275,7 +68,7 @@ export class RLNInstance {
|
||||
): Promise<RLNEncoder> {
|
||||
const { credentials: decryptedCredentials } =
|
||||
await RLNInstance.decryptCredentialsIfNeeded(options.credentials);
|
||||
const credentials = decryptedCredentials || this._credentials;
|
||||
const credentials = decryptedCredentials || this.credentials;
|
||||
|
||||
if (!credentials) {
|
||||
throw Error(
|
||||
@ -293,33 +86,6 @@ export class RLNInstance {
|
||||
});
|
||||
}
|
||||
|
||||
private async verifyCredentialsAgainstContract(
|
||||
credentials: KeystoreEntity
|
||||
): Promise<void> {
|
||||
if (!this._contract) {
|
||||
throw Error(
|
||||
"Failed to verify chain coordinates: no contract initialized."
|
||||
);
|
||||
}
|
||||
|
||||
const registryAddress = credentials.membership.address;
|
||||
const currentRegistryAddress = this._contract.address;
|
||||
if (registryAddress !== currentRegistryAddress) {
|
||||
throw Error(
|
||||
`Failed to verify chain coordinates: credentials contract address=${registryAddress} is not equal to registryContract address=${currentRegistryAddress}`
|
||||
);
|
||||
}
|
||||
|
||||
const chainId = credentials.membership.chainId;
|
||||
const network = await this._contract.provider.getNetwork();
|
||||
const currentChainId = network.chainId;
|
||||
if (chainId !== currentChainId) {
|
||||
throw Error(
|
||||
`Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public createDecoder(
|
||||
contentTopic: ContentTopic
|
||||
): RLNDecoder<IDecodedMessage> {
|
||||
@ -328,4 +94,47 @@ export class RLNInstance {
|
||||
decoder: createDecoder(contentTopic)
|
||||
});
|
||||
}
|
||||
|
||||
public static async loadWitnessCalculator(): Promise<WitnessCalculator> {
|
||||
try {
|
||||
const url = new URL("./resources/rln.wasm", import.meta.url);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch witness calculator: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return await wc.builder(
|
||||
new Uint8Array(await response.arrayBuffer()),
|
||||
false
|
||||
);
|
||||
} catch (error) {
|
||||
log.error("Error loading witness calculator:", error);
|
||||
throw new Error(
|
||||
`Failed to load witness calculator: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async loadZkey(): Promise<Uint8Array> {
|
||||
try {
|
||||
const url = new URL("./resources/rln_final.zkey", import.meta.url);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch zkey: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return new Uint8Array(await response.arrayBuffer());
|
||||
} catch (error) {
|
||||
log.error("Error loading zkey:", error);
|
||||
throw new Error(
|
||||
`Failed to load zkey: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
packages/rln/src/types.ts
Normal file
31
packages/rln/src/types.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import { IdentityCredential } from "./identity.js";
|
||||
import {
|
||||
DecryptedCredentials,
|
||||
EncryptedCredentials
|
||||
} from "./keystore/types.js";
|
||||
|
||||
export type StartRLNOptions = {
|
||||
/**
|
||||
* If not set - will extract MetaMask account and get signer from it.
|
||||
*/
|
||||
signer?: ethers.Signer;
|
||||
/**
|
||||
* If not set - will use default SEPOLIA_CONTRACT address.
|
||||
*/
|
||||
address?: 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?: EncryptedCredentials | DecryptedCredentials;
|
||||
/**
|
||||
* Rate limit for the member.
|
||||
*/
|
||||
rateLimit?: number;
|
||||
};
|
||||
|
||||
export type RegisterMembershipOptions =
|
||||
| { signature: string }
|
||||
| { identity: IdentityCredential };
|
||||
@ -16,7 +16,7 @@ export class Zerokit {
|
||||
public constructor(
|
||||
private readonly zkRLN: number,
|
||||
private readonly witnessCalculator: WitnessCalculator,
|
||||
private readonly rateLimit: number = DEFAULT_RATE_LIMIT
|
||||
private readonly _rateLimit: number = DEFAULT_RATE_LIMIT
|
||||
) {}
|
||||
|
||||
public get getZkRLN(): number {
|
||||
@ -27,8 +27,8 @@ export class Zerokit {
|
||||
return this.witnessCalculator;
|
||||
}
|
||||
|
||||
public get getRateLimit(): number {
|
||||
return this.rateLimit;
|
||||
public get rateLimit(): number {
|
||||
return this._rateLimit;
|
||||
}
|
||||
|
||||
public generateIdentityCredentials(): IdentityCredential {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user