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