tx_bakery/
lib.rs

1//! Transaction Bakery
2
3use 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
42/// Transaction builder
43///
44/// The purpose of this component is to convert a raw TransactionInfo (dough)
45/// into a fully baked valid transaction.
46/// TxBakery does not perform IO and won't change it's internal state once initialized.
47pub 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/// TransactionInfo with additional context required to build a valid transaction
57#[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/// Options to deal with change outputs and collateral returns
68#[derive(Clone, Debug)]
69pub enum ChangeStrategy {
70    /// Send all change to an address
71    Address(Address),
72    /// Use the last output of the TransactionInfo as change output (modify it's value)
73    /// Collateral returns are following the address of the last output
74    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    /// Attach transaction metadata to the context
95    pub fn with_metadata(mut self, metadata: &'a TransactionMetadata) -> Self {
96        self.metadata = Some(metadata);
97        self
98    }
99
100    /// Explicitly add execution units instead of running the ChainQuery evaluation
101    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/// Options to deal with collateral selection
111#[derive(Clone, Debug)]
112pub enum CollateralStrategy {
113    /// Automatically pick a suitable UTxO from the transaction inputs
114    Automatic {
115        min_amount: u64,
116        max_utxo_count: usize,
117    },
118    /// Explicitly set a UTxO (doesn't have to be an input UTxO)
119    Explicit {
120        utxos: Vec<TxInInfo>,
121        min_amount: u64,
122    },
123    /// No collateral (for transaction without scripts)
124    None,
125}
126
127impl TxBakery {
128    /// Query all the parameters required to build a transaction and store it for later use.
129    /// This command will call the ChainQuery service to pull certain chain parameters
130    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    /// Init TxBakey with the required configurations
141    /// This allows to directly inject configurations, and handle them separately from the bakery
142    /// (for example prefetch and cache them)
143    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    /// Create a new CSL transaction builder
193    fn create_tx_builder(&self) -> csl::TransactionBuilder {
194        csl::TransactionBuilder::new(&self.config)
195    }
196
197    /// Convert PLA TransactionInfo inputs, redeemers and datums to a CSL transaction input builder
198    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    /// Add transaction outputs to the builder
336    /// If the change strategy if LastOutput, then we are postponing the addition of this output
337    /// to balancing
338    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    /// Convert PLA mints to CSL and pair them with their corresponding redeemers
367    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    /// Find suitable UTxOs to be used as a collateral. Each UTxO has to be at a pub key address,
451    /// and the total Ada amount of must be at least the configured collateral amount
452    // TODO(szg251): we could calculate the exact minimum collateral amount using protocol params
453    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    /// Convert a PLA TransactionInfo into a CSL transaction builder.
518    /// The result is not yet balanced and witnesses are not added. This is useful for
519    /// some further manual processing of the transaction before finalising.
520    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    /// Extract witness datums and redeemers from TransactionInfo
684    /// Inline datums are embedded in the transaction body, so they won't appear here
685    /// Redeemers execution units are set to 0
686    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    /// Collect witness scripts (validators and mintig policies)
742    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    /// Calculate script integrity hash
784    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    /// Apply execution units to redeemers
826    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    /// Calculate script integrity hash, balance the transaction and add change if necessary
842    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    /// Convert a TransactionInfo into a valid TransactionBody and prepare all witnesses (except
905    /// wallet signatures)
906    /// If the transaction context does not include execution units, we use Ogmios to calculate
907    /// those
908    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    /// Convert a TransactionInfo into a valid signed Transaction
988    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    /// Convert a TransactionInfo into a valid signed Transaction and submit it to the chain
1000    /// (Not waiting for confirmation)
1001    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}