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

Issue:
Introduce ErgoBoxCandidate builder with min box value per byte parameter + optional box members (tokens, regs, etc.)
Min box value per byte should be a constructor parameters. Plan for new methods for optional box parameters (tokens, token minting, registers, etc.) to be added in the future.


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-wallet-lib-wasm/src/contract.rs b/bindings/ergo-wallet-lib-wasm/src/contract.rs
index c0721fc07..c86abc9ca 100644
--- a/bindings/ergo-wallet-lib-wasm/src/contract.rs
+++ b/bindings/ergo-wallet-lib-wasm/src/contract.rs
@@ -19,8 +19,8 @@ impl Contract {
     }
 }
 
-impl Into<chain::contract::Contract> for Contract {
-    fn into(self) -> chain::contract::Contract {
-        self.0
+impl From<Contract> for chain::contract::Contract {
+    fn from(c: Contract) -> Self {
+        c.0
     }
 }
diff --git a/bindings/ergo-wallet-lib-wasm/src/ergo_box.rs b/bindings/ergo-wallet-lib-wasm/src/ergo_box.rs
index 29e9134b9..1574f8c52 100644
--- a/bindings/ergo-wallet-lib-wasm/src/ergo_box.rs
+++ b/bindings/ergo-wallet-lib-wasm/src/ergo_box.rs
@@ -24,40 +24,15 @@ use wasm_bindgen::prelude::*;
 
 use crate::{contract::Contract, transaction::TxId};
 
+pub mod box_builder;
+
 /// ErgoBox candidate not yet included in any transaction on the chain
 #[wasm_bindgen]
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct ErgoBoxCandidate(chain::ergo_box::ErgoBoxCandidate);
 
 #[wasm_bindgen]
-impl ErgoBoxCandidate {
-    /// make a new box with:
-    /// `value` - amount of money associated with the box
-    /// `contract` - guarding contract([`Contract`]), which should be evaluated to true in order
-    /// to open(spend) this box
-    /// `creation_height` - height when a transaction containing the box is created.
-    /// It should not exceed height of the block, containing the transaction with this box.
-    #[wasm_bindgen(constructor)]
-    pub fn new(
-        value: &BoxValue,
-        creation_height: u32,
-        contract: &Contract,
-    ) -> Result<ErgoBoxCandidate, JsValue> {
-        let chain_contract: chain::contract::Contract = contract.clone().into();
-        chain::ergo_box::ErgoBoxCandidate::new(
-            value.0,
-            chain_contract.get_ergo_tree(),
-            creation_height,
-        )
-        .map_err(|e| JsValue::from_str(&format!("{}", e)))
-        .map(ErgoBoxCandidate)
-    }
-
-    // JSON representation
-    // pub fn to_json(&self) -> Result<JsValue, JsValue> {
-    //     JsValue::from_serde(&self.0).map_err(|e| JsValue::from_str(&format!("{}", e)))
-    // }
-}
+impl ErgoBoxCandidate {}
 
 impl Into<chain::ergo_box::ErgoBoxCandidate> for ErgoBoxCandidate {
     fn into(self) -> chain::ergo_box::ErgoBoxCandidate {
@@ -120,10 +95,11 @@ pub struct BoxValue(chain::ergo_box::box_value::BoxValue);
 
 #[wasm_bindgen]
 impl BoxValue {
-    /// Minimal value, calculated from smallest possible box size and original value per byte requirement
+    /// Recommended (safe) minimal box value to use in case box size estimation is unavailable.
+    /// Allows box size upto 2777 bytes with current min box value per byte of 360 nanoERGs
     #[allow(non_snake_case)]
-    pub fn MIN() -> BoxValue {
-        BoxValue(chain::ergo_box::box_value::BoxValue::MIN)
+    pub fn SAFE_USER_MIN() -> BoxValue {
+        BoxValue(chain::ergo_box::box_value::BoxValue::SAFE_USER_MIN)
     }
 
     /// Create from u32 with bounds check
@@ -135,8 +111,14 @@ impl BoxValue {
     }
 }
 
-impl Into<chain::ergo_box::box_value::BoxValue> for BoxValue {
-    fn into(self) -> chain::ergo_box::box_value::BoxValue {
-        self.0
+impl From<BoxValue> for chain::ergo_box::box_value::BoxValue {
+    fn from(v: BoxValue) -> Self {
+        v.0
+    }
+}
+
+impl From<chain::ergo_box::box_value::BoxValue> for BoxValue {
+    fn from(v: chain::ergo_box::box_value::BoxValue) -> Self {
+        BoxValue(v)
     }
 }
diff --git a/bindings/ergo-wallet-lib-wasm/src/ergo_box/box_builder.rs b/bindings/ergo-wallet-lib-wasm/src/ergo_box/box_builder.rs
new file mode 100644
index 000000000..7932c2634
--- /dev/null
+++ b/bindings/ergo-wallet-lib-wasm/src/ergo_box/box_builder.rs
@@ -0,0 +1,68 @@
+//! ErgoBoxCandidate builder
+use sigma_tree::chain;
+use wasm_bindgen::prelude::*;
+
+use crate::contract::Contract;
+
+use super::BoxValue;
+use super::ErgoBoxCandidate;
+
+/// ErgoBoxCandidate builder
+#[wasm_bindgen]
+pub struct ErgoBoxCandidateBuilder(chain::ergo_box::box_builder::ErgoBoxCandidateBuilder);
+
+#[wasm_bindgen]
+impl ErgoBoxCandidateBuilder {
+    /// Create builder with required box parameters:
+    /// `value` - amount of money associated with the box
+    /// `contract` - guarding contract([`Contract`]), which should be evaluated to true in order
+    /// to open(spend) this box
+    /// `creation_height` - height when a transaction containing the box is created.
+    /// It should not exceed height of the block, containing the transaction with this box.
+    #[wasm_bindgen(constructor)]
+    pub fn new(value: &BoxValue, contract: &Contract, creation_height: u32) -> Self {
+        ErgoBoxCandidateBuilder(chain::ergo_box::box_builder::ErgoBoxCandidateBuilder::new(
+            value.clone().into(),
+            chain::contract::Contract::from(contract.clone()).get_ergo_tree(),
+            creation_height,
+        ))
+    }
+
+    /// 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);
+    }
+
+    /// Get minimal value (per byte of the serialized box size)
+    pub fn min_box_value_per_byte(&self) -> u32 {
+        self.0.min_box_value_per_byte()
+    }
+
+    /// Set new box value
+    pub fn set_value(&mut self, new_value: BoxValue) {
+        self.0.set_value(new_value.into());
+    }
+
+    /// Get box value
+    pub fn value(&self) -> BoxValue {
+        (*self.0.value()).into()
+    }
+
+    /// Calculate serialized box size(in bytes)
+    pub fn calc_box_size_bytes(&self) -> usize {
+        self.0.calc_box_size_bytes()
+    }
+
+    /// Calculate minimal box value for the current box serialized size(in bytes)
+    pub fn calc_min_box_value(&self) -> BoxValue {
+        self.0.calc_min_box_value().into()
+    }
+
+    /// Build the box candidate
+    pub fn build(self) -> Result<ErgoBoxCandidate, JsValue> {
+        self.0
+            .build()
+            .map_err(|e| JsValue::from_str(&format!("{}", e)))
+            .map(ErgoBoxCandidate)
+    }
+}
diff --git a/bindings/ergo-wallet-lib-wasm/src/tx_builder.rs b/bindings/ergo-wallet-lib-wasm/src/tx_builder.rs
index 89d82de0b..0ec8a11fd 100644
--- a/bindings/ergo-wallet-lib-wasm/src/tx_builder.rs
+++ b/bindings/ergo-wallet-lib-wasm/src/tx_builder.rs
@@ -48,7 +48,7 @@ impl TxBuilder {
     }
 
     /// Build the unsigned transaction
-    pub fn build(&self) -> Result<UnsignedTransaction, JsValue> {
+    pub fn build(self) -> Result<UnsignedTransaction, JsValue> {
         self.0
             .build()
             .map_err(|e| JsValue::from_str(&format!("{}", e)))
diff --git a/sigma-tree/src/chain/ergo_box.rs b/sigma-tree/src/chain/ergo_box.rs
index 3511c48c4..0e427f69c 100644
--- a/sigma-tree/src/chain/ergo_box.rs
+++ b/sigma-tree/src/chain/ergo_box.rs
@@ -1,5 +1,6 @@
 //! Ergo box
 
+pub mod box_builder;
 pub mod box_id;
 pub mod box_value;
 pub mod register;
@@ -28,8 +29,8 @@ use register::NonMandatoryRegisters;
 use serde::{Deserialize, Serialize};
 #[cfg(feature = "with-serde")]
 use std::convert::TryFrom;
-use std::convert::TryInto;
 use std::io;
+#[cfg(feature = "with-serde")]
 use thiserror::Error;
 
 /// Box (aka coin, or an unspent output) is a basic concept of a UTXO-based cryptocurrency.
@@ -286,48 +287,7 @@ pub struct ErgoBoxCandidate {
     pub creation_height: u32,
 }
 
-/// ErgoBoxCandidate errors
-#[derive(Error, PartialEq, Eq, Clone, Debug)]
-pub enum ErgoBoxCandidateError {
-    /// Box value is too low
-    #[error("Box value is too low, minimum value for box size of {box_size_bytes} bytes is: {min_box_value:?} nanoERGs")]
-    BoxValueTooLow {
-        /// minimum box value for that box size
-        min_box_value: BoxValue,
-        /// box size in bytes
-        box_size_bytes: usize,
-    },
-}
-
 impl ErgoBoxCandidate {
-    /// create box with value guarded by ErgoTree
-    pub fn new(
-        value: BoxValue,
-        ergo_tree: ErgoTree,
-        creation_height: u32,
-    ) -> Result<ErgoBoxCandidate, ErgoBoxCandidateError> {
-        let b = ErgoBoxCandidate {
-            value,
-            ergo_tree,
-            tokens: vec![],
-            additional_registers: NonMandatoryRegisters::empty(),
-            creation_height,
-        };
-        let box_size_bytes = b.sigma_serialise_bytes().len();
-        // TODO: extract min value per byte as parameter. ErgoBoxCandidateBuilder?
-        let min_box_value: BoxValue = (box_size_bytes as i64 * BoxValue::MIN_VALUE_PER_BOX_BYTE)
-            .try_into()
-            .unwrap();
-        if value >= min_box_value {
-            Ok(b)
-        } else {
-            Err(ErgoBoxCandidateError::BoxValueTooLow {
-                min_box_value,
-                box_size_bytes,
-            })
-        }
-    }
-
     /// Box serialization with token ids optionally saved in transaction
     /// (in this case only token index is saved)
     pub fn serialize_body_with_indexed_digests<W: SigmaByteWrite>(
diff --git a/sigma-tree/src/chain/ergo_box/box_builder.rs b/sigma-tree/src/chain/ergo_box/box_builder.rs
new file mode 100644
index 000000000..afad0e2d1
--- /dev/null
+++ b/sigma-tree/src/chain/ergo_box/box_builder.rs
@@ -0,0 +1,177 @@
+//! ErgoBoxCandidate builder
+
+use std::convert::TryFrom;
+use std::convert::TryInto;
+
+use crate::chain::token::TokenAmount;
+use crate::serialization::SigmaSerializable;
+use crate::ErgoTree;
+
+use super::box_value::BoxValue;
+use super::register::NonMandatoryRegisters;
+use super::ErgoBoxCandidate;
+use thiserror::Error;
+
+/// ErgoBoxCandidate builder errors
+#[derive(Error, PartialEq, Eq, Clone, Debug)]
+pub enum ErgoBoxCandidateBuilderError {
+    /// Box value is too low
+    #[error("Box value is too low, minimum value for box size of {box_size_bytes} bytes is: {min_box_value:?} nanoERGs")]
+    BoxValueTooLow {
+        /// minimum box value for that box size
+        min_box_value: BoxValue,
+        /// box size in bytes
+        box_size_bytes: usize,
+    },
+}
+
+/// ErgoBoxCandidate builder
+pub struct ErgoBoxCandidateBuilder {
+    min_value_per_byte: u32,
+    value: BoxValue,
+    ergo_tree: ErgoTree,
+    tokens: Vec<TokenAmount>,
+    additional_registers: NonMandatoryRegisters,
+    creation_height: u32,
+}
+
+impl ErgoBoxCandidateBuilder {
+    /// Create builder with required box parameters:
+    /// `value` - box value in nanoErgs,
+    /// `ergo_tree` - ErgoTree to guard this box,
+    /// `current_height` - chain height that will be set in the built box
+    pub fn new(value: BoxValue, ergo_tree: ErgoTree, creation_height: u32) -> Self {
+        ErgoBoxCandidateBuilder {
+            min_value_per_byte: BoxValue::MIN_VALUE_PER_BOX_BYTE,
+            value,
+            ergo_tree,
+            tokens: vec![],
+            additional_registers: NonMandatoryRegisters::empty(),
+            creation_height,
+        }
+    }
+
+    /// 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.min_value_per_byte = new_min_value_per_byte;
+    }
+
+    /// Get minimal value (per byte of the serialized box size)
+    pub fn min_box_value_per_byte(&self) -> u32 {
+        self.min_value_per_byte
+    }
+
+    /// Set new box value
+    pub fn set_value(&mut self, new_value: BoxValue) {
+        self.value = new_value;
+    }
+
+    /// Get box value
+    pub fn value(&self) -> &BoxValue {
+        &self.value
+    }
+
+    /// Calculate serialized box size(in bytes)
+    pub fn calc_box_size_bytes(&self) -> usize {
+        let b = ErgoBoxCandidate {
+            value: self.value,
+            ergo_tree: self.ergo_tree.clone(),
+            tokens: self.tokens.clone(),
+            additional_registers: self.additional_registers.clone(),
+            creation_height: self.creation_height,
+        };
+        b.sigma_serialise_bytes().len()
+    }
+
+    /// Calculate minimal box value for the current box serialized size(in bytes)
+    pub fn calc_min_box_value(&self) -> BoxValue {
+        let box_size_bytes = self.calc_box_size_bytes();
+        BoxValue::try_from(box_size_bytes as i64 * BoxValue::MIN_VALUE_PER_BOX_BYTE as i64).unwrap()
+    }
+
+    /// Build the box candidate
+    pub fn build(self) -> Result<ErgoBoxCandidate, ErgoBoxCandidateBuilderError> {
+        let b = ErgoBoxCandidate {
+            value: self.value,
+            ergo_tree: self.ergo_tree,
+            tokens: self.tokens,
+            additional_registers: self.additional_registers,
+            creation_height: self.creation_height,
+        };
+        let box_size_bytes = b.sigma_serialise_bytes().len();
+        let min_box_value: BoxValue = (box_size_bytes as i64 * self.min_value_per_byte as i64)
+            .try_into()
+            .unwrap();
+        if self.value >= min_box_value {
+            Ok(b)
+        } else {
+            Err(ErgoBoxCandidateBuilderError::BoxValueTooLow {
+                min_box_value,
+                box_size_bytes,
+            })
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use crate::test_util::force_any_val;
+
+    use super::*;
+
+    #[test]
+    fn test_build() {
+        let tree = force_any_val::<ErgoTree>();
+        let value = BoxValue::SAFE_USER_MIN;
+        let builder = ErgoBoxCandidateBuilder::new(value, tree.clone(), 1);
+        assert_eq!(builder.value(), &value);
+        let b = builder.build().unwrap();
+        assert_eq!(b.value, value);
+        assert_eq!(b.ergo_tree, tree);
+        assert_eq!(b.creation_height, 1);
+    }
+
+    #[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);
+        assert_eq!(builder.value(), &new_value);
+        let b = builder.build().unwrap();
+        assert_eq!(b.value, new_value);
+    }
+
+    #[test]
+    fn test_default_min_box_value() {
+        let builder =
+            ErgoBoxCandidateBuilder::new(BoxValue::SAFE_USER_MIN, force_any_val::<ErgoTree>(), 1);
+        assert_eq!(
+            builder.min_box_value_per_byte(),
+            BoxValue::MIN_VALUE_PER_BOX_BYTE
+        );
+    }
+
+    #[test]
+    fn test_box_size_estimation() {
+        let builder =
+            ErgoBoxCandidateBuilder::new(BoxValue::SAFE_USER_MIN, force_any_val::<ErgoTree>(), 1);
+        let estimated_box_size = builder.calc_box_size_bytes();
+        let b = builder.build().unwrap();
+        assert_eq!(b.sigma_serialise_bytes().len(), estimated_box_size);
+    }
+
+    #[test]
+    fn test_calc_min_box_value() {
+        let builder =
+            ErgoBoxCandidateBuilder::new(BoxValue::SAFE_USER_MIN, force_any_val::<ErgoTree>(), 1);
+        assert!(builder.calc_min_box_value() > BoxValue::MIN);
+    }
+
+    #[test]
+    fn test_build_fail_box_value_too_low() {
+        let builder = ErgoBoxCandidateBuilder::new(BoxValue::MIN, force_any_val::<ErgoTree>(), 1);
+        assert!(builder.build().is_err());
+    }
+}
diff --git a/sigma-tree/src/chain/ergo_box/box_value.rs b/sigma-tree/src/chain/ergo_box/box_value.rs
index e9a70b381..563e24fbf 100644
--- a/sigma-tree/src/chain/ergo_box/box_value.rs
+++ b/sigma-tree/src/chain/ergo_box/box_value.rs
@@ -16,18 +16,23 @@ pub struct BoxValue(i64);
 
 impl BoxValue {
     /// Minimal box value per byte of the serialized box that was set on on launch
-    pub const MIN_VALUE_PER_BOX_BYTE: i64 = 360;
+    pub const MIN_VALUE_PER_BOX_BYTE: u32 = 360;
     /// Minimal theoretical box size (smallest tree, no tokens, no registers, etc.)
-    const MIN_BOX_SIZE_BYTES: i64 = 30;
+    const MIN_BOX_SIZE_BYTES: usize = 30;
 
-    /// Mininal allowed box value
-    pub const MIN_RAW: i64 = BoxValue::MIN_VALUE_PER_BOX_BYTE * BoxValue::MIN_BOX_SIZE_BYTES;
-    /// Maximal allowed box value
+    /// Absolute minimal value, calculated from smallest possible box size and original value per byte requirement
+    pub const MIN_RAW: i64 =
+        BoxValue::MIN_VALUE_PER_BOX_BYTE as i64 * BoxValue::MIN_BOX_SIZE_BYTES as i64;
+    /// Absolue maximal allowed box value
     pub const MAX_RAW: i64 = i64::MAX;
 
-    /// Minimal value, calculated from smallest possible box size and original value per byte requirement
+    /// Absolute minimal value, calculated from smallest possible box size and original value per byte requirement
     pub const MIN: BoxValue = BoxValue(BoxValue::MIN_RAW);
 
+    /// Recommended (safe) minimal box value to use in case box size estimation is unavailable.
+    /// Allows box size upto 2777 bytes with current min box value per byte of 360 nanoERGs
+    pub const SAFE_USER_MIN: BoxValue = BoxValue(1000000);
+
     /// create from u64 with bounds check
     pub fn new(v: u64) -> Result<BoxValue, BoxValueError> {
         BoxValue::try_from(v)
diff --git a/sigma-tree/src/lib.rs b/sigma-tree/src/lib.rs
index 8f3a54b17..29fd6abdb 100644
--- a/sigma-tree/src/lib.rs
+++ b/sigma-tree/src/lib.rs
@@ -25,4 +25,6 @@ pub mod sigma_protocol;
 pub mod wallet;
 
 pub use ergo_tree::*;
-// pub use serialization::serializable::*;
+
+#[cfg(test)]
+pub mod test_util;
diff --git a/sigma-tree/src/wallet/box_selector/simple.rs b/sigma-tree/src/wallet/box_selector/simple.rs
index a7690f1c9..8ba8a2c20 100644
--- a/sigma-tree/src/wallet/box_selector/simple.rs
+++ b/sigma-tree/src/wallet/box_selector/simple.rs
@@ -82,27 +82,26 @@ mod tests {
     fn test_empty_inputs() {
         let s = SimpleBoxSelector::new();
         let inputs: Vec<ErgoBox> = vec![];
-        let r = s.select(inputs, BoxValue::MIN, vec![].as_slice());
+        let r = s.select(inputs, BoxValue::SAFE_USER_MIN, vec![].as_slice());
         assert!(r.is_err());
     }
 
     proptest! {
         #[test]
-        fn test_select(inputs in vec(any_with::<ErgoBox>((BoxValue::MIN_RAW * 2 .. BoxValue::MIN_RAW * 10).into()), 1..10)) {
+        fn test_select(inputs in vec(any_with::<ErgoBox>((BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 1..10)) {
             let s = SimpleBoxSelector::new();
             let all_inputs_val = box_value::checked_sum(inputs.iter().map(|b| b.value)).unwrap();
 
-            let balance_too_much = all_inputs_val.checked_add(&BoxValue::MIN).unwrap();
+            let balance_too_much = all_inputs_val.checked_add(&BoxValue::SAFE_USER_MIN).unwrap();
             prop_assert!(s.select(inputs.clone(), balance_too_much, vec![].as_slice()).is_err());
 
             let balance_exact = all_inputs_val;
             let selection_exact = s.select(inputs.clone(), balance_exact, vec![].as_slice()).unwrap();
             prop_assert!(selection_exact.change_boxes.is_empty());
             prop_assert!(selection_exact.boxes == inputs);
-
-            let balance_less = all_inputs_val.checked_sub(&BoxValue::MIN).unwrap();
+            let balance_less = all_inputs_val.checked_sub(&BoxValue::SAFE_USER_MIN).unwrap();
             let selection_less = s.select(inputs.clone(), balance_less, vec![].as_slice()).unwrap();
-            let expected_change_box = ErgoBoxAssetsData {value: BoxValue::MIN, tokens: vec![]};
+            let expected_change_box = ErgoBoxAssetsData {value: BoxValue::SAFE_USER_MIN, tokens: vec![]};
             prop_assert!(selection_less.change_boxes == vec![expected_change_box]);
             prop_assert!(selection_less.boxes == inputs);
         }
diff --git a/sigma-tree/src/wallet/signing.rs b/sigma-tree/src/wallet/signing.rs
index ba6886c80..9e1ff6514 100644
--- a/sigma-tree/src/wallet/signing.rs
+++ b/sigma-tree/src/wallet/signing.rs
@@ -70,13 +70,12 @@ mod tests {
     use proptest::collection::vec;
     use proptest::prelude::*;
 
+    use crate::chain::ergo_box::box_builder::ErgoBoxCandidateBuilder;
     use crate::chain::ergo_box::box_value::BoxValue;
     use crate::{
         ast::{Constant, Expr},
         chain::{
-            ergo_box::{register::NonMandatoryRegisters, ErgoBoxCandidate},
-            input::UnsignedInput,
-            transaction::TxId,
+            ergo_box::register::NonMandatoryRegisters, input::UnsignedInput, transaction::TxId,
         },
         sigma_protocol::{
             prover::TestProver,
@@ -118,7 +117,7 @@ mod tests {
                     tpe: SType::SSigmaProp,
                     v: pk.into(),
                 })));
-                ErgoBox::new(BoxValue::MIN.checked_mul_u32(100).unwrap(),
+                ErgoBox::new(BoxValue::SAFE_USER_MIN,
                              tree,
                              vec![],
                              NonMandatoryRegisters::empty(),
@@ -134,7 +133,8 @@ mod tests {
                     tpe: SType::SSigmaProp,
                     v: secrets.get(0).unwrap().public_image().into(),
             })));
-            let candidate = ErgoBoxCandidate::new(BoxValue::MIN.checked_mul_u32(100).unwrap(), ergo_tree, 0).unwrap();
+            let candidate = ErgoBoxCandidateBuilder::new(BoxValue::SAFE_USER_MIN, ergo_tree, 0)
+                .build().unwrap();
             let output_candidates = vec![candidate];
             let tx = UnsignedTransaction::new(inputs, vec![], output_candidates);
 
diff --git a/sigma-tree/src/wallet/tx_builder.rs b/sigma-tree/src/wallet/tx_builder.rs
index 05bdb65d1..555ea9b6b 100644
--- a/sigma-tree/src/wallet/tx_builder.rs
+++ b/sigma-tree/src/wallet/tx_builder.rs
@@ -7,8 +7,9 @@ use crate::chain::address::Address;
 use crate::chain::address::AddressEncoder;
 use crate::chain::address::NetworkPrefix;
 use crate::chain::contract::Contract;
+use crate::chain::ergo_box::box_builder::ErgoBoxCandidateBuilder;
+use crate::chain::ergo_box::box_builder::ErgoBoxCandidateBuilderError;
 use crate::chain::ergo_box::box_value;
-use crate::chain::ergo_box::ErgoBoxCandidateError;
 use crate::chain::{
     ergo_box::ErgoBoxAssets,
     ergo_box::ErgoBoxId,
@@ -73,7 +74,7 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
     }
 
     /// Build the unsigned transaction
-    pub fn build(&self) -> Result<UnsignedTransaction, TxBuilderError> {
+    pub fn build(self) -> Result<UnsignedTransaction, TxBuilderError> {
         let total_output_value: BoxValue =
             box_value::checked_sum(self.output_candidates.iter().map(|b| b.value))?
                 .checked_add(&self.fee_amount)?;
@@ -86,16 +87,17 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
 
         let change_address_ergo_tree =
             Contract::pay_to_address(&self.change_address)?.get_ergo_tree();
-        let change_boxes: Result<Vec<ErgoBoxCandidate>, ErgoBoxCandidateError> = selection
+        let change_boxes: Result<Vec<ErgoBoxCandidate>, ErgoBoxCandidateBuilderError> = selection
             .change_boxes
             .iter()
             .filter(|b| b.value >= self.min_change_value)
             .map(|b| {
-                ErgoBoxCandidate::new(
+                ErgoBoxCandidateBuilder::new(
                     b.value,
                     change_address_ergo_tree.clone(),
                     self.current_height,
                 )
+                .build()
             })
             .collect();
         output_candidates.append(&mut change_boxes?);
@@ -118,13 +120,13 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
 pub fn new_miner_fee_box(
     fee_amount: BoxValue,
     creation_height: u32,
-) -> Result<ErgoBoxCandidate, ErgoBoxCandidateError> {
+) -> Result<ErgoBoxCandidate, ErgoBoxCandidateBuilderError> {
     let address_encoder = AddressEncoder::new(NetworkPrefix::Mainnet);
     let miner_fee_address = address_encoder
         .parse_address_from_str(MINERS_FEE_MAINNET_ADDRESS)
         .unwrap();
     let ergo_tree = miner_fee_address.script().unwrap();
-    ErgoBoxCandidate::new(fee_amount, ergo_tree, creation_height)
+    ErgoBoxCandidateBuilder::new(fee_amount, ergo_tree, creation_height).build()
 }
 
 /// Errors of TxBuilder
@@ -143,8 +145,8 @@ pub enum TxBuilderError {
     #[error("Invalid arguments: {0}")]
     InvalidArgs(String),
     /// ErgoBoxCandidate error
-    #[error("ErgoBoxCandidate error: {0}")]
-    ErgoBoxCandidateError(ErgoBoxCandidateError),
+    #[error("ErgoBoxCandidateBuilder error: {0}")]
+    ErgoBoxCandidateBuilderError(ErgoBoxCandidateBuilderError),
 }
 
 impl From<BoxSelectorError> for TxBuilderError {
@@ -165,29 +167,23 @@ impl From<SerializationError> for TxBuilderError {
     }
 }
 
-impl From<ErgoBoxCandidateError> for TxBuilderError {
-    fn from(e: ErgoBoxCandidateError) -> Self {
-        TxBuilderError::ErgoBoxCandidateError(e)
+impl From<ErgoBoxCandidateBuilderError> for TxBuilderError {
+    fn from(e: ErgoBoxCandidateBuilderError) -> Self {
+        TxBuilderError::ErgoBoxCandidateBuilderError(e)
     }
 }
 
 #[cfg(test)]
 mod tests {
 
-    use proptest::strategy::ValueTree;
-    use proptest::test_runner::TestRunner;
-    use proptest::{arbitrary::Arbitrary, collection::vec, prelude::*};
+    use proptest::{collection::vec, prelude::*};
 
     use crate::chain::ergo_box::ErgoBox;
+    use crate::test_util::force_any_val;
     use crate::wallet::box_selector::simple::SimpleBoxSelector;
 
     use super::*;
 
-    fn force_any_val<T: Arbitrary>() -> T {
-        let mut runner = TestRunner::default();
-        any::<T>().new_tree(&mut runner).unwrap().current()
-    }
-
     #[test]
     fn test_empty_inputs() {
         let inputs: Vec<ErgoBox> = vec![];
@@ -198,7 +194,7 @@ mod tests {
             1,
             force_any_val::<BoxValue>(),
             force_any_val::<Address>(),
-            BoxValue::MIN,
+            BoxValue::SAFE_USER_MIN,
         );
         assert!(r.is_err());
     }
@@ -213,7 +209,7 @@ mod tests {
             1,
             force_any_val::<BoxValue>(),
             force_any_val::<Address>(),
-            BoxValue::MIN,
+            BoxValue::SAFE_USER_MIN,
         );
         assert!(r.is_err());
     }
@@ -221,11 +217,11 @@ mod tests {
     proptest! {
 
         #[test]
-        fn test_build_tx(inputs in vec(any_with::<ErgoBox>((BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 1..10),
+        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 * 1000..BoxValue::MIN_RAW * 2000).into())) {
-            let min_change_value = BoxValue::MIN;
+                         miners_fee in any_with::<BoxValue>((BoxValue::MIN_RAW * 100..BoxValue::MIN_RAW * 200).into())) {
+            let min_change_value = BoxValue::SAFE_USER_MIN;
 
             let all_outputs = box_value::checked_sum(outputs.iter().map(|b| b.value)).unwrap()
                                                                              .checked_add(&miners_fee)


Test patch:
diff --git a/bindings/ergo-wallet-lib-wasm/tests/test_transaction.js b/bindings/ergo-wallet-lib-wasm/tests/test_transaction.js
index 126cfce01..5e1b699c0 100644
--- a/bindings/ergo-wallet-lib-wasm/tests/test_transaction.js
+++ b/bindings/ergo-wallet-lib-wasm/tests/test_transaction.js
@@ -1,7 +1,7 @@
 import { expect, assert } from 'chai';
 
 import {
-  Address, Wallet, ErgoBox, ErgoBoxCandidate, Contract,
+  Address, Wallet, ErgoBox, ErgoBoxCandidateBuilder, Contract,
   ErgoBoxes, ErgoBoxCandidates,
   ErgoStateContext, TxBuilder, BoxValue, UnsignedTransaction, BoxSelector, SecretKey, TxId,
 } from '../pkg/ergo_wallet_lib_wasm';
@@ -21,11 +21,11 @@ it('TxBuilder test', async () => {
     }
   ]);
   const contract = Contract.pay_to_address(recipient);
-  const outbox = new ErgoBoxCandidate(BoxValue.from_u32(10000000), 0, contract);
+  const outbox = new ErgoBoxCandidateBuilder(BoxValue.from_u32(10000000), contract, 0).build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
   const fee = BoxValue.from_u32(1000000);
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
-  const min_change_value = BoxValue.MIN();
+  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 tx = tx_builder.build();
   assert(tx != null);
@@ -41,11 +41,11 @@ it('sign transaction', async () => {
   const recipient = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const unspent_boxes = new ErgoBoxes(input_box);
   const contract = Contract.pay_to_address(recipient);
-  const outbox = new ErgoBoxCandidate(BoxValue.from_u32(10000000), 0, contract);
+  const outbox = new ErgoBoxCandidateBuilder(BoxValue.from_u32(10000000), contract, 0).build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
   const fee = BoxValue.from_u32(1000000);
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
-  const min_change_value = BoxValue.MIN();
+  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 tx = tx_builder.build();
   const tx_data_inputs = ErgoBoxes.from_boxes_json([]);
diff --git a/bindings/ergo-wallet-lib-wasm/tests/transaction.rs b/bindings/ergo-wallet-lib-wasm/tests/transaction.rs
index 1bf2d8835..25b9df02e 100644
--- a/bindings/ergo-wallet-lib-wasm/tests/transaction.rs
+++ b/bindings/ergo-wallet-lib-wasm/tests/transaction.rs
@@ -4,10 +4,11 @@ extern crate wasm_bindgen_test;
 
 use std::{convert::TryInto, rc::Rc};
 
+use ergo_wallet_lib_wasm::ergo_box::box_builder::ErgoBoxCandidateBuilder;
 use ergo_wallet_lib_wasm::{
     address::Address, box_coll::ErgoBoxCandidates, box_coll::ErgoBoxes, box_selector::BoxSelector,
-    contract::Contract, ergo_box::BoxValue, ergo_box::ErgoBoxCandidate,
-    ergo_state_ctx::ErgoStateContext, secret_key::SecretKey, tx_builder::TxBuilder, wallet::Wallet,
+    contract::Contract, ergo_box::BoxValue, ergo_state_ctx::ErgoStateContext,
+    secret_key::SecretKey, tx_builder::TxBuilder, wallet::Wallet,
 };
 use sigma_tree::{
     ast::Expr, chain::ergo_box::register::NonMandatoryRegisters, chain::ergo_box::ErgoBox,
@@ -42,14 +43,15 @@ fn test_sign_transaction() {
         Address::from_testnet_str("3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
             .expect("failed");
     let contract = Contract::pay_to_address(&recipient).expect("failed");
-    let outbox =
-        ErgoBoxCandidate::new(&BoxValue::from_u32(10000000).unwrap(), 0, &contract).unwrap();
+    let outbox = ErgoBoxCandidateBuilder::new(&BoxValue::from_u32(10000000).unwrap(), &contract, 0)
+        .build()
+        .unwrap();
     let tx_outputs = ErgoBoxCandidates::new(&outbox);
     let fee = BoxValue::from_u32(1000000).unwrap();
     let change_address =
         Address::from_testnet_str("3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
             .expect("failed");
-    let min_change_value = BoxValue::MIN();
+    let min_change_value = BoxValue::SAFE_USER_MIN();
     let tx_builder = TxBuilder::new(
         BoxSelector::Simple,
         &tx_inputs,
@@ -98,14 +100,15 @@ fn test_tx_builder() {
         Address::from_testnet_str("3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
             .expect("failed");
     let contract = Contract::pay_to_address(&recipient).expect("failed");
-    let outbox =
-        ErgoBoxCandidate::new(&BoxValue::from_u32(10000000).unwrap(), 0, &contract).unwrap();
+    let outbox = ErgoBoxCandidateBuilder::new(&BoxValue::from_u32(10000000).unwrap(), &contract, 0)
+        .build()
+        .unwrap();
     let tx_outputs = ErgoBoxCandidates::new(&outbox);
     let fee = BoxValue::from_u32(1000000).unwrap();
     let change_address =
         Address::from_testnet_str("3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
             .expect("failed");
-    let min_change_value = BoxValue::MIN();
+    let min_change_value = BoxValue::SAFE_USER_MIN();
     let tx_builder = TxBuilder::new(
         BoxSelector::Simple,
         &tx_inputs,
@@ -116,6 +119,5 @@ fn test_tx_builder() {
         &min_change_value,
     )
     .unwrap();
-    // assert!(tx_builder.is_ok());
     let _tx = tx_builder.build().unwrap();
 }
diff --git a/sigma-tree/proptest-regressions/wallet/signing.txt b/sigma-tree/proptest-regressions/wallet/signing.txt
new file mode 100644
index 000000000..4e94deb8e
--- /dev/null
+++ b/sigma-tree/proptest-regressions/wallet/signing.txt
@@ -0,0 +1,8 @@
+# Seeds for failure cases proptest has generated in the past. It is
+# automatically read and these particular cases re-run before any
+# novel cases are generated.
+#
+# It is recommended to check this file in to source control so that
+# everyone who runs the test benefits from these saved cases.
+cc 412026e548113b617adbf29b5e6f7f77f06214e70d133c3878c3cd6b7c17e149 # shrinks to secrets = [DlogProverInput { w: Scalar(Scalar4x64([2534451601720644535, 3728917935300645465, 11398511672496504935, 17733221342642325133])) }]
+cc 8ee684c0070e1329c1b57626759b847c6ff607453c40f2741cdac5e23d26e1cd # shrinks to secrets = [DlogProverInput { w: Scalar(Scalar4x64([12796785284189841021, 14909825182994045105, 13731081180851305275, 4935121123420806630])) }]
diff --git a/sigma-tree/proptest-regressions/wallet/tx_builder.txt b/sigma-tree/proptest-regressions/wallet/tx_builder.txt
new file mode 100644
index 000000000..34ec5ed1f
--- /dev/null
+++ b/sigma-tree/proptest-regressions/wallet/tx_builder.txt
@@ -0,0 +1,8 @@
+# Seeds for failure cases proptest has generated in the past. It is
+# automatically read and these particular cases re-run before any
+# novel cases are generated.
+#
+# It is recommended to check this file in to source control so that
+# everyone who runs the test benefits from these saved cases.
+cc 8bd0f5c261309fb2ee37b30b74e9991d23b8faebeb1cdc4c093267418b8f2def # shrinks to inputs = [ErgoBox { box_id: BoxId(Digest32([192, 169, 0, 117, 37, 2, 92, 55, 182, 174, 94, 69, 47, 33, 232, 18, 103, 232, 166, 28, 148, 73, 133, 10, 52, 224, 177, 244, 135, 12, 11, 3])), value: BoxValue(27740066), ergo_tree: ErgoTree { header: ErgoTreeHeader(0), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([]), creation_height: 0, transaction_id: TxId(Digest32([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])), index: 0 }], outputs = [ErgoBoxCandidate { value: BoxValue(13356530), ergo_tree: ErgoTree { header: ErgoTreeHeader(0), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([]), creation_height: 0 }], change_address = P2PK(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) }), miners_fee = BoxValue(14368777)
+cc 202e4da03d96c5ce67e6c229a270638be71a51f2ef86dc70c2e121cbdf75b782 # shrinks to inputs = [ErgoBox { box_id: BoxId(Digest32([71, 138, 231, 27, 15, 127, 230, 252, 65, 149, 214, 218, 25, 86, 145, 54, 2, 11, 164, 211, 227, 204, 2, 29, 19, 152, 230, 90, 118, 164, 104, 118])), value: BoxValue(22258517), ergo_tree: ErgoTree { header: ErgoTreeHeader(0), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([]), creation_height: 0, transaction_id: TxId(Digest32([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])), index: 0 }], outputs = [ErgoBoxCandidate { value: BoxValue(20403935), ergo_tree: ErgoTree { header: ErgoTreeHeader(0), tree: Ok(ParsedTree { constants: [], root: Ok(Const(Constant { tpe: SSigmaProp, v: SigmaProp(SigmaProp(ProofOfKnowledge(ProveDlog(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) })))) })) }) }, tokens: [], additional_registers: NonMandatoryRegisters([]), creation_height: 0 }], change_address = P2PK(ProveDlog { h: EcPoint(ProjectivePoint { x: FieldElement(FieldElementImpl { value: FieldElement5x52([705178180786072, 3855836460717471, 4089131105950716, 3301581525494108, 133858670344668]), magnitude: 1, normalized: true }), y: FieldElement(FieldElementImpl { value: FieldElement5x52([2199641648059576, 1278080618437060, 3959378566518708, 3455034269351872, 79417610544803]), magnitude: 1, normalized: true }), z: FieldElement(FieldElementImpl { value: FieldElement5x52([1, 0, 0, 0, 0]), magnitude: 1, normalized: true }) }) }), miners_fee = BoxValue(1839823)
diff --git a/sigma-tree/src/test_util.rs b/sigma-tree/src/test_util.rs
new file mode 100644
index 000000000..38d8d781a
--- /dev/null
+++ b/sigma-tree/src/test_util.rs
@@ -0,0 +1,8 @@
+use proptest::strategy::ValueTree;
+use proptest::test_runner::TestRunner;
+use proptest::{arbitrary::Arbitrary, prelude::*};
+
+pub fn force_any_val<T: Arbitrary>() -> T {
+    let mut runner = TestRunner::default();
+    any::<T>().new_tree(&mut runner).unwrap().current()
+}