@0xbow/privacy-pools-core-sdk 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +102 -23
  2. package/dist/esm/{fetchArtifacts.esm-DbVRphob.js → fetchArtifacts.esm-B0qaot8v.js} +2 -2
  3. package/dist/esm/{fetchArtifacts.esm-DbVRphob.js.map → fetchArtifacts.esm-B0qaot8v.js.map} +1 -1
  4. package/dist/esm/{fetchArtifacts.node-D-fJGtzV.js → fetchArtifacts.node-PzijuwVc.js} +2 -2
  5. package/dist/esm/{fetchArtifacts.node-D-fJGtzV.js.map → fetchArtifacts.node-PzijuwVc.js.map} +1 -1
  6. package/dist/esm/{index-DkNRxKxP.js → index-BjOXETm6.js} +312 -315
  7. package/dist/esm/{index-DkNRxKxP.js.map → index-BjOXETm6.js.map} +1 -1
  8. package/dist/esm/index.mjs +1 -1
  9. package/dist/index.d.mts +81 -0
  10. package/dist/node/{fetchArtifacts.esm-BIT-b_1_.js → fetchArtifacts.esm-B6uU6QdA.js} +2 -2
  11. package/dist/node/{fetchArtifacts.esm-BIT-b_1_.js.map → fetchArtifacts.esm-B6uU6QdA.js.map} +1 -1
  12. package/dist/node/{fetchArtifacts.node-CKwwU50E.js → fetchArtifacts.node-CZRy6KmV.js} +2 -2
  13. package/dist/node/{fetchArtifacts.node-CKwwU50E.js.map → fetchArtifacts.node-CZRy6KmV.js.map} +1 -1
  14. package/dist/node/{index-C3RV9Cri.js → index-b-U_m4Mi.js} +333 -336
  15. package/dist/node/{index-C3RV9Cri.js.map → index-b-U_m4Mi.js.map} +1 -1
  16. package/dist/node/index.mjs +1 -1
  17. package/dist/types/circuits/artifactHashes.d.ts +19 -0
  18. package/dist/types/core/account.service.d.ts +79 -0
  19. package/dist/types/core/tmp.d.ts +1 -0
  20. package/dist/types/{fetchArtifacts.esm-DT5RuODl.js → fetchArtifacts.esm-BKxGrC6w.js} +1 -1
  21. package/dist/types/{fetchArtifacts.node-D_iVIPqW.js → fetchArtifacts.node-kXMUDgNn.js} +1 -1
  22. package/dist/types/{index-CHy3YamH.js → index-BwyNuaY0.js} +332 -335
  23. package/dist/types/index.js +1 -1
  24. package/dist/types/types/account.d.ts +2 -0
  25. package/package.json +1 -1
  26. package/src/circuits/artifactHashes.ts +74 -0
  27. package/src/circuits/circuits.impl.ts +8 -0
  28. package/src/core/account.service.ts +329 -35
  29. package/src/core/data.service.ts +3 -10
  30. package/src/core/tmp.ts +4 -0
  31. package/src/crypto.ts +5 -6
  32. package/src/types/account.ts +3 -1
  33. package/dist/types/keys.d.ts +0 -18
  34. package/src/keys.ts +0 -42
@@ -1,4 +1,4 @@
1
- export { o as AccountError, A as AccountService, B as BlockchainProvider, p as CircuitName, k as Circuits, C as CommitmentService, n as ContractError, l as ContractInteractionsService, a as DEFAULT_LOG_FETCH_CONFIG, D as DataService, E as ErrorCode, I as InvalidRpcUrl, P as PrivacyPoolSDK, m as ProofError, S as SDKError, W as WithdrawalService, f as bigintToHash, i as bigintToHex, j as calculateContext, b as generateDepositSecrets, g as generateMasterKeys, e as generateMerkleProof, c as generateWithdrawalSecrets, d as getCommitment, h as hashPrecommitment } from './index-CHy3YamH.js';
1
+ export { o as AccountError, A as AccountService, B as BlockchainProvider, p as CircuitName, k as Circuits, C as CommitmentService, n as ContractError, l as ContractInteractionsService, a as DEFAULT_LOG_FETCH_CONFIG, D as DataService, E as ErrorCode, I as InvalidRpcUrl, P as PrivacyPoolSDK, m as ProofError, S as SDKError, W as WithdrawalService, f as bigintToHash, i as bigintToHex, j as calculateContext, b as generateDepositSecrets, g as generateMasterKeys, e as generateMerkleProof, c as generateWithdrawalSecrets, d as getCommitment, h as hashPrecommitment } from './index-BwyNuaY0.js';
2
2
  import 'viem/accounts';
3
3
  import 'buffer';
4
4
  import 'http';
@@ -6,6 +6,7 @@ export interface PoolAccount {
6
6
  deposit: AccountCommitment;
7
7
  children: AccountCommitment[];
8
8
  ragequit?: RagequitEvent;
9
+ isMigrated?: boolean;
9
10
  }
10
11
  export interface AccountCommitment {
11
12
  hash: Hash;
@@ -16,6 +17,7 @@ export interface AccountCommitment {
16
17
  blockNumber: bigint;
17
18
  timestamp?: bigint;
18
19
  txHash: Hex;
20
+ isMigration?: boolean;
19
21
  }
20
22
  export interface PrivacyPoolAccount {
21
23
  masterKeys: [masterNullifier: Secret, masterSecret: Secret];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xbow/privacy-pools-core-sdk",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Typescript SDK for the Privacy Pool protocol",
5
5
  "repository": "https://github.com/0xbow-io/privacy-pools-core",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,74 @@
1
+ import { CircuitName, CircuitNameString } from "./circuits.interface.js";
2
+
3
+ type ArtifactType = "wasm" | "vkey" | "zkey";
4
+
5
+ /**
6
+ * Expected SHA-256 hex digests for every downloaded circuit artifact.
7
+ *
8
+ * vkey and zkey hashes are derived from the trusted-setup ceremony outputs
9
+ * committed in packages/circuits/trusted-setup/final-keys/.
10
+ *
11
+ * wasm hashes are derived from the compiled circuit outputs
12
+ * in packages/circuits/build/.
13
+ *
14
+ * Every artifact downloaded by the SDK MUST have a hash entry here.
15
+ * verifyArtifactIntegrity throws if a hash is missing — refusing to
16
+ * load unverified artifacts is the correct security posture.
17
+ */
18
+ export const ARTIFACT_HASHES: Record<
19
+ CircuitNameString,
20
+ Partial<Record<ArtifactType, string>>
21
+ > = {
22
+ [CircuitName.Commitment]: {
23
+ wasm: "254d2130607182fd6fd1aee67971526b13cfe178c88e360da96dce92663828d8",
24
+ vkey: "7d48b4eb3dedc12fb774348287b587f0c18c3c7254cd60e9cf0f8b3636a570d8",
25
+ zkey: "494ae92d64098fda2a5649690ddc5821fcd7449ca5fe8ef99ee7447544d7e1f3",
26
+ },
27
+ [CircuitName.Withdraw]: {
28
+ wasm: "36cda22791def3d520a55c0fc808369cd5849532a75fab65686e666ed3d55c10",
29
+ vkey: "666bd0983b20c1611543b04f7712e067fbe8cad69f07ada8a310837ff398d21e",
30
+ zkey: "2a893b42174c813566e5c40c715a8b90cd49fc4ecf384e3a6024158c3d6de677",
31
+ },
32
+ [CircuitName.MerkleTree]: {},
33
+ };
34
+
35
+ // Freeze the manifest so runtime code cannot swap out trusted hashes.
36
+ for (const circuitHashes of Object.values(ARTIFACT_HASHES)) {
37
+ if (circuitHashes != null) {
38
+ Object.freeze(circuitHashes);
39
+ }
40
+ }
41
+
42
+ Object.freeze(ARTIFACT_HASHES);
43
+
44
+ export async function sha256Hex(data: Uint8Array): Promise<string> {
45
+ const hashBuffer = await globalThis.crypto.subtle.digest(
46
+ "SHA-256",
47
+ data as ArrayBufferView<ArrayBuffer>,
48
+ );
49
+ return Array.from(new Uint8Array(hashBuffer))
50
+ .map((b) => b.toString(16).padStart(2, "0"))
51
+ .join("");
52
+ }
53
+
54
+ export async function verifyArtifactIntegrity(
55
+ circuitName: CircuitNameString,
56
+ artifactType: ArtifactType,
57
+ data: Uint8Array,
58
+ ): Promise<void> {
59
+ const expectedHash = ARTIFACT_HASHES[circuitName]?.[artifactType];
60
+ if (expectedHash === undefined) {
61
+ throw new Error(
62
+ `No integrity hash registered for ${circuitName}.${artifactType}. ` +
63
+ `Refusing to load unverified artifact.`,
64
+ );
65
+ }
66
+
67
+ const actualHash = await sha256Hex(data);
68
+ if (actualHash !== expectedHash) {
69
+ throw new Error(
70
+ `Integrity check failed for ${circuitName}.${artifactType}: ` +
71
+ `expected ${expectedHash}, got ${actualHash}`,
72
+ );
73
+ }
74
+ }
@@ -10,6 +10,7 @@ import {
10
10
  VersionString,
11
11
  } from "./circuits.interface.js";
12
12
  import { importFetchVersionedArtifact } from "./fetchArtifacts.js";
13
+ import { verifyArtifactIntegrity } from "./artifactHashes.js";
13
14
 
14
15
  interface CircuitOptions {
15
16
  baseUrl?: string;
@@ -141,6 +142,13 @@ export class Circuits implements CircuitsInterface {
141
142
  this._fetchVersionedArtifact(["artifacts", assetName.vkey].join("/")),
142
143
  this._fetchVersionedArtifact(["artifacts", assetName.zkey].join("/")),
143
144
  ]);
145
+
146
+ await Promise.all([
147
+ verifyArtifactIntegrity(circuitName, "wasm", wasm),
148
+ verifyArtifactIntegrity(circuitName, "vkey", vkey),
149
+ verifyArtifactIntegrity(circuitName, "zkey", zkey),
150
+ ]);
151
+
144
152
  return { wasm, vkey, zkey };
145
153
  }
146
154
 
@@ -2,6 +2,7 @@ import { poseidon } from "maci-crypto/build/ts/hashing.js";
2
2
  import { Hash, Secret } from "../types/commitment.js";
3
3
  import { Hex, bytesToNumber } from "viem";
4
4
  import { mnemonicToAccount } from "viem/accounts";
5
+ import { generateMasterKeys } from "../crypto.js";
5
6
  import { mapLimit } from "async";
6
7
  import { DataService } from "./data.service.js";
7
8
  import {
@@ -71,7 +72,7 @@ export class AccountService {
71
72
  }
72
73
 
73
74
  /**
74
- * Initializes a new account from a mnemonic phrase.
75
+ * Initializes a new account from a mnemonic phrase for the legacy account.
75
76
  *
76
77
  * @param mnemonic - The mnemonic phrase to derive keys from
77
78
  * @returns A new PrivacyPoolAccount with derived master keys
@@ -85,9 +86,8 @@ export class AccountService {
85
86
  * @throws {AccountError} If account initialization fails
86
87
  * @private
87
88
  */
88
- private _initializeAccount(mnemonic: string): PrivacyPoolAccount {
89
+ protected static _initializeLegacyAccount(mnemonic: string): PrivacyPoolAccount {
89
90
  try {
90
- this.logger.debug("Initializing account with mnemonic");
91
91
 
92
92
  const masterNullifierSeed = bytesToNumber(
93
93
  mnemonicToAccount(mnemonic, { accountIndex: 0 }).getHdKey().privateKey!
@@ -113,6 +113,40 @@ export class AccountService {
113
113
  }
114
114
  }
115
115
 
116
+ /**
117
+ * Initializes a new account from a mnemonic phrase.
118
+ *
119
+ * @param mnemonic - The mnemonic phrase to derive keys from
120
+ * @returns A new PrivacyPoolAccount with derived master keys
121
+ *
122
+ * @remarks
123
+ * This method derives two master keys from the mnemonic:
124
+ * 1. A master nullifier key from account index 0
125
+ * 2. A master secret key from account index 1
126
+ * These keys are used to deterministically generate nullifiers and secrets for deposits and withdrawals.
127
+ *
128
+ * @throws {AccountError} If account initialization fails
129
+ * @private
130
+ */
131
+ private _initializeAccount(mnemonic: string): PrivacyPoolAccount {
132
+ try {
133
+ this.logger.debug("Initializing account with mnemonic");
134
+
135
+ const { masterNullifier, masterSecret } = generateMasterKeys(mnemonic);
136
+
137
+ return {
138
+ masterKeys: [masterNullifier, masterSecret],
139
+ poolAccounts: new Map(),
140
+ creationTimestamp: 0n,
141
+ lastUpdateTimestamp: 0n,
142
+ };
143
+ } catch (error) {
144
+ throw AccountError.accountInitializationFailed(
145
+ error instanceof Error ? error.message : "Unknown error"
146
+ );
147
+ }
148
+ }
149
+
116
150
  /**
117
151
  * Generates a deterministic nullifier for a deposit.
118
152
  *
@@ -212,7 +246,7 @@ export class AccountService {
212
246
 
213
247
  for (const account of accounts) {
214
248
  // Skip accounts that have been ragequit
215
- if (account.ragequit) {
249
+ if (account.ragequit || account.isMigrated) {
216
250
  continue;
217
251
  }
218
252
 
@@ -426,6 +460,75 @@ export class AccountService {
426
460
  return newCommitment;
427
461
  }
428
462
 
463
+ /**
464
+ * Adds a new commitment to the account after migrate
465
+ *
466
+ * @param parentCommitment - The commitment that was spent
467
+ * @param value - The remaining value after spending
468
+ * @param nullifier - The nullifier used for migrate
469
+ * @param secret - The secret used for migrate
470
+ * @param blockNumber - The block number of the withdrawal
471
+ * @param txHash - The transaction hash of the withdrawal
472
+ * @returns The new commitment
473
+ *
474
+ * @remarks
475
+ * This method finds the account containing the parent commitment, creates a new
476
+ * commitment with the provided parameters, and adds it to the account's children.
477
+ * The new commitment inherits the label from the parent commitment.
478
+ *
479
+ * @throws {AccountError} If no account is found for the commitment
480
+ */
481
+ public addMigrationCommitment(
482
+ parentCommitment: AccountCommitment,
483
+ value: bigint,
484
+ nullifier: Secret,
485
+ secret: Secret,
486
+ blockNumber: bigint,
487
+ txHash: Hex
488
+ ): AccountCommitment {
489
+ let foundAccount: PoolAccount | undefined;
490
+ let foundScope: bigint | undefined;
491
+
492
+ for (const [scope, accounts] of this.account.poolAccounts.entries()) {
493
+ foundAccount = accounts.find((account) => {
494
+ if (account.deposit.hash === parentCommitment.hash) return true;
495
+ return account.children.some(
496
+ (child) => child.hash === parentCommitment.hash
497
+ );
498
+ });
499
+
500
+ if (foundAccount) {
501
+ foundScope = scope;
502
+ break;
503
+ }
504
+ }
505
+
506
+ if (!foundAccount || !foundScope) {
507
+ throw AccountError.commitmentNotFound(parentCommitment.hash);
508
+ }
509
+
510
+ const precommitment = this._hashPrecommitment(nullifier, secret);
511
+ const newCommitment: AccountCommitment = {
512
+ hash: this._hashCommitment(value, parentCommitment.label, precommitment),
513
+ value,
514
+ label: parentCommitment.label,
515
+ nullifier,
516
+ secret,
517
+ blockNumber,
518
+ txHash,
519
+ isMigration: true
520
+ };
521
+
522
+ foundAccount.children.push(newCommitment);
523
+ foundAccount.isMigrated = true;
524
+
525
+ this.logger.info(
526
+ `Added new commitment with value ${value} to account with label ${parentCommitment.label}`
527
+ );
528
+
529
+ return newCommitment;
530
+ }
531
+
429
532
  /**
430
533
  * Adds a ragequit event to an existing pool account
431
534
  *
@@ -646,14 +749,15 @@ export class AccountService {
646
749
  */
647
750
  private _processDepositEvents(
648
751
  scope: Hash,
649
- depositEvents: Map<Hash, DepositEvent>
752
+ depositEvents: Map<Hash, DepositEvent>,
753
+ startIndex: bigint = 0n,
650
754
  ): void {
651
755
  const MAX_CONSECUTIVE_MISSES = 10; // Large enough to avoid tx failures
652
756
 
653
757
  const foundIndices = new Set<bigint>();
654
758
  let consecutiveMisses = 0;
655
759
 
656
- for (let index = BigInt(0); ; index++) {
760
+ for (let index = startIndex; ; index++) {
657
761
  // Generate nullifier, secret, and precommitment for this index
658
762
  const { nullifier, secret, precommitment } = this.createDepositSecrets(
659
763
  scope,
@@ -727,9 +831,9 @@ export class AccountService {
727
831
  // Process each account in parallel for better performance
728
832
  for (const account of accounts) {
729
833
  let currentCommitment = account.deposit;
730
- let index = BigInt(0);
834
+ let index = BigInt(account.children.length);
731
835
 
732
- // Continue processing withdrawals until no more are found secuentially
836
+ // Continue processing withdrawals until no more are found sequentially
733
837
  while (true) {
734
838
  // Generate nullifier for this withdrawal
735
839
  const nullifierHash = poseidon([currentCommitment.nullifier]) as Hash;
@@ -740,22 +844,48 @@ export class AccountService {
740
844
  break;
741
845
  }
742
846
 
847
+ const remainingValue = currentCommitment.value - withdrawal.withdrawn;
848
+
743
849
  // Generate secret for this withdrawal
744
850
  const nullifier = this._genWithdrawalNullifier(account.label, index);
745
851
  const secret = this._genWithdrawalSecret(account.label, index);
852
+ const precommitment = this._hashPrecommitment(nullifier, secret);
853
+ const accountCommitment = this._hashCommitment(remainingValue, currentCommitment.label, precommitment)
854
+
855
+
856
+ // If the locally-computed hash doesn't match the on-chain commitment,
857
+ // the withdrawal was performed with different keys (e.g. migration from
858
+ // legacy to safe keys). Mark the child as unspendable from this account.
859
+ if (accountCommitment !== withdrawal.newCommitment) {
860
+ this.logger.info(
861
+ `Withdrawal commitment hash mismatch — marking as unspendable (migrated with different keys)`,
862
+ { label: currentCommitment.label, expected: withdrawal.newCommitment, computed: accountCommitment }
863
+ );
864
+
865
+ // Add the withdrawal commitment to the account
866
+ const migrationCommitment = this.addMigrationCommitment(
867
+ currentCommitment,
868
+ remainingValue,
869
+ nullifier,
870
+ secret,
871
+ withdrawal.blockNumber,
872
+ withdrawal.transactionHash
873
+ );
874
+
875
+ currentCommitment = migrationCommitment;
876
+ } else {
877
+ // Add the withdrawal commitment to the account
878
+ const withdrawalCommitment = this.addWithdrawalCommitment(
879
+ currentCommitment,
880
+ remainingValue,
881
+ nullifier,
882
+ secret,
883
+ withdrawal.blockNumber,
884
+ withdrawal.transactionHash
885
+ );
746
886
 
747
- // Add the withdrawal commitment to the account
748
- const newCommitment = this.addWithdrawalCommitment(
749
- currentCommitment,
750
- currentCommitment.value - withdrawal.withdrawn,
751
- nullifier,
752
- secret,
753
- withdrawal.blockNumber,
754
- withdrawal.transactionHash
755
- );
756
-
757
- // Update current commitment to the newly created one
758
- currentCommitment = newCommitment;
887
+ currentCommitment = withdrawalCommitment;
888
+ }
759
889
 
760
890
  // Increment index for next potential withdrawal
761
891
  index++;
@@ -799,6 +929,88 @@ export class AccountService {
799
929
  }
800
930
  }
801
931
 
932
+ /**
933
+ * Discovers commitments that were migrated from legacy accounts via 0-value withdrawal.
934
+ *
935
+ * @param scope - The scope of the pool
936
+ * @param legacyAccounts - The legacy pool accounts for this scope
937
+ * @param withdrawalEvents - The map of withdrawal events (keyed by spentNullifier)
938
+ *
939
+ * @remarks
940
+ * When a legacy account performs a 0-value withdrawal to rotate keys (migration),
941
+ * the resulting on-chain commitment is created with safe keys. This method finds
942
+ * those commitments by:
943
+ * 1. Identifying legacy accounts with the `isMigrated` flag (set by `addMigrationCommitment`)
944
+ * 2. Computing the expected commitment hash using safe keys at withdrawal index 0
945
+ * 3. Verifying the hash exists in on-chain withdrawal events
946
+ * 4. Adding verified commitments as new safe pool accounts
947
+ *
948
+ * @private
949
+ */
950
+ private _discoverMigratedCommitments(
951
+ scope: Hash,
952
+ legacyAccounts: PoolAccount[],
953
+ withdrawalEvents: Map<Hash, WithdrawalEvent>
954
+ ): void {
955
+ // Build reverse lookup: newCommitment hash → WithdrawalEvent
956
+ const newCommitmentMap = new Map<Hash, WithdrawalEvent>();
957
+ for (const event of withdrawalEvents.values()) {
958
+ newCommitmentMap.set(event.newCommitment, event);
959
+ }
960
+
961
+ for (const legacyAccount of legacyAccounts) {
962
+ // Skip if not flagged as migrated (set by addMigrationCommitment)
963
+ if (!legacyAccount.isMigrated) continue;
964
+
965
+ const migrationChild = legacyAccount.children.find(c => c.isMigration);
966
+ if (!migrationChild) continue;
967
+
968
+ const label = legacyAccount.label;
969
+
970
+ // The migration child's value is the remaining value carried forward.
971
+ // Zero-value migrations (full withdrawal + key rotation) are valid and
972
+ // must still be registered so that poolAccounts.length reflects the
973
+ // correct slot count for deposit index alignment in step C.
974
+ const remainingValue = migrationChild.value;
975
+
976
+ // Generate safe nullifier/secret at withdrawal index 0
977
+ const nullifier = this._genWithdrawalNullifier(label, 0n);
978
+ const secret = this._genWithdrawalSecret(label, 0n);
979
+
980
+ // Compute expected commitment hash
981
+ const precommitment = this._hashPrecommitment(nullifier, secret);
982
+ const expectedHash = this._hashCommitment(remainingValue, label, precommitment);
983
+
984
+ // Verify hash exists in withdrawal events' newCommitment
985
+ const withdrawalEvent = newCommitmentMap.get(expectedHash);
986
+ if (!withdrawalEvent) continue;
987
+
988
+ // Verified — add as a new safe pool account
989
+ const newAccount = this.addPoolAccount(
990
+ scope,
991
+ remainingValue,
992
+ nullifier,
993
+ secret,
994
+ label,
995
+ withdrawalEvent.blockNumber,
996
+ withdrawalEvent.transactionHash,
997
+ );
998
+
999
+ this.addWithdrawalCommitment(
1000
+ newAccount.deposit,
1001
+ remainingValue,
1002
+ nullifier,
1003
+ secret,
1004
+ withdrawalEvent.blockNumber,
1005
+ withdrawalEvent.transactionHash,
1006
+ )
1007
+
1008
+ this.logger.info(
1009
+ `Discovered migrated commitment for label ${label} with value ${remainingValue}`,
1010
+ );
1011
+ }
1012
+ }
1013
+
802
1014
  /**
803
1015
  * Initializes an AccountService instance with events for a given set of pools
804
1016
  *
@@ -830,7 +1042,7 @@ export class AccountService {
830
1042
  service: AccountService;
831
1043
  },
832
1044
  pools: PoolInfo[]
833
- ): Promise<{ account: AccountService; errors: PoolEventsError[] }> {
1045
+ ): Promise<{ account: AccountService; legacyAccount?: AccountService; errors: PoolEventsError[] }> {
834
1046
  // Log the start of the history retrieval process
835
1047
  const logger = new Logger({ prefix: "Account" });
836
1048
  logger.info(`Fetching events for pools`, { poolLength: pools.length });
@@ -844,32 +1056,114 @@ export class AccountService {
844
1056
  uniqueScopes.add(pool.scope);
845
1057
  }
846
1058
 
1059
+ // Retry path (non-migration): reuse the existing service's account and
1060
+ // only process pools whose scopes haven't been fully processed yet.
1061
+ // Already-processed scopes are skipped to avoid duplicate deposits and
1062
+ // withdrawal misclassification.
1063
+ //
1064
+ // This path performs simple deposit/withdrawal/ragequit processing only
1065
+ // — no migration discovery. For migration-aware retries, the caller
1066
+ // should re-invoke with { mnemonic } scoped to only the failed pools;
1067
+ // the mnemonic path builds both safe and legacy accounts from scratch
1068
+ // with no shared references.
1069
+ if (!('mnemonic' in source)) {
1070
+ const account = new AccountService(
1071
+ dataService,
1072
+ { account: source.service.account }
1073
+ );
1074
+ const processedScopes = source.service.account.poolAccounts;
1075
+ const newPools = pools.filter((p) => !processedScopes.has(p.scope));
1076
+
1077
+ const errors = await account._processEvents(newPools);
1078
+ return { account, errors };
1079
+ }
1080
+
1081
+ // Mnemonic path: phased processing with migration discovery
1082
+ const account = new AccountService(dataService, { mnemonic: source.mnemonic });
1083
+ const legacyPrivacyPoolAccount = AccountService._initializeLegacyAccount(source.mnemonic);
1084
+ const legacyAccount = new AccountService(dataService, { account: legacyPrivacyPoolAccount });
1085
+
1086
+ const errors = await account._processEvents(pools, legacyAccount);
1087
+ return { account, legacyAccount, errors };
1088
+ }
1089
+
1090
+ /**
1091
+ * Fetches and processes events for a set of pools.
1092
+ *
1093
+ * When a legacyAccount is provided, the full migration-aware pipeline runs
1094
+ * for each scope:
1095
+ * 1. Legacy account: process deposits and withdrawals (to detect migrations)
1096
+ * 2. Safe account: discover migrated commitments from the legacy accounts
1097
+ * 3. Safe account (this): process deposits (starting after migrated accounts)
1098
+ * 4. Safe account: process withdrawals (now includes migrated accounts)
1099
+ * 5. Both accounts: process ragequits
1100
+ *
1101
+ * Migration discovery (step 2) must run before safe deposit scanning (step 3)
1102
+ * so that the migrated account count can be used as the starting index.
1103
+ * Post-migration deposits use poolAccounts.length as their index, which
1104
+ * sits right after the migrated slots; scanning from 0 would hit
1105
+ * MAX_CONSECUTIVE_MISSES on the legacy-key indices and never reach them.
1106
+ *
1107
+ * Without a legacyAccount, only steps 3, 4, and 5 run (simple processing).
1108
+ *
1109
+ * Per-scope errors are caught and returned rather than thrown, and any
1110
+ * partial state left by a mid-scope failure is cleaned from both accounts
1111
+ * so that a subsequent retry starts fresh for that scope.
1112
+ */
1113
+ private async _processEvents(
1114
+ pools: PoolInfo[],
1115
+ legacyAccount?: AccountService,
1116
+ ): Promise<PoolEventsError[]> {
847
1117
  const errors: PoolEventsError[] = [];
848
- const account = new AccountService(
849
- dataService,
850
- "mnemonic" in source
851
- ? { mnemonic: source.mnemonic }
852
- : { account: source.service.account }
853
- );
854
1118
 
855
- const events = await account.getEvents(pools);
1119
+ const events = await this.getEvents(pools);
856
1120
 
857
1121
  for (const [scope, result] of events.entries()) {
858
1122
  if ("reason" in result) {
859
1123
  errors.push(result);
860
1124
  } else {
861
- // Process deposit events an create pool accounts
862
- account._processDepositEvents(scope, result.depositEvents);
1125
+ try {
1126
+ // a. Legacy: process deposits + withdrawals
1127
+ if (legacyAccount) {
1128
+ legacyAccount._processDepositEvents(scope, result.depositEvents);
1129
+ legacyAccount._processWithdrawalEvents(scope, result.withdrawalEvents);
1130
+ }
1131
+
1132
+ // b. Safe: discover migrated commitments from legacy accounts.
1133
+ // Must run before safe deposit scanning so that the migrated
1134
+ // account count can serve as the starting index for step (c),
1135
+ // avoiding a gap of consecutive misses over legacy-key indices.
1136
+ if (legacyAccount) {
1137
+ const legacyAccounts = legacyAccount.account.poolAccounts.get(scope) ?? [];
1138
+ this._discoverMigratedCommitments(scope, legacyAccounts, result.withdrawalEvents);
1139
+ }
1140
+
1141
+ // c. Safe: process deposits, starting after any migrated accounts.
1142
+ // New deposits created after migration use poolAccounts.length as
1143
+ // their index, so they sit right after the migrated slots.
1144
+ const depositStartIndex = BigInt(this.account.poolAccounts.get(scope)?.length ?? 0);
1145
+ this._processDepositEvents(scope, result.depositEvents, depositStartIndex);
863
1146
 
864
- // Process withdrawal events and add commitments to pool accounts
865
- account._processWithdrawalEvents(scope, result.withdrawalEvents);
1147
+ // d. Safe: process withdrawals (now includes migrated accounts)
1148
+ this._processWithdrawalEvents(scope, result.withdrawalEvents);
866
1149
 
867
- // Process ragequit events and add ragequit to pool accounts
868
- account._processRagequitEvents(scope, result.ragequitEvents);
1150
+ // e. Both: process ragequits
1151
+ if (legacyAccount) {
1152
+ legacyAccount._processRagequitEvents(scope, result.ragequitEvents);
1153
+ }
1154
+ this._processRagequitEvents(scope, result.ragequitEvents);
1155
+ } catch (e) {
1156
+ this.account.poolAccounts.delete(scope);
1157
+ legacyAccount?.account.poolAccounts.delete(scope);
1158
+ errors.push({
1159
+ reason: e instanceof Error ? e.message : String(e),
1160
+ scope,
1161
+ });
1162
+ }
869
1163
  }
870
1164
  }
871
1165
 
872
- return { account, errors };
1166
+ return errors;
873
1167
  }
874
1168
 
875
1169
  /**
@@ -158,7 +158,7 @@ export class DataService {
158
158
  depositor: depositor.toLowerCase(),
159
159
  commitment: commitment as Hash,
160
160
  label: label as Hash,
161
- value: value || BigInt(0),
161
+ value: value ?? BigInt(0),
162
162
  precommitment: precommitment as Hash,
163
163
  blockNumber: BigInt(typedLog.blockNumber),
164
164
  transactionHash: typedLog.transactionHash,
@@ -252,14 +252,7 @@ export class DataService {
252
252
  _newCommitment: newCommitment,
253
253
  } = typedLog.args;
254
254
 
255
- if (
256
- value === undefined ||
257
- value === null ||
258
- !spentNullifier ||
259
- !newCommitment ||
260
- !typedLog.blockNumber ||
261
- !typedLog.transactionHash
262
- ) {
255
+ if (value == null || !spentNullifier || !newCommitment || !typedLog.blockNumber || !typedLog.transactionHash) {
263
256
  throw DataError.invalidLog("withdrawal", "missing required fields");
264
257
  }
265
258
 
@@ -375,7 +368,7 @@ export class DataService {
375
368
  ragequitter: ragequitter.toLowerCase(),
376
369
  commitment: commitment as Hash,
377
370
  label: label as Hash,
378
- value: value || BigInt(0),
371
+ value: value ?? BigInt(0),
379
372
  blockNumber: BigInt(typedLog.blockNumber),
380
373
  transactionHash: typedLog.transactionHash,
381
374
  };
@@ -0,0 +1,4 @@
1
+ "0x4626A182030D9e98b13f690FFF3C443191a918ff",
2
+ "0x2C7bCaf3f966F20506096696bA9Beae216D005a4",
3
+ "0x3706e38af05bf0158BCdbB46239f8289980b093f",
4
+ "0xA63e0bdc3A193d1E6e7c9bE72CB502BE4B7fC244",
package/src/crypto.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { mnemonicToAccount } from "viem/accounts";
2
- import { bytesToNumber } from "viem/utils";
3
2
  import { poseidon } from "maci-crypto/build/ts/hashing.js";
4
3
  import { LeanIMT, LeanIMTMerkleProof } from "@zk-kit/lean-imt";
5
4
  import {
@@ -13,7 +12,7 @@ import {
13
12
  Withdrawal,
14
13
  MasterKeys,
15
14
  } from "./types/index.js";
16
- import { encodeAbiParameters, Hex, keccak256, numberToHex } from "viem";
15
+ import { bytesToBigInt, encodeAbiParameters, Hex, keccak256, numberToHex } from "viem";
17
16
  import { SNARK_SCALAR_FIELD } from "./constants.js";
18
17
 
19
18
  /**
@@ -45,16 +44,16 @@ export function generateMasterKeys(mnemonic: string): MasterKeys {
45
44
  );
46
45
  }
47
46
 
48
- const key1 = bytesToNumber(
47
+ const key1 = bytesToBigInt(
49
48
  mnemonicToAccount(mnemonic, { accountIndex: 0 }).getHdKey().privateKey!,
50
49
  );
51
50
 
52
- const key2 = bytesToNumber(
51
+ const key2 = bytesToBigInt(
53
52
  mnemonicToAccount(mnemonic, { accountIndex: 1 }).getHdKey().privateKey!,
54
53
  );
55
54
 
56
- const masterNullifier = poseidon([BigInt(key1)]) as Secret;
57
- const masterSecret = poseidon([BigInt(key2)]) as Secret;
55
+ const masterNullifier = poseidon([key1]) as Secret;
56
+ const masterSecret = poseidon([key2]) as Secret;
58
57
 
59
58
  return { masterNullifier, masterSecret };
60
59
  }