@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.3

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