Skip to main content

iris_wasm/
tx.rs

1use alloc::format;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use std::collections::BTreeMap;
5
6use iris_crypto::PrivateKey as CryptoPrivateKey;
7use iris_grpc_proto::pb::common::v1 as pb_v1;
8use iris_grpc_proto::pb::common::v2 as pb;
9use iris_nockchain_types::{
10    builder::{MissingUnlocks, TxBuilder},
11    note::{Name, Note},
12    tx::RawTx,
13    v1::{Lock, LockRoot, NockchainTx, RawTxV1, SeedV1 as Seed, SpendCondition},
14    Nicks, SpendBuilder, TxEngineSettings,
15};
16use iris_ztd::{cue, Digest, U256};
17use serde::{Deserialize, Serialize};
18use wasm_bindgen::prelude::*;
19
20// ============================================================================
21// Wasm Types - Adapters and Helpers
22// ============================================================================
23
24#[wasm_bindgen(js_name = digestToProtobuf)]
25pub fn digest_to_protobuf(d: Digest) -> pb_v1::Hash {
26    d.into()
27}
28
29#[wasm_bindgen(js_name = digestFromProtobuf)]
30pub fn digest_from_protobuf(value: pb_v1::Hash) -> Result<Digest, JsValue> {
31    value
32        .try_into()
33        .map_err(|e| JsValue::from_str(&format!("{}", e)))
34}
35
36/// Return default transaction engine settings for V1 signing.
37#[wasm_bindgen(js_name = txEngineSettingsV1Default)]
38pub fn tx_engine_settings_v1_default() -> TxEngineSettings {
39    TxEngineSettings::v1_default()
40}
41
42/// Return default transaction engine settings for V1 Bythos signing.
43#[wasm_bindgen(js_name = txEngineSettingsV1BythosDefault)]
44pub fn tx_engine_settings_v1_bythos_default() -> TxEngineSettings {
45    TxEngineSettings::v1_bythos_default()
46}
47
48/// Convert protobuf spend condition to native SpendCondition.
49/// Accepts the protobuf format used by the Nockchain gRPC interface and external dApps
50#[wasm_bindgen(js_name = spendConditionFromProtobuf)]
51pub fn spend_condition_from_protobuf(value: pb::SpendCondition) -> Result<SpendCondition, JsValue> {
52    value
53        .try_into()
54        .map_err(|e| JsValue::from_str(&format!("{}", e)))
55}
56
57/// Convert native SpendCondition to protobuf format.
58/// Returns the protobuf format used by the Nockchain gRPC interface and external dApps.
59#[wasm_bindgen(js_name = spendConditionToProtobuf)]
60pub fn spend_condition_to_protobuf(value: SpendCondition) -> pb::SpendCondition {
61    value.into()
62}
63
64#[wasm_bindgen(js_name = noteToProtobuf)]
65pub fn note_to_protobuf(note: Note) -> pb::Note {
66    note.into()
67}
68
69#[wasm_bindgen(js_name = noteFromProtobuf)]
70pub fn note_from_protobuf(value: pb::Note) -> Result<Note, JsValue> {
71    value
72        .try_into()
73        .map_err(|e| JsValue::from_str(&format!("{}", e)))
74}
75
76/// Convert raw transaction into protobuf format.
77///
78/// Protobuf format is the one used by the Nockchain's gRPC interface, and the initial iris
79/// extension format. The new iris transaction signing API moves away from this format to use
80/// `NockchainTx`, as it includes the necessary spend condition and note information.
81#[wasm_bindgen(js_name = rawTxToProtobuf)]
82pub fn raw_tx_to_protobuf(tx: RawTxV1) -> pb::RawTransaction {
83    tx.into()
84}
85
86#[wasm_bindgen(js_name = rawTxFromProtobuf)]
87pub fn raw_tx_from_protobuf(tx: pb::RawTransaction) -> Result<RawTx, JsValue> {
88    tx.try_into()
89        .map_err(|e| JsValue::from_str(&format!("{}", e)))
90}
91
92#[wasm_bindgen]
93pub fn locky(sp: iris_nockchain_types::v1::SpendCondition) -> iris_nockchain_types::v1::Lock {
94    iris_nockchain_types::v1::Lock::Single(sp)
95}
96
97#[derive(Serialize, Deserialize, tsify::Tsify)]
98#[tsify(into_wasm_abi, from_wasm_abi)]
99pub struct TxNotes {
100    pub notes: Vec<Note>,
101    pub refund_locks: Vec<Option<LockRoot>>,
102}
103
104#[derive(Serialize, Deserialize, tsify::Tsify)]
105#[tsify(into_wasm_abi, from_wasm_abi)]
106#[serde(untagged)]
107#[allow(clippy::large_enum_variant)]
108pub enum TxLock {
109    None,
110    Some { lock: Lock, lock_sp_index: usize },
111}
112
113impl TxLock {
114    fn into_tuple(self) -> Option<(Lock, usize)> {
115        match self {
116            TxLock::None => None,
117            TxLock::Some {
118                lock,
119                lock_sp_index,
120            } => Some((lock, lock_sp_index)),
121        }
122    }
123}
124
125// ============================================================================
126// Wasm Transaction Builder
127// ============================================================================
128
129enum PrivateKeyBackend {
130    Bytes(BytesPrivateKeyBackend),
131}
132
133struct BytesPrivateKeyBackend {
134    signing_key: CryptoPrivateKey,
135    public_key_bytes: [u8; 97],
136}
137
138#[wasm_bindgen(js_name = PrivateKey)]
139pub struct WasmPrivateKey {
140    backend: PrivateKeyBackend,
141}
142
143#[wasm_bindgen(js_class = PrivateKey)]
144impl WasmPrivateKey {
145    /// Construct a wasm `PrivateKey` from 32-byte private key material.
146    ///
147    /// This object is created in JavaScript and then passed into Rust signing APIs.
148    ///
149    /// # JavaScript example
150    ///
151    /// ```javascript
152    /// import init, { PrivateKey, TxBuilder } from "iris-wasm";
153    ///
154    /// await init();
155    ///
156    /// const keyBytes = Uint8Array.from([
157    ///   // 32 bytes
158    /// ]);
159    ///
160    /// const key = PrivateKey.fromBytes(keyBytes);
161    ///
162    /// const builder = new TxBuilder(settings);
163    /// // ... configure builder ...
164    /// await builder.sign(key);
165    /// ```
166    #[wasm_bindgen(constructor)]
167    pub fn new(signing_key_bytes: &[u8]) -> Result<Self, JsValue> {
168        Self::from_bytes(signing_key_bytes)
169    }
170
171    /// Construct a bytes-backed key.
172    #[wasm_bindgen(js_name = fromBytes)]
173    pub fn from_bytes(signing_key_bytes: &[u8]) -> Result<Self, JsValue> {
174        if signing_key_bytes.len() != 32 {
175            return Err(JsValue::from_str("Private key must be 32 bytes"));
176        }
177
178        let signing_key = CryptoPrivateKey(U256::from_be_slice(signing_key_bytes));
179        let public_key_bytes = signing_key.public_key().to_be_bytes();
180
181        Ok(Self {
182            backend: PrivateKeyBackend::Bytes(BytesPrivateKeyBackend {
183                signing_key,
184                public_key_bytes,
185            }),
186        })
187    }
188
189    /// Return this key's public key as 97-byte uncompressed bytes.
190    #[wasm_bindgen(getter, js_name = publicKey)]
191    pub fn public_key(&self) -> Vec<u8> {
192        match &self.backend {
193            PrivateKeyBackend::Bytes(bytes_backend) => bytes_backend.public_key_bytes.to_vec(),
194        }
195    }
196
197    /// Return the derivation path for this key backend, if available.
198    ///
199    /// Bytes-backed keys return `undefined` in JavaScript.
200    #[wasm_bindgen(getter, js_name = derivationPath)]
201    pub fn derivation_path(&self) -> Option<String> {
202        match &self.backend {
203            PrivateKeyBackend::Bytes(_) => None,
204        }
205    }
206
207    /// Return the backend kind for debugging and feature checks.
208    #[wasm_bindgen(js_name = backendKind)]
209    pub fn backend_kind(&self) -> String {
210        match &self.backend {
211            PrivateKeyBackend::Bytes(_) => "bytes".to_string(),
212        }
213    }
214}
215
216impl WasmPrivateKey {
217    fn signing_key(&self) -> &CryptoPrivateKey {
218        match &self.backend {
219            PrivateKeyBackend::Bytes(bytes_backend) => &bytes_backend.signing_key,
220        }
221    }
222}
223
224#[wasm_bindgen(js_name = TxBuilder)]
225pub struct WasmTxBuilder {
226    builder: TxBuilder,
227}
228
229#[wasm_bindgen(js_class = TxBuilder)]
230impl WasmTxBuilder {
231    /// Create an empty transaction builder
232    #[wasm_bindgen(constructor)]
233    pub fn new(settings: TxEngineSettings) -> Self {
234        Self {
235            builder: TxBuilder::new(settings),
236        }
237    }
238
239    /// Reconstruct a builder from raw transaction and its input notes.
240    #[wasm_bindgen(js_name = fromTx)]
241    pub fn from_tx(
242        tx: RawTx,
243        notes: Vec<Note>,
244        refund_lock: Option<LockRoot>,
245        settings: TxEngineSettings,
246    ) -> Result<Self, JsValue> {
247        let internal_notes: BTreeMap<Name, (Note, Option<LockRoot>)> = notes
248            .into_iter()
249            .map(|n| (n.name(), (n, refund_lock.clone())))
250            .collect();
251
252        let builder =
253            TxBuilder::from_tx(tx, internal_notes, settings).map_err(|e| e.to_string())?;
254
255        Ok(Self { builder })
256    }
257
258    #[allow(clippy::too_many_arguments)]
259    #[wasm_bindgen(js_name = simpleSpend)]
260    pub fn simple_spend(
261        &mut self,
262        notes: Vec<Note>,
263        locks: Vec<TxLock>,
264        recipient: Digest,
265        gift: Nicks,
266        fee_override: Option<Nicks>,
267        refund_pkh: Digest,
268        include_lock_data: bool,
269    ) -> Result<(), JsValue> {
270        if notes.len() != locks.len() {
271            return Err(JsValue::from_str(
272                "notes and locks must have the same length",
273            ));
274        }
275
276        let internal_notes: Vec<(Note, Option<(Lock, usize)>)> = notes
277            .into_iter()
278            .zip(locks)
279            .map(|(n, lck)| (n, lck.into_tuple()))
280            .collect();
281
282        self.builder
283            .simple_spend_base(
284                internal_notes,
285                recipient,
286                gift,
287                refund_pkh,
288                include_lock_data,
289            )
290            .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
291
292        if let Some(fee) = fee_override {
293            self.builder
294                .set_fee_and_balance_refund(fee, false, include_lock_data)
295        } else {
296            self.builder.recalc_and_set_fee(include_lock_data)
297        }
298        .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
299
300        Ok(())
301    }
302
303    /// Append a `SpendBuilder` to this transaction
304    pub fn spend(&mut self, spend: WasmSpendBuilder) -> Option<WasmSpendBuilder> {
305        self.builder.spend(spend.into()).map(|v| v.into())
306    }
307
308    #[wasm_bindgen(js_name = setFeeAndBalanceRefund)]
309    pub fn set_fee_and_balance_refund(
310        &mut self,
311        fee: Nicks,
312        adjust_fee: bool,
313        include_lock_data: bool,
314    ) -> Result<(), JsValue> {
315        self.builder
316            .set_fee_and_balance_refund(fee, adjust_fee, include_lock_data)
317            .map_err(|e| e.to_string())?;
318        Ok(())
319    }
320
321    #[wasm_bindgen(js_name = recalcAndSetFee)]
322    pub fn recalc_and_set_fee(&mut self, include_lock_data: bool) -> Result<(), JsValue> {
323        self.builder
324            .recalc_and_set_fee(include_lock_data)
325            .map_err(|e| e.to_string())?;
326        Ok(())
327    }
328
329    #[wasm_bindgen(js_name = addPreimage)]
330    pub fn add_preimage(&mut self, preimage_jam: &[u8]) -> Result<Option<Digest>, JsValue> {
331        let preimage = cue(preimage_jam).ok_or("Unable to cue preimage jam")?;
332        Ok(self.builder.add_preimage(preimage))
333    }
334
335    #[wasm_bindgen]
336    pub async fn sign(&mut self, signing_key: &WasmPrivateKey) -> Result<(), JsValue> {
337        self.builder.sign(signing_key.signing_key());
338
339        Ok(())
340    }
341
342    #[wasm_bindgen]
343    pub fn validate(&mut self) -> Result<(), JsValue> {
344        self.builder
345            .validate()
346            .map_err(|v| JsValue::from_str(&v.to_string()))?;
347
348        Ok(())
349    }
350
351    #[wasm_bindgen(js_name = curFee)]
352    pub fn cur_fee(&self) -> Nicks {
353        self.builder.cur_fee()
354    }
355
356    #[wasm_bindgen(js_name = calcFee)]
357    pub fn calc_fee(&self) -> Nicks {
358        self.builder.calc_fee()
359    }
360
361    #[wasm_bindgen(js_name = allNotes)]
362    pub fn all_notes(&self) -> Result<Vec<Note>, JsValue> {
363        let mut ret = Vec::new();
364        for note in self.builder.all_notes().into_values() {
365            ret.push(note);
366        }
367        Ok(ret)
368    }
369
370    #[wasm_bindgen]
371    pub fn build(&self) -> Result<NockchainTx, JsValue> {
372        Ok(self.builder.build())
373    }
374
375    #[wasm_bindgen(js_name = allSpends)]
376    pub fn all_spends(&self) -> Vec<WasmSpendBuilder> {
377        self.builder
378            .all_spends()
379            .values()
380            .map(WasmSpendBuilder::from_internal)
381            .collect()
382    }
383}
384
385// ============================================================================
386// Wasm Spend Builder
387// ============================================================================
388
389#[wasm_bindgen(js_name = SpendBuilder)]
390pub struct WasmSpendBuilder {
391    builder: SpendBuilder,
392}
393
394#[wasm_bindgen(js_class = SpendBuilder)]
395impl WasmSpendBuilder {
396    /// Create a new `SpendBuilder` with a given note and spend condition
397    #[wasm_bindgen(constructor)]
398    pub fn new(
399        note: Note,
400        lock: Option<Lock>,
401        lock_sp_index: Option<usize>,
402        refund_lock: Option<LockRoot>,
403    ) -> Result<Self, JsValue> {
404        Ok(Self {
405            builder: SpendBuilder::new(note, lock.zip(lock_sp_index), refund_lock)
406                .map_err(|e| JsValue::from_str(&e.to_string()))?,
407        })
408    }
409
410    pub fn fee(&mut self, fee: Nicks) {
411        self.builder.fee(fee);
412    }
413
414    #[wasm_bindgen(js_name = computeRefund)]
415    pub fn compute_refund(&mut self, include_lock_data: bool) {
416        self.builder.compute_refund(include_lock_data);
417    }
418
419    #[wasm_bindgen(js_name = curRefund)]
420    pub fn cur_refund(&self) -> Option<Seed> {
421        self.builder.cur_refund().cloned()
422    }
423
424    #[wasm_bindgen(js_name = isBalanced)]
425    pub fn is_balanced(&self) -> bool {
426        self.builder.is_balanced()
427    }
428
429    pub fn seed(&mut self, seed: Seed) -> Result<(), JsValue> {
430        self.builder.seed(seed);
431        Ok(())
432    }
433
434    #[wasm_bindgen(js_name = invalidateSigs)]
435    pub fn invalidate_sigs(&mut self) {
436        self.builder.invalidate_sigs();
437    }
438
439    #[wasm_bindgen(js_name = missingUnlocks)]
440    pub fn missing_unlocks(&self) -> Result<Vec<MissingUnlocks>, JsValue> {
441        // MissingUnlocks is now Tsify, so we can return Vec<MissingUnlocks>
442        Ok(self.builder.missing_unlocks())
443    }
444
445    #[wasm_bindgen(js_name = addPreimage)]
446    pub fn add_preimage(&mut self, preimage_jam: &[u8]) -> Result<Option<Digest>, JsValue> {
447        let preimage = cue(preimage_jam).ok_or("Unable to cue preimage jam")?;
448        Ok(self.builder.add_preimage(preimage))
449    }
450
451    pub async fn sign(&mut self, signing_key: &WasmPrivateKey) -> Result<bool, JsValue> {
452        Ok(self.builder.sign(signing_key.signing_key()))
453    }
454
455    fn from_internal(internal: &SpendBuilder) -> Self {
456        Self {
457            builder: internal.clone(),
458        }
459    }
460}
461
462impl From<SpendBuilder> for WasmSpendBuilder {
463    fn from(builder: SpendBuilder) -> Self {
464        Self { builder }
465    }
466}
467
468impl From<WasmSpendBuilder> for SpendBuilder {
469    fn from(value: WasmSpendBuilder) -> Self {
470        value.builder
471    }
472}