@0xbow/privacy-pools-core-sdk 0.0.0-3dpf8pk

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 (117) hide show
  1. package/README.md +73 -0
  2. package/dist/esm/fetchArtifacts.esm-DTr__iP-.js +18 -0
  3. package/dist/esm/fetchArtifacts.esm-DTr__iP-.js.map +1 -0
  4. package/dist/esm/fetchArtifacts.node-BLw8nwbt.js +31 -0
  5. package/dist/esm/fetchArtifacts.node-BLw8nwbt.js.map +1 -0
  6. package/dist/esm/index-Derz3sX9.js +73457 -0
  7. package/dist/esm/index-Derz3sX9.js.map +1 -0
  8. package/dist/esm/index.mjs +7 -0
  9. package/dist/esm/index.mjs.map +1 -0
  10. package/dist/index.d.mts +1326 -0
  11. package/dist/node/fetchArtifacts.esm-ZwE-hqnx.js +35 -0
  12. package/dist/node/fetchArtifacts.esm-ZwE-hqnx.js.map +1 -0
  13. package/dist/node/fetchArtifacts.node-CY8wLnXd.js +48 -0
  14. package/dist/node/fetchArtifacts.node-CY8wLnXd.js.map +1 -0
  15. package/dist/node/index-B804ILXn.js +80507 -0
  16. package/dist/node/index-B804ILXn.js.map +1 -0
  17. package/dist/node/index.mjs +24 -0
  18. package/dist/node/index.mjs.map +1 -0
  19. package/dist/types/abi/ERC20.d.ts +38 -0
  20. package/dist/types/abi/IEntrypoint.d.ts +823 -0
  21. package/dist/types/abi/IPrivacyPool.d.ts +51 -0
  22. package/dist/types/circuits/circuits.impl.d.ts +120 -0
  23. package/dist/types/circuits/circuits.interface.d.ts +129 -0
  24. package/dist/types/circuits/fetchArtifacts.d.ts +1 -0
  25. package/dist/types/circuits/fetchArtifacts.esm.d.ts +1 -0
  26. package/dist/types/circuits/fetchArtifacts.node.d.ts +1 -0
  27. package/dist/types/circuits/index.d.ts +2 -0
  28. package/dist/types/constants.d.ts +2 -0
  29. package/dist/types/core/account.service.d.ts +355 -0
  30. package/dist/types/core/bruteForce.service.d.ts +61 -0
  31. package/dist/types/core/commitment.service.d.ts +30 -0
  32. package/dist/types/core/contracts.service.d.ts +114 -0
  33. package/dist/types/core/data.service.d.ts +72 -0
  34. package/dist/types/core/sdk.d.ts +45 -0
  35. package/dist/types/core/withdrawal.service.d.ts +32 -0
  36. package/dist/types/crypto.d.ts +67 -0
  37. package/dist/types/dirname.helper.d.ts +2 -0
  38. package/dist/types/errors/account.error.d.ts +10 -0
  39. package/dist/types/errors/base.error.d.ts +53 -0
  40. package/dist/types/errors/data.error.d.ts +7 -0
  41. package/dist/types/errors/events.error.d.ts +9 -0
  42. package/dist/types/exceptions/circuitInitialization.exception.d.ts +3 -0
  43. package/dist/types/exceptions/fetchArtifacts.exception.d.ts +3 -0
  44. package/dist/types/exceptions/index.d.ts +4 -0
  45. package/dist/types/exceptions/invalidRpcUrl.exception.d.ts +3 -0
  46. package/dist/types/exceptions/privacyPool.exception.d.ts +13 -0
  47. package/dist/types/external.d.ts +7 -0
  48. package/dist/types/fetchArtifacts.esm-m-j0Hu4b.js +34 -0
  49. package/dist/types/fetchArtifacts.node-CGJMDWIh.js +47 -0
  50. package/dist/types/filename.helper.d.ts +2 -0
  51. package/dist/types/index-B6kM6ceO.js +80520 -0
  52. package/dist/types/index.d.ts +14 -0
  53. package/dist/types/index.js +23 -0
  54. package/dist/types/interfaces/blockchainProvider.interface.d.ts +12 -0
  55. package/dist/types/interfaces/circuits.interface.d.ts +30 -0
  56. package/dist/types/interfaces/contracts.interface.d.ts +35 -0
  57. package/dist/types/interfaces/index.d.ts +1 -0
  58. package/dist/types/internal.d.ts +6 -0
  59. package/dist/types/keys.d.ts +18 -0
  60. package/dist/types/providers/blockchainProvider.d.ts +8 -0
  61. package/dist/types/providers/index.d.ts +1 -0
  62. package/dist/types/types/account.d.ts +31 -0
  63. package/dist/types/types/commitment.d.ts +48 -0
  64. package/dist/types/types/events.d.ts +72 -0
  65. package/dist/types/types/index.d.ts +4 -0
  66. package/dist/types/types/keys.d.ts +5 -0
  67. package/dist/types/types/rateLimit.d.ts +51 -0
  68. package/dist/types/types/withdrawal.d.ts +30 -0
  69. package/dist/types/utils/logger.d.ts +22 -0
  70. package/package.json +84 -0
  71. package/src/abi/ERC20.ts +222 -0
  72. package/src/abi/IEntrypoint.ts +1059 -0
  73. package/src/abi/IPrivacyPool.ts +576 -0
  74. package/src/circuits/circuits.impl.ts +232 -0
  75. package/src/circuits/circuits.interface.ts +166 -0
  76. package/src/circuits/fetchArtifacts.esm.ts +12 -0
  77. package/src/circuits/fetchArtifacts.node.ts +23 -0
  78. package/src/circuits/fetchArtifacts.ts +7 -0
  79. package/src/circuits/index.ts +2 -0
  80. package/src/constants.ts +3 -0
  81. package/src/core/account.service.ts +1343 -0
  82. package/src/core/bruteForce.service.ts +120 -0
  83. package/src/core/commitment.service.ts +84 -0
  84. package/src/core/contracts.service.ts +442 -0
  85. package/src/core/data.service.ts +608 -0
  86. package/src/core/sdk.ts +92 -0
  87. package/src/core/withdrawal.service.ts +126 -0
  88. package/src/crypto.ts +226 -0
  89. package/src/dirname.helper.ts +4 -0
  90. package/src/errors/account.error.ts +49 -0
  91. package/src/errors/base.error.ts +125 -0
  92. package/src/errors/data.error.ts +34 -0
  93. package/src/errors/events.error.ts +38 -0
  94. package/src/exceptions/circuitInitialization.exception.ts +6 -0
  95. package/src/exceptions/fetchArtifacts.exception.ts +7 -0
  96. package/src/exceptions/index.ts +4 -0
  97. package/src/exceptions/invalidRpcUrl.exception.ts +6 -0
  98. package/src/exceptions/privacyPool.exception.ts +19 -0
  99. package/src/external.ts +13 -0
  100. package/src/filename.helper.ts +4 -0
  101. package/src/index.ts +25 -0
  102. package/src/interfaces/blockchainProvider.interface.ts +13 -0
  103. package/src/interfaces/circuits.interface.ts +34 -0
  104. package/src/interfaces/contracts.interface.ts +66 -0
  105. package/src/interfaces/index.ts +1 -0
  106. package/src/internal.ts +6 -0
  107. package/src/keys.ts +42 -0
  108. package/src/providers/blockchainProvider.ts +26 -0
  109. package/src/providers/index.ts +1 -0
  110. package/src/types/account.ts +35 -0
  111. package/src/types/commitment.ts +50 -0
  112. package/src/types/events.ts +82 -0
  113. package/src/types/index.ts +4 -0
  114. package/src/types/keys.ts +6 -0
  115. package/src/types/rateLimit.ts +66 -0
  116. package/src/types/withdrawal.ts +33 -0
  117. package/src/utils/logger.ts +56 -0
@@ -0,0 +1,1343 @@
1
+ import { poseidon } from "maci-crypto/build/ts/hashing.js";
2
+ import { Hash, Secret } from "../types/commitment.js";
3
+ import { Hex, bytesToNumber } from "viem";
4
+ import { mnemonicToAccount } from "viem/accounts";
5
+ import { mapLimit } from "async";
6
+ import { DataService } from "./data.service.js";
7
+ import {
8
+ AccountCommitment,
9
+ PoolAccount,
10
+ PoolInfo,
11
+ PrivacyPoolAccount,
12
+ } from "../types/account.js";
13
+ import {
14
+ DepositEvent,
15
+ PoolEventsError,
16
+ PoolEventsResult,
17
+ RagequitEvent,
18
+ WithdrawalEvent,
19
+ } from "../types/events.js";
20
+
21
+ import { Logger, LogLevel } from "../utils/logger.js";
22
+ import { AccountError } from "../errors/account.error.js";
23
+ import { ErrorCode } from "../errors/base.error.js";
24
+ import { EventError } from "../errors/events.error.js";
25
+
26
+ type AccountServiceConfig =
27
+ | {
28
+ mnemonic: string;
29
+ poolConcurrency?: number;
30
+ }
31
+ | {
32
+ account: PrivacyPoolAccount;
33
+ poolConcurrency?: number;
34
+ };
35
+
36
+ /**
37
+ * Service responsible for managing privacy pool accounts and their associated commitments.
38
+ * Handles account initialization, deposit/withdrawal tracking, and history synchronization.
39
+ *
40
+ * @remarks
41
+ * This service maintains the state of all pool accounts and their commitments across different
42
+ * chains and scopes. It uses deterministic key generation to recover account state from a mnemonic.
43
+ */
44
+ export class AccountService {
45
+ account: PrivacyPoolAccount;
46
+ private readonly logger: Logger;
47
+ private readonly poolConcurrency: number;
48
+
49
+ /**
50
+ * Creates a new AccountService instance.
51
+ *
52
+ * @param dataService - Service for fetching on-chain events
53
+ * @param config - Configuration for the account service (either mnemonic or existing account)
54
+ * @param config.mnemonic - Optional mnemonic for deterministic key generation
55
+ * @param config.account - Optional existing account to initialize with
56
+ * @param config.poolConcurrency - Optional maximum number of pools to fetch events for concurrently (default: 2)
57
+ *
58
+ * @throws {AccountError} If account initialization fails
59
+ */
60
+ constructor(
61
+ private readonly dataService: DataService,
62
+ config: AccountServiceConfig
63
+ ) {
64
+ this.logger = new Logger({ prefix: "Account", level: LogLevel.DEBUG });
65
+ this.poolConcurrency = config.poolConcurrency ?? 2;
66
+ if ("mnemonic" in config) {
67
+ this.account = this._initializeAccount(config.mnemonic);
68
+ } else {
69
+ this.account = config.account;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Initializes a new account from a mnemonic phrase.
75
+ *
76
+ * @param mnemonic - The mnemonic phrase to derive keys from
77
+ * @returns A new PrivacyPoolAccount with derived master keys
78
+ *
79
+ * @remarks
80
+ * This method derives two master keys from the mnemonic:
81
+ * 1. A master nullifier key from account index 0
82
+ * 2. A master secret key from account index 1
83
+ * These keys are used to deterministically generate nullifiers and secrets for deposits and withdrawals.
84
+ *
85
+ * @throws {AccountError} If account initialization fails
86
+ * @private
87
+ */
88
+ private _initializeAccount(mnemonic: string): PrivacyPoolAccount {
89
+ try {
90
+ this.logger.debug("Initializing account with mnemonic");
91
+
92
+ const masterNullifierSeed = bytesToNumber(
93
+ mnemonicToAccount(mnemonic, { accountIndex: 0 }).getHdKey().privateKey!
94
+ );
95
+
96
+ const masterSecretSeed = bytesToNumber(
97
+ mnemonicToAccount(mnemonic, { accountIndex: 1 }).getHdKey().privateKey!
98
+ );
99
+
100
+ const masterNullifier = poseidon([BigInt(masterNullifierSeed)]) as Secret;
101
+ const masterSecret = poseidon([BigInt(masterSecretSeed)]) as Secret;
102
+
103
+ return {
104
+ masterKeys: [masterNullifier, masterSecret],
105
+ poolAccounts: new Map(),
106
+ creationTimestamp: 0n,
107
+ lastUpdateTimestamp: 0n,
108
+ };
109
+ } catch (error) {
110
+ throw AccountError.accountInitializationFailed(
111
+ error instanceof Error ? error.message : "Unknown error"
112
+ );
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Generates a deterministic nullifier for a deposit.
118
+ *
119
+ * @param scope - The scope of the pool
120
+ * @param index - The index of the deposit
121
+ * @returns A deterministic nullifier for the deposit
122
+ * @private
123
+ */
124
+ private _genDepositNullifier(scope: Hash, index: bigint): Secret {
125
+ const [masterNullifier] = this.account.masterKeys;
126
+ return poseidon([masterNullifier, scope, index]) as Secret;
127
+ }
128
+
129
+ /**
130
+ * Generates a deterministic secret for a deposit.
131
+ *
132
+ * @param scope - The scope of the pool
133
+ * @param index - The index of the deposit
134
+ * @returns A deterministic secret for the deposit
135
+ * @private
136
+ */
137
+ private _genDepositSecret(scope: Hash, index: bigint): Secret {
138
+ const [, masterSecret] = this.account.masterKeys;
139
+ return poseidon([masterSecret, scope, index]) as Secret;
140
+ }
141
+
142
+ /**
143
+ * Generates a deterministic nullifier for a withdrawal.
144
+ *
145
+ * @param label - The label of the commitment
146
+ * @param index - The index of the withdrawal
147
+ * @returns A deterministic nullifier for the withdrawal
148
+ * @private
149
+ */
150
+ private _genWithdrawalNullifier(label: Hash, index: bigint): Secret {
151
+ const [masterNullifier] = this.account.masterKeys;
152
+ return poseidon([masterNullifier, label, index]) as Secret;
153
+ }
154
+
155
+ /**
156
+ * Generates a deterministic secret for a withdrawal.
157
+ *
158
+ * @param label - The label of the commitment
159
+ * @param index - The index of the withdrawal
160
+ * @returns A deterministic secret for the withdrawal
161
+ * @private
162
+ */
163
+ private _genWithdrawalSecret(label: Hash, index: bigint): Secret {
164
+ const [, masterSecret] = this.account.masterKeys;
165
+ return poseidon([masterSecret, label, index]) as Secret;
166
+ }
167
+
168
+ /**
169
+ * Hashes a commitment using the Poseidon hash function.
170
+ *
171
+ * @param value - The value of the commitment
172
+ * @param label - The label of the commitment
173
+ * @param precommitment - The precommitment hash
174
+ * @returns The commitment hash
175
+ * @private
176
+ */
177
+ private _hashCommitment(
178
+ value: bigint,
179
+ label: Hash,
180
+ precommitment: Hash
181
+ ): Hash {
182
+ return poseidon([value, label, precommitment]) as Hash;
183
+ }
184
+
185
+ /**
186
+ * Hashes a precommitment using the Poseidon hash function.
187
+ *
188
+ * @param nullifier - The nullifier for the commitment
189
+ * @param secret - The secret for the commitment
190
+ * @returns The precommitment hash
191
+ * @private
192
+ */
193
+ private _hashPrecommitment(nullifier: Secret, secret: Secret): Hash {
194
+ return poseidon([nullifier, secret]) as Hash;
195
+ }
196
+
197
+ /**
198
+ * Gets all spendable commitments across all pools.
199
+ *
200
+ * @returns A map of scope to array of spendable commitments
201
+ *
202
+ * @remarks
203
+ * A commitment is considered spendable if:
204
+ * 1. It has a non-zero value
205
+ * 2. The account it belongs to has not been ragequit
206
+ */
207
+ public getSpendableCommitments(): Map<bigint, AccountCommitment[]> {
208
+ const result = new Map<bigint, AccountCommitment[]>();
209
+
210
+ for (const [scope, accounts] of this.account.poolAccounts.entries()) {
211
+ const nonZeroCommitments: AccountCommitment[] = [];
212
+
213
+ for (const account of accounts) {
214
+ // Skip accounts that have been ragequit
215
+ if (account.ragequit) {
216
+ continue;
217
+ }
218
+
219
+ const lastCommitment =
220
+ account.children.length > 0
221
+ ? account.children[account.children.length - 1]
222
+ : account.deposit;
223
+
224
+ if (lastCommitment!.value !== BigInt(0)) {
225
+ nonZeroCommitments.push(lastCommitment!);
226
+ }
227
+ }
228
+
229
+ if (nonZeroCommitments.length > 0) {
230
+ result.set(scope, nonZeroCommitments);
231
+ }
232
+ }
233
+ return result;
234
+ }
235
+
236
+ /**
237
+ * Creates nullifier and secret for a new deposit
238
+ *
239
+ * @param scope - The scope of the pool to deposit into
240
+ * @param index - Optional index for deterministic generation
241
+ * @returns The nullifier, secret, and precommitment for the deposit
242
+ *
243
+ * @remarks
244
+ * If no index is provided, it uses the current number of accounts for the scope.
245
+ * The precommitment is a hash of the nullifier and secret, used in the deposit process.
246
+ */
247
+ public createDepositSecrets(
248
+ scope: Hash,
249
+ index?: bigint
250
+ ): {
251
+ nullifier: Secret;
252
+ secret: Secret;
253
+ precommitment: Hash;
254
+ } {
255
+ if (index && index < 0n) {
256
+ throw AccountError.invalidIndex(index);
257
+ }
258
+
259
+ const accounts = this.account.poolAccounts.get(scope);
260
+ index = index ?? BigInt(accounts?.length || 0);
261
+
262
+ const nullifier = this._genDepositNullifier(scope, index);
263
+ const secret = this._genDepositSecret(scope, index);
264
+ const precommitment = this._hashPrecommitment(nullifier, secret);
265
+
266
+ return { nullifier, secret, precommitment };
267
+ }
268
+
269
+ /**
270
+ * Creates nullifier and secret for spending a commitment
271
+ *
272
+ * @param commitment - The commitment to spend
273
+ * @returns The nullifier and secret for the new commitment
274
+ *
275
+ * @remarks
276
+ * The index used for generating the withdrawal nullifier and secret is based on
277
+ * the number of children the account already has, ensuring each withdrawal has
278
+ * a unique nullifier.
279
+ *
280
+ * @throws {AccountError} If no account is found for the commitment
281
+ */
282
+ public createWithdrawalSecrets(commitment: AccountCommitment): {
283
+ nullifier: Secret;
284
+ secret: Secret;
285
+ } {
286
+ let index: bigint | undefined;
287
+
288
+ for (const accounts of this.account.poolAccounts.values()) {
289
+ const account = accounts.find((acc) => acc.label === commitment.label);
290
+ if (account) {
291
+ index = BigInt(account.children.length);
292
+ break;
293
+ }
294
+ }
295
+
296
+ if (index === undefined) {
297
+ throw AccountError.commitmentNotFound(commitment.label);
298
+ }
299
+
300
+ const nullifier = this._genWithdrawalNullifier(commitment.label, index);
301
+ const secret = this._genWithdrawalSecret(commitment.label, index);
302
+
303
+ return { nullifier, secret };
304
+ }
305
+
306
+ /**
307
+ * Adds a new pool account after depositing
308
+ *
309
+ * @param scope - The scope of the pool
310
+ * @param value - The deposit value
311
+ * @param nullifier - The nullifier used for the deposit
312
+ * @param secret - The secret used for the deposit
313
+ * @param label - The label for the commitment
314
+ * @param blockNumber - The block number of the deposit
315
+ * @param txHash - The transaction hash of the deposit
316
+ * @returns The new pool account
317
+ *
318
+ * @remarks
319
+ * This method creates a new account with the deposit commitment and adds it to the
320
+ * pool accounts map under the specified scope. The commitment hash is calculated
321
+ * from the value, label, and precommitment.
322
+ */
323
+ public addPoolAccount(
324
+ scope: Hash,
325
+ value: bigint,
326
+ nullifier: Secret,
327
+ secret: Secret,
328
+ label: Hash,
329
+ blockNumber: bigint,
330
+ txHash: Hex
331
+ ): PoolAccount {
332
+ const precommitment = this._hashPrecommitment(nullifier, secret);
333
+ const commitment = this._hashCommitment(value, label, precommitment);
334
+
335
+ const newAccount: PoolAccount = {
336
+ label,
337
+ deposit: {
338
+ hash: commitment,
339
+ value,
340
+ label,
341
+ nullifier,
342
+ secret,
343
+ blockNumber,
344
+ txHash,
345
+ },
346
+ children: [],
347
+ };
348
+
349
+ if (!this.account.poolAccounts.has(scope)) {
350
+ this.account.poolAccounts.set(scope, []);
351
+ }
352
+
353
+ this.account.poolAccounts.get(scope)!.push(newAccount);
354
+
355
+ this.logger.info(
356
+ `Added new pool account with value ${value} and label ${label}`
357
+ );
358
+
359
+ return newAccount;
360
+ }
361
+
362
+ /**
363
+ * Adds a new commitment to the account after spending
364
+ *
365
+ * @param parentCommitment - The commitment that was spent
366
+ * @param value - The remaining value after spending
367
+ * @param nullifier - The nullifier used for spending
368
+ * @param secret - The secret used for spending
369
+ * @param blockNumber - The block number of the withdrawal
370
+ * @param txHash - The transaction hash of the withdrawal
371
+ * @returns The new commitment
372
+ *
373
+ * @remarks
374
+ * This method finds the account containing the parent commitment, creates a new
375
+ * commitment with the provided parameters, and adds it to the account's children.
376
+ * The new commitment inherits the label from the parent commitment.
377
+ *
378
+ * @throws {AccountError} If no account is found for the commitment
379
+ */
380
+ public addWithdrawalCommitment(
381
+ parentCommitment: AccountCommitment,
382
+ value: bigint,
383
+ nullifier: Secret,
384
+ secret: Secret,
385
+ blockNumber: bigint,
386
+ txHash: Hex
387
+ ): AccountCommitment {
388
+ let foundAccount: PoolAccount | undefined;
389
+ let foundScope: bigint | undefined;
390
+
391
+ for (const [scope, accounts] of this.account.poolAccounts.entries()) {
392
+ foundAccount = accounts.find((account) => {
393
+ if (account.deposit.hash === parentCommitment.hash) return true;
394
+ return account.children.some(
395
+ (child) => child.hash === parentCommitment.hash
396
+ );
397
+ });
398
+
399
+ if (foundAccount) {
400
+ foundScope = scope;
401
+ break;
402
+ }
403
+ }
404
+
405
+ if (!foundAccount || !foundScope) {
406
+ throw AccountError.commitmentNotFound(parentCommitment.hash);
407
+ }
408
+
409
+ const precommitment = this._hashPrecommitment(nullifier, secret);
410
+ const newCommitment: AccountCommitment = {
411
+ hash: this._hashCommitment(value, parentCommitment.label, precommitment),
412
+ value,
413
+ label: parentCommitment.label,
414
+ nullifier,
415
+ secret,
416
+ blockNumber,
417
+ txHash,
418
+ };
419
+
420
+ foundAccount.children.push(newCommitment);
421
+
422
+ this.logger.info(
423
+ `Added new commitment with value ${value} to account with label ${parentCommitment.label}`
424
+ );
425
+
426
+ return newCommitment;
427
+ }
428
+
429
+ /**
430
+ * Adds a ragequit event to an existing pool account
431
+ *
432
+ * @param label - The label of the account to add the ragequit to
433
+ * @param ragequit - The ragequit event to add
434
+ * @returns The updated pool account
435
+ *
436
+ * @remarks
437
+ * When an account has a ragequit event, it can no longer be spent.
438
+ * This method finds the account with the matching label and attaches
439
+ * the ragequit event to it.
440
+ *
441
+ * @throws {AccountError} If no account is found with the given label
442
+ */
443
+ public addRagequitToAccount(
444
+ label: Hash,
445
+ ragequit: RagequitEvent
446
+ ): PoolAccount {
447
+ let foundAccount: PoolAccount | undefined;
448
+ let foundScope: Hash | undefined;
449
+
450
+ // Find the account with the matching label
451
+ for (const [scope, accounts] of this.account.poolAccounts.entries()) {
452
+ foundAccount = accounts.find((account) => account.label === label);
453
+ if (foundAccount) {
454
+ foundScope = scope;
455
+ break;
456
+ }
457
+ }
458
+
459
+ if (!foundAccount || !foundScope) {
460
+ throw new AccountError(
461
+ `No account found with label ${label}`,
462
+ ErrorCode.INVALID_INPUT
463
+ );
464
+ }
465
+
466
+ // Add the ragequit event to the account
467
+ foundAccount.ragequit = ragequit;
468
+
469
+ this.logger.info(
470
+ `Added ragequit event to account with label ${label}, value ${ragequit.value}`
471
+ );
472
+
473
+ return foundAccount;
474
+ }
475
+
476
+ /**
477
+ * Fetches deposit events for a given pool and returns a map of precommitments to their events for efficient lookup
478
+ *
479
+ * @param pool - The pool to fetch deposit events for
480
+ *
481
+ * @returns A map of precommitments to their events
482
+ */
483
+ public async getDepositEvents(
484
+ pool: PoolInfo
485
+ ): Promise<Map<Hash, DepositEvent>> {
486
+ try {
487
+ const depositEvents = await this.dataService.getDeposits(pool);
488
+
489
+ this.logger.info(`Found deposits for pool`, {
490
+ poolAddress: pool.address,
491
+ poolChainId: pool.chainId,
492
+ depositCount: depositEvents.length,
493
+ });
494
+
495
+ const depositMap = new Map<Hash, DepositEvent>();
496
+ for (const event of depositEvents) {
497
+ const existingEvent = depositMap.get(event.precommitment);
498
+
499
+ // If no existing event, or current event is older (earlier block), use current event
500
+ if (!existingEvent || event.blockNumber < existingEvent.blockNumber) {
501
+ depositMap.set(event.precommitment, event);
502
+ }
503
+ }
504
+
505
+ return depositMap;
506
+ } catch (error) {
507
+ throw EventError.depositEventError(
508
+ pool.chainId,
509
+ pool.scope,
510
+ error as Error
511
+ );
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Fetches withdrawal events for a given pool and returns a map of spent nullifiers to their events for efficient lookup
517
+ *
518
+ * @param pool - The pool to fetch withdrawal events for
519
+ *
520
+ * @returns A map of spent nullifiers to their events
521
+ */
522
+ public async getWithdrawalEvents(
523
+ pool: PoolInfo
524
+ ): Promise<Map<Hash, WithdrawalEvent>> {
525
+ try {
526
+ const withdrawalEvents = await this.dataService.getWithdrawals(pool);
527
+ const withdrawalMap = new Map<Hash, WithdrawalEvent>();
528
+ for (const event of withdrawalEvents) {
529
+ withdrawalMap.set(event.spentNullifier, event);
530
+ }
531
+
532
+ return withdrawalMap;
533
+ } catch (error) {
534
+ throw EventError.withdrawalEventError(
535
+ pool.chainId,
536
+ pool.scope,
537
+ error as Error
538
+ );
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Fetches ragequit events for a given pool and returns a map of ragequit labels to their events for efficient lookup
544
+ *
545
+ * @param pool - The pool to fetch ragequit events for
546
+ *
547
+ * @returns A map of ragequit labels to their events
548
+ */
549
+ public async getRagequitEvents(
550
+ pool: PoolInfo
551
+ ): Promise<Map<Hash, RagequitEvent>> {
552
+ try {
553
+ const ragequitEvents = await this.dataService.getRagequits(pool);
554
+ const ragequitMap = new Map<Hash, RagequitEvent>();
555
+ for (const event of ragequitEvents) {
556
+ ragequitMap.set(event.label, event);
557
+ }
558
+
559
+ return ragequitMap;
560
+ } catch (error) {
561
+ throw EventError.ragequitEventError(
562
+ pool.chainId,
563
+ pool.scope,
564
+ error as Error
565
+ );
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Fetches events for a given set of pools
571
+ *
572
+ * @param pools - The pools to fetch events for
573
+ *
574
+ * @returns A map of pool scopes to their events
575
+ */
576
+ public async getEvents(pools: PoolInfo[]): Promise<PoolEventsResult> {
577
+ const events: PoolEventsResult = new Map();
578
+
579
+ // Use mapLimit to control concurrency at pool level
580
+ const poolEventResults = await mapLimit(
581
+ pools,
582
+ this.poolConcurrency,
583
+ async (pool: PoolInfo) => {
584
+ try {
585
+ this.logger.info(`Fetching events for pool`, {
586
+ poolAddress: pool.address,
587
+ poolChainId: pool.chainId,
588
+ poolDeploymentBlock: pool.deploymentBlock,
589
+ });
590
+
591
+ const [depositEvents, withdrawalEvents, ragequitEvents] =
592
+ await Promise.all([
593
+ this.getDepositEvents(pool),
594
+ this.getWithdrawalEvents(pool),
595
+ this.getRagequitEvents(pool),
596
+ ]);
597
+
598
+ return {
599
+ status: "fulfilled" as const,
600
+ value: {
601
+ scope: pool.scope,
602
+ depositEvents,
603
+ withdrawalEvents,
604
+ ragequitEvents,
605
+ },
606
+ };
607
+ } catch (error) {
608
+ return {
609
+ status: "rejected" as const,
610
+ reason: error as Error,
611
+ };
612
+ }
613
+ }
614
+ );
615
+
616
+ for (const result of poolEventResults) {
617
+ if (result.status === "fulfilled") {
618
+ const { scope, depositEvents, withdrawalEvents, ragequitEvents } =
619
+ result.value;
620
+ events.set(scope, {
621
+ depositEvents,
622
+ withdrawalEvents,
623
+ ragequitEvents,
624
+ });
625
+ } else {
626
+ const errorWithDetails = result.reason as Error & { details?: { scope?: Hash } };
627
+ const scope = errorWithDetails.details?.scope as Hash;
628
+
629
+ events.set(scope, {
630
+ reason: result.reason.message,
631
+ scope: scope,
632
+ });
633
+ }
634
+ }
635
+
636
+ return events;
637
+ }
638
+
639
+ /**
640
+ * Processes deposit events for a given scope and adds them to the account
641
+ * Deterministically generate deposit secrets and check if they match on-chain deposits
642
+ *
643
+ * @param scope - The scope of the pool
644
+ * @param depositEvents - The map of deposit events
645
+ *
646
+ */
647
+ private _processDepositEvents(
648
+ scope: Hash,
649
+ depositEvents: Map<Hash, DepositEvent>
650
+ ): void {
651
+ const MAX_CONSECUTIVE_MISSES = 10; // Large enough to avoid tx failures
652
+
653
+ const foundIndices = new Set<bigint>();
654
+ let consecutiveMisses = 0;
655
+
656
+ this.logger.debug(`[DepositScan] Starting deposit scan`, {
657
+ scope,
658
+ totalOnChainDeposits: depositEvents.size,
659
+ maxConsecutiveMisses: MAX_CONSECUTIVE_MISSES,
660
+ });
661
+
662
+ for (let index = BigInt(0); ; index++) {
663
+ // Generate nullifier, secret, and precommitment for this index
664
+ const { nullifier, secret, precommitment } = this.createDepositSecrets(
665
+ scope,
666
+ index
667
+ );
668
+
669
+ this.logger.debug(`[DepositScan] Scanning index`, {
670
+ scope,
671
+ index: index.toString(),
672
+ computedPrecommitment: precommitment,
673
+ consecutiveMissesSoFar: consecutiveMisses,
674
+ });
675
+
676
+ // Look for a deposit with this precommitment
677
+ const event = depositEvents.get(precommitment);
678
+
679
+ if (!event) {
680
+ consecutiveMisses++;
681
+ this.logger.debug(`[DepositScan] No on-chain deposit found for index`, {
682
+ scope,
683
+ index: index.toString(),
684
+ computedPrecommitment: precommitment,
685
+ consecutiveMisses,
686
+ maxConsecutiveMisses: MAX_CONSECUTIVE_MISSES,
687
+ });
688
+ if (consecutiveMisses >= MAX_CONSECUTIVE_MISSES) {
689
+ this.logger.debug(
690
+ `[DepositScan] Consecutive miss limit reached — stopping scan`,
691
+ {
692
+ scope,
693
+ stoppedAtIndex: index.toString(),
694
+ consecutiveMisses,
695
+ totalDepositsMatched: foundIndices.size,
696
+ }
697
+ );
698
+ break;
699
+ }
700
+ continue;
701
+ }
702
+
703
+ // Can reset counter in case if user had any tx failures for
704
+ // newer deposits
705
+ consecutiveMisses = 0;
706
+ foundIndices.add(index);
707
+
708
+ this.logger.debug(`[DepositScan] Matched on-chain deposit at index`, {
709
+ scope,
710
+ index: index.toString(),
711
+ computedPrecommitment: precommitment,
712
+ onChainLabel: event.label,
713
+ onChainValue: event.value.toString(),
714
+ onChainBlockNumber: event.blockNumber.toString(),
715
+ onChainTransactionHash: event.transactionHash,
716
+ depositor: event.depositor,
717
+ consecutiveMissesReset: true,
718
+ });
719
+
720
+ // Create a new pool account for this deposit
721
+ this.addPoolAccount(
722
+ scope,
723
+ event.value,
724
+ nullifier,
725
+ secret,
726
+ event.label,
727
+ event.blockNumber,
728
+ event.transactionHash
729
+ );
730
+ }
731
+
732
+ this.logger.debug(`[DepositScan] Deposit scan complete`, {
733
+ scope,
734
+ totalIndicesScanned: foundIndices.size > 0
735
+ ? (Math.max(...[...foundIndices].map(Number)) + 1 + MAX_CONSECUTIVE_MISSES)
736
+ : MAX_CONSECUTIVE_MISSES,
737
+ totalDepositsMatched: foundIndices.size,
738
+ matchedIndices: [...foundIndices].map(String),
739
+ });
740
+ }
741
+
742
+ /**
743
+ * Processes withdrawal events for a given scope and adds them to the account
744
+ *
745
+ * @param scope - The scope of the pool
746
+ * @param withdrawalEvents - The map of withdrawal events
747
+ *
748
+ * @remarks
749
+ * This method performs the following steps for each pool:
750
+ * 1. Identifies the earliest deposit block for each scope
751
+ * 2. For each account, reconstructs the withdrawal history by:
752
+ * - Generating nullifiers sequentially
753
+ * - Matching them against on-chain events
754
+ * - Adding matched withdrawals to the account state
755
+ *
756
+ * @throws {DataError} If event fetching fails
757
+ * @private
758
+ *
759
+ */
760
+ private _processWithdrawalEvents(
761
+ scope: Hash,
762
+ withdrawalEvents: Map<Hash, WithdrawalEvent>
763
+ ): void {
764
+ const accounts = this.account.poolAccounts.get(scope);
765
+
766
+ // Skip if no accounts for this scope
767
+ if (!accounts || accounts.length === 0) {
768
+ this.logger.debug(`[WithdrawalScan] No accounts found for scope — skipping`, {
769
+ scope,
770
+ });
771
+ return;
772
+ }
773
+
774
+ this.logger.debug(`[WithdrawalScan] Starting withdrawal scan`, {
775
+ scope,
776
+ accountCount: accounts.length,
777
+ totalOnChainWithdrawals: withdrawalEvents.size,
778
+ });
779
+
780
+ // Process each account sequentially
781
+ for (const account of accounts) {
782
+ let currentCommitment = account.deposit;
783
+ let index = BigInt(0);
784
+
785
+ this.logger.debug(`[WithdrawalScan] Processing account`, {
786
+ scope,
787
+ accountLabel: account.label,
788
+ depositValue: account.deposit.value.toString(),
789
+ depositBlockNumber: account.deposit.blockNumber.toString(),
790
+ depositTransactionHash: account.deposit.txHash,
791
+ });
792
+
793
+ // Continue processing withdrawals until no more are found sequentially
794
+ while (true) {
795
+ // Generate nullifier for this withdrawal
796
+ const nullifierHash = poseidon([currentCommitment.nullifier]) as Hash;
797
+
798
+ this.logger.debug(`[WithdrawalScan] Checking nullifier hash`, {
799
+ scope,
800
+ accountLabel: account.label,
801
+ withdrawalIndex: index.toString(),
802
+ currentCommitmentHash: currentCommitment.hash,
803
+ currentCommitmentValue: currentCommitment.value.toString(),
804
+ nullifierHash,
805
+ });
806
+
807
+ // Look for a withdrawal event with this nullifier
808
+ const withdrawal = withdrawalEvents.get(nullifierHash);
809
+ if (!withdrawal) {
810
+ this.logger.debug(`[WithdrawalScan] No on-chain withdrawal found for nullifier — stopping for this account`, {
811
+ scope,
812
+ accountLabel: account.label,
813
+ withdrawalIndex: index.toString(),
814
+ nullifierHash,
815
+ finalRemainingValue: currentCommitment.value.toString(),
816
+ totalWithdrawalsProcessed: index.toString(),
817
+ });
818
+ break;
819
+ }
820
+
821
+ this.logger.debug(`[WithdrawalScan] Matched on-chain withdrawal`, {
822
+ scope,
823
+ accountLabel: account.label,
824
+ withdrawalIndex: index.toString(),
825
+ nullifierHash,
826
+ spentNullifier: withdrawal.spentNullifier,
827
+ withdrawn: withdrawal.withdrawn.toString(),
828
+ newCommitmentHash: withdrawal.newCommitment,
829
+ remainingValue: (currentCommitment.value - withdrawal.withdrawn).toString(),
830
+ blockNumber: withdrawal.blockNumber.toString(),
831
+ transactionHash: withdrawal.transactionHash,
832
+ });
833
+
834
+ // Generate secret for this withdrawal
835
+ const nullifier = this._genWithdrawalNullifier(account.label, index);
836
+ const secret = this._genWithdrawalSecret(account.label, index);
837
+
838
+ // Add the withdrawal commitment to the account
839
+ const newCommitment = this.addWithdrawalCommitment(
840
+ currentCommitment,
841
+ currentCommitment.value - withdrawal.withdrawn,
842
+ nullifier,
843
+ secret,
844
+ withdrawal.blockNumber,
845
+ withdrawal.transactionHash
846
+ );
847
+
848
+ // Update current commitment to the newly created one
849
+ currentCommitment = newCommitment;
850
+
851
+ // Increment index for next potential withdrawal
852
+ index++;
853
+ }
854
+ }
855
+
856
+ this.logger.debug(`[WithdrawalScan] Withdrawal scan complete`, {
857
+ scope,
858
+ accountCount: accounts.length,
859
+ });
860
+ }
861
+
862
+ /**
863
+ * Processes ragequit events for a given scope and adds them to the account
864
+ *
865
+ * @param scope - The scope of the pool
866
+ * @param ragequitEvents - The map of ragequit events
867
+ *
868
+ * @remarks
869
+ * This method performs the following steps for each pool:
870
+ * 1. Adds ragequit events to accounts if found
871
+ *
872
+ * @throws {DataError} If event fetching fails
873
+ * @private
874
+ *
875
+ */
876
+ private _processRagequitEvents(
877
+ scope: Hash,
878
+ ragequitEvents: Map<Hash, RagequitEvent>
879
+ ): void {
880
+ const accounts = this.account.poolAccounts.get(scope);
881
+
882
+ if (!accounts || accounts.length === 0) {
883
+ this.logger.debug(`[RagequitScan] No accounts found for scope — skipping`, {
884
+ scope,
885
+ });
886
+ return;
887
+ }
888
+
889
+ this.logger.debug(`[RagequitScan] Starting ragequit scan`, {
890
+ scope,
891
+ accountCount: accounts.length,
892
+ totalOnChainRagequits: ragequitEvents.size,
893
+ });
894
+
895
+ for (const account of accounts) {
896
+ this.logger.debug(`[RagequitScan] Checking account for ragequit`, {
897
+ scope,
898
+ accountLabel: account.label,
899
+ });
900
+
901
+ const ragequit = ragequitEvents.get(account.label);
902
+ if (ragequit) {
903
+ this.logger.debug(`[RagequitScan] Ragequit found for account`, {
904
+ scope,
905
+ accountLabel: account.label,
906
+ ragequitCommitment: ragequit.commitment,
907
+ ragequitValue: ragequit.value.toString(),
908
+ ragequitBlockNumber: ragequit.blockNumber.toString(),
909
+ ragequitTransactionHash: ragequit.transactionHash,
910
+ ragequitter: ragequit.ragequitter,
911
+ });
912
+ this.addRagequitToAccount(account.label, ragequit);
913
+ } else {
914
+ this.logger.debug(`[RagequitScan] No ragequit found for account`, {
915
+ scope,
916
+ accountLabel: account.label,
917
+ });
918
+ }
919
+ }
920
+
921
+ this.logger.debug(`[RagequitScan] Ragequit scan complete`, {
922
+ scope,
923
+ accountCount: accounts.length,
924
+ });
925
+ }
926
+
927
+ /**
928
+ * Initializes an AccountService instance with events for a given set of pools
929
+ *
930
+ * @param dataService - The data service to use for fetching events
931
+ * @param source - The source to use for initializing the account. Either a mnemonic or an existing account service instance
932
+ * @param pools - The pools to fetch events for
933
+ *
934
+ * @remarks
935
+ * This method performs the following steps for each pool:
936
+ * 1. Fetches deposit, withdrawal, and ragequit events for each pool
937
+ * 2. Processes deposit events and creates pool accounts
938
+ * 3. Processes withdrawal events and adds commitments to pool accounts
939
+ * 4. Processes ragequit events and adds ragequit to pool accounts
940
+ *
941
+ * @returns The initialized AccountService instance and array of errors if any pool events fetching fails
942
+ *
943
+ * if any pool events fetching fails, the account will be initialized without the events for that pool
944
+ * user can then call to this method again with the same account and missing pools to fetch the missing events
945
+ *
946
+ * @throws {AccountError} If account state reconstruction fails or if duplicate pools are found
947
+ */
948
+ static async initializeWithEvents(
949
+ dataService: DataService,
950
+ source:
951
+ | {
952
+ mnemonic: string;
953
+ }
954
+ | {
955
+ service: AccountService;
956
+ },
957
+ pools: PoolInfo[]
958
+ ): Promise<{ account: AccountService; errors: PoolEventsError[] }> {
959
+ // Log the start of the history retrieval process
960
+ const logger = new Logger({ prefix: "Account", level: LogLevel.DEBUG });
961
+ logger.debug(`[Init] initializeWithEvents started`, {
962
+ poolCount: pools.length,
963
+ pools: pools.map((p) => ({
964
+ chainId: p.chainId,
965
+ address: p.address,
966
+ scope: p.scope,
967
+ deploymentBlock: p.deploymentBlock.toString(),
968
+ })),
969
+ });
970
+
971
+ // verify that pools don't contain duplicates based on scope
972
+ const uniqueScopes = new Set<bigint>();
973
+ for (const pool of pools) {
974
+ if (uniqueScopes.has(pool.scope)) {
975
+ throw AccountError.duplicatePools(pool.scope);
976
+ }
977
+ uniqueScopes.add(pool.scope);
978
+ }
979
+
980
+ const errors: PoolEventsError[] = [];
981
+ const account = new AccountService(
982
+ dataService,
983
+ "mnemonic" in source
984
+ ? { mnemonic: source.mnemonic }
985
+ : { account: source.service.account }
986
+ );
987
+
988
+ const events = await account.getEvents(pools);
989
+
990
+ for (const [scope, result] of events.entries()) {
991
+ if ("reason" in result) {
992
+ logger.debug(`[Init] Failed to fetch events for scope`, {
993
+ scope,
994
+ reason: result.reason,
995
+ });
996
+ errors.push(result);
997
+ } else {
998
+ logger.debug(`[Init] Processing events for scope`, {
999
+ scope,
1000
+ depositCount: result.depositEvents.size,
1001
+ withdrawalCount: result.withdrawalEvents.size,
1002
+ ragequitCount: result.ragequitEvents.size,
1003
+ });
1004
+
1005
+ // Process deposit events and create pool accounts
1006
+ account._processDepositEvents(scope, result.depositEvents);
1007
+
1008
+ // Process withdrawal events and add commitments to pool accounts
1009
+ account._processWithdrawalEvents(scope, result.withdrawalEvents);
1010
+
1011
+ // Process ragequit events and add ragequit to pool accounts
1012
+ account._processRagequitEvents(scope, result.ragequitEvents);
1013
+ }
1014
+ }
1015
+
1016
+ logger.debug(`[Init] initializeWithEvents complete`, {
1017
+ totalErrors: errors.length,
1018
+ errors: errors.map((e) => ({ scope: e.scope, reason: e.reason })),
1019
+ });
1020
+
1021
+ account.diagnosticSummary();
1022
+
1023
+ return { account, errors };
1024
+ }
1025
+
1026
+ /**
1027
+ * @deprecated Use `initializeWithEvents` for instantiating an account with history reconstruction
1028
+ * Retrieves the history of deposits and withdrawals for the given pools.
1029
+ *
1030
+ * @param pools - Array of pool configurations to sync history for
1031
+ *
1032
+ * @remarks
1033
+ * This method performs the following steps:
1034
+ * 1. Initializes pool accounts for each pool if they don't exist
1035
+ * 2. For each pool, fetches deposit events and reconstructs accounts
1036
+ * 3. Processes withdrawals and ragequits to update account state
1037
+ *
1038
+ * The account reconstruction is deterministic based on the master keys,
1039
+ * allowing the full state to be recovered from on-chain events.
1040
+ *
1041
+ * @throws {DataError} If event fetching fails
1042
+ * @throws {AccountError} If account state reconstruction fails
1043
+ */
1044
+ public async retrieveHistory(pools: PoolInfo[]): Promise<void> {
1045
+ // Log the start of the history retrieval process
1046
+ this.logger.info(`Fetching events for ${pools.length} pools`);
1047
+
1048
+ // Initialize pool accounts map for each pool if it doesn't exist
1049
+ for (const pool of pools) {
1050
+ if (!this.account.poolAccounts.has(pool.scope)) {
1051
+ this.account.poolAccounts.set(pool.scope, []);
1052
+ }
1053
+ }
1054
+
1055
+ // Process all pools in parallel for better performance
1056
+ await Promise.all(
1057
+ pools.map(async (pool) => {
1058
+ // Log which pool is being processed
1059
+ this.logger.info(
1060
+ `Processing pool ${pool.address} on chain ${pool.chainId} from block ${pool.deploymentBlock}`
1061
+ );
1062
+
1063
+ // Fetch all deposit events for this pool
1064
+ const deposits = await this.dataService.getDeposits(pool);
1065
+
1066
+ this.logger.info(
1067
+ `Found ${deposits.length} deposits for pool ${pool.address}`
1068
+ );
1069
+
1070
+ // Create a map of deposits by precommitment for efficient lookup
1071
+ const depositMap = new Map<Hash, DepositEvent>();
1072
+ for (const deposit of deposits) {
1073
+ if (!depositMap.has(deposit.precommitment)) {
1074
+ depositMap.set(deposit.precommitment, deposit);
1075
+ }
1076
+ }
1077
+
1078
+ // Track found deposits for logging and debugging
1079
+ const foundDeposits: Array<{
1080
+ index: bigint;
1081
+ nullifier: Secret;
1082
+ secret: Secret;
1083
+ pool: PoolInfo;
1084
+ deposit: (typeof deposits)[0];
1085
+ }> = [];
1086
+
1087
+ // Start with index 0 and try to find deposits deterministically
1088
+ let index = BigInt(0);
1089
+ let firstDepositBlock: bigint | undefined;
1090
+
1091
+ // Deterministically generate deposit secrets and check if they match on-chain deposits
1092
+ while (true) {
1093
+ // Generate nullifier, secret, and precommitment for this index
1094
+ const nullifier = this._genDepositNullifier(pool.scope, index);
1095
+ const secret = this._genDepositSecret(pool.scope, index);
1096
+ const precommitment = this._hashPrecommitment(nullifier, secret);
1097
+
1098
+ // Look for a deposit with this precommitment
1099
+ const deposit = depositMap.get(precommitment);
1100
+ if (!deposit) break; // No more deposits found, exit the loop
1101
+
1102
+ // Track the earliest deposit block for later withdrawal processing
1103
+ if (!firstDepositBlock || deposit.blockNumber < firstDepositBlock) {
1104
+ firstDepositBlock = deposit.blockNumber;
1105
+ }
1106
+
1107
+ // Create a new pool account for this deposit
1108
+ this.addPoolAccount(
1109
+ pool.scope,
1110
+ deposit.value,
1111
+ nullifier,
1112
+ secret,
1113
+ deposit.label,
1114
+ deposit.blockNumber,
1115
+ deposit.transactionHash
1116
+ );
1117
+
1118
+ // Track the found deposit
1119
+ foundDeposits.push({ index, nullifier, secret, pool, deposit });
1120
+
1121
+ // Move to the next index
1122
+ index++;
1123
+ }
1124
+
1125
+ // If no accounts were found for this scope, log and skip further processing
1126
+ if (this.account.poolAccounts.get(pool.scope)!.length === 0) {
1127
+ this.logger.info(
1128
+ `No Pool Accounts were found for scope ${pool.scope}`
1129
+ );
1130
+ return;
1131
+ }
1132
+
1133
+ this.logger.info(
1134
+ `Found ${foundDeposits.length} deposits for pool ${pool.address}`
1135
+ );
1136
+ })
1137
+ );
1138
+
1139
+ // Process withdrawals and ragequits for all pools
1140
+ // This is done after all deposits are processed to ensure we have the complete account state
1141
+ await this._processWithdrawalsAndRagequits(pools);
1142
+ }
1143
+
1144
+ /**
1145
+ * Processes withdrawal events for all pools and updates account state.
1146
+ *
1147
+ * @param pools - Array of pool configurations to process withdrawals for
1148
+ *
1149
+ * @remarks
1150
+ * This method performs the following steps for each pool:
1151
+ * 1. Identifies the earliest deposit block for each scope
1152
+ * 2. Fetches withdrawal and ragequit events from that block
1153
+ * 3. Maps withdrawals by nullifier hash and ragequits by label for efficient lookup
1154
+ * 4. For each account, reconstructs the withdrawal history by:
1155
+ * - Generating nullifiers sequentially
1156
+ * - Matching them against on-chain events
1157
+ * - Adding matched withdrawals to the account state
1158
+ * 5. Adds ragequit events to accounts if found
1159
+ *
1160
+ * @throws {DataError} If event fetching fails
1161
+ * @private
1162
+ */
1163
+ private async _processWithdrawalsAndRagequits(
1164
+ pools: PoolInfo[]
1165
+ ): Promise<void> {
1166
+ await Promise.all(
1167
+ pools.map(async (pool) => {
1168
+ const accounts = this.account.poolAccounts.get(pool.scope);
1169
+
1170
+ // Skip if no accounts for this scope
1171
+ if (!accounts || accounts.length === 0) {
1172
+ this.logger.info(
1173
+ `No accounts found for pool ${pool.address} with scope ${pool.scope}`
1174
+ );
1175
+ return;
1176
+ }
1177
+
1178
+ // Find the earliest deposit block for this scope
1179
+ let firstDepositBlock = BigInt(Number.MAX_SAFE_INTEGER);
1180
+ for (const account of accounts) {
1181
+ if (account.deposit.blockNumber < firstDepositBlock) {
1182
+ firstDepositBlock = account.deposit.blockNumber;
1183
+ }
1184
+ }
1185
+
1186
+ // Fetch withdrawal and ragequit events from the first deposit block
1187
+ const withdrawals = await this.dataService.getWithdrawals(
1188
+ pool,
1189
+ firstDepositBlock
1190
+ );
1191
+ const ragequits = await this.dataService.getRagequits(
1192
+ pool,
1193
+ firstDepositBlock
1194
+ );
1195
+
1196
+ this.logger.info(
1197
+ `Found ${withdrawals.length} withdrawals for pool ${pool.address}`
1198
+ );
1199
+
1200
+ if (withdrawals.length === 0 && ragequits.length === 0) {
1201
+ return;
1202
+ }
1203
+
1204
+ // Map withdrawals by spent nullifier for quick lookup
1205
+ const withdrawalMap = new Map<Hash, WithdrawalEvent>();
1206
+ for (const withdrawal of withdrawals) {
1207
+ withdrawalMap.set(withdrawal.spentNullifier, withdrawal);
1208
+ }
1209
+
1210
+ // Map ragequits by label for quick lookup
1211
+ const ragequitMap = new Map<Hash, RagequitEvent>();
1212
+ for (const ragequit of ragequits) {
1213
+ ragequitMap.set(ragequit.label, ragequit);
1214
+ }
1215
+
1216
+ // Process each account
1217
+ for (const account of accounts) {
1218
+ let currentCommitment = account.deposit;
1219
+ let index = BigInt(0);
1220
+
1221
+ // Continue processing withdrawals until no more are found
1222
+ while (true) {
1223
+ // Generate nullifier for this withdrawal
1224
+ const nullifierHash = poseidon([
1225
+ currentCommitment.nullifier,
1226
+ ]) as Hash;
1227
+
1228
+ // Look for a withdrawal event with this nullifier
1229
+ const withdrawal = withdrawalMap.get(nullifierHash);
1230
+ if (!withdrawal) {
1231
+ break;
1232
+ }
1233
+
1234
+ // Generate secret for this withdrawal
1235
+ const nullifier = this._genWithdrawalNullifier(
1236
+ account.label,
1237
+ index
1238
+ );
1239
+ const secret = this._genWithdrawalSecret(account.label, index);
1240
+
1241
+ // Add the withdrawal commitment to the account
1242
+ const newCommitment = this.addWithdrawalCommitment(
1243
+ currentCommitment,
1244
+ currentCommitment.value - withdrawal.withdrawn,
1245
+ nullifier,
1246
+ secret,
1247
+ withdrawal.blockNumber,
1248
+ withdrawal.transactionHash
1249
+ );
1250
+
1251
+ // Update current commitment to the newly created one
1252
+ currentCommitment = newCommitment;
1253
+
1254
+ // Increment index for next potential withdrawal
1255
+ index++;
1256
+ }
1257
+
1258
+ const ragequit = ragequitMap.get(account.label);
1259
+ if (ragequit) {
1260
+ this.addRagequitToAccount(account.label, ragequit);
1261
+ }
1262
+ }
1263
+ })
1264
+ );
1265
+ }
1266
+
1267
+ /**
1268
+ * Logs a structured diagnostic summary of the current account state.
1269
+ * Only public, non-sensitive on-chain data is included.
1270
+ * Call this after initializeWithEvents to produce a full dump for investigation.
1271
+ */
1272
+ public diagnosticSummary(): void {
1273
+ this.logger.debug(`[DiagnosticSummary] ========== ACCOUNT STATE SUMMARY ==========`);
1274
+ this.logger.debug(`[DiagnosticSummary] Total scopes tracked: ${this.account.poolAccounts.size}`);
1275
+
1276
+ let globalTotalAccounts = 0;
1277
+ let globalSpendable = 0;
1278
+ let globalZeroValue = 0;
1279
+ let globalRagequit = 0;
1280
+
1281
+ for (const [scope, accounts] of this.account.poolAccounts.entries()) {
1282
+ this.logger.debug(`[DiagnosticSummary] --- Scope: ${scope} ---`, {
1283
+ scope,
1284
+ accountCount: accounts.length,
1285
+ });
1286
+
1287
+ for (const account of accounts) {
1288
+ globalTotalAccounts++;
1289
+
1290
+ const isRagequit = !!account.ragequit;
1291
+ const lastCommitment =
1292
+ account.children.length > 0
1293
+ ? account.children[account.children.length - 1]
1294
+ : account.deposit;
1295
+ const spendable = !isRagequit && lastCommitment.value > 0n;
1296
+
1297
+ if (isRagequit) globalRagequit++;
1298
+ else if (lastCommitment.value === 0n) globalZeroValue++;
1299
+ else globalSpendable++;
1300
+
1301
+ this.logger.debug(`[DiagnosticSummary] Account`, {
1302
+ scope,
1303
+ label: account.label,
1304
+ depositValue: account.deposit.value.toString(),
1305
+ depositBlockNumber: account.deposit.blockNumber.toString(),
1306
+ depositTransactionHash: account.deposit.txHash,
1307
+ withdrawalCount: account.children.length,
1308
+ currentValue: lastCommitment.value.toString(),
1309
+ currentCommitmentHash: lastCommitment.hash,
1310
+ currentCommitmentBlockNumber: lastCommitment.blockNumber.toString(),
1311
+ currentCommitmentTransactionHash: lastCommitment.txHash,
1312
+ isSpendable: spendable,
1313
+ isZeroValue: !isRagequit && lastCommitment.value === 0n,
1314
+ isRagequit,
1315
+ ragequitTransactionHash: account.ragequit?.transactionHash ?? null,
1316
+ ragequitBlockNumber: account.ragequit?.blockNumber?.toString() ?? null,
1317
+ });
1318
+
1319
+ for (let i = 0; i < account.children.length; i++) {
1320
+ const child = account.children[i];
1321
+ this.logger.debug(`[DiagnosticSummary] Withdrawal child [${i}]`, {
1322
+ scope,
1323
+ accountLabel: account.label,
1324
+ childIndex: i,
1325
+ commitmentHash: child.hash,
1326
+ value: child.value.toString(),
1327
+ blockNumber: child.blockNumber.toString(),
1328
+ transactionHash: child.txHash,
1329
+ });
1330
+ }
1331
+ }
1332
+ }
1333
+
1334
+ this.logger.debug(`[DiagnosticSummary] ========== TOTALS ==========`, {
1335
+ totalScopesTracked: this.account.poolAccounts.size,
1336
+ totalAccounts: globalTotalAccounts,
1337
+ spendableAccounts: globalSpendable,
1338
+ zeroValueAccounts: globalZeroValue,
1339
+ ragequitAccounts: globalRagequit,
1340
+ });
1341
+ this.logger.debug(`[DiagnosticSummary] ===========================================`);
1342
+ }
1343
+ }