Skip to main content

curvy_wasm/
lib.rs

1#![doc = include_str!("../README.md")]
2//!
3//! ## Boundary conventions
4//!
5//! Seed-backed and direct-scalar signing are both supported. Scalar crypto
6//! operations cross the boundary as **decimal strings** (and `Vec<String>` for
7//! points / signatures), matching every existing TS wire shape.
8//! Bulk Merkle operations instead use concatenated canonical 32-byte field
9//! elements so thousands of nodes stay inside wasm.
10//!
11//! Field-element inputs reduce mod the field (`fr_from_dec`); raw 256-bit inputs
12//! (cipher key material, EdDSA message, `sha256BigInt`) are parsed without
13//! reduction (`dec_to_biguint`) - see the core crate for why.
14
15use curvy_core::babyjubjub::{BabyJubPoint, BabyJubScalar};
16use curvy_core::cipher::{decrypt_amount_token, encrypt_amount_token};
17use curvy_core::eddsa::{
18    ScalarSignature, ScalarSigningKey, ephemeral_pub_key, pub_from_private_key_hex, sign_hex,
19    verify_scalar_compat,
20};
21use curvy_core::encoding::dec_to_biguint;
22use curvy_core::field::{Bn254Fr, Fr, fr_from_be_32_checked, fr_from_dec, fr_to_be_32, fr_to_dec};
23use curvy_core::hash_utils::sha256_bigint as core_sha256_bigint;
24use curvy_core::imt::{
25    CompletedShard, FrontierAppend, InclusionProof, IndexedMerkleTree,
26    NotesFrontier as CoreNotesFrontier, OrderedMerkleTree, OwnedNoteWitness,
27    ShardedNotesTree as CoreShardedNotesTree, TreeError, verify_proof,
28};
29use curvy_core::note;
30use curvy_core::poseidon::poseidon as core_poseidon;
31use curvy_core::stealth;
32use wasm_bindgen::prelude::*;
33
34// Threaded builds export `initThreadPool(n)` - call it once (after `init()`)
35// on a cross-origin-isolated page before scans or bulk tree construction.
36#[cfg(feature = "wasm-threads")]
37pub use wasm_bindgen_rayon::init_thread_pool;
38
39/// Poseidon hash of `1..=16` decimal field elements.
40#[wasm_bindgen]
41pub fn poseidon(inputs: Vec<String>) -> String {
42    let fes: Vec<_> = inputs.iter().map(|s| fr_from_dec(s)).collect();
43    fr_to_dec(&core_poseidon(&fes))
44}
45
46/// `ownerHash = Poseidon([pub.x, pub.y, sharedSecret])`.
47#[wasm_bindgen(js_name = ownerHash)]
48pub fn owner_hash(pub_x: String, pub_y: String, shared_secret: String) -> String {
49    fr_to_dec(&note::owner_hash(
50        (fr_from_dec(&pub_x), fr_from_dec(&pub_y)),
51        fr_from_dec(&shared_secret),
52    ))
53}
54
55/// `id = Poseidon([ownerHash, amount, token])`.
56#[wasm_bindgen(js_name = noteId)]
57pub fn note_id(owner_hash: String, amount: String, token: String) -> String {
58    fr_to_dec(&note::note_id(
59        fr_from_dec(&owner_hash),
60        fr_from_dec(&amount),
61        fr_from_dec(&token),
62    ))
63}
64
65/// `nullifier = Poseidon([sharedSecret, pub.x, pub.y])`.
66#[wasm_bindgen]
67pub fn nullifier(shared_secret: String, pub_x: String, pub_y: String) -> String {
68    fr_to_dec(&note::nullifier(
69        fr_from_dec(&shared_secret),
70        (fr_from_dec(&pub_x), fr_from_dec(&pub_y)),
71    ))
72}
73
74/// BabyJubjub public key `[x, y]` from a hex private key (`pubFromPrivateKey`).
75#[wasm_bindgen(js_name = pubFromPrivateKey)]
76pub fn pub_from_private_key(private_key_hex: String) -> Vec<String> {
77    let (x, y) = pub_from_private_key_hex(&private_key_hex);
78    vec![fr_to_dec(&x), fr_to_dec(&y)]
79}
80
81/// Ephemeral public key `R = scalar · Base8` as `[x, y]` (`ephemeralPubKey`).
82#[wasm_bindgen(js_name = ephemeralPubKey)]
83pub fn ephemeral_pub_key_wasm(scalar: String) -> Vec<String> {
84    let (x, y) = ephemeral_pub_key(&dec_to_biguint(&scalar));
85    vec![fr_to_dec(&x), fr_to_dec(&y)]
86}
87
88/// EdDSA-Poseidon signature `[R8.x, R8.y, S]` (`sign`).
89#[wasm_bindgen]
90pub fn sign(message: String, private_key_hex: String) -> Vec<String> {
91    let sig = sign_hex(&dec_to_biguint(&message), &private_key_hex);
92    vec![
93        fr_to_dec(&sig.r8.0),
94        fr_to_dec(&sig.r8.1),
95        sig.s.to_string(),
96    ]
97}
98
99/// BabyJubJub public key `[x, y] = scalar * Base8` from a canonical subgroup
100/// scalar. This path performs no seed hashing, pruning, or clamping.
101#[wasm_bindgen(js_name = pubFromScalar)]
102pub fn pub_from_scalar(scalar: String) -> Result<Vec<String>, JsError> {
103    let key = ScalarSigningKey::from_decimal(&scalar).map_err(|e| JsError::new(&e.to_string()))?;
104    let public = key.verifying_key();
105    Ok(vec![fr_to_dec(&public.x()), fr_to_dec(&public.y())])
106}
107
108/// Curvy-compatible direct-scalar signature `[R8.x, R8.y, S]` from a canonical
109/// BabyJubjub subgroup scalar and canonical BN254 field message.
110#[wasm_bindgen(js_name = signWithScalar)]
111pub fn sign_with_scalar(message: String, scalar: String) -> Result<Vec<String>, JsError> {
112    let message = Bn254Fr::try_from_dec(&message).map_err(|e| JsError::new(&e.to_string()))?;
113    let key = ScalarSigningKey::from_decimal(&scalar).map_err(|e| JsError::new(&e.to_string()))?;
114    let signature = key
115        .sign_curvy_v1(message)
116        .map_err(|e| JsError::new(&e.to_string()))?;
117    Ok(vec![
118        fr_to_dec(&signature.r8.x()),
119        fr_to_dec(&signature.r8.y()),
120        signature.s.to_dec(),
121    ])
122}
123
124/// Verify a scalar-native Curvy signature. Malformed or non-canonical boundary
125/// values throw; a well-formed but invalid signature returns `false`.
126#[wasm_bindgen(js_name = verifyScalarSignature)]
127pub fn verify_scalar_signature(
128    message: String,
129    public_x: String,
130    public_y: String,
131    r8_x: String,
132    r8_y: String,
133    s: String,
134) -> Result<bool, JsError> {
135    let message = Bn254Fr::try_from_dec(&message).map_err(|e| JsError::new(&e.to_string()))?;
136    let public = BabyJubPoint::try_from_dec(&public_x, &public_y)
137        .map_err(|e| JsError::new(&e.to_string()))?;
138    let r8 = BabyJubPoint::try_from_dec(&r8_x, &r8_y).map_err(|e| JsError::new(&e.to_string()))?;
139    let s = BabyJubScalar::try_from_dec(&s).map_err(|e| JsError::new(&e.to_string()))?;
140    Ok(verify_scalar_compat(
141        message,
142        &public,
143        &ScalarSignature { r8, s },
144    ))
145}
146
147/// Encrypt `(amount, token)` -> `[encryptedAmount, encryptedToken]`.
148#[wasm_bindgen(js_name = encryptAmountToken)]
149pub fn encrypt_amount_token_wasm(
150    amount: String,
151    token: String,
152    shared_secret: String,
153    ephemeral_key_x: String,
154    ephemeral_key_y: String,
155) -> Vec<String> {
156    let ss = dec_to_biguint(&shared_secret);
157    let ex = dec_to_biguint(&ephemeral_key_x);
158    let ey = dec_to_biguint(&ephemeral_key_y);
159    let out = encrypt_amount_token(fr_from_dec(&amount), fr_from_dec(&token), &ss, (&ex, &ey));
160    vec![
161        fr_to_dec(&out.encrypted_amount),
162        fr_to_dec(&out.encrypted_token),
163    ]
164}
165
166/// Decrypt `(encryptedAmount, encryptedToken)` -> `[amount, token]`.
167#[wasm_bindgen(js_name = decryptAmountToken)]
168pub fn decrypt_amount_token_wasm(
169    encrypted_amount: String,
170    encrypted_token: String,
171    shared_secret: String,
172    ephemeral_key_x: String,
173    ephemeral_key_y: String,
174) -> Vec<String> {
175    let ss = dec_to_biguint(&shared_secret);
176    let ex = dec_to_biguint(&ephemeral_key_x);
177    let ey = dec_to_biguint(&ephemeral_key_y);
178    let (amount, token) = decrypt_amount_token(
179        fr_from_dec(&encrypted_amount),
180        fr_from_dec(&encrypted_token),
181        &ss,
182        (&ex, &ey),
183    );
184    vec![fr_to_dec(&amount), fr_to_dec(&token)]
185}
186
187/// `sha256BigInt`: raw 256-bit decimal inputs -> decimal digest (no field reduction).
188#[wasm_bindgen(js_name = sha256BigInt)]
189pub fn sha256_bigint(inputs: Vec<String>) -> String {
190    let ints: Vec<_> = inputs.iter().map(|s| dec_to_biguint(s)).collect();
191    core_sha256_bigint(&ints).to_string()
192}
193
194// ── Stateful sharded notes tree ──────────────────────────────────────────────
195
196/// Generic incremental Merkle tree with a reverse leaf index.
197#[wasm_bindgen(js_name = MerkleTree)]
198pub struct WasmMerkleTree {
199    inner: IndexedMerkleTree,
200}
201
202#[wasm_bindgen(js_class = MerkleTree)]
203impl WasmMerkleTree {
204    #[wasm_bindgen(constructor)]
205    pub fn new(depth: u32) -> Result<WasmMerkleTree, JsError> {
206        Ok(Self {
207            inner: IndexedMerkleTree::new(depth as usize).map_err(js_tree_error)?,
208        })
209    }
210
211    #[wasm_bindgen(js_name = fromLeaves)]
212    pub fn from_leaves(depth: u32, packed_leaves: &[u8]) -> Result<WasmMerkleTree, JsError> {
213        Ok(Self {
214            inner: IndexedMerkleTree::from_leaves(
215                depth as usize,
216                &decode_fields(packed_leaves, "leaves")?,
217            )
218            .map_err(js_tree_error)?,
219        })
220    }
221
222    pub fn insert(&mut self, leaf: &[u8]) -> Result<u32, JsError> {
223        let index = self
224            .inner
225            .insert(decode_field(leaf, "leaf")?)
226            .map_err(js_tree_error)?;
227        Ok(index as u32)
228    }
229
230    #[wasm_bindgen(js_name = insertMany)]
231    pub fn insert_many(&mut self, packed_leaves: &[u8]) -> Result<(), JsError> {
232        self.inner
233            .insert_many(&decode_fields(packed_leaves, "leaves")?)
234            .map_err(js_tree_error)
235    }
236
237    #[wasm_bindgen(js_name = getIndex)]
238    pub fn get_index(&self, leaf: &[u8]) -> Result<Option<u32>, JsError> {
239        Ok(self
240            .inner
241            .get_index(decode_field(leaf, "leaf")?)
242            .map(|index| index as u32))
243    }
244
245    pub fn proof(&self, leaf: &[u8]) -> Result<WasmInclusionProof, JsError> {
246        Ok(WasmInclusionProof(
247            self.inner
248                .create_proof(decode_field(leaf, "leaf")?)
249                .map_err(js_tree_error)?,
250        ))
251    }
252
253    #[wasm_bindgen(js_name = proofAt)]
254    pub fn proof_at(&self, index: u32) -> Result<WasmInclusionProof, JsError> {
255        Ok(WasmInclusionProof(
256            self.inner
257                .create_proof_at(index as usize)
258                .map_err(js_tree_error)?,
259        ))
260    }
261
262    pub fn truncate(&mut self, leaf_count: u32) -> Result<(), JsError> {
263        self.inner
264            .truncate(leaf_count as usize)
265            .map_err(js_tree_error)
266    }
267
268    pub fn root(&self) -> Vec<u8> {
269        fr_to_be_32(&self.inner.root()).to_vec()
270    }
271
272    pub fn leaves(&self) -> Vec<u8> {
273        pack_fields(self.inner.leaves())
274    }
275
276    #[wasm_bindgen(getter)]
277    pub fn depth(&self) -> u32 {
278        self.inner.depth() as u32
279    }
280
281    #[wasm_bindgen(getter, js_name = leafCount)]
282    pub fn leaf_count(&self) -> u32 {
283        self.inner.leaf_count() as u32
284    }
285}
286
287/// Position-addressed tree for public vectors whose values may repeat.
288#[wasm_bindgen(js_name = OrderedMerkleTree)]
289pub struct WasmOrderedMerkleTree {
290    inner: OrderedMerkleTree,
291}
292
293#[wasm_bindgen(js_class = OrderedMerkleTree)]
294impl WasmOrderedMerkleTree {
295    #[wasm_bindgen(constructor)]
296    pub fn new(depth: u32) -> Result<WasmOrderedMerkleTree, JsError> {
297        Ok(Self {
298            inner: OrderedMerkleTree::new(depth as usize).map_err(js_tree_error)?,
299        })
300    }
301
302    #[wasm_bindgen(js_name = fromLeaves)]
303    pub fn from_leaves(depth: u32, packed_leaves: &[u8]) -> Result<WasmOrderedMerkleTree, JsError> {
304        Ok(Self {
305            inner: OrderedMerkleTree::from_leaves(
306                depth as usize,
307                &decode_fields(packed_leaves, "leaves")?,
308            )
309            .map_err(js_tree_error)?,
310        })
311    }
312
313    pub fn insert(&mut self, leaf: &[u8]) -> Result<u32, JsError> {
314        let index = self
315            .inner
316            .insert(decode_field(leaf, "leaf")?)
317            .map_err(js_tree_error)?;
318        Ok(index as u32)
319    }
320
321    #[wasm_bindgen(js_name = insertMany)]
322    pub fn insert_many(&mut self, packed_leaves: &[u8]) -> Result<(), JsError> {
323        self.inner
324            .insert_many(&decode_fields(packed_leaves, "leaves")?)
325            .map_err(js_tree_error)
326    }
327
328    #[wasm_bindgen(js_name = proofAt)]
329    pub fn proof_at(&self, index: u32) -> Result<WasmInclusionProof, JsError> {
330        Ok(WasmInclusionProof(
331            self.inner
332                .create_proof_at(index as usize)
333                .map_err(js_tree_error)?,
334        ))
335    }
336
337    pub fn root(&self) -> Vec<u8> {
338        fr_to_be_32(&self.inner.root()).to_vec()
339    }
340
341    #[wasm_bindgen(getter)]
342    pub fn depth(&self) -> u32 {
343        self.inner.depth() as u32
344    }
345
346    #[wasm_bindgen(getter, js_name = leafCount)]
347    pub fn leaf_count(&self) -> u32 {
348        self.inner.leaf_count() as u32
349    }
350}
351
352/// Verify a packed conventional inclusion proof without reimplementing
353/// Poseidon/path ordering in JavaScript.
354#[wasm_bindgen(js_name = verifyMerkleProof)]
355pub fn verify_merkle_proof(
356    leaf: &[u8],
357    index: u32,
358    packed_siblings: &[u8],
359    root: &[u8],
360) -> Result<bool, JsError> {
361    Ok(verify_proof(&InclusionProof {
362        leaf: decode_field(leaf, "leaf")?,
363        index: index as usize,
364        siblings: decode_fields(packed_siblings, "siblings")?,
365        root: decode_field(root, "root")?,
366    }))
367}
368
369/// Constant-space append frontier. It retains no
370/// leaves or witnesses and emits a shard descriptor only at an exact boundary.
371#[wasm_bindgen(js_name = NotesFrontier)]
372pub struct WasmNotesFrontier {
373    inner: CoreNotesFrontier,
374}
375
376#[wasm_bindgen(js_class = NotesFrontier)]
377impl WasmNotesFrontier {
378    #[wasm_bindgen(constructor)]
379    pub fn new(depth: u32, shard_height: u32) -> Result<WasmNotesFrontier, JsError> {
380        Ok(Self {
381            inner: CoreNotesFrontier::new(depth as usize, shard_height as usize)
382                .map_err(js_tree_error)?,
383        })
384    }
385
386    /// An empty frontier with the production notes-tree geometry, so callers
387    /// stop restating `depth = 30` / `shardHeight = 14` on the JS side.
388    #[wasm_bindgen(js_name = production)]
389    pub fn production() -> WasmNotesFrontier {
390        Self {
391            inner: CoreNotesFrontier::production(),
392        }
393    }
394
395    #[wasm_bindgen(js_name = restore)]
396    pub fn restore(snapshot: &[u8]) -> Result<WasmNotesFrontier, JsError> {
397        Ok(Self {
398            inner: CoreNotesFrontier::from_snapshot_bytes(snapshot).map_err(js_tree_error)?,
399        })
400    }
401
402    pub fn append(&mut self, leaf: &[u8]) -> Result<WasmFrontierAppend, JsError> {
403        Ok(WasmFrontierAppend(
404            self.inner
405                .append(decode_field(leaf, "leaf")?)
406                .map_err(js_tree_error)?,
407        ))
408    }
409
410    #[wasm_bindgen(js_name = appendMany)]
411    pub fn append_many(
412        &mut self,
413        packed_leaves: &[u8],
414    ) -> Result<Vec<WasmCompletedShard>, JsError> {
415        Ok(self
416            .inner
417            .append_many(&decode_fields(packed_leaves, "leaves")?)
418            .map_err(js_tree_error)?
419            .into_iter()
420            .map(WasmCompletedShard)
421            .collect())
422    }
423
424    pub fn root(&self) -> Vec<u8> {
425        fr_to_be_32(&self.inner.root()).to_vec()
426    }
427
428    pub fn snapshot(&self) -> Vec<u8> {
429        self.inner.encode_snapshot()
430    }
431
432    #[wasm_bindgen(getter)]
433    pub fn depth(&self) -> u32 {
434        self.inner.depth() as u32
435    }
436
437    #[wasm_bindgen(getter, js_name = shardHeight)]
438    pub fn shard_height(&self) -> u32 {
439        self.inner.shard_height() as u32
440    }
441
442    #[wasm_bindgen(getter, js_name = shardSize)]
443    pub fn shard_size(&self) -> u32 {
444        self.inner.shard_size() as u32
445    }
446
447    #[wasm_bindgen(getter, js_name = leafCount)]
448    pub fn leaf_count(&self) -> u32 {
449        self.inner.leaf_count() as u32
450    }
451
452    #[wasm_bindgen(getter, js_name = shardCount)]
453    pub fn shard_count(&self) -> u32 {
454        self.inner.shard_count() as u32
455    }
456}
457
458/// Protocol notes-tree parameters, exported so JavaScript consumers read them
459/// from the core rather than hardcoding a second copy.
460#[wasm_bindgen(js_name = notesTreeDepth)]
461pub fn notes_tree_depth() -> u32 {
462    curvy_core::NOTES_TREE_DEPTH as u32
463}
464
465#[wasm_bindgen(js_name = notesShardHeight)]
466pub fn notes_shard_height() -> u32 {
467    curvy_core::NOTES_SHARD_HEIGHT as u32
468}
469
470#[wasm_bindgen(js_name = notesShardSize)]
471pub fn notes_shard_size() -> u32 {
472    curvy_core::NOTES_SHARD_SIZE as u32
473}
474
475#[wasm_bindgen(js_name = notesTreeVersion)]
476pub fn notes_tree_version() -> u32 {
477    curvy_core::NOTES_TREE_VERSION
478}
479
480#[wasm_bindgen(js_name = NotesFrontierAppend)]
481pub struct WasmFrontierAppend(FrontierAppend);
482
483#[wasm_bindgen(js_class = NotesFrontierAppend)]
484impl WasmFrontierAppend {
485    #[wasm_bindgen(getter, js_name = leafIndex)]
486    pub fn leaf_index(&self) -> u32 {
487        self.0.leaf_index as u32
488    }
489
490    #[wasm_bindgen(getter, js_name = hasCompletedShard)]
491    pub fn has_completed_shard(&self) -> bool {
492        self.0.completed_shard.is_some()
493    }
494
495    #[wasm_bindgen(getter, js_name = completedShardIndex)]
496    pub fn completed_shard_index(&self) -> Option<u32> {
497        self.0
498            .completed_shard
499            .as_ref()
500            .map(|shard| shard.shard_index as u32)
501    }
502
503    #[wasm_bindgen(getter, js_name = completedShardRoot)]
504    pub fn completed_shard_root(&self) -> Vec<u8> {
505        self.0
506            .completed_shard
507            .as_ref()
508            .map(|shard| fr_to_be_32(&shard.root).to_vec())
509            .unwrap_or_default()
510    }
511}
512
513#[wasm_bindgen(js_name = NotesFrontierCompletedShard)]
514pub struct WasmCompletedShard(CompletedShard);
515
516#[wasm_bindgen(js_class = NotesFrontierCompletedShard)]
517impl WasmCompletedShard {
518    #[wasm_bindgen(getter, js_name = shardIndex)]
519    pub fn shard_index(&self) -> u32 {
520        self.0.shard_index as u32
521    }
522
523    #[wasm_bindgen(getter)]
524    pub fn root(&self) -> Vec<u8> {
525        fr_to_be_32(&self.0.root).to_vec()
526    }
527}
528
529/// Rust-owned sharded notes tree. Field elements cross this bulk boundary as
530/// canonical packed 32-byte big-endian values, avoiding one JS↔wasm call and one
531/// decimal-string allocation per Poseidon node.
532#[wasm_bindgen(js_name = ShardedNotesTree)]
533pub struct WasmShardedNotesTree {
534    inner: CoreShardedNotesTree,
535}
536
537#[wasm_bindgen(js_class = ShardedNotesTree)]
538impl WasmShardedNotesTree {
539    #[wasm_bindgen(constructor)]
540    pub fn new(depth: u32, shard_height: u32) -> Result<WasmShardedNotesTree, JsError> {
541        Ok(Self {
542            inner: CoreShardedNotesTree::new(depth as usize, shard_height as usize)
543                .map_err(js_tree_error)?,
544        })
545    }
546
547    /// Restore a versioned snapshot previously returned by [`Self::snapshot`].
548    #[wasm_bindgen(js_name = restore)]
549    pub fn restore(snapshot: &[u8]) -> Result<WasmShardedNotesTree, JsError> {
550        Ok(Self {
551            inner: CoreShardedNotesTree::from_snapshot_bytes(snapshot).map_err(js_tree_error)?,
552        })
553    }
554
555    /// Restore public tree state from storage tables before account-scoped
556    /// witnesses are marked/adopted.
557    #[wasm_bindgen(js_name = restoreParts)]
558    pub fn restore_parts(
559        depth: u32,
560        shard_height: u32,
561        packed_completed_roots: &[u8],
562        packed_live_leaves: &[u8],
563    ) -> Result<WasmShardedNotesTree, JsError> {
564        Ok(Self {
565            inner: CoreShardedNotesTree::from_parts(
566                depth as usize,
567                shard_height as usize,
568                decode_fields(packed_completed_roots, "completed shard roots")?,
569                decode_fields(packed_live_leaves, "live leaves")?,
570            )
571            .map_err(js_tree_error)?,
572        })
573    }
574
575    /// Append one canonical 32-byte note commitment.
576    pub fn append(&mut self, note_id: &[u8]) -> Result<(), JsError> {
577        self.inner
578            .append(decode_field(note_id, "note id")?)
579            .map_err(js_tree_error)
580    }
581
582    /// Append `N` concatenated 32-byte note commitments in one wasm call.
583    #[wasm_bindgen(js_name = appendMany)]
584    pub fn append_many(&mut self, packed_note_ids: &[u8]) -> Result<(), JsError> {
585        let note_ids = decode_fields(packed_note_ids, "note ids")?;
586        self.inner.append_many(&note_ids).map_err(js_tree_error)
587    }
588
589    #[wasm_bindgen(js_name = markOwned)]
590    pub fn mark_owned(&mut self, note_id: &[u8], leaf_index: u32) -> Result<(), JsError> {
591        self.inner
592            .mark_owned(decode_field(note_id, "note id")?, leaf_index as usize)
593            .map_err(js_tree_error)
594    }
595
596    #[wasm_bindgen(js_name = unmarkOwned)]
597    pub fn unmark_owned(&mut self, note_id: &[u8]) -> Result<bool, JsError> {
598        Ok(self.inner.unmark_owned(decode_field(note_id, "note id")?))
599    }
600
601    #[wasm_bindgen(js_name = adoptFrozenWitness)]
602    pub fn adopt_frozen_witness(
603        &mut self,
604        note_id: &[u8],
605        leaf_index: u32,
606        packed_siblings: &[u8],
607    ) -> Result<(), JsError> {
608        self.inner
609            .adopt_frozen_witness(
610                decode_field(note_id, "note id")?,
611                leaf_index as usize,
612                decode_fields(packed_siblings, "within-shard siblings")?,
613            )
614            .map_err(js_tree_error)
615    }
616
617    pub fn witness(&self, note_id: &[u8]) -> Result<WasmInclusionProof, JsError> {
618        Ok(WasmInclusionProof(
619            self.inner
620                .witness(decode_field(note_id, "note id")?)
621                .map_err(js_tree_error)?,
622        ))
623    }
624
625    /// Rewind only within the current live shard. Restore an earlier persisted
626    /// snapshot when a rollback crosses a completed-shard boundary.
627    #[wasm_bindgen(js_name = rewindLiveTo)]
628    pub fn rewind_live_to(&mut self, leaf_count: u32) -> Result<Vec<u8>, JsError> {
629        let removed = self
630            .inner
631            .rewind_live_to(leaf_count as usize)
632            .map_err(js_tree_error)?;
633        Ok(pack_fields(&removed))
634    }
635
636    pub fn root(&self) -> Vec<u8> {
637        fr_to_be_32(&self.inner.root()).to_vec()
638    }
639
640    #[wasm_bindgen(getter)]
641    pub fn depth(&self) -> u32 {
642        self.inner.depth() as u32
643    }
644
645    #[wasm_bindgen(getter, js_name = shardHeight)]
646    pub fn shard_height(&self) -> u32 {
647        self.inner.shard_height() as u32
648    }
649
650    #[wasm_bindgen(getter, js_name = shardSize)]
651    pub fn shard_size(&self) -> u32 {
652        self.inner.shard_size() as u32
653    }
654
655    #[wasm_bindgen(getter, js_name = leafCount)]
656    pub fn leaf_count(&self) -> u32 {
657        self.inner.leaf_count() as u32
658    }
659
660    #[wasm_bindgen(getter, js_name = completedShardCount)]
661    pub fn completed_shard_count(&self) -> u32 {
662        self.inner.completed_shard_count() as u32
663    }
664
665    #[wasm_bindgen(getter, js_name = ownedNoteCount)]
666    pub fn owned_note_count(&self) -> u32 {
667        self.inner.owned_note_count() as u32
668    }
669
670    #[wasm_bindgen(js_name = completedShardRoots)]
671    pub fn completed_shard_roots(&self) -> Vec<u8> {
672        pack_fields(self.inner.completed_roots())
673    }
674
675    #[wasm_bindgen(js_name = completedShardRoot)]
676    pub fn completed_shard_root(&self, shard_index: u32) -> Result<Vec<u8>, JsError> {
677        Ok(fr_to_be_32(
678            &self
679                .inner
680                .completed_shard_root(shard_index as usize)
681                .map_err(js_tree_error)?,
682        )
683        .to_vec())
684    }
685
686    #[wasm_bindgen(js_name = liveLeaves)]
687    pub fn live_leaves(&self) -> Vec<u8> {
688        pack_fields(self.inner.live_leaves())
689    }
690
691    #[wasm_bindgen(js_name = drainDirtyOwnedNotes)]
692    pub fn drain_dirty_owned_notes(&mut self) -> Vec<WasmOwnedNoteWitness> {
693        self.inner
694            .drain_dirty_owned_notes()
695            .into_iter()
696            .map(WasmOwnedNoteWitness)
697            .collect()
698    }
699
700    #[wasm_bindgen(js_name = ownedNotes)]
701    pub fn owned_notes(&self) -> Vec<WasmOwnedNoteWitness> {
702        self.inner
703            .owned_notes()
704            .into_iter()
705            .map(WasmOwnedNoteWitness)
706            .collect()
707    }
708
709    /// Deterministic versioned binary state. Storage layers should associate
710    /// chain/deployment/block metadata with this opaque tree blob.
711    pub fn snapshot(&self) -> Result<Vec<u8>, JsError> {
712        self.inner.encode_snapshot().map_err(js_tree_error)
713    }
714}
715
716#[wasm_bindgen(js_name = ShardedInclusionProof)]
717pub struct WasmInclusionProof(InclusionProof);
718
719#[wasm_bindgen(js_class = ShardedInclusionProof)]
720impl WasmInclusionProof {
721    #[wasm_bindgen(getter)]
722    pub fn leaf(&self) -> Vec<u8> {
723        fr_to_be_32(&self.0.leaf).to_vec()
724    }
725
726    #[wasm_bindgen(getter)]
727    pub fn index(&self) -> u32 {
728        self.0.index as u32
729    }
730
731    #[wasm_bindgen(getter)]
732    pub fn siblings(&self) -> Vec<u8> {
733        pack_fields(&self.0.siblings)
734    }
735
736    #[wasm_bindgen(getter)]
737    pub fn root(&self) -> Vec<u8> {
738        fr_to_be_32(&self.0.root).to_vec()
739    }
740}
741
742#[wasm_bindgen(js_name = ShardedOwnedNoteWitness)]
743pub struct WasmOwnedNoteWitness(OwnedNoteWitness);
744
745#[wasm_bindgen(js_class = ShardedOwnedNoteWitness)]
746impl WasmOwnedNoteWitness {
747    #[wasm_bindgen(getter, js_name = noteId)]
748    pub fn note_id(&self) -> Vec<u8> {
749        fr_to_be_32(&self.0.note_id).to_vec()
750    }
751
752    #[wasm_bindgen(getter, js_name = leafIndex)]
753    pub fn leaf_index(&self) -> u32 {
754        self.0.leaf_index as u32
755    }
756
757    #[wasm_bindgen(getter)]
758    pub fn frozen(&self) -> bool {
759        self.0.within_shard_siblings.is_some()
760    }
761
762    #[wasm_bindgen(getter, js_name = withinShardSiblings)]
763    pub fn within_shard_siblings(&self) -> Vec<u8> {
764        self.0
765            .within_shard_siblings
766            .as_deref()
767            .map(pack_fields)
768            .unwrap_or_default()
769    }
770}
771
772fn decode_field(bytes: &[u8], what: &str) -> Result<Fr, JsError> {
773    fr_from_be_32_checked(bytes).ok_or_else(|| {
774        JsError::new(&format!(
775            "sharded tree: {what} must be one canonical 32-byte big-endian field element",
776        ))
777    })
778}
779
780fn decode_fields(bytes: &[u8], what: &str) -> Result<Vec<Fr>, JsError> {
781    if !bytes.len().is_multiple_of(32) {
782        return Err(JsError::new(&format!(
783            "sharded tree: packed {what} length {} is not divisible by 32",
784            bytes.len(),
785        )));
786    }
787    bytes
788        .chunks_exact(32)
789        .map(|raw| decode_field(raw, what))
790        .collect()
791}
792
793fn pack_fields(fields: &[Fr]) -> Vec<u8> {
794    let mut packed = Vec::with_capacity(fields.len() * 32);
795    for field in fields {
796        packed.extend_from_slice(&fr_to_be_32(field));
797    }
798    packed
799}
800
801fn js_tree_error(error: TreeError) -> JsError {
802    JsError::new(&error.to_string())
803}
804
805// ── Domain A: the stealth core. Typed params in, plain decimal/hex string values
806// out - NO JSON envelope; wasm-bindgen passes structured values directly.
807// Multi-value results use `Vec<String>` - the same positional convention Domain B
808// uses for points and signatures - except `scan`, which returns its two PAIRED
809// arrays via a small typed result. Points are "x.y"; view tags and private keys
810// are hex.
811
812#[wasm_bindgen]
813pub fn version() -> String {
814    "v1.0.2".to_string()
815}
816
817/// Fresh random meta-keys `[k, v, K, V]` = spend priv, view priv, spend pub, view pub.
818#[wasm_bindgen]
819pub fn new_meta() -> Result<Vec<String>, JsError> {
820    let (k, v, big_k, big_v) = stealth::new_meta()?;
821    Ok(vec![k, v, big_k, big_v])
822}
823
824/// Public meta-keys `[k, v, K, V]` for the given private spend (`k`) / view (`v`) keys.
825/// Throws on degenerate keys (zero reduction).
826#[wasm_bindgen]
827pub fn get_meta(k: String, v: String) -> Result<Vec<String>, JsError> {
828    let (big_k, big_v) = stealth::get_meta(&k, &v)?;
829    Ok(vec![k, v, big_k, big_v])
830}
831
832/// Announce a payment to recipient `(K, V)` → `[r, R, viewTag, spendingPubKey]`.
833/// Throws on malformed / off-curve recipient keys (an unspendable announcement
834/// must never be produced).
835#[wasm_bindgen]
836pub fn send(big_k: String, big_v: String) -> Result<Vec<String>, JsError> {
837    let (r, out) = stealth::send(&big_k, &big_v)?;
838    Ok(vec![r, out.big_r, out.view_tag, out.spending_pub_key])
839}
840
841/// Recipient scan → the SPARSE list of tag-matching announcements, in input
842/// order: each match carries its `index` into the input arrays plus the derived
843/// one-time keys. Matches are CANDIDATES (1-byte viewTag ⇒ ~1/256 false
844/// positives) - the caller's note-commitment recompute confirms ownership.
845/// Malformed / off-curve announcements are non-matches (skipped), never fatal;
846/// throws only on the caller's own inputs (keys, mismatched array lengths).
847#[wasm_bindgen]
848pub fn scan(
849    k: String,
850    v: String,
851    rs: Vec<String>,
852    view_tags: Vec<String>,
853) -> Result<Vec<ScanMatch>, JsError> {
854    Ok(stealth::scan(&k, &v, &rs, &view_tags)?
855        .into_iter()
856        .map(ScanMatch)
857        .collect())
858}
859
860/// Viewer scan (view key `v` + recipient spend pub `K`, no spend key): the same
861/// sparse candidate list, spending PUBLIC keys only.
862#[wasm_bindgen(js_name = viewerScan)]
863pub fn viewer_scan(
864    v: String,
865    big_k: String,
866    rs: Vec<String>,
867    view_tags: Vec<String>,
868) -> Result<Vec<ViewerMatch>, JsError> {
869    Ok(stealth::viewer_scan(&v, &big_k, &rs, &view_tags)?
870        .into_iter()
871        .map(ViewerMatch)
872        .collect())
873}
874
875/// One [`scan`] candidate: `index` into the input arrays + the derived keys.
876#[wasm_bindgen]
877pub struct ScanMatch(stealth::ScanMatch);
878
879#[wasm_bindgen]
880impl ScanMatch {
881    #[wasm_bindgen(getter)]
882    pub fn index(&self) -> u32 {
883        self.0.index
884    }
885    #[wasm_bindgen(getter, js_name = spendingPubKey)]
886    pub fn spending_pub_key(&self) -> String {
887        self.0.spending_pub_key.clone()
888    }
889    #[wasm_bindgen(getter, js_name = spendingPrivKey)]
890    pub fn spending_priv_key(&self) -> String {
891        self.0.spending_priv_key.clone()
892    }
893}
894
895/// One [`viewer_scan`] candidate: `index` + the derived spending PUBLIC key.
896#[wasm_bindgen]
897pub struct ViewerMatch(stealth::ViewerMatch);
898
899#[wasm_bindgen]
900impl ViewerMatch {
901    #[wasm_bindgen(getter)]
902    pub fn index(&self) -> u32 {
903        self.0.index
904    }
905    #[wasm_bindgen(getter, js_name = spendingPubKey)]
906    pub fn spending_pub_key(&self) -> String {
907        self.0.spending_pub_key.clone()
908    }
909}
910
911#[wasm_bindgen(js_name = dbg_isValidBN254Point)]
912pub fn dbg_is_valid_bn254_point(point: String) -> bool {
913    stealth::is_valid_bn254_point(&point)
914}
915
916#[wasm_bindgen(js_name = dbg_isValidSECP256k1Point)]
917pub fn dbg_is_valid_secp256k1_point(point: String) -> bool {
918    stealth::is_valid_secp256k1_point(&point)
919}