1use crate::error::{Error, Result};
4use crate::metadata::TransactionMetadata;
5use crate::time::time_range_into_slots;
6use crate::wallet::Wallet;
7use anyhow::anyhow;
8use chain_query::{ChainQuery, EraSummary, Network, ProtocolParameters};
9use chrono::{DateTime, Utc};
10use itertools::Itertools;
11use num_bigint::BigInt;
12use plutus_ledger_api::csl::{lib as csl, pla_to_csl::TryToCSL};
13use plutus_ledger_api::plutus_data::IsPlutusData;
14use plutus_ledger_api::v3::{
15 address::{Address, AddressWithExtraInfo, Credential},
16 crypto::PaymentPubKeyHash,
17 datum::{Datum, DatumHash, OutputDatum},
18 redeemer::{Redeemer, RedeemerWithExtraInfo},
19 script::{MintingPolicyHash, ScriptHash, ValidatorHash},
20 transaction::{
21 ScriptPurpose, TransactionHash, TransactionInfo, TransactionInput, TransactionOutput,
22 TransactionOutputWithExtraInfo, TxInInfo,
23 },
24 value::{CurrencySymbol, Value},
25};
26use std::collections::BTreeMap;
27use submitter::Submitter;
28use tracing::{debug, info};
29use utils::script::ScriptOrRef;
30
31pub mod chain_query;
32#[cfg(feature = "clap")]
33pub mod clap;
34pub mod error;
35pub mod metadata;
36pub mod submitter;
37pub mod time;
38pub mod tx_info_builder;
39pub mod utils;
40pub mod wallet;
41
42pub struct TxBakery {
48 config: csl::TransactionBuilderConfig,
49 data_cost: csl::DataCost,
50 cost_models: csl::Costmdls,
51 system_start: DateTime<Utc>,
52 era_summaries: Vec<EraSummary>,
53 network_id: u8,
54}
55
56#[derive(Clone, Debug)]
58pub struct TxWithCtx<'a> {
59 pub tx_info: &'a TransactionInfo,
60 pub scripts: &'a BTreeMap<ScriptHash, ScriptOrRef>,
61 pub collateral_strategy: &'a CollateralStrategy,
62 pub change_strategy: &'a ChangeStrategy,
63 pub metadata: Option<&'a TransactionMetadata>,
64 pub ex_units_map: Option<&'a BTreeMap<(csl::RedeemerTag, csl::BigNum), csl::ExUnits>>,
65}
66
67#[derive(Clone, Debug)]
69pub enum ChangeStrategy {
70 Address(Address),
72 LastOutput,
75}
76
77impl<'a> TxWithCtx<'a> {
78 pub fn new(
79 tx_info: &'a TransactionInfo,
80 scripts: &'a BTreeMap<ScriptHash, ScriptOrRef>,
81 collateral_strategy: &'a CollateralStrategy,
82 change_strategy: &'a ChangeStrategy,
83 ) -> Self {
84 TxWithCtx {
85 tx_info,
86 scripts,
87 collateral_strategy,
88 change_strategy,
89 metadata: None,
90 ex_units_map: None,
91 }
92 }
93
94 pub fn with_metadata(mut self, metadata: &'a TransactionMetadata) -> Self {
96 self.metadata = Some(metadata);
97 self
98 }
99
100 pub fn with_ex_units(
102 mut self,
103 ex_units_map: &'a BTreeMap<(csl::RedeemerTag, csl::BigNum), csl::ExUnits>,
104 ) -> Self {
105 self.ex_units_map = Some(ex_units_map);
106 self
107 }
108}
109
110#[derive(Clone, Debug)]
112pub enum CollateralStrategy {
113 Automatic {
115 min_amount: u64,
116 max_utxo_count: usize,
117 },
118 Explicit {
120 utxos: Vec<TxInInfo>,
121 min_amount: u64,
122 },
123 None,
125}
126
127impl TxBakery {
128 pub async fn init(chain_query: &impl ChainQuery) -> Result<Self> {
131 debug!("Initialising Transaction Bakery");
132 let protocol_params = chain_query.query_protocol_params().await?;
133 let system_start = chain_query.query_system_start().await?;
134 let era_summaries = chain_query.query_era_summaries().await?;
135 let network = chain_query.get_network();
136
137 Self::init_with_config(&network, &protocol_params, system_start, era_summaries).await
138 }
139
140 pub async fn init_with_config(
144 network: &Network,
145 protocol_params: &ProtocolParameters,
146 system_start: DateTime<Utc>,
147 era_summaries: Vec<EraSummary>,
148 ) -> Result<Self> {
149 let data_cost =
150 csl::DataCost::new_coins_per_byte(&protocol_params.min_utxo_deposit_coefficient);
151
152 let linear_fee = &csl::LinearFee::new(
153 &protocol_params.min_fee_coefficient,
154 &protocol_params.min_fee_constant,
155 );
156 let config = csl::TransactionBuilderConfigBuilder::new()
157 .fee_algo(linear_fee)
158 .pool_deposit(&protocol_params.stake_pool_deposit)
159 .key_deposit(&protocol_params.stake_credential_deposit)
160 .max_value_size(protocol_params.max_value_size.ok_or(
161 Error::MissingProtocolParameter("max_value_size".to_string()),
162 )?)
163 .max_tx_size(protocol_params.max_transaction_size.ok_or(
164 Error::MissingProtocolParameter("max_transaction_size".to_string()),
165 )?)
166 .coins_per_utxo_byte(&protocol_params.min_utxo_deposit_coefficient)
167 .ex_unit_prices(&protocol_params.script_execution_prices.clone().ok_or(
168 Error::MissingProtocolParameter("script_execution_prices".to_string()),
169 )?)
170 .prefer_pure_change(true);
171
172 let config = match protocol_params.min_fee_reference_scripts {
173 None => config,
174 Some(ref unit_interval) => config.ref_script_coins_per_byte(unit_interval),
175 }
176 .build()
177 .unwrap();
178
179 Ok(TxBakery {
180 config,
181 data_cost,
182 cost_models: protocol_params
183 .plutus_cost_models
184 .clone()
185 .ok_or(Error::MissingProtocolParameter("cost_models".to_string()))?,
186 system_start,
187 era_summaries,
188 network_id: network.to_network_id(),
189 })
190 }
191
192 fn create_tx_builder(&self) -> csl::TransactionBuilder {
194 csl::TransactionBuilder::new(&self.config)
195 }
196
197 fn mk_inputs(
199 &self,
200 inputs: &[TxInInfo],
201 ref_inputs: &[TxInInfo],
202 input_redeemers: &BTreeMap<TransactionInput, Redeemer>,
203 input_datums: &BTreeMap<DatumHash, Datum>,
204 scripts: &BTreeMap<ScriptHash, ScriptOrRef>,
205 ex_units_map: Option<&BTreeMap<(csl::RedeemerTag, csl::BigNum), csl::ExUnits>>,
206 ) -> Result<csl::TxInputsBuilder> {
207 let mut tx_inputs_builder = csl::TxInputsBuilder::new();
208
209 inputs
210 .iter()
211 .enumerate()
212 .try_for_each(|(idx, TxInInfo { reference, output })| {
213 let redeemer = input_redeemers.get(reference);
214
215 match redeemer {
216 None => {
217 tx_inputs_builder
218 .add_regular_input(
219 &AddressWithExtraInfo {
220 address: &output.address,
221 network_tag: self.network_id,
222 }
223 .try_to_csl()?,
224 &reference.try_to_csl()?,
225 &output.value.try_to_csl()?,
226 )
227 .map_err(|err| {
228 Error::TransactionBuildError(anyhow!(
229 "Failed to add regular input {:?}: {}",
230 reference,
231 err
232 ))
233 })?;
234 Ok::<(), Error>(())
235 }
236 Some(redeemer) => {
237 let script_hash = match &output.address.credential {
238 Credential::Script(ValidatorHash(script_hash)) => Ok(script_hash),
239 _ => Err(Error::Internal(anyhow!(
240 "A public key transaction input should not have a redeemer."
241 ))),
242 }?;
243 let script_or_ref = scripts
244 .get(script_hash)
245 .ok_or_else(|| Error::MissingScript(script_hash.clone()))?;
246
247 let csl_redeemer = RedeemerWithExtraInfo {
248 redeemer,
249 tag: &csl::RedeemerTag::new_spend(),
250 index: idx as u64,
251 }
252 .try_to_csl()?;
253
254 let csl_redeemer = match ex_units_map {
255 Some(ex_units_map) => {
256 Self::apply_ex_units(&csl_redeemer, ex_units_map)?
257 }
258 None => csl_redeemer,
259 };
260
261 let script_source = match &script_or_ref {
262 ScriptOrRef::PlutusScript(script) => {
263 csl::PlutusScriptSource::new(script)
264 }
265 ScriptOrRef::RefScript(ref_tx_in, script) => {
266 ref_inputs
267 .iter()
268 .any(|TxInInfo { reference, .. }| reference == ref_tx_in)
269 .then_some(())
270 .ok_or_else(|| {
271 Error::MissingReferenceScript(
272 ref_tx_in.clone(),
273 script_or_ref.get_script_hash(),
274 )
275 })?;
276 csl::PlutusScriptSource::new_ref_input(
277 &script_hash.try_to_csl()?,
278 &ref_tx_in.try_to_csl()?,
279 &script.language_version(),
280 script_or_ref.get_script_size(),
281 )
282 }
283 };
284
285 let datum_source = match &output.datum {
286 OutputDatum::DatumHash(dh) => {
287 let Datum(input_datum) = input_datums
288 .get(dh)
289 .ok_or(Error::MissingDatum(dh.clone()))?;
290
291 Some(csl::DatumSource::new(&input_datum.try_to_csl()?))
292 }
293
294 OutputDatum::InlineDatum(Datum(input_datum)) => {
295 Some(match &script_or_ref {
296 ScriptOrRef::PlutusScript(_) => {
297 csl::DatumSource::new(&input_datum.try_to_csl()?)
298 }
299
300 ScriptOrRef::RefScript(tx_in, _) => {
301 csl::DatumSource::new_ref_input(&tx_in.try_to_csl()?)
302 }
303 })
304 }
305
306 OutputDatum::None => None,
307 };
308
309 let tx_input_witness = match datum_source {
310 Some(datum_source) => csl::PlutusWitness::new_with_ref(
311 &script_source,
312 &datum_source,
313 &csl_redeemer,
314 ),
315 None => csl::PlutusWitness::new_with_ref_without_datum(
316 &script_source,
317 &csl_redeemer,
318 ),
319 };
320
321 tx_inputs_builder.add_plutus_script_input(
322 &tx_input_witness,
323 &reference.try_to_csl()?,
324 &output.value.try_to_csl()?,
325 );
326
327 Ok(())
328 }
329 }
330 })?;
331
332 Ok(tx_inputs_builder)
333 }
334
335 fn mk_outputs(
339 &self,
340 outputs: &Vec<TransactionOutput>,
341 change_strategy: &ChangeStrategy,
342 scripts: &BTreeMap<ScriptHash, ScriptOrRef>,
343 ) -> Result<Vec<csl::TransactionOutput>> {
344 let normal_outputs = match change_strategy {
345 ChangeStrategy::Address(_) => outputs.as_slice(),
346 ChangeStrategy::LastOutput => &outputs[..(outputs.len() - 1)],
347 };
348 let scripts = &scripts
349 .iter()
350 .map(|(hash, script)| (hash.clone(), script.clone().get_script()))
351 .collect();
352 Ok(normal_outputs
353 .iter()
354 .map(|transaction_output| {
355 TransactionOutputWithExtraInfo {
356 scripts,
357 network_id: self.network_id,
358 data_cost: &self.data_cost,
359 transaction_output,
360 }
361 .try_to_csl()
362 })
363 .collect::<std::result::Result<_, _>>()?)
364 }
365
366 fn mk_mints(
368 tx_mint: &Value,
369 ref_inputs: &[TxInInfo],
370 mint_redeemers: &BTreeMap<ScriptHash, Redeemer>,
371 scripts: &BTreeMap<ScriptHash, ScriptOrRef>,
372 ex_units_map: Option<&BTreeMap<(csl::RedeemerTag, csl::BigNum), csl::ExUnits>>,
373 ) -> Result<csl::MintBuilder> {
374 let mut mint_builder = csl::MintBuilder::new();
375 tx_mint
376 .0
377 .iter()
378 .filter_map(|(cur_sym, assets)| match cur_sym {
379 CurrencySymbol::NativeToken(MintingPolicyHash(script_hash)) => {
380 Some((script_hash, assets))
381 }
382 CurrencySymbol::Ada => None,
383 })
384 .enumerate()
385 .try_for_each(|(idx, (script_hash, assets))| {
386 assets.iter().try_for_each(|(token_name, amount)| {
387 let script_or_ref = scripts
388 .get(script_hash)
389 .ok_or(Error::MissingScript(script_hash.clone()))?;
390 let redeemer = mint_redeemers
391 .get(script_hash)
392 .ok_or(Error::MissingMintRedeemer(script_hash.clone()))?;
393
394 let csl_redeemer = RedeemerWithExtraInfo {
395 redeemer,
396 tag: &csl::RedeemerTag::new_mint(),
397 index: idx as u64,
398 }
399 .try_to_csl()?;
400
401 let csl_redeemer = match ex_units_map {
402 Some(ex_units_map) => Self::apply_ex_units(&csl_redeemer, ex_units_map)?,
403 None => csl_redeemer,
404 };
405
406 let script_source = match &script_or_ref {
407 ScriptOrRef::PlutusScript(script) => csl::PlutusScriptSource::new(script),
408 ScriptOrRef::RefScript(ref_tx_in, script) => {
409 ref_inputs
410 .iter()
411 .find(|TxInInfo { reference, .. }| reference == ref_tx_in)
412 .map(|_| ())
413 .ok_or_else(|| {
414 Error::MissingReferenceScript(
415 ref_tx_in.clone(),
416 script_or_ref.get_script_hash(),
417 )
418 })?;
419 csl::PlutusScriptSource::new_ref_input(
420 &script_hash.try_to_csl()?,
421 &ref_tx_in.try_to_csl()?,
422 &script.language_version(),
423 script_or_ref.get_script_size(),
424 )
425 }
426 };
427
428 let mint_witness =
429 csl::MintWitness::new_plutus_script(&script_source, &csl_redeemer);
430 mint_builder
431 .add_asset(
432 &mint_witness,
433 &token_name.try_to_csl()?,
434 &amount.try_to_csl()?,
435 )
436 .map_err(|err| {
437 Error::TransactionBuildError(anyhow!(
438 "Failed to add mint {:?}: {}",
439 MintingPolicyHash(script_hash.clone()),
440 err
441 ))
442 })?;
443 Ok::<(), Error>(())
444 })
445 })?;
446
447 Ok(mint_builder)
448 }
449
450 fn find_collaterals(
454 &self,
455 min_collateral_amount: u64,
456 max_utxo_count: usize,
457
458 tx_inputs: &[TxInInfo],
459 ) -> Result<Vec<TxInInfo>> {
460 let min_collateral_amount = BigInt::from(min_collateral_amount);
461 let (amount, collaterals) = tx_inputs
462 .iter()
463 .sorted_by_key(|input| -input.output.value.get_ada_amount())
464 .take(max_utxo_count)
465 .fold(
466 (BigInt::ZERO, Vec::new()),
467 |(mut acc_amount, mut collaterals), tx_in_info| {
468 if acc_amount < min_collateral_amount {
469 if let Credential::PubKey(_) = tx_in_info.output.address.credential {
470 let ada_amount = tx_in_info.output.value.get_ada_amount();
471 acc_amount += ada_amount;
472 collaterals.push(tx_in_info.clone());
473 }
474 };
475
476 (acc_amount, collaterals)
477 },
478 );
479
480 if amount >= min_collateral_amount {
481 Ok(collaterals)
482 } else {
483 Err(Error::NotEnoughCollaterals {
484 amount,
485 required: min_collateral_amount,
486 utxos: collaterals,
487 })
488 }
489 }
490
491 fn mk_collaterals(&self, tx_inputs: &[TxInInfo]) -> Result<csl::TxInputsBuilder> {
492 let mut tx_inputs_builder = csl::TxInputsBuilder::new();
493 for tx_input in tx_inputs {
494 let TxInInfo { reference, output } = tx_input;
495
496 tx_inputs_builder
497 .add_regular_input(
498 &output
499 .address
500 .with_extra_info(self.network_id)
501 .try_to_csl()?,
502 &reference.try_to_csl()?,
503 &output.value.try_to_csl()?,
504 )
505 .map_err(|err| {
506 Error::TransactionBuildError(anyhow!(
507 "Failed to add regular input {:?}: {}",
508 reference,
509 err
510 ))
511 })?;
512 }
513
514 Ok(tx_inputs_builder)
515 }
516
517 pub fn mk_tx_builder(&self, tx: &TxWithCtx<'_>) -> Result<csl::TransactionBuilder> {
521 let mut tx_builder = self.create_tx_builder();
522
523 let (input_redeemers, mint_redeemers) = tx.tx_info.redeemers.0.iter().fold(
524 (BTreeMap::new(), BTreeMap::new()),
525 |(mut input_reds, mut mint_reds), (purpose, red)| {
526 match purpose {
527 ScriptPurpose::Spending(tx_in) => {
528 input_reds.insert(tx_in.clone(), red.clone());
529 }
530 ScriptPurpose::Minting(CurrencySymbol::NativeToken(MintingPolicyHash(
531 script_hash,
532 ))) => {
533 mint_reds.insert(script_hash.clone(), red.clone());
534 }
535
536 _ => {}
537 };
538 (input_reds, mint_reds)
539 },
540 );
541
542 let input_datums = tx
543 .tx_info
544 .datums
545 .0
546 .iter()
547 .cloned()
548 .collect::<BTreeMap<_, _>>();
549
550 tx_builder.set_inputs(&self.mk_inputs(
551 &tx.tx_info.inputs,
552 &tx.tx_info.reference_inputs,
553 &input_redeemers,
554 &input_datums,
555 tx.scripts,
556 tx.ex_units_map,
557 )?);
558
559 tx.tx_info
560 .reference_inputs
561 .iter()
562 .try_for_each(|TxInInfo { reference, output }| {
563 match output.reference_script {
564 None => tx_builder.add_reference_input(&reference.try_to_csl()?),
565 Some(ref script_hash) => {
566 let script_or_ref = tx
567 .scripts
568 .get(script_hash)
569 .ok_or(Error::MissingScript(script_hash.clone()))?;
570 tx_builder.add_script_reference_input(
571 &reference.try_to_csl()?,
572 script_or_ref.get_script_size(),
573 )
574 }
575 }
576 Ok::<(), Error>(())
577 })?;
578
579 tx_builder.set_mint_builder(&TxBakery::mk_mints(
580 &tx.tx_info.mint,
581 &tx.tx_info.reference_inputs,
582 &mint_redeemers,
583 tx.scripts,
584 tx.ex_units_map,
585 )?);
586
587 let collateral_return_address = match tx.change_strategy {
588 ChangeStrategy::Address(addr) => addr,
589 ChangeStrategy::LastOutput => {
590 &tx.tx_info
591 .outputs
592 .last()
593 .ok_or(Error::MissingChangeOutput)?
594 .address
595 }
596 };
597
598 match &tx.collateral_strategy {
599 CollateralStrategy::Automatic {
600 min_amount,
601 max_utxo_count,
602 } => {
603 let tx_input =
604 self.find_collaterals(*min_amount, *max_utxo_count, &tx.tx_info.inputs)?;
605 let collateral = self.mk_collaterals(&tx_input)?;
606 tx_builder.set_collateral(&collateral);
607 tx_builder
608 .set_total_collateral_and_return(
609 &csl::BigNum::from(*min_amount),
610 &collateral_return_address
611 .with_extra_info(self.network_id)
612 .try_to_csl()?,
613 )
614 .map_err(|source| Error::TransactionBuildError(anyhow!(source)))?;
615 }
616 CollateralStrategy::Explicit { utxos, min_amount } => {
617 let collateral = self.mk_collaterals(utxos)?;
618 tx_builder.set_collateral(&collateral);
619 tx_builder
620 .set_total_collateral_and_return(
621 &csl::BigNum::from(*min_amount),
622 &collateral_return_address
623 .with_extra_info(self.network_id)
624 .try_to_csl()?,
625 )
626 .map_err(|source| Error::TransactionBuildError(anyhow!(source)))?;
627 }
628 CollateralStrategy::None => {}
629 };
630
631 self.mk_outputs(&tx.tx_info.outputs, tx.change_strategy, tx.scripts)?
632 .iter()
633 .try_for_each(|tx_out| {
634 tx_builder
635 .add_output(tx_out)
636 .map_err(|source| Error::TransactionBuildError(anyhow!(source)))
637 })?;
638
639 let (validity_start, ttl) = time_range_into_slots(
640 &self.era_summaries,
641 &self.system_start,
642 tx.tx_info.valid_range.clone(),
643 )?;
644
645 if let Some(validity_start) = validity_start {
646 tx_builder.set_validity_start_interval_bignum(validity_start);
647 }
648 if let Some(ttl) = ttl {
649 tx_builder.set_ttl_bignum(&ttl);
650 }
651
652 tx.tx_info
653 .signatories
654 .iter()
655 .try_for_each(|PaymentPubKeyHash(pkh)| {
656 tx_builder.add_required_signer(&pkh.try_to_csl()?);
657 Ok::<(), Error>(())
658 })?;
659
660 if let Some(metadata) = tx.metadata {
661 tx_builder.set_metadata(&metadata.try_into()?);
662 }
663
664 Ok(tx_builder)
665 }
666
667 pub fn mk_tx_body(&self, tx: &TxWithCtx<'_>) -> Result<csl::TransactionBody> {
668 let tx_builder = self.mk_tx_builder(tx)?;
669
670 let (datums, redeemers) = TxBakery::extract_witnesses(tx.tx_info)?;
671 let (wit_scripts, ref_scripts) = TxBakery::collect_scripts(tx.tx_info, tx.scripts)?;
672
673 self.balance_transaction(
674 tx,
675 tx_builder,
676 &datums,
677 &redeemers,
678 &wit_scripts,
679 &ref_scripts,
680 )
681 }
682
683 fn extract_witnesses(
687 tx_info: &TransactionInfo,
688 ) -> Result<(Vec<csl::PlutusData>, Vec<csl::Redeemer>)> {
689 let datums = tx_info
690 .datums
691 .0
692 .iter()
693 .map(|(_dh, Datum(d))| Ok(d.to_plutus_data().try_to_csl()?))
694 .collect::<Result<_>>()?;
695
696 let redeemers = tx_info
697 .redeemers
698 .0
699 .iter()
700 .filter_map(|(script_purpose, redeemer)| match script_purpose {
701 ScriptPurpose::Spending(reference) => tx_info
702 .inputs
703 .iter()
704 .enumerate()
705 .find(|(_idx, tx_input)| &tx_input.reference == reference)
706 .map(|(idx, _)| {
707 Ok(RedeemerWithExtraInfo {
708 redeemer,
709 tag: &csl::RedeemerTag::new_spend(),
710 index: idx as u64,
711 }
712 .try_to_csl()?)
713 }),
714 ScriptPurpose::Minting(reference) => tx_info
715 .mint
716 .0
717 .iter()
718 .filter(|(cur_sym, _)| match cur_sym {
719 CurrencySymbol::NativeToken(_) => true,
720 CurrencySymbol::Ada => false,
721 })
722 .enumerate()
723 .find(|(_idx, (currency_symbol, _assets))| currency_symbol == &reference)
724 .map(|(idx, _)| {
725 Ok(RedeemerWithExtraInfo {
726 redeemer,
727 tag: &csl::RedeemerTag::new_mint(),
728 index: idx as u64,
729 }
730 .try_to_csl()?)
731 }),
732 _ => Some(Err(Error::Unsupported(
733 "Only spending and minting redeemers are supported".to_string(),
734 ))),
735 })
736 .collect::<Result<_>>()?;
737
738 Ok((datums, redeemers))
739 }
740
741 fn collect_scripts(
743 tx_info: &TransactionInfo,
744 scripts: &BTreeMap<ScriptHash, ScriptOrRef>,
745 ) -> Result<(Vec<csl::PlutusScript>, Vec<csl::PlutusScript>)> {
746 let res = tx_info
747 .inputs
748 .iter()
749 .filter_map(
750 |TxInInfo {
751 reference: _,
752 output,
753 }| {
754 match &output.address {
755 Address {
756 credential: Credential::Script(ValidatorHash(script_hash)),
757 staking_credential: _,
758 } => Some(script_hash),
759 _ => None,
760 }
761 },
762 )
763 .chain(tx_info.mint.0.iter().filter_map(|(cs, _)| match cs {
764 CurrencySymbol::NativeToken(MintingPolicyHash(script_hash)) => Some(script_hash),
765 _ => None,
766 }))
767 .map(|script_hash| {
768 scripts
769 .get(script_hash)
770 .cloned()
771 .ok_or(Error::MissingScript((*script_hash).clone()))
772 })
773 .collect::<Result<Vec<_>>>()?
774 .into_iter()
775 .partition_map(|script_or_ref| match script_or_ref {
776 ScriptOrRef::PlutusScript(script) => itertools::Either::Left(script),
777 ScriptOrRef::RefScript(_, script) => itertools::Either::Right(script),
778 });
779
780 Ok(res)
781 }
782
783 fn calc_script_data_hash(
785 &self,
786 mut tx_builder: csl::TransactionBuilder,
787 wit_datums: &[csl::PlutusData],
788 wit_redeemers: &[csl::Redeemer],
789 wit_scripts: &[csl::PlutusScript],
790 ref_scripts: &[csl::PlutusScript],
791 ) -> csl::TransactionBuilder {
792 debug!("Calculating script integrity hash");
793 let mut redeemers = csl::Redeemers::new();
794 wit_redeemers.iter().for_each(|red| redeemers.add(red));
795
796 if !wit_datums.is_empty() || !wit_redeemers.is_empty() {
797 let datums = if wit_datums.is_empty() {
798 None
799 } else {
800 let mut ds = csl::PlutusList::new();
801 wit_datums.iter().for_each(|dat| ds.add(dat));
802 Some(ds)
803 };
804
805 let mut used_langs = csl::Languages::new();
806 wit_scripts.iter().chain(ref_scripts).for_each(|script| {
807 match script.language_version().kind() {
808 csl::LanguageKind::PlutusV1 => used_langs.add(csl::Language::new_plutus_v1()),
809 csl::LanguageKind::PlutusV2 => used_langs.add(csl::Language::new_plutus_v2()),
810 csl::LanguageKind::PlutusV3 => used_langs.add(csl::Language::new_plutus_v3()),
811 }
812 });
813
814 let script_data_hash = csl::hash_script_data(
815 &redeemers,
816 &self.cost_models.retain_language_versions(&used_langs),
817 datums,
818 );
819
820 tx_builder.set_script_data_hash(&script_data_hash);
821 }
822 tx_builder
823 }
824
825 fn apply_ex_units(
827 redeemer: &csl::Redeemer,
828 ex_units_map: &BTreeMap<(csl::RedeemerTag, csl::BigNum), csl::ExUnits>,
829 ) -> Result<csl::Redeemer> {
830 debug!("Apply execution units.");
831 let key = (redeemer.tag(), redeemer.index());
832 let ex_units = ex_units_map.get(&key).ok_or(Error::MissingExUnits(key))?;
833 Ok(csl::Redeemer::new(
834 &redeemer.tag(),
835 &redeemer.index(),
836 &redeemer.data(),
837 ex_units,
838 ))
839 }
840
841 fn balance_transaction(
843 &self,
844 tx: &TxWithCtx<'_>,
845 mut tx_builder: csl::TransactionBuilder,
846 wit_datums: &[csl::PlutusData],
847 wit_redeemers: &[csl::Redeemer],
848 wit_scripts: &[csl::PlutusScript],
849 ref_scripts: &[csl::PlutusScript],
850 ) -> Result<csl::TransactionBody> {
851 debug!("Balance transaction");
852 let mut redeemers = csl::Redeemers::new();
853 wit_redeemers.iter().for_each(|red| redeemers.add(red));
854
855 tx_builder = self.calc_script_data_hash(
856 tx_builder,
857 wit_datums,
858 wit_redeemers,
859 wit_scripts,
860 ref_scripts,
861 );
862
863 let (change_addr, change_datum) = match tx.change_strategy {
864 ChangeStrategy::Address(address) => (
865 AddressWithExtraInfo {
866 address,
867 network_tag: self.network_id,
868 }
869 .try_to_csl()?,
870 None,
871 ),
872 ChangeStrategy::LastOutput => {
873 let last_output = tx
874 .tx_info
875 .outputs
876 .last()
877 .ok_or(Error::MissingChangeOutput)?;
878
879 (
880 AddressWithExtraInfo {
881 address: &last_output.address,
882 network_tag: self.network_id,
883 }
884 .try_to_csl()?,
885 last_output.datum.try_to_csl()?,
886 )
887 }
888 };
889
890 match change_datum {
891 None => tx_builder
892 .add_change_if_needed(&change_addr)
893 .map_err(|source| Error::TransactionBuildError(anyhow!(source)))?,
894 Some(output_datum) => tx_builder
895 .add_change_if_needed_with_datum(&change_addr, &output_datum)
896 .map_err(|source| Error::TransactionBuildError(anyhow!(source)))?,
897 };
898
899 tx_builder
900 .build()
901 .map_err(|source| Error::TransactionBuildError(anyhow!(source)))
902 }
903
904 pub async fn bake_balanced_tx(
909 &self,
910 submitter: &impl Submitter,
911 tx: TxWithCtx<'_>,
912 ) -> Result<csl::FixedTransaction> {
913 info!("Bake balanced transaction.");
914 let (datums, redeemers) = TxBakery::extract_witnesses(tx.tx_info)?;
915 let (wit_scripts, ref_scripts) = TxBakery::collect_scripts(tx.tx_info, tx.scripts)?;
916
917 let aux_data: Result<Option<csl::AuxiliaryData>> = tx
918 .metadata
919 .map(|metadata| {
920 let mut aux_data = csl::AuxiliaryData::new();
921 aux_data.set_metadata(&metadata.try_into()?);
922 Ok(aux_data)
923 })
924 .transpose();
925 let aux_data = aux_data?;
926
927 let (tx_builder, ex_units) = match &tx.ex_units_map {
928 Some(ex_units_map) => {
929 debug!("Using supplied execution units.");
930 (self.mk_tx_builder(&tx)?, (*ex_units_map).clone())
931 }
932 None => {
933 debug!("Using Ogmios to calculate execution units.");
934 let tx_builder = self.mk_tx_builder(&tx)?;
935
936 let ex_units = submitter
937 .evaluate_transaction(&tx_builder, &wit_scripts, &redeemers)
938 .await?;
939
940 debug!("Applying execution units to transaction.");
941
942 (
943 self.mk_tx_builder(&tx.clone().with_ex_units(&ex_units))?,
944 ex_units,
945 )
946 }
947 };
948
949 let redeemers_w_ex_u = redeemers
950 .iter()
951 .map(|r| TxBakery::apply_ex_units(r, &ex_units))
952 .collect::<Result<Vec<_>>>()?;
953
954 let tx_body = self.balance_transaction(
955 &tx,
956 tx_builder,
957 &datums,
958 &redeemers_w_ex_u,
959 &wit_scripts,
960 &ref_scripts,
961 )?;
962
963 let mut witness_set = csl::TransactionWitnessSet::new();
964 let mut script_witnesses = csl::PlutusScripts::new();
965
966 wit_scripts
967 .iter()
968 .for_each(|script| script_witnesses.add(script));
969
970 let mut redeemer_witnesses = csl::Redeemers::new();
971
972 redeemers_w_ex_u
973 .iter()
974 .for_each(|redeemer| redeemer_witnesses.add(redeemer));
975
976 witness_set.set_plutus_scripts(&script_witnesses);
977 witness_set.set_redeemers(&redeemer_witnesses);
978
979 let tx = csl::Transaction::new(&tx_body, &witness_set, aux_data);
980
981 let fixed_tx = csl::FixedTransaction::from_bytes(tx.to_bytes())
982 .map_err(|source| Error::TransactionBuildError(anyhow!(source)))?;
983
984 Ok(fixed_tx)
985 }
986
987 pub async fn bake_signed_tx(
989 &self,
990 submitter: &impl Submitter,
991 wallet: &impl Wallet,
992 tx: TxWithCtx<'_>,
993 ) -> Result<csl::FixedTransaction> {
994 let tx = self.bake_balanced_tx(submitter, tx).await?;
995 debug!("Signing transaction.");
996 Ok(wallet.sign_transaction(&tx))
997 }
998
999 pub async fn bake_and_deliver(
1002 &self,
1003 submitter: &impl Submitter,
1004 wallet: &impl Wallet,
1005 tx: TxWithCtx<'_>,
1006 ) -> Result<TransactionHash> {
1007 let tx = self.bake_signed_tx(submitter, wallet, tx).await?;
1008
1009 debug!("Submitting transaction.");
1010 Ok(submitter.submit_transaction(&tx).await?)
1011 }
1012}