From 1ca5b7e99ace63e1ec643c0742c9f290f183953a Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Mon, 27 Jan 2025 18:42:28 +0530 Subject: [PATCH] feat: upgrade contract adapter to the RLN v2 contract --- package-lock.json | 252 ++++++++++++++++++++++++- package.json | 2 + src/contract/rln_contract.spec.ts | 208 +++++++++++++++----- src/contract/rln_contract.ts | 304 +++++++++++++++++++++--------- src/rln.ts | 3 +- 5 files changed, 628 insertions(+), 141 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33f6173..b7e7847 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@waku/rln", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@waku/rln", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT OR Apache-2.0", "dependencies": { "@chainsafe/bls-keystore": "^3.0.0", @@ -35,6 +35,7 @@ "@types/lodash": "^4.14.199", "@types/mocha": "^9.1.0", "@types/node": "^17.0.6", + "@types/sinon": "^17.0.3", "@types/tail": "^2.0.0", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^6.6.0", @@ -78,6 +79,7 @@ "process": "^0.11.10", "rollup": "^2.75.0", "rollup-plugin-copy": "^3.4.0", + "sinon": "^19.0.2", "size-limit": "^8.0.0", "tail": "^2.2.0", "ts-loader": "^9.3.1", @@ -2191,6 +2193,55 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@sitespeed.io/tracium": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@sitespeed.io/tracium/-/tracium-0.3.3.tgz", @@ -2487,6 +2538,23 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/tail": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@types/tail/-/tail-2.2.3.tgz", @@ -7768,6 +7836,13 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, "node_modules/karma": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", @@ -7811,6 +7886,7 @@ "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", "dev": true, + "license": "MIT", "dependencies": { "which": "^1.2.1" } @@ -8162,6 +8238,14 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.mapvalues": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", @@ -8782,6 +8866,20 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -9462,6 +9560,16 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10569,6 +10677,35 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/size-limit": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/size-limit/-/size-limit-8.2.6.tgz", @@ -13979,6 +14116,49 @@ "@scure/base": "~1.1.4" } }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, + "@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + }, + "dependencies": { + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, "@sitespeed.io/tracium": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@sitespeed.io/tracium/-/tracium-0.3.3.tgz", @@ -14248,6 +14428,21 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "@types/tail": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@types/tail/-/tail-2.2.3.tgz", @@ -18202,6 +18397,12 @@ "universalify": "^2.0.0" } }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "karma": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", @@ -18527,6 +18728,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "lodash.mapvalues": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", @@ -18981,6 +19188,19 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -19473,6 +19693,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -20269,6 +20495,28 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true + } + } + }, "size-limit": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/size-limit/-/size-limit-8.2.6.tgz", diff --git a/package.json b/package.json index b945257..3fce2b4 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/lodash": "^4.14.199", "@types/mocha": "^9.1.0", "@types/node": "^17.0.6", + "@types/sinon": "^17.0.3", "@types/tail": "^2.0.0", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^6.6.0", @@ -109,6 +110,7 @@ "process": "^0.11.10", "rollup": "^2.75.0", "rollup-plugin-copy": "^3.4.0", + "sinon": "^19.0.2", "size-limit": "^8.0.0", "tail": "^2.2.0", "ts-loader": "^9.3.1", diff --git a/src/contract/rln_contract.spec.ts b/src/contract/rln_contract.spec.ts index 4b6e1d5..8790b63 100644 --- a/src/contract/rln_contract.spec.ts +++ b/src/contract/rln_contract.spec.ts @@ -1,38 +1,101 @@ +import { hexToBytes } from "@waku/utils/bytes"; import chai from "chai"; import spies from "chai-spies"; import * as ethers from "ethers"; +import sinon, { SinonSandbox } from "sinon"; import { createRLN } from "../create.js"; +import type { IdentityCredential } from "../identity.js"; import { SEPOLIA_CONTRACT } from "./constants.js"; import { RLNContract } from "./rln_contract.js"; chai.use(spies); -describe("RLN Contract abstraction", () => { - it("should be able to fetch members from events and store to rln instance", async () => { +const DEFAULT_RATE_LIMIT = 10; + +function mockRLNv2RegisteredEvent(idCommitment?: string): ethers.Event { + return { + args: { + idCommitment: + idCommitment || + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + rateLimit: DEFAULT_RATE_LIMIT, + index: ethers.BigNumber.from(1) + }, + event: "MembershipRegistered" + } as unknown as ethers.Event; +} + +describe("RLN Contract abstraction - RLN v2", () => { + let sandbox: SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should fetch members from events and store them in the RLN instance", async () => { const rlnInstance = await createRLN(); rlnInstance.zerokit.insertMember = () => undefined; const insertMemberSpy = chai.spy.on(rlnInstance.zerokit, "insertMember"); - const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address); - const rlnContract = new RLNContract(rlnInstance, { + const membershipRegisteredEvent = mockRLNv2RegisteredEvent(); + + const mockedRegistryContract = { + queryFilter: () => [membershipRegisteredEvent], + provider: { + getLogs: () => [], + getBlockNumber: () => Promise.resolve(1000) + }, + interface: { + getEvent: (eventName: string) => ({ + name: eventName, + format: () => {} + }) + }, + filters: { + MembershipRegistered: () => ({}), + MembershipRemoved: () => ({}) + }, + on: () => ({}), + removeAllListeners: () => ({}) + }; + + const queryFilterSpy = chai.spy.on(mockedRegistryContract, "queryFilter"); + + const provider = new ethers.providers.JsonRpcProvider(); + const voidSigner = new ethers.VoidSigner( + SEPOLIA_CONTRACT.address, + provider + ); + const rlnContract = await RLNContract.init(rlnInstance, { registryAddress: SEPOLIA_CONTRACT.address, - signer: voidSigner + signer: voidSigner, + storageIndex: 0, + registryContract: mockedRegistryContract as unknown as ethers.Contract }); - rlnContract["storageContract"] = { - queryFilter: () => Promise.resolve([mockEvent()]) - } as unknown as ethers.Contract; - rlnContract["_membersFilter"] = { - address: "", - topics: [] - } as unknown as ethers.EventFilter; + await rlnContract.fetchMembers(rlnInstance, { + fromBlock: 0, + fetchRange: 1000, + fetchChunks: 2 + }); - await rlnContract.fetchMembers(rlnInstance); + chai + .expect(insertMemberSpy) + .to.have.been.called.with( + ethers.utils.zeroPad( + hexToBytes(membershipRegisteredEvent.args!.idCommitment), + 32 + ) + ); - chai.expect(insertMemberSpy).to.have.been.called(); + chai.expect(queryFilterSpy).to.have.been.called; }); it("should register a member", async () => { @@ -40,39 +103,96 @@ describe("RLN Contract abstraction", () => { "0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c"; const rlnInstance = await createRLN(); - const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address); - const rlnContract = new RLNContract(rlnInstance, { - registryAddress: SEPOLIA_CONTRACT.address, - signer: voidSigner - }); + const identity: IdentityCredential = + rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature); - rlnContract["storageIndex"] = 1; - rlnContract["_membersFilter"] = { - address: "", - topics: [] - } as unknown as ethers.EventFilter; - rlnContract["registryContract"] = { - "register(uint16,uint256)": () => - Promise.resolve({ wait: () => Promise.resolve(undefined) }) - } as unknown as ethers.Contract; - const contractSpy = chai.spy.on( - rlnContract["registryContract"], - "register(uint16,uint256)" + rlnInstance.zerokit.insertMember = () => undefined; + const insertMemberSpy = chai.spy.on(rlnInstance.zerokit, "insertMember"); + + const formatIdCommitment = (idCommitmentBigInt: bigint): string => + "0x" + idCommitmentBigInt.toString(16).padStart(64, "0"); + + const membershipRegisteredEvent = mockRLNv2RegisteredEvent( + formatIdCommitment(identity.IDCommitmentBigInt) ); - const identity = - rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature); - await rlnContract.registerWithIdentity(identity); + const mockedRegistryContract = { + register: () => ({ + wait: () => + Promise.resolve({ + events: [ + { + event: "MembershipRegistered", + args: { + idCommitment: formatIdCommitment(identity.IDCommitmentBigInt), + rateLimit: 0, + index: ethers.BigNumber.from(1) + } + } + ] + }) + }), + queryFilter: () => [membershipRegisteredEvent], + provider: { + getLogs: () => [], + getBlockNumber: () => Promise.resolve(1000), + getNetwork: () => Promise.resolve({ chainId: 11155111 }) + }, + address: SEPOLIA_CONTRACT.address, + interface: { + getEvent: (eventName: string) => ({ + name: eventName, + format: () => {} + }) + }, + filters: { + MembershipRegistered: () => ({}), + MembershipRemoved: () => ({}) + }, + on: () => ({}), + removeAllListeners: () => ({}) + }; - chai.expect(contractSpy).to.have.been.called(); + const provider = new ethers.providers.JsonRpcProvider(); + const voidSigner = new ethers.VoidSigner( + SEPOLIA_CONTRACT.address, + provider + ); + const rlnContract = await RLNContract.init(rlnInstance, { + registryAddress: SEPOLIA_CONTRACT.address, + signer: voidSigner, + storageIndex: 0, + registryContract: mockedRegistryContract as unknown as ethers.Contract + }); + + const registerSpy = chai.spy.on(mockedRegistryContract, "register"); + + const decryptedCredentials = + await rlnContract.registerWithIdentity(identity); + + chai.expect(decryptedCredentials).to.not.be.undefined; + if (!decryptedCredentials) + throw new Error("Decrypted credentials should not be undefined"); + + chai + .expect(registerSpy) + .to.have.been.called.with(identity.IDCommitmentBigInt, 0, [], { + gasLimit: 300000 + }); + + chai.expect(decryptedCredentials).to.have.property("identity"); + chai.expect(decryptedCredentials).to.have.property("membership"); + chai.expect(decryptedCredentials.membership).to.include({ + address: SEPOLIA_CONTRACT.address, + treeIndex: 1 + }); + + const expectedIdCommitment = ethers.utils.zeroPad( + hexToBytes(formatIdCommitment(identity.IDCommitmentBigInt)), + 32 + ); + const insertMemberCalls = (insertMemberSpy as any).__spy.calls; + chai.expect(insertMemberCalls).to.have.length(2); + chai.expect(insertMemberCalls[1][0]).to.deep.equal(expectedIdCommitment); }); }); - -function mockEvent(): ethers.Event { - return { - args: { - idCommitment: { _hex: "0xb3df1c4e5600ef2b" }, - index: ethers.BigNumber.from(1) - } - } as unknown as ethers.Event; -} diff --git a/src/contract/rln_contract.ts b/src/contract/rln_contract.ts index 335a280..6160418 100644 --- a/src/contract/rln_contract.ts +++ b/src/contract/rln_contract.ts @@ -12,41 +12,76 @@ import { RLN_V2_ABI } from "./abis/rlnv2.js"; const log = debug("waku:rln:contract"); -type Member = { +interface MembershipRegisteredEvent { + idCommitment: string; + rateLimit: number; + index: ethers.BigNumber; +} + +interface Member { idCommitment: string; index: ethers.BigNumber; -}; +} -type Signer = ethers.Signer; - -type RLNContractOptions = { - signer: Signer; +interface RLNContractOptions { + signer: ethers.Signer; registryAddress: string; -}; + storageIndex: number; +} -type RLNStorageOptions = { - storageIndex?: number; -}; - -type RLNContractInitOptions = RLNContractOptions & RLNStorageOptions; - -type FetchMembersOptions = { +interface FetchMembersOptions { fromBlock?: number; fetchRange?: number; fetchChunks?: number; -}; +} + +interface RLNContractInitOptions extends RLNContractOptions { + registryContract?: ethers.Contract; +} export class RLNContract { - private registryContract: ethers.Contract; + private contract: ethers.Contract; private merkleRootTracker: MerkleRootTracker; private deployBlock: undefined | number; - private storageIndex: undefined | number; - private storageContract: undefined | ethers.Contract; - private _membersFilter: undefined | ethers.EventFilter; + private storageIndex: number; + private _membersFilter: ethers.EventFilter; + private _membersRemovedFilter: ethers.EventFilter; private _members: Map = new Map(); + /** + * Private constructor to enforce the use of the async init method. + */ + private constructor( + rlnInstance: RLNInstance, + options: RLNContractInitOptions + ) { + const { registryAddress, signer, storageIndex, registryContract } = options; + + if (storageIndex === undefined) { + throw new Error("storageIndex must be provided in RLNContractOptions."); + } + + this.storageIndex = storageIndex; + + const initialRoot = rlnInstance.zerokit.getMerkleRoot(); + + // Use the injected registryContract if provided; otherwise, instantiate a new one. + this.contract = + registryContract || + new ethers.Contract(registryAddress, RLN_V2_ABI, signer); + this.merkleRootTracker = new MerkleRootTracker(5, initialRoot); + + // Initialize event filters for MembershipRegistered and MembershipRemoved + this._membersFilter = this.contract.filters.MembershipRegistered(); + this._membersRemovedFilter = this.contract.filters.MembershipRemoved(); + } + + /** + * Asynchronous initializer for RLNContract. + * Allows injecting a mocked registryContract for testing purposes. + */ public static async init( rlnInstance: RLNInstance, options: RLNContractInitOptions @@ -59,32 +94,8 @@ export class RLNContract { return rlnContract; } - constructor( - rlnInstance: RLNInstance, - { registryAddress, signer }: RLNContractOptions - ) { - const initialRoot = rlnInstance.zerokit.getMerkleRoot(); - - this.registryContract = new ethers.Contract( - registryAddress, - RLN_V2_ABI, - signer - ); - this.merkleRootTracker = new MerkleRootTracker(5, initialRoot); - } - public get registry(): ethers.Contract { - if (!this.registryContract) { - throw Error("Registry contract was not initialized"); - } - return this.registryContract as ethers.Contract; - } - - public get contract(): ethers.Contract { - if (!this.storageContract) { - throw Error("Storage contract was not initialized"); - } - return this.storageContract as ethers.Contract; + return this.contract; } public get members(): Member[] { @@ -98,7 +109,14 @@ export class RLNContract { if (!this._membersFilter) { throw Error("Members filter was not initialized."); } - return this._membersFilter as ethers.EventFilter; + return this._membersFilter; + } + + private get membersRemovedFilter(): ethers.EventFilter { + if (!this._membersRemovedFilter) { + throw Error("MembersRemoved filter was not initialized."); + } + return this._membersRemovedFilter; } public async fetchMembers( @@ -110,7 +128,15 @@ export class RLNContract { ...options, membersFilter: this.membersFilter }); - this.processEvents(rlnInstance, registeredMemberEvents); + + const removedMemberEvents = await queryFilter(this.contract, { + fromBlock: this.deployBlock, + ...options, + membersFilter: this.membersRemovedFilter + }); + + const events = [...registeredMemberEvents, ...removedMemberEvents]; + this.processEvents(rlnInstance, events); } public processEvents(rlnInstance: RLNInstance, events: ethers.Event[]): void { @@ -122,8 +148,8 @@ export class RLNContract { return; } - if (evt.removed) { - const index: ethers.BigNumber = evt.args.index; + if (evt.event === "MembershipRemoved") { + const index = evt.args.index as ethers.BigNumber; const toRemoveVal = toRemoveTable.get(evt.blockNumber); if (toRemoveVal != undefined) { toRemoveVal.push(index.toNumber()); @@ -131,7 +157,7 @@ export class RLNContract { } else { toRemoveTable.set(evt.blockNumber, [index.toNumber()]); } - } else { + } else if (evt.event === "MembershipRegistered") { let eventsPerBlock = toInsertTable.get(evt.blockNumber); if (eventsPerBlock == undefined) { eventsPerBlock = []; @@ -152,18 +178,20 @@ export class RLNContract { ): void { toInsert.forEach((events: ethers.Event[], blockNumber: number) => { events.forEach((evt) => { - const _idCommitment = evt?.args?.idCommitment; - const index: ethers.BigNumber = evt?.args?.index; + if (!evt.args) return; + + const _idCommitment = evt.args.idCommitment as string; + const index = evt.args.index as ethers.BigNumber; if (!_idCommitment || !index) { return; } - const idCommitment = zeroPadLE(hexToBytes(_idCommitment?._hex), 32); + const idCommitment = zeroPadLE(hexToBytes(_idCommitment), 32); rlnInstance.zerokit.insertMember(idCommitment); this._members.set(index.toNumber(), { index, - idCommitment: _idCommitment?._hex + idCommitment: _idCommitment }); }); @@ -176,7 +204,7 @@ export class RLNContract { rlnInstance: RLNInstance, toRemove: Map ): void { - const removeDescending = new Map([...toRemove].sort().reverse()); + const removeDescending = new Map([...toRemove].sort((a, b) => b[0] - a[0])); removeDescending.forEach((indexes: number[], blockNumber: number) => { indexes.forEach((index) => { if (this._members.has(index)) { @@ -190,63 +218,151 @@ export class RLNContract { } public subscribeToMembers(rlnInstance: RLNInstance): void { - this.contract.on(this.membersFilter, (_pubkey, _index, event) => - this.processEvents(rlnInstance, [event]) + this.contract.on( + this.membersFilter, + ( + _idCommitment: string, + _rateLimit: number, + _index: ethers.BigNumber, + event: ethers.Event + ) => { + this.processEvents(rlnInstance, [event]); + } + ); + + this.contract.on( + this.membersRemovedFilter, + ( + _idCommitment: string, + _index: ethers.BigNumber, + event: ethers.Event + ) => { + this.processEvents(rlnInstance, [event]); + } ); } public async registerWithIdentity( identity: IdentityCredential ): Promise { - if (this.storageIndex === undefined) { - throw Error( - "Cannot register credential, no storage contract index found." - ); - } - const txRegisterResponse: ethers.ContractTransaction = - await this.registryContract["register(uint16,uint256)"]( - this.storageIndex, - identity.IDCommitmentBigInt, - { gasLimit: 100000 } - ); - const txRegisterReceipt = await txRegisterResponse.wait(); + try { + const txRegisterResponse: ethers.ContractTransaction = + await this.contract.register( + identity.IDCommitmentBigInt, + this.storageIndex, + [], + { gasLimit: 300000 } + ); + const txRegisterReceipt = await txRegisterResponse.wait(); - // assumption: register(uint16,uint256) emits one event - const memberRegistered = txRegisterReceipt?.events?.[0]; + const memberRegistered = txRegisterReceipt.events?.find( + (event) => event.event === "MembershipRegistered" + ); - if (!memberRegistered) { + if (!memberRegistered || !memberRegistered.args) { + return undefined; + } + + const decodedData: MembershipRegisteredEvent = { + idCommitment: memberRegistered.args.idCommitment, + rateLimit: memberRegistered.args.rateLimit, + index: memberRegistered.args.index + }; + + const network = await this.contract.provider.getNetwork(); + const address = this.contract.address; + const membershipId = decodedData.index.toNumber(); + + return { + identity, + membership: { + address, + treeIndex: membershipId, + chainId: network.chainId + } + }; + } catch (error) { + log(`Error in registerWithIdentity: ${(error as Error).message}`); return undefined; } + } - const decodedData = this.contract.interface.decodeEventLog( - "MemberRegistered", - memberRegistered.data - ); + public async registerWithPermitAndErase( + identity: IdentityCredential, + permit: { + owner: string; + deadline: number; + v: number; + r: string; + s: string; + }, + idCommitmentsToErase: string[] + ): Promise { + try { + const txRegisterResponse: ethers.ContractTransaction = + await this.contract.registerWithPermit( + permit.owner, + permit.deadline, + permit.v, + permit.r, + permit.s, + identity.IDCommitmentBigInt, + this.storageIndex, + idCommitmentsToErase.map((id) => ethers.BigNumber.from(id)) + ); + const txRegisterReceipt = await txRegisterResponse.wait(); - const network = await this.registryContract.provider.getNetwork(); - const address = this.registryContract.address; - const membershipId = decodedData.index.toNumber(); + const memberRegistered = txRegisterReceipt.events?.find( + (event) => event.event === "MembershipRegistered" + ); - return { - identity, - membership: { - address, - treeIndex: membershipId, - chainId: network.chainId + if (!memberRegistered || !memberRegistered.args) { + return undefined; } - }; + + const decodedData: MembershipRegisteredEvent = { + idCommitment: memberRegistered.args.idCommitment, + rateLimit: memberRegistered.args.rateLimit, + index: memberRegistered.args.index + }; + + const network = await this.contract.provider.getNetwork(); + const address = this.contract.address; + const membershipId = decodedData.index.toNumber(); + + return { + identity, + membership: { + address, + treeIndex: membershipId, + chainId: network.chainId + } + }; + } catch (error) { + log(`Error in registerWithPermitAndErase: ${(error as Error).message}`); + return undefined; + } } public roots(): Uint8Array[] { return this.merkleRootTracker.roots(); } + + public async withdraw(token: string, holder: string): Promise { + try { + const tx = await this.contract.withdraw(token, { from: holder }); + await tx.wait(); + } catch (error) { + log(`Error in withdraw: ${(error as Error).message}`); + } + } } -type CustomQueryOptions = FetchMembersOptions & { +interface CustomQueryOptions extends FetchMembersOptions { membersFilter: ethers.EventFilter; -}; +} -// these value should be tested on other networks +// These values should be tested on other networks const FETCH_CHUNK = 5; const BLOCK_RANGE = 3000; @@ -261,18 +377,18 @@ async function queryFilter( fetchChunks = FETCH_CHUNK } = options; - if (!fromBlock) { + if (fromBlock === undefined) { return contract.queryFilter(membersFilter); } - if (!contract.signer.provider) { - throw Error("No provider found on the contract's signer."); + if (!contract.provider) { + throw Error("No provider found on the contract."); } - const toBlock = await contract.signer.provider.getBlockNumber(); + const toBlock = await contract.provider.getBlockNumber(); if (toBlock - fromBlock < fetchRange) { - return contract.queryFilter(membersFilter); + return contract.queryFilter(membersFilter, fromBlock, toBlock); } const events: ethers.Event[][] = []; @@ -294,7 +410,7 @@ function splitToChunks( to: number, step: number ): Array<[number, number]> { - const chunks = []; + const chunks: Array<[number, number]> = []; let left = from; while (left < to) { diff --git a/src/rln.ts b/src/rln.ts index 633cb9d..f679120 100644 --- a/src/rln.ts +++ b/src/rln.ts @@ -129,7 +129,8 @@ export class RLNInstance { this._signer = signer!; this._contract = await RLNContract.init(this, { registryAddress: registryAddress!, - signer: signer! + signer: signer!, + storageIndex: 0 }); this.started = true; } finally {