1use crate::cardano_keys::CardanoPaymentSigningKey;
2use crate::plutus_script::PlutusScript;
3use cardano_serialization_lib::*;
4use fraction::{FromPrimitive, Ratio};
5use ogmios_client::query_ledger_state::ReferenceScriptsCosts;
6use ogmios_client::transactions::Transactions;
7use ogmios_client::{
8 query_ledger_state::{PlutusCostModels, ProtocolParametersResponse, QueryLedgerState},
9 query_network::QueryNetwork,
10 transactions::OgmiosEvaluateTransactionResponse,
11 types::{OgmiosUtxo, OgmiosValue},
12};
13use sidechain_domain::{AssetId, NetworkType, UtxoId};
14use std::collections::HashMap;
15
16pub fn transaction_from_bytes(cbor: Vec<u8>) -> anyhow::Result<Transaction> {
18 Transaction::from_bytes(cbor).map_err(|e| anyhow::anyhow!(e))
19}
20
21pub fn vkey_witness_from_bytes(cbor: Vec<u8>) -> anyhow::Result<Vkeywitness> {
23 Vkeywitness::from_bytes(cbor).map_err(|e| anyhow::anyhow!(e))
24}
25
26pub(crate) fn plutus_script_hash(script_bytes: &[u8], language: Language) -> [u8; 28] {
27 let mut buf: Vec<u8> = vec![language_to_u8(language)];
29 buf.extend(script_bytes);
30 sidechain_domain::crypto::blake2b(buf.as_slice())
31}
32
33pub fn script_address(script_bytes: &[u8], network: NetworkIdKind, language: Language) -> Address {
35 let script_hash = plutus_script_hash(script_bytes, language);
36 EnterpriseAddress::new(
37 network_id_kind_to_u8(network),
38 &Credential::from_scripthash(&script_hash.into()),
39 )
40 .to_address()
41}
42
43pub fn payment_address(cardano_verification_key_bytes: &[u8], network: NetworkIdKind) -> Address {
45 let key_hash = sidechain_domain::crypto::blake2b(cardano_verification_key_bytes);
46 EnterpriseAddress::new(
47 network_id_kind_to_u8(network),
48 &Credential::from_keyhash(&key_hash.into()),
49 )
50 .to_address()
51}
52
53pub fn key_hash_address(pub_key_hash: &Ed25519KeyHash, network: NetworkIdKind) -> Address {
55 EnterpriseAddress::new(network_id_kind_to_u8(network), &Credential::from_keyhash(pub_key_hash))
56 .to_address()
57}
58
59pub trait NetworkTypeExt {
61 fn to_csl(&self) -> NetworkIdKind;
63}
64impl NetworkTypeExt for NetworkType {
65 fn to_csl(&self) -> NetworkIdKind {
66 match self {
67 Self::Mainnet => NetworkIdKind::Mainnet,
68 Self::Testnet => NetworkIdKind::Testnet,
69 }
70 }
71}
72
73fn network_id_kind_to_u8(network: NetworkIdKind) -> u8 {
74 match network {
75 NetworkIdKind::Mainnet => 1,
76 NetworkIdKind::Testnet => 0,
77 }
78}
79
80fn language_to_u8(language: Language) -> u8 {
81 match language.kind() {
82 LanguageKind::PlutusV1 => 1,
83 LanguageKind::PlutusV2 => 2,
84 LanguageKind::PlutusV3 => 3,
85 }
86}
87
88pub(crate) fn get_builder_config(
91 context: &TransactionContext,
92) -> Result<TransactionBuilderConfig, JsError> {
93 let protocol_parameters = &context.protocol_parameters;
94 TransactionBuilderConfigBuilder::new()
95 .fee_algo(&linear_fee(protocol_parameters))
96 .pool_deposit(&protocol_parameters.stake_pool_deposit.to_csl()?.coin())
97 .key_deposit(&protocol_parameters.stake_credential_deposit.to_csl()?.coin())
98 .max_value_size(protocol_parameters.max_value_size.bytes)
99 .max_tx_size(protocol_parameters.max_transaction_size.bytes)
100 .ex_unit_prices(&ExUnitPrices::new(
101 &ratio_to_unit_interval(&protocol_parameters.script_execution_prices.memory),
102 &ratio_to_unit_interval(&protocol_parameters.script_execution_prices.cpu),
103 ))
104 .coins_per_utxo_byte(&protocol_parameters.min_utxo_deposit_coefficient.into())
105 .ref_script_coins_per_byte(&convert_reference_script_costs(
106 &protocol_parameters.min_fee_reference_scripts.clone(),
107 )?)
108 .deduplicate_explicit_ref_inputs_with_regular_inputs(true)
109 .build()
110}
111
112fn linear_fee(protocol_parameters: &ProtocolParametersResponse) -> LinearFee {
113 let constant: BigNum = protocol_parameters.min_fee_constant.lovelace.into();
114 LinearFee::new(&protocol_parameters.min_fee_coefficient.into(), &constant)
115}
116
117fn ratio_to_unit_interval(ratio: &fraction::Ratio<u64>) -> UnitInterval {
118 UnitInterval::new(&(*ratio.numer()).into(), &(*ratio.denom()).into())
119}
120
121pub trait OgmiosValueExt {
123 fn to_csl(&self) -> Result<Value, JsError>;
126}
127
128impl OgmiosValueExt for OgmiosValue {
129 fn to_csl(&self) -> Result<Value, JsError> {
130 if !self.native_tokens.is_empty() {
131 let mut multiasset = MultiAsset::new();
132 for (policy_id, assets) in self.native_tokens.iter() {
133 let mut csl_assets = Assets::new();
134 for asset in assets.iter() {
135 let asset_name = AssetName::new(asset.name.clone()).map_err(|e| {
136 JsError::from_str(&format!(
137 "Could not convert Ogmios UTXO value, asset name is invalid: '{}'",
138 e
139 ))
140 })?;
141 csl_assets.insert(&asset_name, &asset.amount.into());
142 }
143 multiasset.insert(&ScriptHash::from(*policy_id), &csl_assets);
144 }
145 Ok(Value::new_with_assets(&self.lovelace.into(), &multiasset))
146 } else {
147 Ok(Value::new(&self.lovelace.into()))
148 }
149 }
150}
151
152pub(crate) fn convert_cost_models(m: &PlutusCostModels) -> Costmdls {
154 let mut mdls = Costmdls::new();
155 mdls.insert(&Language::new_plutus_v1(), &CostModel::from(m.plutus_v1.to_owned()));
156 mdls.insert(&Language::new_plutus_v2(), &CostModel::from(m.plutus_v2.to_owned()));
157 mdls.insert(&Language::new_plutus_v3(), &CostModel::from(m.plutus_v3.to_owned()));
158 mdls
159}
160
161pub(crate) fn convert_reference_script_costs(
162 costs: &ReferenceScriptsCosts,
163) -> Result<UnitInterval, JsError> {
164 let r = Ratio::<u64>::from_f64(costs.base).ok_or_else(|| {
165 JsError::from_str(&format!("Failed to decode cost base {} as a u64 ratio", costs.base))
166 })?;
167 let numerator = BigNum::from(*r.numer());
168 let denominator = BigNum::from(*r.denom());
169 Ok(UnitInterval::new(&numerator, &denominator))
170}
171
172fn ex_units_from_response(resp: OgmiosEvaluateTransactionResponse) -> ExUnits {
173 ExUnits::new(&resp.budget.memory.into(), &resp.budget.cpu.into())
174}
175
176pub(crate) enum Costs {
178 ZeroCosts,
180 Costs {
182 mints: HashMap<cardano_serialization_lib::ScriptHash, ExUnits>,
184 spends: HashMap<u32, ExUnits>,
186 },
187}
188
189pub(crate) trait CostStore {
191 fn get_mint(&self, script: &PlutusScript) -> ExUnits;
193 fn get_spend(&self, spend_ix: u32) -> ExUnits;
195 fn get_one_spend(&self) -> ExUnits;
198 fn get_spend_indices(&self) -> Vec<u32>;
201}
202
203impl CostStore for Costs {
204 fn get_mint(&self, script: &PlutusScript) -> ExUnits {
205 match self {
206 Costs::ZeroCosts => zero_ex_units(),
207 Costs::Costs { mints, .. } => mints
208 .get(&script.csl_script_hash())
209 .expect("get_mint should not be called with an unknown script")
210 .clone(),
211 }
212 }
213 fn get_spend(&self, spend_ix: u32) -> ExUnits {
214 match self {
215 Costs::ZeroCosts => zero_ex_units(),
216 Costs::Costs { spends, .. } => spends
217 .get(&spend_ix)
218 .expect("get_spend should not be called with an unknown spend index")
219 .clone(),
220 }
221 }
222 fn get_one_spend(&self) -> ExUnits {
223 match self {
224 Costs::ZeroCosts => zero_ex_units(),
225 Costs::Costs { spends, .. } => match spends.values().collect::<Vec<_>>()[..] {
226 [x] => x.clone(),
227 _ => panic!(
228 "get_one_spend should only be called when exactly one spend is expected to be present"
229 ),
230 },
231 }
232 }
233 fn get_spend_indices(&self) -> Vec<u32> {
234 match self {
235 Costs::ZeroCosts => vec![],
236 Costs::Costs { spends, .. } => spends.keys().cloned().collect(),
237 }
238 }
239}
240
241impl Costs {
242 #[cfg(test)]
243 pub(crate) fn new(
245 mints: HashMap<cardano_serialization_lib::ScriptHash, ExUnits>,
246 spends: HashMap<u32, ExUnits>,
247 ) -> Costs {
248 Costs::Costs { mints, spends }
249 }
250
251 pub(crate) async fn calculate_costs<T: Transactions, F>(
258 make_tx: F,
259 client: &T,
260 ) -> anyhow::Result<Transaction>
261 where
262 F: Fn(Costs) -> anyhow::Result<Transaction>,
263 {
264 let tx = make_tx(Costs::ZeroCosts)?;
266 let costs = Self::from_ogmios(&tx, client).await?;
268
269 let tx = make_tx(costs)?;
270 let costs = Self::from_ogmios(&tx, client).await?;
272
273 make_tx(costs)
274 }
275
276 async fn from_ogmios<T: Transactions>(tx: &Transaction, client: &T) -> anyhow::Result<Costs> {
277 let evaluate_response = client.evaluate_transaction(&tx.to_bytes()).await?;
278
279 let mut mints = HashMap::new();
280 let mut spends = HashMap::new();
281 for er in evaluate_response {
282 match er.validator.purpose.as_str() {
283 "mint" => {
284 mints.insert(
285 tx.body()
286 .mint()
287 .expect(
288 "tx.body.mint() should not be empty if we received a 'mint' response from Ogmios",
289 )
290 .keys()
291 .get(er.validator.index as usize),
292 ex_units_from_response(er),
293 );
294 },
295 "spend" => {
296 spends.insert(er.validator.index, ex_units_from_response(er));
297 },
298 _ => {},
299 }
300 }
301
302 Ok(Costs::Costs { mints, spends })
303 }
304}
305
306pub(crate) fn empty_asset_name() -> AssetName {
307 AssetName::new(vec![]).expect("Hardcoded empty asset name is valid")
308}
309
310fn zero_ex_units() -> ExUnits {
311 ExUnits::new(&BigNum::zero(), &BigNum::zero())
312}
313
314pub(crate) trait OgmiosUtxoExt {
315 fn to_csl_tx_input(&self) -> TransactionInput;
316 fn to_csl_tx_output(&self) -> Result<TransactionOutput, JsError>;
317 fn to_csl(&self) -> Result<TransactionUnspentOutput, JsError>;
318
319 fn get_asset_amount(&self, asset: &AssetId) -> u64;
320
321 fn get_plutus_data(&self) -> Option<PlutusData>;
322}
323
324impl OgmiosUtxoExt for OgmiosUtxo {
325 fn to_csl_tx_input(&self) -> TransactionInput {
326 TransactionInput::new(&TransactionHash::from(self.transaction.id), self.index.into())
327 }
328
329 fn to_csl_tx_output(&self) -> Result<TransactionOutput, JsError> {
330 let mut tx_out = TransactionOutput::new(
331 &Address::from_bech32(&self.address).map_err(|e| {
332 JsError::from_str(&format!("Couldn't convert address from ogmios: '{}'", e))
333 })?,
334 &self.value.to_csl()?,
335 );
336 if let Some(script) = self.script.clone() {
337 let plutus_script_ref_opt =
338 script.clone().try_into().ok().map(|plutus_script: PlutusScript| {
339 ScriptRef::new_plutus_script(&plutus_script.to_csl())
340 });
341 let script_ref_opt = plutus_script_ref_opt.or_else(|| {
342 NativeScript::from_bytes(script.cbor)
343 .ok()
344 .map(|native_script| ScriptRef::new_native_script(&native_script))
345 });
346 if let Some(script_ref) = script_ref_opt {
347 tx_out.set_script_ref(&script_ref);
348 }
349 }
350 if let Some(data) = self.get_plutus_data() {
351 tx_out.set_plutus_data(&data);
352 }
353 Ok(tx_out)
354 }
355
356 fn to_csl(&self) -> Result<TransactionUnspentOutput, JsError> {
357 Ok(TransactionUnspentOutput::new(&self.to_csl_tx_input(), &self.to_csl_tx_output()?))
358 }
359
360 fn get_asset_amount(&self, asset_id: &AssetId) -> u64 {
361 self.value
362 .native_tokens
363 .get(&asset_id.policy_id.0)
364 .cloned()
365 .unwrap_or_default()
366 .iter()
367 .find(|asset| asset.name == asset_id.asset_name.0.to_vec())
368 .map_or_else(|| 0, |asset| asset.amount)
369 }
370
371 fn get_plutus_data(&self) -> Option<PlutusData> {
372 (self.datum.as_ref())
373 .map(|datum| datum.bytes.clone())
374 .and_then(|bytes| PlutusData::from_bytes(bytes).ok())
375 }
376}
377
378pub trait UtxoIdExt {
380 fn to_csl(&self) -> TransactionInput;
382}
383
384impl UtxoIdExt for UtxoId {
385 fn to_csl(&self) -> TransactionInput {
386 TransactionInput::new(&TransactionHash::from(self.tx_hash.0), self.index.0.into())
387 }
388}
389
390#[derive(Clone)]
391pub(crate) struct TransactionContext {
392 pub(crate) payment_key: CardanoPaymentSigningKey,
394 pub(crate) payment_key_utxos: Vec<OgmiosUtxo>,
397 pub(crate) network: NetworkIdKind,
398 pub(crate) protocol_parameters: ProtocolParametersResponse,
399 pub(crate) change_address: Address,
400}
401
402impl TransactionContext {
403 pub(crate) async fn for_payment_key<C: QueryLedgerState + QueryNetwork>(
406 payment_key: &CardanoPaymentSigningKey,
407 client: &C,
408 ) -> Result<TransactionContext, anyhow::Error> {
409 let payment_key = payment_key.clone();
410 let network = client.shelley_genesis_configuration().await?.network.to_csl();
411 let protocol_parameters = client.query_protocol_parameters().await?;
412 let payment_address = key_hash_address(&payment_key.0.to_public().hash(), network);
413 let payment_key_utxos = client.query_utxos(&[payment_address.to_bech32(None)?]).await?;
414 Ok(TransactionContext {
415 payment_key,
416 payment_key_utxos,
417 network,
418 protocol_parameters,
419 change_address: payment_address,
420 })
421 }
422
423 pub(crate) fn with_change_address(&self, change_address: &Address) -> Self {
424 Self {
425 payment_key: self.payment_key.clone(),
426 payment_key_utxos: self.payment_key_utxos.clone(),
427 network: self.network,
428 protocol_parameters: self.protocol_parameters.clone(),
429 change_address: change_address.clone(),
430 }
431 }
432
433 pub(crate) fn payment_key_hash(&self) -> Ed25519KeyHash {
434 self.payment_key.0.to_public().hash()
435 }
436
437 pub(crate) fn sign(&self, tx: &Transaction) -> Transaction {
438 let tx_hash: [u8; 32] = sidechain_domain::crypto::blake2b(tx.body().to_bytes().as_ref());
439 let signature = self.payment_key.0.sign(&tx_hash);
440 let mut witness_set = tx.witness_set();
441 let mut vkeywitnesses = witness_set.vkeys().unwrap_or_else(Vkeywitnesses::new);
442 vkeywitnesses
443 .add(&Vkeywitness::new(&Vkey::new(&self.payment_key.0.to_public()), &signature));
444 witness_set.set_vkeys(&vkeywitnesses);
445 Transaction::new(&tx.body(), &witness_set, tx.auxiliary_data())
446 }
447}
448
449pub(crate) trait OgmiosUtxosExt {
450 fn to_csl(&self) -> Result<TransactionUnspentOutputs, JsError>;
451}
452
453impl OgmiosUtxosExt for [OgmiosUtxo] {
454 fn to_csl(&self) -> Result<TransactionUnspentOutputs, JsError> {
455 let mut utxos = TransactionUnspentOutputs::new();
456 for utxo in self {
457 utxos.add(&utxo.to_csl()?);
458 }
459 Ok(utxos)
460 }
461}
462
463pub(crate) trait TransactionBuilderExt {
464 fn add_output_with_one_script_token(
466 &mut self,
467 validator: &PlutusScript,
468 policy: &PlutusScript,
469 datum: &PlutusData,
470 ctx: &TransactionContext,
471 ) -> Result<(), JsError>;
472
473 fn add_collateral_inputs(
475 &mut self,
476 ctx: &TransactionContext,
477 inputs: &[OgmiosUtxo],
478 ) -> Result<(), JsError>;
479
480 fn add_mint_one_script_token(
482 &mut self,
483 script: &PlutusScript,
484 asset_name: &AssetName,
485 redeemer_data: &PlutusData,
486 ex_units: &ExUnits,
487 ) -> Result<(), JsError>;
488
489 fn add_mint_script_tokens(
490 &mut self,
491 script: &PlutusScript,
492 asset_name: &AssetName,
493 redeemer_data: &PlutusData,
494 ex_units: &ExUnits,
495 amount: &Int,
496 ) -> Result<(), JsError>;
497
498 fn add_mint_script_token_using_reference_script(
503 &mut self,
504 script: &Script,
505 ref_input: &TransactionInput,
506 amount: &Int,
507 costs: &Costs,
508 ) -> Result<(), JsError>;
509
510 fn add_mint_one_script_token_using_reference_script(
515 &mut self,
516 script: &Script,
517 ref_input: &TransactionInput,
518 costs: &Costs,
519 ) -> Result<(), JsError> {
520 self.add_mint_script_token_using_reference_script(
521 script,
522 ref_input,
523 &Int::new_i32(1),
524 costs,
525 )
526 }
527
528 fn balance_update_and_build(
532 &mut self,
533 ctx: &TransactionContext,
534 ) -> Result<Transaction, JsError>;
535}
536
537impl TransactionBuilderExt for TransactionBuilder {
538 fn add_output_with_one_script_token(
539 &mut self,
540 validator: &PlutusScript,
541 policy: &PlutusScript,
542 datum: &PlutusData,
543 ctx: &TransactionContext,
544 ) -> Result<(), JsError> {
545 let amount_builder = TransactionOutputBuilder::new()
546 .with_address(&validator.address(ctx.network))
547 .with_plutus_data(datum)
548 .next()?;
549 let ma = MultiAsset::new().with_asset_amount(&policy.empty_name_asset(), 1u64)?;
550 let output = amount_builder.with_minimum_ada_and_asset(&ma, ctx)?.build()?;
551 self.add_output(&output)
552 }
553
554 fn add_collateral_inputs(
555 &mut self,
556 ctx: &TransactionContext,
557 inputs: &[OgmiosUtxo],
558 ) -> Result<(), JsError> {
559 let mut collateral_builder = TxInputsBuilder::new();
560 for utxo in inputs.iter() {
561 collateral_builder.add_regular_input(
562 &key_hash_address(&ctx.payment_key_hash(), ctx.network),
563 &utxo.to_csl_tx_input(),
564 &utxo.value.to_csl()?,
565 )?;
566 }
567 self.set_collateral(&collateral_builder);
568 Ok(())
569 }
570
571 fn add_mint_one_script_token(
572 &mut self,
573 script: &PlutusScript,
574 asset_name: &AssetName,
575 redeemer_data: &PlutusData,
576 ex_units: &ExUnits,
577 ) -> Result<(), JsError> {
578 let mut mint_builder = self.get_mint_builder().unwrap_or(MintBuilder::new());
579
580 let validator_source = PlutusScriptSource::new(&script.to_csl());
581 let mint_witness = MintWitness::new_plutus_script(
582 &validator_source,
583 &Redeemer::new(&RedeemerTag::new_mint(), &0u32.into(), redeemer_data, ex_units),
584 );
585 mint_builder.add_asset(&mint_witness, asset_name, &Int::new_i32(1))?;
586 self.set_mint_builder(&mint_builder);
587 Ok(())
588 }
589
590 fn add_mint_script_tokens(
591 &mut self,
592 script: &PlutusScript,
593 asset_name: &AssetName,
594 redeemer_data: &PlutusData,
595 ex_units: &ExUnits,
596 amount: &Int,
597 ) -> Result<(), JsError> {
598 let mut mint_builder = self.get_mint_builder().unwrap_or(MintBuilder::new());
599
600 let validator_source = PlutusScriptSource::new(&script.to_csl());
601 let mint_witness = MintWitness::new_plutus_script(
602 &validator_source,
603 &Redeemer::new(&RedeemerTag::new_mint(), &0u32.into(), redeemer_data, ex_units),
604 );
605 mint_builder.add_asset(&mint_witness, asset_name, amount)?;
606 self.set_mint_builder(&mint_builder);
607 Ok(())
608 }
609
610 fn add_mint_script_token_using_reference_script(
611 &mut self,
612 script: &Script,
613 ref_input: &TransactionInput,
614 amount: &Int,
615 costs: &Costs,
616 ) -> Result<(), JsError> {
617 let mut mint_builder = self.get_mint_builder().unwrap_or(MintBuilder::new());
618
619 match script {
620 Script::Plutus(script) => {
621 let source = PlutusScriptSource::new_ref_input(
622 &script.csl_script_hash(),
623 ref_input,
624 &script.language,
625 script.bytes.len(),
626 );
627 let mint_witness = MintWitness::new_plutus_script(
628 &source,
629 &Redeemer::new(
630 &RedeemerTag::new_mint(),
631 &0u32.into(),
632 &unit_plutus_data(),
633 &costs.get_mint(script),
634 ),
635 );
636 mint_builder.add_asset(&mint_witness, &empty_asset_name(), amount)?;
637 self.set_mint_builder(&mint_builder);
638 },
639 Script::Native(script) => {
640 let source = NativeScriptSource::new(script);
642 let mint_witness = MintWitness::new_native_script(&source);
643 mint_builder.add_asset(&mint_witness, &empty_asset_name(), amount)?;
644 self.set_mint_builder(&mint_builder);
645 self.add_reference_input(ref_input);
646 },
647 }
648 Ok(())
649 }
650
651 fn balance_update_and_build(
652 &mut self,
653 ctx: &TransactionContext,
654 ) -> Result<Transaction, JsError> {
655 fn max_possible_collaterals(ctx: &TransactionContext) -> Vec<OgmiosUtxo> {
656 let mut utxos = ctx.payment_key_utxos.clone();
657 utxos.sort_by(|a, b| b.value.lovelace.cmp(&a.value.lovelace));
658 let max_inputs = ctx.protocol_parameters.max_collateral_inputs;
659 utxos
660 .into_iter()
661 .take(max_inputs.try_into().expect("max_collateral_input fit in usize"))
662 .collect()
663 }
664 fn try_balance(
666 builder: &mut TransactionBuilder,
667 collateral_inputs: &[OgmiosUtxo],
668 ctx: &TransactionContext,
669 ) -> Result<Transaction, JsError> {
670 builder.add_required_signer(&ctx.payment_key_hash());
671 if collateral_inputs.is_empty() {
672 builder.add_inputs_from_and_change(
673 &ctx.payment_key_utxos.to_csl()?,
674 CoinSelectionStrategyCIP2::LargestFirstMultiAsset,
675 &ChangeConfig::new(&ctx.change_address),
676 )?;
677 } else {
678 builder.add_collateral_inputs(ctx, collateral_inputs)?;
679 builder.set_script_data_hash(&[0u8; 32].into());
680 builder.add_inputs_from_and_change_with_collateral_return(
682 &ctx.payment_key_utxos.to_csl()?,
683 CoinSelectionStrategyCIP2::LargestFirstMultiAsset,
684 &ChangeConfig::new(&ctx.change_address),
685 &ctx.protocol_parameters.collateral_percentage.into(),
686 )?;
687 builder.calc_script_data_hash(&convert_cost_models(
688 &ctx.protocol_parameters.plutus_cost_models,
689 ))?;
690 }
691 builder.build_tx()
692 }
693 let mut selected = vec![];
696 for input in max_possible_collaterals(ctx) {
697 let mut builder = self.clone();
698 let result = try_balance(&mut builder, &selected, ctx);
700 if result.is_ok() {
701 return result;
702 }
703 selected.push(input);
704 }
705
706 let balanced_transaction =
707 try_balance(self, &selected, ctx)
708 .map_err(|e| JsError::from_str(&format!("Could not balance transaction. Usually it means that the payment key does not own UTXO set required to cover transaction outputs and fees or to provide collateral. Cause: {}", e)))?;
709
710 debug_assert!(
711 balanced_transaction.body().collateral().is_some(),
712 "BUG: Balanced transaction should have collateral set."
713 );
714 debug_assert!(
715 balanced_transaction.body().collateral_return().is_some(),
716 "BUG: Balanced transaction should have collateral returned."
717 );
718
719 Ok(balanced_transaction)
720 }
721}
722
723#[derive(Clone, Debug)]
724pub enum Script {
726 Plutus(PlutusScript),
728 Native(NativeScript),
730}
731
732impl Script {
733 #[cfg(test)]
734 pub(crate) fn script_hash(&self) -> [u8; 28] {
735 match self {
736 Self::Plutus(script) => script.script_hash(),
737 Self::Native(script) => {
738 script.hash().to_bytes().try_into().expect("CSL script hash is always 28 bytes")
739 },
740 }
741 }
742}
743
744pub(crate) trait TransactionOutputAmountBuilderExt: Sized {
745 fn get_minimum_ada(&self, ctx: &TransactionContext) -> Result<BigNum, JsError>;
746 fn with_minimum_ada(self, ctx: &TransactionContext) -> Result<Self, JsError>;
747 fn with_minimum_ada_and_asset(
748 self,
749 ma: &MultiAsset,
750 ctx: &TransactionContext,
751 ) -> Result<Self, JsError>;
752}
753
754impl TransactionOutputAmountBuilderExt for TransactionOutputAmountBuilder {
755 fn get_minimum_ada(&self, ctx: &TransactionContext) -> Result<BigNum, JsError> {
756 MinOutputAdaCalculator::new(
757 &self.build()?,
758 &DataCost::new_coins_per_byte(
759 &ctx.protocol_parameters.min_utxo_deposit_coefficient.into(),
760 ),
761 )
762 .calculate_ada()
763 }
764
765 fn with_minimum_ada(self, ctx: &TransactionContext) -> Result<Self, JsError> {
766 let min_ada = self.with_coin(&0u64.into()).get_minimum_ada(ctx)?;
767 Ok(self.with_coin(&min_ada))
768 }
769
770 fn with_minimum_ada_and_asset(
771 self,
772 ma: &MultiAsset,
773 ctx: &TransactionContext,
774 ) -> Result<Self, JsError> {
775 let min_ada = self.with_coin_and_asset(&0u64.into(), ma).get_minimum_ada(ctx)?;
776 Ok(self.with_coin_and_asset(&min_ada, ma))
777 }
778}
779
780pub(crate) trait InputsBuilderExt: Sized {
781 fn add_script_utxo_input(
782 &mut self,
783 utxo: &OgmiosUtxo,
784 script: &PlutusScript,
785 data: &PlutusData,
786 ex_units: &ExUnits,
787 ) -> Result<(), JsError>;
788
789 fn add_regular_inputs(&mut self, utxos: &[OgmiosUtxo]) -> Result<(), JsError>;
791
792 fn with_regular_inputs(utxos: &[OgmiosUtxo]) -> Result<Self, JsError>;
793}
794
795impl InputsBuilderExt for TxInputsBuilder {
796 fn add_script_utxo_input(
797 &mut self,
798 utxo: &OgmiosUtxo,
799 script: &PlutusScript,
800 data: &PlutusData,
801 ex_units: &ExUnits,
802 ) -> Result<(), JsError> {
803 let input = utxo.to_csl_tx_input();
804 let amount = &utxo.value.to_csl()?;
805 let witness = PlutusWitness::new_without_datum(
806 &script.to_csl(),
807 &Redeemer::new(
808 &RedeemerTag::new_spend(),
809 &0u32.into(),
811 data,
812 ex_units,
813 ),
814 );
815 self.add_plutus_script_input(&witness, &input, amount);
816 Ok(())
817 }
818
819 fn add_regular_inputs(&mut self, utxos: &[OgmiosUtxo]) -> Result<(), JsError> {
820 for utxo in utxos.iter() {
821 self.add_regular_utxo(&utxo.to_csl()?)?;
822 }
823 Ok(())
824 }
825
826 fn with_regular_inputs(utxos: &[OgmiosUtxo]) -> Result<Self, JsError> {
827 let mut tx_input_builder = Self::new();
828 tx_input_builder.add_regular_inputs(utxos)?;
829 Ok(tx_input_builder)
830 }
831}
832
833pub(crate) trait AssetNameExt: Sized {
834 fn to_csl(&self) -> Result<cardano_serialization_lib::AssetName, JsError>;
835 fn from_csl(asset_name: cardano_serialization_lib::AssetName) -> Result<Self, JsError>;
836}
837
838impl AssetNameExt for sidechain_domain::AssetName {
839 fn to_csl(&self) -> Result<cardano_serialization_lib::AssetName, JsError> {
840 cardano_serialization_lib::AssetName::new(self.0.to_vec())
841 }
842 fn from_csl(asset_name: cardano_serialization_lib::AssetName) -> Result<Self, JsError> {
843 let name = asset_name.name().try_into().map_err(|err| {
844 JsError::from_str(&format!("Failed to cast CSL asset name to domain: {err:?}"))
845 })?;
846 Ok(Self(name))
847 }
848}
849
850pub(crate) trait AssetIdExt {
851 fn to_multi_asset(&self, amount: impl Into<BigNum>) -> Result<MultiAsset, JsError>;
852}
853impl AssetIdExt for AssetId {
854 fn to_multi_asset(&self, amount: impl Into<BigNum>) -> Result<MultiAsset, JsError> {
855 let mut ma = MultiAsset::new();
856 let mut assets = Assets::new();
857 assets.insert(&self.asset_name.to_csl()?, &amount.into());
858 ma.insert(&self.policy_id.0.into(), &assets);
859 Ok(ma)
860 }
861}
862
863pub(crate) trait MultiAssetExt: Sized {
864 fn from_ogmios_utxo(utxo: &OgmiosUtxo) -> Result<Self, JsError>;
865 fn with_asset_amount(self, asset: &AssetId, amount: impl Into<BigNum>)
866 -> Result<Self, JsError>;
867}
868
869impl MultiAssetExt for MultiAsset {
870 fn from_ogmios_utxo(utxo: &OgmiosUtxo) -> Result<Self, JsError> {
871 let mut ma = MultiAsset::new();
872 for (policy, policy_assets) in utxo.value.native_tokens.iter() {
873 let mut assets = Assets::new();
874 for asset in policy_assets {
875 assets.insert(
876 &cardano_serialization_lib::AssetName::new(asset.name.clone())?,
877 &asset.amount.into(),
878 );
879 }
880 ma.insert(&PolicyID::from(*policy), &assets);
881 }
882 Ok(ma)
883 }
884 fn with_asset_amount(
885 mut self,
886 asset: &AssetId,
887 amount: impl Into<BigNum>,
888 ) -> Result<Self, JsError> {
889 let policy_id = asset.policy_id.0.into();
890 let asset_name = asset.asset_name.to_csl()?;
891 let amount: BigNum = amount.into();
892 if amount > BigNum::zero() {
893 self.set_asset(&policy_id, &asset_name, &amount);
894 Ok(self)
895 } else {
896 let current_value = self.get_asset(&policy_id, &asset_name);
898 if current_value > BigNum::zero() {
899 let ma_to_sub = MultiAsset::new().with_asset_amount(asset, current_value)?;
900 Ok(self.sub(&ma_to_sub))
901 } else {
902 Ok(self)
903 }
904 }
905 }
906}
907
908pub(crate) trait TransactionExt: Sized {
909 fn remove_native_script_witnesses(self) -> Self;
911}
912
913impl TransactionExt for Transaction {
914 fn remove_native_script_witnesses(self) -> Self {
915 let ws = self.witness_set();
916 let mut new_ws = TransactionWitnessSet::new();
917 if let Some(bootstraps) = ws.bootstraps() {
918 new_ws.set_bootstraps(&bootstraps)
919 }
920 if let Some(plutus_data) = ws.plutus_data() {
921 new_ws.set_plutus_data(&plutus_data);
922 }
923 if let Some(plutus_scripts) = ws.plutus_scripts() {
924 new_ws.set_plutus_scripts(&plutus_scripts);
925 }
926 if let Some(redeemers) = ws.redeemers() {
927 new_ws.set_redeemers(&redeemers);
928 }
929 if let Some(vkeys) = ws.vkeys() {
930 new_ws.set_vkeys(&vkeys);
931 }
932 Transaction::new(&self.body(), &new_ws, self.auxiliary_data())
933 }
934}
935
936pub(crate) fn unit_plutus_data() -> PlutusData {
939 PlutusData::new_empty_constr_plutus_data(&BigNum::zero())
940}
941
942#[cfg(test)]
943mod tests {
944 use super::*;
945 use crate::plutus_script::PlutusScript;
946 use crate::test_values::protocol_parameters;
947 use cardano_serialization_lib::{AssetName, Language, NetworkIdKind};
948 use hex_literal::hex;
949 use ogmios_client::types::{Asset, OgmiosValue};
950 use pretty_assertions::assert_eq;
951
952 #[test]
953 fn candidates_script_address_test() {
954 let address = PlutusScript::from_cbor(
955 &crate::plutus_script::tests::CANDIDATES_SCRIPT_WITH_APPLIED_PARAMS,
956 Language::new_plutus_v2(),
957 )
958 .address(NetworkIdKind::Testnet);
959 assert_eq!(
960 address.to_bech32(None).unwrap(),
961 "addr_test1wpcsmvsxdjal5jxytvgd3hfntg9eav888mzfykukdjfcx2ce6knnp"
962 );
963 }
964
965 #[test]
966 fn payment_address_test() {
967 let address = payment_address(
968 &hex!("a35ef86f1622172816bb9e916aea86903b2c8d32c728ad5c9b9472be7e3c5e88"),
969 NetworkIdKind::Testnet,
970 );
971 assert_eq!(
972 address.to_bech32(None).unwrap(),
973 "addr_test1vqezxrh24ts0775hulcg3ejcwj7hns8792vnn8met6z9gwsxt87zy"
974 )
975 }
976
977 #[test]
978 fn linear_fee_test() {
979 let fee = super::linear_fee(&protocol_parameters());
980 assert_eq!(fee.constant(), 155381u32.into());
981 assert_eq!(fee.coefficient(), 44u32.into());
982 }
983
984 #[test]
985 fn ratio_to_unit_interval_test() {
986 let ratio = fraction::Ratio::new(577, 10000);
987 let unit_interval = super::ratio_to_unit_interval(&ratio);
988 assert_eq!(unit_interval.numerator(), 577u64.into());
989 assert_eq!(unit_interval.denominator(), 10000u64.into());
990 }
991
992 #[test]
993 fn convert_value_without_multi_asset_test() {
994 let ogmios_value = OgmiosValue::new_lovelace(1234567);
995 let value = &ogmios_value.to_csl().unwrap();
996 assert_eq!(value.coin(), 1234567u64.into());
997 assert_eq!(value.multiasset(), None);
998 }
999
1000 #[test]
1001 fn convert_value_with_multi_asset_test() {
1002 let ogmios_value = OgmiosValue {
1003 lovelace: 1234567,
1004 native_tokens: vec![
1005 ([0u8; 28], vec![Asset { name: vec![], amount: 111 }]),
1006 (
1007 [1u8; 28],
1008 vec![
1009 Asset { name: hex!("222222").to_vec(), amount: 222 },
1010 Asset { name: hex!("333333").to_vec(), amount: 333 },
1011 ],
1012 ),
1013 ]
1014 .into_iter()
1015 .collect(),
1016 };
1017 let value = &ogmios_value.to_csl().unwrap();
1018 assert_eq!(value.coin(), 1234567u64.into());
1019 let multiasset = value.multiasset().unwrap();
1020 assert_eq!(
1021 multiasset.get_asset(&[0u8; 28].into(), &AssetName::new(vec![]).unwrap()),
1022 111u64.into()
1023 );
1024 assert_eq!(
1025 multiasset
1026 .get_asset(&[1u8; 28].into(), &AssetName::new(hex!("222222").to_vec()).unwrap()),
1027 222u64.into()
1028 );
1029 assert_eq!(
1030 multiasset
1031 .get_asset(&[1u8; 28].into(), &AssetName::new(hex!("333333").to_vec()).unwrap()),
1032 333u64.into()
1033 );
1034 }
1035
1036 #[test]
1037 fn convert_cost_models_test() {
1038 let cost_models = super::convert_cost_models(&protocol_parameters().plutus_cost_models);
1039 assert_eq!(cost_models.keys().len(), 3);
1040 assert_eq!(
1041 cost_models
1042 .get(&Language::new_plutus_v1())
1043 .unwrap()
1044 .get(0)
1045 .unwrap()
1046 .as_i32_or_nothing()
1047 .unwrap(),
1048 898148
1049 );
1050 assert_eq!(
1051 cost_models
1052 .get(&Language::new_plutus_v2())
1053 .unwrap()
1054 .get(1)
1055 .unwrap()
1056 .as_i32_or_nothing()
1057 .unwrap(),
1058 10
1059 );
1060 assert_eq!(
1061 cost_models
1062 .get(&Language::new_plutus_v3())
1063 .unwrap()
1064 .get(0)
1065 .unwrap()
1066 .as_i32_or_nothing()
1067 .unwrap(),
1068 -900
1069 );
1070 }
1071
1072 #[test]
1073 fn ogmios_utxo_to_csl_with_plutus_script_attached() {
1074 let json = serde_json::json!(
1075 {
1076 "transaction": {
1077 "id": "1fd4a3df3e0bd48dd189878bc8e4d7419fea24c8669c84019609c897adc40f09"
1078 },
1079 "index": 0,
1080 "address": "addr_test1vq0sjaaupatuvl9x6aefdsd4whlqtfku93068qzkhf3u2rqt9cnuq",
1081 "value": {
1082 "ada": {
1083 "lovelace": 8904460
1084 }
1085 },
1086 "script": {
1087 "language": "plutus:v2",
1088 "cbor": "59072301000033233223222253232335332232353232325333573466e1d20000021323232323232332212330010030023232325333573466e1d2000002132323232323232323232332323233323333323332332332222222222221233333333333300100d00c00b00a00900800700600500400300230013574202460026ae84044c00c8c8c8c94ccd5cd19b87480000084cc8848cc00400c008c070d5d080098029aba135744002260489201035054310035573c0046aae74004dd5000998018009aba100f23232325333573466e1d20000021323232333322221233330010050040030023232325333573466e1d20000021332212330010030023020357420026600803e6ae84d5d100089814a481035054310035573c0046aae74004dd51aba1004300835742006646464a666ae68cdc3a4000004224440062a666ae68cdc3a4004004264244460020086eb8d5d08008a999ab9a3370e9002001099091118010021aba100113029491035054310035573c0046aae74004dd51aba10023300175c6ae84d5d1001111919192999ab9a3370e900100108910008a999ab9a3370e9000001099091180100198029aba10011302a491035054310035573c0046aae74004dd50009aba20013574400226046921035054310035573c0046aae74004dd500098009aba100d30013574201860046004eb4cc00404cd5d080519980200a3ad35742012646464a666ae68cdc3a40000042646466442466002006004646464a666ae68cdc3a40000042664424660020060046600aeb4d5d080098021aba1357440022604c921035054310035573c0046aae74004dd51aba10033232325333573466e1d20000021332212330010030023300575a6ae84004c010d5d09aba2001130264901035054310035573c0046aae74004dd51aba1357440064646464a666ae68cdc3a400000420482a666ae68cdc3a4004004204a2604c921035054310035573c0046aae74004dd5000911919192999ab9a3370e9000001089110010a999ab9a3370e90010010990911180180218029aba100115333573466e1d20040021122200113026491035054310035573c0046aae74004dd500089810a49035054310035573c0046aae74004dd51aba10083300175c6ae8401c8c88c008dd60009813111999aab9f0012028233502730043574200460066ae88008084ccc00c044008d5d0802998008011aba1004300275c40024464460046eac004c09088cccd55cf800901311919a8131991091980080180118031aab9d001300535573c00260086ae8800cd5d080100f98099aba1357440026ae88004d5d10009aba2001357440026ae88004d5d10009aba2001357440026ae88004d5d100089808249035054310035573c0046aae74004dd51aba10073001357426ae8801c8c8c8c94ccd5cd19b87480000084c848888c00c014dd71aba100115333573466e1d20020021321222230010053008357420022a666ae68cdc3a400800426424444600400a600c6ae8400454ccd5cd19b87480180084c848888c010014c014d5d080089808249035054310035573c0046aae74004dd500091919192999ab9a3370e900000109909111111180280418029aba100115333573466e1d20020021321222222230070083005357420022a666ae68cdc3a400800426644244444446600c012010600a6ae84004dd71aba1357440022a666ae68cdc3a400c0042664424444444660040120106eb8d5d08009bae357426ae8800454ccd5cd19b87480200084cc8848888888cc004024020dd71aba1001375a6ae84d5d10008a999ab9a3370e90050010891111110020a999ab9a3370e900600108911111100189807a49035054310035573c0046aae74004dd500091919192999ab9a3370e9000001099091180100198029aba100115333573466e1d2002002132333222122333001005004003375a6ae84008dd69aba1001375a6ae84d5d10009aba20011300e4901035054310035573c0046aae74004dd500091919192999ab9a3370e900000109909118010019bae357420022a666ae68cdc3a400400426424460020066eb8d5d080089806a481035054310035573c0046aae74004dd500091919192999ab9a3370e900000109991091980080180118029aba1001375a6ae84d5d1000898062481035054310035573c0046aae74004dd500091919192999ab9a3370e900000109bae3574200226016921035054310035573c0046aae74004dd500089803a49035054310035573c0046aae74004dd5003111999a8009002919199ab9a337126602044a66a002290001109a801112999ab9a3371e004010260260022600c006600244444444444401066e0ccdc09a9a980091111111111100291001112999a80110a99a80108008b0b0b002a4181520e00e00ca006400a400a6eb401c48800848800440084c00524010350543500232633573800200424002600644a66a002290001109a8011119b800013006003122002122122330010040032323001001223300330020020014c01051a67998a9b0001"
1089 }
1090 });
1091
1092 let ogmios_utxo: OgmiosUtxo = serde_json::from_value(json).unwrap();
1093 ogmios_utxo.to_csl().unwrap();
1094 }
1095
1096 #[test]
1097 fn ogmios_utxo_to_csl_with_native_script_attached() {
1098 let json = serde_json::json!(
1099 {
1100 "transaction": {
1101 "id": "57342ce4f30afa749bd78f0c093609366d997a1c4747d206ec7fd0aea9a35b55"
1102 },
1103 "index": 0,
1104 "address": "addr_test1wplvesjjxtg8lhyy34ak2dr9l3kz8ged3hajvcvpanfx7rcwzvtc5",
1105 "value": {
1106 "ada": {
1107 "lovelace": 1430920
1108 },
1109 "ab81fe48f392989bd215f9fdc25ece3335a248696b2a64abc1acb595": {
1110 "56657273696f6e206f7261636c65": 1
1111 }
1112 },
1113 "datum": "9f1820581cab81fe48f392989bd215f9fdc25ece3335a248696b2a64abc1acb595ff",
1114 "script": {
1115 "language": "native",
1116 "json": {
1117 "clause": "some",
1118 "atLeast": 1,
1119 "from": [
1120 {
1121 "clause": "signature",
1122 "from": "e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"
1123 }
1124 ]
1125 },
1126 "cbor": "830301818200581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"
1127 }
1128 });
1129
1130 let ogmios_utxo: OgmiosUtxo = serde_json::from_value(json).unwrap();
1131 ogmios_utxo.to_csl().unwrap();
1132 }
1133}
1134
1135#[cfg(test)]
1136mod prop_tests {
1137 use super::{
1138 OgmiosUtxoExt, TransactionBuilderExt, TransactionContext, empty_asset_name,
1139 get_builder_config, unit_plutus_data, zero_ex_units,
1140 };
1141 use crate::test_values::*;
1142 use cardano_serialization_lib::{
1143 NetworkIdKind, Transaction, TransactionBuilder, TransactionInputs, TransactionOutput, Value,
1144 };
1145 use ogmios_client::types::OgmiosValue;
1146 use ogmios_client::types::{OgmiosTx, OgmiosUtxo};
1147 use proptest::{
1148 array::uniform32,
1149 collection::{hash_set, vec},
1150 prelude::*,
1151 };
1152 use sidechain_domain::{McTxHash, UtxoId, UtxoIndex};
1153
1154 const MIN_UTXO_LOVELACE: u64 = 1000000;
1155 const FIVE_ADA: u64 = 5000000;
1156
1157 fn multi_asset_transaction_balancing_test(payment_utxos: Vec<OgmiosUtxo>) {
1158 let ctx = TransactionContext {
1159 payment_key: payment_key(),
1160 payment_key_utxos: payment_utxos.clone(),
1161 network: NetworkIdKind::Testnet,
1162 protocol_parameters: protocol_parameters(),
1163 change_address: payment_addr(),
1164 };
1165 let mut tx_builder = TransactionBuilder::new(&get_builder_config(&ctx).unwrap());
1166 tx_builder
1167 .add_mint_one_script_token(
1168 &test_policy(),
1169 &empty_asset_name(),
1170 &unit_plutus_data(),
1171 &zero_ex_units(),
1172 )
1173 .unwrap();
1174 tx_builder
1175 .add_output_with_one_script_token(
1176 &test_validator(),
1177 &test_policy(),
1178 &test_plutus_data(),
1179 &ctx,
1180 )
1181 .unwrap();
1182
1183 let tx = tx_builder.balance_update_and_build(&ctx).unwrap();
1184
1185 used_inputs_lovelace_equals_outputs_and_fee(&tx, &payment_utxos);
1186 selected_collateral_inputs_equal_total_collateral_and_collateral_return(&tx, payment_utxos);
1187 fee_is_less_than_one_and_half_ada(&tx);
1188 }
1189
1190 fn ada_only_transaction_balancing_test(payment_utxos: Vec<OgmiosUtxo>) {
1191 let ctx = TransactionContext {
1192 payment_key: payment_key(),
1193 payment_key_utxos: payment_utxos.clone(),
1194 network: NetworkIdKind::Testnet,
1195 protocol_parameters: protocol_parameters(),
1196 change_address: payment_addr(),
1197 };
1198 let mut tx_builder = TransactionBuilder::new(&get_builder_config(&ctx).unwrap());
1199 tx_builder
1200 .add_output(&TransactionOutput::new(&payment_addr(), &Value::new(&1500000u64.into())))
1201 .unwrap();
1202
1203 let tx = tx_builder.balance_update_and_build(&ctx).unwrap();
1204
1205 used_inputs_lovelace_equals_outputs_and_fee(&tx, &payment_utxos);
1206 there_is_no_collateral(&tx);
1207 fee_is_less_than_one_and_half_ada(&tx);
1208 }
1209
1210 fn used_inputs_lovelace_equals_outputs_and_fee(tx: &Transaction, payment_utxos: &[OgmiosUtxo]) {
1211 let used_inputs: Vec<OgmiosUtxo> = match_inputs(&tx.body().inputs(), payment_utxos);
1212 let used_inputs_value: u64 = sum_lovelace(&used_inputs);
1213 let outputs_lovelace_sum: u64 = tx
1214 .body()
1215 .outputs()
1216 .into_iter()
1217 .map(|output| {
1218 let value: u64 = output.amount().coin().into();
1219 value
1220 })
1221 .sum();
1222 let fee: u64 = tx.body().fee().into();
1223 assert_eq!(used_inputs_value, outputs_lovelace_sum + fee);
1225 }
1226
1227 fn selected_collateral_inputs_equal_total_collateral_and_collateral_return(
1228 tx: &Transaction,
1229 payment_utxos: Vec<OgmiosUtxo>,
1230 ) {
1231 let collateral_inputs_sum: u64 =
1232 sum_lovelace(&match_inputs(&tx.body().collateral().unwrap(), &payment_utxos));
1233 let collateral_return: u64 = tx.body().collateral_return().unwrap().amount().coin().into();
1234 let total_collateral: u64 = tx.body().total_collateral().unwrap().into();
1235 assert_eq!(collateral_inputs_sum, collateral_return + total_collateral);
1236 }
1237
1238 fn fee_is_less_than_one_and_half_ada(tx: &Transaction) {
1240 assert!(tx.body().fee() <= 1500000u64.into());
1241 }
1242
1243 fn there_is_no_collateral(tx: &Transaction) {
1244 assert!(tx.body().total_collateral().is_none());
1245 assert!(tx.body().collateral_return().is_none());
1246 assert!(tx.body().collateral().is_none())
1247 }
1248
1249 fn match_inputs(inputs: &TransactionInputs, payment_utxos: &[OgmiosUtxo]) -> Vec<OgmiosUtxo> {
1250 inputs
1251 .into_iter()
1252 .map(|input| {
1253 payment_utxos
1254 .iter()
1255 .find(|utxo| utxo.to_csl_tx_input() == *input)
1256 .unwrap()
1257 .clone()
1258 })
1259 .collect()
1260 }
1261
1262 fn sum_lovelace(utxos: &[OgmiosUtxo]) -> u64 {
1263 utxos.iter().map(|utxo| utxo.value.lovelace).sum()
1264 }
1265
1266 proptest! {
1267 #[test]
1268 fn balance_tx_with_minted_token(payment_utxos in arb_payment_utxos(10)
1269 .prop_filter("Inputs total lovelace too low", |utxos| sum_lovelace(utxos) > 4000000)) {
1270 multi_asset_transaction_balancing_test(payment_utxos)
1271 }
1272
1273 #[test]
1274 fn balance_tx_with_ada_only_token(payment_utxos in arb_payment_utxos(10)
1275 .prop_filter("Inputs total lovelace too low", |utxos| sum_lovelace(utxos) > 3000000)) {
1276 ada_only_transaction_balancing_test(payment_utxos)
1277 }
1278 }
1279
1280 prop_compose! {
1281 fn arb_payment_utxos(n: usize)
1283 (utxo_ids in hash_set(arb_utxo_id(), 1..n))
1284 (utxo_ids in Just(utxo_ids.clone()), values in vec(arb_utxo_lovelace(), utxo_ids.len())
1285 ) -> Vec<OgmiosUtxo> {
1286 utxo_ids.into_iter().zip(values.into_iter()).map(|(utxo_id, value)| OgmiosUtxo {
1287 transaction: OgmiosTx { id: utxo_id.tx_hash.0 },
1288 index: utxo_id.index.0,
1289 value,
1290 address: PAYMENT_ADDR.into(),
1291 ..Default::default()
1292 }).collect()
1293 }
1294 }
1295
1296 prop_compose! {
1297 fn arb_utxo_lovelace()(value in MIN_UTXO_LOVELACE..FIVE_ADA) -> OgmiosValue {
1298 OgmiosValue::new_lovelace(value)
1299 }
1300 }
1301
1302 prop_compose! {
1303 fn arb_utxo_id()(tx_hash in uniform32(0u8..255u8), index in any::<u16>()) -> UtxoId {
1304 UtxoId {
1305 tx_hash: McTxHash(tx_hash),
1306 index: UtxoIndex(index),
1307 }
1308 }
1309 }
1310}