iris_wasm/
tx.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use alloc::format;
4use alloc::string::{String, ToString};
5use alloc::vec::Vec;
6use ibig::UBig;
7use iris_crypto::PrivateKey;
8use iris_grpc_proto::pb::common::v1 as pb_v1;
9use iris_grpc_proto::pb::common::v2 as pb;
10use iris_nockchain_types::{
11    builder::TxBuilder,
12    note::{Name, Note, NoteData, NoteDataEntry, Pkh, TimelockRange, Version},
13    tx::{LockPrimitive, RawTx, Seed, SpendCondition},
14    Nicks,
15};
16use iris_nockchain_types::{Hax, LockTim, MissingUnlocks, Source, SpendBuilder, Spends};
17use iris_ztd::{cue, jam, Digest, Hashable as HashableTrait, NounDecode};
18use serde::{Deserialize, Serialize};
19use wasm_bindgen::prelude::*;
20
21// ============================================================================
22// Wasm Types - Core Types
23// ============================================================================
24
25#[wasm_bindgen(js_name = Digest)]
26#[derive(Clone, Serialize, Deserialize)]
27#[serde(transparent)]
28pub struct WasmDigest {
29    #[wasm_bindgen(skip)]
30    pub value: String,
31}
32
33#[wasm_bindgen(js_class = Digest)]
34impl WasmDigest {
35    #[wasm_bindgen(constructor)]
36    pub fn new(value: String) -> Self {
37        Self { value }
38    }
39
40    #[wasm_bindgen(getter)]
41    pub fn value(&self) -> String {
42        self.value.clone()
43    }
44
45    fn to_internal(&self) -> Result<Digest, &'static str> {
46        self.value.as_str().try_into()
47    }
48
49    fn from_internal(digest: &Digest) -> Self {
50        Self {
51            value: digest.to_string(),
52        }
53    }
54
55    #[wasm_bindgen(js_name = toProtobuf)]
56    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
57        let digest = self.to_internal().map_err(JsValue::from_str)?;
58        let pb = pb_v1::Hash::from(digest);
59        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
60    }
61
62    #[wasm_bindgen(js_name = fromProtobuf)]
63    pub fn from_protobuf(value: JsValue) -> Result<WasmDigest, JsValue> {
64        let pb: pb_v1::Hash = serde_wasm_bindgen::from_value(value)?;
65        let digest: Digest = pb
66            .try_into()
67            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
68        Ok(WasmDigest::from_internal(&digest))
69    }
70}
71
72#[wasm_bindgen(js_name = Version)]
73#[derive(Clone, Serialize, Deserialize)]
74pub struct WasmVersion {
75    version: u32,
76}
77
78#[wasm_bindgen(js_class = Version)]
79impl WasmVersion {
80    #[wasm_bindgen(constructor)]
81    pub fn new(version: u32) -> Self {
82        Self { version }
83    }
84
85    #[wasm_bindgen(js_name = V0)]
86    pub fn v0() -> Self {
87        Self { version: 0 }
88    }
89
90    #[wasm_bindgen(js_name = V1)]
91    pub fn v1() -> Self {
92        Self { version: 1 }
93    }
94
95    #[wasm_bindgen(js_name = V2)]
96    pub fn v2() -> Self {
97        Self { version: 2 }
98    }
99
100    fn to_internal(&self) -> Version {
101        self.version.into()
102    }
103
104    fn from_internal(version: &Version) -> Self {
105        Self {
106            version: version.clone().into(),
107        }
108    }
109}
110
111#[wasm_bindgen(js_name = Name)]
112#[derive(Clone, Serialize, Deserialize)]
113pub struct WasmName {
114    #[wasm_bindgen(skip)]
115    pub first: Digest,
116    #[wasm_bindgen(skip)]
117    pub last: Digest,
118}
119
120#[wasm_bindgen(js_class = Name)]
121impl WasmName {
122    #[wasm_bindgen(constructor)]
123    pub fn new(first: String, last: String) -> Result<Self, JsValue> {
124        let first = Digest::try_from(&*first)?;
125        let last = Digest::try_from(&*last)?;
126        Ok(Self { first, last })
127    }
128
129    #[wasm_bindgen(getter)]
130    pub fn first(&self) -> String {
131        self.first.to_string()
132    }
133
134    #[wasm_bindgen(getter)]
135    pub fn last(&self) -> String {
136        self.last.to_string()
137    }
138
139    fn to_internal(&self) -> Name {
140        Name::new(self.first, self.last)
141    }
142
143    #[allow(dead_code)]
144    fn from_internal(name: &Name) -> Self {
145        // We need to access Name fields via hash since they are private
146        // For now, we'll only support construction, not reading back
147        Self {
148            first: name.first,
149            last: name.last,
150        }
151    }
152
153    #[wasm_bindgen(js_name = toProtobuf)]
154    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
155        let name = self.to_internal();
156        let pb = pb_v1::Name::from(name);
157        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
158    }
159
160    #[wasm_bindgen(js_name = fromProtobuf)]
161    pub fn from_protobuf(value: JsValue) -> Result<WasmName, JsValue> {
162        let pb: pb_v1::Name = serde_wasm_bindgen::from_value(value)?;
163        let name: Name = pb
164            .try_into()
165            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
166        Ok(WasmName::from_internal(&name))
167    }
168}
169
170#[wasm_bindgen(js_name = TimelockRange)]
171#[derive(Clone, Serialize, Deserialize)]
172pub struct WasmTimelockRange {
173    #[wasm_bindgen(skip)]
174    pub min: Option<u64>,
175    #[wasm_bindgen(skip)]
176    pub max: Option<u64>,
177}
178
179#[wasm_bindgen(js_class = TimelockRange)]
180impl WasmTimelockRange {
181    #[wasm_bindgen(constructor)]
182    pub fn new(min: Option<u64>, max: Option<u64>) -> Self {
183        Self { min, max }
184    }
185
186    #[wasm_bindgen(getter)]
187    pub fn min(&self) -> Option<u64> {
188        self.min
189    }
190
191    #[wasm_bindgen(getter)]
192    pub fn max(&self) -> Option<u64> {
193        self.max
194    }
195
196    fn to_internal(&self) -> TimelockRange {
197        TimelockRange::new(self.min, self.max)
198    }
199
200    fn from_internal(internal: TimelockRange) -> WasmTimelockRange {
201        WasmTimelockRange {
202            min: internal.min,
203            max: internal.max,
204        }
205    }
206}
207
208#[wasm_bindgen(js_name = Source)]
209#[derive(Clone, Serialize, Deserialize)]
210pub struct WasmSource {
211    #[wasm_bindgen(skip)]
212    pub hash: WasmDigest,
213    #[wasm_bindgen(skip)]
214    pub is_coinbase: bool,
215}
216
217#[wasm_bindgen(js_class = Source)]
218impl WasmSource {
219    #[wasm_bindgen(getter, js_name = hash)]
220    pub fn hash(&self) -> WasmDigest {
221        self.hash.clone()
222    }
223
224    #[wasm_bindgen(getter, js_name = isCoinbase)]
225    pub fn is_coinbase(&self) -> bool {
226        self.is_coinbase
227    }
228
229    fn to_internal(&self) -> Result<Source, String> {
230        Ok(Source {
231            hash: self.hash.to_internal()?,
232            is_coinbase: self.is_coinbase,
233        })
234    }
235
236    fn from_internal(internal: &Source) -> Self {
237        Self {
238            hash: WasmDigest::from_internal(&internal.hash),
239            is_coinbase: internal.is_coinbase,
240        }
241    }
242}
243
244// ============================================================================
245// Wasm Types - Note Types
246// ============================================================================
247
248#[wasm_bindgen(js_name = NoteDataEntry)]
249#[derive(Clone, Serialize, Deserialize)]
250pub struct WasmNoteDataEntry {
251    #[wasm_bindgen(skip)]
252    pub key: String,
253    #[wasm_bindgen(skip)]
254    pub blob: Vec<u8>,
255}
256
257#[wasm_bindgen(js_class = NoteDataEntry)]
258impl WasmNoteDataEntry {
259    #[wasm_bindgen(constructor)]
260    pub fn new(key: String, blob: Vec<u8>) -> Self {
261        Self { key, blob }
262    }
263
264    #[wasm_bindgen(getter)]
265    pub fn key(&self) -> String {
266        self.key.clone()
267    }
268
269    #[wasm_bindgen(getter)]
270    pub fn blob(&self) -> Vec<u8> {
271        self.blob.clone()
272    }
273
274    fn to_internal(&self) -> Result<NoteDataEntry, String> {
275        let val = cue(&self.blob).ok_or_else(|| "Failed to deserialize noun".to_string())?;
276        Ok(NoteDataEntry {
277            key: self.key.clone(),
278            val,
279        })
280    }
281
282    fn from_internal(entry: &NoteDataEntry) -> Self {
283        Self {
284            key: entry.key.clone(),
285            blob: jam(entry.val.clone()),
286        }
287    }
288
289    #[wasm_bindgen(js_name = toProtobuf)]
290    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
291        let entry = self.to_internal().map_err(|e| JsValue::from_str(&e))?;
292        let pb = pb::NoteDataEntry::from(entry);
293        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
294    }
295
296    #[wasm_bindgen(js_name = fromProtobuf)]
297    pub fn from_protobuf(value: JsValue) -> Result<WasmNoteDataEntry, JsValue> {
298        let pb: pb::NoteDataEntry = serde_wasm_bindgen::from_value(value)?;
299        let entry: NoteDataEntry = pb
300            .try_into()
301            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
302        Ok(WasmNoteDataEntry::from_internal(&entry))
303    }
304}
305
306#[wasm_bindgen(js_name = NoteData)]
307#[derive(Clone, Serialize, Deserialize)]
308pub struct WasmNoteData {
309    #[wasm_bindgen(skip)]
310    pub entries: Vec<WasmNoteDataEntry>,
311}
312
313#[wasm_bindgen(js_class = NoteData)]
314impl WasmNoteData {
315    #[wasm_bindgen(constructor)]
316    pub fn new(entries: Vec<WasmNoteDataEntry>) -> Self {
317        Self { entries }
318    }
319
320    #[wasm_bindgen]
321    pub fn empty() -> Self {
322        Self {
323            entries: Vec::new(),
324        }
325    }
326
327    #[wasm_bindgen(js_name = fromPkh)]
328    pub fn from_pkh(pkh: WasmPkh) -> Result<Self, JsValue> {
329        let note_data = NoteData::from_pkh(pkh.to_internal()?);
330        Ok(Self::from_internal(&note_data))
331    }
332
333    #[wasm_bindgen(getter)]
334    pub fn entries(&self) -> Vec<WasmNoteDataEntry> {
335        self.entries.clone()
336    }
337
338    fn to_internal(&self) -> Result<NoteData, String> {
339        let entries: Result<Vec<NoteDataEntry>, String> =
340            self.entries.iter().map(|e| e.to_internal()).collect();
341        Ok(NoteData(entries?))
342    }
343
344    fn from_internal(note_data: &NoteData) -> Self {
345        Self {
346            entries: note_data
347                .0
348                .iter()
349                .map(WasmNoteDataEntry::from_internal)
350                .collect(),
351        }
352    }
353
354    #[wasm_bindgen(js_name = toProtobuf)]
355    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
356        let data = self.to_internal().map_err(|e| JsValue::from_str(&e))?;
357        let pb = pb::NoteData::from(data);
358        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
359    }
360
361    #[wasm_bindgen(js_name = fromProtobuf)]
362    pub fn from_protobuf(value: JsValue) -> Result<WasmNoteData, JsValue> {
363        let pb: pb::NoteData = serde_wasm_bindgen::from_value(value)?;
364        let data: NoteData = pb
365            .try_into()
366            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
367        Ok(WasmNoteData::from_internal(&data))
368    }
369}
370
371#[wasm_bindgen(js_name = Note)]
372#[derive(Clone, Serialize, Deserialize)]
373pub struct WasmNote {
374    #[wasm_bindgen(skip)]
375    pub version: WasmVersion,
376    #[wasm_bindgen(skip)]
377    pub origin_page: u64,
378    #[wasm_bindgen(skip)]
379    pub name: WasmName,
380    #[wasm_bindgen(skip)]
381    pub note_data: WasmNoteData,
382    #[wasm_bindgen(skip)]
383    pub assets: Nicks,
384}
385
386#[wasm_bindgen(js_class = Note)]
387impl WasmNote {
388    #[wasm_bindgen(constructor)]
389    pub fn new(
390        version: WasmVersion,
391        origin_page: u64,
392        name: WasmName,
393        note_data: WasmNoteData,
394        assets: Nicks,
395    ) -> Self {
396        Self {
397            version,
398            origin_page,
399            name,
400            note_data,
401            assets,
402        }
403    }
404
405    #[wasm_bindgen(getter)]
406    pub fn version(&self) -> WasmVersion {
407        self.version.clone()
408    }
409
410    #[wasm_bindgen(getter, js_name = originPage)]
411    pub fn origin_page(&self) -> u64 {
412        self.origin_page
413    }
414
415    #[wasm_bindgen(getter)]
416    pub fn name(&self) -> WasmName {
417        self.name.clone()
418    }
419
420    #[wasm_bindgen(getter, js_name = noteData)]
421    pub fn note_data(&self) -> WasmNoteData {
422        self.note_data.clone()
423    }
424
425    #[wasm_bindgen(getter)]
426    pub fn assets(&self) -> Nicks {
427        self.assets
428    }
429
430    #[wasm_bindgen]
431    pub fn hash(&self) -> Result<WasmDigest, JsValue> {
432        let note = self
433            .to_internal()
434            .map_err(|e| JsValue::from_str(&e.to_string()))?;
435        Ok(WasmDigest::from_internal(&note.hash()))
436    }
437
438    /// Create a WasmNote from a protobuf Note object (from get_balance response)
439    /// Expects response.notes[i].note (handles version internally)
440    #[wasm_bindgen(js_name = fromProtobuf)]
441    pub fn from_protobuf(pb_note: JsValue) -> Result<WasmNote, JsValue> {
442        let pb: pb::Note = serde_wasm_bindgen::from_value(pb_note)?;
443        let note: Note = pb
444            .try_into()
445            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
446        Ok(WasmNote::from_internal(note))
447    }
448
449    #[wasm_bindgen(js_name = toProtobuf)]
450    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
451        let note = self
452            .to_internal()
453            .map_err(|e| JsValue::from_str(&e.to_string()))?;
454        let pb = pb::Note::from(note);
455        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
456    }
457
458    fn to_internal(&self) -> Result<Note, String> {
459        Ok(Note::new(
460            self.version.to_internal(),
461            self.origin_page,
462            self.name.to_internal(),
463            self.note_data.to_internal()?,
464            self.assets,
465        ))
466    }
467
468    fn from_internal(internal: Note) -> Self {
469        Self {
470            version: WasmVersion::from_internal(&internal.version),
471            origin_page: internal.origin_page,
472            name: WasmName::from_internal(&internal.name),
473            note_data: WasmNoteData::from_internal(&internal.note_data),
474            assets: internal.assets,
475        }
476    }
477}
478
479// ============================================================================
480// Wasm Types - Transaction Types
481// ============================================================================
482
483#[wasm_bindgen(js_name = Pkh)]
484#[derive(Clone, Serialize, Deserialize)]
485pub struct WasmPkh {
486    #[wasm_bindgen(skip)]
487    pub m: u64,
488    #[wasm_bindgen(skip)]
489    pub hashes: Vec<String>,
490}
491
492#[wasm_bindgen(js_class = Pkh)]
493impl WasmPkh {
494    #[wasm_bindgen(constructor)]
495    pub fn new(m: u64, hashes: Vec<String>) -> Self {
496        Self { m, hashes }
497    }
498
499    #[wasm_bindgen]
500    pub fn single(hash: String) -> Self {
501        Self {
502            m: 1,
503            hashes: alloc::vec![hash],
504        }
505    }
506
507    #[wasm_bindgen(getter)]
508    pub fn m(&self) -> u64 {
509        self.m
510    }
511
512    #[wasm_bindgen(getter)]
513    pub fn hashes(&self) -> Vec<String> {
514        self.hashes.clone()
515    }
516
517    fn to_internal(&self) -> Result<Pkh, String> {
518        let hashes: Result<Vec<Digest>, _> =
519            self.hashes.iter().map(|s| s.as_str().try_into()).collect();
520        Ok(Pkh::new(self.m, hashes?))
521    }
522
523    fn from_internal(internal: Pkh) -> Self {
524        Self::new(
525            internal.m,
526            internal.hashes.into_iter().map(|v| v.to_string()).collect(),
527        )
528    }
529
530    #[wasm_bindgen(js_name = toProtobuf)]
531    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
532        let pkh = self.to_internal().map_err(|e| JsValue::from_str(&e))?;
533        let pb = pb::PkhLock::from(pkh);
534        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
535    }
536
537    #[wasm_bindgen(js_name = fromProtobuf)]
538    pub fn from_protobuf(value: JsValue) -> Result<WasmPkh, JsValue> {
539        let pb: pb::PkhLock = serde_wasm_bindgen::from_value(value)?;
540        let pkh: Pkh = pb
541            .try_into()
542            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
543        Ok(WasmPkh::from_internal(pkh))
544    }
545}
546
547#[wasm_bindgen(js_name = LockTim)]
548#[derive(Clone, Serialize, Deserialize)]
549pub struct WasmLockTim {
550    #[wasm_bindgen(skip)]
551    pub rel: WasmTimelockRange,
552    #[wasm_bindgen(skip)]
553    pub abs: WasmTimelockRange,
554}
555
556#[wasm_bindgen(js_class = LockTim)]
557impl WasmLockTim {
558    #[wasm_bindgen(constructor)]
559    pub fn new(rel: WasmTimelockRange, abs: WasmTimelockRange) -> Self {
560        Self { rel, abs }
561    }
562
563    #[wasm_bindgen]
564    pub fn coinbase() -> Self {
565        let tim = LockTim::coinbase();
566        Self {
567            rel: WasmTimelockRange {
568                min: tim.rel.min,
569                max: tim.rel.max,
570            },
571            abs: WasmTimelockRange {
572                min: tim.abs.min,
573                max: tim.abs.max,
574            },
575        }
576    }
577
578    #[wasm_bindgen(getter)]
579    pub fn rel(&self) -> WasmTimelockRange {
580        self.rel.clone()
581    }
582
583    #[wasm_bindgen(getter)]
584    pub fn abs(&self) -> WasmTimelockRange {
585        self.abs.clone()
586    }
587
588    fn to_internal(&self) -> LockTim {
589        LockTim {
590            rel: self.rel.to_internal(),
591            abs: self.abs.to_internal(),
592        }
593    }
594
595    fn from_internal(internal: LockTim) -> WasmLockTim {
596        WasmLockTim {
597            rel: WasmTimelockRange::from_internal(internal.rel),
598            abs: WasmTimelockRange::from_internal(internal.abs),
599        }
600    }
601
602    #[wasm_bindgen(js_name = toProtobuf)]
603    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
604        let tim = self.to_internal();
605        let pb = pb::LockTim::from(tim);
606        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
607    }
608
609    #[wasm_bindgen(js_name = fromProtobuf)]
610    pub fn from_protobuf(value: JsValue) -> Result<WasmLockTim, JsValue> {
611        let pb: pb::LockTim = serde_wasm_bindgen::from_value(value)?;
612        let tim: LockTim = pb
613            .try_into()
614            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
615        Ok(WasmLockTim::from_internal(tim))
616    }
617}
618
619#[wasm_bindgen(js_name = Hax)]
620#[derive(Clone, Serialize, Deserialize)]
621pub struct WasmHax {
622    #[wasm_bindgen(skip)]
623    pub digests: Vec<WasmDigest>,
624}
625
626#[wasm_bindgen(js_class = Hax)]
627impl WasmHax {
628    #[wasm_bindgen(constructor)]
629    pub fn new(digests: Vec<WasmDigest>) -> Self {
630        Self { digests }
631    }
632
633    #[wasm_bindgen(getter)]
634    pub fn digests(&self) -> Vec<WasmDigest> {
635        self.digests.clone()
636    }
637
638    fn to_internal(&self) -> Result<Hax, String> {
639        Ok(Hax(self
640            .digests
641            .iter()
642            .map(WasmDigest::to_internal)
643            .collect::<Result<Vec<_>, _>>()?))
644    }
645
646    fn from_internal(internal: Hax) -> Self {
647        Self::new(internal.0.iter().map(WasmDigest::from_internal).collect())
648    }
649}
650
651#[wasm_bindgen(js_name = LockPrimitive)]
652#[derive(Clone, Serialize, Deserialize)]
653pub struct WasmLockPrimitive {
654    variant: String,
655    #[wasm_bindgen(skip)]
656    pub pkh_data: Option<WasmPkh>,
657    #[wasm_bindgen(skip)]
658    pub tim_data: Option<WasmLockTim>,
659    #[wasm_bindgen(skip)]
660    pub hax_data: Option<WasmHax>,
661}
662
663#[wasm_bindgen(js_class = LockPrimitive)]
664impl WasmLockPrimitive {
665    #[wasm_bindgen(js_name = newPkh)]
666    pub fn new_pkh(pkh: WasmPkh) -> WasmLockPrimitive {
667        Self {
668            variant: "pkh".to_string(),
669            pkh_data: Some(pkh),
670            tim_data: None,
671            hax_data: None,
672        }
673    }
674
675    #[wasm_bindgen(js_name = newTim)]
676    pub fn new_tim(tim: WasmLockTim) -> WasmLockPrimitive {
677        Self {
678            variant: "tim".to_string(),
679            pkh_data: None,
680            tim_data: Some(tim),
681            hax_data: None,
682        }
683    }
684
685    #[wasm_bindgen(js_name = newHax)]
686    pub fn new_hax(hax: WasmHax) -> Self {
687        Self {
688            variant: "hax".to_string(),
689            pkh_data: None,
690            tim_data: None,
691            hax_data: Some(hax),
692        }
693    }
694
695    #[wasm_bindgen(js_name = newBrn)]
696    pub fn new_brn() -> Self {
697        Self {
698            variant: "brn".to_string(),
699            pkh_data: None,
700            tim_data: None,
701            hax_data: None,
702        }
703    }
704
705    fn to_internal(&self) -> Result<LockPrimitive, String> {
706        match self.variant.as_str() {
707            "pkh" => {
708                if let Some(ref pkh) = self.pkh_data {
709                    Ok(LockPrimitive::Pkh(pkh.to_internal()?))
710                } else {
711                    Err("Missing pkh data".to_string())
712                }
713            }
714            "tim" => {
715                if let Some(ref tim) = self.tim_data {
716                    Ok(LockPrimitive::Tim(tim.to_internal()))
717                } else {
718                    Err("Missing tim data".to_string())
719                }
720            }
721            "hax" => {
722                if let Some(ref hax) = self.hax_data {
723                    Ok(LockPrimitive::Hax(hax.to_internal()?))
724                } else {
725                    Err("Missing hax data".to_string())
726                }
727            }
728            "brn" => Ok(LockPrimitive::Brn),
729            _ => Err("Invalid lock primitive variant".to_string()),
730        }
731    }
732
733    fn from_internal(internal: LockPrimitive) -> Self {
734        match internal {
735            LockPrimitive::Pkh(p) => Self::new_pkh(WasmPkh::from_internal(p)),
736            LockPrimitive::Tim(t) => Self::new_tim(WasmLockTim::from_internal(t)),
737            LockPrimitive::Hax(h) => Self::new_hax(WasmHax::from_internal(h)),
738            LockPrimitive::Brn => Self::new_brn(),
739        }
740    }
741
742    #[wasm_bindgen(js_name = toProtobuf)]
743    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
744        let prim = self.to_internal().map_err(|e| JsValue::from_str(&e))?;
745        let pb = pb::LockPrimitive::from(prim);
746        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
747    }
748
749    #[wasm_bindgen(js_name = fromProtobuf)]
750    pub fn from_protobuf(value: JsValue) -> Result<WasmLockPrimitive, JsValue> {
751        let pb: pb::LockPrimitive = serde_wasm_bindgen::from_value(value)?;
752        let prim: LockPrimitive = pb
753            .try_into()
754            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
755        Ok(WasmLockPrimitive::from_internal(prim))
756    }
757}
758
759#[wasm_bindgen(js_name = SpendCondition)]
760#[derive(Clone, Serialize, Deserialize)]
761pub struct WasmSpendCondition {
762    #[wasm_bindgen(skip)]
763    pub primitives: Vec<WasmLockPrimitive>,
764}
765
766#[wasm_bindgen(js_class = SpendCondition)]
767impl WasmSpendCondition {
768    #[wasm_bindgen(constructor)]
769    pub fn new(primitives: Vec<WasmLockPrimitive>) -> Self {
770        Self { primitives }
771    }
772
773    #[wasm_bindgen(js_name = newPkh)]
774    pub fn new_pkh(pkh: WasmPkh) -> WasmSpendCondition {
775        let primitive = WasmLockPrimitive::new_pkh(pkh);
776        Self {
777            primitives: alloc::vec![primitive],
778        }
779    }
780
781    #[wasm_bindgen]
782    pub fn hash(&self) -> Result<WasmDigest, JsValue> {
783        let condition = self
784            .to_internal()
785            .map_err(|e| JsValue::from_str(&e.to_string()))?;
786        Ok(WasmDigest::from_internal(&condition.hash()))
787    }
788
789    #[wasm_bindgen(js_name = firstName)]
790    pub fn first_name(&self) -> Result<WasmDigest, JsValue> {
791        let condition = self
792            .to_internal()
793            .map_err(|e| JsValue::from_str(&e.to_string()))?;
794        Ok(WasmDigest::from_internal(&condition.first_name()))
795    }
796
797    fn to_internal(&self) -> Result<SpendCondition, String> {
798        let mut primitives = Vec::new();
799        for prim in &self.primitives {
800            primitives.push(prim.to_internal()?);
801        }
802        Ok(SpendCondition(primitives))
803    }
804
805    fn from_internal(internal: SpendCondition) -> Self {
806        Self::new(
807            internal
808                .0
809                .into_iter()
810                .map(WasmLockPrimitive::from_internal)
811                .collect(),
812        )
813    }
814
815    #[wasm_bindgen(js_name = toProtobuf)]
816    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
817        let cond = self.to_internal().map_err(|e| JsValue::from_str(&e))?;
818        let pb = pb::SpendCondition::from(cond);
819        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
820    }
821
822    #[wasm_bindgen(js_name = fromProtobuf)]
823    pub fn from_protobuf(value: JsValue) -> Result<WasmSpendCondition, JsValue> {
824        let pb: pb::SpendCondition = serde_wasm_bindgen::from_value(value)?;
825        let cond: SpendCondition = pb
826            .try_into()
827            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
828        Ok(WasmSpendCondition::from_internal(cond))
829    }
830}
831
832#[wasm_bindgen(js_name = Seed)]
833pub struct WasmSeed {
834    #[wasm_bindgen(skip)]
835    pub output_source: Option<WasmSource>,
836    #[wasm_bindgen(skip)]
837    pub lock_root: WasmDigest,
838    #[wasm_bindgen(skip)]
839    pub gift: Nicks,
840    #[wasm_bindgen(skip)]
841    pub note_data: WasmNoteData,
842    #[wasm_bindgen(skip)]
843    pub parent_hash: WasmDigest,
844}
845
846#[wasm_bindgen(js_class = Seed)]
847impl WasmSeed {
848    #[wasm_bindgen(constructor)]
849    pub fn new(
850        output_source: Option<WasmSource>,
851        lock_root: WasmDigest,
852        gift: Nicks,
853        note_data: WasmNoteData,
854        parent_hash: WasmDigest,
855    ) -> Self {
856        Self {
857            output_source,
858            lock_root,
859            gift,
860            note_data,
861            parent_hash,
862        }
863    }
864
865    #[wasm_bindgen(js_name = newSinglePkh)]
866    pub fn new_single_pkh(
867        pkh: WasmDigest,
868        gift: Nicks,
869        parent_hash: WasmDigest,
870        include_lock_data: bool,
871    ) -> Result<Self, JsValue> {
872        let seed = Seed::new_single_pkh(
873            pkh.to_internal()?,
874            gift,
875            parent_hash.to_internal()?,
876            include_lock_data,
877        );
878        Ok(seed.into())
879    }
880
881    #[wasm_bindgen(getter, js_name = outputSource)]
882    pub fn output_source(&self) -> Option<WasmSource> {
883        self.output_source.clone()
884    }
885
886    #[wasm_bindgen(setter, js_name = outputSource)]
887    pub fn set_output_source(&mut self, output_source: Option<WasmSource>) {
888        self.output_source = output_source;
889    }
890
891    #[wasm_bindgen(getter, js_name = lockRoot)]
892    pub fn lock_root(&self) -> WasmDigest {
893        self.lock_root.clone()
894    }
895
896    #[wasm_bindgen(setter, js_name = lockRoot)]
897    pub fn set_lock_root(&mut self, lock_root: WasmDigest) {
898        self.lock_root = lock_root;
899    }
900
901    #[wasm_bindgen(getter)]
902    pub fn gift(&self) -> Nicks {
903        self.gift
904    }
905
906    #[wasm_bindgen(setter)]
907    pub fn set_gift(&mut self, gift: Nicks) {
908        self.gift = gift;
909    }
910
911    #[wasm_bindgen(getter, js_name = noteData)]
912    pub fn note_data(&self) -> WasmNoteData {
913        self.note_data.clone()
914    }
915
916    #[wasm_bindgen(setter, js_name = noteData)]
917    pub fn set_note_data(&mut self, note_data: WasmNoteData) {
918        self.note_data = note_data;
919    }
920
921    #[wasm_bindgen(getter, js_name = parentHash)]
922    pub fn parent_hash(&self) -> WasmDigest {
923        self.parent_hash.clone()
924    }
925
926    #[wasm_bindgen(setter, js_name = parentHash)]
927    pub fn set_parent_hash(&mut self, parent_hash: WasmDigest) {
928        self.parent_hash = parent_hash;
929    }
930
931    fn to_internal(&self) -> Result<Seed, String> {
932        Ok(Seed {
933            output_source: self
934                .output_source
935                .as_ref()
936                .map(WasmSource::to_internal)
937                .transpose()?,
938            lock_root: self.lock_root.to_internal()?,
939            gift: self.gift,
940            note_data: self.note_data.to_internal()?,
941            parent_hash: self.parent_hash.to_internal()?,
942        })
943    }
944}
945
946impl From<Seed> for WasmSeed {
947    fn from(value: Seed) -> Self {
948        Self {
949            output_source: value.output_source.as_ref().map(WasmSource::from_internal),
950            lock_root: WasmDigest::from_internal(&value.lock_root),
951            gift: value.gift,
952            note_data: WasmNoteData::from_internal(&value.note_data),
953            parent_hash: WasmDigest::from_internal(&value.parent_hash),
954        }
955    }
956}
957
958#[wasm_bindgen]
959impl WasmSeed {
960    #[wasm_bindgen(js_name = toProtobuf)]
961    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
962        let seed = self.to_internal().map_err(|e| JsValue::from_str(&e))?;
963        let pb = pb::Seed::from(seed);
964        serde_wasm_bindgen::to_value(&pb).map_err(|e| e.into())
965    }
966
967    fn from_internal(seed: Seed) -> Self {
968        seed.into()
969    }
970
971    #[wasm_bindgen(js_name = fromProtobuf)]
972    pub fn from_protobuf(value: JsValue) -> Result<WasmSeed, JsValue> {
973        let pb: pb::Seed = serde_wasm_bindgen::from_value(value)?;
974        let seed: Seed = pb
975            .try_into()
976            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
977        Ok(WasmSeed::from_internal(seed))
978    }
979}
980
981// ============================================================================
982// Wasm Transaction Builder
983// ============================================================================
984
985#[wasm_bindgen(js_name = TxBuilder)]
986pub struct WasmTxBuilder {
987    builder: TxBuilder,
988}
989
990#[wasm_bindgen(js_class = TxBuilder)]
991impl WasmTxBuilder {
992    /// Create an empty transaction builder
993    #[wasm_bindgen(constructor)]
994    pub fn new(fee_per_word: Nicks) -> Self {
995        Self {
996            builder: TxBuilder::new(fee_per_word),
997        }
998    }
999
1000    /// Reconstruct a builder from raw transaction and its input notes.
1001    ///
1002    /// To get the builder back, you must pass the notes and their corresponding spend conditions.
1003    /// If serializing the builder, call `WasmTxBuilder::all_notes`.
1004    #[wasm_bindgen(js_name = fromTx)]
1005    pub fn from_tx(
1006        tx: WasmRawTx,
1007        notes: Vec<WasmNote>,
1008        spend_conditions: Vec<WasmSpendCondition>,
1009    ) -> Result<Self, JsValue> {
1010        if notes.len() != spend_conditions.len() {
1011            return Err(JsValue::from_str(
1012                "notes and spend_conditions must have the same length",
1013            ));
1014        }
1015
1016        let internal_notes: Result<BTreeMap<Name, (Note, SpendCondition)>, String> = notes
1017            .iter()
1018            .zip(spend_conditions.iter())
1019            .map(|(n, sc)| Ok((n.to_internal()?, sc.to_internal()?)))
1020            .map(|v| v.map(|(a, b)| (a.name.clone(), (a, b))))
1021            .collect();
1022        let internal_notes = internal_notes.map_err(|e| JsValue::from_str(&e.to_string()))?;
1023
1024        let builder = TxBuilder::from_tx(tx.internal, internal_notes).map_err(|e| e.to_string())?;
1025
1026        Ok(Self { builder })
1027    }
1028
1029    /// Perform a simple-spend on this builder.
1030    ///
1031    /// It is HIGHLY recommended to not mix `simpleSpend` with other types of spends.
1032    ///
1033    /// This performs a fairly complex set of operations, in order to mimic behavior of nockchain
1034    /// CLI wallet's create-tx option. Note that we do not do 1-1 mapping of that functionality,
1035    /// most notably - if `recipient` is the same as `refund_pkh`, we will create 1 seed, while the
1036    /// CLI wallet will create 2.
1037    ///
1038    /// Another difference is that you should call `sign` and `validate` after calling this method.
1039    ///
1040    /// Internally, the transaction builder takes ALL of the `notes` provided, and stores them for
1041    /// fee adjustments. If there are multiple notes being used, our fee setup also differs from
1042    /// the CLI, because we first greedily spend the notes out, and then take fees from any
1043    /// remaining refunds.
1044    ///
1045    /// This function prioritizes using the least number of notes possible, because that lowers the
1046    /// fee used.
1047    ///
1048    /// You may choose to override the fee with `fee_override`, but do note that `validate` will
1049    /// fail, in case this fee is too small.
1050    ///
1051    /// `include_lock_data` can be used to include `%lock` key in note-data, with the
1052    /// `SpendCondition` used. However, note-data costs 1 << 15 nicks, which means, it can get
1053    /// expensive.
1054    ///
1055    /// Optional parameter `remove_unused_notes`, if set to false, will keep the notes in the
1056    /// transaction builder. This is meant to be used whenever additional operations are performed
1057    /// on the builder, such as additional spends, or `addPreimage` calls. All of these increase
1058    /// the required fee (which can be checked with `calcFee`), and unused notes can then be used
1059    /// to adjust fees with `setFeeAndBalanceRefund` or `recalcAndSetFee`. Once all operations are
1060    /// done, one should call `removeUnusedNotes` to ensure these notes are not used within the
1061    /// transaction.
1062    #[allow(clippy::too_many_arguments)]
1063    #[wasm_bindgen(js_name = simpleSpend)]
1064    pub fn simple_spend(
1065        &mut self,
1066        notes: Vec<WasmNote>,
1067        spend_conditions: Vec<WasmSpendCondition>,
1068        recipient: WasmDigest,
1069        gift: Nicks,
1070        fee_override: Option<Nicks>,
1071        refund_pkh: WasmDigest,
1072        include_lock_data: bool,
1073    ) -> Result<(), JsValue> {
1074        if notes.len() != spend_conditions.len() {
1075            return Err(JsValue::from_str(
1076                "notes and spend_conditions must have the same length",
1077            ));
1078        }
1079
1080        let internal_notes: Result<Vec<(Note, SpendCondition)>, String> = notes
1081            .iter()
1082            .zip(spend_conditions.iter())
1083            .map(|(n, sc)| Ok((n.to_internal()?, sc.to_internal()?)))
1084            .collect();
1085        let internal_notes = internal_notes.map_err(|e| JsValue::from_str(&e.to_string()))?;
1086
1087        self.builder
1088            .simple_spend_base(
1089                internal_notes,
1090                recipient.to_internal()?,
1091                gift,
1092                refund_pkh.to_internal()?,
1093                include_lock_data,
1094            )
1095            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
1096
1097        if let Some(fee) = fee_override {
1098            self.builder
1099                .set_fee_and_balance_refund(fee, false, include_lock_data)
1100        } else {
1101            self.builder.recalc_and_set_fee(include_lock_data)
1102        }
1103        .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
1104
1105        Ok(())
1106    }
1107
1108    /// Append a `SpendBuilder` to this transaction
1109    pub fn spend(&mut self, spend: WasmSpendBuilder) -> Option<WasmSpendBuilder> {
1110        self.builder.spend(spend.into()).map(|v| v.into())
1111    }
1112
1113    /// Distributes `fee` across builder's spends, and balances refunds out
1114    ///
1115    /// `adjust_fee` parameter allows the fee to be slightly tweaked, whenever notes are added or
1116    /// removed to/from the builder's fee note pool. This is because using more or less notes
1117    /// impacts the exact fee being required. If the caller estimates fee and sets it, adding more
1118    /// notes will change the exact fee needed, and setting this parameter to true will allow one
1119    /// to not have to call this function multiple times.
1120    #[wasm_bindgen(js_name = setFeeAndBalanceRefund)]
1121    pub fn set_fee_and_balance_refund(
1122        &mut self,
1123        fee: Nicks,
1124        adjust_fee: bool,
1125        include_lock_data: bool,
1126    ) -> Result<(), JsValue> {
1127        self.builder
1128            .set_fee_and_balance_refund(fee, adjust_fee, include_lock_data)
1129            .map_err(|e| e.to_string())?;
1130        Ok(())
1131    }
1132
1133    /// Recalculate fee and set it, balancing things out with refunds
1134    #[wasm_bindgen(js_name = recalcAndSetFee)]
1135    pub fn recalc_and_set_fee(&mut self, include_lock_data: bool) -> Result<(), JsValue> {
1136        self.builder
1137            .recalc_and_set_fee(include_lock_data)
1138            .map_err(|e| e.to_string())?;
1139        Ok(())
1140    }
1141
1142    /// Appends `preimage_jam` to all spend conditions that expect this preimage.
1143    #[wasm_bindgen(js_name = addPreimage)]
1144    pub fn add_preimage(&mut self, preimage_jam: &[u8]) -> Result<Option<WasmDigest>, JsValue> {
1145        let preimage = cue(preimage_jam).ok_or("Unable to cue preimage jam")?;
1146        Ok(self
1147            .builder
1148            .add_preimage(preimage)
1149            .map(|v| WasmDigest::from_internal(&v)))
1150    }
1151
1152    /// Sign the transaction with a private key.
1153    ///
1154    /// This will sign all spends that are still missing signature from
1155    #[wasm_bindgen]
1156    pub fn sign(&mut self, signing_key_bytes: &[u8]) -> Result<(), JsValue> {
1157        if signing_key_bytes.len() != 32 {
1158            return Err(JsValue::from_str("Private key must be 32 bytes"));
1159        }
1160        let signing_key = PrivateKey(UBig::from_be_bytes(signing_key_bytes));
1161
1162        self.builder.sign(&signing_key);
1163
1164        Ok(())
1165    }
1166
1167    /// Validate the transaction.
1168    #[wasm_bindgen]
1169    pub fn validate(&mut self) -> Result<(), JsValue> {
1170        self.builder
1171            .validate()
1172            .map_err(|v| JsValue::from_str(&v.to_string()))?;
1173
1174        Ok(())
1175    }
1176
1177    /// Gets the current fee set on all spends.
1178    #[wasm_bindgen(js_name = curFee)]
1179    pub fn cur_fee(&self) -> Nicks {
1180        self.builder.cur_fee()
1181    }
1182
1183    /// Calculates the fee needed for the transaction.
1184    ///
1185    /// NOTE: if the transaction is unsigned, this function will estimate the fee needed, supposing
1186    /// all signatures are added. However, this heuristic is only accurate for one signature. In
1187    /// addition, this fee calculation does not estimate the size of missing preimages.
1188    ///
1189    /// So, first, add missing preimages, and only then calc the fee. If you're building a multisig
1190    /// transaction, this value might be incorrect.
1191    #[wasm_bindgen(js_name = calcFee)]
1192    pub fn calc_fee(&self) -> Nicks {
1193        self.builder.calc_fee()
1194    }
1195
1196    #[wasm_bindgen(js_name = allNotes)]
1197    pub fn all_notes(&self) -> WasmTxNotes {
1198        let mut ret = WasmTxNotes {
1199            notes: vec![],
1200            spend_conditions: vec![],
1201        };
1202        self.builder
1203            .all_notes()
1204            .into_values()
1205            .for_each(|(note, spend_condition)| {
1206                ret.notes.push(WasmNote::from_internal(note));
1207                ret.spend_conditions
1208                    .push(WasmSpendCondition::from_internal(spend_condition));
1209            });
1210        ret
1211    }
1212
1213    #[wasm_bindgen]
1214    pub fn build(&self) -> Result<WasmRawTx, JsValue> {
1215        let tx = self.builder.build();
1216        Ok(WasmRawTx::from_internal(&tx))
1217    }
1218}
1219
1220#[wasm_bindgen(js_name = TxNotes)]
1221pub struct WasmTxNotes {
1222    #[wasm_bindgen(skip)]
1223    pub notes: Vec<WasmNote>,
1224    #[wasm_bindgen(skip)]
1225    pub spend_conditions: Vec<WasmSpendCondition>,
1226}
1227
1228#[wasm_bindgen(js_class = TxNotes)]
1229impl WasmTxNotes {
1230    #[wasm_bindgen(getter)]
1231    pub fn notes(&self) -> Vec<WasmNote> {
1232        self.notes.clone()
1233    }
1234
1235    #[wasm_bindgen(getter, js_name = spendConditions)]
1236    pub fn spend_conditions(&self) -> Vec<WasmSpendCondition> {
1237        self.spend_conditions.clone()
1238    }
1239}
1240
1241// ============================================================================
1242// Wasm Spend Builder
1243// ============================================================================
1244
1245#[wasm_bindgen(js_name = SpendBuilder)]
1246pub struct WasmSpendBuilder {
1247    builder: SpendBuilder,
1248}
1249
1250#[wasm_bindgen(js_class = SpendBuilder)]
1251impl WasmSpendBuilder {
1252    /// Create a new `SpendBuilder` with a given note and spend condition
1253    #[wasm_bindgen(constructor)]
1254    pub fn new(
1255        note: WasmNote,
1256        spend_condition: WasmSpendCondition,
1257        refund_lock: Option<WasmSpendCondition>,
1258    ) -> Result<Self, JsValue> {
1259        Ok(Self {
1260            builder: SpendBuilder::new(
1261                note.to_internal()?,
1262                spend_condition.to_internal()?,
1263                refund_lock.map(|v| v.to_internal()).transpose()?,
1264            ),
1265        })
1266    }
1267
1268    /// Set the fee of this spend
1269    pub fn fee(&mut self, fee: Nicks) {
1270        self.builder.fee(fee);
1271    }
1272
1273    /// Compute refund from any spare assets, given `refund_lock` was passed
1274    #[wasm_bindgen(js_name = computeRefund)]
1275    pub fn compute_refund(&mut self, include_lock_data: bool) {
1276        self.builder.compute_refund(include_lock_data);
1277    }
1278
1279    /// Get current refund
1280    #[wasm_bindgen(js_name = curRefund)]
1281    pub fn cur_refund(&self) -> Option<WasmSeed> {
1282        self.builder.cur_refund().map(|v| WasmSeed::from(v.clone()))
1283    }
1284
1285    /// Checks whether note.assets = seeds + fee
1286    ///
1287    /// This function needs to return true for `TxBuilder::validate` to pass
1288    #[wasm_bindgen(js_name = isBalanced)]
1289    pub fn is_balanced(&self) -> bool {
1290        self.builder.is_balanced()
1291    }
1292
1293    /// Add seed to this spend
1294    ///
1295    /// Seed is an output with a recipient (as defined by the spend condition).
1296    ///
1297    /// Nockchain transaction engine will take all seeds with matching lock from all spends in the
1298    /// transaction, and merge them into one output note.
1299    pub fn seed(&mut self, seed: WasmSeed) -> Result<(), JsValue> {
1300        self.builder.seed(seed.to_internal()?);
1301        Ok(())
1302    }
1303
1304    /// Manually invalidate signatures
1305    ///
1306    /// Each spend's fee+seeds are bound to one or more signatures. If they get changed, the
1307    /// signature becomes invalid. This builder automatically invalidates signatures upon relevant
1308    /// modifications, but this functionality is provided nonetheless.
1309    #[wasm_bindgen(js_name = invalidateSigs)]
1310    pub fn invalidate_sigs(&mut self) {
1311        self.builder.invalidate_sigs();
1312    }
1313
1314    /// Get the list of missing "unlocks"
1315    ///
1316    /// An unlock is a spend condition to be satisfied. For instance, for a `Pkh` spend condition,
1317    /// if the transaction is unsigned, this function will return a Pkh type missing unlock, with
1318    /// the list of valid PKH's and number of signatures needed. This will not return PKHs that are
1319    /// already attatched to the spend (relevant for multisigs). For `Hax` spend condition, this
1320    /// will return any missing preimages. This function will return a list of not-yet-validated
1321    /// spend conditions.
1322    #[wasm_bindgen(js_name = missingUnlocks)]
1323    pub fn missing_unlocks(&self) -> Result<Vec<JsValue>, JsValue> {
1324        self.builder
1325            .missing_unlocks()
1326            .into_iter()
1327            .map(|v| serde_wasm_bindgen::to_value(&WasmMissingUnlocks::from_internal(&v)))
1328            .collect::<Result<Vec<_>, _>>()
1329            .map_err(|e| e.into())
1330    }
1331
1332    /// Attatch a preimage to this spend
1333    #[wasm_bindgen(js_name = addPreimage)]
1334    pub fn add_preimage(&mut self, preimage_jam: &[u8]) -> Result<Option<WasmDigest>, JsValue> {
1335        let preimage = cue(preimage_jam).ok_or("Unable to cue preimage jam")?;
1336        Ok(self
1337            .builder
1338            .add_preimage(preimage)
1339            .map(|v| WasmDigest::from_internal(&v)))
1340    }
1341
1342    /// Sign the transaction with a given private key
1343    pub fn sign(&mut self, signing_key_bytes: &[u8]) -> Result<bool, JsValue> {
1344        if signing_key_bytes.len() != 32 {
1345            return Err(JsValue::from_str("Private key must be 32 bytes"));
1346        }
1347        let signing_key = PrivateKey(UBig::from_be_bytes(signing_key_bytes));
1348        Ok(self.builder.sign(&signing_key))
1349    }
1350}
1351
1352impl From<SpendBuilder> for WasmSpendBuilder {
1353    fn from(builder: SpendBuilder) -> Self {
1354        Self { builder }
1355    }
1356}
1357
1358impl From<WasmSpendBuilder> for SpendBuilder {
1359    fn from(value: WasmSpendBuilder) -> Self {
1360        value.builder
1361    }
1362}
1363
1364#[derive(Serialize, Deserialize)]
1365pub enum WasmMissingUnlocks {
1366    Pkh {
1367        num_sigs: u64,
1368        sig_of: BTreeSet<String>,
1369    },
1370    Hax {
1371        preimages_for: BTreeSet<String>,
1372    },
1373    Brn,
1374}
1375
1376impl WasmMissingUnlocks {
1377    fn from_internal(internal: &MissingUnlocks) -> Self {
1378        match internal {
1379            MissingUnlocks::Pkh { num_sigs, sig_of } => Self::Pkh {
1380                num_sigs: *num_sigs,
1381                sig_of: sig_of
1382                    .iter()
1383                    .map(|v| WasmDigest::from_internal(v).value)
1384                    .collect(),
1385            },
1386            MissingUnlocks::Hax { preimages_for } => Self::Hax {
1387                preimages_for: preimages_for
1388                    .iter()
1389                    .map(|v| WasmDigest::from_internal(v).value)
1390                    .collect(),
1391            },
1392            MissingUnlocks::Brn => Self::Brn,
1393        }
1394    }
1395}
1396
1397// ============================================================================
1398// Wasm Raw Transaction
1399// ============================================================================
1400
1401#[wasm_bindgen(js_name = RawTx)]
1402pub struct WasmRawTx {
1403    // Store the full RawTx internally so we can convert to protobuf
1404    #[wasm_bindgen(skip)]
1405    pub(crate) internal: RawTx,
1406}
1407
1408#[wasm_bindgen(js_class = RawTx)]
1409impl WasmRawTx {
1410    #[wasm_bindgen(getter)]
1411    pub fn version(&self) -> WasmVersion {
1412        WasmVersion::from_internal(&self.internal.version)
1413    }
1414
1415    #[wasm_bindgen(getter)]
1416    pub fn id(&self) -> WasmDigest {
1417        WasmDigest::from_internal(&self.internal.id)
1418    }
1419
1420    #[wasm_bindgen(getter)]
1421    pub fn name(&self) -> String {
1422        self.internal.id.to_string()
1423    }
1424
1425    fn from_internal(tx: &RawTx) -> Self {
1426        Self {
1427            internal: tx.clone(),
1428        }
1429    }
1430
1431    /// Convert to protobuf RawTransaction for sending via gRPC
1432    #[wasm_bindgen(js_name = toProtobuf)]
1433    pub fn to_protobuf(&self) -> Result<JsValue, JsValue> {
1434        let pb_tx = pb::RawTransaction::from(self.internal.clone());
1435        serde_wasm_bindgen::to_value(&pb_tx)
1436            .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1437    }
1438
1439    #[wasm_bindgen(js_name = fromProtobuf)]
1440    pub fn from_protobuf(value: JsValue) -> Result<WasmRawTx, JsValue> {
1441        let pb: pb::RawTransaction = serde_wasm_bindgen::from_value(value)?;
1442        let tx: RawTx = pb
1443            .try_into()
1444            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
1445        Ok(WasmRawTx::from_internal(&tx))
1446    }
1447
1448    /// Convert to jammed transaction file for inspecting through CLI
1449    #[wasm_bindgen(js_name = toJam)]
1450    pub fn to_jam(&self) -> js_sys::Uint8Array {
1451        let n = self.internal.to_nockchain_tx();
1452        js_sys::Uint8Array::from(&jam(n)[..])
1453    }
1454
1455    #[wasm_bindgen(js_name = fromJam)]
1456    pub fn from_jam(jam: &[u8]) -> Result<Self, JsValue> {
1457        let n = cue(jam).ok_or("Unable to decode jam")?;
1458        let (txid, spends): (String, Spends) =
1459            NounDecode::from_noun(&n).ok_or("Unable to decode noun")?;
1460        let tx = RawTx {
1461            version: Version::V1,
1462            id: Digest::try_from(&*txid)?,
1463            spends,
1464        };
1465        Ok(Self::from_internal(&tx))
1466    }
1467
1468    /// Calculate output notes from the transaction spends.
1469    #[wasm_bindgen]
1470    pub fn outputs(&self) -> Vec<WasmNote> {
1471        self.internal
1472            .outputs()
1473            .into_iter()
1474            .map(WasmNote::from_internal)
1475            .collect()
1476    }
1477}