chore(rln): update ABI and contract address to Linea Sepolia, enhancements (#2294)

* 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(tests): rate limit additions

* chore: update gitignore
This commit is contained in:
Danish Arora 2025-03-31 14:32:29 +05:30 committed by GitHub
parent ea6daae927
commit a8ff776962
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1747 additions and 1282 deletions

View File

@ -8,8 +8,8 @@
"ahadns",
"Alives",
"alphabeta",
"Arraylike",
"arrayify",
"Arraylike",
"asym",
"autoshard",
"autosharding",
@ -70,6 +70,7 @@
"libauth",
"libp",
"lightpush",
"LINEA",
"livechat",
"Merkle",
"mkdir",

5
.gitignore vendored
View File

@ -12,4 +12,7 @@ docs
test-results
playwright-report
example
packages/discovery/mock_local_storage
packages/discovery/mock_local_storage
.cursorrules
.giga
.cursor

1008
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,11 @@ import { RLN } from '@waku/rln';
// Usage examples coming soon
```
## Constants
- Implementation contract: 0xde2260ca49300357d5af4153cda0d18f7b3ea9b3
- Proxy contract: 0xb9cd878c90e49f797b4431fbf4fb333108cb90e6
## License
MIT OR Apache-2.0

View File

@ -59,14 +59,9 @@
"@types/sinon": "^17.0.3",
"@waku/build-utils": "^1.0.0",
"@waku/message-encryption": "^0.0.32",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",
"chai-subset": "^1.6.0",
"deep-equal-in-any-order": "^2.0.6",
"fast-check": "^3.23.2",
"rollup-plugin-copy": "^3.5.0",
"sinon": "^19.0.2"
"rollup-plugin-copy": "^3.5.0"
},
"files": [
"dist",
@ -86,6 +81,11 @@
"ethereum-cryptography": "^3.1.0",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
"uuid": "^11.0.5"
"uuid": "^11.0.5",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",
"chai-subset": "^1.6.0",
"sinon": "^19.0.2"
}
}

View File

@ -3,7 +3,6 @@ import {
createEncoder,
DecodedMessage
} from "@waku/core/lib/message/version_0";
import type { IProtoMessage } from "@waku/interfaces";
import {
generatePrivateKey,
generateSymmetricKey,
@ -25,40 +24,30 @@ import {
RLNDecoder,
RLNEncoder
} from "./codec.js";
import { createRLN } from "./create.js";
import {
createTestMetaSetter,
createTestRLNCodecSetup,
EMPTY_PROTO_MESSAGE,
TEST_CONSTANTS,
verifyRLNMessage
} from "./codec.test-utils.js";
import { RlnMessage } from "./message.js";
import { epochBytesToInt } from "./utils/index.js";
const TestContentTopic = "/test/1/waku-message/utf8";
const EMPTY_PUBSUB_TOPIC = "";
const EMPTY_PROTO_MESSAGE = {
timestamp: undefined,
contentTopic: "",
ephemeral: undefined,
meta: undefined,
rateLimitProof: undefined,
version: undefined
};
describe("RLN codec with version 0", () => {
it("toWire", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const rlnEncoder = createRLNEncoder({
encoder: createEncoder({ contentTopic: TestContentTopic }),
encoder: createEncoder({ contentTopic: TEST_CONSTANTS.contentTopic }),
rlnInstance,
index,
credential
});
const rlnDecoder = createRLNDecoder({
rlnInstance,
decoder: createDecoder(TestContentTopic)
decoder: createDecoder(TEST_CONSTANTS.contentTopic)
});
const bytes = await rlnEncoder.toWire({ payload });
@ -67,78 +56,49 @@ describe("RLN codec with version 0", () => {
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
TEST_CONSTANTS.emptyPubsubTopic,
protoResult!
))!;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
it("toProtoObj", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const rlnEncoder = new RLNEncoder(
createEncoder({ contentTopic: TestContentTopic }),
createEncoder({ contentTopic: TEST_CONSTANTS.contentTopic }),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createDecoder(TestContentTopic)
createDecoder(TEST_CONSTANTS.contentTopic)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
TEST_CONSTANTS.emptyPubsubTopic,
proto!
)) as RlnMessage<DecodedMessage>;
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
});
describe("RLN codec with version 1", () => {
it("Symmetric, toWire", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const symKey = generateSymmetricKey();
const rlnEncoder = new RLNEncoder(
createSymEncoder({
contentTopic: TestContentTopic,
contentTopic: TEST_CONSTANTS.contentTopic,
symKey
}),
rlnInstance,
@ -147,45 +107,30 @@ describe("RLN codec with version 1", () => {
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createSymDecoder(TestContentTopic, symKey)
createSymDecoder(TEST_CONSTANTS.contentTopic, symKey)
);
const bytes = await rlnEncoder.toWire({ payload });
expect(bytes).to.not.be.undefined;
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
TEST_CONSTANTS.emptyPubsubTopic,
protoResult!
))!;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(1);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
});
it("Symmetric, toProtoObj", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const symKey = generateSymmetricKey();
const rlnEncoder = new RLNEncoder(
createSymEncoder({
contentTopic: TestContentTopic,
contentTopic: TEST_CONSTANTS.contentTopic,
symKey
}),
rlnInstance,
@ -194,45 +139,29 @@ describe("RLN codec with version 1", () => {
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createSymDecoder(TestContentTopic, symKey)
createSymDecoder(TEST_CONSTANTS.contentTopic, symKey)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
TEST_CONSTANTS.emptyPubsubTopic,
proto!
)) as RlnMessage<DecodedMessage>;
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(1);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
});
it("Asymmetric, toWire", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const privateKey = generatePrivateKey();
const publicKey = getPublicKey(privateKey);
const rlnEncoder = new RLNEncoder(
createAsymEncoder({
contentTopic: TestContentTopic,
contentTopic: TEST_CONSTANTS.contentTopic,
publicKey
}),
rlnInstance,
@ -241,46 +170,31 @@ describe("RLN codec with version 1", () => {
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createAsymDecoder(TestContentTopic, privateKey)
createAsymDecoder(TEST_CONSTANTS.contentTopic, privateKey)
);
const bytes = await rlnEncoder.toWire({ payload });
expect(bytes).to.not.be.undefined;
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
TEST_CONSTANTS.emptyPubsubTopic,
protoResult!
))!;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(1);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
});
it("Asymmetric, toProtoObj", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const privateKey = generatePrivateKey();
const publicKey = getPublicKey(privateKey);
const rlnEncoder = new RLNEncoder(
createAsymEncoder({
contentTopic: TestContentTopic,
contentTopic: TEST_CONSTANTS.contentTopic,
publicKey
}),
rlnInstance,
@ -289,106 +203,73 @@ describe("RLN codec with version 1", () => {
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createAsymDecoder(TestContentTopic, privateKey)
createAsymDecoder(TEST_CONSTANTS.contentTopic, privateKey)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
TEST_CONSTANTS.emptyPubsubTopic,
proto!
)) as RlnMessage<DecodedMessage>;
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(1);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
});
});
describe("RLN Codec - epoch", () => {
it("toProtoObj", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const rlnEncoder = new RLNEncoder(
createEncoder({ contentTopic: TestContentTopic }),
createEncoder({ contentTopic: TEST_CONSTANTS.contentTopic }),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createDecoder(TestContentTopic)
createDecoder(TEST_CONSTANTS.contentTopic)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
TEST_CONSTANTS.emptyPubsubTopic,
proto!
)) as RlnMessage<DecodedMessage>;
const epochBytes = proto!.rateLimitProof!.epoch;
const epoch = epochBytesToInt(epochBytes);
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch!.toString(10).length).to.eq(9);
expect(msg.epoch).to.eq(epoch);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
});
describe("RLN codec with version 0 and meta setter", () => {
// Encode the length of the payload
// Not a relevant real life example
const metaSetter = (msg: IProtoMessage & { meta: undefined }): Uint8Array => {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, msg.payload.length, false);
return new Uint8Array(buffer);
};
it("toWire", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const metaSetter = createTestMetaSetter();
const rlnEncoder = createRLNEncoder({
encoder: createEncoder({ contentTopic: TestContentTopic, metaSetter }),
encoder: createEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
metaSetter
}),
rlnInstance,
index,
credential
});
const rlnDecoder = createRLNDecoder({
rlnInstance,
decoder: createDecoder(TestContentTopic)
decoder: createDecoder(TEST_CONSTANTS.contentTopic)
});
const bytes = await rlnEncoder.toWire({ payload });
@ -397,7 +278,7 @@ describe("RLN codec with version 0 and meta setter", () => {
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
TEST_CONSTANTS.emptyPubsubTopic,
protoResult!
))!;
@ -407,43 +288,30 @@ describe("RLN codec with version 0 and meta setter", () => {
});
expect(msg!.meta).to.deep.eq(expectedMeta);
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
it("toProtoObj", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const metaSetter = createTestMetaSetter();
const rlnEncoder = new RLNEncoder(
createEncoder({ contentTopic: TestContentTopic, metaSetter }),
createEncoder({ contentTopic: TEST_CONSTANTS.contentTopic, metaSetter }),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createDecoder(TestContentTopic)
createDecoder(TEST_CONSTANTS.contentTopic)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
TEST_CONSTANTS.emptyPubsubTopic,
proto!
)) as RlnMessage<DecodedMessage>;
@ -453,18 +321,6 @@ describe("RLN codec with version 0 and meta setter", () => {
});
expect(msg!.meta).to.deep.eq(expectedMeta);
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
});

View File

@ -0,0 +1,80 @@
import type { IProtoMessage } from "@waku/interfaces";
import { expect } from "chai";
import { createRLN } from "./create.js";
import type { IdentityCredential } from "./identity.js";
export interface TestRLNCodecSetup {
rlnInstance: any;
credential: IdentityCredential;
index: number;
payload: Uint8Array;
}
export const TEST_CONSTANTS = {
contentTopic: "/test/1/waku-message/utf8",
emptyPubsubTopic: "",
defaultIndex: 0,
defaultPayload: new Uint8Array([1, 2, 3, 4, 5])
} as const;
export const EMPTY_PROTO_MESSAGE = {
timestamp: undefined,
contentTopic: "",
ephemeral: undefined,
meta: undefined,
rateLimitProof: undefined,
version: undefined
} as const;
/**
* Creates a basic RLN setup for codec tests
*/
export async function createTestRLNCodecSetup(): Promise<TestRLNCodecSetup> {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
rlnInstance.zerokit.insertMember(credential.IDCommitment);
return {
rlnInstance,
credential,
index: TEST_CONSTANTS.defaultIndex,
payload: TEST_CONSTANTS.defaultPayload
};
}
/**
* Creates a meta setter function for testing
*/
export function createTestMetaSetter(): (
msg: IProtoMessage & { meta: undefined }
) => Uint8Array {
return (msg: IProtoMessage & { meta: undefined }): Uint8Array => {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, msg.payload.length, false);
return new Uint8Array(buffer);
};
}
/**
* Verifies common RLN message properties
*/
export function verifyRLNMessage(
msg: any,
payload: Uint8Array,
contentTopic: string,
version: number,
rlnInstance: any
): void {
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(contentTopic);
expect(msg.msg.version).to.eq(version);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}

View File

@ -1,392 +1,646 @@
export const RLN_ABI = [
{ inputs: [], stateMutability: "nonpayable", type: "constructor" },
{
type: "constructor",
inputs: [],
stateMutability: "nonpayable"
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "CannotEraseActiveMembership",
type: "error"
},
{ inputs: [], name: "CannotExceedMaxTotalRateLimit", type: "error" },
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "CannotExtendNonGracePeriodMembership",
type: "error"
},
{
type: "error",
name: "DuplicateIdCommitment",
inputs: []
},
{
type: "error",
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "InvalidIdCommitment",
inputs: [
{
name: "idCommitment",
type: "uint256"
}
]
type: "error"
},
{ inputs: [], name: "InvalidMembershipRateLimit", type: "error" },
{
type: "error",
inputs: [
{ internalType: "uint256", name: "startIndex", type: "uint256" },
{ internalType: "uint256", name: "endIndex", type: "uint256" }
],
name: "InvalidPaginationQuery",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "MembershipDoesNotExist",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "NonHolderCannotEraseGracePeriodMembership",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "NonHolderCannotExtend",
type: "error"
},
{
anonymous: false,
inputs: [
{
name: "startIndex",
type: "uint256"
},
{
name: "endIndex",
type: "uint256"
}
]
},
{
type: "function",
name: "MAX_MEMBERSHIP_SET_SIZE",
inputs: [],
outputs: [
{
name: "",
type: "uint32"
}
],
stateMutability: "view"
},
{
type: "function",
name: "MERKLE_TREE_DEPTH",
inputs: [],
outputs: [
{
name: "",
type: "uint8"
}
],
stateMutability: "view"
},
{
type: "function",
name: "Q",
inputs: [],
outputs: [
{
name: "",
type: "uint256"
}
],
stateMutability: "view"
},
{
type: "function",
name: "activeDurationForNewMemberships",
inputs: [],
outputs: [
{
name: "",
type: "uint32"
}
],
stateMutability: "view"
},
{
type: "function",
name: "currentTotalRateLimit",
inputs: [],
outputs: [
{
name: "",
type: "uint256"
}
],
stateMutability: "view"
},
{
type: "function",
name: "deployedBlockNumber",
inputs: [],
outputs: [
{
name: "",
type: "uint32"
}
],
stateMutability: "view"
},
{
type: "function",
name: "depositsToWithdraw",
inputs: [
{
name: "holder",
indexed: false,
internalType: "address",
name: "previousAdmin",
type: "address"
},
{
name: "token",
indexed: false,
internalType: "address",
name: "newAdmin",
type: "address"
}
],
outputs: [
{
name: "balance",
type: "uint256"
}
],
stateMutability: "view"
name: "AdminChanged",
type: "event"
},
{
type: "function",
name: "eraseMemberships",
anonymous: false,
inputs: [
{
name: "idCommitments",
type: "uint256[]"
indexed: true,
internalType: "address",
name: "beacon",
type: "address"
}
],
outputs: [],
stateMutability: "nonpayable"
name: "BeaconUpgraded",
type: "event"
},
{
type: "function",
name: "eraseMemberships",
anonymous: false,
inputs: [
{
name: "idCommitments",
type: "uint256[]"
},
{
name: "eraseFromMembershipSet",
type: "bool"
}
{ indexed: false, internalType: "uint8", name: "version", type: "uint8" }
],
outputs: [],
stateMutability: "nonpayable"
name: "Initialized",
type: "event"
},
{
type: "function",
name: "extendMemberships",
inputs: [
{
name: "idCommitments",
type: "uint256[]"
}
],
outputs: [],
stateMutability: "nonpayable"
},
{
type: "function",
name: "getMembershipInfo",
anonymous: false,
inputs: [
{
indexed: false,
internalType: "uint256",
name: "idCommitment",
type: "uint256"
}
},
{
indexed: false,
internalType: "uint32",
name: "membershipRateLimit",
type: "uint32"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" }
],
outputs: [
name: "MembershipErased",
type: "event"
},
{
anonymous: false,
inputs: [
{
name: "",
type: "uint32"
indexed: false,
internalType: "uint256",
name: "idCommitment",
type: "uint256"
},
{
name: "",
indexed: false,
internalType: "uint32",
name: "membershipRateLimit",
type: "uint32"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" }
],
name: "MembershipExpired",
type: "event"
},
{
anonymous: false,
inputs: [
{
name: "",
indexed: false,
internalType: "uint256",
name: "idCommitment",
type: "uint256"
},
{
indexed: false,
internalType: "uint32",
name: "membershipRateLimit",
type: "uint32"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" },
{
indexed: false,
internalType: "uint256",
name: "newGracePeriodStartTimestamp",
type: "uint256"
}
],
stateMutability: "view"
name: "MembershipExtended",
type: "event"
},
{
type: "function",
name: "getMerkleProof",
anonymous: false,
inputs: [
{
name: "index",
type: "uint40"
}
],
outputs: [
{
name: "",
type: "uint256[20]"
}
],
stateMutability: "view"
},
{
type: "function",
name: "getRateCommitmentsInRangeBoundsInclusive",
inputs: [
{
name: "startIndex",
type: "uint32"
indexed: false,
internalType: "uint256",
name: "idCommitment",
type: "uint256"
},
{
name: "endIndex",
type: "uint32"
}
indexed: false,
internalType: "uint256",
name: "membershipRateLimit",
type: "uint256"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" }
],
outputs: [
{
name: "",
type: "uint256[]"
}
],
stateMutability: "view"
name: "MembershipRegistered",
type: "event"
},
{
type: "function",
name: "gracePeriodDurationForNewMemberships",
inputs: [],
outputs: [
{
name: "",
type: "uint32"
}
],
stateMutability: "view"
},
{
type: "function",
name: "initialize",
anonymous: false,
inputs: [
{
name: "_priceCalculator",
indexed: true,
internalType: "address",
name: "previousOwner",
type: "address"
},
{
name: "_maxTotalRateLimit",
type: "uint32"
},
indexed: true,
internalType: "address",
name: "newOwner",
type: "address"
}
],
name: "OwnershipTransferred",
type: "event"
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "implementation",
type: "address"
}
],
name: "Upgraded",
type: "event"
},
{
inputs: [],
name: "MAX_MEMBERSHIP_SET_SIZE",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "MERKLE_TREE_DEPTH",
outputs: [{ internalType: "uint8", name: "", type: "uint8" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "Q",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "activeDurationForNewMemberships",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "currentTotalRateLimit",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "deployedBlockNumber",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "holder", type: "address" },
{ internalType: "address", name: "token", type: "address" }
],
name: "depositsToWithdraw",
outputs: [{ internalType: "uint256", name: "balance", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256[]", name: "idCommitments", type: "uint256[]" }
],
name: "eraseMemberships",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256[]", name: "idCommitments", type: "uint256[]" },
{ internalType: "bool", name: "eraseFromMembershipSet", type: "bool" }
],
name: "eraseMemberships",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256[]", name: "idCommitments", type: "uint256[]" }
],
name: "extendMemberships",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "getMembershipInfo",
outputs: [
{ internalType: "uint32", name: "", type: "uint32" },
{ internalType: "uint32", name: "", type: "uint32" },
{ internalType: "uint256", name: "", type: "uint256" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [{ internalType: "uint40", name: "index", type: "uint40" }],
name: "getMerkleProof",
outputs: [{ internalType: "uint256[20]", name: "", type: "uint256[20]" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint32", name: "startIndex", type: "uint32" },
{ internalType: "uint32", name: "endIndex", type: "uint32" }
],
name: "getRateCommitmentsInRangeBoundsInclusive",
outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "gracePeriodDurationForNewMemberships",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [{ internalType: "uint256", name: "", type: "uint256" }],
name: "indicesOfLazilyErasedMemberships",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "_priceCalculator", type: "address" },
{ internalType: "uint32", name: "_maxTotalRateLimit", type: "uint32" },
{
internalType: "uint32",
name: "_minMembershipRateLimit",
type: "uint32"
},
{
internalType: "uint32",
name: "_maxMembershipRateLimit",
type: "uint32"
},
{
name: "_activeDuration",
type: "uint32"
},
{
name: "_gracePeriod",
type: "uint32"
}
{ internalType: "uint32", name: "_activeDuration", type: "uint32" },
{ internalType: "uint32", name: "_gracePeriod", type: "uint32" }
],
name: "initialize",
outputs: [],
stateMutability: "nonpayable"
stateMutability: "nonpayable",
type: "function"
},
{
type: "function",
inputs: [
{ internalType: "uint256", name: "_idCommitment", type: "uint256" }
],
name: "isExpired",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{
name: "_idCommitment",
type: "uint256"
}
{ internalType: "uint256", name: "_idCommitment", type: "uint256" }
],
name: "isInGracePeriod",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "isInMembershipSet",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "isValidIdCommitment",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "pure",
type: "function"
},
{
inputs: [{ internalType: "uint32", name: "rateLimit", type: "uint32" }],
name: "isValidMembershipRateLimit",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "maxMembershipRateLimit",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "maxTotalRateLimit",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "_idCommitment", type: "uint256" }
],
name: "membershipExpirationTimestamp",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "memberships",
outputs: [
{ internalType: "uint256", name: "depositAmount", type: "uint256" },
{ internalType: "uint32", name: "activeDuration", type: "uint32" },
{
name: "",
type: "bool"
}
internalType: "uint256",
name: "gracePeriodStartTimestamp",
type: "uint256"
},
{ internalType: "uint32", name: "gracePeriodDuration", type: "uint32" },
{ internalType: "uint32", name: "rateLimit", type: "uint32" },
{ internalType: "uint32", name: "index", type: "uint32" },
{ internalType: "address", name: "holder", type: "address" },
{ internalType: "address", name: "token", type: "address" }
],
stateMutability: "view"
stateMutability: "view",
type: "function"
},
{
type: "function",
inputs: [],
name: "merkleTree",
outputs: [
{ internalType: "uint40", name: "maxIndex", type: "uint40" },
{ internalType: "uint40", name: "numberOfLeaves", type: "uint40" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "minMembershipRateLimit",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "nextFreeIndex",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "owner",
outputs: [{ internalType: "address", name: "", type: "address" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "priceCalculator",
outputs: [
{ internalType: "contract IPriceCalculator", name: "", type: "address" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "proxiableUUID",
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" },
{ internalType: "uint32", name: "rateLimit", type: "uint32" },
{
internalType: "uint256[]",
name: "idCommitmentsToErase",
type: "uint256[]"
}
],
name: "register",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "owner", type: "address" },
{ internalType: "uint256", name: "deadline", type: "uint256" },
{ internalType: "uint8", name: "v", type: "uint8" },
{ internalType: "bytes32", name: "r", type: "bytes32" },
{ internalType: "bytes32", name: "s", type: "bytes32" },
{ internalType: "uint256", name: "idCommitment", type: "uint256" },
{ internalType: "uint32", name: "rateLimit", type: "uint32" },
{
name: "idCommitment",
type: "uint256"
},
{
name: "rateLimit",
type: "uint32"
},
{
internalType: "uint256[]",
name: "idCommitmentsToErase",
type: "uint256[]"
}
],
outputs: [],
stateMutability: "nonpayable"
},
{
type: "function",
name: "registerWithPermit",
inputs: [
{
name: "owner",
type: "address"
},
{
name: "deadline",
type: "uint256"
},
{
name: "v",
type: "uint8"
},
{
name: "r",
type: "bytes32"
},
{
name: "s",
type: "bytes32"
},
{
name: "idCommitment",
type: "uint256"
},
{
name: "rateLimit",
type: "uint32"
},
{
name: "idCommitmentsToErase",
type: "uint256[]"
}
],
outputs: [],
stateMutability: "nonpayable"
stateMutability: "nonpayable",
type: "function"
},
{
type: "event",
name: "MembershipRegistered",
inputs: [
{
name: "idCommitment",
type: "uint256",
indexed: false
},
{
name: "rateLimit",
type: "uint32",
indexed: false
},
{
name: "index",
type: "uint256",
indexed: false
}
],
anonymous: false
inputs: [],
name: "renounceOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [],
name: "root",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
type: "event",
name: "MembershipRemoved",
inputs: [
{
name: "idCommitment",
type: "uint256",
indexed: false
},
{
name: "index",
type: "uint256",
indexed: false
internalType: "uint32",
name: "_activeDurationForNewMembership",
type: "uint32"
}
],
anonymous: false
name: "setActiveDuration",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_gracePeriodDurationForNewMembership",
type: "uint32"
}
],
name: "setGracePeriodDuration",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_maxMembershipRateLimit",
type: "uint32"
}
],
name: "setMaxMembershipRateLimit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint32", name: "_maxTotalRateLimit", type: "uint32" }
],
name: "setMaxTotalRateLimit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_minMembershipRateLimit",
type: "uint32"
}
],
name: "setMinMembershipRateLimit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "_priceCalculator", type: "address" }
],
name: "setPriceCalculator",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [{ internalType: "address", name: "newOwner", type: "address" }],
name: "transferOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "newImplementation", type: "address" }
],
name: "upgradeTo",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "newImplementation", type: "address" },
{ internalType: "bytes", name: "data", type: "bytes" }
],
name: "upgradeToAndCall",
outputs: [],
stateMutability: "payable",
type: "function"
},
{
inputs: [{ internalType: "address", name: "token", type: "address" }],
name: "withdraw",
outputs: [],
stateMutability: "nonpayable",
type: "function"
}
];

View File

@ -1,8 +1,8 @@
import { RLN_ABI } from "./abi.js";
export const SEPOLIA_CONTRACT = {
chainId: 11155111,
address: "0xCB33Aa5B38d79E3D9Fa8B10afF38AA201399a7e3",
export const LINEA_CONTRACT = {
chainId: 59141,
address: "0xb9cd878c90e49f797b4431fbf4fb333108cb90e6",
abi: RLN_ABI
};

View File

@ -4,244 +4,87 @@ import chaiAsPromised from "chai-as-promised";
import * as ethers from "ethers";
import sinon, { SinonSandbox } from "sinon";
import { createRLN } from "../create.js";
import type { IdentityCredential } from "../identity.js";
import { DEFAULT_RATE_LIMIT, SEPOLIA_CONTRACT } from "./constants.js";
import { RLNContract } from "./rln_contract.js";
import { createTestRLNInstance, initializeRLNContract } from "./test-setup.js";
import {
createMockRegistryContract,
createRegisterStub,
mockRLNRegisteredEvent,
verifyRegistration
} from "./test-utils.js";
use(chaiAsPromised);
describe("RLN Contract abstraction - RLN", () => {
let sandbox: SinonSandbox;
let rlnInstance: any;
let mockedRegistryContract: any;
const mockRateLimits = {
minRate: 20,
maxRate: 600,
maxTotalRate: 1000,
currentTotalRate: 500
};
beforeEach(async () => {
sandbox = sinon.createSandbox();
rlnInstance = await createRLN();
rlnInstance.zerokit.insertMember = () => undefined;
mockedRegistryContract = {
minMembershipRateLimit: () =>
Promise.resolve(ethers.BigNumber.from(mockRateLimits.minRate)),
maxMembershipRateLimit: () =>
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxRate)),
maxTotalRateLimit: () =>
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxTotalRate)),
currentTotalRateLimit: () =>
Promise.resolve(ethers.BigNumber.from(mockRateLimits.currentTotalRate)),
queryFilter: () => [mockRLNRegisteredEvent()],
provider: {
getLogs: () => [],
getBlockNumber: () => Promise.resolve(1000),
getNetwork: () => Promise.resolve({ chainId: 11155111 })
},
filters: {
MembershipRegistered: () => ({}),
MembershipRemoved: () => ({})
},
on: () => ({})
};
const provider = new ethers.providers.JsonRpcProvider();
const voidSigner = new ethers.VoidSigner(
SEPOLIA_CONTRACT.address,
provider
);
await RLNContract.init(rlnInstance, {
address: SEPOLIA_CONTRACT.address,
signer: voidSigner,
rateLimit: DEFAULT_RATE_LIMIT,
contract: mockedRegistryContract as unknown as ethers.Contract
});
});
afterEach(() => {
sandbox.restore();
});
it("should fetch members from events and store them in the RLN instance", async () => {
const rlnInstance = await createRLN();
describe("Member Registration", () => {
it("should fetch members from events and store them in the RLN instance", async () => {
const { rlnInstance, insertMemberSpy } = await createTestRLNInstance();
const membershipRegisteredEvent = mockRLNRegisteredEvent();
const queryFilterStub = sinon.stub().returns([membershipRegisteredEvent]);
const insertMemberSpy = sinon.stub();
rlnInstance.zerokit.insertMember = insertMemberSpy;
const mockedRegistryContract = createMockRegistryContract({
queryFilter: queryFilterStub
});
const membershipRegisteredEvent = mockRLNRegisteredEvent();
const rlnContract = await initializeRLNContract(
rlnInstance,
mockedRegistryContract
);
const queryFilterStub = sinon.stub().returns([membershipRegisteredEvent]);
const mockedRegistryContract = {
queryFilter: queryFilterStub,
provider: {
getLogs: () => [],
getBlockNumber: () => Promise.resolve(1000)
},
interface: {
getEvent: (eventName: string) => ({
name: eventName,
format: () => {}
})
},
filters: {
MembershipRegistered: () => ({}),
MembershipRemoved: () => ({})
},
on: () => ({}),
removeAllListeners: () => ({})
};
await rlnContract.fetchMembers(rlnInstance, {
fromBlock: 0,
fetchRange: 1000,
fetchChunks: 2
});
const provider = new ethers.providers.JsonRpcProvider();
const voidSigner = new ethers.VoidSigner(
SEPOLIA_CONTRACT.address,
provider
);
const rlnContract = await RLNContract.init(rlnInstance, {
address: SEPOLIA_CONTRACT.address,
signer: voidSigner,
rateLimit: DEFAULT_RATE_LIMIT,
contract: mockedRegistryContract as unknown as ethers.Contract
});
await rlnContract.fetchMembers(rlnInstance, {
fromBlock: 0,
fetchRange: 1000,
fetchChunks: 2
});
expect(
insertMemberSpy.calledWith(
ethers.utils.zeroPad(
hexToBytes(membershipRegisteredEvent.args!.idCommitment),
32
expect(
insertMemberSpy.calledWith(
ethers.utils.zeroPad(
hexToBytes(membershipRegisteredEvent.args!.idCommitment),
32
)
)
)
).to.be.true;
expect(queryFilterStub.called).to.be.true;
});
it("should register a member", async () => {
const mockSignature =
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
const rlnInstance = await createRLN();
const identity: IdentityCredential =
rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature);
const insertMemberSpy = sinon.stub();
rlnInstance.zerokit.insertMember = insertMemberSpy;
const formatIdCommitment = (idCommitmentBigInt: bigint): string =>
"0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
const membershipRegisteredEvent = mockRLNRegisteredEvent(
formatIdCommitment(identity.IDCommitmentBigInt)
);
const registerStub = sinon.stub().returns({
wait: () =>
Promise.resolve({
events: [
{
event: "MembershipRegistered",
args: {
idCommitment: formatIdCommitment(identity.IDCommitmentBigInt),
rateLimit: DEFAULT_RATE_LIMIT,
index: ethers.BigNumber.from(1)
}
}
]
})
).to.be.true;
expect(queryFilterStub.called).to.be.true;
});
const mockedRegistryContract = {
register: registerStub,
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: () => ({})
};
it("should register a member", async () => {
const { rlnInstance, identity, insertMemberSpy } =
await createTestRLNInstance();
const provider = new ethers.providers.JsonRpcProvider();
const voidSigner = new ethers.VoidSigner(
SEPOLIA_CONTRACT.address,
provider
);
const rlnContract = await RLNContract.init(rlnInstance, {
signer: voidSigner,
address: SEPOLIA_CONTRACT.address,
rateLimit: DEFAULT_RATE_LIMIT,
contract: mockedRegistryContract as unknown as ethers.Contract
const registerStub = createRegisterStub(identity);
const mockedRegistryContract = createMockRegistryContract({
register: registerStub,
queryFilter: () => []
});
const rlnContract = await initializeRLNContract(
rlnInstance,
mockedRegistryContract
);
const decryptedCredentials =
await rlnContract.registerWithIdentity(identity);
if (!decryptedCredentials) {
throw new Error("Failed to retrieve credentials");
}
verifyRegistration(
decryptedCredentials,
identity,
registerStub,
insertMemberSpy
);
});
const decryptedCredentials =
await rlnContract.registerWithIdentity(identity);
expect(decryptedCredentials).to.not.be.undefined;
if (!decryptedCredentials) {
throw new Error("Decrypted credentials should not be undefined");
}
expect(
registerStub.calledWith(
identity.IDCommitmentBigInt,
DEFAULT_RATE_LIMIT,
[],
{
gasLimit: 300000
}
)
).to.be.true;
expect(decryptedCredentials).to.have.property("identity");
expect(decryptedCredentials).to.have.property("membership");
expect(decryptedCredentials.membership).to.include({
address: SEPOLIA_CONTRACT.address,
treeIndex: 1
});
const expectedIdCommitment = ethers.utils.zeroPad(
hexToBytes(formatIdCommitment(identity.IDCommitmentBigInt)),
32
);
expect(insertMemberSpy.callCount).to.equal(2);
expect(insertMemberSpy.getCall(1).args[0]).to.deep.equal(
expectedIdCommitment
);
});
});
function mockRLNRegisteredEvent(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;
}

View File

@ -30,7 +30,7 @@ interface RLNContractInitOptions extends RLNContractOptions {
export interface MembershipRegisteredEvent {
idCommitment: string;
rateLimit: number;
membershipRateLimit: ethers.BigNumber;
index: ethers.BigNumber;
}
@ -65,7 +65,8 @@ export class RLNContract {
private _members: Map<number, Member> = new Map();
private _membersFilter: ethers.EventFilter;
private _membersRemovedFilter: ethers.EventFilter;
private _membershipErasedFilter: ethers.EventFilter;
private _membersExpiredFilter: ethers.EventFilter;
/**
* Asynchronous initializer for RLNContract.
@ -94,15 +95,7 @@ export class RLNContract {
contract
} = options;
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`
);
}
this.validateRateLimit(rateLimit);
this.rateLimit = rateLimit;
const initialRoot = rlnInstance.zerokit.getMerkleRoot();
@ -111,9 +104,25 @@ export class RLNContract {
this.contract = contract || new ethers.Contract(address, RLN_ABI, signer);
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
// Initialize event filters for MembershipRegistered and MembershipRemoved
// Initialize event filters
this._membersFilter = this.contract.filters.MembershipRegistered();
this._membersRemovedFilter = this.contract.filters.MembershipRemoved();
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`
);
}
}
/**
@ -182,7 +191,7 @@ export class RLNContract {
this.contract.maxTotalRateLimit(),
this.contract.currentTotalRateLimit()
]);
return maxTotal.sub(currentTotal).toNumber();
return Number(maxTotal) - Number(currentTotal);
}
/**
@ -190,6 +199,7 @@ export class RLNContract {
* @param newRateLimit The new rate limit to use
*/
public async setRateLimit(newRateLimit: number): Promise<void> {
this.validateRateLimit(newRateLimit);
this.rateLimit = newRateLimit;
}
@ -207,11 +217,18 @@ export class RLNContract {
return this._membersFilter;
}
private get membersRemovedFilter(): ethers.EventFilter {
if (!this._membersRemovedFilter) {
throw Error("MembersRemoved filter was not initialized.");
private get membershipErasedFilter(): ethers.EventFilter {
if (!this._membershipErasedFilter) {
throw Error("MembershipErased filter was not initialized.");
}
return this._membersRemovedFilter;
return this._membershipErasedFilter;
}
private get membersExpiredFilter(): ethers.EventFilter {
if (!this._membersExpiredFilter) {
throw Error("MembersExpired filter was not initialized.");
}
return this._membersExpiredFilter;
}
public async fetchMembers(
@ -226,10 +243,19 @@ export class RLNContract {
const removedMemberEvents = await queryFilter(this.contract, {
fromBlock: this.deployBlock,
...options,
membersFilter: this.membersRemovedFilter
membersFilter: this.membershipErasedFilter
});
const expiredMemberEvents = await queryFilter(this.contract, {
fromBlock: this.deployBlock,
...options,
membersFilter: this.membersExpiredFilter
});
const events = [...registeredMemberEvents, ...removedMemberEvents];
const events = [
...registeredMemberEvents,
...removedMemberEvents,
...expiredMemberEvents
];
this.processEvents(rlnInstance, events);
}
@ -242,8 +268,26 @@ export class RLNContract {
return;
}
if (evt.event === "MembershipRemoved") {
const index = evt.args.index as ethers.BigNumber;
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);
} else {
log.error("Index is not a number or string", {
index,
event: evt
});
return;
}
const toRemoveVal = toRemoveTable.get(evt.blockNumber);
if (toRemoveVal != undefined) {
toRemoveVal.push(index.toNumber());
@ -275,15 +319,21 @@ export class RLNContract {
if (!evt.args) return;
const _idCommitment = evt.args.idCommitment as string;
const index = evt.args.index as ethers.BigNumber;
let index = evt.args.index;
if (!_idCommitment || !index) {
return;
}
if (typeof index === "number" || typeof index === "string") {
index = ethers.BigNumber.from(index);
}
const idCommitment = zeroPadLE(hexToBytes(_idCommitment), 32);
rlnInstance.zerokit.insertMember(idCommitment);
this._members.set(index.toNumber(), {
const numericIndex = index.toNumber();
this._members.set(numericIndex, {
index,
idCommitment: _idCommitment
});
@ -316,7 +366,7 @@ export class RLNContract {
this.membersFilter,
(
_idCommitment: string,
_rateLimit: number,
_membershipRateLimit: ethers.BigNumber,
_index: ethers.BigNumber,
event: ethers.Event
) => {
@ -325,9 +375,22 @@ export class RLNContract {
);
this.contract.on(
this.membersRemovedFilter,
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
) => {
@ -344,15 +407,45 @@ export class RLNContract {
`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: 300000 }
{ 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"
);
@ -366,30 +459,55 @@ export class RLNContract {
const decodedData: MembershipRegisteredEvent = {
idCommitment: memberRegistered.args.idCommitment,
rateLimit: memberRegistered.args.rateLimit,
membershipRateLimit: memberRegistered.args.membershipRateLimit,
index: memberRegistered.args.index
};
log.info(
`Successfully registered membership with index ${decodedData.index} ` +
`and rate limit ${decodedData.rateLimit}`
`and rate limit ${decodedData.membershipRateLimit}`
);
const network = await this.contract.provider.getNetwork();
const address = this.contract.address;
const membershipId = decodedData.index.toNumber();
const membershipId = Number(decodedData.index);
return {
identity,
membership: {
address,
treeIndex: membershipId,
chainId: network.chainId
chainId: network.chainId,
rateLimit: decodedData.membershipRateLimit.toNumber()
}
};
} catch (error) {
log.error(`Error in registerWithIdentity: ${(error as Error).message}`);
return undefined;
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
});
}
}
}
@ -467,25 +585,26 @@ export class RLNContract {
const decodedData: MembershipRegisteredEvent = {
idCommitment: memberRegistered.args.idCommitment,
rateLimit: memberRegistered.args.rateLimit,
membershipRateLimit: memberRegistered.args.membershipRateLimit,
index: memberRegistered.args.index
};
log.info(
`Successfully registered membership with permit. Index: ${decodedData.index}, ` +
`Rate limit: ${decodedData.rateLimit}, Erased ${idCommitmentsToErase.length} commitments`
`Rate limit: ${decodedData.membershipRateLimit}, Erased ${idCommitmentsToErase.length} commitments`
);
const network = await this.contract.provider.getNetwork();
const address = this.contract.address;
const membershipId = decodedData.index.toNumber();
const membershipId = ethers.BigNumber.from(decodedData.index).toNumber();
return {
identity,
membership: {
address,
treeIndex: membershipId,
chainId: network.chainId
chainId: network.chainId,
rateLimit: decodedData.membershipRateLimit.toNumber()
}
};
} catch (error) {
@ -560,16 +679,9 @@ export class RLNContract {
public async registerMembership(
idCommitment: string,
rateLimit: number = DEFAULT_RATE_LIMIT
rateLimit: number = this.rateLimit
): 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}`
);
}
this.validateRateLimit(rateLimit);
return this.contract.register(idCommitment, rateLimit, []);
}

View File

@ -0,0 +1,86 @@
import { hexToBytes } from "@waku/utils/bytes";
import { ethers } from "ethers";
import sinon from "sinon";
import { createRLN } from "../create.js";
import type { IdentityCredential } from "../identity.js";
import { DEFAULT_RATE_LIMIT, LINEA_CONTRACT } from "./constants.js";
import { RLNContract } from "./rln_contract.js";
export interface TestRLNInstance {
rlnInstance: any;
identity: IdentityCredential;
insertMemberSpy: sinon.SinonStub;
}
/**
* Creates a test RLN instance with basic setup
*/
export async function createTestRLNInstance(): Promise<TestRLNInstance> {
const rlnInstance = await createRLN();
const insertMemberSpy = sinon.stub();
rlnInstance.zerokit.insertMember = insertMemberSpy;
const mockSignature =
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
const identity =
rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature);
return {
rlnInstance,
identity,
insertMemberSpy
};
}
/**
* Initializes an RLN contract with the given registry contract
*/
export async function initializeRLNContract(
rlnInstance: any,
mockedRegistryContract: ethers.Contract
): Promise<RLNContract> {
const provider = new ethers.providers.JsonRpcProvider();
const voidSigner = new ethers.VoidSigner(LINEA_CONTRACT.address, provider);
const originalRegister = mockedRegistryContract.register;
(mockedRegistryContract as any).register = function (...args: any[]) {
const result = originalRegister.apply(this, args);
if (args[0] && rlnInstance.zerokit) {
const idCommitmentBigInt = args[0];
const idCommitmentHex =
"0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
const idCommitment = ethers.utils.zeroPad(
hexToBytes(idCommitmentHex),
32
);
rlnInstance.zerokit.insertMember(idCommitment);
}
return result;
};
const contract = await RLNContract.init(rlnInstance, {
address: LINEA_CONTRACT.address,
signer: voidSigner,
rateLimit: DEFAULT_RATE_LIMIT,
contract: mockedRegistryContract
});
return contract;
}
/**
* Common test message data
*/
export const TEST_DATA = {
contentTopic: "/test/1/waku-message/utf8",
emptyPubsubTopic: "",
testMessage: Uint8Array.from(
"Hello World".split("").map((x) => x.charCodeAt(0))
),
mockSignature:
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c"
} as const;

View File

@ -0,0 +1,179 @@
import { hexToBytes } from "@waku/utils/bytes";
import { expect } from "chai";
import * as ethers from "ethers";
import sinon from "sinon";
import type { IdentityCredential } from "../identity.js";
import { DEFAULT_RATE_LIMIT, LINEA_CONTRACT } from "./constants.js";
export const mockRateLimits = {
minRate: 20,
maxRate: 600,
maxTotalRate: 1200,
currentTotalRate: 500
};
type MockProvider = {
getLogs: () => never[];
getBlockNumber: () => Promise<number>;
getNetwork: () => Promise<{ chainId: number }>;
};
type MockFilters = {
MembershipRegistered: () => { address: string };
MembershipErased: () => { address: string };
MembershipExpired: () => { address: string };
};
export function createMockProvider(): MockProvider {
return {
getLogs: () => [],
getBlockNumber: () => Promise.resolve(1000),
getNetwork: () => Promise.resolve({ chainId: 11155111 })
};
}
export function createMockFilters(): MockFilters {
return {
MembershipRegistered: () => ({ address: LINEA_CONTRACT.address }),
MembershipErased: () => ({ address: LINEA_CONTRACT.address }),
MembershipExpired: () => ({ address: LINEA_CONTRACT.address })
};
}
type ContractOverrides = Partial<{
filters: Record<string, unknown>;
[key: string]: unknown;
}>;
export function createMockRegistryContract(
overrides: ContractOverrides = {}
): ethers.Contract {
const filters = {
MembershipRegistered: () => ({ address: LINEA_CONTRACT.address }),
MembershipErased: () => ({ address: LINEA_CONTRACT.address }),
MembershipExpired: () => ({ address: LINEA_CONTRACT.address })
};
const baseContract = {
minMembershipRateLimit: () =>
Promise.resolve(ethers.BigNumber.from(mockRateLimits.minRate)),
maxMembershipRateLimit: () =>
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxRate)),
maxTotalRateLimit: () =>
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxTotalRate)),
currentTotalRateLimit: () =>
Promise.resolve(ethers.BigNumber.from(mockRateLimits.currentTotalRate)),
queryFilter: () => [],
provider: createMockProvider(),
filters,
on: () => ({}),
removeAllListeners: () => ({}),
register: () => ({
wait: () =>
Promise.resolve({
events: [mockRLNRegisteredEvent()]
})
}),
estimateGas: {
register: () => Promise.resolve(ethers.BigNumber.from(100000))
},
functions: {
register: () => Promise.resolve()
},
getMemberIndex: () => Promise.resolve(null),
interface: {
getEvent: (eventName: string) => ({
name: eventName,
format: () => {}
})
},
address: LINEA_CONTRACT.address
};
// Merge overrides while preserving filters
const merged = {
...baseContract,
...overrides,
filters: { ...filters, ...(overrides.filters || {}) }
};
return merged as unknown as ethers.Contract;
}
export function mockRLNRegisteredEvent(idCommitment?: string): ethers.Event {
return {
args: {
idCommitment:
idCommitment ||
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
membershipRateLimit: ethers.BigNumber.from(DEFAULT_RATE_LIMIT),
index: ethers.BigNumber.from(1)
},
event: "MembershipRegistered"
} as unknown as ethers.Event;
}
export function formatIdCommitment(idCommitmentBigInt: bigint): string {
return "0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
}
export function createRegisterStub(
identity: IdentityCredential
): sinon.SinonStub {
return sinon.stub().callsFake(() => ({
wait: () =>
Promise.resolve({
events: [
{
event: "MembershipRegistered",
args: {
idCommitment: formatIdCommitment(identity.IDCommitmentBigInt),
membershipRateLimit: ethers.BigNumber.from(DEFAULT_RATE_LIMIT),
index: ethers.BigNumber.from(1)
}
}
]
})
}));
}
export function verifyRegistration(
decryptedCredentials: any,
identity: IdentityCredential,
registerStub: sinon.SinonStub,
insertMemberSpy: sinon.SinonStub
): void {
if (!decryptedCredentials) {
throw new Error("Decrypted credentials should not be undefined");
}
// Verify registration call
expect(
registerStub.calledWith(
sinon.match.same(identity.IDCommitmentBigInt),
sinon.match.same(DEFAULT_RATE_LIMIT),
sinon.match.array,
sinon.match.object
)
).to.be.true;
// Verify credential properties
expect(decryptedCredentials).to.have.property("identity");
expect(decryptedCredentials).to.have.property("membership");
expect(decryptedCredentials.membership).to.include({
address: LINEA_CONTRACT.address,
treeIndex: 1
});
// Verify member insertion
const expectedIdCommitment = ethers.utils.zeroPad(
hexToBytes(formatIdCommitment(identity.IDCommitmentBigInt)),
32
);
expect(insertMemberSpy.callCount).to.equal(1);
expect(insertMemberSpy.getCall(0).args[0]).to.deep.equal(
expectedIdCommitment
);
}

View File

@ -1,6 +1,6 @@
import { RLNDecoder, RLNEncoder } from "./codec.js";
import { RLN_ABI } from "./contract/abi.js";
import { RLNContract, SEPOLIA_CONTRACT } from "./contract/index.js";
import { LINEA_CONTRACT, RLNContract } from "./contract/index.js";
import { createRLN } from "./create.js";
import { IdentityCredential } from "./identity.js";
import { Keystore } from "./keystore/index.js";
@ -19,7 +19,18 @@ export {
RLNDecoder,
MerkleRootTracker,
RLNContract,
SEPOLIA_CONTRACT,
LINEA_CONTRACT,
extractMetaMaskSigner,
RLN_ABI
};
export type {
DecryptedCredentials,
EncryptedCredentials,
Keccak256Hash,
KeystoreEntity,
MembershipHash,
MembershipInfo,
Password,
Sha256Hash
} from "./keystore/types.js";

View File

@ -231,7 +231,8 @@ describe("Keystore", () => {
const membership = {
chainId: "0xAA36A7",
treeIndex: 8,
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71"
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71",
rateLimit: undefined
} as unknown as MembershipInfo;
const store = Keystore.create();
@ -246,6 +247,11 @@ describe("Keystore", () => {
expectedHash,
DEFAULT_PASSWORD
);
if (!actualCredentials) {
throw new Error("Failed to retrieve credentials");
}
expect(actualCredentials).to.deep.equalInAnyOrder({
identity,
membership
@ -276,7 +282,8 @@ describe("Keystore", () => {
const membership = {
chainId: "0xAA36A7",
treeIndex: 8,
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71"
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71",
rateLimit: undefined
} as unknown as MembershipInfo;
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);

View File

@ -274,7 +274,8 @@ export class Keystore {
membership: {
treeIndex: _.get(obj, "treeIndex"),
chainId: _.get(obj, "membershipContract.chainId"),
address: _.get(obj, "membershipContract.address")
address: _.get(obj, "membershipContract.address"),
rateLimit: _.get(obj, "membershipContract.rateLimit")
}
};
} catch (err) {

View File

@ -11,6 +11,7 @@ export type MembershipInfo = {
chainId: number;
address: string;
treeIndex: number;
rateLimit: number;
};
export type KeystoreEntity = {

View File

@ -16,7 +16,7 @@ import {
type RLNEncoder
} from "./codec.js";
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
import { RLNContract, SEPOLIA_CONTRACT } from "./contract/index.js";
import { LINEA_CONTRACT, RLNContract } from "./contract/index.js";
import { IdentityCredential } from "./identity.js";
import { Keystore } from "./keystore/index.js";
import type {
@ -108,7 +108,7 @@ type StartRLNOptions = {
*/
signer?: ethers.Signer;
/**
* If not set - will use default SEPOLIA_CONTRACT address.
* If not set - will use default LINEA_CONTRACT address.
*/
address?: string;
/**
@ -190,10 +190,10 @@ export class RLNInstance {
const address =
credentials?.membership.address ||
options.address ||
SEPOLIA_CONTRACT.address;
LINEA_CONTRACT.address;
if (address === SEPOLIA_CONTRACT.address) {
chainId = SEPOLIA_CONTRACT.chainId;
if (address === LINEA_CONTRACT.address) {
chainId = LINEA_CONTRACT.chainId;
}
const signer = options.signer || (await extractMetaMaskSigner());