@waku/rln 0.0.2-09108d9.0 → 0.0.2-3670e82.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/index.js +2 -1
  4. package/bundle/packages/rln/dist/contract/abi.js +648 -0
  5. package/bundle/packages/rln/dist/contract/constants.js +25 -65
  6. package/bundle/packages/rln/dist/contract/rln_contract.js +365 -74
  7. package/bundle/packages/rln/dist/message.js +1 -1
  8. package/bundle/packages/rln/dist/rln.js +36 -20
  9. package/bundle/packages/rln/dist/zerokit.js +34 -14
  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/abi.d.ts +46 -0
  21. package/dist/contract/abi.js +647 -0
  22. package/dist/contract/abi.js.map +1 -0
  23. package/dist/contract/constants.d.ts +63 -3
  24. package/dist/contract/constants.js +23 -64
  25. package/dist/contract/constants.js.map +1 -1
  26. package/dist/contract/rln_contract.d.ts +100 -17
  27. package/dist/contract/rln_contract.js +364 -72
  28. package/dist/contract/rln_contract.js.map +1 -1
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.js +3 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/message.js +1 -1
  33. package/dist/message.js.map +1 -1
  34. package/dist/rln.d.ts +5 -1
  35. package/dist/rln.js +36 -19
  36. package/dist/rln.js.map +1 -1
  37. package/dist/zerokit.d.ts +10 -6
  38. package/dist/zerokit.js +34 -14
  39. package/dist/zerokit.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/contract/abi.ts +646 -0
  42. package/src/contract/constants.ts +26 -65
  43. package/src/contract/rln_contract.ts +578 -108
  44. package/src/index.ts +4 -9
  45. package/src/message.ts +1 -1
  46. package/src/rln.ts +57 -21
  47. package/src/zerokit.ts +74 -15
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console */
1
2
  import { Logger } from "@waku/utils";
2
3
  import { hexToBytes } from "@waku/utils/bytes";
3
4
  import { ethers } from "ethers";
@@ -6,9 +7,10 @@ import type { IdentityCredential } from "../identity.js";
6
7
  import type { DecryptedCredentials } from "../keystore/index.js";
7
8
  import type { RLNInstance } from "../rln.js";
8
9
  import { MerkleRootTracker } from "../root_tracker.js";
9
- import { zeroPadLE } from "../utils/index.js";
10
+ import { zeroPadLE } from "../utils/bytes.js";
10
11
 
11
- import { RLN_REGISTRY_ABI, RLN_STORAGE_ABI } from "./constants.js";
12
+ import { RLN_ABI } from "./abi.js";
13
+ import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./constants.js";
12
14
 
13
15
  const log = new Logger("waku:rln:contract");
14
16
 
@@ -17,18 +19,21 @@ type Member = {
17
19
  index: ethers.BigNumber;
18
20
  };
19
21
 
20
- type Signer = ethers.Signer;
21
-
22
- type RLNContractOptions = {
23
- signer: Signer;
24
- registryAddress: string;
25
- };
22
+ interface RLNContractOptions {
23
+ signer: ethers.Signer;
24
+ address: string;
25
+ rateLimit?: number;
26
+ }
26
27
 
27
- type RLNStorageOptions = {
28
- storageIndex?: number;
29
- };
28
+ interface RLNContractInitOptions extends RLNContractOptions {
29
+ contract?: ethers.Contract;
30
+ }
30
31
 
31
- type RLNContractInitOptions = RLNContractOptions & RLNStorageOptions;
32
+ export interface MembershipRegisteredEvent {
33
+ idCommitment: string;
34
+ membershipRateLimit: ethers.BigNumber;
35
+ index: ethers.BigNumber;
36
+ }
32
37
 
33
38
  type FetchMembersOptions = {
34
39
  fromBlock?: number;
@@ -36,80 +41,159 @@ type FetchMembersOptions = {
36
41
  fetchChunks?: number;
37
42
  };
38
43
 
44
+ export interface MembershipInfo {
45
+ index: ethers.BigNumber;
46
+ idCommitment: string;
47
+ rateLimit: number;
48
+ startBlock: number;
49
+ endBlock: number;
50
+ state: MembershipState;
51
+ }
52
+
53
+ export enum MembershipState {
54
+ Active = "Active",
55
+ GracePeriod = "GracePeriod",
56
+ Expired = "Expired",
57
+ ErasedAwaitsWithdrawal = "ErasedAwaitsWithdrawal"
58
+ }
59
+
39
60
  export class RLNContract {
40
- private registryContract: ethers.Contract;
61
+ public contract: ethers.Contract;
41
62
  private merkleRootTracker: MerkleRootTracker;
42
63
 
43
64
  private deployBlock: undefined | number;
44
- private storageIndex: undefined | number;
45
- private storageContract: undefined | ethers.Contract;
46
- private _membersFilter: undefined | ethers.EventFilter;
65
+ private rateLimit: number;
47
66
 
48
67
  private _members: Map<number, Member> = new Map();
49
-
68
+ private _membersFilter: ethers.EventFilter;
69
+ private _membersRemovedFilter: ethers.EventFilter;
70
+ private _membersExpiredFilter: ethers.EventFilter;
71
+
72
+ /**
73
+ * Asynchronous initializer for RLNContract.
74
+ * Allows injecting a mocked contract for testing purposes.
75
+ */
50
76
  public static async init(
51
77
  rlnInstance: RLNInstance,
52
78
  options: RLNContractInitOptions
53
79
  ): Promise<RLNContract> {
54
80
  const rlnContract = new RLNContract(rlnInstance, options);
55
81
 
56
- await rlnContract.initStorageContract(options.signer);
57
82
  await rlnContract.fetchMembers(rlnInstance);
58
83
  rlnContract.subscribeToMembers(rlnInstance);
59
84
 
60
85
  return rlnContract;
61
86
  }
62
87
 
63
- public constructor(
88
+ private constructor(
64
89
  rlnInstance: RLNInstance,
65
- { registryAddress, signer }: RLNContractOptions
90
+ options: RLNContractInitOptions
66
91
  ) {
92
+ const {
93
+ address,
94
+ signer,
95
+ rateLimit = DEFAULT_RATE_LIMIT,
96
+ contract
97
+ } = options;
98
+
99
+ if (
100
+ rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
101
+ rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
102
+ ) {
103
+ throw new Error(
104
+ `Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE} messages per epoch`
105
+ );
106
+ }
107
+
108
+ this.rateLimit = rateLimit;
109
+
67
110
  const initialRoot = rlnInstance.zerokit.getMerkleRoot();
68
111
 
69
- this.registryContract = new ethers.Contract(
70
- registryAddress,
71
- RLN_REGISTRY_ABI,
72
- signer
73
- );
112
+ // Use the injected contract if provided; otherwise, instantiate a new one.
113
+ this.contract = contract || new ethers.Contract(address, RLN_ABI, signer);
74
114
  this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
115
+
116
+ // Initialize event filters
117
+ this._membersFilter = this.contract.filters.MembershipRegistered();
118
+ this._membersRemovedFilter = this.contract.filters.MembershipErased();
119
+ this._membersExpiredFilter = this.contract.filters.MembershipExpired();
75
120
  }
76
121
 
77
- private async initStorageContract(
78
- signer: Signer,
79
- options: RLNStorageOptions = {}
80
- ): Promise<void> {
81
- const storageIndex = options?.storageIndex
82
- ? options.storageIndex
83
- : await this.registryContract.usingStorageIndex();
84
- const storageAddress = await this.registryContract.storages(storageIndex);
122
+ /**
123
+ * Gets the current rate limit for this contract instance
124
+ */
125
+ public getRateLimit(): number {
126
+ return this.rateLimit;
127
+ }
85
128
 
86
- if (!storageAddress || storageAddress === ethers.constants.AddressZero) {
87
- throw Error("No RLN Storage initialized on registry contract.");
88
- }
129
+ /**
130
+ * Gets the contract address
131
+ */
132
+ public get address(): string {
133
+ return this.contract.address;
134
+ }
89
135
 
90
- this.storageIndex = storageIndex;
91
- this.storageContract = new ethers.Contract(
92
- storageAddress,
93
- RLN_STORAGE_ABI,
94
- signer
95
- );
96
- this._membersFilter = this.storageContract.filters.MemberRegistered();
136
+ /**
137
+ * Gets the contract provider
138
+ */
139
+ public get provider(): ethers.providers.Provider {
140
+ return this.contract.provider;
141
+ }
97
142
 
98
- this.deployBlock = await this.storageContract.deployedBlockNumber();
143
+ /**
144
+ * Gets the minimum allowed rate limit from the contract
145
+ * @returns Promise<number> The minimum rate limit in messages per epoch
146
+ */
147
+ public async getMinRateLimit(): Promise<number> {
148
+ const minRate = await this.contract.minMembershipRateLimit();
149
+ return minRate.toNumber();
99
150
  }
100
151
 
101
- public get registry(): ethers.Contract {
102
- if (!this.registryContract) {
103
- throw Error("Registry contract was not initialized");
104
- }
105
- return this.registryContract as ethers.Contract;
152
+ /**
153
+ * Gets the maximum allowed rate limit from the contract
154
+ * @returns Promise<number> The maximum rate limit in messages per epoch
155
+ */
156
+ public async getMaxRateLimit(): Promise<number> {
157
+ const maxRate = await this.contract.maxMembershipRateLimit();
158
+ return maxRate.toNumber();
106
159
  }
107
160
 
108
- public get contract(): ethers.Contract {
109
- if (!this.storageContract) {
110
- throw Error("Storage contract was not initialized");
111
- }
112
- return this.storageContract as ethers.Contract;
161
+ /**
162
+ * Gets the maximum total rate limit across all memberships
163
+ * @returns Promise<number> The maximum total rate limit in messages per epoch
164
+ */
165
+ public async getMaxTotalRateLimit(): Promise<number> {
166
+ const maxTotalRate = await this.contract.maxTotalRateLimit();
167
+ return maxTotalRate.toNumber();
168
+ }
169
+
170
+ /**
171
+ * Gets the current total rate limit usage across all memberships
172
+ * @returns Promise<number> The current total rate limit usage in messages per epoch
173
+ */
174
+ public async getCurrentTotalRateLimit(): Promise<number> {
175
+ const currentTotal = await this.contract.currentTotalRateLimit();
176
+ return currentTotal.toNumber();
177
+ }
178
+
179
+ /**
180
+ * Gets the remaining available total rate limit that can be allocated
181
+ * @returns Promise<number> The remaining rate limit that can be allocated
182
+ */
183
+ public async getRemainingTotalRateLimit(): Promise<number> {
184
+ const [maxTotal, currentTotal] = await Promise.all([
185
+ this.contract.maxTotalRateLimit(),
186
+ this.contract.currentTotalRateLimit()
187
+ ]);
188
+ return maxTotal.sub(currentTotal).toNumber();
189
+ }
190
+
191
+ /**
192
+ * Updates the rate limit for future registrations
193
+ * @param newRateLimit The new rate limit to use
194
+ */
195
+ public async setRateLimit(newRateLimit: number): Promise<void> {
196
+ this.rateLimit = newRateLimit;
113
197
  }
114
198
 
115
199
  public get members(): Member[] {
@@ -123,7 +207,21 @@ export class RLNContract {
123
207
  if (!this._membersFilter) {
124
208
  throw Error("Members filter was not initialized.");
125
209
  }
126
- return this._membersFilter as ethers.EventFilter;
210
+ return this._membersFilter;
211
+ }
212
+
213
+ private get membersRemovedFilter(): ethers.EventFilter {
214
+ if (!this._membersRemovedFilter) {
215
+ throw Error("MembersErased filter was not initialized.");
216
+ }
217
+ return this._membersRemovedFilter;
218
+ }
219
+
220
+ private get membersExpiredFilter(): ethers.EventFilter {
221
+ if (!this._membersExpiredFilter) {
222
+ throw Error("MembersExpired filter was not initialized.");
223
+ }
224
+ return this._membersExpiredFilter;
127
225
  }
128
226
 
129
227
  public async fetchMembers(
@@ -135,7 +233,23 @@ export class RLNContract {
135
233
  ...options,
136
234
  membersFilter: this.membersFilter
137
235
  });
138
- this.processEvents(rlnInstance, registeredMemberEvents);
236
+ const removedMemberEvents = await queryFilter(this.contract, {
237
+ fromBlock: this.deployBlock,
238
+ ...options,
239
+ membersFilter: this.membersRemovedFilter
240
+ });
241
+ const expiredMemberEvents = await queryFilter(this.contract, {
242
+ fromBlock: this.deployBlock,
243
+ ...options,
244
+ membersFilter: this.membersExpiredFilter
245
+ });
246
+
247
+ const events = [
248
+ ...registeredMemberEvents,
249
+ ...removedMemberEvents,
250
+ ...expiredMemberEvents
251
+ ];
252
+ this.processEvents(rlnInstance, events);
139
253
  }
140
254
 
141
255
  public processEvents(rlnInstance: RLNInstance, events: ethers.Event[]): void {
@@ -147,8 +261,22 @@ export class RLNContract {
147
261
  return;
148
262
  }
149
263
 
150
- if (evt.removed) {
151
- const index: ethers.BigNumber = evt.args.index;
264
+ if (
265
+ evt.event === "MembershipErased" ||
266
+ evt.event === "MembershipExpired"
267
+ ) {
268
+ // Both MembershipErased and MembershipExpired events should remove members
269
+ let index = evt.args.index;
270
+
271
+ if (!index) {
272
+ return;
273
+ }
274
+
275
+ // Convert index to ethers.BigNumber if it's not already
276
+ if (typeof index === "number" || typeof index === "string") {
277
+ index = ethers.BigNumber.from(index);
278
+ }
279
+
152
280
  const toRemoveVal = toRemoveTable.get(evt.blockNumber);
153
281
  if (toRemoveVal != undefined) {
154
282
  toRemoveVal.push(index.toNumber());
@@ -156,7 +284,7 @@ export class RLNContract {
156
284
  } else {
157
285
  toRemoveTable.set(evt.blockNumber, [index.toNumber()]);
158
286
  }
159
- } else {
287
+ } else if (evt.event === "MembershipRegistered") {
160
288
  let eventsPerBlock = toInsertTable.get(evt.blockNumber);
161
289
  if (eventsPerBlock == undefined) {
162
290
  eventsPerBlock = [];
@@ -177,18 +305,29 @@ export class RLNContract {
177
305
  ): void {
178
306
  toInsert.forEach((events: ethers.Event[], blockNumber: number) => {
179
307
  events.forEach((evt) => {
180
- const _idCommitment = evt?.args?.idCommitment;
181
- const index: ethers.BigNumber = evt?.args?.index;
308
+ if (!evt.args) return;
309
+
310
+ const _idCommitment = evt.args.idCommitment as string;
311
+ let index = evt.args.index;
182
312
 
313
+ // Ensure index is an ethers.BigNumber
183
314
  if (!_idCommitment || !index) {
184
315
  return;
185
316
  }
186
317
 
187
- const idCommitment = zeroPadLE(hexToBytes(_idCommitment?._hex), 32);
318
+ // Convert index to ethers.BigNumber if it's not already
319
+ if (typeof index === "number" || typeof index === "string") {
320
+ index = ethers.BigNumber.from(index);
321
+ }
322
+
323
+ const idCommitment = zeroPadLE(hexToBytes(_idCommitment), 32);
188
324
  rlnInstance.zerokit.insertMember(idCommitment);
189
- this._members.set(index.toNumber(), {
190
- index,
191
- idCommitment: _idCommitment?._hex
325
+
326
+ // Always store the numeric index as the key, but the BigNumber as the value
327
+ const numericIndex = index.toNumber();
328
+ this._members.set(numericIndex, {
329
+ index, // This is always a BigNumber
330
+ idCommitment: _idCommitment
192
331
  });
193
332
  });
194
333
 
@@ -201,7 +340,7 @@ export class RLNContract {
201
340
  rlnInstance: RLNInstance,
202
341
  toRemove: Map<number, number[]>
203
342
  ): void {
204
- const removeDescending = new Map([...toRemove].sort().reverse());
343
+ const removeDescending = new Map([...toRemove].reverse());
205
344
  removeDescending.forEach((indexes: number[], blockNumber: number) => {
206
345
  indexes.forEach((index) => {
207
346
  if (this._members.has(index)) {
@@ -215,63 +354,385 @@ export class RLNContract {
215
354
  }
216
355
 
217
356
  public subscribeToMembers(rlnInstance: RLNInstance): void {
218
- this.contract.on(this.membersFilter, (_pubkey, _index, event) =>
219
- this.processEvents(rlnInstance, [event])
357
+ this.contract.on(
358
+ this.membersFilter,
359
+ (
360
+ _idCommitment: string,
361
+ _membershipRateLimit: ethers.BigNumber,
362
+ _index: ethers.BigNumber,
363
+ event: ethers.Event
364
+ ) => {
365
+ this.processEvents(rlnInstance, [event]);
366
+ }
367
+ );
368
+
369
+ this.contract.on(
370
+ this.membersRemovedFilter,
371
+ (
372
+ _idCommitment: string,
373
+ _membershipRateLimit: ethers.BigNumber,
374
+ _index: ethers.BigNumber,
375
+ event: ethers.Event
376
+ ) => {
377
+ this.processEvents(rlnInstance, [event]);
378
+ }
379
+ );
380
+
381
+ this.contract.on(
382
+ this.membersExpiredFilter,
383
+ (
384
+ _idCommitment: string,
385
+ _membershipRateLimit: ethers.BigNumber,
386
+ _index: ethers.BigNumber,
387
+ event: ethers.Event
388
+ ) => {
389
+ this.processEvents(rlnInstance, [event]);
390
+ }
220
391
  );
221
392
  }
222
393
 
223
394
  public async registerWithIdentity(
224
395
  identity: IdentityCredential
225
396
  ): Promise<DecryptedCredentials | undefined> {
226
- if (this.storageIndex === undefined) {
227
- throw Error(
228
- "Cannot register credential, no storage contract index found."
397
+ try {
398
+ console.log("registerWithIdentity - starting registration process");
399
+ console.log("registerWithIdentity - identity:", identity);
400
+ console.log(
401
+ "registerWithIdentity - IDCommitmentBigInt:",
402
+ identity.IDCommitmentBigInt.toString()
229
403
  );
230
- }
231
- const txRegisterResponse: ethers.ContractTransaction =
232
- await this.registryContract["register(uint16,uint256)"](
233
- this.storageIndex,
234
- identity.IDCommitmentBigInt,
235
- { gasLimit: 100000 }
404
+ console.log("registerWithIdentity - rate limit:", this.rateLimit);
405
+
406
+ log.info(
407
+ `Registering identity with rate limit: ${this.rateLimit} messages/epoch`
408
+ );
409
+
410
+ console.log("registerWithIdentity - calling contract.register");
411
+ const txRegisterResponse: ethers.ContractTransaction =
412
+ await this.contract.register(
413
+ identity.IDCommitmentBigInt,
414
+ this.rateLimit,
415
+ [],
416
+ { gasLimit: 300000 }
417
+ );
418
+ console.log(
419
+ "registerWithIdentity - txRegisterResponse:",
420
+ txRegisterResponse
421
+ );
422
+ console.log("registerWithIdentity - hash:", txRegisterResponse.hash);
423
+ console.log(
424
+ "registerWithIdentity - waiting for transaction confirmation..."
236
425
  );
237
- const txRegisterReceipt = await txRegisterResponse.wait();
238
426
 
239
- // assumption: register(uint16,uint256) emits one event
240
- const memberRegistered = txRegisterReceipt?.events?.[0];
427
+ const txRegisterReceipt = await txRegisterResponse.wait();
428
+ console.log(
429
+ "registerWithIdentity - txRegisterReceipt:",
430
+ txRegisterReceipt
431
+ );
432
+ console.log(
433
+ "registerWithIdentity - transaction status:",
434
+ txRegisterReceipt.status
435
+ );
436
+ console.log(
437
+ "registerWithIdentity - block number:",
438
+ txRegisterReceipt.blockNumber
439
+ );
440
+ console.log(
441
+ "registerWithIdentity - gas used:",
442
+ txRegisterReceipt.gasUsed.toString()
443
+ );
444
+
445
+ const memberRegistered = txRegisterReceipt.events?.find(
446
+ (event) => event.event === "MembershipRegistered"
447
+ );
448
+ console.log(
449
+ "registerWithIdentity - memberRegistered event:",
450
+ memberRegistered
451
+ );
452
+
453
+ if (!memberRegistered || !memberRegistered.args) {
454
+ console.log(
455
+ "registerWithIdentity - ERROR: no memberRegistered event found"
456
+ );
457
+ console.log(
458
+ "registerWithIdentity - all events:",
459
+ txRegisterReceipt.events
460
+ );
461
+ log.error(
462
+ "Failed to register membership: No MembershipRegistered event found"
463
+ );
464
+ return undefined;
465
+ }
466
+
467
+ console.log(
468
+ "registerWithIdentity - memberRegistered args:",
469
+ memberRegistered.args
470
+ );
471
+ const decodedData: MembershipRegisteredEvent = {
472
+ idCommitment: memberRegistered.args.idCommitment,
473
+ membershipRateLimit: memberRegistered.args.membershipRateLimit,
474
+ index: memberRegistered.args.index
475
+ };
476
+ console.log("registerWithIdentity - decodedData:", decodedData);
477
+ console.log(
478
+ "registerWithIdentity - index:",
479
+ decodedData.index.toString()
480
+ );
481
+ console.log(
482
+ "registerWithIdentity - membershipRateLimit:",
483
+ decodedData.membershipRateLimit.toString()
484
+ );
485
+
486
+ log.info(
487
+ `Successfully registered membership with index ${decodedData.index} ` +
488
+ `and rate limit ${decodedData.membershipRateLimit}`
489
+ );
490
+
491
+ console.log("registerWithIdentity - getting network information");
492
+ const network = await this.contract.provider.getNetwork();
493
+ console.log("registerWithIdentity - network:", network);
494
+ console.log("registerWithIdentity - chainId:", network.chainId);
495
+
496
+ const address = this.contract.address;
497
+ console.log("registerWithIdentity - contract address:", address);
498
+
499
+ const membershipId = decodedData.index.toNumber();
500
+ console.log("registerWithIdentity - membershipId:", membershipId);
241
501
 
242
- if (!memberRegistered) {
502
+ const result = {
503
+ identity,
504
+ membership: {
505
+ address,
506
+ treeIndex: membershipId,
507
+ chainId: network.chainId
508
+ }
509
+ };
510
+ console.log("registerWithIdentity - returning result:", result);
511
+
512
+ return result;
513
+ } catch (error) {
514
+ console.log("registerWithIdentity - ERROR:", error);
515
+ console.log(
516
+ "registerWithIdentity - error message:",
517
+ (error as Error).message
518
+ );
519
+ console.log(
520
+ "registerWithIdentity - error stack:",
521
+ (error as Error).stack
522
+ );
523
+ log.error(`Error in registerWithIdentity: ${(error as Error).message}`);
243
524
  return undefined;
244
525
  }
526
+ }
245
527
 
246
- const decodedData = this.contract.interface.decodeEventLog(
247
- "MemberRegistered",
248
- memberRegistered.data
249
- );
528
+ /**
529
+ * Helper method to get remaining messages in current epoch
530
+ * @param membershipId The ID of the membership to check
531
+ * @returns number of remaining messages allowed in current epoch
532
+ */
533
+ public async getRemainingMessages(membershipId: number): Promise<number> {
534
+ try {
535
+ const [startTime, , rateLimit] =
536
+ await this.contract.getMembershipInfo(membershipId);
537
+
538
+ // Calculate current epoch
539
+ const currentTime = Math.floor(Date.now() / 1000);
540
+ const epochsPassed = Math.floor(
541
+ (currentTime - startTime) / RATE_LIMIT_PARAMS.EPOCH_LENGTH
542
+ );
543
+ const currentEpochStart =
544
+ startTime + epochsPassed * RATE_LIMIT_PARAMS.EPOCH_LENGTH;
545
+
546
+ // Get message count in current epoch using contract's function
547
+ const messageCount = await this.contract.getMessageCount(
548
+ membershipId,
549
+ currentEpochStart
550
+ );
551
+ return Math.max(0, rateLimit.sub(messageCount).toNumber());
552
+ } catch (error) {
553
+ log.error(
554
+ `Error getting remaining messages: ${(error as Error).message}`
555
+ );
556
+ return 0; // Fail safe: assume no messages remaining on error
557
+ }
558
+ }
250
559
 
251
- const network = await this.registryContract.provider.getNetwork();
252
- const address = this.registryContract.address;
253
- const membershipId = decodedData.index.toNumber();
560
+ public async registerWithPermitAndErase(
561
+ identity: IdentityCredential,
562
+ permit: {
563
+ owner: string;
564
+ deadline: number;
565
+ v: number;
566
+ r: string;
567
+ s: string;
568
+ },
569
+ idCommitmentsToErase: string[]
570
+ ): Promise<DecryptedCredentials | undefined> {
571
+ try {
572
+ log.info(
573
+ `Registering identity with permit and rate limit: ${this.rateLimit} messages/epoch`
574
+ );
254
575
 
255
- return {
256
- identity,
257
- membership: {
258
- address,
259
- treeIndex: membershipId,
260
- chainId: network.chainId
576
+ const txRegisterResponse: ethers.ContractTransaction =
577
+ await this.contract.registerWithPermit(
578
+ permit.owner,
579
+ permit.deadline,
580
+ permit.v,
581
+ permit.r,
582
+ permit.s,
583
+ identity.IDCommitmentBigInt,
584
+ this.rateLimit,
585
+ idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
586
+ );
587
+ const txRegisterReceipt = await txRegisterResponse.wait();
588
+
589
+ const memberRegistered = txRegisterReceipt.events?.find(
590
+ (event) => event.event === "MembershipRegistered"
591
+ );
592
+
593
+ if (!memberRegistered || !memberRegistered.args) {
594
+ log.error(
595
+ "Failed to register membership with permit: No MembershipRegistered event found"
596
+ );
597
+ return undefined;
261
598
  }
262
- };
599
+
600
+ const decodedData: MembershipRegisteredEvent = {
601
+ idCommitment: memberRegistered.args.idCommitment,
602
+ membershipRateLimit: memberRegistered.args.membershipRateLimit,
603
+ index: memberRegistered.args.index
604
+ };
605
+
606
+ log.info(
607
+ `Successfully registered membership with permit. Index: ${decodedData.index}, ` +
608
+ `Rate limit: ${decodedData.membershipRateLimit}, Erased ${idCommitmentsToErase.length} commitments`
609
+ );
610
+
611
+ const network = await this.contract.provider.getNetwork();
612
+ const address = this.contract.address;
613
+ const membershipId = decodedData.index.toNumber();
614
+
615
+ return {
616
+ identity,
617
+ membership: {
618
+ address,
619
+ treeIndex: membershipId,
620
+ chainId: network.chainId
621
+ }
622
+ };
623
+ } catch (error) {
624
+ log.error(
625
+ `Error in registerWithPermitAndErase: ${(error as Error).message}`
626
+ );
627
+ return undefined;
628
+ }
263
629
  }
264
630
 
265
631
  public roots(): Uint8Array[] {
266
632
  return this.merkleRootTracker.roots();
267
633
  }
634
+
635
+ public async withdraw(token: string, holder: string): Promise<void> {
636
+ try {
637
+ const tx = await this.contract.withdraw(token, { from: holder });
638
+ await tx.wait();
639
+ } catch (error) {
640
+ log.error(`Error in withdraw: ${(error as Error).message}`);
641
+ }
642
+ }
643
+
644
+ public async getMembershipInfo(
645
+ idCommitment: string
646
+ ): Promise<MembershipInfo | undefined> {
647
+ try {
648
+ const [startBlock, endBlock, rateLimit] =
649
+ await this.contract.getMembershipInfo(idCommitment);
650
+ const currentBlock = await this.contract.provider.getBlockNumber();
651
+
652
+ let state: MembershipState;
653
+ if (currentBlock < startBlock) {
654
+ state = MembershipState.Active;
655
+ } else if (currentBlock < endBlock) {
656
+ state = MembershipState.GracePeriod;
657
+ } else {
658
+ state = MembershipState.Expired;
659
+ }
660
+
661
+ const index = await this.getMemberIndex(idCommitment);
662
+ if (!index) return undefined;
663
+
664
+ return {
665
+ index,
666
+ idCommitment,
667
+ rateLimit: rateLimit.toNumber(),
668
+ startBlock: startBlock.toNumber(),
669
+ endBlock: endBlock.toNumber(),
670
+ state
671
+ };
672
+ } catch (error) {
673
+ return undefined;
674
+ }
675
+ }
676
+
677
+ public async extendMembership(
678
+ idCommitment: string
679
+ ): Promise<ethers.ContractReceipt> {
680
+ const tx = await this.contract.extendMemberships([idCommitment]);
681
+ return await tx.wait();
682
+ }
683
+
684
+ public async eraseMembership(
685
+ idCommitment: string,
686
+ eraseFromMembershipSet: boolean = true
687
+ ): Promise<ethers.ContractReceipt> {
688
+ const tx = await this.contract.eraseMemberships(
689
+ [idCommitment],
690
+ eraseFromMembershipSet
691
+ );
692
+ return await tx.wait();
693
+ }
694
+
695
+ public async registerMembership(
696
+ idCommitment: string,
697
+ rateLimit: number = DEFAULT_RATE_LIMIT
698
+ ): Promise<ethers.ContractTransaction> {
699
+ if (
700
+ rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
701
+ rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
702
+ ) {
703
+ throw new Error(
704
+ `Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
705
+ );
706
+ }
707
+ console.log("registering membership", idCommitment, rateLimit);
708
+ const txn = this.contract.register(idCommitment, rateLimit, []);
709
+ console.log("txn", txn);
710
+ return txn;
711
+ }
712
+
713
+ private async getMemberIndex(
714
+ idCommitment: string
715
+ ): Promise<ethers.BigNumber | undefined> {
716
+ try {
717
+ const events = await this.contract.queryFilter(
718
+ this.contract.filters.MembershipRegistered(idCommitment)
719
+ );
720
+ if (events.length === 0) return undefined;
721
+
722
+ // Get the most recent registration event
723
+ const event = events[events.length - 1];
724
+ return event.args?.index;
725
+ } catch (error) {
726
+ return undefined;
727
+ }
728
+ }
268
729
  }
269
730
 
270
- type CustomQueryOptions = FetchMembersOptions & {
731
+ interface CustomQueryOptions extends FetchMembersOptions {
271
732
  membersFilter: ethers.EventFilter;
272
- };
733
+ }
273
734
 
274
- // these value should be tested on other networks
735
+ // These values should be tested on other networks
275
736
  const FETCH_CHUNK = 5;
276
737
  const BLOCK_RANGE = 3000;
277
738
 
@@ -286,18 +747,18 @@ async function queryFilter(
286
747
  fetchChunks = FETCH_CHUNK
287
748
  } = options;
288
749
 
289
- if (!fromBlock) {
750
+ if (fromBlock === undefined) {
290
751
  return contract.queryFilter(membersFilter);
291
752
  }
292
753
 
293
- if (!contract.signer.provider) {
294
- throw Error("No provider found on the contract's signer.");
754
+ if (!contract.provider) {
755
+ throw Error("No provider found on the contract.");
295
756
  }
296
757
 
297
- const toBlock = await contract.signer.provider.getBlockNumber();
758
+ const toBlock = await contract.provider.getBlockNumber();
298
759
 
299
760
  if (toBlock - fromBlock < fetchRange) {
300
- return contract.queryFilter(membersFilter);
761
+ return contract.queryFilter(membersFilter, fromBlock, toBlock);
301
762
  }
302
763
 
303
764
  const events: ethers.Event[][] = [];
@@ -319,7 +780,7 @@ function splitToChunks(
319
780
  to: number,
320
781
  step: number
321
782
  ): Array<[number, number]> {
322
- const chunks = [];
783
+ const chunks: Array<[number, number]> = [];
323
784
 
324
785
  let left = from;
325
786
  while (left < to) {
@@ -345,9 +806,18 @@ function* takeN<T>(array: T[], size: number): Iterable<T[]> {
345
806
  }
346
807
  }
347
808
 
348
- function ignoreErrors<T>(promise: Promise<T>, defaultValue: T): Promise<T> {
349
- return promise.catch((err) => {
350
- log.info(`Ignoring an error during query: ${err?.message}`);
809
+ async function ignoreErrors<T>(
810
+ promise: Promise<T>,
811
+ defaultValue: T
812
+ ): Promise<T> {
813
+ try {
814
+ return await promise;
815
+ } catch (err: unknown) {
816
+ if (err instanceof Error) {
817
+ log.info(`Ignoring an error during query: ${err.message}`);
818
+ } else {
819
+ log.info(`Ignoring an unknown error during query`);
820
+ }
351
821
  return defaultValue;
352
- });
822
+ }
353
823
  }