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

Issue:
`TransactionContext::get_boxes_to_spend` is dangerous
Since the order of the tx inputs could be different we should eliminate `TransactionContext::get_boxes_to_spend()` and introduce `TransactionContext::get_input_box(box_id: BoxId) -> Option<ErgoBox>`.

Besides that, we should move `TransactionContext` to a submodule `tx_context` but leave the export `pub use tx_context::TransactionContext` in the `signing` module to not break the existing imports. So that no function in `signing` would have the access to private `TransactionContext::boxes_to_spend` property.

While we are at it let's change `TransactionContext::new(...)` to accept `Vec` for inputs and do the check here (see call sites from Wasm layer for the unwanted boilerplate).


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-c-core/src/reduced.rs b/bindings/ergo-lib-c-core/src/reduced.rs
index 9712c20ca..61580e08a 100644
--- a/bindings/ergo-lib-c-core/src/reduced.rs
+++ b/bindings/ergo-lib-c-core/src/reduced.rs
@@ -2,7 +2,7 @@
 //! is augmented with ReducedInput which contains a script reduction result.
 
 use ergo_lib::{
-    chain::transaction::{reduced::reduce_tx, TxIoVec},
+    chain::transaction::reduced::reduce_tx,
     ergotree_ir::{serialization::SigmaSerializable, sigma_protocol::sigma_boolean::SigmaBoolean},
 };
 
@@ -70,16 +70,8 @@ pub unsafe fn reduced_tx_from_unsigned_tx(
     let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
     let boxes_to_spend = const_ptr_as_ref(boxes_to_spend_ptr, "boxes_to_spend_ptr")?;
     let reduced_tx_out = mut_ptr_as_mut(reduced_tx_out, "reduced_tx_out")?;
-    let boxes_to_spend =
-        TxIoVec::from_vec(boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect())?;
-    let data_boxes = {
-        let d: Vec<_> = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
-        if d.is_empty() {
-            None
-        } else {
-            Some(TxIoVec::from_vec(d)?)
-        }
-    };
+    let boxes_to_spend = boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect();
+    let data_boxes = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
     let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
         unsigned_tx.0.clone(),
         boxes_to_spend,
diff --git a/bindings/ergo-lib-c-core/src/transaction.rs b/bindings/ergo-lib-c-core/src/transaction.rs
index db3653ee4..e34e83efa 100644
--- a/bindings/ergo-lib-c-core/src/transaction.rs
+++ b/bindings/ergo-lib-c-core/src/transaction.rs
@@ -3,7 +3,7 @@
 use std::convert::{TryFrom, TryInto};
 
 use ergo_lib::{
-    chain::{self, transaction::TxIoVec},
+    chain,
     ergo_chain_types::{Base16DecodedBytes, Base16EncodedBytes},
 };
 
@@ -145,15 +145,8 @@ pub unsafe fn transaction_extract_hints(
     let transaction_hints_bag_out =
         mut_ptr_as_mut(transaction_hints_bag_out, "transaction_hints_bag_out")?;
 
-    let boxes_to_spend =
-        TxIoVec::from_vec(boxes_to_spend.0.clone().into_iter().map(|x| x.0).collect())?;
-    let data_boxes = if !data_boxes.0.is_empty() {
-        Some(TxIoVec::from_vec(
-            data_boxes.0.clone().into_iter().map(|x| x.0).collect(),
-        )?)
-    } else {
-        None
-    };
+    let boxes_to_spend = boxes_to_spend.0.clone().into_iter().map(|x| x.0).collect();
+    let data_boxes = data_boxes.0.clone().into_iter().map(|x| x.0).collect();
     let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
         signed_transaction.0.clone(),
         boxes_to_spend,
diff --git a/bindings/ergo-lib-c-core/src/wallet.rs b/bindings/ergo-lib-c-core/src/wallet.rs
index 6b6e24a6e..07cd67538 100644
--- a/bindings/ergo-lib-c-core/src/wallet.rs
+++ b/bindings/ergo-lib-c-core/src/wallet.rs
@@ -1,8 +1,6 @@
 //! Wallet-like features
 
-use ergo_lib::{
-    chain::transaction::TxIoVec, ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean,
-};
+use ergo_lib::ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean;
 
 use crate::{
     address::{Address, ConstAddressPtr},
@@ -83,16 +81,8 @@ pub unsafe fn wallet_sign_transaction(
     let boxes_to_spend = const_ptr_as_ref(boxes_to_spend_ptr, "boxes_to_spend_ptr")?;
     let data_boxes = const_ptr_as_ref(data_boxes_ptr, "data_boxes_ptr")?;
     let transaction_out = mut_ptr_as_mut(transaction_out, "transaction_out")?;
-    let boxes_to_spend =
-        TxIoVec::from_vec(boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect())?;
-    let data_boxes = {
-        let d: Vec<_> = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
-        if d.is_empty() {
-            None
-        } else {
-            Some(TxIoVec::from_vec(d)?)
-        }
-    };
+    let boxes_to_spend = boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect();
+    let data_boxes = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
     let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
         unsigned_tx.0.clone(),
         boxes_to_spend,
@@ -122,16 +112,8 @@ pub unsafe fn wallet_sign_transaction_multi(
     let data_boxes = const_ptr_as_ref(data_boxes_ptr, "data_boxes_ptr")?;
     let tx_hints = const_ptr_as_ref(tx_hints_ptr, "tx_hints_ptr")?;
     let transaction_out = mut_ptr_as_mut(transaction_out, "transaction_out")?;
-    let boxes_to_spend =
-        TxIoVec::from_vec(boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect())?;
-    let data_boxes = {
-        let d: Vec<_> = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
-        if d.is_empty() {
-            None
-        } else {
-            Some(TxIoVec::from_vec(d)?)
-        }
-    };
+    let boxes_to_spend = boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect();
+    let data_boxes = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
     let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
         unsigned_tx.0.clone(),
         boxes_to_spend,
@@ -195,16 +177,8 @@ pub unsafe fn wallet_generate_commitments(
     let boxes_to_spend = const_ptr_as_ref(boxes_to_spend_ptr, "boxes_to_spend_ptr")?;
     let data_boxes = const_ptr_as_ref(data_boxes_ptr, "data_boxes_ptr")?;
     let transaction_hints_bag_out = mut_ptr_as_mut(transaction_hints_bag_out, "transaction_out")?;
-    let boxes_to_spend =
-        TxIoVec::from_vec(boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect())?;
-    let data_boxes = {
-        let d: Vec<_> = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
-        if d.is_empty() {
-            None
-        } else {
-            Some(TxIoVec::from_vec(d)?)
-        }
-    };
+    let boxes_to_spend = boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect();
+    let data_boxes = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
     let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
         unsigned_tx.0.clone(),
         boxes_to_spend,
diff --git a/bindings/ergo-lib-wasm/src/transaction.rs b/bindings/ergo-lib-wasm/src/transaction.rs
index f5452ac13..ae0e2e621 100644
--- a/bindings/ergo-lib-wasm/src/transaction.rs
+++ b/bindings/ergo-lib-wasm/src/transaction.rs
@@ -22,7 +22,6 @@ use crate::ergo_state_ctx::ErgoStateContext;
 use crate::transaction::reduced::Propositions;
 use derive_more::{From, Into};
 use ergo_lib::ergo_chain_types::{Base16DecodedBytes, Base16EncodedBytes, Digest32};
-use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox;
 
 pub mod reduced;
 
@@ -106,16 +105,13 @@ pub fn extract_hints(
     signed_transaction: Transaction,
     state_context: &ErgoStateContext,
     boxes_to_spend: &ErgoBoxes,
-    _data_boxes: &ErgoBoxes,
+    data_boxes: &ErgoBoxes,
     real_propositions: Propositions,
     simulated_propositions: Propositions,
 ) -> Result<TransactionHintsBag, JsValue> {
-    let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend.clone().into()).map_err(to_js)?;
-    let mut data_boxes: Option<TxIoVec<ErgoBox>> = None;
+    let boxes_to_spend = boxes_to_spend.clone().into();
 
-    if !_data_boxes.0.is_empty() {
-        data_boxes = Some(TxIoVec::from_vec(_data_boxes.clone().into()).map_err(to_js)?);
-    }
+    let data_boxes = data_boxes.clone().into();
     let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
         signed_transaction.0,
         boxes_to_spend,
diff --git a/bindings/ergo-lib-wasm/src/transaction/reduced.rs b/bindings/ergo-lib-wasm/src/transaction/reduced.rs
index 5b1d82616..3834365c0 100644
--- a/bindings/ergo-lib-wasm/src/transaction/reduced.rs
+++ b/bindings/ergo-lib-wasm/src/transaction/reduced.rs
@@ -7,7 +7,6 @@ use crate::ergo_state_ctx::ErgoStateContext;
 use crate::error_conversion::to_js;
 
 use ergo_lib::chain::transaction::reduced::reduce_tx;
-use ergo_lib::chain::transaction::TxIoVec;
 use ergo_lib::ergotree_ir::serialization::SigmaSerializable;
 use ergo_lib::ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean;
 use wasm_bindgen::prelude::*;
@@ -53,15 +52,8 @@ impl ReducedTransaction {
         data_boxes: &ErgoBoxes,
         state_context: &ErgoStateContext,
     ) -> Result<ReducedTransaction, JsValue> {
-        let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend.clone().into()).map_err(to_js)?;
-        let data_boxes = {
-            let d: Vec<_> = data_boxes.clone().into();
-            if d.is_empty() {
-                None
-            } else {
-                Some(TxIoVec::from_vec(d).map_err(to_js)?)
-            }
-        };
+        let boxes_to_spend = boxes_to_spend.clone().into();
+        let data_boxes = data_boxes.clone().into();
         let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
             unsigned_tx.0.clone(),
             boxes_to_spend,
diff --git a/bindings/ergo-lib-wasm/src/wallet.rs b/bindings/ergo-lib-wasm/src/wallet.rs
index f46e4bb96..39d5a3486 100644
--- a/bindings/ergo-lib-wasm/src/wallet.rs
+++ b/bindings/ergo-lib-wasm/src/wallet.rs
@@ -1,8 +1,6 @@
 //! Wallet-like features
 use derive_more::{From, Into};
-use ergo_lib::{
-    chain::transaction::TxIoVec, ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean,
-};
+use ergo_lib::ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean;
 use js_sys::Uint8Array;
 use wasm_bindgen::prelude::*;
 
@@ -61,15 +59,8 @@ impl Wallet {
         boxes_to_spend: &ErgoBoxes,
         data_boxes: &ErgoBoxes,
     ) -> Result<Transaction, JsValue> {
-        let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend.clone().into()).map_err(to_js)?;
-        let data_boxes = {
-            let d: Vec<_> = data_boxes.clone().into();
-            if d.is_empty() {
-                None
-            } else {
-                Some(TxIoVec::from_vec(d).map_err(to_js)?)
-            }
-        };
+        let boxes_to_spend = boxes_to_spend.clone().into();
+        let data_boxes = data_boxes.clone().into();
         let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
             tx.0.clone(),
             boxes_to_spend,
@@ -96,15 +87,8 @@ impl Wallet {
         data_boxes: &ErgoBoxes,
         tx_hints: &TransactionHintsBag,
     ) -> Result<Transaction, JsValue> {
-        let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend.clone().into()).map_err(to_js)?;
-        let data_boxes = {
-            let d: Vec<_> = data_boxes.clone().into();
-            if d.is_empty() {
-                None
-            } else {
-                Some(TxIoVec::from_vec(d).map_err(to_js)?)
-            }
-        };
+        let boxes_to_spend = boxes_to_spend.clone().into();
+        let data_boxes = data_boxes.clone().into();
         let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
             tx.0.clone(),
             boxes_to_spend,
@@ -160,15 +144,8 @@ impl Wallet {
         boxes_to_spend: &ErgoBoxes,
         data_boxes: &ErgoBoxes,
     ) -> Result<TransactionHintsBag, JsValue> {
-        let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend.clone().into()).map_err(to_js)?;
-        let data_boxes = {
-            let d: Vec<_> = data_boxes.clone().into();
-            if d.is_empty() {
-                None
-            } else {
-                Some(TxIoVec::from_vec(d).map_err(to_js)?)
-            }
-        };
+        let boxes_to_spend = boxes_to_spend.clone().into();
+        let data_boxes = data_boxes.clone().into();
         let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
             tx.0.clone(),
             boxes_to_spend,
diff --git a/ergo-lib/src/chain/transaction/reduced.rs b/ergo-lib/src/chain/transaction/reduced.rs
index ce50add83..ec357f496 100644
--- a/ergo-lib/src/chain/transaction/reduced.rs
+++ b/ergo-lib/src/chain/transaction/reduced.rs
@@ -74,8 +74,7 @@ pub fn reduce_tx(
         .enumerated()
         .try_mapped::<_, _, TxSigningError>(|(idx, input)| {
             let input_box = tx_context
-                .get_boxes_to_spend()
-                .find(|b| b.box_id() == input.box_id)
+                .get_input_box(&input.box_id)
                 .ok_or(TxSigningError::InputBoxNotFound(idx))?;
             let ctx = Rc::new(make_context(state_context, &tx_context, idx)?);
             let expr = input_box
diff --git a/ergo-lib/src/wallet.rs b/ergo-lib/src/wallet.rs
index 53baf9aaf..b1f76f4de 100644
--- a/ergo-lib/src/wallet.rs
+++ b/ergo-lib/src/wallet.rs
@@ -11,6 +11,7 @@ pub mod multi_sig;
 pub mod secret_key;
 pub mod signing;
 pub mod tx_builder;
+mod tx_context;
 
 use ergotree_interpreter::sigma_protocol::private_input::PrivateInput;
 use ergotree_interpreter::sigma_protocol::prover::Prover;
diff --git a/ergo-lib/src/wallet/multi_sig.rs b/ergo-lib/src/wallet/multi_sig.rs
index 37d368937..3c77b81de 100644
--- a/ergo-lib/src/wallet/multi_sig.rs
+++ b/ergo-lib/src/wallet/multi_sig.rs
@@ -226,8 +226,7 @@ pub fn generate_commitments(
     let mut hints_bag = TransactionHintsBag::empty();
     for (i, input) in tx.inputs.iter().enumerate() {
         let input_box = tx_context
-            .get_boxes_to_spend()
-            .find(|b| b.box_id() == input.box_id)
+            .get_input_box(&input.box_id)
             .ok_or(TxSigningError::InputBoxNotFound(i))?;
         let ctx = Rc::new(make_context(state_context, &tx_context, i)?);
         let tree = input_box.ergo_tree.clone();
@@ -255,14 +254,11 @@ pub fn extract_hints(
     let mut hints_bag = TransactionHintsBag::empty();
     for (i, input) in tx_ctx.spending_tx.inputs.iter().enumerate() {
         let input_box = tx_ctx
-            .get_boxes_to_spend()
-            .find(|b| b.box_id() == input.box_id)
+            .get_input_box(&input.box_id)
             .ok_or(TxSigningError::InputBoxNotFound(i))?;
         let height = state_context.pre_header.height;
         let self_box = tx_ctx
-            .get_boxes_to_spend()
-            .find(|b| b.box_id() == tx_ctx.spending_tx.inputs.as_vec()[i].box_id)
-            .cloned()
+            .get_input_box(&tx_ctx.spending_tx.inputs.as_vec()[i].box_id)
             .ok_or_else(|| {
                 TxSigningError::ContextError("self_index is out of bounds".to_string())
             })?;
@@ -294,10 +290,8 @@ pub fn extract_hints(
             .enumerated()
             .try_mapped(|(idx, u)| {
                 tx_ctx
-                    .boxes_to_spend
-                    .iter()
-                    .find(|b| u == b.box_id())
-                    .map(|b| Rc::new(b.clone()))
+                    .get_input_box(&u)
+                    .map(Rc::new)
                     .ok_or(TxSigningError::InputBoxNotFound(idx))
             })?;
         let extension = tx_ctx
diff --git a/ergo-lib/src/wallet/signing.rs b/ergo-lib/src/wallet/signing.rs
index d935500d1..cd3c351cb 100644
--- a/ergo-lib/src/wallet/signing.rs
+++ b/ergo-lib/src/wallet/signing.rs
@@ -30,6 +30,15 @@ pub enum TxSigningError {
     /// Failed to find an input in `boxes_to_spend`
     #[error("Input box not found (index {0})")]
     InputBoxNotFound(usize),
+    /// Too many input boxes
+    #[error("A maximum of 255 input boxes is allowed (have {0})")]
+    TooManyInputBoxes(usize),
+    /// `boxes_to_spend` is empty
+    #[error("No Input boxes found")]
+    NoInputBoxes,
+    /// Too many data input boxes
+    #[error("A maximum of 255 data input boxes is allowed (have {0})")]
+    TooManyDataInputBoxes(usize),
     /// Failed to find a data input in `data_boxes`
     #[error("Data input box not found (index {0})")]
     DataInputBoxNotFound(usize),
@@ -41,47 +50,7 @@ pub enum TxSigningError {
     SerializationError(#[from] SigmaSerializationError),
 }
 
-/// Transaction and an additional info required for signing
-#[derive(PartialEq, Debug, Clone)]
-pub struct TransactionContext<T: ErgoTransaction> {
-    /// Unsigned transaction to sign
-    pub spending_tx: T,
-    /// Boxes corresponding to [`UnsignedTransaction::inputs`]
-    pub(crate) boxes_to_spend: TxIoVec<ErgoBox>,
-    /// Boxes corresponding to [`UnsignedTransaction::data_inputs`]
-    pub(crate) data_boxes: Option<TxIoVec<ErgoBox>>,
-}
-
-impl<T: ErgoTransaction> TransactionContext<T> {
-    /// New TransactionContext
-    pub fn new(
-        spending_tx: T,
-        boxes_to_spend: TxIoVec<ErgoBox>,
-        data_boxes: Option<TxIoVec<ErgoBox>>,
-    ) -> Result<Self, TxSigningError> {
-        for (i, unsigned_input) in spending_tx.inputs_ids().enumerated() {
-            if !boxes_to_spend.iter().any(|b| unsigned_input == b.box_id()) {
-                return Err(TxSigningError::InputBoxNotFound(i));
-            }
-        }
-        if let Some(data_inputs) = spending_tx.data_inputs().as_ref() {
-            if let Some(data_boxes) = data_boxes.as_ref() {
-                for (i, data_input) in data_inputs.iter().enumerate() {
-                    if !data_boxes.iter().any(|b| data_input.box_id == b.box_id()) {
-                        return Err(TxSigningError::DataInputBoxNotFound(i));
-                    }
-                }
-            } else {
-                return Err(TxSigningError::DataInputBoxNotFound(0));
-            }
-        }
-        Ok(TransactionContext {
-            spending_tx,
-            boxes_to_spend,
-            data_boxes,
-        })
-    }
-}
+pub use super::tx_context::TransactionContext;
 
 /// Exposes common properties for signed and unsigned transactions
 pub trait ErgoTransaction {
@@ -111,13 +80,6 @@ impl ErgoTransaction for Transaction {
     }
 }
 
-impl<T: ErgoTransaction> TransactionContext<T> {
-    /// Get boxes corresponding to [`UnsignedTransaction::inputs`]
-    pub fn get_boxes_to_spend(&self) -> impl Iterator<Item = &ErgoBox> {
-        self.boxes_to_spend.iter()
-    }
-}
-
 /// `self_index` - index of the SELF box in the tx_ctx.spending_tx.inputs
 pub fn make_context(
     state_ctx: &ErgoStateContext,
@@ -128,10 +90,7 @@ pub fn make_context(
 
     // Find self_box by matching BoxIDs
     let self_box = tx_ctx
-        .boxes_to_spend
-        .iter()
-        .find(|b| b.box_id() == tx_ctx.spending_tx.inputs.as_vec()[self_index].box_id)
-        .cloned()
+        .get_input_box(&tx_ctx.spending_tx.inputs.as_vec()[self_index].box_id)
         .ok_or_else(|| TxSigningError::ContextError("self_index is out of bounds".to_string()))?;
 
     let outputs = tx_ctx
@@ -163,10 +122,8 @@ pub fn make_context(
         .enumerated()
         .try_mapped(|(idx, u)| {
             tx_ctx
-                .boxes_to_spend
-                .iter()
-                .find(|b| u == b.box_id())
-                .map(|b| Rc::new(b.clone()))
+                .get_input_box(&u)
+                .map(Rc::new)
                 .ok_or(TxSigningError::InputBoxNotFound(idx))
         })?;
     let extension = tx_ctx
@@ -203,9 +160,7 @@ pub fn sign_transaction(
     let message_to_sign = tx.bytes_to_sign()?;
     let signed_inputs = tx.inputs.enumerated().try_mapped(|(idx, input)| {
         let input_box = tx_context
-            .boxes_to_spend
-            .iter()
-            .find(|b| b.box_id() == input.box_id)
+            .get_input_box(&input.box_id)
             .ok_or(TxSigningError::InputBoxNotFound(idx))?;
         let ctx = Rc::new(make_context(state_context, &tx_context, idx)?);
         let mut hints_bag = HintsBag::empty();
@@ -367,8 +322,7 @@ mod tests {
             let output_candidates = vec![candidate];
             let tx = UnsignedTransaction::new(inputs.try_into().unwrap(),
                 None, output_candidates.try_into().unwrap()).unwrap();
-            let tx_context = TransactionContext { spending_tx: tx,
-                                                  boxes_to_spend: TxIoVec::from_vec(boxes_to_spend.clone()).unwrap(), data_boxes: None };
+            let tx_context = TransactionContext::new(tx, boxes_to_spend.clone(), vec![]).unwrap();
             let tx_hint_bag=TransactionHintsBag::empty();
             let res = sign_transaction(prover.as_ref(), tx_context.clone(), &force_any_val::<ErgoStateContext>(), Some(&tx_hint_bag));
             let signed_tx = res.unwrap();
@@ -415,8 +369,7 @@ mod tests {
           // Reverse boxes for `UnsignedTransaction`
           boxes_to_spend.reverse();
           data_input_boxes.reverse();
-          let data_input_boxes = Some(TxIoVec::from_vec(data_input_boxes.clone()).unwrap());
-          let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend.clone()).unwrap();
+          let boxes_to_spend = boxes_to_spend;
           let spending_tx = UnsignedTransaction::new(
               ut_inputs,
               data_inputs,
diff --git a/ergo-lib/src/wallet/tx_context.rs b/ergo-lib/src/wallet/tx_context.rs
new file mode 100644
index 000000000..8ab95f99f
--- /dev/null
+++ b/ergo-lib/src/wallet/tx_context.rs
@@ -0,0 +1,77 @@
+//! Transaction context
+
+use ergotree_ir::chain::ergo_box::ErgoBox;
+
+use crate::ergotree_ir::chain::ergo_box::BoxId;
+use ergotree_interpreter::eval::context::TxIoVec;
+
+use super::signing::{ErgoTransaction, TxSigningError};
+
+/// Transaction and an additional info required for signing
+#[derive(PartialEq, Debug, Clone)]
+pub struct TransactionContext<T: ErgoTransaction> {
+    /// Unsigned transaction to sign
+    pub spending_tx: T,
+    /// Boxes corresponding to [`crate::chain::transaction::unsigned::UnsignedTransaction::inputs`]
+    boxes_to_spend: TxIoVec<ErgoBox>,
+    /// Boxes corresponding to [`crate::chain::transaction::unsigned::UnsignedTransaction::data_inputs`]
+    pub(crate) data_boxes: Option<TxIoVec<ErgoBox>>,
+}
+
+impl<T: ErgoTransaction> TransactionContext<T> {
+    /// New TransactionContext
+    pub fn new(
+        spending_tx: T,
+        boxes_to_spend: Vec<ErgoBox>,
+        data_boxes: Vec<ErgoBox>,
+    ) -> Result<Self, TxSigningError> {
+        let boxes_to_spend = TxIoVec::from_vec(boxes_to_spend).map_err(|e| match e {
+            bounded_vec::BoundedVecOutOfBounds::LowerBoundError { .. } => {
+                TxSigningError::NoInputBoxes
+            }
+            bounded_vec::BoundedVecOutOfBounds::UpperBoundError { got, .. } => {
+                TxSigningError::TooManyInputBoxes(got)
+            }
+        })?;
+        for (i, unsigned_input) in spending_tx.inputs_ids().enumerated() {
+            if !boxes_to_spend.iter().any(|b| unsigned_input == b.box_id()) {
+                return Err(TxSigningError::InputBoxNotFound(i));
+            }
+        }
+        let data_boxes = if !data_boxes.is_empty() {
+            Some(TxIoVec::from_vec(data_boxes).map_err(|e| match e {
+                bounded_vec::BoundedVecOutOfBounds::LowerBoundError { .. } => unreachable!(),
+                bounded_vec::BoundedVecOutOfBounds::UpperBoundError { got, .. } => {
+                    TxSigningError::TooManyDataInputBoxes(got)
+                }
+            })?)
+        } else {
+            None
+        };
+
+        if let Some(data_inputs) = spending_tx.data_inputs().as_ref() {
+            if let Some(data_boxes) = data_boxes.as_ref() {
+                for (i, data_input) in data_inputs.iter().enumerate() {
+                    if !data_boxes.iter().any(|b| data_input.box_id == b.box_id()) {
+                        return Err(TxSigningError::DataInputBoxNotFound(i));
+                    }
+                }
+            } else {
+                return Err(TxSigningError::DataInputBoxNotFound(0));
+            }
+        }
+        Ok(TransactionContext {
+            spending_tx,
+            boxes_to_spend,
+            data_boxes,
+        })
+    }
+
+    /// Returns box with given id, if it exists.
+    pub fn get_input_box(&self, box_id: &BoxId) -> Option<ErgoBox> {
+        self.boxes_to_spend
+            .iter()
+            .find(|b| b.box_id() == *box_id)
+            .cloned()
+    }
+}


Test patch: