@unlink-xyz/core 0.1.3-canary.fd5dddf → 0.1.4

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 (213) hide show
  1. package/README.md +9 -0
  2. package/dist/account/account.d.ts +31 -2
  3. package/dist/account/account.d.ts.map +1 -1
  4. package/dist/account/accounts.d.ts +42 -0
  5. package/dist/account/accounts.d.ts.map +1 -0
  6. package/dist/account/seed.d.ts +45 -0
  7. package/dist/account/seed.d.ts.map +1 -0
  8. package/dist/account/serialization.d.ts +6 -0
  9. package/dist/account/serialization.d.ts.map +1 -0
  10. package/dist/browser/index.js +34424 -86406
  11. package/dist/browser/index.js.map +1 -1
  12. package/dist/browser/wallet/index.js +55942 -0
  13. package/dist/browser/wallet/index.js.map +1 -0
  14. package/dist/clients/broadcaster.d.ts +1 -0
  15. package/dist/clients/broadcaster.d.ts.map +1 -1
  16. package/dist/clients/indexer.d.ts +5 -0
  17. package/dist/clients/indexer.d.ts.map +1 -1
  18. package/dist/config.d.ts +6 -4
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/core.d.ts.map +1 -1
  21. package/dist/crypto/adapters/index.d.ts +17 -0
  22. package/dist/crypto/adapters/index.d.ts.map +1 -0
  23. package/dist/crypto/adapters/polyfills.d.ts +5 -0
  24. package/dist/crypto/adapters/polyfills.d.ts.map +1 -0
  25. package/dist/crypto/encrypt.d.ts +33 -0
  26. package/dist/crypto/encrypt.d.ts.map +1 -0
  27. package/dist/crypto/secure-memory.d.ts.map +1 -0
  28. package/dist/errors.d.ts +8 -0
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/index.d.ts +6 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +6721 -23
  33. package/dist/index.js.map +1 -0
  34. package/dist/keys/derive.d.ts +2 -2
  35. package/dist/keys/derive.d.ts.map +1 -1
  36. package/dist/keys/hex.d.ts +1 -4
  37. package/dist/keys/hex.d.ts.map +1 -1
  38. package/dist/keys/mnemonic.d.ts +0 -2
  39. package/dist/keys/mnemonic.d.ts.map +1 -1
  40. package/dist/keys.d.ts +1 -0
  41. package/dist/keys.d.ts.map +1 -1
  42. package/dist/prover/config.d.ts +54 -9
  43. package/dist/prover/config.d.ts.map +1 -1
  44. package/dist/prover/integrity.d.ts +20 -0
  45. package/dist/prover/integrity.d.ts.map +1 -0
  46. package/dist/prover/prover.d.ts +16 -31
  47. package/dist/prover/prover.d.ts.map +1 -1
  48. package/dist/state/merkle/hydrator.d.ts +21 -19
  49. package/dist/state/merkle/hydrator.d.ts.map +1 -1
  50. package/dist/state/merkle/index.d.ts +1 -1
  51. package/dist/state/merkle/index.d.ts.map +1 -1
  52. package/dist/state/store/ciphertext-store.d.ts +7 -0
  53. package/dist/state/store/ciphertext-store.d.ts.map +1 -1
  54. package/dist/state/store/index.d.ts +1 -1
  55. package/dist/state/store/index.d.ts.map +1 -1
  56. package/dist/state/store/job-store.d.ts.map +1 -1
  57. package/dist/state/store/jobs.d.ts +14 -16
  58. package/dist/state/store/jobs.d.ts.map +1 -1
  59. package/dist/state/store/leaf-store.d.ts +4 -0
  60. package/dist/state/store/leaf-store.d.ts.map +1 -1
  61. package/dist/state/store/nullifier-store.d.ts.map +1 -1
  62. package/dist/state/store/records.d.ts +8 -0
  63. package/dist/state/store/records.d.ts.map +1 -1
  64. package/dist/state/store/store.d.ts +18 -0
  65. package/dist/state/store/store.d.ts.map +1 -1
  66. package/dist/storage/indexeddb.d.ts.map +1 -1
  67. package/dist/storage/memory.d.ts.map +1 -1
  68. package/dist/transactions/adapter.d.ts +31 -0
  69. package/dist/transactions/adapter.d.ts.map +1 -0
  70. package/dist/transactions/deposit.d.ts +1 -1
  71. package/dist/transactions/deposit.d.ts.map +1 -1
  72. package/dist/transactions/index.d.ts +4 -2
  73. package/dist/transactions/index.d.ts.map +1 -1
  74. package/dist/transactions/note-sync.d.ts +3 -3
  75. package/dist/transactions/note-sync.d.ts.map +1 -1
  76. package/dist/transactions/reconcile.d.ts +1 -1
  77. package/dist/transactions/reconcile.d.ts.map +1 -1
  78. package/dist/transactions/transact.d.ts +21 -2
  79. package/dist/transactions/transact.d.ts.map +1 -1
  80. package/dist/transactions/transaction-planner.d.ts +1 -1
  81. package/dist/transactions/transaction-planner.d.ts.map +1 -1
  82. package/dist/transactions/transfer-planner.d.ts +2 -1
  83. package/dist/transactions/transfer-planner.d.ts.map +1 -1
  84. package/dist/transactions/types/deposit.d.ts +3 -3
  85. package/dist/transactions/types/deposit.d.ts.map +1 -1
  86. package/dist/transactions/types/domain.d.ts +3 -0
  87. package/dist/transactions/types/domain.d.ts.map +1 -1
  88. package/dist/transactions/types/options.d.ts +14 -5
  89. package/dist/transactions/types/options.d.ts.map +1 -1
  90. package/dist/transactions/types/planning.d.ts +2 -0
  91. package/dist/transactions/types/planning.d.ts.map +1 -1
  92. package/dist/transactions/types/state-stores.d.ts +53 -5
  93. package/dist/transactions/types/state-stores.d.ts.map +1 -1
  94. package/dist/transactions/types/transact.d.ts +10 -3
  95. package/dist/transactions/types/transact.d.ts.map +1 -1
  96. package/dist/transactions/withdrawal-planner.d.ts +1 -1
  97. package/dist/transactions/withdrawal-planner.d.ts.map +1 -1
  98. package/dist/tsconfig.tsbuildinfo +1 -1
  99. package/dist/tsup.config.d.ts +8 -0
  100. package/dist/tsup.config.d.ts.map +1 -0
  101. package/dist/types.d.ts +1 -0
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/utils/amounts.d.ts +0 -13
  104. package/dist/utils/amounts.d.ts.map +1 -1
  105. package/dist/utils/async.js +37 -34
  106. package/dist/utils/async.js.map +1 -0
  107. package/dist/utils/bigint.d.ts +0 -2
  108. package/dist/utils/bigint.d.ts.map +1 -1
  109. package/dist/utils/random.d.ts +5 -0
  110. package/dist/utils/random.d.ts.map +1 -1
  111. package/dist/utils/validators.d.ts.map +1 -1
  112. package/dist/vitest.config.d.ts.map +1 -1
  113. package/dist/wallet/adapter.d.ts +21 -0
  114. package/dist/wallet/adapter.d.ts.map +1 -0
  115. package/dist/wallet/burner/service.d.ts +32 -0
  116. package/dist/wallet/burner/service.d.ts.map +1 -0
  117. package/dist/wallet/burner/types.d.ts +47 -0
  118. package/dist/wallet/burner/types.d.ts.map +1 -0
  119. package/dist/wallet/index.d.ts +20 -0
  120. package/dist/wallet/index.d.ts.map +1 -0
  121. package/dist/wallet/index.js +6462 -0
  122. package/dist/wallet/index.js.map +1 -0
  123. package/dist/wallet/sdk.d.ts +48 -0
  124. package/dist/wallet/sdk.d.ts.map +1 -0
  125. package/dist/wallet/types.d.ts +457 -0
  126. package/dist/wallet/types.d.ts.map +1 -0
  127. package/dist/wallet/unlink-wallet.d.ts +187 -0
  128. package/dist/wallet/unlink-wallet.d.ts.map +1 -0
  129. package/package.json +16 -6
  130. package/dist/account/account.js +0 -142
  131. package/dist/circuits.json +0 -74
  132. package/dist/clients/broadcaster.js +0 -30
  133. package/dist/clients/http.js +0 -72
  134. package/dist/clients/indexer.js +0 -94
  135. package/dist/config.js +0 -36
  136. package/dist/constants.js +0 -5
  137. package/dist/core.js +0 -15
  138. package/dist/crypto-adapters/auto-init.d.ts +0 -2
  139. package/dist/crypto-adapters/auto-init.d.ts.map +0 -1
  140. package/dist/crypto-adapters/auto-init.js +0 -7
  141. package/dist/crypto-adapters/index.d.ts +0 -22
  142. package/dist/crypto-adapters/index.d.ts.map +0 -1
  143. package/dist/crypto-adapters/index.js +0 -47
  144. package/dist/crypto-adapters/polyfills.d.ts +0 -5
  145. package/dist/crypto-adapters/polyfills.d.ts.map +0 -1
  146. package/dist/crypto-adapters/polyfills.js +0 -8
  147. package/dist/errors.js +0 -36
  148. package/dist/history/index.js +0 -2
  149. package/dist/history/service.js +0 -354
  150. package/dist/history/types.js +0 -1
  151. package/dist/keys/address.js +0 -55
  152. package/dist/keys/derive.js +0 -112
  153. package/dist/keys/hex.js +0 -66
  154. package/dist/keys/index.js +0 -4
  155. package/dist/keys/mnemonic.js +0 -23
  156. package/dist/keys.js +0 -45
  157. package/dist/prover/config.js +0 -70
  158. package/dist/prover/index.js +0 -1
  159. package/dist/prover/prover.js +0 -291
  160. package/dist/prover/registry.js +0 -18
  161. package/dist/schema.js +0 -14
  162. package/dist/state/index.js +0 -2
  163. package/dist/state/merkle/hydrator.js +0 -37
  164. package/dist/state/merkle/index.js +0 -2
  165. package/dist/state/merkle/merkle-tree.js +0 -113
  166. package/dist/state/store/ciphertext-store.js +0 -37
  167. package/dist/state/store/history-store.js +0 -53
  168. package/dist/state/store/index.js +0 -9
  169. package/dist/state/store/job-store.js +0 -144
  170. package/dist/state/store/jobs.js +0 -1
  171. package/dist/state/store/leaf-store.js +0 -32
  172. package/dist/state/store/note-store.js +0 -146
  173. package/dist/state/store/nullifier-store.js +0 -60
  174. package/dist/state/store/records.js +0 -1
  175. package/dist/state/store/root-store.js +0 -26
  176. package/dist/state/store/store.js +0 -113
  177. package/dist/storage/index.js +0 -2
  178. package/dist/storage/indexeddb.js +0 -205
  179. package/dist/storage/memory.js +0 -91
  180. package/dist/transactions/deposit.js +0 -220
  181. package/dist/transactions/index.js +0 -9
  182. package/dist/transactions/note-selection.js +0 -201
  183. package/dist/transactions/note-sync.js +0 -485
  184. package/dist/transactions/reconcile.js +0 -85
  185. package/dist/transactions/transact.js +0 -450
  186. package/dist/transactions/transaction-planner.js +0 -116
  187. package/dist/transactions/transfer-planner.js +0 -85
  188. package/dist/transactions/types/deposit.js +0 -1
  189. package/dist/transactions/types/domain.js +0 -4
  190. package/dist/transactions/types/index.js +0 -17
  191. package/dist/transactions/types/options.js +0 -1
  192. package/dist/transactions/types/planning.js +0 -1
  193. package/dist/transactions/types/state-stores.js +0 -1
  194. package/dist/transactions/types/transact.js +0 -1
  195. package/dist/transactions/withdrawal-planner.js +0 -128
  196. package/dist/tsup.browser.config.js +0 -34
  197. package/dist/types.js +0 -1
  198. package/dist/utils/amounts.js +0 -89
  199. package/dist/utils/bigint.js +0 -29
  200. package/dist/utils/crypto.d.ts +0 -18
  201. package/dist/utils/crypto.d.ts.map +0 -1
  202. package/dist/utils/crypto.js +0 -45
  203. package/dist/utils/format.js +0 -33
  204. package/dist/utils/json-codec.js +0 -25
  205. package/dist/utils/notes.js +0 -14
  206. package/dist/utils/polling.js +0 -11
  207. package/dist/utils/random.js +0 -27
  208. package/dist/utils/secure-memory.d.ts.map +0 -1
  209. package/dist/utils/secure-memory.js +0 -28
  210. package/dist/utils/signature.js +0 -14
  211. package/dist/utils/validators.js +0 -96
  212. package/dist/vitest.config.js +0 -13
  213. /package/dist/{utils → crypto}/secure-memory.d.ts +0 -0
@@ -1,201 +0,0 @@
1
- import { ValidationError } from "../errors.js";
2
- import { CIRCUIT_REGISTRY } from "../prover/registry.js";
3
- /** Maximum inputs supported by available circuits */
4
- const MAX_CIRCUIT_INPUTS = Math.max(...Object.values(CIRCUIT_REGISTRY).map((c) => c.inputs));
5
- /** Maximum outputs supported by available circuits */
6
- const MAX_CIRCUIT_OUTPUTS = Math.max(...Object.values(CIRCUIT_REGISTRY).map((c) => c.outputs));
7
- /**
8
- * Get the value of a note as bigint.
9
- * Handles both bigint and string values for NoteRecord compatibility.
10
- */
11
- function getNoteValue(note) {
12
- return typeof note.value === "bigint" ? note.value : BigInt(note.value);
13
- }
14
- /**
15
- * Select notes for a transaction with consolidation-first strategy.
16
- *
17
- * Priorities:
18
- * 1. Consolidation - prefer spending more small notes
19
- * 2. Minimize change - find combinations with less overshoot
20
- * 3. Respect maxInputs constraint
21
- *
22
- * @param notes - Available unspent notes (caller filters by token)
23
- * @param totalNeeded - Total amount needed for all recipients
24
- * @param options - Selection options
25
- * @returns Selection result with notes and amounts
26
- * @throws ValidationError if insufficient balance or constraints can't be met
27
- */
28
- export function selectNotesForAmount(notes, totalNeeded, options) {
29
- const maxInputs = options?.maxInputs ?? MAX_CIRCUIT_INPUTS;
30
- const maxOutputs = options?.maxOutputs ?? MAX_CIRCUIT_OUTPUTS;
31
- const recipientCount = options?.recipientCount ?? 1;
32
- // Validate inputs
33
- if (totalNeeded <= 0n) {
34
- throw new ValidationError("Amount must be positive");
35
- }
36
- if (!notes.length) {
37
- throw new ValidationError("No notes available");
38
- }
39
- // Validate output constraints: recipients + potential change <= maxOutputs
40
- // Change is needed when selected sum > totalNeeded (almost always)
41
- // So we conservatively assume change will be needed
42
- if (recipientCount + 1 > maxOutputs) {
43
- throw new ValidationError(`Too many recipients: ${recipientCount} recipients + change exceeds maxOutputs (${maxOutputs})`);
44
- }
45
- // Check total balance
46
- const totalAvailable = notes.reduce((sum, n) => sum + getNoteValue(n), 0n);
47
- if (totalAvailable < totalNeeded) {
48
- throw new ValidationError("Insufficient balance");
49
- }
50
- // Sort ascending for consolidation preference (smallest first)
51
- const sortedAsc = [...notes].sort((a, b) => {
52
- const aVal = getNoteValue(a);
53
- const bVal = getNoteValue(b);
54
- return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
55
- });
56
- // Phase 1: Try greedy smallest-first
57
- let baseline = greedySelect(sortedAsc, totalNeeded, maxInputs);
58
- // Phase 2: If greedy smallest-first didn't work, try largest-first as fallback
59
- if (baseline.sum < totalNeeded) {
60
- const sortedDesc = [...sortedAsc].reverse();
61
- baseline = greedySelect(sortedDesc, totalNeeded, maxInputs);
62
- }
63
- // Phase 3: If we have a valid baseline, use branch-and-bound to optimize
64
- // Only run B&B on a subset of notes to keep it fast
65
- let optimized;
66
- if (baseline.sum >= totalNeeded) {
67
- // Limit B&B to reasonable subset for performance (30 smallest notes)
68
- const maxNotesForBnB = Math.min(sortedAsc.length, 30);
69
- const notesForBnB = sortedAsc.slice(0, maxNotesForBnB);
70
- optimized = findOptimalSelection(notesForBnB, totalNeeded, maxInputs, baseline);
71
- }
72
- else {
73
- optimized = baseline;
74
- }
75
- // If no valid solution found
76
- if (optimized.sum < totalNeeded) {
77
- throw new ValidationError("Cannot meet amount within maxInputs constraint");
78
- }
79
- return {
80
- selected: optimized.notes,
81
- totalInput: optimized.sum,
82
- totalNeeded,
83
- change: optimized.sum - totalNeeded,
84
- };
85
- }
86
- /**
87
- * Greedy selection: accumulate notes until threshold met.
88
- * Returns a partial solution if it can't meet the target.
89
- */
90
- function greedySelect(sorted, target, maxInputs) {
91
- const selected = [];
92
- let sum = 0n;
93
- for (const note of sorted) {
94
- if (sum >= target)
95
- break;
96
- if (selected.length >= maxInputs)
97
- break;
98
- selected.push(note);
99
- sum += getNoteValue(note);
100
- }
101
- return { notes: selected, sum };
102
- }
103
- /**
104
- * Compare two solutions.
105
- * Returns true if (candidate) is better than (best).
106
- *
107
- * Priority 0: Prefer valid solutions (sum >= target)
108
- * Priority 1: Prefer more notes (consolidation)
109
- * Priority 2: Prefer less change
110
- */
111
- function isBetterSolution(candidate, best, target) {
112
- const candidateValid = candidate.sum >= target;
113
- const bestValid = best.sum >= target;
114
- // Priority 0: Prefer valid solutions
115
- if (candidateValid && !bestValid)
116
- return true;
117
- if (!candidateValid && bestValid)
118
- return false;
119
- if (!candidateValid && !bestValid) {
120
- // Neither valid - prefer higher sum (closer to target)
121
- return candidate.sum > best.sum;
122
- }
123
- // Both valid - apply consolidation + change minimization
124
- const candidateChange = candidate.sum - target;
125
- const bestChange = best.sum - target;
126
- // Priority 1: Prefer more notes (consolidation)
127
- if (candidate.notes.length > best.notes.length)
128
- return true;
129
- if (candidate.notes.length < best.notes.length)
130
- return false;
131
- // Priority 2: Prefer less change
132
- return candidateChange < bestChange;
133
- }
134
- /**
135
- * Branch-and-bound search for optimal selection.
136
- *
137
- * Explores the solution space with aggressive pruning:
138
- * - Stop when sum >= target (don't over-select beyond what's needed)
139
- * - Stop when remaining notes can't reach target
140
- * - Stop when maxInputs exceeded
141
- * - Early termination when we find a solution with maxInputs notes and exact match
142
- */
143
- function findOptimalSelection(sorted, target, maxInputs, baseline) {
144
- let best = baseline;
145
- let foundOptimal = false;
146
- // Precompute remaining sums for pruning
147
- const remainingSums = new Array(sorted.length + 1).fill(0n);
148
- for (let i = sorted.length - 1; i >= 0; i--) {
149
- const nextSum = remainingSums[i + 1] ?? 0n;
150
- const note = sorted[i];
151
- const noteValue = note ? getNoteValue(note) : 0n;
152
- remainingSums[i] = nextSum + noteValue;
153
- }
154
- function search(index, selected, sum) {
155
- // Early exit if we found optimal solution
156
- if (foundOptimal)
157
- return;
158
- // Pruning: too many notes selected
159
- if (selected.length > maxInputs)
160
- return;
161
- // Found valid solution, now comparing with best
162
- if (sum >= target) {
163
- const candidate = { notes: selected, sum };
164
- if (isBetterSolution(candidate, best, target)) {
165
- best = { notes: [...selected], sum };
166
- // Check if this is optimal (maxInputs notes with exact match)
167
- if (selected.length === maxInputs && sum === target) {
168
- foundOptimal = true;
169
- }
170
- }
171
- // Don't add more notes once target is met
172
- return;
173
- }
174
- // Pruning: can't reach target with remaining notes
175
- const remaining = remainingSums[index] ?? 0n;
176
- if (sum + remaining < target)
177
- return;
178
- // Pruning: reached end of notes
179
- if (index >= sorted.length)
180
- return;
181
- // Pruning: can't possibly beat best's note count
182
- const remainingSlots = maxInputs - selected.length;
183
- const remainingNotes = sorted.length - index;
184
- if (selected.length + Math.min(remainingSlots, remainingNotes) <
185
- best.notes.length) {
186
- // Even selecting all remaining notes won't beat best's count
187
- // Only skip if best is already valid
188
- if (best.sum >= target)
189
- return;
190
- }
191
- const currentNote = sorted[index];
192
- if (!currentNote)
193
- return;
194
- // Try including current note (explore this branch first for consolidation)
195
- search(index + 1, [...selected, currentNote], sum + getNoteValue(currentNote));
196
- // Try excluding current note
197
- search(index + 1, selected, sum);
198
- }
199
- search(0, [], 0n);
200
- return best;
201
- }
@@ -1,485 +0,0 @@
1
- import { createIndexerClient } from "../clients/indexer.js";
2
- import { createServiceConfig } from "../config.js";
3
- import { poseidon } from "../crypto-adapters/index.js";
4
- import { CoreError, InitializationError } from "../errors.js";
5
- import { FieldSize, Hex } from "../keys/hex.js";
6
- import { createMerkleTrees, rebuildTreeFromStore, } from "../state/index.js";
7
- import { formatUint256, parseHexToBigInt } from "../utils/bigint.js";
8
- import { decryptNote } from "../utils/crypto.js";
9
- import { ensureChainId } from "../utils/validators.js";
10
- const DEFAULT_BATCH_SIZE = 256;
11
- function buildCiphertext(bytes) {
12
- return {
13
- data: [
14
- parseHexToBigInt(bytes[0]),
15
- parseHexToBigInt(bytes[1]),
16
- parseHexToBigInt(bytes[2]),
17
- ],
18
- };
19
- }
20
- function encodeCiphertext(ciphertext) {
21
- const payload = new Uint8Array(FieldSize.SCALAR * ciphertext.data.length);
22
- ciphertext.data.forEach((value, index) => {
23
- const start = index * FieldSize.SCALAR;
24
- payload.set(Hex.toBytes(formatUint256(value)), start);
25
- });
26
- return payload;
27
- }
28
- export function createNoteSyncService(stateStore, options = {}) {
29
- const trees = options.merkleTrees ?? createMerkleTrees();
30
- const limit = options.limit ?? DEFAULT_BATCH_SIZE;
31
- const status = new Map();
32
- // Initialize indexer client
33
- let indexerClient = options.indexerClient;
34
- if (!indexerClient) {
35
- const fetchImpl = options.fetch ?? (typeof fetch === "function" ? fetch : undefined);
36
- if (!fetchImpl) {
37
- throw new InitializationError("fetch dependency is required to sync indexer commitments");
38
- }
39
- if (!options.rpcUrl) {
40
- throw new InitializationError("rpcUrl is required to sync indexer commitments");
41
- }
42
- const serviceConfig = createServiceConfig(options.rpcUrl);
43
- indexerClient = createIndexerClient(serviceConfig.indexerBaseUrl, {
44
- fetch: fetchImpl,
45
- });
46
- }
47
- function createNoteRecord(params) {
48
- const { chainId, index, commitment, token, amount, npk, mpk, random, nullifier, createdTxHash, createdEventType, createdAt, spentAt, spentTxHash, } = params;
49
- const base = {
50
- chainId,
51
- index,
52
- token,
53
- value: amount.toString(),
54
- commitment,
55
- npk: formatUint256(npk),
56
- mpk: formatUint256(mpk),
57
- random: formatUint256(random),
58
- nullifier,
59
- createdTxHash,
60
- createdEventType,
61
- createdAt,
62
- spentTxHash,
63
- };
64
- return spentAt === undefined ? base : { ...base, spentAt };
65
- }
66
- // Try to decrypt a note. Returns the note record if successful, null otherwise.
67
- function tryDecryptNoteRecord(chainId, record, account) {
68
- const ciphertext = buildCiphertext(record.ciphertext);
69
- let note;
70
- try {
71
- note = decryptNote(ciphertext, account.masterPublicKey);
72
- }
73
- catch (err) {
74
- if (err instanceof CoreError) {
75
- return null;
76
- }
77
- throw err;
78
- }
79
- const random = note.random;
80
- const amount = note.amount;
81
- const token = note.token;
82
- const npk = poseidon([account.masterPublicKey, random]);
83
- const commitment = poseidon([npk, BigInt(token), amount]);
84
- if (commitment !== parseHexToBigInt(record.commitment)) {
85
- return null;
86
- }
87
- const nullifier = poseidon([account.nullifyingKey, BigInt(record.index)]);
88
- const nullifierHex = formatUint256(nullifier);
89
- return createNoteRecord({
90
- chainId,
91
- index: record.index,
92
- commitment: record.commitment,
93
- token,
94
- amount,
95
- npk,
96
- mpk: account.masterPublicKey,
97
- random,
98
- nullifier: nullifierHex,
99
- createdTxHash: record.txHash,
100
- createdEventType: record.eventType,
101
- createdAt: record.insertedAt != null ? record.insertedAt * 1000 : undefined,
102
- });
103
- }
104
- // Pulls commitments from the indexer, updating local tree and notes atomically.
105
- // Uses batch nullifier check to avoid individual 404-generating queries.
106
- async function ingestFrom(chainId, start, account) {
107
- let cursor = start;
108
- for (;;) {
109
- const batch = await indexerClient.fetchCommitmentBatch({
110
- chainId,
111
- start: cursor,
112
- limit,
113
- });
114
- if (batch.commitments.length === 0) {
115
- break;
116
- }
117
- // Phase 1: Ingest commitments into tree + store
118
- const decryptedNotes = [];
119
- for (const record of batch.commitments) {
120
- const expectedIndex = trees.getLeafCount(chainId);
121
- if (record.index !== expectedIndex) {
122
- throw new CoreError(`indexed commitments out of order: expected index ${expectedIndex}, got ${record.index} (cursor=${cursor})`);
123
- }
124
- const value = Hex.toBigInt(record.commitment);
125
- const { index } = trees.addLeaf(chainId, value);
126
- if (index !== record.index) {
127
- throw new CoreError("local merkle tree desynchronized");
128
- }
129
- const noteRecord = tryDecryptNoteRecord(chainId, record, account);
130
- const ciphertext = buildCiphertext(record.ciphertext);
131
- const ciphertextBytes = encodeCiphertext(ciphertext);
132
- await stateStore.syncCommitment({
133
- leaf: {
134
- chainId,
135
- index: record.index,
136
- commitment: record.commitment,
137
- txHash: record.txHash,
138
- eventType: record.eventType,
139
- insertedAt: record.insertedAt,
140
- },
141
- ciphertext: {
142
- chainId,
143
- index: record.index,
144
- payload: ciphertextBytes,
145
- },
146
- root: { chainId, root: record.root },
147
- note: noteRecord ?? undefined,
148
- });
149
- if (noteRecord) {
150
- decryptedNotes.push(noteRecord);
151
- }
152
- cursor = record.index + 1;
153
- }
154
- // Phase 2: Batch nullifier check for decrypted notes
155
- if (decryptedNotes.length > 0) {
156
- const notesToCheck = [];
157
- for (const note of decryptedNotes) {
158
- const existing = await stateStore.getNote(chainId, note.index);
159
- if (existing?.spentAt !== undefined) {
160
- await stateStore.putNullifier({
161
- chainId,
162
- nullifier: note.nullifier,
163
- noteIndex: note.index,
164
- });
165
- }
166
- else {
167
- notesToCheck.push(note);
168
- }
169
- }
170
- if (notesToCheck.length > 0) {
171
- const spentNullifiers = await indexerClient.checkNullifiers({
172
- chainId,
173
- nullifiers: notesToCheck.map((n) => n.nullifier),
174
- });
175
- const spentMap = new Map(spentNullifiers.map((n) => [n.nullifier, n]));
176
- for (const note of notesToCheck) {
177
- const spent = spentMap.get(note.nullifier);
178
- if (spent) {
179
- const spentAtMs = spent.spentAt * 1000;
180
- await stateStore.putNullifier({
181
- chainId,
182
- nullifier: note.nullifier,
183
- noteIndex: note.index,
184
- });
185
- await stateStore.markNoteSpent(chainId, note.index, spentAtMs, spent.txHash);
186
- }
187
- }
188
- }
189
- }
190
- }
191
- }
192
- async function diagnoseChainState(chainId, localCount) {
193
- // Tier 1: compare first commitment
194
- const firstBatch = await indexerClient.fetchCommitmentBatch({
195
- chainId,
196
- start: 0,
197
- limit: 1,
198
- });
199
- const localFirst = await stateStore.getLeaf(chainId, 0);
200
- if (firstBatch.commitments.length === 0 || !localFirst) {
201
- return { kind: "full-divergence" };
202
- }
203
- if (firstBatch.commitments[0].commitment !== localFirst.commitment) {
204
- return { kind: "full-divergence" };
205
- }
206
- // Single-leaf shortcut: first matches and there's only one local leaf.
207
- if (localCount === 1) {
208
- return { kind: "consistent" };
209
- }
210
- // Compare last local commitment
211
- const lastBatch = await indexerClient.fetchCommitmentBatch({
212
- chainId,
213
- start: localCount - 1,
214
- limit: 1,
215
- });
216
- const localLast = await stateStore.getLeaf(chainId, localCount - 1);
217
- if (lastBatch.commitments.length > 0 &&
218
- localLast &&
219
- lastBatch.commitments[0].commitment === localLast.commitment) {
220
- return { kind: "consistent" };
221
- }
222
- // Tier 2: binary search for divergence point
223
- const divergePoint = await findDivergencePoint(chainId, 1, localCount - 1);
224
- return { kind: "partial-divergence", validPrefix: divergePoint };
225
- }
226
- async function findDivergencePoint(chainId, low, high) {
227
- while (low < high) {
228
- const mid = Math.floor((low + high) / 2);
229
- const batch = await indexerClient.fetchCommitmentBatch({
230
- chainId,
231
- start: mid,
232
- limit: 1,
233
- });
234
- const localLeaf = await stateStore.getLeaf(chainId, mid);
235
- if (batch.commitments.length > 0 &&
236
- localLeaf &&
237
- batch.commitments[0].commitment === localLeaf.commitment) {
238
- low = mid + 1;
239
- }
240
- else {
241
- high = mid;
242
- }
243
- }
244
- return low;
245
- }
246
- // Clears stale tail data and re-ingests from the divergence point.
247
- async function partialResync(chainId, fromIndex, account) {
248
- await Promise.all([
249
- stateStore.clearChainDataFromIndex(chainId, fromIndex),
250
- stateStore.clearNullifiersFromIndex(chainId, fromIndex),
251
- ]);
252
- trees.reset(chainId);
253
- await rebuildTreeFromStore({
254
- chainId,
255
- trees,
256
- loadLeaf: stateStore.getLeaf.bind(stateStore),
257
- });
258
- await ingestFrom(chainId, fromIndex, account);
259
- }
260
- // Clears local state for a chain and rehydrates fully from the indexer.
261
- async function fullResync(chainId, account) {
262
- trees.reset(chainId);
263
- await Promise.all([
264
- stateStore.clearLeaves(chainId),
265
- stateStore.clearNotes(chainId),
266
- stateStore.clearCiphertexts(chainId),
267
- stateStore.clearNullifiers(chainId),
268
- ]);
269
- await ingestFrom(chainId, 0, account);
270
- }
271
- // Decode a stored ciphertext (3 x 32-byte values) into a Ciphertext object
272
- function decodeCiphertext(payload) {
273
- const data = [];
274
- for (let i = 0; i < 3; i++) {
275
- const start = i * FieldSize.SCALAR;
276
- const end = start + FieldSize.SCALAR;
277
- const slice = payload.slice(start, end);
278
- data.push(Hex.toBigInt(Hex.fromBytes(slice)));
279
- }
280
- return { data: data };
281
- }
282
- // Re-scan stored ciphertexts for a new account, using batch nullifier check.
283
- async function rescanCiphertextsForAccount(chainId, account, leafCount) {
284
- const accountMpk = formatUint256(account.masterPublicKey);
285
- const pendingNotes = [];
286
- for (let index = 0; index < leafCount; index++) {
287
- const existingNote = await stateStore.getNote(chainId, index);
288
- if (existingNote && existingNote.mpk === accountMpk) {
289
- continue;
290
- }
291
- const ciphertextBytes = await stateStore.getCiphertext(chainId, index);
292
- if (!ciphertextBytes)
293
- continue;
294
- const leaf = await stateStore.getLeaf(chainId, index);
295
- if (!leaf)
296
- continue;
297
- const ciphertext = decodeCiphertext(ciphertextBytes);
298
- let note;
299
- try {
300
- note = decryptNote(ciphertext, account.masterPublicKey);
301
- }
302
- catch (err) {
303
- if (err instanceof CoreError)
304
- continue;
305
- throw err;
306
- }
307
- const npk = poseidon([account.masterPublicKey, note.random]);
308
- const commitment = poseidon([npk, BigInt(note.token), note.amount]);
309
- if (commitment !== parseHexToBigInt(leaf.commitment))
310
- continue;
311
- const nullifier = poseidon([account.nullifyingKey, BigInt(index)]);
312
- const nullifierHex = formatUint256(nullifier);
313
- const baseNoteParams = {
314
- chainId,
315
- index,
316
- commitment: leaf.commitment,
317
- token: note.token,
318
- amount: note.amount,
319
- npk,
320
- mpk: account.masterPublicKey,
321
- random: note.random,
322
- nullifier: nullifierHex,
323
- createdTxHash: leaf.txHash,
324
- createdEventType: leaf.eventType,
325
- createdAt: leaf.insertedAt != null ? leaf.insertedAt * 1000 : undefined,
326
- };
327
- pendingNotes.push({
328
- note: createNoteRecord(baseNoteParams),
329
- baseParams: baseNoteParams,
330
- });
331
- }
332
- if (pendingNotes.length > 0) {
333
- const spentNullifiers = await indexerClient.checkNullifiers({
334
- chainId,
335
- nullifiers: pendingNotes.map((p) => p.note.nullifier),
336
- });
337
- const spentMap = new Map(spentNullifiers.map((n) => [n.nullifier, n]));
338
- for (const { note, baseParams } of pendingNotes) {
339
- const spent = spentMap.get(note.nullifier);
340
- if (spent) {
341
- await stateStore.putNullifier({
342
- chainId,
343
- nullifier: note.nullifier,
344
- noteIndex: note.index,
345
- });
346
- await stateStore.putNote(createNoteRecord({
347
- ...baseParams,
348
- spentAt: spent.spentAt * 1000,
349
- spentTxHash: spent.txHash,
350
- }));
351
- }
352
- else {
353
- await stateStore.putNote(note);
354
- }
355
- }
356
- }
357
- }
358
- // Check existing unspent notes for nullification using batch check.
359
- async function checkExistingNotesForNullification(chainId, account) {
360
- const accountMpk = formatUint256(account.masterPublicKey);
361
- const unspentNotes = await stateStore.listNotes({
362
- chainId,
363
- mpk: accountMpk,
364
- includeSpent: false,
365
- });
366
- if (unspentNotes.length === 0)
367
- return;
368
- const spentNullifiers = await indexerClient.checkNullifiers({
369
- chainId,
370
- nullifiers: unspentNotes.map((n) => n.nullifier),
371
- });
372
- const spentMap = new Map(spentNullifiers.map((n) => [n.nullifier, n]));
373
- for (const note of unspentNotes) {
374
- const spent = spentMap.get(note.nullifier);
375
- if (spent) {
376
- await stateStore.putNullifier({
377
- chainId,
378
- nullifier: note.nullifier,
379
- noteIndex: note.index,
380
- });
381
- await stateStore.markNoteSpent(chainId, note.index, spent.spentAt * 1000, spent.txHash);
382
- }
383
- }
384
- }
385
- /**
386
- * Syncs a single chain into local state, optionally forcing a full rebuild.
387
- */
388
- async function syncChain(chainId, account, opts = {}) {
389
- ensureChainId(chainId);
390
- const current = status.get(chainId) ?? {
391
- inFlight: false,
392
- lastSuccess: null,
393
- };
394
- if (current.inFlight)
395
- return;
396
- status.set(chainId, { ...current, inFlight: true, lastError: undefined });
397
- try {
398
- const start = opts.forceFullResync
399
- ? 0
400
- : await rebuildTreeFromStore({
401
- chainId,
402
- trees,
403
- loadLeaf: stateStore.getLeaf.bind(stateStore),
404
- });
405
- if (opts.forceFullResync) {
406
- await fullResync(chainId, account);
407
- }
408
- else if (start > 0) {
409
- try {
410
- const diagnosis = await diagnoseChainState(chainId, start);
411
- switch (diagnosis.kind) {
412
- case "consistent":
413
- try {
414
- await ingestFrom(chainId, start, account);
415
- }
416
- catch (err) {
417
- console.warn(`[note-sync] incremental ingest failed for chain ${chainId}, falling back to full resync:`, err);
418
- await fullResync(chainId, account);
419
- }
420
- break;
421
- case "partial-divergence":
422
- await partialResync(chainId, diagnosis.validPrefix, account);
423
- break;
424
- case "full-divergence":
425
- await fullResync(chainId, account);
426
- break;
427
- }
428
- }
429
- catch (err) {
430
- console.warn(`[note-sync] chain diagnosis failed for chain ${chainId}, falling back to full resync:`, err);
431
- await fullResync(chainId, account);
432
- }
433
- }
434
- else {
435
- // start === 0, fresh first sync
436
- try {
437
- await ingestFrom(chainId, 0, account);
438
- }
439
- catch (err) {
440
- console.warn(`[note-sync] initial ingest failed for chain ${chainId}, falling back to full resync:`, err);
441
- await fullResync(chainId, account);
442
- }
443
- }
444
- // Rescan ciphertexts for multi-account support
445
- const accountMpk = formatUint256(account.masterPublicKey);
446
- const leafCount = trees.getLeafCount(chainId);
447
- if (leafCount > 0 && !opts.forceFullResync) {
448
- const accountNotes = await stateStore.listNotes({
449
- chainId,
450
- mpk: accountMpk,
451
- });
452
- if (accountNotes.length < leafCount) {
453
- await rescanCiphertextsForAccount(chainId, account, leafCount);
454
- }
455
- }
456
- // Check if any existing unspent notes have been nullified
457
- await checkExistingNotesForNullification(chainId, account);
458
- status.set(chainId, { inFlight: false, lastSuccess: Date.now() });
459
- }
460
- catch (err) {
461
- status.set(chainId, {
462
- inFlight: false,
463
- lastSuccess: current.lastSuccess ?? null,
464
- lastError: err instanceof Error ? err.message : String(err),
465
- });
466
- throw err;
467
- }
468
- }
469
- /**
470
- * Syncs multiple chains sequentially.
471
- */
472
- async function syncChains(chainIds, account, opts) {
473
- for (const chainId of chainIds) {
474
- await syncChain(chainId, account, opts);
475
- }
476
- }
477
- function getStatus() {
478
- return status;
479
- }
480
- return {
481
- syncChain,
482
- syncChains,
483
- getStatus,
484
- };
485
- }