@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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/package.json +108 -0
- package/packages/btc-client/src/esplora-client.ts +153 -0
- package/packages/btc-client/src/index.ts +3 -0
- package/packages/btc-client/src/op-return.ts +93 -0
- package/packages/btc-client/src/types.ts +112 -0
- package/packages/sdk/README.md +277 -0
- package/packages/sdk/dist/announcement-client.d.ts +64 -0
- package/packages/sdk/dist/announcement-client.js +337 -0
- package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
- package/packages/sdk/dist/auditor-ciphertext.js +135 -0
- package/packages/sdk/dist/auditor.d.ts +115 -0
- package/packages/sdk/dist/auditor.js +292 -0
- package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
- package/packages/sdk/dist/bitcoin/ika.js +74 -0
- package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
- package/packages/sdk/dist/bitcoin/index.js +1 -0
- package/packages/sdk/dist/bound-params.d.ts +96 -0
- package/packages/sdk/dist/bound-params.js +249 -0
- package/packages/sdk/dist/chadbuffer.d.ts +124 -0
- package/packages/sdk/dist/chadbuffer.js +405 -0
- package/packages/sdk/dist/claim-link.d.ts +30 -0
- package/packages/sdk/dist/claim-link.js +50 -0
- package/packages/sdk/dist/client.d.ts +233 -0
- package/packages/sdk/dist/client.js +416 -0
- package/packages/sdk/dist/commitment-tree.d.ts +268 -0
- package/packages/sdk/dist/commitment-tree.js +564 -0
- package/packages/sdk/dist/config.d.ts +177 -0
- package/packages/sdk/dist/config.js +426 -0
- package/packages/sdk/dist/core/esplora.d.ts +110 -0
- package/packages/sdk/dist/core/esplora.js +187 -0
- package/packages/sdk/dist/core/mempool.d.ts +60 -0
- package/packages/sdk/dist/core/mempool.js +107 -0
- package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
- package/packages/sdk/dist/crypto-babyjub.js +312 -0
- package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
- package/packages/sdk/dist/crypto-ed25519.js +235 -0
- package/packages/sdk/dist/crypto.d.ts +59 -0
- package/packages/sdk/dist/crypto.js +140 -0
- package/packages/sdk/dist/event-client.d.ts +64 -0
- package/packages/sdk/dist/event-client.js +165 -0
- package/packages/sdk/dist/events.d.ts +127 -0
- package/packages/sdk/dist/events.js +305 -0
- package/packages/sdk/dist/explorer.d.ts +86 -0
- package/packages/sdk/dist/explorer.js +197 -0
- package/packages/sdk/dist/index.d.ts +61 -0
- package/packages/sdk/dist/index.js +216 -0
- package/packages/sdk/dist/instructions.d.ts +1125 -0
- package/packages/sdk/dist/instructions.js +1760 -0
- package/packages/sdk/dist/keys.d.ts +467 -0
- package/packages/sdk/dist/keys.js +799 -0
- package/packages/sdk/dist/logger.d.ts +10 -0
- package/packages/sdk/dist/logger.js +40 -0
- package/packages/sdk/dist/magicblock.d.ts +90 -0
- package/packages/sdk/dist/magicblock.js +164 -0
- package/packages/sdk/dist/merkle.d.ts +82 -0
- package/packages/sdk/dist/merkle.js +141 -0
- package/packages/sdk/dist/note.d.ts +348 -0
- package/packages/sdk/dist/note.js +483 -0
- package/packages/sdk/dist/pda.d.ts +160 -0
- package/packages/sdk/dist/pda.js +384 -0
- package/packages/sdk/dist/pool-state.d.ts +100 -0
- package/packages/sdk/dist/pool-state.js +126 -0
- package/packages/sdk/dist/poseidon.d.ts +53 -0
- package/packages/sdk/dist/poseidon.js +136 -0
- package/packages/sdk/dist/prover/index.d.ts +10 -0
- package/packages/sdk/dist/prover/index.js +10 -0
- package/packages/sdk/dist/prover/mobile.d.ts +45 -0
- package/packages/sdk/dist/prover/mobile.js +227 -0
- package/packages/sdk/dist/prover/web.d.ts +136 -0
- package/packages/sdk/dist/prover/web.js +597 -0
- package/packages/sdk/dist/psbt.d.ts +73 -0
- package/packages/sdk/dist/psbt.js +202 -0
- package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
- package/packages/sdk/dist/selective-disclosure.js +172 -0
- package/packages/sdk/dist/sender-memo.d.ts +149 -0
- package/packages/sdk/dist/sender-memo.js +250 -0
- package/packages/sdk/dist/sns-resolver.d.ts +121 -0
- package/packages/sdk/dist/sns-resolver.js +229 -0
- package/packages/sdk/dist/solana/connection.d.ts +77 -0
- package/packages/sdk/dist/solana/connection.js +133 -0
- package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
- package/packages/sdk/dist/solana/priority-fee.js +137 -0
- package/packages/sdk/dist/stealth.d.ts +429 -0
- package/packages/sdk/dist/stealth.js +703 -0
- package/packages/sdk/dist/taproot.d.ts +172 -0
- package/packages/sdk/dist/taproot.js +494 -0
- package/packages/sdk/dist/token-registry.d.ts +96 -0
- package/packages/sdk/dist/token-registry.js +122 -0
- package/packages/sdk/dist/utils/encoding.d.ts +19 -0
- package/packages/sdk/dist/utils/encoding.js +29 -0
- package/packages/sdk/dist/vk-registry.d.ts +100 -0
- 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
|
+
}
|