feat: upgrade contract adapter to the RLN v2 contract

This commit is contained in:
Danish Arora 2025-01-27 18:42:28 +05:30
parent c3dff5fd28
commit 1ca5b7e99a
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
5 changed files with 628 additions and 141 deletions

252
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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;
}

View File

@ -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<number, Member> = 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<number, number[]>
): 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<DecryptedCredentials | undefined> {
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<DecryptedCredentials | undefined> {
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<void> {
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) {

View File

@ -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 {