@utxopia/sdk 0.1.0-alpha.1

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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/package.json +108 -0
  4. package/packages/btc-client/src/esplora-client.ts +153 -0
  5. package/packages/btc-client/src/index.ts +3 -0
  6. package/packages/btc-client/src/op-return.ts +93 -0
  7. package/packages/btc-client/src/types.ts +112 -0
  8. package/packages/sdk/README.md +277 -0
  9. package/packages/sdk/dist/announcement-client.d.ts +64 -0
  10. package/packages/sdk/dist/announcement-client.js +337 -0
  11. package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
  12. package/packages/sdk/dist/auditor-ciphertext.js +135 -0
  13. package/packages/sdk/dist/auditor.d.ts +115 -0
  14. package/packages/sdk/dist/auditor.js +292 -0
  15. package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
  16. package/packages/sdk/dist/bitcoin/ika.js +74 -0
  17. package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
  18. package/packages/sdk/dist/bitcoin/index.js +1 -0
  19. package/packages/sdk/dist/bound-params.d.ts +96 -0
  20. package/packages/sdk/dist/bound-params.js +249 -0
  21. package/packages/sdk/dist/chadbuffer.d.ts +124 -0
  22. package/packages/sdk/dist/chadbuffer.js +405 -0
  23. package/packages/sdk/dist/claim-link.d.ts +30 -0
  24. package/packages/sdk/dist/claim-link.js +50 -0
  25. package/packages/sdk/dist/client.d.ts +233 -0
  26. package/packages/sdk/dist/client.js +416 -0
  27. package/packages/sdk/dist/commitment-tree.d.ts +268 -0
  28. package/packages/sdk/dist/commitment-tree.js +564 -0
  29. package/packages/sdk/dist/config.d.ts +177 -0
  30. package/packages/sdk/dist/config.js +426 -0
  31. package/packages/sdk/dist/core/esplora.d.ts +110 -0
  32. package/packages/sdk/dist/core/esplora.js +187 -0
  33. package/packages/sdk/dist/core/mempool.d.ts +60 -0
  34. package/packages/sdk/dist/core/mempool.js +107 -0
  35. package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
  36. package/packages/sdk/dist/crypto-babyjub.js +312 -0
  37. package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
  38. package/packages/sdk/dist/crypto-ed25519.js +235 -0
  39. package/packages/sdk/dist/crypto.d.ts +59 -0
  40. package/packages/sdk/dist/crypto.js +140 -0
  41. package/packages/sdk/dist/event-client.d.ts +64 -0
  42. package/packages/sdk/dist/event-client.js +165 -0
  43. package/packages/sdk/dist/events.d.ts +127 -0
  44. package/packages/sdk/dist/events.js +305 -0
  45. package/packages/sdk/dist/explorer.d.ts +86 -0
  46. package/packages/sdk/dist/explorer.js +197 -0
  47. package/packages/sdk/dist/index.d.ts +61 -0
  48. package/packages/sdk/dist/index.js +216 -0
  49. package/packages/sdk/dist/instructions.d.ts +1125 -0
  50. package/packages/sdk/dist/instructions.js +1760 -0
  51. package/packages/sdk/dist/keys.d.ts +467 -0
  52. package/packages/sdk/dist/keys.js +799 -0
  53. package/packages/sdk/dist/logger.d.ts +10 -0
  54. package/packages/sdk/dist/logger.js +40 -0
  55. package/packages/sdk/dist/magicblock.d.ts +90 -0
  56. package/packages/sdk/dist/magicblock.js +164 -0
  57. package/packages/sdk/dist/merkle.d.ts +82 -0
  58. package/packages/sdk/dist/merkle.js +141 -0
  59. package/packages/sdk/dist/note.d.ts +348 -0
  60. package/packages/sdk/dist/note.js +483 -0
  61. package/packages/sdk/dist/pda.d.ts +160 -0
  62. package/packages/sdk/dist/pda.js +384 -0
  63. package/packages/sdk/dist/pool-state.d.ts +100 -0
  64. package/packages/sdk/dist/pool-state.js +126 -0
  65. package/packages/sdk/dist/poseidon.d.ts +53 -0
  66. package/packages/sdk/dist/poseidon.js +136 -0
  67. package/packages/sdk/dist/prover/index.d.ts +10 -0
  68. package/packages/sdk/dist/prover/index.js +10 -0
  69. package/packages/sdk/dist/prover/mobile.d.ts +45 -0
  70. package/packages/sdk/dist/prover/mobile.js +227 -0
  71. package/packages/sdk/dist/prover/web.d.ts +136 -0
  72. package/packages/sdk/dist/prover/web.js +597 -0
  73. package/packages/sdk/dist/psbt.d.ts +73 -0
  74. package/packages/sdk/dist/psbt.js +202 -0
  75. package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
  76. package/packages/sdk/dist/selective-disclosure.js +172 -0
  77. package/packages/sdk/dist/sender-memo.d.ts +149 -0
  78. package/packages/sdk/dist/sender-memo.js +250 -0
  79. package/packages/sdk/dist/sns-resolver.d.ts +121 -0
  80. package/packages/sdk/dist/sns-resolver.js +229 -0
  81. package/packages/sdk/dist/solana/connection.d.ts +77 -0
  82. package/packages/sdk/dist/solana/connection.js +133 -0
  83. package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
  84. package/packages/sdk/dist/solana/priority-fee.js +137 -0
  85. package/packages/sdk/dist/stealth.d.ts +429 -0
  86. package/packages/sdk/dist/stealth.js +703 -0
  87. package/packages/sdk/dist/taproot.d.ts +172 -0
  88. package/packages/sdk/dist/taproot.js +494 -0
  89. package/packages/sdk/dist/token-registry.d.ts +96 -0
  90. package/packages/sdk/dist/token-registry.js +122 -0
  91. package/packages/sdk/dist/utils/encoding.d.ts +19 -0
  92. package/packages/sdk/dist/utils/encoding.js +29 -0
  93. package/packages/sdk/dist/vk-registry.d.ts +100 -0
  94. package/packages/sdk/dist/vk-registry.js +212 -0
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Commitment Tree Client
3
+ *
4
+ * Implements an incremental Merkle tree matching the on-chain logic exactly.
5
+ * Uses frontier array + pre-computed zero hashes (Tornado Cash/Semaphore pattern).
6
+ *
7
+ * The tree supports up to 2^20 (~1M) leaf commitments.
8
+ * Standard Merkle path proofs compatible with ZK circuits.
9
+ */
10
+ import { poseidonHashSync, initPoseidon } from "./poseidon";
11
+ import { debug, warn } from "./logger";
12
+ // Re-export initPoseidon for tree initialization
13
+ export { initPoseidon };
14
+ /**
15
+ * Convert Uint8Array to bigint (little-endian)
16
+ * Used for parsing on-chain u64 values
17
+ */
18
+ function bytesToBigintLE(bytes) {
19
+ let result = 0n;
20
+ for (let i = bytes.length - 1; i >= 0; i--) {
21
+ result = (result << 8n) | BigInt(bytes[i]);
22
+ }
23
+ return result;
24
+ }
25
+ // Tree constants (must match on-chain — depth 16 = 65,536 leaves)
26
+ export const TREE_DEPTH = 16;
27
+ export const ROOT_HISTORY_SIZE = 100;
28
+ export const MAX_LEAVES = 1n << BigInt(TREE_DEPTH);
29
+ // Discriminator for CommitmentTree account
30
+ export const COMMITMENT_TREE_DISCRIMINATOR = 0x05;
31
+ /**
32
+ * Pre-computed zero hashes for each level of the tree
33
+ * ZERO[0] = 0 (empty leaf)
34
+ * ZERO[i] = Poseidon(ZERO[i-1], ZERO[i-1])
35
+ *
36
+ * These values MUST match the contract's ZERO_HASHES exactly!
37
+ * Using Circom-compatible Poseidon (matches Solana's sol_poseidon).
38
+ */
39
+ export const ZERO_HASHES = [
40
+ 0x0000000000000000000000000000000000000000000000000000000000000000n, // Level 0
41
+ 0x2098f5fb9e239eab3ceac3f27b81e481dc3124d55ffed523a839ee8446b64864n, // Level 1
42
+ 0x1069673dcdb12263df301a6ff584a7ec261a44cb9dc68df067a4774460b1f1e1n, // Level 2
43
+ 0x18f43331537ee2af2e3d758d50f72106467c6eea50371dd528d57eb2b856d238n, // Level 3
44
+ 0x07f9d837cb17b0d36320ffe93ba52345f1b728571a568265caac97559dbc952an, // Level 4
45
+ 0x2b94cf5e8746b3f5c9631f4c5df32907a699c58c94b2ad4d7b5cec1639183f55n, // Level 5
46
+ 0x2dee93c5a666459646ea7d22cca9e1bcfed71e6951b953611d11dda32ea09d78n, // Level 6
47
+ 0x078295e5a22b84e982cf601eb639597b8b0515a88cb5ac7fa8a4aabe3c87349dn, // Level 7
48
+ 0x2fa5e5f18f6027a6501bec864564472a616b2e274a41211a444cbe3a99f3cc61n, // Level 8
49
+ 0x0e884376d0d8fd21ecb780389e941f66e45e7acce3e228ab3e2156a614fcd747n, // Level 9
50
+ 0x1b7201da72494f1e28717ad1a52eb469f95892f957713533de6175e5da190af2n, // Level 10
51
+ 0x1f8d8822725e36385200c0b201249819a6e6e1e4650808b5bebc6bface7d7636n, // Level 11
52
+ 0x2c5d82f66c914bafb9701589ba8cfcfb6162b0a12acf88a8d0879a0471b5f85an, // Level 12
53
+ 0x14c54148a0940bb820957f5adf3fa1134ef5c4aaa113f4646458f270e0bfbfd0n, // Level 13
54
+ 0x190d33b12f986f961e10c0ee44d8b9af11be25588cad89d416118e4bf4ebe80cn, // Level 14
55
+ 0x22f98aa9ce704152ac17354914ad73ed1167ae6596af510aa5b3649325e06c92n, // Level 15
56
+ 0x2a7c7c9b6ce5880b9f6f228d72bf6a575a526f29c66ecceef8b753d38bba7323n, // Level 16
57
+ 0x2e8186e558698ec1c67af9c14d463ffc470043c9c2988b954d75dd643f36b992n, // Level 17
58
+ 0x0f57c5571e9a4eab49e2c8cf050dae948aef6ead647392273546249d1c1ff10fn, // Level 18
59
+ 0x1830ee67b5fb554ad5f63d4388800e1cfe78e310697d46e43c9ce36134f72ccan, // Level 19
60
+ 0x2134e76ac5d21aab186c2be1dd8f84ee880a1e46eaf712f9d371b6df22191f3en, // Level 20: Empty tree root
61
+ ];
62
+ /**
63
+ * Parse commitment tree account data
64
+ *
65
+ * On-chain layout:
66
+ * - discriminator: 1 byte
67
+ * - bump: 1 byte
68
+ * - padding: 6 bytes
69
+ * - current_root: 32 bytes
70
+ * - next_index: 8 bytes
71
+ * - frontier: 20 * 32 = 640 bytes
72
+ * - root_history: 100 * 32 = 3200 bytes
73
+ * - root_history_index: 4 bytes
74
+ * - reserved: 60 bytes
75
+ */
76
+ export function parseCommitmentTreeData(data) {
77
+ const EXPECTED_MIN_SIZE = 8 + 32 + 8 + TREE_DEPTH * 32 + ROOT_HISTORY_SIZE * 32 + 4 + 60;
78
+ if (data.length < EXPECTED_MIN_SIZE) {
79
+ throw new Error(`Invalid commitment tree data length: ${data.length} < ${EXPECTED_MIN_SIZE}`);
80
+ }
81
+ if (data[0] !== COMMITMENT_TREE_DISCRIMINATOR) {
82
+ throw new Error(`Invalid commitment tree discriminator: ${data[0]}`);
83
+ }
84
+ const discriminator = data[0];
85
+ const bump = data[1];
86
+ // Skip 6 bytes padding (indices 2-7)
87
+ const currentRoot = data.slice(8, 40);
88
+ // next_index is stored as little-endian u64
89
+ const nextIndex = bytesToBigintLE(data.slice(40, 48));
90
+ // Parse frontier (20 x 32 bytes)
91
+ const frontier = [];
92
+ let offset = 48;
93
+ for (let i = 0; i < TREE_DEPTH; i++) {
94
+ frontier.push(data.slice(offset, offset + 32));
95
+ offset += 32;
96
+ }
97
+ // Parse root history (100 x 32 bytes)
98
+ const rootHistory = [];
99
+ for (let i = 0; i < ROOT_HISTORY_SIZE; i++) {
100
+ rootHistory.push(data.slice(offset, offset + 32));
101
+ offset += 32;
102
+ }
103
+ const rootHistoryIndex = data[offset] |
104
+ (data[offset + 1] << 8) |
105
+ (data[offset + 2] << 16) |
106
+ (data[offset + 3] << 24);
107
+ return {
108
+ discriminator,
109
+ bump,
110
+ currentRoot,
111
+ nextIndex,
112
+ frontier,
113
+ rootHistory,
114
+ rootHistoryIndex,
115
+ };
116
+ }
117
+ /**
118
+ * Check if a root is valid (current or in history)
119
+ */
120
+ export function isValidRoot(state, root) {
121
+ // Check current root
122
+ if (arraysEqual(state.currentRoot, root)) {
123
+ return true;
124
+ }
125
+ // Check historical roots
126
+ for (const histRoot of state.rootHistory) {
127
+ if (arraysEqual(histRoot, root)) {
128
+ return true;
129
+ }
130
+ }
131
+ return false;
132
+ }
133
+ function arraysEqual(a, b) {
134
+ if (a.length !== b.length)
135
+ return false;
136
+ for (let i = 0; i < a.length; i++) {
137
+ if (a[i] !== b[i])
138
+ return false;
139
+ }
140
+ return true;
141
+ }
142
+ /**
143
+ * Convert bigint to 32-byte array (big-endian)
144
+ */
145
+ function bigintToBytes32(n) {
146
+ const hex = n.toString(16).padStart(64, "0");
147
+ const bytes = new Uint8Array(32);
148
+ for (let i = 0; i < 32; i++) {
149
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
150
+ }
151
+ return bytes;
152
+ }
153
+ /**
154
+ * Convert 32-byte array (big-endian) to bigint
155
+ */
156
+ function bytes32ToBigint(bytes) {
157
+ let hex = "0x";
158
+ for (const b of bytes) {
159
+ hex += b.toString(16).padStart(2, "0");
160
+ }
161
+ return BigInt(hex);
162
+ }
163
+ /**
164
+ * Local Commitment Tree Index (Incremental Merkle Tree)
165
+ *
166
+ * Uses the same algorithm as the on-chain contract:
167
+ * - Frontier array to track rightmost filled nodes at each level
168
+ * - Pre-computed zero hashes for empty subtrees
169
+ *
170
+ * This produces the same root as the on-chain tree.
171
+ */
172
+ export class CommitmentTreeIndex {
173
+ constructor() {
174
+ this.commitments = new Map();
175
+ this.leaves = [];
176
+ // Frontier: rightmost filled node at each level (like on-chain)
177
+ this.frontier = [];
178
+ this.nextIndex = 0n;
179
+ // Initialize empty tree
180
+ this.frontier = new Array(TREE_DEPTH).fill(0n);
181
+ this.currentRoot = ZERO_HASHES[TREE_DEPTH]; // Empty tree root
182
+ }
183
+ /**
184
+ * Add a commitment to the index
185
+ * Uses the same algorithm as on-chain insert_leaf()
186
+ */
187
+ addCommitment(commitment, amount) {
188
+ const leafIndex = this.nextIndex;
189
+ if (leafIndex >= MAX_LEAVES) {
190
+ throw new Error("Tree is full");
191
+ }
192
+ // Store in map for lookup
193
+ const commitmentHex = commitment.toString(16).padStart(64, "0");
194
+ this.commitments.set(commitmentHex, { index: leafIndex, amount });
195
+ // Add to leaves array
196
+ this.leaves.push(commitment);
197
+ // Update tree using incremental algorithm
198
+ let currentHash = commitment;
199
+ let currentIndex = Number(leafIndex);
200
+ // Walk up the tree from leaf to root
201
+ for (let level = 0; level < TREE_DEPTH; level++) {
202
+ if (currentIndex % 2 === 0) {
203
+ // This is a left child - save to frontier and pair with zero hash
204
+ this.frontier[level] = currentHash;
205
+ currentHash = poseidonHashSync([currentHash, ZERO_HASHES[level]]);
206
+ }
207
+ else {
208
+ // This is a right child - pair with frontier (left sibling)
209
+ currentHash = poseidonHashSync([this.frontier[level], currentHash]);
210
+ }
211
+ currentIndex = Math.floor(currentIndex / 2);
212
+ }
213
+ // Update root
214
+ this.currentRoot = currentHash;
215
+ this.nextIndex = leafIndex + 1n;
216
+ return leafIndex;
217
+ }
218
+ /**
219
+ * Get merkle proof for a commitment
220
+ *
221
+ * Returns siblings and path indices for ZK circuit verification.
222
+ */
223
+ getMerkleProof(commitment) {
224
+ const commitmentHex = commitment.toString(16).padStart(64, "0");
225
+ const entry = this.commitments.get(commitmentHex);
226
+ if (!entry) {
227
+ return null;
228
+ }
229
+ const { index } = entry;
230
+ const siblings = [];
231
+ const indices = [];
232
+ // We need to reconstruct the path from stored leaves
233
+ // This requires recomputing sibling hashes
234
+ const proof = this.computeMerkleProof(Number(index));
235
+ return {
236
+ siblings: proof.siblings,
237
+ indices: proof.indices,
238
+ leafIndex: index,
239
+ root: this.currentRoot,
240
+ };
241
+ }
242
+ /**
243
+ * Compute merkle proof for a leaf at given index
244
+ * Optimized: only computes nodes needed for the proof path
245
+ */
246
+ computeMerkleProof(leafIndex) {
247
+ const siblings = [];
248
+ const indices = [];
249
+ const numLeaves = this.leaves.length;
250
+ if (numLeaves === 0) {
251
+ // Empty tree - all siblings are zero hashes
252
+ for (let level = 0; level < TREE_DEPTH; level++) {
253
+ siblings.push(ZERO_HASHES[level]);
254
+ indices.push((leafIndex >> level) & 1);
255
+ }
256
+ return { siblings, indices };
257
+ }
258
+ // Build tree level by level, but only compute the nodes we actually need
259
+ // For each level, we only need: the path node and its sibling
260
+ let currentLevel = [...this.leaves];
261
+ for (let level = 0; level < TREE_DEPTH; level++) {
262
+ const idx = leafIndex >> level;
263
+ const siblingIdx = idx ^ 1;
264
+ // Get sibling value
265
+ const sibling = siblingIdx < currentLevel.length
266
+ ? currentLevel[siblingIdx]
267
+ : ZERO_HASHES[level];
268
+ siblings.push(sibling);
269
+ indices.push(idx & 1);
270
+ // Build next level - only need pairs that lead to our path
271
+ // Compute minimal set of parent nodes needed
272
+ const nextLevel = [];
273
+ const numPairs = Math.ceil(currentLevel.length / 2);
274
+ for (let i = 0; i < numPairs; i++) {
275
+ const left = currentLevel[i * 2] ?? ZERO_HASHES[level];
276
+ const right = currentLevel[i * 2 + 1] ?? ZERO_HASHES[level];
277
+ nextLevel.push(poseidonHashSync([left, right]));
278
+ }
279
+ // If our path goes beyond the computed nodes, we need to add zero-hash parents
280
+ const neededIdx = Math.floor(idx / 2);
281
+ while (nextLevel.length <= neededIdx) {
282
+ nextLevel.push(ZERO_HASHES[level + 1]);
283
+ }
284
+ currentLevel = nextLevel;
285
+ }
286
+ return { siblings, indices };
287
+ }
288
+ /**
289
+ * Get commitment info by hex string
290
+ */
291
+ getCommitment(commitmentHex) {
292
+ return this.commitments.get(commitmentHex) ?? null;
293
+ }
294
+ /**
295
+ * Get current merkle root
296
+ */
297
+ getRoot() {
298
+ return this.currentRoot;
299
+ }
300
+ /**
301
+ * Get root as bytes for comparison with on-chain
302
+ */
303
+ getRootBytes() {
304
+ return bigintToBytes32(this.currentRoot);
305
+ }
306
+ /**
307
+ * Get number of commitments
308
+ */
309
+ size() {
310
+ return this.leaves.length;
311
+ }
312
+ /**
313
+ * Get next leaf index
314
+ */
315
+ getNextIndex() {
316
+ return this.nextIndex;
317
+ }
318
+ /**
319
+ * Get tree status (for API compatibility)
320
+ */
321
+ getStatus() {
322
+ return {
323
+ root: this.currentRoot.toString(16).padStart(64, "0"),
324
+ nextIndex: Number(this.nextIndex),
325
+ size: this.leaves.length,
326
+ };
327
+ }
328
+ /**
329
+ * Get path (merkle proof) for a commitment by hex string
330
+ * Convenience wrapper around getMerkleProof for API compatibility
331
+ */
332
+ getPath(commitmentHex) {
333
+ // Normalize hex
334
+ const normalized = commitmentHex.startsWith("0x")
335
+ ? commitmentHex.slice(2)
336
+ : commitmentHex;
337
+ const commitment = BigInt("0x" + normalized);
338
+ const proof = this.getMerkleProof(commitment);
339
+ if (!proof)
340
+ return null;
341
+ return {
342
+ siblings: proof.siblings.map((s) => s.toString(16).padStart(64, "0")),
343
+ indices: proof.indices,
344
+ leafIndex: proof.leafIndex.toString(),
345
+ root: proof.root.toString(16).padStart(64, "0"),
346
+ };
347
+ }
348
+ /**
349
+ * Export index for persistence
350
+ */
351
+ export() {
352
+ return {
353
+ commitments: Array.from(this.commitments.entries()).map(([k, v]) => [
354
+ k,
355
+ { index: v.index.toString(), amount: v.amount.toString() },
356
+ ]),
357
+ };
358
+ }
359
+ /**
360
+ * Import index from persistence
361
+ */
362
+ import(data) {
363
+ // Reset state
364
+ this.commitments.clear();
365
+ this.leaves = [];
366
+ this.frontier = new Array(TREE_DEPTH).fill(0n);
367
+ this.currentRoot = ZERO_HASHES[TREE_DEPTH];
368
+ this.nextIndex = 0n;
369
+ // Sort by index and add in order
370
+ const sorted = [...data.commitments].sort((a, b) => Number(BigInt(a[1].index)) - Number(BigInt(b[1].index)));
371
+ for (const [hexCommitment, entry] of sorted) {
372
+ const commitment = BigInt("0x" + hexCommitment);
373
+ const amount = BigInt(entry.amount);
374
+ this.addCommitment(commitment, amount);
375
+ }
376
+ }
377
+ /**
378
+ * Import from on-chain state
379
+ * Reconstructs tree state from on-chain frontier and root
380
+ */
381
+ importFromOnChainState(state) {
382
+ // Reset local state
383
+ this.commitments.clear();
384
+ this.leaves = [];
385
+ this.nextIndex = state.nextIndex;
386
+ // Copy frontier from on-chain state
387
+ this.frontier = state.frontier.map(bytes32ToBigint);
388
+ // Copy current root
389
+ this.currentRoot = bytes32ToBigint(state.currentRoot);
390
+ // Note: We don't have the actual leaf values from on-chain state
391
+ // This is just for syncing root/frontier state
392
+ }
393
+ }
394
+ /**
395
+ * Fetch commitment tree state from Solana
396
+ */
397
+ export async function fetchCommitmentTree(connection, commitmentTreePDA) {
398
+ const accountInfo = await connection.getAccountInfo(commitmentTreePDA);
399
+ if (!accountInfo) {
400
+ return null;
401
+ }
402
+ return parseCommitmentTreeData(accountInfo.data);
403
+ }
404
+ // Global index instance (for frontend use)
405
+ let globalIndex = null;
406
+ /**
407
+ * Get or create the global commitment index
408
+ */
409
+ export function getCommitmentIndex() {
410
+ if (!globalIndex) {
411
+ globalIndex = new CommitmentTreeIndex();
412
+ // Try to load from localStorage if available
413
+ if (typeof window !== "undefined" && window.localStorage) {
414
+ try {
415
+ const stored = localStorage.getItem("utxopia_commitment_index");
416
+ if (stored) {
417
+ globalIndex.import(JSON.parse(stored));
418
+ debug("tree", `Loaded ${globalIndex.size()} commitments from storage`);
419
+ }
420
+ }
421
+ catch (e) {
422
+ warn("tree", "Failed to load from storage:", e);
423
+ }
424
+ }
425
+ }
426
+ return globalIndex;
427
+ }
428
+ /**
429
+ * Save the global commitment index to localStorage
430
+ */
431
+ export function saveCommitmentIndex() {
432
+ if (!globalIndex)
433
+ return;
434
+ if (typeof window !== "undefined" && window.localStorage) {
435
+ try {
436
+ const data = globalIndex.export();
437
+ localStorage.setItem("utxopia_commitment_index", JSON.stringify(data));
438
+ debug("tree", `Saved ${globalIndex.size()} commitments`);
439
+ }
440
+ catch (e) {
441
+ warn("tree", "Failed to save to storage:", e);
442
+ }
443
+ }
444
+ }
445
+ /**
446
+ * Build commitment tree from indexed commitment data.
447
+ *
448
+ * Stealth announcements are now emitted as events, not stored as PDAs.
449
+ * This function accepts a commitments map (leafIndex → commitment bigint)
450
+ * from the event indexer and builds the local Merkle tree.
451
+ *
452
+ * @param _rpc - RPC client (unused, kept for API compatibility)
453
+ * @param _programId - UTXOpia program ID (unused)
454
+ * @param options - commitments map from indexer
455
+ */
456
+ export async function buildCommitmentTreeFromChain(_rpc, _programId, options) {
457
+ const commitmentsByIndex = options?.commitments ?? new Map();
458
+ // Sort by leaf index
459
+ const sorted = Array.from(commitmentsByIndex.entries()).sort((a, b) => a[0] - b[0]);
460
+ if (options?.maxLeafIndex !== undefined) {
461
+ const max = options.maxLeafIndex;
462
+ while (sorted.length > 0 && sorted[sorted.length - 1][0] >= max) {
463
+ sorted.pop();
464
+ }
465
+ }
466
+ const tree = new CommitmentTreeIndex();
467
+ for (const [leafIndex, commitment] of sorted) {
468
+ // Fill gaps with zero commitments
469
+ while (tree.getNextIndex() < BigInt(leafIndex)) {
470
+ tree.addCommitment(0n, 0n);
471
+ }
472
+ tree.addCommitment(commitment, 0n);
473
+ }
474
+ debug("tree", `Built tree with ${tree.size()} leaves`);
475
+ return tree;
476
+ }
477
+ /**
478
+ * Get leaf index for a commitment.
479
+ *
480
+ * Commitments are emitted as sol_log_data events (not stored in PDAs).
481
+ * Use scanUnifiedNotes() to get leafIndex for your notes, or query the indexer API.
482
+ * This function builds the full tree from provided commitments to find the index.
483
+ *
484
+ * @param rpc - RPC client
485
+ * @param programId - UTXOpia program ID
486
+ * @param commitment - Commitment to find
487
+ * @returns Leaf index or -1 if not found
488
+ */
489
+ export async function fetchLeafIndexForCommitment(rpc, programId, commitment) {
490
+ try {
491
+ const tree = await buildCommitmentTreeFromChain(rpc, programId);
492
+ if (!tree)
493
+ return -1;
494
+ const proof = tree.getMerkleProof(commitment);
495
+ return proof ? Number(proof.leafIndex) : -1;
496
+ }
497
+ catch (error) {
498
+ console.error("[CommitmentTree] Error fetching leaf index:", error);
499
+ return -1;
500
+ }
501
+ }
502
+ /**
503
+ * Fetch merkle proof for a commitment from on-chain data
504
+ *
505
+ * Builds tree from chain and computes merkle proof for the given commitment.
506
+ * For better performance with multiple proofs, use buildCommitmentTreeFromChain
507
+ * once and call getMerkleProof on the resulting tree.
508
+ *
509
+ * @param rpc - RPC client
510
+ * @param programId - UTXOpia program ID
511
+ * @param commitment - Commitment to get proof for
512
+ * @returns Merkle proof or null if commitment not found
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * const proof = await fetchMerkleProofForCommitment(connection, programId, myCommitment);
517
+ * if (proof) {
518
+ * // Use proof for ZK circuit
519
+ * const claimInputs = {
520
+ * merkleRoot: proof.root,
521
+ * merkleProof: { siblings: proof.siblings, indices: proof.indices },
522
+ * leafIndex: BigInt(proof.leafIndex),
523
+ * // ...
524
+ * };
525
+ * }
526
+ * ```
527
+ */
528
+ export async function fetchMerkleProofForCommitment(rpc, programId, commitment) {
529
+ // Build full tree from chain
530
+ const tree = await buildCommitmentTreeFromChain(rpc, programId);
531
+ // Get proof from local tree
532
+ const proof = tree.getMerkleProof(commitment);
533
+ if (!proof) {
534
+ warn("tree", "Commitment not found in tree");
535
+ return null;
536
+ }
537
+ return {
538
+ siblings: proof.siblings,
539
+ indices: proof.indices,
540
+ leafIndex: Number(proof.leafIndex),
541
+ root: proof.root,
542
+ commitment,
543
+ };
544
+ }
545
+ /**
546
+ * Fetch merkle proof using cached tree (more efficient for multiple lookups)
547
+ *
548
+ * @param tree - Pre-built commitment tree from buildCommitmentTreeFromChain
549
+ * @param commitment - Commitment to get proof for
550
+ * @returns Merkle proof or null if not found
551
+ */
552
+ export function getMerkleProofFromTree(tree, commitment) {
553
+ const proof = tree.getMerkleProof(commitment);
554
+ if (!proof) {
555
+ return null;
556
+ }
557
+ return {
558
+ siblings: proof.siblings,
559
+ indices: proof.indices,
560
+ leafIndex: Number(proof.leafIndex),
561
+ root: proof.root,
562
+ commitment,
563
+ };
564
+ }