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

Issue:
Expose distributed signatures support in C/Swift bindings
Distributed signatures were implemented in #470

Expose in the public API  commitment generation, tx signing with hints, and hint extraction from signed tx.
In `ergo-lib-c` and `ergo-lib-c-core` crates and in `bindings/ergo-lib-ios` Swift wrapper.


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 21a5a3991..9712c20ca 100644
--- a/bindings/ergo-lib-c-core/src/reduced.rs
+++ b/bindings/ergo-lib-c-core/src/reduced.rs
@@ -1,7 +1,10 @@
 //! Represent `reduced` transaction, i.e. unsigned transaction where each unsigned input
 //! is augmented with ReducedInput which contains a script reduction result.
 
-use ergo_lib::chain::transaction::{reduced::reduce_tx, TxIoVec};
+use ergo_lib::{
+    chain::transaction::{reduced::reduce_tx, TxIoVec},
+    ergotree_ir::{serialization::SigmaSerializable, sigma_protocol::sigma_boolean::SigmaBoolean},
+};
 
 use crate::{
     collections::ConstCollectionPtr,
@@ -12,6 +15,35 @@ use crate::{
     Error,
 };
 
+/// Propositions list(public keys)
+pub struct Propositions(pub(crate) Vec<SigmaBoolean>);
+pub type PropositionsPtr = *mut Propositions;
+pub type ConstPropositionsPtr = *const Propositions;
+
+/// Create empty proposition holder
+pub unsafe fn propositions_new(propositions_out: *mut PropositionsPtr) -> Result<(), Error> {
+    let propositions_out = mut_ptr_as_mut(propositions_out, "propositions_out")?;
+    *propositions_out = Box::into_raw(Box::new(Propositions(vec![])));
+    Ok(())
+}
+
+/// Adding new proposition
+pub unsafe fn propositions_add_proposition_from_bytes(
+    propositions_mut: PropositionsPtr,
+    bytes_ptr: *const u8,
+    len: usize,
+) -> Result<(), Error> {
+    if bytes_ptr.is_null() {
+        return Err(Error::Misc("bytes_ptr is null".into()));
+    }
+    let bytes = std::slice::from_raw_parts(bytes_ptr, len);
+    let propositions_mut = mut_ptr_as_mut(propositions_mut, "propositions_mut")?;
+    propositions_mut
+        .0
+        .push(SigmaBoolean::sigma_parse_bytes(bytes)?);
+    Ok(())
+}
+
 /// Represent `reduced` transaction, i.e. unsigned transaction where each unsigned input
 /// is augmented with ReducedInput which contains a script reduction result.
 /// After an unsigned transaction is reduced it can be signed without context.
diff --git a/bindings/ergo-lib-c-core/src/transaction.rs b/bindings/ergo-lib-c-core/src/transaction.rs
index 27632ad6c..db3653ee4 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,
+    chain::{self, transaction::TxIoVec},
     ergo_chain_types::{Base16DecodedBytes, Base16EncodedBytes},
 };
 
@@ -11,12 +11,166 @@ use crate::{
     collections::{Collection, CollectionPtr, ConstCollectionPtr},
     data_input::DataInput,
     ergo_box::{ErgoBox, ErgoBoxCandidate},
+    ergo_state_ctx::ConstErgoStateContextPtr,
     input::{Input, UnsignedInput},
     json::{TransactionJsonEip12, UnsignedTransactionJsonEip12},
+    reduced::ConstPropositionsPtr,
     util::{const_ptr_as_ref, mut_ptr_as_mut, ByteArray},
     Error,
 };
 
+/// CommitmentHint
+pub struct CommitmentHint(
+    pub(crate) ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::CommitmentHint,
+);
+pub type CommitmentHintPtr = *mut CommitmentHint;
+pub type ConstCommitmentHintPtr = *const CommitmentHint;
+
+/// `HintsBag`
+pub struct HintsBag(
+    pub(crate) ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::HintsBag,
+);
+pub type HintsBagPtr = *mut HintsBag;
+pub type ConstHintsBagPtr = *const HintsBag;
+
+/// Empty HintsBag
+pub unsafe fn hints_bag_empty(hints_bag_out: *mut HintsBagPtr) -> Result<(), Error> {
+    let hints_bag_out = mut_ptr_as_mut(hints_bag_out, "hints_bag_out")?;
+    *hints_bag_out = Box::into_raw(Box::new(HintsBag(
+        ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::HintsBag::empty(),
+    )));
+    Ok(())
+}
+
+/// Add commitment hint to the bag
+pub unsafe fn hints_bag_add_commitment(
+    hints_bag_mut: HintsBagPtr,
+    hint_ptr: ConstCommitmentHintPtr,
+) -> Result<(), Error> {
+    let hints_bag_mut = mut_ptr_as_mut(hints_bag_mut, "hints_bag_mut")?;
+    let hint = const_ptr_as_ref(hint_ptr, "hint_ptr")?;
+    hints_bag_mut.0.add_hint(
+        ergo_lib::ergotree_interpreter::sigma_protocol::prover::hint::Hint::CommitmentHint(
+            hint.0.clone(),
+        ),
+    );
+    Ok(())
+}
+
+/// Length of HintsBag
+pub unsafe fn hints_bag_len(hints_bag_ptr: ConstHintsBagPtr) -> Result<usize, Error> {
+    let hints_bag = const_ptr_as_ref(hints_bag_ptr, "hints_bag_ptr")?;
+    Ok(hints_bag.0.hints.len())
+}
+
+/// Get commitment
+pub unsafe fn hints_bag_get(
+    hints_bag_ptr: ConstHintsBagPtr,
+    index: usize,
+    hint_out: *mut CommitmentHintPtr,
+) -> Result<bool, Error> {
+    let hints_bag = const_ptr_as_ref(hints_bag_ptr, "hints_bag_ptr")?;
+    let hint_out = mut_ptr_as_mut(hint_out, "hint_out")?;
+    if let Some(commitment) = hints_bag.0.commitments().get(index) {
+        *hint_out = Box::into_raw(Box::new(CommitmentHint(commitment.clone())));
+        return Ok(true);
+    }
+    Ok(false)
+}
+
+/// TransactionHintsBag
+pub struct TransactionHintsBag(pub(crate) ergo_lib::wallet::multi_sig::TransactionHintsBag);
+pub type TransactionHintsBagPtr = *mut TransactionHintsBag;
+pub type ConstTransactionHintsBagPtr = *const TransactionHintsBag;
+
+/// Empty TransactionHintsBag
+pub unsafe fn transaction_hints_bag_empty(
+    transaction_hints_bag_out: *mut TransactionHintsBagPtr,
+) -> Result<(), Error> {
+    let transaction_hints_bag_out =
+        mut_ptr_as_mut(transaction_hints_bag_out, "transaction_hints_bag_out")?;
+    *transaction_hints_bag_out = Box::into_raw(Box::new(TransactionHintsBag(
+        ergo_lib::wallet::multi_sig::TransactionHintsBag::empty(),
+    )));
+    Ok(())
+}
+
+/// Adding hints for input
+pub unsafe fn transaction_hints_bag_add_hints_for_input(
+    transaction_hints_bag_mut: TransactionHintsBagPtr,
+    index: usize,
+    hints_bag_ptr: ConstHintsBagPtr,
+) -> Result<(), Error> {
+    let transaction_hints_bag_mut =
+        mut_ptr_as_mut(transaction_hints_bag_mut, "transaction_hints_bag_mut")?;
+    let hints_bag = const_ptr_as_ref(hints_bag_ptr, "hints_bag_ptr")?;
+    transaction_hints_bag_mut
+        .0
+        .add_hints_for_input(index, hints_bag.0.clone());
+    Ok(())
+}
+
+/// Get HintsBag corresponding to input index
+pub unsafe fn transaction_hints_bag_all_hints_for_input(
+    transaction_hints_bag_ptr: ConstTransactionHintsBagPtr,
+    index: usize,
+    hints_bag_out: *mut HintsBagPtr,
+) -> Result<(), Error> {
+    let transaction_hints_bag =
+        const_ptr_as_ref(transaction_hints_bag_ptr, "transaction_hints_bag_ptr")?;
+    let hints_bag_out = mut_ptr_as_mut(hints_bag_out, "hints_bag_out")?;
+    *hints_bag_out = Box::into_raw(Box::new(HintsBag(
+        transaction_hints_bag.0.all_hints_for_input(index),
+    )));
+    Ok(())
+}
+
+/// Extract hints from signed transaction
+pub unsafe fn transaction_extract_hints(
+    signed_transaction_ptr: ConstTransactionPtr,
+    state_context_ptr: ConstErgoStateContextPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    real_propositions_ptr: ConstPropositionsPtr,
+    simulated_propositions_ptr: ConstPropositionsPtr,
+    transaction_hints_bag_out: *mut TransactionHintsBagPtr,
+) -> Result<(), Error> {
+    let signed_transaction = const_ptr_as_ref(signed_transaction_ptr, "signed_transaction_ptr")?;
+    let state_context = const_ptr_as_ref(state_context_ptr, "state_context_ptr")?;
+    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 real_propositions = const_ptr_as_ref(real_propositions_ptr, "real_propositions_ptr")?;
+    let simulated_propositions =
+        const_ptr_as_ref(simulated_propositions_ptr, "simulated_propositions_ptr")?;
+    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 tx_context = ergo_lib::wallet::signing::TransactionContext::new(
+        signed_transaction.0.clone(),
+        boxes_to_spend,
+        data_boxes,
+    )?;
+
+    *transaction_hints_bag_out = Box::into_raw(Box::new(TransactionHintsBag(
+        ergo_lib::wallet::multi_sig::extract_hints(
+            &tx_context,
+            &state_context.0.clone(),
+            real_propositions.0.clone(),
+            simulated_propositions.0.clone(),
+        )?,
+    )));
+    Ok(())
+}
+
 /// Unsigned (inputs without proofs) transaction
 #[derive(PartialEq, Debug, Clone)]
 pub struct UnsignedTransaction(pub(crate) chain::transaction::unsigned::UnsignedTransaction);
diff --git a/bindings/ergo-lib-c-core/src/wallet.rs b/bindings/ergo-lib-c-core/src/wallet.rs
index 9b6958517..870b1f44d 100644
--- a/bindings/ergo-lib-c-core/src/wallet.rs
+++ b/bindings/ergo-lib-c-core/src/wallet.rs
@@ -8,7 +8,10 @@ use crate::{
     ergo_state_ctx::ConstErgoStateContextPtr,
     reduced::ConstReducedTransactionPtr,
     secret_key::SecretKey,
-    transaction::{ConstUnsignedTransactionPtr, Transaction, TransactionPtr},
+    transaction::{
+        ConstTransactionHintsBagPtr, ConstUnsignedTransactionPtr, Transaction, TransactionHintsBag,
+        TransactionHintsBagPtr, TransactionPtr,
+    },
     util::{const_ptr_as_ref, mut_ptr_as_mut},
     Error,
 };
@@ -99,6 +102,45 @@ pub unsafe fn wallet_sign_transaction(
     Ok(())
 }
 
+/// Signs a multi signature transaction
+pub unsafe fn wallet_sign_transaction_multi(
+    wallet_ptr: ConstWalletPtr,
+    state_context_ptr: ConstErgoStateContextPtr,
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    tx_hints_ptr: ConstTransactionHintsBagPtr,
+    transaction_out: *mut TransactionPtr,
+) -> Result<(), Error> {
+    let wallet = const_ptr_as_ref(wallet_ptr, "wallet_ptr")?;
+    let state_context = const_ptr_as_ref(state_context_ptr, "state_context_ptr")?;
+    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 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 tx_context = ergo_lib::wallet::signing::TransactionContext::new(
+        unsigned_tx.0.clone(),
+        boxes_to_spend,
+        data_boxes,
+    )?;
+    let tx = wallet
+        .0
+        .sign_transaction(tx_context, &state_context.0, Some(&tx_hints.0))?;
+    *transaction_out = Box::into_raw(Box::new(Transaction(tx)));
+    Ok(())
+}
+
 /// Signs a reduced transaction (generating proofs for inputs)
 pub unsafe fn wallet_sign_reduced_transaction(
     wallet_ptr: ConstWalletPtr,
@@ -115,3 +157,77 @@ pub unsafe fn wallet_sign_reduced_transaction(
     *transaction_out = Box::into_raw(Box::new(tx));
     Ok(())
 }
+
+/// Signs a multi signature reduced transaction
+pub unsafe fn wallet_sign_reduced_transaction_multi(
+    wallet_ptr: ConstWalletPtr,
+    reduced_tx_ptr: ConstReducedTransactionPtr,
+    tx_hints_ptr: ConstTransactionHintsBagPtr,
+    transaction_out: *mut TransactionPtr,
+) -> Result<(), Error> {
+    let wallet = const_ptr_as_ref(wallet_ptr, "wallet_ptr")?;
+    let reduced_tx = const_ptr_as_ref(reduced_tx_ptr, "reduced_tx_ptr")?;
+    let transaction_out = mut_ptr_as_mut(transaction_out, "transaction_out")?;
+    let tx_hints = const_ptr_as_ref(tx_hints_ptr, "tx_hints_ptr")?;
+    let tx = wallet
+        .0
+        .sign_reduced_transaction(reduced_tx.0.clone(), Some(&tx_hints.0))
+        .map(Transaction)?;
+    *transaction_out = Box::into_raw(Box::new(tx));
+    Ok(())
+}
+
+/// Generate Commitments for unsigned tx
+pub unsafe fn wallet_generate_commitments(
+    wallet_ptr: ConstWalletPtr,
+    state_context_ptr: ConstErgoStateContextPtr,
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    transaction_hints_bag_out: *mut TransactionHintsBagPtr,
+) -> Result<(), Error> {
+    let wallet = const_ptr_as_ref(wallet_ptr, "wallet_ptr")?;
+    let state_context = const_ptr_as_ref(state_context_ptr, "state_context_ptr")?;
+    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 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 tx_context = ergo_lib::wallet::signing::TransactionContext::new(
+        unsigned_tx.0.clone(),
+        boxes_to_spend,
+        data_boxes,
+    )?;
+    *transaction_hints_bag_out = Box::into_raw(Box::new(TransactionHintsBag(
+        wallet
+            .0
+            .generate_commitments(tx_context, &state_context.0)?,
+    )));
+    Ok(())
+}
+
+/// Generate Commitments for reduced transaction
+pub unsafe fn wallet_generate_commitments_for_reduced_transaction(
+    wallet_ptr: ConstWalletPtr,
+    reduced_tx_ptr: ConstReducedTransactionPtr,
+    transaction_hints_bag_out: *mut TransactionHintsBagPtr,
+) -> Result<(), Error> {
+    let wallet = const_ptr_as_ref(wallet_ptr, "wallet_ptr")?;
+    let reduced_tx = const_ptr_as_ref(reduced_tx_ptr, "reduced_tx_ptr")?;
+    let transaction_hints_bag_out = mut_ptr_as_mut(transaction_hints_bag_out, "transaction_out")?;
+    *transaction_hints_bag_out = Box::into_raw(Box::new(TransactionHintsBag(
+        wallet
+            .0
+            .generate_commitments_for_reduced_transaction(reduced_tx.0.clone())?,
+    )));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c/src/reduced.rs b/bindings/ergo-lib-c/src/reduced.rs
index ab1e3abc2..3aecf7e73 100644
--- a/bindings/ergo-lib-c/src/reduced.rs
+++ b/bindings/ergo-lib-c/src/reduced.rs
@@ -46,3 +46,27 @@ pub unsafe extern "C" fn ergo_lib_reduced_tx_unsigned_tx(
 pub extern "C" fn ergo_lib_reduced_tx_delete(ptr: ReducedTransactionPtr) {
     unsafe { delete_ptr(ptr) }
 }
+
+/// Create empty proposition holder
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_propositions_new(propositions_out: *mut PropositionsPtr) {
+    #[allow(clippy::unwrap_used)]
+    propositions_new(propositions_out).unwrap();
+}
+
+/// Adding new proposition
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_propositions_add_proposition_from_bytes(
+    propositions_mut: PropositionsPtr,
+    bytes_ptr: *const u8,
+    len: usize,
+) -> ErrorPtr {
+    let res = propositions_add_proposition_from_bytes(propositions_mut, bytes_ptr, len);
+    Error::c_api_from(res)
+}
+
+/// Drop `Propositions`
+#[no_mangle]
+pub extern "C" fn ergo_lib_propositions_delete(ptr: PropositionsPtr) {
+    unsafe { delete_ptr(ptr) }
+}
diff --git a/bindings/ergo-lib-c/src/transaction.rs b/bindings/ergo-lib-c/src/transaction.rs
index 0af5f6fcb..4787f29a8 100644
--- a/bindings/ergo-lib-c/src/transaction.rs
+++ b/bindings/ergo-lib-c/src/transaction.rs
@@ -4,7 +4,9 @@ use ergo_lib_c_core::{
     collections::{CollectionPtr, ConstCollectionPtr},
     data_input::DataInput,
     ergo_box::{ErgoBox, ErgoBoxCandidate},
+    ergo_state_ctx::ConstErgoStateContextPtr,
     input::{Input, UnsignedInput},
+    reduced::ConstPropositionsPtr,
     transaction::*,
     util::ByteArray,
     Error, ErrorPtr,
@@ -16,7 +18,7 @@ use std::{
     os::raw::c_char,
 };
 
-use crate::delete_ptr;
+use crate::{delete_ptr, ReturnOption};
 
 // Need to define these here because the generated code from the `make_collection!` macro
 // invocations don't yet exist.
@@ -27,6 +29,129 @@ type UnsignedInputsPtr = CollectionPtr<UnsignedInput>;
 type ErgoBoxCandidatesPtr = CollectionPtr<ErgoBoxCandidate>;
 type ErgoBoxesPtr = CollectionPtr<ErgoBox>;
 
+// `CommitmentHint` bindings -----------------------------------------------------------------------
+
+/// Drop `CommitmentHint`
+#[no_mangle]
+pub extern "C" fn ergo_lib_commitment_hint_delete(ptr: CommitmentHintPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+// `HintsBag` bindings -----------------------------------------------------------------------------
+
+/// Empty HintsBag
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_hints_bag_empty(hints_bag_out: *mut HintsBagPtr) {
+    #[allow(clippy::unwrap_used)]
+    hints_bag_empty(hints_bag_out).unwrap();
+}
+
+/// Add commitment hint to the bag
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_hints_bag_add_commitment(
+    hints_bag_mut: HintsBagPtr,
+    hint_ptr: ConstCommitmentHintPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    hints_bag_add_commitment(hints_bag_mut, hint_ptr).unwrap();
+}
+
+/// Length of HintsBag
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_hints_bag_len(hints_bag_ptr: ConstHintsBagPtr) -> usize {
+    #[allow(clippy::unwrap_used)]
+    hints_bag_len(hints_bag_ptr).unwrap()
+}
+
+/// Get commitment
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_hints_bag_get(
+    hints_bag_ptr: ConstHintsBagPtr,
+    index: usize,
+    hint_out: *mut CommitmentHintPtr,
+) -> ReturnOption {
+    match hints_bag_get(hints_bag_ptr, index, hint_out) {
+        Ok(is_some) => ReturnOption {
+            is_some,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnOption {
+            is_some: false, // Dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Drop `HintsBag`
+#[no_mangle]
+pub extern "C" fn ergo_lib_hints_bag_delete(ptr: HintsBagPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+// `TransactionHintsBag` bindings ------------------------------------------------------------------
+
+/// Empty TransactionHintsBag
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_transaction_hints_bag_empty(
+    transaction_hints_bag_out: *mut TransactionHintsBagPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    transaction_hints_bag_empty(transaction_hints_bag_out).unwrap();
+}
+
+/// Adding hints for input
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_transaction_hints_bag_add_hints_for_input(
+    transaction_hints_bag_mut: TransactionHintsBagPtr,
+    index: usize,
+    hints_bag_ptr: ConstHintsBagPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    transaction_hints_bag_add_hints_for_input(transaction_hints_bag_mut, index, hints_bag_ptr)
+        .unwrap();
+}
+
+/// Get HintsBag corresponding to input index
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_transaction_hints_bag_all_hints_for_input(
+    transaction_hints_bag_ptr: ConstTransactionHintsBagPtr,
+    index: usize,
+    hints_bag_out: *mut HintsBagPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    transaction_hints_bag_all_hints_for_input(transaction_hints_bag_ptr, index, hints_bag_out)
+        .unwrap();
+}
+
+/// Extract hints from signed transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_transaction_extract_hints(
+    signed_transaction_ptr: ConstTransactionPtr,
+    state_context_ptr: ConstErgoStateContextPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    real_propositions_ptr: ConstPropositionsPtr,
+    simulated_propositions_ptr: ConstPropositionsPtr,
+    transaction_hints_bag_out: *mut TransactionHintsBagPtr,
+) -> ErrorPtr {
+    let res = transaction_extract_hints(
+        signed_transaction_ptr,
+        state_context_ptr,
+        boxes_to_spend_ptr,
+        data_boxes_ptr,
+        real_propositions_ptr,
+        simulated_propositions_ptr,
+        transaction_hints_bag_out,
+    );
+    Error::c_api_from(res)
+}
+
+/// Drop `TransactionHintsBag`
+#[no_mangle]
+pub extern "C" fn ergo_lib_transaction_hints_bag_delete(ptr: TransactionHintsBagPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
 // `UnsignedTransaction` bindings ------------------------------------------------------------------
 
 /// Get id for transaction
diff --git a/bindings/ergo-lib-c/src/wallet.rs b/bindings/ergo-lib-c/src/wallet.rs
index 3cedae2df..501dd4f91 100644
--- a/bindings/ergo-lib-c/src/wallet.rs
+++ b/bindings/ergo-lib-c/src/wallet.rs
@@ -8,7 +8,10 @@ use ergo_lib_c_core::{
     ergo_state_ctx::ConstErgoStateContextPtr,
     reduced::ConstReducedTransactionPtr,
     secret_key::SecretKey,
-    transaction::{ConstUnsignedTransactionPtr, TransactionPtr},
+    transaction::{
+        ConstTransactionHintsBagPtr, ConstUnsignedTransactionPtr, TransactionHintsBagPtr,
+        TransactionPtr,
+    },
     wallet::*,
     Error, ErrorPtr,
 };
@@ -69,6 +72,29 @@ pub unsafe extern "C" fn ergo_lib_wallet_sign_transaction(
     Error::c_api_from(res)
 }
 
+/// Signs a multi signature transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_wallet_sign_transaction_multi(
+    wallet_ptr: ConstWalletPtr,
+    state_context_ptr: ConstErgoStateContextPtr,
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    tx_hints_ptr: ConstTransactionHintsBagPtr,
+    transaction_out: *mut TransactionPtr,
+) -> ErrorPtr {
+    let res = wallet_sign_transaction_multi(
+        wallet_ptr,
+        state_context_ptr,
+        unsigned_tx_ptr,
+        boxes_to_spend_ptr,
+        data_boxes_ptr,
+        tx_hints_ptr,
+        transaction_out,
+    );
+    Error::c_api_from(res)
+}
+
 /// Signs a reduced transaction (generating proofs for inputs)
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_wallet_sign_reduced_transaction(
@@ -80,6 +106,59 @@ pub unsafe extern "C" fn ergo_lib_wallet_sign_reduced_transaction(
     Error::c_api_from(res)
 }
 
+/// Signs a multi signature reduced transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_wallet_sign_reduced_transaction_multi(
+    wallet_ptr: ConstWalletPtr,
+    reduced_tx_ptr: ConstReducedTransactionPtr,
+    tx_hints_ptr: ConstTransactionHintsBagPtr,
+    transaction_out: *mut TransactionPtr,
+) -> ErrorPtr {
+    let res = wallet_sign_reduced_transaction_multi(
+        wallet_ptr,
+        reduced_tx_ptr,
+        tx_hints_ptr,
+        transaction_out,
+    );
+    Error::c_api_from(res)
+}
+
+/// Generate Commitments for unsigned tx
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_wallet_generate_commitments(
+    wallet_ptr: ConstWalletPtr,
+    state_context_ptr: ConstErgoStateContextPtr,
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    transaction_hints_bag_out: *mut TransactionHintsBagPtr,
+) -> ErrorPtr {
+    let res = wallet_generate_commitments(
+        wallet_ptr,
+        state_context_ptr,
+        unsigned_tx_ptr,
+        boxes_to_spend_ptr,
+        data_boxes_ptr,
+        transaction_hints_bag_out,
+    );
+    Error::c_api_from(res)
+}
+
+/// Generate Commitments for reduced transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_wallet_generate_commitments_for_reduced_transaction(
+    wallet_ptr: ConstWalletPtr,
+    reduced_tx_ptr: ConstReducedTransactionPtr,
+    transaction_hints_bag_out: *mut TransactionHintsBagPtr,
+) -> ErrorPtr {
+    let res = wallet_generate_commitments_for_reduced_transaction(
+        wallet_ptr,
+        reduced_tx_ptr,
+        transaction_hints_bag_out,
+    );
+    Error::c_api_from(res)
+}
+
 /// Drop `Wallet`
 #[no_mangle]
 pub extern "C" fn ergo_lib_wallet_delete(ptr: WalletPtr) {
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/ReducedTransaction.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/ReducedTransaction.swift
index 6dbca514a..31414e679 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/ReducedTransaction.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/ReducedTransaction.swift
@@ -42,3 +42,25 @@ class ReducedTransaction {
         ergo_lib_reduced_tx_delete(self.pointer)
     }
 }
+
+/// Propositions list(public keys)
+class Propositions {
+    internal var pointer: PropositionsPtr
+    
+    /// Create empty proposition holder
+    init() {
+        var ptr: PropositionsPtr?
+        ergo_lib_propositions_new(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Add new proposition
+    func addProposition(fromBytes : [UInt8]) throws {
+        let error = ergo_lib_propositions_add_proposition_from_bytes(self.pointer, fromBytes, UInt(fromBytes.count))
+        try checkError(error)
+    }
+    
+    deinit {
+        ergo_lib_propositions_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Transaction.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Transaction.swift
index 33ac2c7a0..9622242a7 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/Transaction.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Transaction.swift
@@ -2,6 +2,123 @@
 import Foundation
 import ErgoLibC
 
+/// A family of hints which are about a correspondence between a public image of a secret image and prover's commitment
+/// to randomness ("a" in a sigma protocol).
+class CommitmentHint {
+    internal var pointer: CommitmentHintPtr
+    
+    /// Takes ownership of an existing ``CommitmentHint``. Note: we must ensure that no other instance
+    /// of ``CommitmentHint`` can hold this pointer.
+    internal init(withRawPointer ptr: CommitmentHintPtr) {
+        self.pointer = ptr
+    }
+    
+    deinit {
+        ergo_lib_commitment_hint_delete(self.pointer)
+    }
+}
+
+/// Collection of hints to be used by a prover
+class HintsBag {
+    internal var pointer: HintsBagPtr
+    
+    /// Create empty ``HintsBag``
+    init() {
+        var ptr: HintsBagPtr?
+        ergo_lib_hints_bag_empty(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Add commitment hint to the bag
+    func addCommitmentHint(hint: CommitmentHint) {
+        ergo_lib_hints_bag_add_commitment(self.pointer, hint.pointer)
+    }
+    
+    /// Length of ``HintsBag``
+    func len() -> UInt {
+        ergo_lib_hints_bag_len(self.pointer)
+    }
+    
+    /// Returns the ``CommitmentHint`` at location `index` if it exists.
+    func getCommitmentHint(index: UInt) -> CommitmentHint? {
+        var ptr: CommitmentHintPtr?
+        let res = ergo_lib_hints_bag_get(self.pointer, index, &ptr)
+        assert(res.error == nil)
+        if res.is_some {
+            return CommitmentHint(withRawPointer: ptr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Takes ownership of an existing ``HintsBag``. Note: we must ensure that no other instance
+    /// of ``HintsBag`` can hold this pointer.
+    internal init(withRawPointer ptr: HintsBagPtr) {
+        self.pointer = ptr
+    }
+    
+    deinit {
+        ergo_lib_hints_bag_delete(self.pointer)
+    }
+}
+
+/// TransactionHintsBag
+class TransactionHintsBag {
+    internal var pointer: TransactionHintsBagPtr
+    
+    /// Create empty ``TransactionHintsBag``
+    init() {
+        var ptr: TransactionHintsBagPtr?
+        ergo_lib_transaction_hints_bag_empty(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Adding hints for input
+    func addHintsForInput(index: UInt, hintsBag: HintsBag) {
+        ergo_lib_transaction_hints_bag_add_hints_for_input(self.pointer, index, hintsBag.pointer)
+    }
+    
+    /// Get HintsBag corresponding to input index
+    func allHintsForInput(index: UInt) -> HintsBag {
+        var ptr: HintsBagPtr?
+        ergo_lib_transaction_hints_bag_all_hints_for_input(self.pointer, index, &ptr)
+        return HintsBag(withRawPointer: ptr!)
+    }
+    
+    /// Takes ownership of an existing ``TransactionHintsBag``. Note: we must ensure that no other instance
+    /// of ``TransactionHintsBag`` can hold this pointer.
+    internal init(withRawPointer ptr: TransactionHintsBagPtr) {
+        self.pointer = ptr
+    }
+    
+    deinit {
+        ergo_lib_transaction_hints_bag_delete(self.pointer)
+    }
+}
+
+/// Extract hints from signed transaction
+func extractHintsFromSignedTransaction(
+    transaction: Transaction,
+    stateContext: ErgoStateContext,
+    boxesToSpend: ErgoBoxes,
+    dataBoxes: ErgoBoxes,
+    realPropositions: Propositions,
+    simulatedPropositions: Propositions
+) throws -> TransactionHintsBag {
+    var ptr: TransactionHintsBagPtr?
+    let error = ergo_lib_transaction_extract_hints(
+        transaction.pointer,
+        stateContext.pointer,
+        boxesToSpend.pointer,
+        dataBoxes.pointer,
+        realPropositions.pointer,
+        simulatedPropositions.pointer,
+        &ptr)
+    
+    try checkError(error)
+    return TransactionHintsBag(withRawPointer: ptr!)
+}
+
 /// Unsigned (inputs without proofs) transaction
 class UnsignedTransaction {
     internal var pointer: UnsignedTransactionPtr
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Wallet.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Wallet.swift
index e1e6a99f1..93c5d5389 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/Wallet.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Wallet.swift
@@ -44,6 +44,28 @@ class Wallet {
         return Transaction(withRawPointer: ptr!)
     }
     
+    /// Sign a multi signature transaction
+    func signTransactionMulti(
+        stateContext: ErgoStateContext,
+        unsignedTx: UnsignedTransaction,
+        boxesToSpend: ErgoBoxes,
+        dataBoxes: ErgoBoxes,
+        txHints: TransactionHintsBag
+    ) throws -> Transaction {
+        var ptr: TransactionPtr?
+        let error = ergo_lib_wallet_sign_transaction_multi(
+            self.pointer,
+            stateContext.pointer,
+            unsignedTx.pointer,
+            boxesToSpend.pointer,
+            dataBoxes.pointer,
+            txHints.pointer,
+            &ptr
+        )
+        try checkError(error)
+        return Transaction(withRawPointer: ptr!)
+    }
+    
     /// Signs a reduced transaction (generating proofs for inputs)
     func signReducedTransaction(
         reducedTx: ReducedTransaction
@@ -54,6 +76,56 @@ class Wallet {
         return Transaction(withRawPointer: ptr!)
     }
     
+    /// Signs a multi signature reduced transaction
+    func signReducedTransactionMulti(
+        reducedTx: ReducedTransaction,
+        txHints: TransactionHintsBag
+    ) throws -> Transaction {
+        var ptr: TransactionPtr?
+        let error = ergo_lib_wallet_sign_reduced_transaction_multi(
+            self.pointer,
+            reducedTx.pointer,
+            txHints.pointer,
+            &ptr
+        )
+        try checkError(error)
+        return Transaction(withRawPointer: ptr!)
+    }
+    
+    /// Generate Commitments for unsigned tx
+    func generateCommitments(
+        stateContext: ErgoStateContext,
+        unsignedTx: UnsignedTransaction,
+        boxesToSpend: ErgoBoxes,
+        dataBoxes: ErgoBoxes
+    ) throws -> TransactionHintsBag {
+        var ptr: TransactionHintsBagPtr?
+        let error = ergo_lib_wallet_generate_commitments(
+            self.pointer,
+            stateContext.pointer,
+            unsignedTx.pointer,
+            boxesToSpend.pointer,
+            dataBoxes.pointer,
+            &ptr
+        )
+        try checkError(error)
+        return TransactionHintsBag(withRawPointer: ptr!)
+    }
+    
+    /// Generate Commitments for reduced transaction
+    func generateCommitmentsForReducedTransaction(
+        reducedTx: ReducedTransaction
+    ) throws -> TransactionHintsBag {
+        var ptr: TransactionHintsBagPtr?
+        let error = ergo_lib_wallet_generate_commitments_for_reduced_transaction(
+            self.pointer,
+            reducedTx.pointer,
+            &ptr
+        )
+        try checkError(error)
+        return TransactionHintsBag(withRawPointer: ptr!)
+    }
+    
     deinit {
         ergo_lib_wallet_delete(self.pointer)
     }
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift
new file mode 100644
index 000000000..13476f3c9
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift
@@ -0,0 +1,16 @@
+// The following two functions copied from: https://stackoverflow.com/a/49890939
+
+func toPairsOfChars(pairs: [String], string: String) -> [String] {
+    if string.count == 0 {
+        return pairs
+    }
+    var pairsMod = pairs
+    pairsMod.append(String(string.prefix(2)))
+    return toPairsOfChars(pairs: pairsMod, string: String(string.dropFirst(2)))
+}
+
+func base16StringToBytes(_ string: String) -> [UInt8]? {
+    // omit error checking: remove '0x', make sure even, valid chars
+    let pairs = toPairsOfChars(pairs: [], string: string)
+    return pairs.map { UInt8($0, radix: 16)! }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/ConstantTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ConstantTests.swift
index ea3a8a643..f14ebec2d 100644
--- a/bindings/ergo-lib-ios/Tests/ErgoLibTests/ConstantTests.swift
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ConstantTests.swift
@@ -27,7 +27,7 @@ final class ConstantTests: XCTestCase {
     
     func testECPointBytes() throws {
         let str = "02d6b2141c21e4f337e9b065a031a6269fb5a49253094fc6243d38662eb765db00"
-        XCTAssertNoThrow(try Constant(withECPointBytes: stringToBytes(str)!))
+        XCTAssertNoThrow(try Constant(withECPointBytes: base16StringToBytes(str)!))
     }
     
     func testErgoBoxRoundtrip() throws {
@@ -50,20 +50,3 @@ final class ConstantTests: XCTestCase {
         XCTAssertEqual(c, decodedC)
     }
 }
-
-// The following two functions copied from: https://stackoverflow.com/a/49890939
-
-func toPairsOfChars(pairs: [String], string: String) -> [String] {
-    if string.count == 0 {
-        return pairs
-    }
-    var pairsMod = pairs
-    pairsMod.append(String(string.prefix(2)))
-    return toPairsOfChars(pairs: pairsMod, string: String(string.dropFirst(2)))
-}
-
-func stringToBytes(_ string: String) -> [UInt8]? {
-    // omit error checking: remove '0x', make sure even, valid chars
-    let pairs = toPairsOfChars(pairs: [], string: string)
-    return pairs.map { UInt8($0, radix: 16)! }
-}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift
index d0fe8b096..01d58b96e 100644
--- a/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift
@@ -308,4 +308,109 @@ final class TransactionTests: XCTestCase {
         let pass = "password1234"
         XCTAssertNoThrow(try Wallet(mnemonicPhrase: phrase, mnemonicPass: pass))
     }
+    
+    func testMultiSigTx() throws {
+        let aliceSecret =
+            try SecretKey(fromBytes: base16StringToBytes("e726ad60a073a49f7851f4d11a83de6c9c7f99e17314fcce560f00a51a8a3d18")!
+        )
+        let bobSecret =
+            try SecretKey(fromBytes: base16StringToBytes("9e6616b4e44818d21b8dfdd5ea87eb822480e7856ab910d00f5834dc64db79b3")!)
+        let alicePkBytes = base16StringToBytes("cd03c8e1527efae4be9868cea6767157fcccac66489842738efed0a302e4f81710d0")!
+        
+        // Pay 2 Script address of a multi_sig contract with contract { alicePK && bobPK }
+        let multiSigAddress =
+          try Address(withTestnetAddress: "JryiCXrc7x5D8AhS9DYX1TDzW5C5mT6QyTMQaptF76EQkM15cetxtYKq3u6LymLZLVCyjtgbTKFcfuuX9LLi49Ec5m2p6cwsg5NyEsCQ7na83yEPN")
+        let inputContract = try Contract.init(payToAddress: multiSigAddress)
+        let txId = try TxId(withString: "0000000000000000000000000000000000000000000000000000000000000000")
+        let inputBox = try ErgoBox(
+            boxValue: try BoxValue(fromInt64: 1000000000),
+            creationHeight: UInt32(0),
+            contract: inputContract,
+            txId: txId,
+            index: UInt16(0),
+            tokens: Tokens()
+        )
+        
+        // create a transaction that spends the "simulated" box
+        let recipient = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let unspentBoxes = ErgoBoxes()
+        unspentBoxes.add(ergoBox: inputBox)
+        let contract = try Contract(payToAddress: recipient)
+        let outboxValue = BoxValue.SAFE_USER_MIN()
+        let outbox = try ErgoBoxCandidateBuilder(boxValue: outboxValue, contract: contract, creationHeight: UInt32(0)).build()
+        let txOutputs = ErgoBoxCandidates()
+        txOutputs.add(ergoBoxCandidate: outbox)
+        let fee = TxBuilder.SUGGESTED_TX_FEE()
+        let changeAddress = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let minChangeValue = BoxValue.SAFE_USER_MIN()
+        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: UInt32(0),
+            feeAmount: fee,
+            changeAddress: changeAddress,
+            minChangeValue: minChangeValue
+        )
+        let tx = try txBuilder.build()
+        let txDataInputs = try ErgoBoxes.init(fromJSON: [])
+        let blockHeaders = try HeaderTests.generateBlockHeadersFromJSON()
+        let preHeader = PreHeader(withBlockHeader: blockHeaders.get(index: UInt(0))!)
+        let ctx = try ErgoStateContext(preHeader: preHeader, headers: blockHeaders)
+        let sksAlice = SecretKeys()
+        sksAlice.add(secretKey: aliceSecret)
+        let walletAlice = Wallet(secrets: sksAlice)
+        let sksBob = SecretKeys()
+        sksBob.add(secretKey: bobSecret)
+        let walletBob = Wallet(secrets: sksBob)
+        let bobHints = try walletBob.generateCommitments(
+            stateContext: ctx,
+            unsignedTx: tx,
+            boxesToSpend: unspentBoxes,
+            dataBoxes: txDataInputs
+        ).allHintsForInput(index: UInt(0))
+        let bobKnown = bobHints.getCommitmentHint(index: UInt(0))!
+        let bobOwn = bobHints.getCommitmentHint(index: UInt(1))!
+        let hintsBag = HintsBag()
+        hintsBag.addCommitmentHint(hint: bobKnown)
+        let aliceTxHintsBag = TransactionHintsBag()
+        aliceTxHintsBag.addHintsForInput(index: UInt(0), hintsBag: hintsBag)
+        let partialSigned = try walletAlice.signTransactionMulti(
+            stateContext: ctx,
+            unsignedTx: tx,
+            boxesToSpend: unspentBoxes,
+            dataBoxes: txDataInputs,
+            txHints: aliceTxHintsBag
+        )
+        let realPropositions = Propositions()
+        let simulatedPropositions = Propositions()
+        try realPropositions.addProposition(fromBytes: alicePkBytes)
+        let bobHintsBag = try extractHintsFromSignedTransaction(
+            transaction: partialSigned,
+            stateContext: ctx,
+            boxesToSpend: unspentBoxes,
+            dataBoxes: txDataInputs,
+            realPropositions: realPropositions,
+            simulatedPropositions: simulatedPropositions
+        ).allHintsForInput(index: UInt(0))
+        bobHintsBag.addCommitmentHint(hint: bobOwn)
+        let bobTxHintsBag = TransactionHintsBag()
+        bobTxHintsBag.addHintsForInput(index: UInt(0), hintsBag: bobHintsBag)
+        XCTAssertNoThrow(
+            try walletBob.signTransactionMulti(
+                stateContext: ctx,
+                unsignedTx: tx,
+                boxesToSpend: unspentBoxes,
+                dataBoxes: txDataInputs,
+                txHints: bobTxHintsBag
+            )
+        )
+        
+    }
 }


Test patch: