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