Repository: ergoplatform/sigma-rust (language: rust)

Issue:
Data inputs support in TxBuilder (+ WASM)



Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index b597d64a9..13107fa74 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -50,7 +50,7 @@ matrix:
         - cargo clean
         - cargo build
       after_success:
-        - cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID
+        - cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID --exclude-files bindings/*
 
     - name: JS tests
       dist: bionic
diff --git a/bindings/ergo-lib-wasm/src/data_input.rs b/bindings/ergo-lib-wasm/src/data_input.rs
new file mode 100644
index 000000000..330793b8a
--- /dev/null
+++ b/bindings/ergo-lib-wasm/src/data_input.rs
@@ -0,0 +1,43 @@
+//! DataInput type
+
+use ergo_lib::chain;
+use wasm_bindgen::prelude::*;
+
+/// Inputs, that are used to enrich script context, but won't be spent by the transaction
+#[wasm_bindgen]
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct DataInput(chain::data_input::DataInput);
+
+/// DataInput collection
+#[wasm_bindgen]
+pub struct DataInputs(Vec<DataInput>);
+
+#[wasm_bindgen]
+impl DataInputs {
+    /// Create empty DataInputs
+    #[wasm_bindgen(constructor)]
+    pub fn new() -> Self {
+        DataInputs(vec![])
+    }
+
+    /// Returns the number of elements in the collection
+    pub fn len(&self) -> usize {
+        self.0.len()
+    }
+
+    /// Returns the element of the collection with a given index
+    pub fn get(&self, index: usize) -> DataInput {
+        self.0[index].clone()
+    }
+
+    /// Adds an elements to the collection
+    pub fn add(&mut self, elem: &DataInput) {
+        self.0.push(elem.clone());
+    }
+}
+
+impl From<&DataInputs> for Vec<chain::data_input::DataInput> {
+    fn from(v: &DataInputs) -> Self {
+        v.0.clone().iter().map(|i| i.0.clone()).collect()
+    }
+}
diff --git a/bindings/ergo-lib-wasm/src/ergo_box/box_builder.rs b/bindings/ergo-lib-wasm/src/ergo_box/box_builder.rs
index d93f0f71e..811bfde00 100644
--- a/bindings/ergo-lib-wasm/src/ergo_box/box_builder.rs
+++ b/bindings/ergo-lib-wasm/src/ergo_box/box_builder.rs
@@ -29,8 +29,8 @@ impl ErgoBoxCandidateBuilder {
     }
 
     /// Set minimal value (per byte of the serialized box size)
-    pub fn set_min_box_value_per_byte(&mut self, new_min_value_per_byte: u32) {
-        self.0.set_min_box_value_per_byte(new_min_value_per_byte);
+    pub fn set_min_box_value_per_byte(self, new_min_value_per_byte: u32) -> Self {
+        ErgoBoxCandidateBuilder(self.0.set_min_box_value_per_byte(new_min_value_per_byte))
     }
 
     /// Get minimal value (per byte of the serialized box size)
@@ -39,8 +39,8 @@ impl ErgoBoxCandidateBuilder {
     }
 
     /// Set new box value
-    pub fn set_value(&mut self, new_value: BoxValue) {
-        self.0.set_value(new_value.into());
+    pub fn set_value(self, new_value: BoxValue) -> Self {
+        ErgoBoxCandidateBuilder(self.0.set_value(new_value.into()))
     }
 
     /// Get box value
diff --git a/bindings/ergo-lib-wasm/src/lib.rs b/bindings/ergo-lib-wasm/src/lib.rs
index d25c9e9f7..0d30bd3f0 100644
--- a/bindings/ergo-lib-wasm/src/lib.rs
+++ b/bindings/ergo-lib-wasm/src/lib.rs
@@ -10,12 +10,16 @@
 #![deny(unused_imports)]
 #![deny(missing_docs)]
 #![allow(unused_variables)]
+// Clippy warnings
+#![allow(clippy::new_without_default)]
+#![allow(clippy::len_without_is_empty)]
 
 pub mod address;
 pub mod ast;
 pub mod box_coll;
 pub mod box_selector;
 pub mod contract;
+pub mod data_input;
 pub mod ergo_box;
 pub mod ergo_state_ctx;
 pub mod secret_key;
diff --git a/bindings/ergo-lib-wasm/src/tx_builder.rs b/bindings/ergo-lib-wasm/src/tx_builder.rs
index 492dcf850..d92d9d2ec 100644
--- a/bindings/ergo-lib-wasm/src/tx_builder.rs
+++ b/bindings/ergo-lib-wasm/src/tx_builder.rs
@@ -1,7 +1,8 @@
 //! Unsigned transaction builder
-use ergo_lib::{chain::ergo_box::ErgoBox, wallet};
+use ergo_lib::{chain, wallet};
 use wasm_bindgen::prelude::*;
 
+use crate::data_input::DataInputs;
 use crate::{
     address::Address,
     box_coll::{ErgoBoxCandidates, ErgoBoxes},
@@ -12,7 +13,7 @@ use crate::{
 
 /// Unsigned transaction builder
 #[wasm_bindgen]
-pub struct TxBuilder(wallet::tx_builder::TxBuilder<ErgoBox>);
+pub struct TxBuilder(wallet::tx_builder::TxBuilder<chain::ergo_box::ErgoBox>);
 
 #[wasm_bindgen]
 impl TxBuilder {
@@ -33,18 +34,21 @@ impl TxBuilder {
         fee_amount: &BoxValue,
         change_address: &Address,
         min_change_value: &BoxValue,
-    ) -> Result<TxBuilder, JsValue> {
-        ergo_lib::wallet::tx_builder::TxBuilder::new(
-            box_selector.inner::<ErgoBox>(),
+    ) -> TxBuilder {
+        TxBuilder(ergo_lib::wallet::tx_builder::TxBuilder::new(
+            box_selector.inner::<chain::ergo_box::ErgoBox>(),
             inputs.clone().into(),
             output_candidates.clone().into(),
             current_height,
             fee_amount.clone().into(),
             change_address.clone().into(),
             min_change_value.clone().into(),
-        )
-        .map_err(|e| JsValue::from_str(&format!("{}", e)))
-        .map(TxBuilder)
+        ))
+    }
+
+    /// Set transaction's data inputs
+    pub fn set_data_inputs(self, data_inputs: &DataInputs) -> TxBuilder {
+        TxBuilder(self.0.set_data_inputs(data_inputs.into()))
     }
 
     /// Build the unsigned transaction
diff --git a/ergo-lib/src/chain/data_input.rs b/ergo-lib/src/chain/data_input.rs
index fabec954a..752e4c857 100644
--- a/ergo-lib/src/chain/data_input.rs
+++ b/ergo-lib/src/chain/data_input.rs
@@ -15,7 +15,7 @@ use proptest_derive::Arbitrary;
 use serde::{Deserialize, Serialize};
 
 /// Inputs, that are used to enrich script context, but won't be spent by the transaction
-#[derive(PartialEq, Debug, Clone)]
+#[derive(PartialEq, Eq, Debug, Clone)]
 #[cfg_attr(test, derive(Arbitrary))]
 #[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
 pub struct DataInput {
diff --git a/ergo-lib/src/chain/ergo_box/box_builder.rs b/ergo-lib/src/chain/ergo_box/box_builder.rs
index afad0e2d1..ecd5dba5d 100644
--- a/ergo-lib/src/chain/ergo_box/box_builder.rs
+++ b/ergo-lib/src/chain/ergo_box/box_builder.rs
@@ -52,8 +52,9 @@ impl ErgoBoxCandidateBuilder {
     }
 
     /// Set minimal value (per byte of the serialized box size)
-    pub fn set_min_box_value_per_byte(&mut self, new_min_value_per_byte: u32) {
+    pub fn set_min_box_value_per_byte(mut self, new_min_value_per_byte: u32) -> Self {
         self.min_value_per_byte = new_min_value_per_byte;
+        self
     }
 
     /// Get minimal value (per byte of the serialized box size)
@@ -62,8 +63,9 @@ impl ErgoBoxCandidateBuilder {
     }
 
     /// Set new box value
-    pub fn set_value(&mut self, new_value: BoxValue) {
+    pub fn set_value(mut self, new_value: BoxValue) -> Self {
         self.value = new_value;
+        self
     }
 
     /// Get box value
@@ -134,10 +136,10 @@ mod tests {
 
     #[test]
     fn test_set_value() {
-        let mut builder =
-            ErgoBoxCandidateBuilder::new(BoxValue::SAFE_USER_MIN, force_any_val::<ErgoTree>(), 1);
         let new_value = BoxValue::SAFE_USER_MIN.checked_mul_u32(10).unwrap();
-        builder.set_value(new_value);
+        let builder =
+            ErgoBoxCandidateBuilder::new(BoxValue::SAFE_USER_MIN, force_any_val::<ErgoTree>(), 1)
+                .set_value(new_value);
         assert_eq!(builder.value(), &new_value);
         let b = builder.build().unwrap();
         assert_eq!(b.value, new_value);
diff --git a/ergo-lib/src/wallet/tx_builder.rs b/ergo-lib/src/wallet/tx_builder.rs
index 59017983a..ea879a5d6 100644
--- a/ergo-lib/src/wallet/tx_builder.rs
+++ b/ergo-lib/src/wallet/tx_builder.rs
@@ -7,6 +7,7 @@ use crate::chain::address::Address;
 use crate::chain::address::AddressEncoder;
 use crate::chain::address::NetworkPrefix;
 use crate::chain::contract::Contract;
+use crate::chain::data_input::DataInput;
 use crate::chain::ergo_box::box_builder::ErgoBoxCandidateBuilder;
 use crate::chain::ergo_box::box_builder::ErgoBoxCandidateBuilderError;
 use crate::chain::ergo_box::box_value;
@@ -26,6 +27,7 @@ use super::box_selector::{BoxSelection, BoxSelector, BoxSelectorError};
 pub struct TxBuilder<S: ErgoBoxAssets> {
     box_selector: Box<dyn BoxSelector<S>>,
     boxes_to_spend: Vec<S>,
+    data_inputs: Vec<DataInput>,
     output_candidates: Vec<ErgoBoxCandidate>,
     current_height: u32,
     fee_amount: BoxValue,
@@ -51,30 +53,37 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
         fee_amount: BoxValue,
         change_address: Address,
         min_change_value: BoxValue,
-    ) -> Result<TxBuilder<S>, TxBuilderError> {
-        if boxes_to_spend.is_empty() {
-            return Err(TxBuilderError::InvalidArgs(
-                "boxes_to_spend is empty".to_string(),
-            ));
-        }
-        if output_candidates.is_empty() {
-            return Err(TxBuilderError::InvalidArgs(
-                "output_candidates is empty".to_string(),
-            ));
-        }
-        Ok(TxBuilder {
+    ) -> TxBuilder<S> {
+        TxBuilder {
             box_selector,
             boxes_to_spend,
+            data_inputs: vec![],
             output_candidates,
             current_height,
             fee_amount,
             change_address,
             min_change_value,
-        })
+        }
+    }
+
+    /// Set transaction's data inputs
+    pub fn set_data_inputs(mut self, data_inputs: Vec<DataInput>) -> Self {
+        self.data_inputs = data_inputs;
+        self
     }
 
     /// Build the unsigned transaction
     pub fn build(self) -> Result<UnsignedTransaction, TxBuilderError> {
+        if self.boxes_to_spend.is_empty() {
+            return Err(TxBuilderError::InvalidArgs(
+                "boxes_to_spend is empty".to_string(),
+            ));
+        }
+        if self.output_candidates.is_empty() {
+            return Err(TxBuilderError::InvalidArgs(
+                "output_candidates is empty".to_string(),
+            ));
+        }
         let total_output_value: BoxValue =
             box_value::checked_sum(self.output_candidates.iter().map(|b| b.value))?
                 .checked_add(&self.fee_amount)?;
@@ -109,7 +118,7 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
                 .into_iter()
                 .map(UnsignedInput::from)
                 .collect(),
-            vec![],
+            self.data_inputs,
             output_candidates,
         ))
     }
@@ -195,7 +204,7 @@ mod tests {
             force_any_val::<Address>(),
             BoxValue::SAFE_USER_MIN,
         );
-        assert!(r.is_err());
+        assert!(r.build().is_err());
     }
 
     #[test]
@@ -210,7 +219,7 @@ mod tests {
             force_any_val::<Address>(),
             BoxValue::SAFE_USER_MIN,
         );
-        assert!(r.is_err());
+        assert!(r.build().is_err());
     }
 
     proptest! {
@@ -219,7 +228,8 @@ mod tests {
         fn test_build_tx(inputs in vec(any_with::<ErgoBox>((BoxValue::MIN_RAW * 5000 .. BoxValue::MIN_RAW * 10000).into()), 1..10),
                          outputs in vec(any_with::<ErgoBoxCandidate>((BoxValue::MIN_RAW * 1000 ..BoxValue::MIN_RAW * 2000).into()), 1..2),
                          change_address in any::<Address>(),
-                         miners_fee in any_with::<BoxValue>((BoxValue::MIN_RAW * 100..BoxValue::MIN_RAW * 200).into())) {
+                         miners_fee in any_with::<BoxValue>((BoxValue::MIN_RAW * 100..BoxValue::MIN_RAW * 200).into()),
+                         data_inputs in vec(any::<DataInput>(), 0..2)) {
             let min_change_value = BoxValue::SAFE_USER_MIN;
 
             let all_outputs = box_value::checked_sum(outputs.iter().map(|b| b.value)).unwrap()
@@ -237,7 +247,8 @@ mod tests {
                 miners_fee,
                 change_address.clone(),
                 min_change_value,
-            ).unwrap();
+            )
+            .set_data_inputs(data_inputs.clone());
             let tx = tx_builder.build().unwrap();
             prop_assert!(outputs.into_iter().all(|i| tx.output_candidates.iter().any(|o| *o == i)),
                          "tx.output_candidates is missing some outputs");
@@ -253,6 +264,7 @@ mod tests {
             prop_assert!(tx.output_candidates.iter().any(|b| {
                 b.value == miners_fee
             }), "box with miner's fee {:?} is not found in outputs: {:?}", miners_fee, tx.output_candidates);
+            prop_assert_eq!(tx.data_inputs, data_inputs, "unexpected data inputs");
         }
     }
 }


Test patch:
diff --git a/bindings/ergo-lib-wasm/tests/test_transaction.js b/bindings/ergo-lib-wasm/tests/test_transaction.js
index 9e0aabd4c..c7fe5599d 100644
--- a/bindings/ergo-lib-wasm/tests/test_transaction.js
+++ b/bindings/ergo-lib-wasm/tests/test_transaction.js
@@ -3,7 +3,7 @@ import { expect, assert } from 'chai';
 import {
   Address, Wallet, ErgoBox, ErgoBoxCandidateBuilder, Contract,
   ErgoBoxes, ErgoBoxCandidates,
-  ErgoStateContext, TxBuilder, BoxValue, UnsignedTransaction, BoxSelector, SecretKey, TxId,
+  ErgoStateContext, TxBuilder, BoxValue, UnsignedTransaction, BoxSelector, SecretKey, TxId, DataInputs
 } from '../pkg/ergo_lib_wasm';
 
 it('TxBuilder test', async () => {
@@ -26,7 +26,9 @@ it('TxBuilder test', async () => {
   const fee = BoxValue.from_u32(1000000);
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const min_change_value = BoxValue.SAFE_USER_MIN();
-  const tx_builder = TxBuilder.new(BoxSelector.SelectAll, unspent_boxes, tx_outputs, 0, fee, change_address, min_change_value);
+  const data_inputs = new DataInputs();
+  const tx_builder = TxBuilder.new(BoxSelector.SelectAll, unspent_boxes, tx_outputs, 0, fee, change_address, min_change_value)
+                              .set_data_inputs(data_inputs);
   const tx = tx_builder.build();
   assert(tx != null);
 });
diff --git a/bindings/ergo-lib-wasm/tests/transaction.rs b/bindings/ergo-lib-wasm/tests/transaction.rs
index cd25c9bde..ff7b72005 100644
--- a/bindings/ergo-lib-wasm/tests/transaction.rs
+++ b/bindings/ergo-lib-wasm/tests/transaction.rs
@@ -60,8 +60,7 @@ fn test_sign_transaction() {
         &fee,
         &change_address,
         &min_change_value,
-    )
-    .unwrap();
+    );
     let tx = tx_builder.build().unwrap();
     let wallet = Wallet::from_secret(&sk);
     let dummy_ctx = ErgoStateContext::dummy();
@@ -117,7 +116,6 @@ fn test_tx_builder() {
         &fee,
         &change_address,
         &min_change_value,
-    )
-    .unwrap();
+    );
     let _tx = tx_builder.build().unwrap();
 }