partner_chains_cardano_offchain/
csl.rs

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
17/// Constructs [Transaction] from CBOR bytes
18pub fn transaction_from_bytes(cbor: Vec<u8>) -> anyhow::Result<Transaction> {
19	Transaction::from_bytes(cbor).map_err(|e| anyhow::anyhow!(e))
20}
21
22/// Constructs [Vkeywitness] from CBOR bytes
23pub 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	// Before hashing the script, we need to prepend with byte denoting the language.
29	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
34/// Builds a CSL [Address] for plutus script from the data obtained from smart contracts.
35pub 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
44/// Builds a CSL [Address] for the specified network from Cardano verification key bytes.
45pub 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
54/// Builds a CSL [Address] for the specified network from a Cardano verification key hash.
55pub 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
60/// Extension trait for [NetworkType].
61pub trait NetworkTypeExt {
62	/// Converts [NetworkType] to CSL [cardano_serialization_lib::Value].
63	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
89/// Creates a CSL [`TransactionBuilderConfig`] for given [`ProtocolParametersResponse`].
90/// This function is not unit-testable because [`TransactionBuilderConfig`] has no public getters.
91pub(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
122/// Extension trait for [OgmiosValue].
123pub trait OgmiosValueExt {
124	/// Converts [OgmiosValue] to CSL [cardano_serialization_lib::Value].
125	/// It can fail if the input contains negative values, for example Ogmios values representing burn.
126	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
153/// Conversion of ogmios-client cost models to CSL
154pub(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
177/// Type representing transaction execution costs.
178pub(crate) enum Costs {
179	/// Zero costs. Used as a dummy value when submitting a transaction for cost calculation.
180	ZeroCosts,
181	/// Variant containing actual costs.
182	Costs {
183		/// Mapping script hashes to minting policy execution costs.
184		mints: HashMap<cardano_serialization_lib::ScriptHash, ExUnits>,
185		/// Mapping spend indices to validator script execution costs.
186		spends: HashMap<u32, ExUnits>,
187	},
188}
189
190/// Interface for retrieving execution costs.
191pub(crate) trait CostStore {
192	/// Returns [ExUnits] cost of a minting policy for a given [PlutusScript].
193	fn get_mint(&self, script: &PlutusScript) -> ExUnits;
194	/// Returns [ExUnits] cost of a validator script for a given spend index.
195	fn get_spend(&self, spend_ix: u32) -> ExUnits;
196	/// Returns spend cost of the single validator script in a transaction.
197	/// It panics if there is not exactly one validator script execution.
198	fn get_one_spend(&self) -> ExUnits;
199	/// Returns indices of validator scripts as they appear in the CSL transaction.
200	/// These indices can be used in conjunction with [get_spend].
201	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	/// Constructs new [Costs] with given `mints` and `spends`.
245	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	/// Creates a [Transaction] with correctly set script execution costs.
253	///
254	/// Arguments:
255	///  - `make_tx`: A function that takes a [Costs] value, and returns a [anyhow::Result<Transaction>].
256	///               This function is meant to describe which execution cost is used where in the transaction.
257	///  - `client`: Ogmios client
258	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		// This double evaluation is needed to correctly set costs in some cases.
266		let tx = make_tx(Costs::ZeroCosts)?;
267		// stage 1
268		let costs = Self::from_ogmios(&tx, client).await?;
269
270		let tx = make_tx(costs)?;
271		// stage 2
272		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
382/// Extension trait for [UtxoId].
383pub trait UtxoIdExt {
384	/// Converts domain [UtxoId] to CSL [TransactionInput].
385	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	/// This key is added as required signer and used to sign the transaction.
397	pub(crate) payment_key: CardanoPaymentSigningKey,
398	/// Used to pay for the transaction fees and uncovered transaction inputs
399	/// and as source of collateral inputs
400	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	/// Gets `TransactionContext`, having UTXOs for the given payment key and the network configuration,
408	/// required to perform most of the partner-chains smart contract operations.
409	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	/// Creates output on the script address with datum that has 1 token with asset for the script and it has given datum attached.
469	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	/// Adds ogmios inputs as collateral inputs to the tx builder.
478	fn add_collateral_inputs(
479		&mut self,
480		ctx: &TransactionContext,
481		inputs: &[OgmiosUtxo],
482	) -> Result<(), JsError>;
483
484	/// Adds minting of 1 token (with empty asset name) for the given script
485	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	/// Adds minting of tokens (with empty asset name) for the given script using reference input.
503	/// IMPORTANT: Because CSL doesn't properly calculate transaction fee if the script is Native,
504	/// this function adds reference input and regular native script, that is added to witnesses.
505	/// This native script has to be removed from witnesses, otherwise the transaction is rejected!
506	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	/// Adds minting of 1 token (with empty asset name) for the given script using reference input.
515	/// IMPORTANT: Because CSL doesn't properly calculate transaction fee if the script is Native,
516	/// this function adds reference input and regular native script, that is added to witnesses.
517	/// This native script has to be removed from witnesses, otherwise the transaction is rejected!
518	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	/// Sets fields required by the most of partner-chains smart contract transactions.
533	/// Uses input from `ctx` to cover already present outputs.
534	/// Adds collateral inputs using quite a simple algorithm.
535	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				// new_ref_input causes invalid fee
645				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		// Tries to balance tx with given collateral inputs
669		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				// Fake script script data hash is required for proper fee computation
685				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		// Tries if the largest UTXO is enough to cover collateral, if not, adds more UTXOs
698		// starting from the largest remaining.
699		let mut selected = vec![];
700		for input in max_possible_collaterals(ctx) {
701			let mut builder = self.clone();
702			// Check if the used inputs are enough
703			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)]
728/// Type representing a Cardano script.
729pub enum Script {
730	/// Plutus script
731	Plutus(PlutusScript),
732	/// Native script
733	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	/// Adds ogmios inputs to the tx inputs builder.
794	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				// CSL will set redeemer index for the index of script input after sorting transaction inputs
814				&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			// CSL doesn't have a public API to remove asset from MultiAsset, setting it to 0 isn't really helpful.
901			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	/// Removes all native scripts from transaction witness set.
914	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
940/// In Plutus smart-contracts, unit value is represented as `Constr 0 []`.
941/// It is used in many places where there is no particular value needed for redeemer.
942pub(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		// Used inputs are qual to the sum of the outputs plus the fee
1228		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	// Exact fee depends on inputs and outputs, but it definately is less than 1.5 ADA
1243	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		// Set is needed to be used, because we have to avoid UTXOs with the same id.
1286		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}