@waku/rln 0.1.5-053bb95.0 → 0.1.5-35b50c3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/bundle/_virtual/utils.js +2 -2
  2. package/bundle/_virtual/utils2.js +2 -2
  3. package/bundle/index.js +2 -1
  4. package/bundle/packages/rln/dist/contract/constants.js +6 -2
  5. package/bundle/packages/rln/dist/contract/rln_base_contract.js +279 -144
  6. package/bundle/packages/rln/dist/contract/rln_contract.js +74 -89
  7. package/bundle/packages/rln/dist/credentials_manager.js +1 -1
  8. package/bundle/packages/rln/dist/identity.js +0 -8
  9. package/bundle/packages/rln/dist/keystore/keystore.js +28 -15
  10. package/bundle/packages/rln/node_modules/@chainsafe/bls-keystore/node_modules/ethereum-cryptography/random.js +1 -1
  11. package/bundle/packages/rln/node_modules/@chainsafe/bls-keystore/node_modules/ethereum-cryptography/utils.js +2 -2
  12. package/bundle/packages/rln/node_modules/@noble/hashes/_sha2.js +1 -1
  13. package/bundle/packages/rln/node_modules/@noble/hashes/hmac.js +1 -1
  14. package/bundle/packages/rln/node_modules/@noble/hashes/pbkdf2.js +1 -1
  15. package/bundle/packages/rln/node_modules/@noble/hashes/scrypt.js +1 -1
  16. package/bundle/packages/rln/node_modules/@noble/hashes/sha256.js +1 -1
  17. package/bundle/packages/rln/node_modules/@noble/hashes/sha512.js +1 -1
  18. package/bundle/packages/rln/node_modules/@noble/hashes/utils.js +1 -1
  19. package/dist/.tsbuildinfo +1 -1
  20. package/dist/contract/constants.d.ts +1 -1
  21. package/dist/contract/constants.js +1 -1
  22. package/dist/contract/constants.js.map +1 -1
  23. package/dist/contract/index.d.ts +1 -0
  24. package/dist/contract/index.js +1 -0
  25. package/dist/contract/index.js.map +1 -1
  26. package/dist/contract/rln_base_contract.d.ts +27 -25
  27. package/dist/contract/rln_base_contract.js +279 -144
  28. package/dist/contract/rln_base_contract.js.map +1 -1
  29. package/dist/contract/rln_contract.d.ts +4 -24
  30. package/dist/contract/rln_contract.js +74 -89
  31. package/dist/contract/rln_contract.js.map +1 -1
  32. package/dist/contract/types.d.ts +5 -0
  33. package/dist/contract/types.js.map +1 -1
  34. package/dist/credentials_manager.js +1 -1
  35. package/dist/credentials_manager.js.map +1 -1
  36. package/dist/identity.d.ts +0 -1
  37. package/dist/identity.js +0 -8
  38. package/dist/identity.js.map +1 -1
  39. package/dist/index.d.ts +2 -1
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/keystore/keystore.d.ts +1 -0
  43. package/dist/keystore/keystore.js +28 -15
  44. package/dist/keystore/keystore.js.map +1 -1
  45. package/dist/keystore/types.d.ts +2 -2
  46. package/package.json +1 -1
  47. package/src/contract/constants.ts +1 -1
  48. package/src/contract/index.ts +1 -0
  49. package/src/contract/rln_base_contract.ts +427 -216
  50. package/src/contract/rln_contract.ts +95 -120
  51. package/src/contract/types.ts +5 -0
  52. package/src/credentials_manager.ts +1 -1
  53. package/src/identity.ts +0 -9
  54. package/src/index.ts +3 -1
  55. package/src/keystore/keystore.ts +54 -29
  56. package/src/keystore/types.ts +2 -2
  57. package/bundle/packages/rln/dist/contract/errors.js +0 -62
  58. package/dist/contract/errors.d.ts +0 -30
  59. package/dist/contract/errors.js +0 -61
  60. package/dist/contract/errors.js.map +0 -1
  61. package/src/contract/errors.ts +0 -75
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console */
1
2
  import { Logger } from "@waku/utils";
2
3
  import { ethers } from "ethers";
3
4
 
@@ -7,15 +8,8 @@ import { DecryptedCredentials } from "../keystore/types.js";
7
8
  import { RLN_ABI } from "./abi.js";
8
9
  import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./constants.js";
9
10
  import {
10
- InvalidMembershipError,
11
- InvalidRateLimitError,
12
- MembershipExistsError,
13
- MembershipNotFoundError,
14
- RateLimitExceededError,
15
- RLNContractError,
16
- TransactionError
17
- } from "./errors.js";
18
- import {
11
+ CustomQueryOptions,
12
+ FetchMembersOptions,
19
13
  Member,
20
14
  MembershipInfo,
21
15
  MembershipRegisteredEvent,
@@ -27,8 +21,18 @@ const log = new Logger("waku:rln:contract:base");
27
21
 
28
22
  export class RLNBaseContract {
29
23
  public contract: ethers.Contract;
24
+ private deployBlock: undefined | number;
30
25
  private rateLimit: number;
31
26
 
27
+ protected _members: Map<number, Member> = new Map();
28
+ private _membersFilter: ethers.EventFilter;
29
+ private _membershipErasedFilter: ethers.EventFilter;
30
+ private _membersExpiredFilter: ethers.EventFilter;
31
+
32
+ /**
33
+ * Constructor for RLNBaseContract.
34
+ * Allows injecting a mocked contract for testing purposes.
35
+ */
32
36
  public constructor(options: RLNContractInitOptions) {
33
37
  const {
34
38
  address,
@@ -37,9 +41,38 @@ export class RLNBaseContract {
37
41
  contract
38
42
  } = options;
39
43
 
40
- this.validateRateLimit(rateLimit);
44
+ log.info("Initializing RLNBaseContract", { address, rateLimit });
45
+
41
46
  this.contract = contract || new ethers.Contract(address, RLN_ABI, signer);
42
47
  this.rateLimit = rateLimit;
48
+
49
+ try {
50
+ log.info("Setting up event filters");
51
+ // Initialize event filters
52
+ this._membersFilter = this.contract.filters.MembershipRegistered();
53
+ this._membershipErasedFilter = this.contract.filters.MembershipErased();
54
+ this._membersExpiredFilter = this.contract.filters.MembershipExpired();
55
+ log.info("Event filters initialized successfully");
56
+ } catch (error) {
57
+ log.error("Failed to initialize event filters", { error });
58
+ throw new Error(
59
+ "Failed to initialize event filters: " + (error as Error).message
60
+ );
61
+ }
62
+
63
+ // Initialize members and subscriptions
64
+ this.fetchMembers()
65
+ .then(() => {
66
+ this.subscribeToMembers();
67
+ })
68
+ .catch((error) => {
69
+ log.error("Failed to initialize members", { error });
70
+ });
71
+
72
+ // Validate rate limit asynchronously
73
+ this.validateRateLimit(rateLimit).catch((error) => {
74
+ log.error("Failed to validate initial rate limit", { error });
75
+ });
43
76
  }
44
77
 
45
78
  /**
@@ -116,151 +149,313 @@ export class RLNBaseContract {
116
149
  * @param newRateLimit The new rate limit to use
117
150
  */
118
151
  public async setRateLimit(newRateLimit: number): Promise<void> {
119
- this.validateRateLimit(newRateLimit);
152
+ await this.validateRateLimit(newRateLimit);
120
153
  this.rateLimit = newRateLimit;
121
154
  }
122
155
 
123
- /**
124
- * Gets all members in the given range
125
- * @param startIndex Start index (inclusive)
126
- * @param endIndex End index (exclusive)
127
- */
128
- public async getMembersInRange(
129
- startIndex: number,
130
- endIndex: number
131
- ): Promise<Member[]> {
132
- try {
133
- // Get all commitments in one call
134
- const idCommitments =
135
- await this.contract.getRateCommitmentsInRangeBoundsInclusive(
136
- startIndex,
137
- endIndex - 1 // -1 because getRateCommitmentsInRangeBoundsInclusive is inclusive
138
- );
156
+ public get members(): Member[] {
157
+ const sortedMembers = Array.from(this._members.values()).sort(
158
+ (left, right) => left.index.toNumber() - right.index.toNumber()
159
+ );
160
+ return sortedMembers;
161
+ }
139
162
 
140
- // Get membership info for each commitment in a single batch
141
- const membershipPromises = idCommitments.map(
142
- (idCommitment: ethers.BigNumber) =>
143
- this.contract
144
- .memberships(idCommitment)
145
- .then((info: { index: number | ethers.BigNumber }) => ({
146
- idCommitment: idCommitment.toString(),
147
- index: ethers.BigNumber.from(info.index)
148
- }))
149
- .catch(() => null) // Skip invalid members
150
- );
163
+ public async fetchMembers(options: FetchMembersOptions = {}): Promise<void> {
164
+ const registeredMemberEvents = await RLNBaseContract.queryFilter(
165
+ this.contract,
166
+ {
167
+ fromBlock: this.deployBlock,
168
+ ...options,
169
+ membersFilter: this.membersFilter
170
+ }
171
+ );
172
+ const removedMemberEvents = await RLNBaseContract.queryFilter(
173
+ this.contract,
174
+ {
175
+ fromBlock: this.deployBlock,
176
+ ...options,
177
+ membersFilter: this.membershipErasedFilter
178
+ }
179
+ );
180
+ const expiredMemberEvents = await RLNBaseContract.queryFilter(
181
+ this.contract,
182
+ {
183
+ fromBlock: this.deployBlock,
184
+ ...options,
185
+ membersFilter: this.membersExpiredFilter
186
+ }
187
+ );
188
+
189
+ const events = [
190
+ ...registeredMemberEvents,
191
+ ...removedMemberEvents,
192
+ ...expiredMemberEvents
193
+ ];
194
+ this.processEvents(events);
195
+ }
196
+
197
+ public static async queryFilter(
198
+ contract: ethers.Contract,
199
+ options: CustomQueryOptions
200
+ ): Promise<ethers.Event[]> {
201
+ const FETCH_CHUNK = 5;
202
+ const BLOCK_RANGE = 3000;
203
+
204
+ const {
205
+ fromBlock,
206
+ membersFilter,
207
+ fetchRange = BLOCK_RANGE,
208
+ fetchChunks = FETCH_CHUNK
209
+ } = options;
210
+
211
+ if (fromBlock === undefined) {
212
+ return contract.queryFilter(membersFilter);
213
+ }
214
+
215
+ if (!contract.provider) {
216
+ throw Error("No provider found on the contract.");
217
+ }
218
+
219
+ const toBlock = await contract.provider.getBlockNumber();
220
+
221
+ if (toBlock - fromBlock < fetchRange) {
222
+ return contract.queryFilter(membersFilter, fromBlock, toBlock);
223
+ }
224
+
225
+ const events: ethers.Event[][] = [];
226
+ const chunks = RLNBaseContract.splitToChunks(
227
+ fromBlock,
228
+ toBlock,
229
+ fetchRange
230
+ );
151
231
 
152
- // Wait for all promises to resolve
153
- const members = (await Promise.all(membershipPromises)).filter(
154
- (m): m is Member => m !== null
232
+ for (const portion of RLNBaseContract.takeN<[number, number]>(
233
+ chunks,
234
+ fetchChunks
235
+ )) {
236
+ const promises = portion.map(([left, right]) =>
237
+ RLNBaseContract.ignoreErrors(
238
+ contract.queryFilter(membersFilter, left, right),
239
+ []
240
+ )
155
241
  );
156
- return members;
157
- } catch (error) {
242
+ const fetchedEvents = await Promise.all(promises);
243
+ events.push(fetchedEvents.flatMap((v) => v));
244
+ }
245
+
246
+ return events.flatMap((v) => v);
247
+ }
248
+
249
+ public processEvents(events: ethers.Event[]): void {
250
+ const toRemoveTable = new Map<number, number[]>();
251
+ const toInsertTable = new Map<number, ethers.Event[]>();
252
+
253
+ events.forEach((evt) => {
254
+ if (!evt.args) {
255
+ return;
256
+ }
257
+
158
258
  if (
159
- error instanceof Error &&
160
- error.message.includes("InvalidPaginationQuery")
259
+ evt.event === "MembershipErased" ||
260
+ evt.event === "MembershipExpired"
161
261
  ) {
162
- throw new RLNContractError(
163
- `Invalid pagination range: start=${startIndex}, end=${endIndex}`
164
- );
262
+ let index = evt.args.index;
263
+
264
+ if (!index) {
265
+ return;
266
+ }
267
+
268
+ if (typeof index === "number" || typeof index === "string") {
269
+ index = ethers.BigNumber.from(index);
270
+ }
271
+
272
+ const toRemoveVal = toRemoveTable.get(evt.blockNumber);
273
+ if (toRemoveVal != undefined) {
274
+ toRemoveVal.push(index.toNumber());
275
+ toRemoveTable.set(evt.blockNumber, toRemoveVal);
276
+ } else {
277
+ toRemoveTable.set(evt.blockNumber, [index.toNumber()]);
278
+ }
279
+ } else if (evt.event === "MembershipRegistered") {
280
+ let eventsPerBlock = toInsertTable.get(evt.blockNumber);
281
+ if (eventsPerBlock == undefined) {
282
+ eventsPerBlock = [];
283
+ }
284
+
285
+ eventsPerBlock.push(evt);
286
+ toInsertTable.set(evt.blockNumber, eventsPerBlock);
165
287
  }
166
- throw error;
288
+ });
289
+ }
290
+
291
+ public static splitToChunks(
292
+ from: number,
293
+ to: number,
294
+ step: number
295
+ ): Array<[number, number]> {
296
+ const chunks: Array<[number, number]> = [];
297
+
298
+ let left = from;
299
+ while (left < to) {
300
+ const right = left + step < to ? left + step : to;
301
+
302
+ chunks.push([left, right] as [number, number]);
303
+
304
+ left = right;
167
305
  }
306
+
307
+ return chunks;
168
308
  }
169
309
 
170
- /**
171
- * Gets all current members
172
- */
173
- public async getAllMembers(): Promise<Member[]> {
174
- const nextIndex = (await this.contract.nextFreeIndex()).toNumber();
175
- return this.getMembersInRange(0, nextIndex);
310
+ public static *takeN<T>(array: T[], size: number): Iterable<T[]> {
311
+ let start = 0;
312
+
313
+ while (start < array.length) {
314
+ const portion = array.slice(start, start + size);
315
+
316
+ yield portion;
317
+
318
+ start += size;
319
+ }
176
320
  }
177
321
 
178
- /**
179
- * Gets the member index if it exists, or null if it doesn't
180
- * Throws only on actual errors (invalid input, network issues, etc)
181
- */
182
- private async getMemberIndex(
183
- idCommitment: string
184
- ): Promise<ethers.BigNumber | null> {
322
+ public static async ignoreErrors<T>(
323
+ promise: Promise<T>,
324
+ defaultValue: T
325
+ ): Promise<T> {
185
326
  try {
186
- const isValid = await this.contract.isInMembershipSet(idCommitment);
187
- if (!isValid) {
188
- return null;
327
+ return await promise;
328
+ } catch (err: unknown) {
329
+ if (err instanceof Error) {
330
+ log.info(`Ignoring an error during query: ${err.message}`);
331
+ } else {
332
+ log.info(`Ignoring an unknown error during query`);
189
333
  }
190
-
191
- const membershipInfo = await this.contract.memberships(idCommitment);
192
- return ethers.BigNumber.from(membershipInfo.index);
193
- } catch (error) {
194
- log.error(`Error getting member index: ${(error as Error).message}`);
195
- throw new InvalidMembershipError(idCommitment);
334
+ return defaultValue;
196
335
  }
197
336
  }
198
337
 
338
+ public subscribeToMembers(): void {
339
+ this.contract.on(
340
+ this.membersFilter,
341
+ (
342
+ _idCommitment: bigint,
343
+ _membershipRateLimit: ethers.BigNumber,
344
+ _index: ethers.BigNumber,
345
+ event: ethers.Event
346
+ ) => {
347
+ this.processEvents([event]);
348
+ }
349
+ );
350
+
351
+ this.contract.on(
352
+ this.membershipErasedFilter,
353
+ (
354
+ _idCommitment: bigint,
355
+ _membershipRateLimit: ethers.BigNumber,
356
+ _index: ethers.BigNumber,
357
+ event: ethers.Event
358
+ ) => {
359
+ this.processEvents([event]);
360
+ }
361
+ );
362
+
363
+ this.contract.on(
364
+ this.membersExpiredFilter,
365
+ (
366
+ _idCommitment: bigint,
367
+ _membershipRateLimit: ethers.BigNumber,
368
+ _index: ethers.BigNumber,
369
+ event: ethers.Event
370
+ ) => {
371
+ this.processEvents([event]);
372
+ }
373
+ );
374
+ }
375
+
199
376
  public async getMembershipInfo(
200
- idCommitment: string
201
- ): Promise<MembershipInfo> {
377
+ idCommitmentBigInt: bigint
378
+ ): Promise<MembershipInfo | undefined> {
202
379
  try {
203
- const [startBlock, endBlock, rateLimit] =
204
- await this.contract.getMembershipInfo(idCommitment);
380
+ const membershipData =
381
+ await this.contract.memberships(idCommitmentBigInt);
205
382
  const currentBlock = await this.contract.provider.getBlockNumber();
383
+ console.log("membershipData", membershipData);
384
+
385
+ const [
386
+ depositAmount,
387
+ activeDuration,
388
+ gracePeriodStartTimestamp,
389
+ gracePeriodDuration,
390
+ rateLimit,
391
+ index,
392
+ holder,
393
+ token
394
+ ] = membershipData;
395
+
396
+ const gracePeriodEnd = gracePeriodStartTimestamp.add(gracePeriodDuration);
206
397
 
207
398
  let state: MembershipState;
208
- if (currentBlock < startBlock) {
399
+ if (currentBlock < gracePeriodStartTimestamp.toNumber()) {
209
400
  state = MembershipState.Active;
210
- } else if (currentBlock < endBlock) {
401
+ } else if (currentBlock < gracePeriodEnd.toNumber()) {
211
402
  state = MembershipState.GracePeriod;
212
403
  } else {
213
404
  state = MembershipState.Expired;
214
405
  }
215
406
 
216
- const index = await this.getMemberIndex(idCommitment);
217
- if (index === null) {
218
- throw new MembershipNotFoundError(idCommitment);
219
- }
220
-
221
407
  return {
222
408
  index,
223
- idCommitment,
224
- rateLimit: rateLimit.toNumber(),
225
- startBlock: startBlock.toNumber(),
226
- endBlock: endBlock.toNumber(),
227
- state
409
+ idCommitment: idCommitmentBigInt.toString(),
410
+ rateLimit: Number(rateLimit),
411
+ startBlock: gracePeriodStartTimestamp.toNumber(),
412
+ endBlock: gracePeriodEnd.toNumber(),
413
+ state,
414
+ depositAmount,
415
+ activeDuration,
416
+ gracePeriodDuration,
417
+ holder,
418
+ token
228
419
  };
229
420
  } catch (error) {
230
- if (error instanceof RLNContractError) {
231
- throw error;
232
- }
233
- log.error(`Error getting membership info: ${(error as Error).message}`);
234
- throw new InvalidMembershipError(idCommitment);
421
+ console.error("Error in getMembershipInfo:", error);
422
+ return undefined;
235
423
  }
236
424
  }
237
425
 
238
- public async extendMembership(idCommitment: string): Promise<void> {
239
- const tx = await this.contract.extendMemberships([idCommitment]);
240
- await this.confirmTransaction(tx, "MembershipExtended", (event) => ({
241
- idCommitment: event.args!.idCommitment,
242
- endBlock: event.args!.endBlock
243
- }));
426
+ public async extendMembership(
427
+ idCommitmentBigInt: bigint
428
+ ): Promise<ethers.ContractTransaction> {
429
+ const tx = await this.contract.extendMemberships([idCommitmentBigInt]);
430
+ await tx.wait();
431
+ return tx;
244
432
  }
245
433
 
246
434
  public async eraseMembership(
247
- idCommitment: string,
435
+ idCommitmentBigInt: bigint,
248
436
  eraseFromMembershipSet: boolean = true
249
- ): Promise<void> {
437
+ ): Promise<ethers.ContractTransaction> {
250
438
  const tx = await this.contract.eraseMemberships(
251
- [idCommitment],
439
+ [idCommitmentBigInt],
252
440
  eraseFromMembershipSet
253
441
  );
254
- await this.confirmTransaction(tx, "MembershipErased", (event) => ({
255
- idCommitment: event.args!.idCommitment,
256
- index: event.args!.index
257
- }));
442
+ await tx.wait();
443
+ return tx;
444
+ }
445
+
446
+ public async withdraw(token: string, from: string): Promise<void> {
447
+ try {
448
+ const tx = await this.contract.withdraw(token, from);
449
+ await tx.wait();
450
+ } catch (error) {
451
+ log.error(`Error in withdraw: ${(error as Error).message}`);
452
+ }
258
453
  }
259
454
 
260
455
  public async registerMembership(
261
- idCommitment: string,
456
+ idCommitmentBigInt: bigint,
262
457
  rateLimit: number = DEFAULT_RATE_LIMIT
263
- ): Promise<void> {
458
+ ): Promise<ethers.ContractTransaction> {
264
459
  if (
265
460
  rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
266
461
  rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
@@ -269,26 +464,7 @@ export class RLNBaseContract {
269
464
  `Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
270
465
  );
271
466
  }
272
- const tx = await this.contract.register(idCommitment, rateLimit, []);
273
- await this.confirmTransaction(tx, "MembershipRegistered", (event) => ({
274
- idCommitment: event.args!.idCommitment,
275
- membershipRateLimit: event.args!.membershipRateLimit,
276
- index: event.args!.index
277
- }));
278
- }
279
-
280
- public async withdraw(token: string, holder: string): Promise<void> {
281
- try {
282
- const tx = await this.contract.withdraw(token, { from: holder });
283
- await this.confirmTransaction(tx, "TokenWithdrawn", (event) => ({
284
- token: event.args!.token,
285
- holder: event.args!.holder,
286
- amount: event.args!.amount
287
- }));
288
- } catch (error) {
289
- log.error(`Error in withdraw: ${(error as Error).message}`);
290
- throw error;
291
- }
467
+ return this.contract.register(idCommitmentBigInt, rateLimit, []);
292
468
  }
293
469
 
294
470
  public async registerWithIdentity(
@@ -301,20 +477,20 @@ export class RLNBaseContract {
301
477
 
302
478
  // Check if the ID commitment is already registered
303
479
  const existingIndex = await this.getMemberIndex(
304
- identity.IDCommitmentBigInt.toString()
480
+ identity.IDCommitmentBigInt
305
481
  );
306
-
307
- if (existingIndex !== null) {
308
- throw new MembershipExistsError(
309
- identity.IDCommitmentBigInt.toString(),
310
- existingIndex.toString()
482
+ if (existingIndex) {
483
+ throw new Error(
484
+ `ID commitment is already registered with index ${existingIndex}`
311
485
  );
312
486
  }
313
487
 
314
488
  // Check if there's enough remaining rate limit
315
489
  const remainingRateLimit = await this.getRemainingTotalRateLimit();
316
490
  if (remainingRateLimit < this.rateLimit) {
317
- throw new RateLimitExceededError(this.rateLimit, remainingRateLimit);
491
+ throw new Error(
492
+ `Not enough remaining rate limit. Requested: ${this.rateLimit}, Available: ${remainingRateLimit}`
493
+ );
318
494
  }
319
495
 
320
496
  const estimatedGas = await this.contract.estimateGas.register(
@@ -324,23 +500,37 @@ export class RLNBaseContract {
324
500
  );
325
501
  const gasLimit = estimatedGas.add(10000);
326
502
 
327
- const tx = await this.contract.register(
328
- identity.IDCommitmentBigInt,
329
- this.rateLimit,
330
- [],
331
- { gasLimit }
332
- );
503
+ const txRegisterResponse: ethers.ContractTransaction =
504
+ await this.contract.register(
505
+ identity.IDCommitmentBigInt,
506
+ this.rateLimit,
507
+ [],
508
+ { gasLimit }
509
+ );
510
+
511
+ const txRegisterReceipt = await txRegisterResponse.wait();
333
512
 
334
- const decodedData = await this.confirmTransaction(
335
- tx,
336
- "MembershipRegistered",
337
- (event): MembershipRegisteredEvent => ({
338
- idCommitment: event.args!.idCommitment,
339
- membershipRateLimit: event.args!.membershipRateLimit,
340
- index: event.args!.index
341
- })
513
+ if (txRegisterReceipt.status === 0) {
514
+ throw new Error("Transaction failed on-chain");
515
+ }
516
+
517
+ const memberRegistered = txRegisterReceipt.events?.find(
518
+ (event) => event.event === "MembershipRegistered"
342
519
  );
343
520
 
521
+ if (!memberRegistered || !memberRegistered.args) {
522
+ log.error(
523
+ "Failed to register membership: No MembershipRegistered event found"
524
+ );
525
+ return undefined;
526
+ }
527
+
528
+ const decodedData: MembershipRegisteredEvent = {
529
+ idCommitment: memberRegistered.args.idCommitment,
530
+ membershipRateLimit: memberRegistered.args.membershipRateLimit,
531
+ index: memberRegistered.args.index
532
+ };
533
+
344
534
  log.info(
345
535
  `Successfully registered membership with index ${decodedData.index} ` +
346
536
  `and rate limit ${decodedData.membershipRateLimit}`
@@ -348,55 +538,44 @@ export class RLNBaseContract {
348
538
 
349
539
  const network = await this.contract.provider.getNetwork();
350
540
  const address = this.contract.address;
351
- const membershipId = decodedData.index.toString();
541
+ const membershipId = Number(decodedData.index);
352
542
 
353
543
  return {
354
544
  identity,
355
545
  membership: {
356
546
  address,
357
- treeIndex: parseInt(membershipId),
547
+ treeIndex: membershipId,
358
548
  chainId: network.chainId.toString(),
359
549
  rateLimit: decodedData.membershipRateLimit.toNumber()
360
550
  }
361
551
  };
362
552
  } catch (error) {
363
- if (error instanceof RLNContractError) {
364
- throw error;
365
- }
366
-
367
553
  if (error instanceof Error) {
368
554
  const errorMessage = error.message;
369
555
  log.error("registerWithIdentity - error message:", errorMessage);
370
556
  log.error("registerWithIdentity - error stack:", error.stack);
371
557
 
372
- // Map contract errors to our custom errors
558
+ // Try to extract more specific error information
373
559
  if (errorMessage.includes("CannotExceedMaxTotalRateLimit")) {
374
- throw new RateLimitExceededError(
375
- this.rateLimit,
376
- await this.getRemainingTotalRateLimit()
560
+ throw new Error(
561
+ "Registration failed: Cannot exceed maximum total rate limit"
377
562
  );
378
563
  } else if (errorMessage.includes("InvalidIdCommitment")) {
379
- throw new InvalidMembershipError(
380
- identity.IDCommitmentBigInt.toString()
381
- );
564
+ throw new Error("Registration failed: Invalid ID commitment");
382
565
  } else if (errorMessage.includes("InvalidMembershipRateLimit")) {
383
- throw new InvalidRateLimitError(
384
- this.rateLimit,
385
- RATE_LIMIT_PARAMS.MIN_RATE,
386
- RATE_LIMIT_PARAMS.MAX_RATE
387
- );
566
+ throw new Error("Registration failed: Invalid membership rate limit");
388
567
  } else if (errorMessage.includes("execution reverted")) {
389
- throw new TransactionError(
568
+ throw new Error(
390
569
  "Contract execution reverted. Check contract requirements."
391
570
  );
571
+ } else {
572
+ throw new Error(`Error in registerWithIdentity: ${errorMessage}`);
392
573
  }
393
-
394
- throw new RLNContractError(
395
- `Error in registerWithIdentity: ${errorMessage}`
396
- );
574
+ } else {
575
+ throw new Error("Unknown error in registerWithIdentity", {
576
+ cause: error
577
+ });
397
578
  }
398
-
399
- throw new RLNContractError("Unknown error in registerWithIdentity");
400
579
  }
401
580
  }
402
581
 
@@ -416,27 +595,36 @@ export class RLNBaseContract {
416
595
  `Registering identity with permit and rate limit: ${this.rateLimit} messages/epoch`
417
596
  );
418
597
 
419
- const tx = await this.contract.registerWithPermit(
420
- permit.owner,
421
- permit.deadline,
422
- permit.v,
423
- permit.r,
424
- permit.s,
425
- identity.IDCommitmentBigInt,
426
- this.rateLimit,
427
- idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
428
- );
598
+ const txRegisterResponse: ethers.ContractTransaction =
599
+ await this.contract.registerWithPermit(
600
+ permit.owner,
601
+ permit.deadline,
602
+ permit.v,
603
+ permit.r,
604
+ permit.s,
605
+ identity.IDCommitmentBigInt,
606
+ this.rateLimit,
607
+ idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
608
+ );
609
+ const txRegisterReceipt = await txRegisterResponse.wait();
429
610
 
430
- const decodedData = await this.confirmTransaction(
431
- tx,
432
- "MembershipRegistered",
433
- (event): MembershipRegisteredEvent => ({
434
- idCommitment: event.args!.idCommitment,
435
- membershipRateLimit: event.args!.membershipRateLimit,
436
- index: event.args!.index
437
- })
611
+ const memberRegistered = txRegisterReceipt.events?.find(
612
+ (event) => event.event === "MembershipRegistered"
438
613
  );
439
614
 
615
+ if (!memberRegistered || !memberRegistered.args) {
616
+ log.error(
617
+ "Failed to register membership with permit: No MembershipRegistered event found"
618
+ );
619
+ return undefined;
620
+ }
621
+
622
+ const decodedData: MembershipRegisteredEvent = {
623
+ idCommitment: memberRegistered.args.idCommitment,
624
+ membershipRateLimit: memberRegistered.args.membershipRateLimit,
625
+ index: memberRegistered.args.index
626
+ };
627
+
440
628
  log.info(
441
629
  `Successfully registered membership with permit. Index: ${decodedData.index}, ` +
442
630
  `Rate limit: ${decodedData.membershipRateLimit}, Erased ${idCommitmentsToErase.length} commitments`
@@ -444,13 +632,13 @@ export class RLNBaseContract {
444
632
 
445
633
  const network = await this.contract.provider.getNetwork();
446
634
  const address = this.contract.address;
447
- const membershipId = decodedData.index.toString();
635
+ const membershipId = Number(decodedData.index);
448
636
 
449
637
  return {
450
638
  identity,
451
639
  membership: {
452
640
  address,
453
- treeIndex: parseInt(membershipId),
641
+ treeIndex: membershipId,
454
642
  chainId: network.chainId.toString(),
455
643
  rateLimit: decodedData.membershipRateLimit.toNumber()
456
644
  }
@@ -459,7 +647,7 @@ export class RLNBaseContract {
459
647
  log.error(
460
648
  `Error in registerWithPermitAndErase: ${(error as Error).message}`
461
649
  );
462
- throw error;
650
+ return undefined;
463
651
  }
464
652
  }
465
653
 
@@ -467,34 +655,57 @@ export class RLNBaseContract {
467
655
  * Validates that the rate limit is within the allowed range
468
656
  * @throws Error if the rate limit is outside the allowed range
469
657
  */
470
- private validateRateLimit(rateLimit: number): void {
471
- if (
472
- rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
473
- rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
474
- ) {
475
- throw new InvalidRateLimitError(
476
- rateLimit,
477
- RATE_LIMIT_PARAMS.MIN_RATE,
478
- RATE_LIMIT_PARAMS.MAX_RATE
658
+ private async validateRateLimit(rateLimit: number): Promise<void> {
659
+ const [minRate, maxRate] = await Promise.all([
660
+ this.contract.minMembershipRateLimit(),
661
+ this.contract.maxMembershipRateLimit()
662
+ ]);
663
+
664
+ const minRateNum = ethers.BigNumber.from(minRate).toNumber();
665
+ const maxRateNum = ethers.BigNumber.from(maxRate).toNumber();
666
+
667
+ if (rateLimit < minRateNum || rateLimit > maxRateNum) {
668
+ throw new Error(
669
+ `Rate limit must be between ${minRateNum} and ${maxRateNum} messages per epoch`
479
670
  );
480
671
  }
481
672
  }
482
673
 
483
- /**
484
- * Helper to confirm a transaction and extract event data
485
- */
486
- private async confirmTransaction<T>(
487
- tx: ethers.ContractTransaction,
488
- expectedEvent: string,
489
- transform: (event: ethers.Event) => T
490
- ): Promise<T> {
491
- const receipt = await tx.wait();
492
- const event = receipt.events?.find((e) => e.event === expectedEvent);
674
+ private get membersFilter(): ethers.EventFilter {
675
+ if (!this._membersFilter) {
676
+ throw Error("Members filter was not initialized.");
677
+ }
678
+ return this._membersFilter;
679
+ }
493
680
 
494
- if (!event || !event.args) {
495
- throw new TransactionError(`Expected event ${expectedEvent} not found`);
681
+ private get membershipErasedFilter(): ethers.EventFilter {
682
+ if (!this._membershipErasedFilter) {
683
+ throw Error("MembershipErased filter was not initialized.");
496
684
  }
685
+ return this._membershipErasedFilter;
686
+ }
687
+
688
+ private get membersExpiredFilter(): ethers.EventFilter {
689
+ if (!this._membersExpiredFilter) {
690
+ throw Error("MembersExpired filter was not initialized.");
691
+ }
692
+ return this._membersExpiredFilter;
693
+ }
694
+
695
+ private async getMemberIndex(
696
+ idCommitmentBigInt: bigint
697
+ ): Promise<ethers.BigNumber | undefined> {
698
+ try {
699
+ const events = await this.contract.queryFilter(
700
+ this.contract.filters.MembershipRegistered(idCommitmentBigInt)
701
+ );
702
+ if (events.length === 0) return undefined;
497
703
 
498
- return transform(event);
704
+ // Get the most recent registration event
705
+ const event = events[events.length - 1];
706
+ return event.args?.index;
707
+ } catch (error) {
708
+ return undefined;
709
+ }
499
710
  }
500
711
  }