1use alloc::format;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use std::collections::BTreeMap;
5
6use iris_crypto::PrivateKey;
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, Version},
12 tx::RawTx,
13 v0,
14 v1::{self, NockchainTx, NoteData, RawTxV1, SeedV1 as Seed, SpendCondition},
15 BlockHeight, Nicks, Source, SpendBuilder, TxEngineSettings,
16};
17use iris_ztd::{cue, Digest, ZSet, U256};
18use serde::{Deserialize, Serialize};
19use wasm_bindgen::prelude::*;
20
21#[wasm_bindgen]
26pub fn digest_to_hex(d: Digest) -> String {
27 d.to_string()
28}
29
30#[wasm_bindgen]
31pub fn hex_to_digest(s: &str) -> Result<Digest, JsValue> {
32 s.try_into().map_err(|e: &str| JsValue::from_str(e))
33}
34
35#[wasm_bindgen]
36pub fn digest_to_protobuf(d: Digest) -> pb_v1::Hash {
37 d.into()
38}
39
40#[wasm_bindgen]
41pub fn digest_from_protobuf(value: pb_v1::Hash) -> Result<Digest, JsValue> {
42 value
43 .try_into()
44 .map_err(|e| JsValue::from_str(&format!("{}", e)))
45}
46
47#[wasm_bindgen]
48pub fn note_hash(note: Note) -> Digest {
49 use iris_ztd::Hashable;
50 note.hash()
51}
52
53#[wasm_bindgen]
54pub fn note_to_protobuf(note: Note) -> pb::Note {
55 note.into()
56}
57
58#[wasm_bindgen]
59pub fn note_from_protobuf(value: pb::Note) -> Result<Note, JsValue> {
60 value
61 .try_into()
62 .map_err(|e| JsValue::from_str(&format!("{}", e)))
63}
64
65#[wasm_bindgen(js_name = nockchainTxToRaw)]
66pub fn nockchain_tx_to_raw(tx: NockchainTx) -> RawTx {
67 RawTx::V1(tx.to_raw_tx())
68}
69
70#[wasm_bindgen(js_name = rawTxToNockchainTx)]
71pub fn raw_tx_to_nockchain_tx(tx: RawTxV1) -> NockchainTx {
72 tx.to_nockchain_tx()
73}
74
75#[wasm_bindgen(js_name = rawTxToProtobuf)]
76pub fn raw_tx_to_protobuf(tx: RawTxV1) -> pb::RawTransaction {
77 tx.into()
78}
79
80#[wasm_bindgen(js_name = rawTxFromProtobuf)]
81pub fn raw_tx_from_protobuf(tx: pb::RawTransaction) -> Result<RawTx, JsValue> {
82 tx.try_into()
83 .map_err(|e| JsValue::from_str(&format!("{}", e)))
84}
85
86#[wasm_bindgen(js_name = rawTxOutputs)]
87pub fn raw_tx_outputs(tx: RawTx) -> Vec<Note> {
88 tx.outputs()
89}
90
91#[wasm_bindgen]
93pub fn create_note_v1(
94 version: Version,
95 origin_page: BlockHeight,
96 name: Name,
97 note_data: NoteData,
98 assets: Nicks,
99) -> Result<Note, JsValue> {
100 let internal = Note::V1(v1::NoteV1::new(
101 version,
102 origin_page,
103 name,
104 note_data,
105 assets,
106 ));
107 Ok(internal)
108}
109
110#[wasm_bindgen]
112pub fn create_note_v0(
113 origin_page: BlockHeight,
114 sig_m: u64,
115 sig_pubkeys: Vec<js_sys::Uint8Array>,
116 source_hash: Digest,
117 is_coinbase: bool,
118 timelock: Option<v0::Timelock>,
119 assets: Nicks,
120) -> Result<Note, JsValue> {
121 use iris_crypto::PublicKey;
122 let pubkeys: Result<ZSet<PublicKey>, JsValue> = sig_pubkeys
126 .iter()
127 .map(|arr| {
128 let bytes = arr.to_vec();
129 if bytes.len() != 97 {
130 return Err(JsValue::from_str(&format!(
131 "Public key must be 97 bytes, got {}",
132 bytes.len()
133 )));
134 }
135 let mut arr = [0u8; 97];
136 arr.copy_from_slice(&bytes);
137 Ok(PublicKey::from_be_bytes(&arr))
138 })
139 .collect();
140 let pubkeys = pubkeys?;
141
142 let sig = v0::Sig { m: sig_m, pubkeys };
143
144 let source = Source {
145 hash: source_hash,
146 is_coinbase,
147 };
148
149 let timelock_intent = v0::TimelockIntent { tim: timelock };
150
151 let name = Name::new_v0(sig.clone(), source, timelock_intent);
152
153 let internal = Note::V0(v0::NoteV0::new(
154 Version::V0,
155 origin_page,
156 timelock_intent,
157 name,
158 sig,
159 source,
160 assets,
161 ));
162 Ok(internal)
163}
164
165#[derive(Serialize, Deserialize, tsify::Tsify)]
166#[tsify(into_wasm_abi, from_wasm_abi)]
167pub struct TxNotes {
168 pub notes: Vec<Note>,
169 pub spend_conditions: Vec<SpendCondition>,
170}
171
172#[wasm_bindgen(js_name = TxBuilder)]
177pub struct WasmTxBuilder {
178 builder: TxBuilder,
179}
180
181#[wasm_bindgen(js_class = TxBuilder)]
182impl WasmTxBuilder {
183 #[wasm_bindgen(constructor)]
185 pub fn new(settings: TxEngineSettings) -> Self {
186 Self {
187 builder: TxBuilder::new(settings),
188 }
189 }
190
191 #[wasm_bindgen(js_name = fromTx)]
193 pub fn from_tx(
194 tx: RawTx,
195 notes: Vec<Note>,
196 spend_conditions: Vec<SpendCondition>,
197 settings: TxEngineSettings,
198 ) -> Result<Self, JsValue> {
199 if notes.len() != spend_conditions.len() {
200 return Err(JsValue::from_str(
201 "notes and spend_conditions must have the same length",
202 ));
203 }
204
205 let internal_notes: Result<BTreeMap<Name, (Note, Option<SpendCondition>)>, String> = notes
206 .into_iter()
207 .zip(spend_conditions)
208 .map(|(n, sc)| Ok((n.name(), (n, Some(sc)))))
209 .collect();
210 let internal_notes = internal_notes.map_err(|e| JsValue::from_str(&e))?;
211
212 let builder =
213 TxBuilder::from_tx(tx, internal_notes, settings).map_err(|e| e.to_string())?;
214
215 Ok(Self { builder })
216 }
217
218 #[allow(clippy::too_many_arguments)]
219 #[wasm_bindgen(js_name = simpleSpend)]
220 pub fn simple_spend(
221 &mut self,
222 notes: Vec<Note>,
223 spend_conditions: Vec<SpendCondition>,
224 recipient: Digest,
225 gift: Nicks,
226 fee_override: Option<Nicks>,
227 refund_pkh: Digest,
228 include_lock_data: bool,
229 ) -> Result<(), JsValue> {
230 if notes.len() != spend_conditions.len() {
231 return Err(JsValue::from_str(
232 "notes and spend_conditions must have the same length",
233 ));
234 }
235
236 let internal_notes: Vec<(Note, Option<SpendCondition>)> = notes
237 .into_iter()
238 .zip(spend_conditions)
239 .map(|(n, sc)| (n, Some(sc)))
240 .collect();
241
242 self.builder
243 .simple_spend_base(
244 internal_notes,
245 recipient,
246 gift,
247 refund_pkh,
248 include_lock_data,
249 )
250 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
251
252 if let Some(fee) = fee_override {
253 self.builder
254 .set_fee_and_balance_refund(fee, false, include_lock_data)
255 } else {
256 self.builder.recalc_and_set_fee(include_lock_data)
257 }
258 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
259
260 Ok(())
261 }
262
263 pub fn spend(&mut self, spend: WasmSpendBuilder) -> Option<WasmSpendBuilder> {
265 self.builder.spend(spend.into()).map(|v| v.into())
266 }
267
268 #[wasm_bindgen(js_name = setFeeAndBalanceRefund)]
269 pub fn set_fee_and_balance_refund(
270 &mut self,
271 fee: Nicks,
272 adjust_fee: bool,
273 include_lock_data: bool,
274 ) -> Result<(), JsValue> {
275 self.builder
276 .set_fee_and_balance_refund(fee, adjust_fee, include_lock_data)
277 .map_err(|e| e.to_string())?;
278 Ok(())
279 }
280
281 #[wasm_bindgen(js_name = recalcAndSetFee)]
282 pub fn recalc_and_set_fee(&mut self, include_lock_data: bool) -> Result<(), JsValue> {
283 self.builder
284 .recalc_and_set_fee(include_lock_data)
285 .map_err(|e| e.to_string())?;
286 Ok(())
287 }
288
289 #[wasm_bindgen(js_name = addPreimage)]
290 pub fn add_preimage(&mut self, preimage_jam: &[u8]) -> Result<Option<Digest>, JsValue> {
291 let preimage = cue(preimage_jam).ok_or("Unable to cue preimage jam")?;
292 Ok(self.builder.add_preimage(preimage))
293 }
294
295 #[wasm_bindgen]
296 pub fn sign(&mut self, signing_key_bytes: &[u8]) -> Result<(), JsValue> {
297 if signing_key_bytes.len() != 32 {
298 return Err(JsValue::from_str("Private key must be 32 bytes"));
299 }
300 let signing_key = PrivateKey(U256::from_be_slice(signing_key_bytes));
301
302 self.builder.sign(&signing_key);
303
304 Ok(())
305 }
306
307 #[wasm_bindgen]
308 pub fn validate(&mut self) -> Result<(), JsValue> {
309 self.builder
310 .validate()
311 .map_err(|v| JsValue::from_str(&v.to_string()))?;
312
313 Ok(())
314 }
315
316 #[wasm_bindgen(js_name = curFee)]
317 pub fn cur_fee(&self) -> Nicks {
318 self.builder.cur_fee()
319 }
320
321 #[wasm_bindgen(js_name = calcFee)]
322 pub fn calc_fee(&self) -> Nicks {
323 self.builder.calc_fee()
324 }
325
326 #[wasm_bindgen(js_name = allNotes)]
327 pub fn all_notes(&self) -> Result<TxNotes, JsValue> {
328 let mut ret = TxNotes {
329 notes: vec![],
330 spend_conditions: vec![],
331 };
332 for (note, spend_condition) in self.builder.all_notes().into_values() {
333 ret.notes.push(note);
334 if let Some(sc) = spend_condition {
335 ret.spend_conditions.push(sc);
336 }
337 }
338 Ok(ret)
339 }
340
341 #[wasm_bindgen]
342 pub fn build(&self) -> Result<NockchainTx, JsValue> {
343 Ok(self.builder.build())
344 }
345
346 #[wasm_bindgen(js_name = allSpends)]
347 pub fn all_spends(&self) -> Vec<WasmSpendBuilder> {
348 self.builder
349 .all_spends()
350 .values()
351 .map(WasmSpendBuilder::from_internal)
352 .collect()
353 }
354}
355
356#[wasm_bindgen(js_name = SpendBuilder)]
361pub struct WasmSpendBuilder {
362 builder: SpendBuilder,
363}
364
365#[wasm_bindgen(js_class = SpendBuilder)]
366impl WasmSpendBuilder {
367 #[wasm_bindgen(constructor)]
369 pub fn new(
370 note: Note,
371 spend_condition: Option<SpendCondition>,
372 refund_lock: Option<SpendCondition>,
373 ) -> Result<Self, JsValue> {
374 Ok(Self {
375 builder: SpendBuilder::new(note, spend_condition, refund_lock)
376 .map_err(|e| JsValue::from_str(&e.to_string()))?,
377 })
378 }
379
380 pub fn fee(&mut self, fee: Nicks) {
381 self.builder.fee(fee);
382 }
383
384 #[wasm_bindgen(js_name = computeRefund)]
385 pub fn compute_refund(&mut self, include_lock_data: bool) {
386 self.builder.compute_refund(include_lock_data);
387 }
388
389 #[wasm_bindgen(js_name = curRefund)]
390 pub fn cur_refund(&self) -> Option<Seed> {
391 self.builder.cur_refund().cloned()
392 }
393
394 #[wasm_bindgen(js_name = isBalanced)]
395 pub fn is_balanced(&self) -> bool {
396 self.builder.is_balanced()
397 }
398
399 pub fn seed(&mut self, seed: Seed) -> Result<(), JsValue> {
400 self.builder.seed(seed);
401 Ok(())
402 }
403
404 #[wasm_bindgen(js_name = invalidateSigs)]
405 pub fn invalidate_sigs(&mut self) {
406 self.builder.invalidate_sigs();
407 }
408
409 #[wasm_bindgen(js_name = missingUnlocks)]
410 pub fn missing_unlocks(&self) -> Result<Vec<MissingUnlocks>, JsValue> {
411 Ok(self.builder.missing_unlocks())
413 }
414
415 #[wasm_bindgen(js_name = addPreimage)]
416 pub fn add_preimage(&mut self, preimage_jam: &[u8]) -> Result<Option<Digest>, JsValue> {
417 let preimage = cue(preimage_jam).ok_or("Unable to cue preimage jam")?;
418 Ok(self.builder.add_preimage(preimage))
419 }
420
421 pub fn sign(&mut self, signing_key_bytes: &[u8]) -> Result<bool, JsValue> {
422 if signing_key_bytes.len() != 32 {
423 return Err(JsValue::from_str("Private key must be 32 bytes"));
424 }
425 let signing_key = PrivateKey(U256::from_be_slice(signing_key_bytes));
426 Ok(self.builder.sign(&signing_key))
427 }
428
429 fn from_internal(internal: &SpendBuilder) -> Self {
430 Self {
431 builder: internal.clone(),
432 }
433 }
434}
435
436impl From<SpendBuilder> for WasmSpendBuilder {
437 fn from(builder: SpendBuilder) -> Self {
438 Self { builder }
439 }
440}
441
442impl From<WasmSpendBuilder> for SpendBuilder {
443 fn from(value: WasmSpendBuilder) -> Self {
444 value.builder
445 }
446}