@0xbow/privacy-pools-core-sdk 0.0.0-b21c14ba

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