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

Issue:
Add tx validity check in tx builder
- Check minted token id == first input id in tx builder; 
- Copy checks in tx builder from sigmastate;


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-wasm/src/utils.rs b/bindings/ergo-lib-wasm/src/utils.rs
index 482492e3c..ff9a36425 100644
--- a/bindings/ergo-lib-wasm/src/utils.rs
+++ b/bindings/ergo-lib-wasm/src/utils.rs
@@ -22,6 +22,11 @@ impl I64 {
     pub fn to_str(&self) -> String {
         format!("{}", self.0)
     }
+
+    /// Get the value as JS number (64-bit float)
+    pub fn as_num(&self) -> js_sys::Number {
+        js_sys::Number::from(self.0 as f64)
+    }
 }
 
 impl From<i64> for I64 {
diff --git a/ergo-lib/src/chain/ergo_box.rs b/ergo-lib/src/chain/ergo_box.rs
index c8b36aa47..04ded5d2e 100644
--- a/ergo-lib/src/chain/ergo_box.rs
+++ b/ergo-lib/src/chain/ergo_box.rs
@@ -7,6 +7,7 @@ pub mod register;
 
 #[cfg(feature = "json")]
 use super::json;
+use super::token::TokenAmount;
 use super::{
     digest32::blake2b256_hash,
     token::{Token, TokenId},
@@ -160,26 +161,24 @@ pub fn sum_value<T: ErgoBoxAssets>(bs: &[T]) -> u64 {
 }
 
 /// Returns the total token amounts (all tokens combined)
-pub fn sum_tokens(ts: &[Token]) -> HashMap<TokenId, u64> {
-    let mut res = HashMap::new();
+pub fn sum_tokens(ts: &[Token]) -> HashMap<TokenId, TokenAmount> {
+    let mut res: HashMap<TokenId, TokenAmount> = HashMap::new();
     ts.iter().for_each(|t| {
-        let token_amt = u64::from(t.amount);
         res.entry(t.token_id.clone())
-            .and_modify(|amt| *amt += token_amt)
-            .or_insert(token_amt);
+            .and_modify(|amt| *amt = amt.checked_add(&t.amount).unwrap())
+            .or_insert(t.amount);
     });
     res
 }
 
 /// Returns the total token amounts (all tokens combined) of the given boxes
-pub fn sum_tokens_from_boxes<T: ErgoBoxAssets>(bs: &[T]) -> HashMap<TokenId, u64> {
-    let mut res: HashMap<TokenId, u64> = HashMap::new();
+pub fn sum_tokens_from_boxes<T: ErgoBoxAssets>(bs: &[T]) -> HashMap<TokenId, TokenAmount> {
+    let mut res: HashMap<TokenId, TokenAmount> = HashMap::new();
     bs.iter().for_each(|b| {
         b.tokens().iter().for_each(|t| {
-            let token_amt = u64::from(t.amount);
             res.entry(t.token_id.clone())
-                .and_modify(|a| *a += token_amt)
-                .or_insert(token_amt);
+                .and_modify(|amt| *amt = amt.checked_add(&t.amount).unwrap())
+                .or_insert(t.amount);
         });
     });
     res
@@ -435,9 +434,11 @@ mod tests {
             tokens: vec![token.clone(), token.clone()],
         };
         assert_eq!(
-            *sum_tokens_from_boxes(vec![b.clone(), b].as_slice())
-                .get(&token.token_id)
-                .unwrap(),
+            u64::from(
+                *sum_tokens_from_boxes(vec![b.clone(), b].as_slice())
+                    .get(&token.token_id)
+                    .unwrap()
+            ),
             u64::from(token.amount) * 4
         );
     }
diff --git a/ergo-lib/src/chain/ergo_box/box_value.rs b/ergo-lib/src/chain/ergo_box/box_value.rs
index 96afcfdbf..3cde9018b 100644
--- a/ergo-lib/src/chain/ergo_box/box_value.rs
+++ b/ergo-lib/src/chain/ergo_box/box_value.rs
@@ -57,30 +57,30 @@ impl BoxValue {
     /// Addition with overflow check
     pub fn checked_add(&self, rhs: &Self) -> Result<Self, BoxValueError> {
         let raw = self.0.checked_add(rhs.0).ok_or(BoxValueError::Overflow)?;
-        if raw > BoxValue::MAX_RAW {
+        if raw > Self::MAX_RAW {
             Err(BoxValueError::OutOfBounds(raw))
         } else {
-            Ok(BoxValue(raw))
+            Ok(Self(raw))
         }
     }
 
     /// Subtraction with overflow and bounds check
     pub fn checked_sub(&self, rhs: &Self) -> Result<Self, BoxValueError> {
         let raw = self.0.checked_sub(rhs.0).ok_or(BoxValueError::Overflow)?;
-        if raw < BoxValue::MIN_RAW {
+        if raw < Self::MIN_RAW {
             Err(BoxValueError::OutOfBounds(raw))
         } else {
-            Ok(BoxValue(raw))
+            Ok(Self(raw))
         }
     }
 
     /// Multiplication with overflow check
     pub fn checked_mul(&self, rhs: &Self) -> Result<Self, BoxValueError> {
         let raw = self.0.checked_mul(rhs.0).ok_or(BoxValueError::Overflow)?;
-        if raw > BoxValue::MAX_RAW {
+        if raw > Self::MAX_RAW {
             Err(BoxValueError::OutOfBounds(raw))
         } else {
-            Ok(BoxValue(raw))
+            Ok(Self(raw))
         }
     }
 
@@ -90,10 +90,10 @@ impl BoxValue {
             .0
             .checked_mul(rhs as u64)
             .ok_or(BoxValueError::Overflow)?;
-        if raw > BoxValue::MAX_RAW {
+        if raw > Self::MAX_RAW {
             Err(BoxValueError::OutOfBounds(raw))
         } else {
-            Ok(BoxValue(raw))
+            Ok(Self(raw))
         }
     }
 }
diff --git a/ergo-lib/src/chain/token.rs b/ergo-lib/src/chain/token.rs
index 0b927bc56..24833eeef 100644
--- a/ergo-lib/src/chain/token.rs
+++ b/ergo-lib/src/chain/token.rs
@@ -48,15 +48,41 @@ impl SigmaSerializable for TokenId {
 }
 
 /// Token amount with bound checks
-#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
+#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, PartialOrd, Ord)]
 #[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
 pub struct TokenAmount(u64);
 
 impl TokenAmount {
     /// minimal allowed value
-    pub const MIN: u64 = 1;
+    pub const MIN_RAW: u64 = 1;
     /// maximal allowed value
-    pub const MAX: u64 = i64::MAX as u64;
+    pub const MAX_RAW: u64 = i64::MAX as u64;
+
+    /// Addition with overflow check
+    pub fn checked_add(&self, rhs: &Self) -> Result<Self, TokenAmountError> {
+        let raw = self
+            .0
+            .checked_add(rhs.0)
+            .ok_or(TokenAmountError::Overflow)?;
+        if raw > Self::MAX_RAW {
+            Err(TokenAmountError::OutOfBounds(raw))
+        } else {
+            Ok(Self(raw))
+        }
+    }
+
+    /// Subtraction with overflow and bounds check
+    pub fn checked_sub(&self, rhs: &Self) -> Result<Self, TokenAmountError> {
+        let raw = self
+            .0
+            .checked_sub(rhs.0)
+            .ok_or(TokenAmountError::Overflow)?;
+        if raw < Self::MIN_RAW {
+            Err(TokenAmountError::OutOfBounds(raw))
+        } else {
+            Ok(Self(raw))
+        }
+    }
 }
 
 /// BoxValue errors
@@ -74,7 +100,7 @@ impl TryFrom<u64> for TokenAmount {
     type Error = TokenAmountError;
 
     fn try_from(v: u64) -> Result<Self, Self::Error> {
-        if v >= TokenAmount::MIN && v <= TokenAmount::MAX {
+        if v >= TokenAmount::MIN_RAW && v <= TokenAmount::MAX_RAW {
             Ok(TokenAmount(v))
         } else {
             Err(TokenAmountError::OutOfBounds(v))
@@ -107,50 +133,73 @@ pub struct Token {
     pub amount: TokenAmount,
 }
 
+impl From<(TokenId, TokenAmount)> for Token {
+    fn from(token_pair: (TokenId, TokenAmount)) -> Self {
+        Token {
+            token_id: token_pair.0,
+            amount: token_pair.1,
+        }
+    }
+}
+
 #[cfg(test)]
-mod tests {
+pub mod tests {
     use super::*;
     use crate::chain::Base16DecodedBytes;
     use crate::serialization::sigma_serialize_roundtrip;
     use proptest::prelude::*;
 
-    impl Arbitrary for TokenId {
-        type Parameters = ();
+    pub enum ArbTokenIdParam {
+        Predef,
+        Arbitrary,
+    }
 
-        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
-            prop_oneof![
-                Just(TokenId::from(
-                    Digest32::try_from(
-                        Base16DecodedBytes::try_from(
-                            "3130a82e45842aebb888742868e055e2f554ab7d92f233f2c828ed4a43793710"
-                                .to_string()
+    impl Default for ArbTokenIdParam {
+        fn default() -> Self {
+            ArbTokenIdParam::Predef
+        }
+    }
+
+    impl Arbitrary for TokenId {
+        type Parameters = ArbTokenIdParam;
+
+        fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
+            match args {
+                ArbTokenIdParam::Predef => prop_oneof![
+                    Just(TokenId::from(
+                        Digest32::try_from(
+                            Base16DecodedBytes::try_from(
+                                "3130a82e45842aebb888742868e055e2f554ab7d92f233f2c828ed4a43793710"
+                                    .to_string()
+                            )
+                            .unwrap()
                         )
                         .unwrap()
-                    )
-                    .unwrap()
-                )),
-                Just(TokenId::from(
-                    Digest32::try_from(
-                        Base16DecodedBytes::try_from(
-                            "e7321ffb4ec5d71deb3110eb1ac09612b9cf57445acab1e0e3b1222d5b5a6c60"
-                                .to_string()
+                    )),
+                    Just(TokenId::from(
+                        Digest32::try_from(
+                            Base16DecodedBytes::try_from(
+                                "e7321ffb4ec5d71deb3110eb1ac09612b9cf57445acab1e0e3b1222d5b5a6c60"
+                                    .to_string()
+                            )
+                            .unwrap()
                         )
                         .unwrap()
-                    )
-                    .unwrap()
-                )),
-                Just(TokenId::from(
-                    Digest32::try_from(
-                        Base16DecodedBytes::try_from(
-                            "ad62f6dd92e7dc850bc406770dfac9a943dd221a7fb440b7b2bcc7d3149c1792"
-                                .to_string()
+                    )),
+                    Just(TokenId::from(
+                        Digest32::try_from(
+                            Base16DecodedBytes::try_from(
+                                "ad62f6dd92e7dc850bc406770dfac9a943dd221a7fb440b7b2bcc7d3149c1792"
+                                    .to_string()
+                            )
+                            .unwrap()
                         )
                         .unwrap()
-                    )
-                    .unwrap()
-                ))
-            ]
-            .boxed()
+                    ))
+                ]
+                .boxed(),
+                ArbTokenIdParam::Arbitrary => (any::<Digest32>()).prop_map_into().boxed(),
+            }
         }
 
         type Strategy = BoxedStrategy<Self>;
@@ -160,7 +209,7 @@ mod tests {
         type Parameters = ();
 
         fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
-            (TokenAmount::MIN..=TokenAmount::MAX / 100000)
+            (TokenAmount::MIN_RAW..=TokenAmount::MAX_RAW / 100000)
                 .prop_map(Self)
                 .boxed()
         }
diff --git a/ergo-lib/src/wallet/box_selector/simple.rs b/ergo-lib/src/wallet/box_selector/simple.rs
index 6cec41e8a..9f0df3c52 100644
--- a/ergo-lib/src/wallet/box_selector/simple.rs
+++ b/ergo-lib/src/wallet/box_selector/simple.rs
@@ -2,7 +2,6 @@
 
 use std::cmp::min;
 use std::collections::HashMap;
-use std::convert::TryFrom;
 use std::convert::TryInto;
 
 use crate::chain::ergo_box::box_value::BoxValue;
@@ -43,15 +42,8 @@ impl<T: ErgoBoxAssets> BoxSelector<T> for SimpleBoxSelector {
         let mut selected_inputs: Vec<T> = vec![];
         let mut selected_boxes_value: u64 = 0;
         let target_balance: u64 = target_balance.into();
-        let mut target_tokens_left: HashMap<TokenId, u64> = HashMap::new();
         // sum all target tokens into hash map (think repeating token ids)
-        target_tokens.iter().for_each(|t| {
-            let token_amt = u64::from(t.amount);
-            target_tokens_left
-                .entry(t.token_id.clone())
-                .and_modify(|amt| *amt += token_amt)
-                .or_insert(token_amt);
-        });
+        let mut target_tokens_left: HashMap<TokenId, TokenAmount> = sum_tokens(target_tokens);
         let mut has_value_change = false;
         let mut has_token_change = false;
         inputs.into_iter().for_each(|b| {
@@ -69,26 +61,28 @@ impl<T: ErgoBoxAssets> BoxSelector<T> for SimpleBoxSelector {
                 if selected_boxes_value > target_balance {
                     has_value_change = true;
                 }
-                let mut selected_tokens_from_this_box: HashMap<TokenId, u64> = HashMap::new();
+                let mut selected_tokens_from_this_box: HashMap<TokenId, TokenAmount> =
+                    HashMap::new();
                 b.tokens().iter().for_each(|t| {
-                    let token_amount_left_to_select =
-                        *target_tokens_left.get(&t.token_id).unwrap_or(&0);
-                    let token_amount_in_box = u64::from(t.amount);
-                    if token_amount_left_to_select <= token_amount_in_box {
-                        target_tokens_left.remove(&t.token_id);
-                    } else {
-                        target_tokens_left.insert(
-                            t.token_id.clone(),
-                            token_amount_left_to_select - token_amount_in_box,
-                        );
-                    }
-                    if token_amount_left_to_select > 0 {
+                    if let Some(token_amount_left_to_select) =
+                        target_tokens_left.get(&t.token_id).cloned()
+                    {
+                        let token_amount_in_box = t.amount;
+                        if token_amount_left_to_select <= token_amount_in_box {
+                            target_tokens_left.remove(&t.token_id);
+                        } else {
+                            target_tokens_left
+                                .entry(t.token_id.clone())
+                                .and_modify(|amt| {
+                                    *amt = amt.checked_sub(&token_amount_in_box).unwrap()
+                                });
+                        }
                         let selected_token_amt =
                             min(token_amount_in_box, token_amount_left_to_select);
                         selected_tokens_from_this_box
                             .entry(t.token_id.clone())
                             .and_modify(|amt| {
-                                *amt += selected_token_amt;
+                                *amt = amt.checked_add(&selected_token_amt).unwrap();
                             })
                             .or_insert(selected_token_amt);
                     }
@@ -106,13 +100,7 @@ impl<T: ErgoBoxAssets> BoxSelector<T> for SimpleBoxSelector {
         }
         if !target_tokens.is_empty() && !target_tokens_left.is_empty() {
             return Err(BoxSelectorError::NotEnoughTokens(
-                target_tokens_left
-                    .iter()
-                    .map(|(token_id, token_amount)| Token {
-                        token_id: token_id.clone(),
-                        amount: TokenAmount::try_from(*token_amount).unwrap(),
-                    })
-                    .collect(),
+                target_tokens_left.into_iter().map(Token::from).collect(),
             ));
         }
         let change_boxes: Vec<ErgoBoxAssetsData> = if !has_value_change && !has_token_change {
@@ -120,24 +108,25 @@ impl<T: ErgoBoxAssets> BoxSelector<T> for SimpleBoxSelector {
         } else {
             let change_value: BoxValue = (selected_boxes_value - target_balance).try_into()?;
             let mut change_tokens = sum_tokens_from_boxes(selected_inputs.as_slice());
-            target_tokens.iter().for_each(|t| {
-                let selected_boxes_t_amt = change_tokens.get(&t.token_id).unwrap();
-                let t_change_amt = *selected_boxes_t_amt - u64::from(t.amount);
-                if t_change_amt == 0 {
-                    change_tokens.remove(&t.token_id);
-                } else {
-                    change_tokens.insert(t.token_id.clone(), t_change_amt);
-                };
-            });
+            target_tokens.iter().try_for_each(|t| {
+                match change_tokens.get(&t.token_id).cloned() {
+                    Some(selected_boxes_t_amt) if selected_boxes_t_amt == t.amount => {
+                        change_tokens.remove(&t.token_id);
+                        Ok(())
+                    }
+                    Some(selected_boxes_t_amt) if selected_boxes_t_amt > t.amount => {
+                        change_tokens.insert(
+                            t.token_id.clone(),
+                            selected_boxes_t_amt.checked_sub(&t.amount).unwrap(),
+                        );
+                        Ok(())
+                    }
+                    _ => Err(BoxSelectorError::NotEnoughTokens(vec![t.clone()])),
+                }
+            })?;
             vec![ErgoBoxAssetsData {
                 value: change_value,
-                tokens: change_tokens
-                    .iter()
-                    .map(|t| Token {
-                        token_id: t.0.clone(),
-                        amount: TokenAmount::try_from(*t.1).unwrap(),
-                    })
-                    .collect(),
+                tokens: change_tokens.into_iter().map(Token::from).collect(),
             }]
         };
         Ok(BoxSelection {
@@ -155,6 +144,8 @@ impl Default for SimpleBoxSelector {
 
 #[cfg(test)]
 mod tests {
+    use std::convert::TryFrom;
+
     use crate::chain::ergo_box::box_value;
     use crate::chain::ergo_box::sum_value;
     use crate::chain::ergo_box::ErgoBox;
@@ -275,8 +266,9 @@ mod tests {
             let target_token_id = all_input_tokens.keys().collect::<Vec<&TokenId>>().get((all_input_tokens.len() - 1) / 2)
                                                                                     .cloned().unwrap();
             let input_token_amount = *all_input_tokens.get(target_token_id).unwrap();
+            let target_token_amount: TokenAmount = target_token_amount.try_into().unwrap();
             prop_assume!(input_token_amount >= target_token_amount);
-            let target_token = Token {token_id: target_token_id.clone(), amount: target_token_amount.try_into().unwrap()};
+            let target_token = Token {token_id: target_token_id.clone(), amount: target_token_amount};
             let selection = s.select(inputs, target_balance, vec![target_token.clone()].as_slice()).unwrap();
             let out_box = ErgoBoxAssetsData {value: target_balance, tokens: vec![target_token]};
             let mut change_boxes_plus_out = vec![out_box];
@@ -301,8 +293,7 @@ mod tests {
             let target_token_id = all_input_tokens.keys().collect::<Vec<&TokenId>>().get((all_input_tokens.len() - 1) / 2)
                                                                                     .cloned().unwrap();
             let input_token_amount = *all_input_tokens.get(target_token_id).unwrap();
-            prop_assume!(input_token_amount > 0);
-            let target_token = Token {token_id: target_token_id.clone(), amount: input_token_amount.try_into().unwrap()};
+            let target_token = Token {token_id: target_token_id.clone(), amount: input_token_amount};
             let selection = s.select(inputs, target_balance, vec![target_token.clone()].as_slice()).unwrap();
             let out_box = ErgoBoxAssetsData {value: target_balance, tokens: vec![target_token]};
             let mut change_boxes_plus_out = vec![out_box];
@@ -331,8 +322,8 @@ mod tests {
             let target_token2_id = all_input_tokens_keys.last().cloned().unwrap();
             let input_token1_amount = *all_input_tokens.get(target_token1_id).unwrap();
             let input_token2_amount = *all_input_tokens.get(target_token2_id).unwrap();
-            prop_assume!(input_token1_amount >= target_token1_amount);
-            prop_assume!(input_token2_amount >= target_token2_amount);
+            prop_assume!(u64::from(input_token1_amount) >= target_token1_amount);
+            prop_assume!(u64::from(input_token2_amount) >= target_token2_amount);
             let target_token1 = Token {token_id: target_token1_id.clone(), amount: target_token1_amount.try_into().unwrap()};
             // simulate repeating token ids (e.g the same token id mentioned twice)
             let target_token2_amount_part1 = target_token2_amount / 2;
@@ -365,8 +356,8 @@ mod tests {
             let all_input_tokens = sum_tokens_from_boxes(inputs.as_slice());
             prop_assume!(!all_input_tokens.is_empty());
             let target_token_id = all_input_tokens.keys().collect::<Vec<&TokenId>>().get(0).cloned().unwrap();
-            let input_token_amount = all_input_tokens.get(target_token_id).unwrap() / 2;
-            let target_token_amount = TokenAmount::MAX;
+            let input_token_amount = u64::from(*all_input_tokens.get(target_token_id).unwrap()) / 2;
+            let target_token_amount = TokenAmount::MAX_RAW;
             prop_assume!(input_token_amount < target_token_amount);
             let target_token = Token {token_id: target_token_id.clone(), amount: target_token_amount.try_into().unwrap()};
             let selection = s.select(inputs, target_balance, vec![target_token].as_slice());
diff --git a/ergo-lib/src/wallet/tx_builder.rs b/ergo-lib/src/wallet/tx_builder.rs
index 1ed8bf6c3..cff2abc57 100644
--- a/ergo-lib/src/wallet/tx_builder.rs
+++ b/ergo-lib/src/wallet/tx_builder.rs
@@ -1,5 +1,7 @@
 //! Builder for an UnsignedTransaction
 
+use std::collections::HashSet;
+
 use box_value::BoxValueError;
 use thiserror::Error;
 
@@ -10,10 +12,15 @@ 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_id::BoxId;
 use crate::chain::ergo_box::box_value;
+use crate::chain::ergo_box::sum_tokens_from_boxes;
+use crate::chain::ergo_box::sum_value;
 use crate::chain::input::Input;
 use crate::chain::prover_result::ProofBytes;
 use crate::chain::prover_result::ProverResult;
+use crate::chain::token::Token;
+use crate::chain::token::TokenId;
 use crate::chain::transaction::Transaction;
 use crate::chain::{
     ergo_box::ErgoBoxAssets,
@@ -108,8 +115,30 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
                 "output_candidates is empty".to_string(),
             ));
         }
-        let mut output_candidates = self.output_candidates.clone();
+        if self.box_selection.boxes.len() > u16::MAX as usize {
+            return Err(TxBuilderError::InvalidArgs("too many inputs".to_string()));
+        }
+        if self
+            .box_selection
+            .boxes
+            .clone()
+            .into_iter()
+            .map(|b| b.box_id())
+            .collect::<HashSet<BoxId>>()
+            .len()
+            != self.box_selection.boxes.len()
+        {
+            return Err(TxBuilderError::InvalidArgs(
+                "duplicate inputs found".to_string(),
+            ));
+        }
+        if self.data_inputs.len() > u16::MAX as usize {
+            return Err(TxBuilderError::InvalidArgs(
+                "too many data inputs".to_string(),
+            ));
+        }
 
+        let mut output_candidates = self.output_candidates.clone();
         let change_address_ergo_tree = Contract::pay_to_address(&self.change_address)?.ergo_tree();
         let change_boxes: Result<Vec<ErgoBoxCandidate>, ErgoBoxCandidateBuilderError> = self
             .box_selection
@@ -129,6 +158,41 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
         // add miner's fee
         let miner_fee_box = new_miner_fee_box(self.fee_amount, self.current_height)?;
         output_candidates.push(miner_fee_box);
+        if output_candidates.len() > u16::MAX as usize {
+            return Err(TxBuilderError::InvalidArgs("too many outputs".to_string()));
+        }
+        // check that inputs have enough coins
+        let total_input_value = sum_value(self.box_selection.boxes.as_slice());
+        let total_output_value = sum_value(output_candidates.as_slice());
+        if total_output_value > total_input_value {
+            return Err(TxBuilderError::NotEnoughCoins(
+                total_output_value - total_input_value,
+            ));
+        }
+        // check that inputs have enough tokens
+        let input_tokens = sum_tokens_from_boxes(self.box_selection.boxes.as_slice());
+        let output_tokens = sum_tokens_from_boxes(output_candidates.as_slice());
+        let first_input_box_id: TokenId = self.box_selection.boxes.first().unwrap().box_id().into();
+        let output_tokens_len = output_tokens.len();
+        let output_tokens_without_minted: Vec<Token> = output_tokens
+            .into_iter()
+            .map(Token::from)
+            .filter(|t| t.token_id != first_input_box_id)
+            .collect();
+        if output_tokens_len - output_tokens_without_minted.len() > 1 {
+            return Err(TxBuilderError::InvalidArgs(
+                "cannot mint more than one token".to_string(),
+            ));
+        }
+        output_tokens_without_minted
+            .iter()
+            .try_for_each(|output_token| {
+                match input_tokens.get(&output_token.token_id).cloned() {
+                    Some(input_token_amount) if input_token_amount >= output_token.amount => Ok(()),
+                    _ => Err(TxBuilderError::NotEnoughTokens(vec![output_token.clone()])),
+                }
+            })?;
+
         Ok(UnsignedTransaction::new(
             self.box_selection
                 .boxes
@@ -178,6 +242,12 @@ pub enum TxBuilderError {
     /// ErgoBoxCandidate error
     #[error("ErgoBoxCandidateBuilder error: {0}")]
     ErgoBoxCandidateBuilderError(#[from] ErgoBoxCandidateBuilderError),
+    /// Not enougn tokens
+    #[error("Not enougn tokens: {0:?}")]
+    NotEnoughTokens(Vec<Token>),
+    /// Not enough coins
+    #[error("Not enough coins({0} nanoERGs are missing)")]
+    NotEnoughCoins(u64),
 }
 
 #[cfg(test)]
@@ -189,7 +259,9 @@ mod tests {
 
     use crate::chain::ergo_box::register::NonMandatoryRegisters;
     use crate::chain::ergo_box::ErgoBox;
+    use crate::chain::token::tests::ArbTokenIdParam;
     use crate::chain::token::Token;
+    use crate::chain::token::TokenAmount;
     use crate::chain::token::TokenId;
     use crate::chain::transaction::TxId;
     use crate::test_util::force_any_val;
@@ -217,6 +289,24 @@ mod tests {
         assert!(r.build().is_err());
     }
 
+    #[test]
+    fn test_duplicate_inputs() {
+        let input_box = force_any_val::<ErgoBox>();
+        let box_selection: BoxSelection<ErgoBox> = BoxSelection {
+            boxes: vec![input_box.clone(), input_box],
+            change_boxes: vec![],
+        };
+        let r = TxBuilder::new(
+            box_selection,
+            vec![force_any_val::<ErgoBoxCandidate>()],
+            1,
+            force_any_val::<BoxValue>(),
+            force_any_val::<Address>(),
+            BoxValue::SAFE_USER_MIN,
+        );
+        assert!(r.build().is_err(), "error on duplicate inputs");
+    }
+
     #[test]
     fn test_empty_outputs() {
         let inputs = vec![force_any_val::<ErgoBox>()];
@@ -231,7 +321,7 @@ mod tests {
             force_any_val::<Address>(),
             BoxValue::SAFE_USER_MIN,
         );
-        assert!(r.build().is_err());
+        assert!(r.build().is_err(), "error on empty inputs");
     }
 
     #[test]
@@ -273,14 +363,145 @@ mod tests {
             BoxValue::SAFE_USER_MIN,
         );
         let tx = tx_builder.build().unwrap();
-        assert!(tx.output_candidates.get(0).unwrap().tokens().is_empty());
+        assert!(
+            tx.output_candidates.get(0).unwrap().tokens().is_empty(),
+            "expected empty tokens in the first output box"
+        );
     }
 
     #[test]
-    fn test_est_tx_size() {
-        let input = force_any_val_with::<ErgoBox>(
+    fn test_mint_token() {
+        let input_box = ErgoBox::new(
+            100000000i64.try_into().unwrap(),
+            force_any_val::<ErgoTree>(),
+            vec![],
+            NonMandatoryRegisters::empty(),
+            1,
+            force_any_val::<TxId>(),
+            0,
+        );
+        let token_pair = Token {
+            token_id: TokenId::from(input_box.box_id()),
+            amount: 1.try_into().unwrap(),
+        };
+        let out_box_value = BoxValue::SAFE_USER_MIN;
+        let token_name = "TKN".to_string();
+        let token_desc = "token desc".to_string();
+        let token_num_dec = 2;
+        let mut box_builder =
+            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
+        box_builder.mint_token(token_pair.clone(), token_name, token_desc, token_num_dec);
+        let out_box = box_builder.build().unwrap();
+
+        let inputs: Vec<ErgoBox> = vec![input_box];
+        let tx_fee = BoxValue::SAFE_USER_MIN;
+        let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
+        let box_selection = SimpleBoxSelector::new()
+            .select(inputs, target_balance, vec![].as_slice())
+            .unwrap();
+        let outputs = vec![out_box];
+        let tx_builder = TxBuilder::new(
+            box_selection,
+            outputs,
+            0,
+            tx_fee,
+            force_any_val::<Address>(),
+            BoxValue::SAFE_USER_MIN,
+        );
+        let tx = tx_builder.build().unwrap();
+        assert_eq!(
+            tx.output_candidates
+                .get(0)
+                .unwrap()
+                .tokens()
+                .first()
+                .unwrap()
+                .token_id,
+            token_pair.token_id,
+            "expected minted token in the first output box"
+        );
+    }
+
+    #[test]
+    fn test_tokens_balance_error() {
+        let input_box = force_any_val_with::<ErgoBox>(
+            (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
+        );
+        let token_pair = Token {
+            token_id: force_any_val_with::<TokenId>(ArbTokenIdParam::Arbitrary),
+            amount: force_any_val::<TokenAmount>(),
+        };
+        let out_box_value = BoxValue::SAFE_USER_MIN;
+        let mut box_builder =
+            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
+        // try to spend a token that is not in inputs
+        box_builder.add_token(token_pair);
+        let out_box = box_builder.build().unwrap();
+        let inputs: Vec<ErgoBox> = vec![input_box];
+        let tx_fee = BoxValue::SAFE_USER_MIN;
+        let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
+        let box_selection = SimpleBoxSelector::new()
+            .select(inputs, target_balance, vec![].as_slice())
+            .unwrap();
+        let outputs = vec![out_box];
+        let tx_builder = TxBuilder::new(
+            box_selection,
+            outputs,
+            0,
+            tx_fee,
+            force_any_val::<Address>(),
+            BoxValue::SAFE_USER_MIN,
+        );
+        assert!(
+            tx_builder.build().is_err(),
+            "expected error trying to spend the token that not in the inputs"
+        );
+    }
+
+    #[test]
+    fn test_balance_error() {
+        let input_box = force_any_val_with::<ErgoBox>(
             (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
         );
+        let out_box_value = input_box
+            .value()
+            .checked_add(&BoxValue::SAFE_USER_MIN)
+            .unwrap();
+        let box_builder =
+            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
+        let out_box = box_builder.build().unwrap();
+        let inputs: Vec<ErgoBox> = vec![input_box];
+        let tx_fee = BoxValue::SAFE_USER_MIN;
+        let box_selection = BoxSelection {
+            boxes: inputs,
+            change_boxes: vec![],
+        };
+        let outputs = vec![out_box];
+        let tx_builder = TxBuilder::new(
+            box_selection,
+            outputs,
+            0,
+            tx_fee,
+            force_any_val::<Address>(),
+            BoxValue::SAFE_USER_MIN,
+        );
+        assert!(
+            tx_builder.build().is_err(),
+            "expected error on trying to spend value exceeding total inputs value"
+        );
+    }
+
+    #[test]
+    fn test_est_tx_size() {
+        let input = ErgoBox::new(
+            10000000i64.try_into().unwrap(),
+            force_any_val::<ErgoTree>(),
+            vec![],
+            NonMandatoryRegisters::empty(),
+            1,
+            force_any_val::<TxId>(),
+            0,
+        );
         let tx_fee = super::SUGGESTED_TX_FEE;
         let out_box_value = input.value.checked_sub(&tx_fee).unwrap();
         let box_builder =
@@ -311,18 +532,15 @@ mod tests {
                          change_address in any::<Address>(),
                          miners_fee in any_with::<BoxValue>((BoxValue::MIN_RAW * 100..BoxValue::MIN_RAW * 200).into()),
                          data_inputs in vec(any::<DataInput>(), 0..2)) {
+            prop_assume!(sum_tokens_from_boxes(outputs.as_slice()).is_empty());
             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)
                                                                              .unwrap();
             let all_inputs = box_value::checked_sum(inputs.iter().map(|b| b.value)).unwrap();
-
             prop_assume!(all_outputs < all_inputs);
-
             let total_output_value: BoxValue = box_value::checked_sum(outputs.iter().map(|b| b.value)).unwrap()
-                .checked_add(&miners_fee).unwrap();
-
+                                                                                                      .checked_add(&miners_fee).unwrap();
             let mut tx_builder = TxBuilder::new(
                 SimpleBoxSelector::new().select(inputs.clone(), total_output_value, &[]).unwrap(),
                 outputs.clone(),


Test patch:
diff --git a/bindings/ergo-lib-wasm/tests/test_transaction.js b/bindings/ergo-lib-wasm/tests/test_transaction.js
index dd6ba9467..ded615c13 100644
--- a/bindings/ergo-lib-wasm/tests/test_transaction.js
+++ b/bindings/ergo-lib-wasm/tests/test_transaction.js
@@ -21,14 +21,16 @@ it('TxBuilder test', async () => {
     }
   ]);
   const contract = Contract.pay_to_address(recipient);
-  const outbox = new ErgoBoxCandidateBuilder(BoxValue.from_u32(10000000), contract, 0).build();
+  const outbox_value = BoxValue.SAFE_USER_MIN();
+  const outbox = new ErgoBoxCandidateBuilder(outbox_value, contract, 0).build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
   const fee = TxBuilder.SUGGESTED_TX_FEE();
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const min_change_value = BoxValue.SAFE_USER_MIN();
   const data_inputs = new DataInputs();
   const box_selector = new SimpleBoxSelector();
-  const box_selection = box_selector.select(unspent_boxes, BoxValue.from_u32(11000000), new Tokens());
+  const target_balance = BoxValue.from_u32(outbox_value.as_i64().as_num() + fee.as_i64().as_num());
+  const box_selection = box_selector.select(unspent_boxes, target_balance, new Tokens());
   const tx_builder = TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
   tx_builder.set_data_inputs(data_inputs);
   const tx = tx_builder.build();
@@ -44,13 +46,15 @@ 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 ErgoBoxCandidateBuilder(BoxValue.from_u32(10000000), contract, 0).build();
+  const outbox_value = BoxValue.SAFE_USER_MIN();
+  const outbox = new ErgoBoxCandidateBuilder(outbox_value, contract, 0).build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
   const fee = TxBuilder.SUGGESTED_TX_FEE();
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const min_change_value = BoxValue.SAFE_USER_MIN();
   const box_selector = new SimpleBoxSelector();
-  const box_selection = box_selector.select(unspent_boxes, BoxValue.from_u32(11000000), new Tokens());
+  const target_balance = BoxValue.from_u32(outbox_value.as_i64().as_num() + fee.as_i64().as_num());
+  const box_selection = box_selector.select(unspent_boxes, target_balance, new Tokens());
   const tx_builder = TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
   const tx = tx_builder.build();
   const tx_data_inputs = ErgoBoxes.from_boxes_json([]);
@@ -74,16 +78,18 @@ it('TxBuilder mint token test', async () => {
       "index": 1
     }
   ]);
+  const fee = TxBuilder.SUGGESTED_TX_FEE();
+  const outbox_value = BoxValue.SAFE_USER_MIN();
+  const target_balance = BoxValue.from_u32(outbox_value.as_i64().as_num() + fee.as_i64().as_num());
   const box_selector = new SimpleBoxSelector();
-  const box_selection = box_selector.select(unspent_boxes, BoxValue.from_u32(11000000), new Tokens());
+  const box_selection = box_selector.select(unspent_boxes, target_balance, new Tokens());
   const contract = Contract.pay_to_address(recipient);
   const token_id = TokenId.from_box_id(box_selection.boxes().get(0).box_id());
-  const box_builder = new ErgoBoxCandidateBuilder(BoxValue.from_u32(10000000), contract, 0);
+  const box_builder = new ErgoBoxCandidateBuilder(outbox_value, contract, 0);
   const token = new Token(token_id, TokenAmount.from_u32(1));
   box_builder.mint_token(token, "TKN", "token desc", 2)
   const outbox = box_builder.build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
-  const fee = TxBuilder.SUGGESTED_TX_FEE();
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const min_change_value = BoxValue.SAFE_USER_MIN();
   const data_inputs = new DataInputs();
@@ -121,14 +127,16 @@ it('TxBuilder burn token test', async () => {
   const box_selector = new SimpleBoxSelector();
   let tokens = new Tokens();
   tokens.add(token);
+  const fee = TxBuilder.SUGGESTED_TX_FEE();
+  const outbox_value = BoxValue.SAFE_USER_MIN();
+  const target_balance = BoxValue.from_u32(outbox_value.as_i64().as_num() + fee.as_i64().as_num());
   // select tokens from inputs
-  const box_selection = box_selector.select(unspent_boxes, BoxValue.from_u32(11000000), tokens);
+  const box_selection = box_selector.select(unspent_boxes, target_balance, tokens);
   const contract = Contract.pay_to_address(recipient);
   // but don't put selected tokens in the output box (burn them)
-  const box_builder = new ErgoBoxCandidateBuilder(BoxValue.from_u32(10000000), contract, 0);
+  const box_builder = new ErgoBoxCandidateBuilder(outbox_value, contract, 0);
   const outbox = box_builder.build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
-  const fee = TxBuilder.SUGGESTED_TX_FEE();
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const min_change_value = BoxValue.SAFE_USER_MIN();
   const data_inputs = new DataInputs();