ergo_lib_wasm/
transaction.rs

1//! Ergo transaction
2
3use crate::box_coll::ErgoBoxCandidates;
4use crate::box_coll::ErgoBoxes;
5use crate::context_extension::ContextExtension;
6use crate::data_input::DataInputs;
7use crate::ergo_box::BoxId;
8use crate::ergo_box::ErgoBox;
9use crate::error_conversion::to_js;
10use crate::input::{Inputs, UnsignedInputs};
11use crate::json::TransactionJsonEip12;
12use crate::json::UnsignedTransactionJsonEip12;
13use ergo_lib::chain;
14use ergo_lib::chain::transaction::{distinct_token_ids, TxIoVec};
15use ergo_lib::ergotree_ir::serialization::SigmaSerializable;
16use ergo_lib::wallet::signing::make_context;
17use gloo_utils::format::JsValueSerdeExt;
18use js_sys::Uint8Array;
19use std::convert::{TryFrom, TryInto};
20use wasm_bindgen::prelude::*;
21
22extern crate derive_more;
23
24use crate::ergo_state_ctx::ErgoStateContext;
25use crate::transaction::reduced::Propositions;
26use derive_more::{From, Into};
27use ergo_lib::ergo_chain_types::{Base16DecodedBytes, Base16EncodedBytes, Digest32};
28
29pub mod reduced;
30
31/// CommitmentHint
32#[wasm_bindgen]
33pub struct CommitmentHint(
34    ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::CommitmentHint,
35);
36
37/// HintsBag
38#[wasm_bindgen]
39pub struct HintsBag(
40    pub(crate) ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::HintsBag,
41);
42
43#[wasm_bindgen]
44impl HintsBag {
45    /// Empty HintsBag
46    pub fn empty() -> HintsBag {
47        HintsBag(ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::HintsBag::empty())
48    }
49
50    /// Add commitment hint to the bag
51    pub fn add_commitment(&mut self, hint: CommitmentHint) {
52        self.0.add_hint(
53            ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::Hint::CommitmentHint(
54                hint.0,
55            ),
56        );
57    }
58
59    /// Length of HintsBag
60    pub fn len(&self) -> usize {
61        self.0.hints.len()
62    }
63
64    /// Get commitment
65    pub fn get(&self, index: usize) -> Result<CommitmentHint, JsValue> {
66        let commitment = self.0.commitments()[index].clone();
67        Ok(CommitmentHint(commitment))
68    }
69}
70
71impl From<ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::HintsBag> for HintsBag {
72    fn from(t: ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::HintsBag) -> Self {
73        HintsBag(t)
74    }
75}
76
77/// TransactionHintsBag
78#[wasm_bindgen]
79pub struct TransactionHintsBag(pub(crate) ergo_lib::wallet::multi_sig::TransactionHintsBag);
80
81#[wasm_bindgen]
82impl TransactionHintsBag {
83    /// Empty TransactionHintsBag
84    pub fn empty() -> TransactionHintsBag {
85        TransactionHintsBag(ergo_lib::wallet::multi_sig::TransactionHintsBag::empty())
86    }
87
88    /// Adding hints for input
89    pub fn add_hints_for_input(&mut self, index: usize, hints_bag: &HintsBag) {
90        self.0.add_hints_for_input(index, hints_bag.0.clone());
91    }
92
93    /// Outputting HintsBag corresponding for an input index
94    pub fn all_hints_for_input(&self, index: usize) -> HintsBag {
95        HintsBag::from(self.0.all_hints_for_input(index))
96    }
97
98    /// Return JSON object (node format)
99    pub fn to_json(&self) -> Result<JsValue, JsValue> {
100        <JsValue as JsValueSerdeExt>::from_serde(&self.0).map_err(to_js)
101    }
102
103    /// Parse from JSON object (node format)
104    pub fn from_json(json: &str) -> Result<TransactionHintsBag, JsValue> {
105        serde_json::from_str(json).map(Self).map_err(to_js)
106    }
107}
108
109impl From<ergo_lib::wallet::multi_sig::TransactionHintsBag> for TransactionHintsBag {
110    fn from(t: ergo_lib::wallet::multi_sig::TransactionHintsBag) -> Self {
111        TransactionHintsBag(t)
112    }
113}
114
115/// Extracting hints form singed(invalid) Transaction
116#[wasm_bindgen]
117pub fn extract_hints(
118    signed_transaction: Transaction,
119    state_context: &ErgoStateContext,
120    boxes_to_spend: &ErgoBoxes,
121    data_boxes: &ErgoBoxes,
122    real_propositions: Propositions,
123    simulated_propositions: Propositions,
124) -> Result<TransactionHintsBag, JsValue> {
125    let boxes_to_spend = boxes_to_spend.clone().into();
126
127    let data_boxes = data_boxes.clone().into();
128    let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
129        signed_transaction.0,
130        boxes_to_spend,
131        data_boxes,
132    )
133    .map_err(to_js)?;
134    Ok(TransactionHintsBag::from(
135        ergo_lib::wallet::multi_sig::extract_hints(
136            &tx_context,
137            &state_context.0.clone(),
138            real_propositions.0,
139            simulated_propositions.0,
140        )
141        .map_err(to_js)?,
142    ))
143}
144
145/// Transaction id
146#[wasm_bindgen]
147#[derive(PartialEq, Eq, Debug, Clone, From, Into)]
148pub struct TxId(pub(crate) chain::transaction::TxId);
149
150#[wasm_bindgen]
151impl TxId {
152    /// Zero (empty) transaction id (to use as dummy value in tests)
153    pub fn zero() -> TxId {
154        chain::transaction::TxId::zero().into()
155    }
156
157    /// get the tx id as bytes
158    pub fn to_str(&self) -> String {
159        let base16_bytes = Base16EncodedBytes::new(self.0 .0 .0.as_ref());
160        base16_bytes.into()
161    }
162
163    /// convert a hex string into a TxId
164    #[allow(clippy::should_implement_trait)]
165    pub fn from_str(s: &str) -> Result<TxId, JsValue> {
166        let bytes = Base16DecodedBytes::try_from(s.to_string()).map_err(to_js)?;
167
168        bytes
169            .try_into()
170            .map(|digest| chain::transaction::TxId(digest).into())
171            .map_err(|_e| {
172                JsValue::from_str(&format!(
173                    "Expected a Vec of length {} but it was {}",
174                    Digest32::SIZE,
175                    s.len()
176                ))
177            })
178    }
179}
180
181/**
182 * ErgoTransaction is an atomic state transition operation. It destroys Boxes from the state
183 * and creates new ones. If transaction is spending boxes protected by some non-trivial scripts,
184 * its inputs should also contain proof of spending correctness - context extension (user-defined
185 * key-value map) and data inputs (links to existing boxes in the state) that may be used during
186 * script reduction to crypto, signatures that satisfies the remaining cryptographic protection
187 * of the script.
188 * Transactions are not encrypted, so it is possible to browse and view every transaction ever
189 * collected into a block.
190 */
191#[wasm_bindgen]
192pub struct Transaction(chain::transaction::Transaction);
193
194#[wasm_bindgen]
195impl Transaction {
196    /// Create new transaction
197    #[wasm_bindgen(constructor)]
198    pub fn new(
199        inputs: &Inputs,
200        data_inputs: &DataInputs,
201        outputs: &ErgoBoxCandidates,
202    ) -> Result<Transaction, JsValue> {
203        let inputs: Vec<chain::transaction::Input> = inputs.into();
204        let data_inputs: Vec<chain::transaction::DataInput> = data_inputs.into();
205        let outputs: Vec<ergo_lib::ergotree_ir::chain::ergo_box::ErgoBoxCandidate> =
206            outputs.clone().into();
207        Ok(Transaction(
208            chain::transaction::Transaction::new(
209                TxIoVec::try_from(inputs).map_err(to_js)?,
210                TxIoVec::try_from(data_inputs).map_err(to_js)?.into(),
211                TxIoVec::try_from(outputs).map_err(to_js)?,
212            )
213            .map_err(to_js)?,
214        ))
215    }
216
217    /// Create Transaction from UnsignedTransaction and an array of proofs in the same order as
218    /// UnsignedTransaction.inputs with empty proof indicated with empty byte array
219    pub fn from_unsigned_tx(
220        unsigned_tx: UnsignedTransaction,
221        proofs: Vec<Uint8Array>,
222    ) -> Result<Transaction, JsValue> {
223        chain::transaction::Transaction::from_unsigned_tx(
224            unsigned_tx.0,
225            proofs
226                .into_iter()
227                .map(|bytes| bytes.to_vec().into())
228                .collect(),
229        )
230        .map_err(|e| JsValue::from_str(&format!("{}", e)))
231        .map(Into::into)
232    }
233
234    /// Get id for transaction
235    pub fn id(&self) -> TxId {
236        self.0.id().into()
237    }
238
239    /// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
240    pub fn to_json(&self) -> Result<String, JsValue> {
241        serde_json::to_string_pretty(&self.0.clone())
242            .map_err(|e| JsValue::from_str(&format!("{}", e)))
243    }
244
245    /// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
246    /// (similar to [`Self::to_json`], but as JS object with box value and token amount encoding as strings)
247    pub fn to_js_eip12(&self) -> Result<JsValue, JsValue> {
248        let tx_dapp: TransactionJsonEip12 = self.0.clone().into();
249        <JsValue as JsValueSerdeExt>::from_serde(&tx_dapp)
250            .map_err(|e| JsValue::from_str(&format!("{}", e)))
251    }
252
253    /// parse from JSON
254    /// supports Ergo Node/Explorer API and box values and token amount encoded as strings
255    pub fn from_json(json: &str) -> Result<Transaction, JsValue> {
256        serde_json::from_str(json).map(Self).map_err(to_js)
257    }
258
259    /// Inputs for transaction
260    pub fn inputs(&self) -> Inputs {
261        self.0.inputs.as_vec().clone().into()
262    }
263
264    /// Data inputs for transaction
265    pub fn data_inputs(&self) -> DataInputs {
266        self.0
267            .data_inputs
268            .clone()
269            .map(|di| di.as_vec().clone())
270            .unwrap_or_default()
271            .into()
272    }
273
274    /// Output candidates for transaction
275    pub fn output_candidates(&self) -> ErgoBoxCandidates {
276        self.0.output_candidates.as_vec().clone().into()
277    }
278
279    /// Returns ErgoBox's created from ErgoBoxCandidate's with tx id and indices
280    pub fn outputs(&self) -> ErgoBoxes {
281        self.0.outputs.clone().to_vec().into()
282    }
283
284    /// Returns serialized bytes or fails with error if cannot be serialized
285    pub fn sigma_serialize_bytes(&self) -> Result<Vec<u8>, JsValue> {
286        self.0.sigma_serialize_bytes().map_err(to_js)
287    }
288
289    /// Parses Transaction or fails with error
290    pub fn sigma_parse_bytes(data: Vec<u8>) -> Result<Transaction, JsValue> {
291        ergo_lib::chain::transaction::Transaction::sigma_parse_bytes(&data)
292            .map(Transaction)
293            .map_err(to_js)
294    }
295
296    /// Check the signature of the transaction's input corresponding
297    /// to the given input box, guarded by P2PK script
298    pub fn verify_p2pk_input(&self, input_box: ErgoBox) -> Result<bool, JsValue> {
299        self.0.verify_p2pk_input(input_box.into()).map_err(to_js)
300    }
301}
302
303impl From<chain::transaction::Transaction> for Transaction {
304    fn from(t: chain::transaction::Transaction) -> Self {
305        Transaction(t)
306    }
307}
308
309/// Unsigned (inputs without proofs) transaction
310#[wasm_bindgen]
311#[derive(PartialEq, Eq, Debug, Clone, From, Into)]
312pub struct UnsignedTransaction(pub(crate) chain::transaction::unsigned::UnsignedTransaction);
313
314#[wasm_bindgen]
315impl UnsignedTransaction {
316    /// Create a new unsigned transaction
317    #[wasm_bindgen(constructor)]
318    pub fn new(
319        inputs: &UnsignedInputs,
320        data_inputs: &DataInputs,
321        output_candidates: &ErgoBoxCandidates,
322    ) -> Result<UnsignedTransaction, JsValue> {
323        let opt_data_input = if data_inputs.len() > 0 {
324            Some(TxIoVec::from_vec(data_inputs.into()).map_err(to_js)?)
325        } else {
326            None
327        };
328
329        Ok(chain::transaction::unsigned::UnsignedTransaction::new(
330            TxIoVec::from_vec(inputs.into()).map_err(to_js)?,
331            opt_data_input,
332            TxIoVec::from_vec(output_candidates.into()).map_err(to_js)?,
333        )
334        .map_err(to_js)?
335        .into())
336    }
337
338    /// Consumes the calling UnsignedTransaction and returns a new UnsignedTransaction containing
339    /// the ContextExtension in the provided input box id or returns an error if the input box cannot be found.
340    /// After the call the calling UnsignedTransaction will be null.
341    pub fn with_input_context_ext(
342        mut self,
343        input_id: &BoxId,
344        ext: &ContextExtension,
345    ) -> Result<UnsignedTransaction, JsValue> {
346        let input = self
347            .0
348            .inputs
349            .iter_mut()
350            .find(|input| input.box_id == input_id.clone().into())
351            .ok_or_else(|| JsValue::from_str("Box input id not found"))?;
352        input.extension = ext.clone().into();
353
354        Ok(self.clone())
355    }
356
357    /// Get id for transaction
358    pub fn id(&self) -> TxId {
359        self.0.id().into()
360    }
361
362    /// Inputs for transaction
363    pub fn inputs(&self) -> UnsignedInputs {
364        self.0.inputs.as_vec().clone().into()
365    }
366
367    /// Data inputs for transaction
368    pub fn data_inputs(&self) -> DataInputs {
369        self.0
370            .clone()
371            .data_inputs
372            .map(|di| di.as_vec().clone())
373            .unwrap_or_default()
374            .into()
375    }
376
377    /// Output candidates for transaction
378    pub fn output_candidates(&self) -> ErgoBoxCandidates {
379        self.0.output_candidates.as_vec().clone().into()
380    }
381
382    /// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
383    pub fn to_json(&self) -> Result<String, JsValue> {
384        serde_json::to_string_pretty(&self.0.clone())
385            .map_err(|e| JsValue::from_str(&format!("{}", e)))
386    }
387
388    /// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
389    /// (similar to [`Self::to_json`], but as JS object with box value and token amount encoding as strings)
390    pub fn to_js_eip12(&self) -> Result<JsValue, JsValue> {
391        let tx_dapp: UnsignedTransactionJsonEip12 = self.0.clone().into();
392        <JsValue as JsValueSerdeExt>::from_serde(&tx_dapp)
393            .map_err(|e| JsValue::from_str(&format!("{}", e)))
394    }
395
396    /// parse from JSON
397    /// supports Ergo Node/Explorer API and box values and token amount encoded as strings
398    pub fn from_json(json: &str) -> Result<UnsignedTransaction, JsValue> {
399        serde_json::from_str(json).map(Self).map_err(to_js)
400    }
401
402    /// Returns distinct token id from output_candidates as array of byte arrays
403    pub fn distinct_token_ids(&self) -> Vec<Uint8Array> {
404        distinct_token_ids(self.0.output_candidates.clone())
405            .iter()
406            .map(|id| Uint8Array::from(id.as_ref()))
407            .collect()
408    }
409}
410
411/// Verify transaction input's proof
412#[wasm_bindgen]
413pub fn verify_tx_input_proof(
414    input_idx: usize,
415    state_context: &ErgoStateContext,
416    tx: &Transaction,
417    boxes_to_spend: &ErgoBoxes,
418    data_boxes: &ErgoBoxes,
419) -> Result<bool, JsValue> {
420    let boxes_to_spend = boxes_to_spend.clone().into();
421    let data_boxes = data_boxes.clone().into();
422    let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
423        tx.0.clone(),
424        boxes_to_spend,
425        data_boxes,
426    )
427    .map_err(to_js)?;
428    let state_context_inner = state_context.clone().into();
429    let mut context = make_context(&state_context_inner, &tx_context, input_idx).map_err(to_js)?;
430    Ok(ergo_lib::chain::transaction::verify_tx_input_proof(
431        &tx_context,
432        &mut context,
433        &state_context_inner,
434        input_idx,
435        &tx_context.spending_tx.bytes_to_sign().map_err(to_js)?,
436    )
437    .map_err(to_js)?
438    .result)
439}
440
441/// Verify transaction
442#[wasm_bindgen]
443pub fn validate_tx(
444    tx: &Transaction,
445    state_context: &ErgoStateContext,
446    boxes_to_spend: &ErgoBoxes,
447    data_boxes: &ErgoBoxes,
448) -> Result<(), JsValue> {
449    let boxes_to_spend = boxes_to_spend.clone().into();
450    let data_boxes = data_boxes.clone().into();
451    let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
452        tx.0.clone(),
453        boxes_to_spend,
454        data_boxes,
455    )
456    .map_err(to_js)?;
457    tx_context.validate(&state_context.0).map_err(to_js)
458}