From 1588f7d58a7c1a8241019d1282abc9bdde3d6eed Mon Sep 17 00:00:00 2001 From: lisicky Date: Tue, 26 Nov 2024 18:49:04 +0800 Subject: [PATCH 1/3] add add_*_utxo methods --- rust/src/builders/tx_builder.rs | 8 +- rust/src/builders/tx_inputs_builder.rs | 130 +++++++++++++++++++++++-- rust/src/error.rs | 8 +- 3 files changed, 134 insertions(+), 12 deletions(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index 1ee62eaf..5da89eb8 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -148,7 +148,7 @@ fn min_fee(tx_builder: &TransactionBuilder) -> Result { } else { if total_ref_script_size > 0 { return Err(JsError::from_str( - "Plutus scripts are present but ref_script_coins_per_byte are missing in the config!", + "Referenced scripts are present but ref_script_coins_per_byte is missing in the config!", )); } } @@ -1680,6 +1680,12 @@ impl TransactionBuilder { } } + //inputs with an inlined scripts + for item in self.inputs.get_inputs_with_ref_script_size() { + add_to_map(item, &mut sizes_map)? + } + + //inputs with a ref script witness for item in self.inputs.get_script_ref_inputs_with_size() { add_to_map(item, &mut sizes_map)? } diff --git a/rust/src/builders/tx_inputs_builder.rs b/rust/src/builders/tx_inputs_builder.rs index f37c03d8..4d7f03af 100644 --- a/rust/src/builders/tx_inputs_builder.rs +++ b/rust/src/builders/tx_inputs_builder.rs @@ -6,6 +6,7 @@ use std::collections::{BTreeMap, BTreeSet}; pub(crate) struct TxBuilderInput { pub(crate) input: TransactionInput, pub(crate) amount: Value, // we need to keep track of the amount in the inputs for input selection + pub(crate) input_ref_script_size: Option, } // We need to know how many of each type of witness will be in the transaction so we can calculate the tx fee @@ -40,8 +41,68 @@ impl TxInputsBuilder { } } - fn push_input(&mut self, e: (TxBuilderInput, Option)) { - self.inputs.insert(e.0.input.clone(), e); + pub fn add_regular_utxo(&mut self, utxo: &TransactionUnspentOutput) -> Result<(), JsError> { + let input = &utxo.input; + let output = &utxo.output; + + let address = &output.address; + let address_kind = address.kind(); + if address_kind == AddressKind::Malformed || address_kind == AddressKind::Reward { + return Err(JsError::from_str(&BuilderError::RegularAddressTypeMismatch.as_str())); + } + + let ref_script_size = output.script_ref.as_ref().map(|x| x.to_unwrapped_bytes().len()); + self.add_regular_input_extended(&output.address, input, &output.amount, ref_script_size) + } + + pub fn add_plutus_script_utxo(&mut self, utxo: &TransactionUnspentOutput, witness: &PlutusWitness) -> Result<(), JsError> { + let input = &utxo.input; + let output = &utxo.output; + let address = &output.address; + let address_kind = address.kind(); + if address_kind == AddressKind::Malformed || address_kind == AddressKind::Reward || address_kind == AddressKind::Byron{ + return Err(JsError::from_str(&BuilderError::ScriptAddressTypeMismatch.as_str())); + } + + let payment_cred = address.payment_cred(); + if let Some(payment_cred) = payment_cred { + if !payment_cred.has_script_hash() { + return Err(JsError::from_str(&BuilderError::ScriptAddressCredentialMismatch.as_str())); + } + } + + let ref_script_size = output.script_ref.as_ref().map(|x| x.to_unwrapped_bytes().len()); + let hash = witness.script.script_hash(); + + self.add_script_input(&hash, input, &output.amount, ref_script_size); + let witness = ScriptWitnessType::PlutusScriptWitness(witness.clone()); + self.insert_input_with_witness(&hash, input, &witness); + Ok(()) + } + + pub fn add_native_script_utxo(&mut self, utxo: &TransactionUnspentOutput, witness: &NativeScriptSource) -> Result<(), JsError> { + let input = &utxo.input; + let output = &utxo.output; + let address = &output.address; + let address_kind = address.kind(); + if address_kind == AddressKind::Malformed || address_kind == AddressKind::Reward || address_kind == AddressKind::Byron{ + return Err(JsError::from_str(&BuilderError::ScriptAddressTypeMismatch.as_str())); + } + + let payment_cred = address.payment_cred(); + if let Some(payment_cred) = payment_cred { + if !payment_cred.has_script_hash() { + return Err(JsError::from_str(&BuilderError::ScriptAddressCredentialMismatch.as_str())); + } + } + + let ref_script_size = output.script_ref.as_ref().map(|x| x.to_unwrapped_bytes().len()); + let hash = witness.script_hash(); + + self.add_script_input(&hash, input, &output.amount, ref_script_size); + let witness = ScriptWitnessType::NativeScriptWitness(witness.0.clone()); + self.insert_input_with_witness(&hash, input, &witness); + Ok(()) } /// We have to know what kind of inputs these are to know what kind of mock witnesses to create since @@ -52,19 +113,31 @@ impl TxInputsBuilder { hash: &Ed25519KeyHash, input: &TransactionInput, amount: &Value, + ) { + self.add_key_input_extended(hash, input, amount, None); + } + + fn add_key_input_extended( + &mut self, + hash: &Ed25519KeyHash, + input: &TransactionInput, + amount: &Value, + input_ref_script_size: Option, ) { let inp = TxBuilderInput { input: input.clone(), amount: amount.clone(), + input_ref_script_size, }; self.push_input((inp, None)); self.required_witnesses.vkeys.add_move(hash.clone()); } - fn add_script_input(&mut self, hash: &ScriptHash, input: &TransactionInput, amount: &Value) { + fn add_script_input(&mut self, hash: &ScriptHash, input: &TransactionInput, amount: &Value, input_ref_script_size: Option) { let inp = TxBuilderInput { input: input.clone(), amount: amount.clone(), + input_ref_script_size, }; self.push_input((inp, Some(hash.clone()))); self.insert_input_with_empty_witness(hash, input); @@ -78,7 +151,7 @@ impl TxInputsBuilder { amount: &Value, ) { let hash = script.script_hash(); - self.add_script_input(&hash, input, amount); + self.add_script_input(&hash, input, amount, None); let witness = ScriptWitnessType::NativeScriptWitness(script.0.clone()); self.insert_input_with_witness(&hash, input, &witness); } @@ -91,8 +164,7 @@ impl TxInputsBuilder { amount: &Value, ) { let hash = witness.script.script_hash(); - - self.add_script_input(&hash, input, amount); + self.add_script_input(&hash, input, amount, None); let witness = ScriptWitnessType::PlutusScriptWitness(witness.clone()); self.insert_input_with_witness(&hash, input, &witness); } @@ -102,26 +174,48 @@ impl TxInputsBuilder { address: &ByronAddress, input: &TransactionInput, amount: &Value, + ) { + self.add_bootstrap_input_extended(address, input, amount, None); + } + + fn add_bootstrap_input_extended( + &mut self, + address: &ByronAddress, + input: &TransactionInput, + amount: &Value, + input_ref_script_size: Option, ) { let inp = TxBuilderInput { input: input.clone(), amount: amount.clone(), + input_ref_script_size, }; self.push_input((inp, None)); self.required_witnesses.bootstraps.insert(address.to_bytes()); } + /// Adds non script input, in case of script or reward address input it will return an error pub fn add_regular_input( &mut self, address: &Address, input: &TransactionInput, amount: &Value, + ) -> Result<(), JsError> { + self.add_regular_input_extended(address, input, amount, None) + } + + fn add_regular_input_extended( + &mut self, + address: &Address, + input: &TransactionInput, + amount: &Value, + input_ref_script_size: Option, ) -> Result<(), JsError> { match &address.0 { AddrType::Base(base_addr) => match &base_addr.payment.0 { CredType::Key(key) => { - self.add_key_input(key, input, amount); + self.add_key_input_extended(key, input, amount, input_ref_script_size); Ok(()) } CredType::Script(_) => Err(JsError::from_str( @@ -130,7 +224,7 @@ impl TxInputsBuilder { }, AddrType::Enterprise(ent_aaddr) => match &ent_aaddr.payment.0 { CredType::Key(key) => { - self.add_key_input(key, input, amount); + self.add_key_input_extended(key, input, amount, input_ref_script_size); Ok(()) } CredType::Script(_) => Err(JsError::from_str( @@ -139,7 +233,7 @@ impl TxInputsBuilder { }, AddrType::Ptr(ptr_addr) => match &ptr_addr.payment.0 { CredType::Key(key) => { - self.add_key_input(key, input, amount); + self.add_key_input_extended(key, input, amount, input_ref_script_size); Ok(()) } CredType::Script(_) => Err(JsError::from_str( @@ -147,7 +241,7 @@ impl TxInputsBuilder { )), }, AddrType::Byron(byron_addr) => { - self.add_bootstrap_input(byron_addr, input, amount); + self.add_bootstrap_input_extended(byron_addr, input, amount, input_ref_script_size); Ok(()) } AddrType::Reward(_) => Err(JsError::from_str( @@ -328,6 +422,18 @@ impl TxInputsBuilder { .filter_map(|wit| wit.get_script_ref_input_with_size()) } + pub(crate) fn get_inputs_with_ref_script_size( + &self, + ) -> impl Iterator { + self.inputs.iter().filter_map(|(tx_in, (tx_builder_input, _))| { + if let Some(size) = tx_builder_input.input_ref_script_size { + Some((tx_in, size)) + } else { + None + } + }) + } + #[allow(dead_code)] pub(crate) fn get_required_signers(&self) -> Ed25519KeyHashes { self.into() @@ -341,6 +447,10 @@ impl TxInputsBuilder { self.inputs.contains_key(input) } + fn push_input(&mut self, e: (TxBuilderInput, Option)) { + self.inputs.insert(e.0.input.clone(), e); + } + fn insert_input_with_witness( &mut self, script_hash: &ScriptHash, diff --git a/rust/src/error.rs b/rust/src/error.rs index 1360db4b..e1f5b13c 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -232,6 +232,9 @@ pub(crate) enum BuilderError { RegularInputIsScript, RegularInputIsFromRewardAddress, MalformedAddress, + ScriptAddressTypeMismatch, + RegularAddressTypeMismatch, + ScriptAddressCredentialMismatch, MintBuilderDifferentScriptType, MintBuilderDifferentRedeemerDataAndExUnits(String, String), MintBuilderDifferentWitnessTypeRef, @@ -241,13 +244,16 @@ pub(crate) enum BuilderError { impl BuilderError { pub(crate) fn as_str(&self) -> String { match self { - BuilderError::RegularInputIsScript => "You can't add a script input to this function. You can use `.add_native_script_input` or `.add_plutus_script_input` directly to register the input along with the witness.".to_string(), + BuilderError::RegularInputIsScript => "You can't add a script input to this function. You can use `.add_native_script_utxo` or `.add_plutus_script_utxo` or `.add_native_script_input` or `.add_plutus_script_input` directly to register the input along with the witness.".to_string(), BuilderError::RegularInputIsFromRewardAddress => "You can't use an input from reward address. To spend funds from reward address you to use withdrawal mechanism.".to_string(), BuilderError::MalformedAddress => "The address is malformed.".to_string(), BuilderError::MintBuilderDifferentScriptType => "You can't add a mint to the same policy id but with different script type.".to_string(), BuilderError::MintBuilderDifferentRedeemerDataAndExUnits(redeemer1, redeemer2) => format!("You can't add a mint to the same policy id but with different redeemer data and ex units. Current redeemer {redeemer1}, your redeemer {redeemer2}"), BuilderError::MintBuilderDifferentWitnessTypeRef => "You can't add a mint to the same policy id but with different witness type. Current witness is ref input".to_string(), BuilderError::MintBuilderDifferentWitnessTypeNonRef => "You can't add a mint to the same policy id but with different witness type. Current witness is non ref".to_string(), + BuilderError::ScriptAddressTypeMismatch => "The address should be Base or Pointer or Enterprise.".to_string(), + BuilderError::RegularAddressTypeMismatch => "The address should be Base or Pointer or Enterprise or Byron.".to_string(), + BuilderError::ScriptAddressCredentialMismatch => "The address should have script payment credential.".to_string(), } } } From f6299c77186e86e813ac613a6b3a1d0553874908 Mon Sep 17 00:00:00 2001 From: lisicky Date: Mon, 16 Dec 2024 12:45:19 +0400 Subject: [PATCH 2/3] use add_regular_utxo in coin selection --- rust/src/builders/tx_builder.rs | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index 5da89eb8..a883a3c1 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -428,11 +428,7 @@ impl TransactionBuilder { //just add first input, to cover needs of one input let input = available_inputs.pop().unwrap(); - self.add_regular_input( - &input.output.address, - &input.input, - &input.output.amount, - )?; + self.inputs.add_regular_utxo(&input)?; input_total = input_total.checked_add(&input.output.amount)?; } @@ -494,11 +490,7 @@ impl TransactionBuilder { &input.input, &input.output.amount, )?; - self.add_regular_input( - &input.output.address, - &input.input, - &input.output.amount, - )?; + self.inputs.add_regular_utxo(&input)?; input_total = input_total.checked_add(&input.output.amount)?; output_total = output_total.checked_add(&Value::new(&input_fee))?; } @@ -578,11 +570,7 @@ impl TransactionBuilder { &input.input, &input.output.amount, )?; - self.add_regular_input( - &input.output.address, - &input.input, - &input.output.amount, - )?; + self.inputs.add_regular_utxo(&input)?; input_total = input_total.checked_add(&input.output.amount)?; output_total = output_total.checked_add(&Value::new(&input_fee))?; } @@ -620,7 +608,7 @@ impl TransactionBuilder { // differing from CIP2, we include the needed fees in the targets instead of just output values let input_fee = self.fee_for_input(&input.output.address, &input.input, &input.output.amount)?; - self.add_regular_input(&input.output.address, &input.input, &input.output.amount)?; + self.inputs.add_regular_utxo(&input)?; *input_total = input_total.checked_add(&input.output.amount)?; *output_total = output_total.checked_add(&Value::new(&input_fee))?; available_indices.swap_remove(available_indices.iter().position(|j| i == j).unwrap()); @@ -740,11 +728,7 @@ impl TransactionBuilder { &input.input, &input.output.amount, )?; - self.add_regular_input( - &input.output.address, - &input.input, - &input.output.amount, - )?; + self.inputs.add_regular_utxo(&input)?; *input_total = input_total.checked_add(&input.output.amount)?; *output_total = output_total.checked_add(&Value::new(&input_fee))?; } @@ -982,11 +966,7 @@ impl TransactionBuilder { let last_input = unused_inputs.0.pop(); match last_input { Some(input) => { - self.add_regular_input( - &input.output().address(), - &input.input(), - &input.output().amount(), - )?; + self.inputs.add_regular_utxo(&input)?; add_change_result = self .add_change_if_needed_with_optional_script_and_datum( &change_config.address, From 6a16c312f98f91bbd29d966726de917d856a7ca6 Mon Sep 17 00:00:00 2001 From: lisicky Date: Mon, 30 Dec 2024 19:20:01 +0400 Subject: [PATCH 3/3] add tests --- rust/src/builders/tx_builder.rs | 2 +- rust/src/tests/builders/tx_builder.rs | 134 +++++++++++++++++++++++--- 2 files changed, 124 insertions(+), 12 deletions(-) diff --git a/rust/src/builders/tx_builder.rs b/rust/src/builders/tx_builder.rs index a883a3c1..148a26dd 100644 --- a/rust/src/builders/tx_builder.rs +++ b/rust/src/builders/tx_builder.rs @@ -1644,7 +1644,7 @@ impl TransactionBuilder { } } - fn get_total_ref_scripts_size(&self) -> Result { + pub(crate) fn get_total_ref_scripts_size(&self) -> Result { let mut sizes_map = HashMap::new(); fn add_to_map<'a>( item: (&'a TransactionInput, usize), diff --git a/rust/src/tests/builders/tx_builder.rs b/rust/src/tests/builders/tx_builder.rs index 91bc3971..af80b1bd 100644 --- a/rust/src/tests/builders/tx_builder.rs +++ b/rust/src/tests/builders/tx_builder.rs @@ -1,5 +1,5 @@ use crate::tests::helpers::harden; -use crate::tests::fakes::{fake_byron_address, fake_anchor, fake_change_address, fake_default_tx_builder, fake_linear_fee, fake_reallistic_tx_builder, fake_redeemer, fake_redeemer_zero_cost, fake_rich_tx_builder, fake_tx_builder, fake_tx_builder_with_amount, fake_tx_builder_with_fee, fake_tx_builder_with_fee_and_pure_change, fake_tx_builder_with_fee_and_val_size, fake_tx_builder_with_key_deposit, fake_base_address, fake_bytes_32, fake_data_hash, fake_key_hash, fake_plutus_script_and_hash, fake_policy_id, fake_script_hash, fake_tx_hash, fake_tx_input, fake_tx_input2, fake_value, fake_value2, fake_vkey_witness, fake_root_key_15, fake_base_address_with_payment_cred, fake_bootsrap_witness_with_attrs}; +use crate::tests::fakes::{fake_byron_address, fake_anchor, fake_change_address, fake_default_tx_builder, fake_linear_fee, fake_reallistic_tx_builder, fake_redeemer, fake_redeemer_zero_cost, fake_rich_tx_builder, fake_tx_builder, fake_tx_builder_with_amount, fake_tx_builder_with_fee, fake_tx_builder_with_fee_and_pure_change, fake_tx_builder_with_fee_and_val_size, fake_tx_builder_with_key_deposit, fake_base_address, fake_bytes_32, fake_data_hash, fake_key_hash, fake_plutus_script_and_hash, fake_policy_id, fake_script_hash, fake_tx_hash, fake_tx_input, fake_tx_input2, fake_value, fake_value2, fake_vkey_witness, fake_root_key_15, fake_base_address_with_payment_cred, fake_bootsrap_witness_with_attrs, fake_plutus_script, fake_base_script_address}; use crate::*; use crate::builders::fakes::fake_private_key; @@ -6605,18 +6605,130 @@ fn tx_builder_min_fee_big_automatic_fee_test() { } #[test] -fn tx_builder_exact_fee_burn_extra_issue() { +fn tx_builder_add_utxo_with_inlined_script() { let mut tx_builder = fake_reallistic_tx_builder(); - let change_address = fake_base_address(1); + let mut inputs_builder = TxInputsBuilder::new(); - let manual_fee = BigNum(100u64); + let utxo_address_1 = fake_base_address(1); + let utxo_address_2 = fake_base_script_address(2); + let utxo_address_3 = fake_base_script_address(3); - let input= fake_tx_input(1); - let utxo_address = fake_base_address(2); - let utxo_value = Value::new(&Coin::from(1000u64)); - tx_builder.add_regular_input(&utxo_address, &input, &utxo_value).unwrap(); - tx_builder.set_fee(&manual_fee); - let change_res = tx_builder.add_inputs_from_and_change(&TransactionUnspentOutputs::new(), CoinSelectionStrategyCIP2::LargestFirstMultiAsset, &ChangeConfig::new(&change_address)); + let utxo_input_1 = fake_tx_input(1); + let utxo_input_2 = fake_tx_input(2); + let utxo_input_3 = fake_tx_input(3); - assert!(change_res.is_err()); + let utxo_value_1 = Value::new(&Coin::from(10000000u64)); + let utxo_value_2 = Value::new(&Coin::from(20000000u64)); + let utxo_value_3 = Value::new(&Coin::from(30000000u64)); + + let mut utxo_output_1 = TransactionOutput::new(&utxo_address_1, &utxo_value_1); + let mut utxo_output_2 = TransactionOutput::new(&utxo_address_2, &utxo_value_2); + let mut utxo_output_3 = TransactionOutput::new(&utxo_address_3, &utxo_value_3); + + let plutus_script_1 = fake_plutus_script(1, &Language::new_plutus_v2()); + let plutus_script_2 = fake_plutus_script(2, &Language::new_plutus_v2()); + let plutus_script_3 = fake_plutus_script(3, &Language::new_plutus_v2()); + + let script_ref_1 = ScriptRef::new_plutus_script(&plutus_script_1); + let script_ref_2 = ScriptRef::new_plutus_script(&plutus_script_2); + let script_ref_3 = ScriptRef::new_plutus_script(&plutus_script_3); + + let script_ref_size_1 = script_ref_1.to_unwrapped_bytes().len(); + let script_ref_size_2 = script_ref_2.to_unwrapped_bytes().len(); + let script_ref_size_3 = script_ref_3.to_unwrapped_bytes().len(); + + utxo_output_1.set_script_ref(&script_ref_1); + utxo_output_2.set_script_ref(&script_ref_2); + utxo_output_3.set_script_ref(&script_ref_3); + + let utxo_1 = TransactionUnspentOutput::new(&utxo_input_1, &utxo_output_1); + let utxo_2 = TransactionUnspentOutput::new(&utxo_input_2, &utxo_output_2); + let utxo_3 = TransactionUnspentOutput::new(&utxo_input_3, &utxo_output_3); + + inputs_builder.add_regular_utxo(&utxo_1).expect("Failed to add utxo"); + + let native_script = NativeScript::new_timelock_start(&TimelockStart::new_timelockstart(&BigNum(1000))); + let native_script_source = NativeScriptSource::new(&native_script); + + inputs_builder.add_native_script_utxo(&utxo_2, &native_script_source).expect("Failed to add utxo"); + + let datum = PlutusData::new_empty_constr_plutus_data(&BigNum(1000)); + let redeemer = Redeemer::new( + &RedeemerTag::new_spend(), + &BigNum(1000), + &datum, + &ExUnits::new(&BigNum(1000), &BigNum(1000)), + ); + let plutus_script = fake_plutus_script(4, &Language::new_plutus_v2()); + let plutus_script_source = PlutusScriptSource::new(&plutus_script); + let datum_source = DatumSource::new(&datum); + + let plutus_witness = PlutusWitness::new_with_ref(&plutus_script_source, &datum_source, &redeemer); + + inputs_builder.add_plutus_script_utxo(&utxo_3, &plutus_witness).expect("Failed to add utxo"); + + tx_builder.set_inputs(&inputs_builder); + + let ref_scripts_size = tx_builder.get_total_ref_scripts_size().expect("Failed to get total ref scripts size"); + assert_eq!(ref_scripts_size, script_ref_size_1 + script_ref_size_2 + script_ref_size_3); +} + +#[test] +fn tx_builder_utxo_selection_with_inlined_script() { + let mut tx_builder = fake_reallistic_tx_builder(); + + let utxo_address_1 = fake_base_address(1); + let utxo_address_2 = fake_base_address(2); + let utxo_address_3 = fake_base_address(3); + + let utxo_input_1 = fake_tx_input(1); + let utxo_input_2 = fake_tx_input(2); + let utxo_input_3 = fake_tx_input(3); + + let utxo_value_1 = Value::new(&Coin::from(10000000u64)); + let utxo_value_2 = Value::new(&Coin::from(20000000u64)); + let utxo_value_3 = Value::new(&Coin::from(1000000u64)); + + let mut utxo_output_1 = TransactionOutput::new(&utxo_address_1, &utxo_value_1); + let mut utxo_output_2 = TransactionOutput::new(&utxo_address_2, &utxo_value_2); + let mut utxo_output_3 = TransactionOutput::new(&utxo_address_3, &utxo_value_3); + + let plutus_script_1 = fake_plutus_script(1, &Language::new_plutus_v2()); + let plutus_script_2 = fake_plutus_script(2, &Language::new_plutus_v2()); + let plutus_script_3 = fake_plutus_script(3, &Language::new_plutus_v2()); + + let script_ref_1 = ScriptRef::new_plutus_script(&plutus_script_1); + let script_ref_2 = ScriptRef::new_plutus_script(&plutus_script_2); + let script_ref_3 = ScriptRef::new_plutus_script(&plutus_script_3); + + let script_ref_size_1 = script_ref_1.to_unwrapped_bytes().len(); + let script_ref_size_2 = script_ref_2.to_unwrapped_bytes().len(); + let script_ref_size_3 = script_ref_3.to_unwrapped_bytes().len(); + + utxo_output_1.set_script_ref(&script_ref_1); + utxo_output_2.set_script_ref(&script_ref_2); + utxo_output_3.set_script_ref(&script_ref_3); + + let mut utxos = TransactionUnspentOutputs::new(); + utxos.add(&TransactionUnspentOutput::new(&utxo_input_1, &utxo_output_1)); + utxos.add(&TransactionUnspentOutput::new(&utxo_input_2, &utxo_output_2)); + utxos.add(&TransactionUnspentOutput::new(&utxo_input_3, &utxo_output_3)); + + tx_builder.add_output(&TransactionOutput::new( + &fake_base_address(4), + &Value::new(&Coin::from(30000000u64)) + )).expect("Failed to add output"); + + let change_config = ChangeConfig::new(&fake_base_address(5)); + + let res = tx_builder.add_inputs_from_and_change(&utxos, CoinSelectionStrategyCIP2::LargestFirstMultiAsset, &change_config); + assert!(res.is_ok()); + + let ref_scripts_size = tx_builder.get_total_ref_scripts_size().expect("Failed to get total ref scripts size"); + assert_eq!(ref_scripts_size, script_ref_size_1 + script_ref_size_2 + script_ref_size_3); + + let tx = tx_builder.build_tx().expect("Failed to build tx"); + + let inputs = tx.body().inputs(); + assert_eq!(inputs.len(), 3); } \ No newline at end of file