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

Issue:
Remove `min_change_value` in `TxBuilder` since it makes loosing change box way too easy
Spotted in #591 and inspired by #590

It's used to filter out change boxes and not include them in the produced transaction. The idea behind it is that in a case that the change value is so small that it should be given to miners because it's smaller than the minimum value allowed for the box to have.
Actually, this will not work as intended since the `ErgoBoxAssets::value` used for representing the change box is of type `BoxValue` that already has the lower bound set to a reasonably minimal box value. So, the change cannot be less the box minimal value anyway.
Besides that, I think that another reason for removal is that `TxBuilder` API should not strive to be smart and manipulate transaction outputs/inputs behind the scenes.

Todo:
- remove `min_change_value`;
- ensure a good error message in the box selector that the change ERG value is less than the minimum box value;


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-c-core/src/tx_builder.rs b/bindings/ergo-lib-c-core/src/tx_builder.rs
index 90703898f..691b4bd16 100644
--- a/bindings/ergo-lib-c-core/src/tx_builder.rs
+++ b/bindings/ergo-lib-c-core/src/tx_builder.rs
@@ -34,7 +34,6 @@ pub unsafe fn tx_builder_suggested_tx_fee(value_out: *mut BoxValuePtr) -> Result
 /// `current_height` - chain height that will be used in additionally created boxes (change, miner's fee, etc.),
 /// `fee_amount` - miner's fee,
 /// `change_address` - change (inputs - outputs) will be sent to this address,
-/// `min_change_value` - minimal value of the change to be sent to `change_address`, value less than that
 /// will be given to miners,
 pub unsafe fn tx_builder_new(
     box_selection_ptr: ConstBoxSelectionPtr,
@@ -42,14 +41,12 @@ pub unsafe fn tx_builder_new(
     current_height: u32,
     fee_amount_ptr: ConstBoxValuePtr,
     change_address_ptr: ConstAddressPtr,
-    min_change_value_ptr: ConstBoxValuePtr,
     tx_builder_out: *mut TxBuilderPtr,
 ) -> Result<(), Error> {
     let box_selection = const_ptr_as_ref(box_selection_ptr, "box_selection_ptr")?;
     let output_candidates = const_ptr_as_ref(output_candidates_ptr, "output_candidates_ptr")?;
     let fee_amount = const_ptr_as_ref(fee_amount_ptr, "fee_amount_ptr")?;
     let change_address = const_ptr_as_ref(change_address_ptr, "change_address_ptr")?;
-    let min_change_value = const_ptr_as_ref(min_change_value_ptr, "min_change_value_ptr")?;
     let tx_builder_out = mut_ptr_as_mut(tx_builder_out, "tx_builder_out")?;
     *tx_builder_out = Box::into_raw(Box::new(TxBuilder(
         ergo_lib::wallet::tx_builder::TxBuilder::new(
@@ -63,7 +60,6 @@ pub unsafe fn tx_builder_new(
             current_height,
             fee_amount.0,
             change_address.0.clone(),
-            min_change_value.0,
         ),
     )));
     Ok(())
@@ -201,14 +197,3 @@ pub unsafe fn tx_builder_change_address(
     *address_out = Box::into_raw(Box::new(Address(tx_builder.0.change_address())));
     Ok(())
 }
-
-/// Get min change value
-pub unsafe fn tx_builder_min_change_value(
-    tx_builder_ptr: ConstTxBuilderPtr,
-    min_change_value_out: *mut BoxValuePtr,
-) -> Result<(), Error> {
-    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
-    let min_change_value_out = mut_ptr_as_mut(min_change_value_out, "min_change_value_out")?;
-    *min_change_value_out = Box::into_raw(Box::new(BoxValue(tx_builder.0.min_change_value())));
-    Ok(())
-}
diff --git a/bindings/ergo-lib-c/src/tx_builder.rs b/bindings/ergo-lib-c/src/tx_builder.rs
index 651e91b8a..3f2f7290e 100644
--- a/bindings/ergo-lib-c/src/tx_builder.rs
+++ b/bindings/ergo-lib-c/src/tx_builder.rs
@@ -28,7 +28,6 @@ pub unsafe extern "C" fn ergo_lib_tx_builder_suggested_tx_fee(value_out: *mut Bo
 /// `current_height` - chain height that will be used in additionally created boxes (change, miner's fee, etc.),
 /// `fee_amount` - miner's fee,
 /// `change_address` - change (inputs - outputs) will be sent to this address,
-/// `min_change_value` - minimal value of the change to be sent to `change_address`, value less than that
 /// will be given to miners,
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_tx_builder_new(
@@ -37,7 +36,6 @@ pub unsafe extern "C" fn ergo_lib_tx_builder_new(
     current_height: u32,
     fee_amount_ptr: ConstBoxValuePtr,
     change_address_ptr: ConstAddressPtr,
-    min_change_value_ptr: ConstBoxValuePtr,
     tx_builder_out: *mut TxBuilderPtr,
 ) {
     #[allow(clippy::unwrap_used)]
@@ -47,7 +45,6 @@ pub unsafe extern "C" fn ergo_lib_tx_builder_new(
         current_height,
         fee_amount_ptr,
         change_address_ptr,
-        min_change_value_ptr,
         tx_builder_out,
     )
     .unwrap();
@@ -153,16 +150,6 @@ pub unsafe extern "C" fn ergo_lib_tx_builder_change_address(
     tx_builder_change_address(tx_builder_ptr, address_out).unwrap();
 }
 
-/// Get min change value
-#[no_mangle]
-pub unsafe extern "C" fn ergo_lib_tx_builder_min_change_value(
-    tx_builder_ptr: ConstTxBuilderPtr,
-    min_change_value_out: *mut BoxValuePtr,
-) {
-    #[allow(clippy::unwrap_used)]
-    tx_builder_min_change_value(tx_builder_ptr, min_change_value_out).unwrap();
-}
-
 /// Drop `TxBuilder`
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_tx_builder_delete(ptr: TxBuilderPtr) {
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/TxBuilder.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/TxBuilder.swift
index 81e6cbc1f..3f5acf5e6 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/TxBuilder.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/TxBuilder.swift
@@ -12,15 +12,13 @@ class TxBuilder {
     ///  - `currentHeight`: chain height that will be used in additionally created boxes (change, miner's fee, etc.),
     ///  - `feeAmount`: miner's fee,
     ///  - `changeAddress`: change (inputs - outputs) will be sent to this address,
-    ///  - `minChangeValue`: minimal value of the change to be sent to `change_address`, value less than that
     ///     will be given to miners,
     init(
         boxSelection : BoxSelection,
         outputCandidates: ErgoBoxCandidates,
         currentHeight: UInt32,
         feeAmount: BoxValue,
-        changeAddress: Address,
-        minChangeValue: BoxValue
+        changeAddress: Address
     ) {
         var ptr: TxBuilderPtr?
         ergo_lib_tx_builder_new(
@@ -29,7 +27,6 @@ class TxBuilder {
             currentHeight,
             feeAmount.pointer,
             changeAddress.pointer,
-            minChangeValue.pointer,
             &ptr
         )
         self.pointer = ptr!
@@ -105,13 +102,6 @@ class TxBuilder {
         return Address(withRawPointer: ptr!)
     }
     
-    /// Get min change value
-    func getMinChangeValue() -> BoxValue {
-        var ptr: BoxValuePtr?
-        ergo_lib_tx_builder_min_change_value(self.pointer, &ptr)
-        return BoxValue(withRawPointer: ptr!)
-    }
-    
     deinit {
         ergo_lib_tx_builder_delete(self.pointer)
     }
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift
index 7d09f079b..f49cec449 100644
--- a/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift
@@ -43,8 +43,7 @@ final class TransactionTests: XCTestCase {
             outputCandidates: txOutputs,
             currentHeight: 0,
             feeAmount: fee,
-            changeAddress: changeAddress,
-            minChangeValue: minChangeValue
+            changeAddress: changeAddress
         )
         txBuilder.setDataInputs(dataInputs: dataInputs)
         let tx = try txBuilder.build()
@@ -75,7 +74,7 @@ final class TransactionTests: XCTestCase {
         // Note that swift forbids integer overflow/underflow by default with hard crash
         let targetBalance = try BoxValue.sumOf(boxValue0: outboxValue, boxValue1: fee)
         let boxSelection = try boxSelector.select(inputs: unspentBoxes, targetBalance: targetBalance, targetTokens: Tokens())
-        let txBuilder = TxBuilder(boxSelection: boxSelection, outputCandidates: txOutputs, currentHeight: 0, feeAmount: fee, changeAddress: changeAddress, minChangeValue: minChangeValue)
+        let txBuilder = TxBuilder(boxSelection: boxSelection, outputCandidates: txOutputs, currentHeight: 0, feeAmount: fee, changeAddress: changeAddress)
         txBuilder.setDataInputs(dataInputs: dataInputs)
         let tx = try txBuilder.build()
         XCTAssertNoThrow(try tx.toJsonEIP12())
@@ -133,8 +132,7 @@ final class TransactionTests: XCTestCase {
             outputCandidates: txOutputs,
             currentHeight: 0,
             feeAmount: fee,
-            changeAddress: changeAddress,
-            minChangeValue: minChangeValue
+            changeAddress: changeAddress
         )
         txBuilder.setDataInputs(dataInputs: dataInputs)
         let tx = try txBuilder.build()
@@ -189,7 +187,7 @@ final class TransactionTests: XCTestCase {
         let changeAddress = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
         let minChangeValue = BoxValue.SAFE_USER_MIN()
         let dataInputs = DataInputs()
-        let txBuilder = TxBuilder(boxSelection: boxSelection, outputCandidates: txOutputs, currentHeight: UInt32(0), feeAmount: fee, changeAddress: changeAddress, minChangeValue: minChangeValue)
+        let txBuilder = TxBuilder(boxSelection: boxSelection, outputCandidates: txOutputs, currentHeight: UInt32(0), feeAmount: fee, changeAddress: changeAddress)
         txBuilder.setDataInputs(dataInputs: dataInputs)
         txBuilder.setTokenBurnPermit(tokens: tokens)
         XCTAssertNoThrow(try txBuilder.build())
@@ -218,7 +216,7 @@ final class TransactionTests: XCTestCase {
         let boxSelector = SimpleBoxSelector()
         let targetBalance = try BoxValue.sumOf(boxValue0: outboxValue, boxValue1: fee)
         let boxSelection = try boxSelector.select(inputs: unspentBoxes, targetBalance: targetBalance, targetTokens: Tokens())
-        let txBuilder = TxBuilder(boxSelection: boxSelection, outputCandidates: txOutputs, currentHeight: 0, feeAmount: fee, changeAddress: changeAddress, minChangeValue: minChangeValue)
+        let txBuilder = TxBuilder(boxSelection: boxSelection, outputCandidates: txOutputs, currentHeight: 0, feeAmount: fee, changeAddress: changeAddress)
         txBuilder.setDataInputs(dataInputs: dataInputs)
         let tx = try txBuilder.build()
         XCTAssertNoThrow(try tx.toJsonEIP12())
@@ -251,8 +249,7 @@ final class TransactionTests: XCTestCase {
             outputCandidates: newTxOutputs,
             currentHeight: UInt32(0),
             feeAmount: fee,
-            changeAddress: changeAddress,
-            minChangeValue: minChangeValue
+            changeAddress: changeAddress
         )
         XCTAssertNoThrow(try newTxBuilder.build())
     }
@@ -291,8 +288,7 @@ final class TransactionTests: XCTestCase {
             outputCandidates: txOutputs,
             currentHeight: 0,
             feeAmount: fee,
-            changeAddress: changeAddress,
-            minChangeValue: minChangeValue
+            changeAddress: changeAddress
         )
         txBuilder.setDataInputs(dataInputs: dataInputs)
         let tx = try txBuilder.build()
@@ -356,8 +352,7 @@ final class TransactionTests: XCTestCase {
             outputCandidates: txOutputs,
             currentHeight: UInt32(0),
             feeAmount: fee,
-            changeAddress: changeAddress,
-            minChangeValue: minChangeValue
+            changeAddress: changeAddress
         )
         let tx = try txBuilder.build()
         let txDataInputs = try ErgoBoxes.init(fromJSON: [])
diff --git a/bindings/ergo-lib-wasm/src/tx_builder.rs b/bindings/ergo-lib-wasm/src/tx_builder.rs
index 659747e0a..24496b2d6 100644
--- a/bindings/ergo-lib-wasm/src/tx_builder.rs
+++ b/bindings/ergo-lib-wasm/src/tx_builder.rs
@@ -34,7 +34,6 @@ impl TxBuilder {
     /// `current_height` - chain height that will be used in additionally created boxes (change, miner's fee, etc.),
     /// `fee_amount` - miner's fee,
     /// `change_address` - change (inputs - outputs) will be sent to this address,
-    /// `min_change_value` - minimal value of the change to be sent to `change_address`, value less than that
     /// will be given to miners,
     pub fn new(
         box_selection: &BoxSelection,
@@ -42,7 +41,6 @@ impl TxBuilder {
         current_height: u32,
         fee_amount: &BoxValue,
         change_address: &Address,
-        min_change_value: &BoxValue,
     ) -> TxBuilder {
         TxBuilder(ergo_lib::wallet::tx_builder::TxBuilder::new(
             box_selection.clone().into(),
@@ -50,7 +48,6 @@ impl TxBuilder {
             current_height,
             fee_amount.clone().into(),
             change_address.clone().into(),
-            min_change_value.clone().into(),
         ))
     }
 
@@ -115,9 +112,4 @@ impl TxBuilder {
     pub fn change_address(&self) -> Address {
         self.0.change_address().into()
     }
-
-    /// Get min change value
-    pub fn min_change_value(&self) -> BoxValue {
-        self.0.min_change_value().into()
-    }
 }
diff --git a/ergo-lib/src/wallet/tx_builder.rs b/ergo-lib/src/wallet/tx_builder.rs
index b93ac5f94..26cacc9fc 100644
--- a/ergo-lib/src/wallet/tx_builder.rs
+++ b/ergo-lib/src/wallet/tx_builder.rs
@@ -43,7 +43,6 @@ pub struct TxBuilder<S: ErgoBoxAssets> {
     current_height: u32,
     fee_amount: BoxValue,
     change_address: Address,
-    min_change_value: BoxValue,
     context_extensions: HashMap<BoxId, ContextExtension>,
     token_burn_permit: Vec<Token>,
 }
@@ -55,7 +54,6 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
     /// `current_height` - chain height that will be used in additionally created boxes (change, miner's fee, etc.),
     /// `fee_amount` - miner's fee (higher values will speed up inclusion in blocks),
     /// `change_address` - change (inputs - outputs) will be sent to this address,
-    /// `min_change_value` - minimal value of the change to be sent to `change_address`, value less than that
     /// will be given to miners,
     pub fn new(
         box_selection: BoxSelection<S>,
@@ -63,7 +61,6 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
         current_height: u32,
         fee_amount: BoxValue,
         change_address: Address,
-        min_change_value: BoxValue,
     ) -> TxBuilder<S> {
         TxBuilder {
             box_selection,
@@ -72,7 +69,6 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
             current_height,
             fee_amount,
             change_address,
-            min_change_value,
             context_extensions: HashMap::new(),
             token_burn_permit: Vec::new(),
         }
@@ -108,11 +104,6 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
         self.change_address.clone()
     }
 
-    /// Get min change value
-    pub fn min_change_value(&self) -> BoxValue {
-        self.min_change_value
-    }
-
     /// Set transaction's data inputs
     pub fn set_data_inputs(&mut self, data_inputs: Vec<DataInput>) {
         self.data_inputs = data_inputs;
@@ -149,7 +140,10 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
 
     fn build_tx(&self) -> Result<UnsignedTransaction, TxBuilderError> {
         if self.box_selection.boxes.is_empty() {
-            return Err(TxBuilderError::InvalidArgs("inputs is empty".to_string()));
+            return Err(TxBuilderError::InvalidArgs("inputs are empty".to_string()));
+        }
+        if self.output_candidates.is_empty() {
+            return Err(TxBuilderError::InvalidArgs("outputs are empty".to_string()));
         }
         if self.box_selection.boxes.len() > u16::MAX as usize {
             return Err(TxBuilderError::InvalidArgs("too many inputs".to_string()));
@@ -180,7 +174,6 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
             .box_selection
             .change_boxes
             .iter()
-            .filter(|b| b.value >= self.min_change_value)
             .map(|b| {
                 let mut candidate = ErgoBoxCandidateBuilder::new(
                     b.value,
@@ -195,27 +188,26 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
             .collect();
         output_candidates.append(&mut change_boxes?);
 
-        // Ergo transactions need at least one output
-        // but that single output could just be the change (ex: if you want to send all ERG to a single address)
-        if output_candidates.is_empty() {
-            return Err(TxBuilderError::InvalidArgs(
-                "output_candidates is empty".to_string(),
-            ));
-        }
         // 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() > Transaction::MAX_OUTPUTS_COUNT {
             return Err(TxBuilderError::InvalidArgs("too many outputs".to_string()));
         }
-        // check that inputs have enough coins
+        // check input's coins preservation
         let total_input_value = sum_value(self.box_selection.boxes.as_slice());
         let total_output_value = sum_value(output_candidates.as_slice());
+        #[allow(clippy::comparison_chain)]
         if total_output_value > total_input_value {
-            return Err(TxBuilderError::NotEnoughCoins(
+            return Err(TxBuilderError::NotEnoughCoinsInInputs(
                 total_output_value - total_input_value,
             ));
+        } else if total_output_value < total_input_value {
+            return Err(TxBuilderError::NotEnoughCoinsInOutputs(
+                total_input_value - total_output_value,
+            ));
         }
+
         // check that inputs have enough tokens
         let input_tokens = sum_tokens_from_boxes(self.box_selection.boxes.as_slice())
             .map_err(TxBuilderError::TooManyTokensInInputBoxes)?;
@@ -305,7 +297,7 @@ pub enum TxBuilderError {
     #[error("Not enougn tokens: {0:?}")]
     NotEnoughTokens(Vec<Token>),
     #[error("Not enough coins({0} nanoERGs are missing)")]
-    NotEnoughCoins(u64),
+    NotEnoughCoinsInInputs(u64),
     #[error("Transaction serialization failed: {0}")]
     SerializationError(#[from] SigmaSerializationError),
     #[error("Invalid tx inputs count: {0}")]
@@ -326,6 +318,8 @@ pub enum TxBuilderError {
     TooManyTokensInOutputCandidates(TokenAmountError),
     #[error("Tokens in output candidate exceed tokens in input boxes: {0}")]
     TokensInOutputsExceedInputs(TokenAmountError),
+    #[error("Coins in outputs are less than coins in inputs for {0} nanoERGs")]
+    NotEnoughCoinsInOutputs(u64),
 }
 
 /// Sums up the tokens into a hash map
@@ -421,9 +415,8 @@ mod tests {
             1,
             force_any_val::<BoxValue>(),
             force_any_val::<Address>(),
-            BoxValue::SAFE_USER_MIN,
         );
-        assert!(r.build().is_err(), "error on duplicate inputs");
+        assert!(matches!(r.build(), Err(TxBuilderError::InvalidArgs(_))));
     }
 
     #[test]
@@ -438,9 +431,8 @@ mod tests {
             1,
             force_any_val::<BoxValue>(),
             force_any_val::<Address>(),
-            BoxValue::SAFE_USER_MIN,
         );
-        assert!(r.build().is_err(), "error on empty inputs");
+        assert!(matches!(r.build(), Err(TxBuilderError::InvalidArgs(_))));
     }
 
     #[test]
@@ -481,10 +473,8 @@ mod tests {
             0,
             tx_fee,
             force_any_val::<Address>(),
-            BoxValue::SAFE_USER_MIN,
         );
         let res = tx_builder.build();
-        assert!(res.is_err(), "error on burn token without permit");
         assert_eq!(
             res,
             Err(TxBuilderError::TokenBurnPermitMissing {
@@ -531,7 +521,6 @@ mod tests {
             0,
             tx_fee,
             force_any_val::<Address>(),
-            BoxValue::SAFE_USER_MIN,
         );
         let token_burn_permit = Token {
             amount: 5.try_into().unwrap(),
@@ -539,7 +528,6 @@ mod tests {
         };
         tx_builder.set_token_burn_permit(vec![token_burn_permit.clone()]);
         let res = tx_builder.build();
-        assert!(res.is_err());
         assert_eq!(
             res,
             Err(TxBuilderError::TokenBurnPermitExceeded {
@@ -587,7 +575,6 @@ mod tests {
             0,
             tx_fee,
             force_any_val::<Address>(),
-            BoxValue::SAFE_USER_MIN,
         );
         tx_builder.set_token_burn_permit(vec![token_to_burn]);
         let _ = tx_builder.build().unwrap();
@@ -630,11 +617,9 @@ mod tests {
             0,
             tx_fee,
             force_any_val::<Address>(),
-            BoxValue::SAFE_USER_MIN,
         );
         tx_builder.set_token_burn_permit(vec![token_to_burn.clone()]);
         let res = tx_builder.build();
-        assert!(res.is_err());
         assert_eq!(
             res,
             Err(TxBuilderError::TokenBurnPermitUnused {
@@ -682,7 +667,6 @@ mod tests {
             0,
             tx_fee,
             force_any_val::<Address>(),
-            BoxValue::SAFE_USER_MIN,
         );
         let tx = tx_builder.build().unwrap();
         assert_eq!(
@@ -711,7 +695,7 @@ mod tests {
         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);
+        box_builder.add_token(token_pair.clone());
         let out_box = box_builder.build().unwrap();
         let inputs: Vec<ErgoBox> = vec![input_box];
         let tx_fee = BoxValue::SAFE_USER_MIN;
@@ -726,25 +710,70 @@ mod tests {
             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"
+        assert_eq!(
+            tx_builder.build(),
+            Err(TxBuilderError::NotEnoughTokens(vec![token_pair])),
         );
     }
 
     #[test]
-    fn test_balance_error() {
+    fn test_balance_error_not_enough_inputs() {
         let input_box = force_any_val_with::<ErgoBox>(
             (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
         );
+        // tx fee on top of this leads to overspending
+        let out_box_value = input_box.value();
+        let mut box_builder =
+            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
+        input_box.tokens.iter().for_each(|tokens| {
+            tokens.iter().for_each(|t| {
+                box_builder.add_token(t.clone());
+            })
+        });
+        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.try_into().unwrap(),
+            change_boxes: vec![],
+        };
+        let outputs = vec![out_box];
+        let tx_builder = TxBuilder::new(
+            box_selection,
+            outputs,
+            0,
+            tx_fee,
+            force_any_val::<Address>(),
+        );
+        assert_eq!(
+            tx_builder.build(),
+            Err(TxBuilderError::NotEnoughCoinsInInputs(
+                *BoxValue::SAFE_USER_MIN.as_u64()
+            )),
+        );
+    }
+
+    #[test]
+    fn test_balance_error_not_enough_outputs() {
+        let input_box = force_any_val_with::<ErgoBox>(
+            (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
+        );
+        // spend not all inputs
         let out_box_value = input_box
             .value()
-            .checked_add(&BoxValue::SAFE_USER_MIN)
+            // goes to tx fee
+            .checked_sub(&BoxValue::SAFE_USER_MIN)
+            .unwrap()
+            .checked_sub(&BoxValue::SAFE_USER_MIN)
             .unwrap();
-        let box_builder =
+        let mut box_builder =
             ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
+        input_box.tokens.iter().for_each(|tokens| {
+            tokens.iter().for_each(|t| {
+                box_builder.add_token(t.clone());
+            })
+        });
         let out_box = box_builder.build().unwrap();
         let inputs: Vec<ErgoBox> = vec![input_box];
         let tx_fee = BoxValue::SAFE_USER_MIN;
@@ -759,11 +788,12 @@ mod tests {
             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"
+        assert_eq!(
+            tx_builder.build(),
+            Err(TxBuilderError::NotEnoughCoinsInOutputs(
+                *BoxValue::SAFE_USER_MIN.as_u64()
+            )),
         );
     }
 
@@ -794,7 +824,6 @@ mod tests {
             0,
             tx_fee,
             force_any_val::<Address>(),
-            BoxValue::SAFE_USER_MIN,
         );
         assert!(tx_builder.estimate_tx_size_bytes().unwrap() > 0);
     }
@@ -811,7 +840,6 @@ mod tests {
                          data_inputs in vec(any::<DataInput>(), 0..2),
                          ctx_ext in any::<ContextExtension>()) {
             prop_assume!(sum_tokens_from_boxes(outputs.as_slice()).unwrap().is_empty());
-            let min_change_value = BoxValue::SAFE_USER_MIN;
             let all_outputs = checked_sum(outputs.iter().map(|b| b.value)).unwrap()
                 .checked_add(&miners_fee)
                 .unwrap();
@@ -827,7 +855,6 @@ mod tests {
                 1,
                 miners_fee,
                 change_address.clone(),
-                min_change_value,
             );
             tx_builder.set_data_inputs(data_inputs.clone());
             tx_builder.set_context_extension(selection.boxes.first().box_id(), ctx_ext.clone());


Test patch:
diff --git a/bindings/ergo-lib-wasm/tests/test_builder_i590.js b/bindings/ergo-lib-wasm/tests/test_builder_i590.js
index ad61f202a..8c88bcc51 100644
--- a/bindings/ergo-lib-wasm/tests/test_builder_i590.js
+++ b/bindings/ergo-lib-wasm/tests/test_builder_i590.js
@@ -2634,7 +2634,6 @@ it('i590 accidental token burn in change box', async () => {
     outputs,
     height,
     fee,
-    changeAddress,
-    sigmaRust.BoxValue.SAFE_USER_MIN()
+    changeAddress
   ).build();
 });
diff --git a/bindings/ergo-lib-wasm/tests/test_transaction.js b/bindings/ergo-lib-wasm/tests/test_transaction.js
index 07e5dbfd4..d48675b7d 100644
--- a/bindings/ergo-lib-wasm/tests/test_transaction.js
+++ b/bindings/ergo-lib-wasm/tests/test_transaction.js
@@ -29,12 +29,11 @@ it('TxBuilder test', async () => {
   const tx_outputs = new ergo_wasm.ErgoBoxCandidates(outbox);
   const fee = ergo_wasm.TxBuilder.SUGGESTED_TX_FEE();
   const change_address = ergo_wasm.Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
-  const min_change_value = ergo_wasm.BoxValue.SAFE_USER_MIN();
   const data_inputs = new ergo_wasm.DataInputs();
   const box_selector = new ergo_wasm.SimpleBoxSelector();
   const target_balance = ergo_wasm.BoxValue.from_i64(outbox_value.as_i64().checked_add(fee.as_i64()));
   const box_selection = box_selector.select(unspent_boxes, target_balance, new ergo_wasm.Tokens());
-  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
+  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address);
   tx_builder.set_data_inputs(data_inputs);
   const tx = tx_builder.build();
   assert(tx != null);
@@ -54,11 +53,10 @@ it('sign transaction', async () => {
   const tx_outputs = new ergo_wasm.ErgoBoxCandidates(outbox);
   const fee = ergo_wasm.TxBuilder.SUGGESTED_TX_FEE();
   const change_address = ergo_wasm.Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
-  const min_change_value = ergo_wasm.BoxValue.SAFE_USER_MIN();
   const box_selector = new ergo_wasm.SimpleBoxSelector();
   const target_balance = ergo_wasm.BoxValue.from_i64(outbox_value.as_i64().checked_add(fee.as_i64()));
   const box_selection = box_selector.select(unspent_boxes, target_balance, new ergo_wasm.Tokens());
-  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
+  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address);
   const tx = tx_builder.build();
   // smoke test for to_js_eip12
   assert(tx.to_js_eip12() != null);
@@ -102,9 +100,8 @@ it('TxBuilder mint token test', async () => {
   const outbox = box_builder.build();
   const tx_outputs = new ergo_wasm.ErgoBoxCandidates(outbox);
   const change_address = ergo_wasm.Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
-  const min_change_value = ergo_wasm.BoxValue.SAFE_USER_MIN();
   const data_inputs = new ergo_wasm.DataInputs();
-  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
+  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address);
   tx_builder.set_data_inputs(data_inputs);
   const tx = tx_builder.build();
   assert(tx != null);
@@ -149,9 +146,8 @@ it('TxBuilder burn token test', async () => {
   const outbox = box_builder.build();
   const tx_outputs = new ergo_wasm.ErgoBoxCandidates(outbox);
   const change_address = ergo_wasm.Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
-  const min_change_value = ergo_wasm.BoxValue.SAFE_USER_MIN();
   const data_inputs = new ergo_wasm.DataInputs();
-  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
+  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address);
   tx_builder.set_data_inputs(data_inputs);
   tx_builder.set_token_burn_permit(tokens);
   const tx = tx_builder.build();
@@ -172,11 +168,10 @@ it('use signed tx outputs as inputs in a new tx', async () => {
   const tx_outputs = new ergo_wasm.ErgoBoxCandidates(outbox);
   const fee = ergo_wasm.TxBuilder.SUGGESTED_TX_FEE();
   const change_address = ergo_wasm.Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
-  const min_change_value = ergo_wasm.BoxValue.SAFE_USER_MIN();
   const box_selector = new ergo_wasm.SimpleBoxSelector();
   const target_balance = ergo_wasm.BoxValue.from_i64(outbox_value.as_i64().checked_add(fee.as_i64()));
   const box_selection = box_selector.select(unspent_boxes, target_balance, new ergo_wasm.Tokens());
-  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
+  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address);
   const tx = tx_builder.build();
   const tx_data_inputs = ergo_wasm.ErgoBoxes.from_boxes_json([]);
   const block_headers = generate_block_headers();
@@ -193,7 +188,7 @@ it('use signed tx outputs as inputs in a new tx', async () => {
   const new_box_selection = box_selector.select(signed_tx.outputs(), new_target_balance, new ergo_wasm.Tokens());
   const new_outbox = new ergo_wasm.ErgoBoxCandidateBuilder(new_outbox_value, contract, 0).build();
   const new_tx_outputs = new ergo_wasm.ErgoBoxCandidates(new_outbox);
-  const new_tx_builder = ergo_wasm.TxBuilder.new(new_box_selection, new_tx_outputs, 0, fee, change_address, min_change_value);
+  const new_tx_builder = ergo_wasm.TxBuilder.new(new_box_selection, new_tx_outputs, 0, fee, change_address);
   const new_tx = new_tx_builder.build();
   assert(new_tx != null);
 });
@@ -218,12 +213,11 @@ it('Transaction::from_unsigned_tx test', async () => {
   const tx_outputs = new ergo_wasm.ErgoBoxCandidates(outbox);
   const fee = ergo_wasm.TxBuilder.SUGGESTED_TX_FEE();
   const change_address = ergo_wasm.Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
-  const min_change_value = ergo_wasm.BoxValue.SAFE_USER_MIN();
   const data_inputs = new ergo_wasm.DataInputs();
   const box_selector = new ergo_wasm.SimpleBoxSelector();
   const target_balance = ergo_wasm.BoxValue.from_i64(outbox_value.as_i64().checked_add(fee.as_i64()));
   const box_selection = box_selector.select(unspent_boxes, target_balance, new ergo_wasm.Tokens());
-  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
+  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address);
   tx_builder.set_data_inputs(data_inputs);
   const tx = tx_builder.build();
   assert(tx != null);
@@ -251,11 +245,10 @@ it('signing multi signature transaction', async () => {
   const tx_outputs = new ergo_wasm.ErgoBoxCandidates(outbox);
   const fee = ergo_wasm.TxBuilder.SUGGESTED_TX_FEE();
   const change_address = ergo_wasm.Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
-  const min_change_value = ergo_wasm.BoxValue.SAFE_USER_MIN();
   const box_selector = new ergo_wasm.SimpleBoxSelector();
   const target_balance = ergo_wasm.BoxValue.from_i64(outbox_value.as_i64().checked_add(fee.as_i64()));
   const box_selection = box_selector.select(unspent_boxes, target_balance, new ergo_wasm.Tokens());
-  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
+  const tx_builder = ergo_wasm.TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address);
   const tx = tx_builder.build();
   const tx_data_inputs = ergo_wasm.ErgoBoxes.from_boxes_json([]);
   const block_headers = generate_block_headers();