cardano_sdk/cardano/
transaction.rs

1//  This Source Code Form is subject to the terms of the Mozilla Public
2//  License, v. 2.0. If a copy of the MPL was not distributed with this
3//  file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use crate::{
6    Address, BoxedIterator, ChangeStrategy, ExecutionUnits, Hash, Input, NetworkId, Output,
7    PlutusData, PlutusScript, PlutusVersion, ProtocolParameters, RedeemerPointer, Signature,
8    SigningKey, SlotBound, Value, VerificationKey, cbor, pallas, pretty,
9};
10use anyhow::anyhow;
11use itertools::Itertools;
12use std::{
13    borrow::Borrow,
14    collections::{BTreeMap, BTreeSet, VecDeque},
15    fmt, iter,
16    marker::PhantomData,
17    mem,
18    ops::Deref,
19};
20
21mod builder;
22pub mod state;
23pub use state::IsTransactionBodyState;
24
25/// A transaction, either under construction or fully signed.
26///
27/// The [`State`](IsTransactionBodyState) captures the current state of the transaction and
28/// restricts the methods available based on the state. In practice, a transaction starts in the
29/// [`state::InConstruction`] using either [`Self::default`] or provided in the callback to
30/// [`Self::build`].
31///
32/// Then it reaches the [`state::ReadyForSigning`] which enables the method [`Self::sign`], and
33/// forbid any method that modifies the transaction body.
34///
35/// Note that [`Self::build`] is currently the only way by which one can get a transaction in the
36/// [`state::ReadyForSigning`].
37pub struct Transaction<State: IsTransactionBodyState> {
38    inner: pallas::Tx,
39    change_strategy: State::ChangeStrategy,
40    state: PhantomData<State>,
41}
42
43// -------------------------------------------------------------------- Building
44
45impl Default for Transaction<state::InConstruction> {
46    fn default() -> Self {
47        Self {
48            change_strategy: ChangeStrategy::default(),
49            state: PhantomData,
50            inner: pallas::Tx {
51                transaction_body: pallas::TransactionBody {
52                    auxiliary_data_hash: None,
53                    certificates: None,
54                    collateral: None,
55                    collateral_return: None,
56                    donation: None,
57                    fee: 0,
58                    inputs: pallas::Set::from(vec![]),
59                    mint: None,
60                    network_id: None,
61                    outputs: vec![],
62                    proposal_procedures: None,
63                    reference_inputs: None,
64                    required_signers: None,
65                    script_data_hash: None,
66                    total_collateral: None,
67                    treasury_value: None,
68                    ttl: None,
69                    validity_interval_start: None,
70                    voting_procedures: None,
71                    withdrawals: None,
72                },
73                transaction_witness_set: pallas::WitnessSet {
74                    bootstrap_witness: None,
75                    native_script: None,
76                    plutus_data: None,
77                    plutus_v1_script: None,
78                    plutus_v2_script: None,
79                    plutus_v3_script: None,
80                    redeemer: None,
81                    vkeywitness: None,
82                },
83                success: true,
84                auxiliary_data: pallas::Nullable::Null,
85            },
86        }
87    }
88}
89
90impl Transaction<state::InConstruction> {
91    pub fn ok(&mut self) -> anyhow::Result<&mut Self> {
92        Ok(self)
93    }
94
95    pub fn with_inputs(
96        &mut self,
97        inputs: impl IntoIterator<Item = (Input, Option<PlutusData<'static>>)>,
98    ) -> &mut Self {
99        let mut redeemers = BTreeMap::new();
100
101        self.inner.transaction_body.inputs = pallas::Set::from(
102            inputs
103                .into_iter()
104                .sorted()
105                .enumerate()
106                .map(|(ix, (input, redeemer))| {
107                    if let Some(data) = redeemer {
108                        redeemers.insert(RedeemerPointer::from_spend(ix as u32), data);
109                    }
110
111                    pallas::TransactionInput::from(input)
112                })
113                .collect::<Vec<_>>(),
114        );
115
116        self.with_redeemers(|tag| matches!(tag, pallas::RedeemerTag::Spend), redeemers);
117
118        self
119    }
120
121    pub fn with_collaterals(&mut self, collaterals: impl IntoIterator<Item = Input>) -> &mut Self {
122        self.inner.transaction_body.collateral = pallas::NonEmptySet::from_vec(
123            collaterals
124                .into_iter()
125                .sorted()
126                .map(pallas::TransactionInput::from)
127                .collect::<Vec<_>>(),
128        );
129        self
130    }
131
132    pub fn with_reference_inputs(
133        &mut self,
134        reference_inputs: impl IntoIterator<Item = Input>,
135    ) -> &mut Self {
136        self.inner.transaction_body.reference_inputs = pallas::NonEmptySet::from_vec(
137            reference_inputs
138                .into_iter()
139                .sorted()
140                .map(pallas::TransactionInput::from)
141                .collect::<Vec<_>>(),
142        );
143        self
144    }
145
146    pub fn with_specified_signatories(
147        &mut self,
148        verification_key_hashes: impl IntoIterator<Item = Hash<28>>,
149    ) -> &mut Self {
150        self.inner.transaction_body.required_signers = pallas::NonEmptySet::from_vec(
151            verification_key_hashes
152                .into_iter()
153                .map(pallas::Hash::from)
154                .collect(),
155        );
156        self
157    }
158
159    pub fn with_outputs(&mut self, outputs: impl IntoIterator<Item = Output>) -> &mut Self {
160        self.inner.transaction_body.outputs = outputs
161            .into_iter()
162            .map(pallas::TransactionOutput::from)
163            .collect::<Vec<_>>();
164        self
165    }
166
167    pub fn with_change_strategy(&mut self, with: ChangeStrategy) -> &mut Self {
168        self.change_strategy = with;
169        self
170    }
171
172    pub fn with_mint(
173        &mut self,
174        mint: BTreeMap<(Hash<28>, PlutusData), BTreeMap<Vec<u8>, i64>>,
175    ) -> &mut Self {
176        let (redeemers, mint) = mint.into_iter().enumerate().fold(
177            (BTreeMap::new(), BTreeMap::new()),
178            |(mut redeemers, mut mint), (index, ((script_hash, data), assets))| {
179                mint.insert(script_hash, assets);
180
181                redeemers.insert(RedeemerPointer::from_mint(index as u32), data);
182
183                (redeemers, mint)
184            },
185        );
186
187        let value = Value::default().with_assets(mint);
188
189        self.inner.transaction_body.mint = <Option<pallas::Multiasset<_>>>::from(&value);
190
191        self.with_redeemers(|tag| matches!(tag, pallas::RedeemerTag::Mint), redeemers);
192
193        self
194    }
195
196    pub fn with_validity_interval(&mut self, from: SlotBound, until: SlotBound) -> &mut Self {
197        // In Conway, the lower-bound is *inclusive* while the upper-bound is *exclusive*; so we
198        // must match what the user set to get the right serialisation.
199
200        let from_inclusive = match from {
201            SlotBound::None => None,
202            SlotBound::Inclusive(bound) => Some(bound),
203            SlotBound::Exclusive(bound) => Some(bound + 1),
204        };
205
206        let until_exclusive = match until {
207            SlotBound::None => None,
208            SlotBound::Inclusive(bound) => Some(bound + 1),
209            SlotBound::Exclusive(bound) => Some(bound),
210        };
211
212        self.inner.transaction_body.validity_interval_start = from_inclusive;
213        self.inner.transaction_body.ttl = until_exclusive;
214
215        self
216    }
217
218    pub fn with_fee(&mut self, fee: u64) -> &mut Self {
219        self.inner.transaction_body.fee = fee;
220        self
221    }
222
223    pub fn with_datums(
224        &mut self,
225        datums: impl IntoIterator<Item = PlutusData<'static>>,
226    ) -> &mut Self {
227        self.inner.transaction_witness_set.plutus_data = pallas::NonEmptySet::from_vec(
228            datums.into_iter().map(pallas::PlutusData::from).collect(),
229        );
230
231        self
232    }
233
234    pub fn with_plutus_scripts(
235        &mut self,
236        scripts: impl IntoIterator<Item = PlutusScript>,
237    ) -> &mut Self {
238        let (v1, v2, v3) = scripts.into_iter().fold(
239            (vec![], vec![], vec![]),
240            |(mut v1, mut v2, mut v3), script| {
241                match script.version() {
242                    PlutusVersion::V1 => {
243                        if let Ok(v1_script) = <pallas::PlutusScript<1>>::try_from(script) {
244                            v1.push(v1_script)
245                        }
246                    }
247                    PlutusVersion::V2 => {
248                        if let Ok(v2_script) = <pallas::PlutusScript<2>>::try_from(script) {
249                            v2.push(v2_script)
250                        }
251                    }
252                    PlutusVersion::V3 => {
253                        if let Ok(v3_script) = <pallas::PlutusScript<3>>::try_from(script) {
254                            v3.push(v3_script)
255                        }
256                    }
257                };
258
259                (v1, v2, v3)
260            },
261        );
262
263        debug_assert!(
264            v1.is_empty(),
265            "trying to set some Plutus V1 scripts; these aren't supported yet and may fail later down the builder.",
266        );
267
268        debug_assert!(
269            v2.is_empty(),
270            "trying to set some Plutus V2 scripts; these aren't supported yet and may fail later down the builder.",
271        );
272
273        self.inner.transaction_witness_set.plutus_v1_script = pallas::NonEmptySet::from_vec(v1);
274        self.inner.transaction_witness_set.plutus_v2_script = pallas::NonEmptySet::from_vec(v2);
275        self.inner.transaction_witness_set.plutus_v3_script = pallas::NonEmptySet::from_vec(v3);
276
277        self
278    }
279}
280
281// -------------------------------------------------------------------- Signing
282
283impl Transaction<state::ReadyForSigning> {
284    pub fn sign(&mut self, signing_key: &SigningKey) -> &mut Self {
285        self.sign_with(|msg| (signing_key.to_verification_key(), signing_key.sign(msg)))
286    }
287
288    /// Like 'sign', but allows signing through a callback to avoid leaking the signing key.
289    pub fn sign_with<
290        VerificationKeyLike: Borrow<VerificationKey>,
291        SignatureLike: Borrow<Signature>,
292    >(
293        &mut self,
294        sign: impl FnOnce(Hash<32>) -> (VerificationKeyLike, SignatureLike),
295    ) -> &mut Self {
296        let (verification_key, signature) = sign(self.id());
297
298        let public_key = pallas::Bytes::from(Vec::from(<[u8; VerificationKey::SIZE]>::from(
299            *(verification_key.borrow()),
300        )));
301
302        let witness = pallas::VKeyWitness {
303            vkey: public_key.clone(),
304            signature: pallas::Bytes::from(Vec::from(signature.borrow().as_ref())),
305        };
306
307        if let Some(signatures) = mem::take(&mut self.inner.transaction_witness_set.vkeywitness) {
308            // Unfortunately, we don't have a proper set at the Pallas level. We also don't want to
309            // use an intermediate BTreeSet here because it would arbitrarily change the order of
310            // witnesses (which do not matter, but may be confusing when indexing / browsing the
311            // witnesses after the fact.
312            //
313            // So, we preserve the set by simply discarding any matching signature, should one
314            // decide to sign again with the same key.
315            self.inner.transaction_witness_set.vkeywitness = pallas::NonEmptySet::from_vec(
316                signatures
317                    .to_vec()
318                    .into_iter()
319                    .filter(|existing_witness| existing_witness.vkey != public_key)
320                    .chain(vec![witness])
321                    .collect(),
322            );
323        } else {
324            self.inner.transaction_witness_set.vkeywitness =
325                pallas::NonEmptySet::from_vec(vec![witness]);
326        }
327
328        self
329    }
330}
331
332// ------------------------------------------------------------------ Inspecting
333
334impl<State: IsTransactionBodyState> fmt::Debug for Transaction<State> {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        self.inner.fmt(f)
337    }
338}
339
340impl<State: IsTransactionBodyState> fmt::Display for Transaction<State> {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        write!(
343            f,
344            "{:#?}",
345            pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
346                let mut debug_struct = f.debug_struct(&format!("Transaction (id = {})", self.id()));
347
348                let body = &self.inner.transaction_body;
349
350                if !body.inputs.is_empty() {
351                    debug_struct.field(
352                        "inputs",
353                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
354                            f.debug_list()
355                                .entries(self.inputs().map(pretty::ViaDisplay))
356                                .finish()
357                        }),
358                    );
359                }
360
361                if !body.outputs.is_empty() {
362                    debug_struct.field(
363                        "outputs",
364                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
365                            f.debug_list()
366                                .entries(self.outputs().map(pretty::ViaDisplay))
367                                .finish()
368                        }),
369                    );
370                }
371
372                debug_struct.field("fee", &self.fee());
373
374                let valid_from = match body.validity_interval_start {
375                    None => "]-∞".to_string(),
376                    Some(i) => format!("[{i}"),
377                };
378
379                let valid_until = match body.ttl {
380                    None => "+∞[".to_string(),
381                    Some(i) => format!("{i}["),
382                };
383
384                debug_struct.field(
385                    "validity",
386                    &pretty::ViaDisplay(format!("{valid_from}; {valid_until}")),
387                );
388
389                debug_assert!(
390                    body.certificates.is_none(),
391                    "found certificates in transaction; not yet supported"
392                );
393
394                debug_assert!(
395                    body.withdrawals.is_none(),
396                    "found withdrawals in transaction; not yet supported"
397                );
398
399                debug_assert!(
400                    body.auxiliary_data_hash.is_none(),
401                    "found auxiliary_data_hash in transaction; not yet supported"
402                );
403
404                if body.mint.is_some() {
405                    debug_struct.field("mint", &pretty::ViaDisplay(self.mint()));
406                }
407
408                if let Some(hash) = body.script_data_hash {
409                    debug_struct.field(
410                        "script_integrity_hash",
411                        &pretty::ViaDisplay(Hash::from(hash)),
412                    );
413                }
414
415                if body.collateral.is_some() {
416                    debug_struct.field(
417                        "collaterals",
418                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
419                            f.debug_list()
420                                .entries(self.collaterals().map(pretty::ViaDisplay))
421                                .finish()
422                        }),
423                    );
424                }
425
426                if body.required_signers.is_some() {
427                    debug_struct.field(
428                        "specified_signatories",
429                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
430                            f.debug_list()
431                                .entries(self.specified_signatories().map(pretty::ViaDisplay))
432                                .finish()
433                        }),
434                    );
435                }
436
437                if let Some(network_id) = body.network_id {
438                    debug_struct.field(
439                        "network_id",
440                        &pretty::ViaDisplay(NetworkId::from(network_id)),
441                    );
442                }
443
444                if let Some(collateral_return) = body
445                    .collateral_return
446                    .as_ref()
447                    .and_then(|c| Output::try_from(c.clone()).ok())
448                {
449                    debug_struct.field("collateral_return", &pretty::ViaDisplay(collateral_return));
450                }
451
452                if let Some(total_collateral) = body.total_collateral {
453                    debug_struct.field("total_collateral", &pretty::ViaDisplay(total_collateral));
454                }
455
456                if body.reference_inputs.is_some() {
457                    debug_struct.field(
458                        "reference_inputs",
459                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
460                            f.debug_list()
461                                .entries(self.reference_inputs().map(pretty::ViaDisplay))
462                                .finish()
463                        }),
464                    );
465                }
466
467                debug_assert!(
468                    body.voting_procedures.is_none(),
469                    "found votes in transaction; not yet supported"
470                );
471
472                debug_assert!(
473                    body.proposal_procedures.is_none(),
474                    "found proposals in transaction; not yet supported"
475                );
476
477                debug_assert!(
478                    body.treasury_value.is_none(),
479                    "found treasury value in transaction; not yet supported"
480                );
481
482                debug_assert!(
483                    body.donation.is_none(),
484                    "found treasury donation in transaction; not yet supported"
485                );
486
487                let witness_set = &self.inner.transaction_witness_set;
488
489                if let Some(signatures) = &witness_set.vkeywitness {
490                    debug_struct.field(
491                        "signatures",
492                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
493                            let mut map = f.debug_map();
494
495                            for witness in signatures.iter() {
496                                map.entry(
497                                    &pretty::ViaDisplay(hex::encode(&witness.vkey[..])),
498                                    &pretty::ViaDisplay(hex::encode(&witness.signature[..])),
499                                );
500                            }
501
502                            map.finish()
503                        }),
504                    );
505                }
506
507                debug_assert!(
508                    witness_set.bootstrap_witness.is_none(),
509                    "found bootstrap witness in transaction; not yet supported",
510                );
511
512                debug_assert!(
513                    witness_set.native_script.is_none(),
514                    "found native script in transaction; not yet supported",
515                );
516
517                if witness_set.plutus_v1_script.is_some()
518                    || witness_set.plutus_v2_script.is_some()
519                    || witness_set.plutus_v3_script.is_some()
520                {
521                    debug_struct.field(
522                        "scripts",
523                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
524                            let v1_scripts = witness_set
525                                .plutus_v1_script
526                                .as_ref()
527                                .map(|set| {
528                                    Box::new(set.iter().cloned().map(PlutusScript::from))
529                                        as BoxedIterator<PlutusScript>
530                                })
531                                .unwrap_or_else(|| {
532                                    Box::new(iter::empty()) as BoxedIterator<PlutusScript>
533                                });
534
535                            let v2_scripts = witness_set
536                                .plutus_v2_script
537                                .as_ref()
538                                .map(|set| {
539                                    Box::new(set.iter().cloned().map(PlutusScript::from))
540                                        as BoxedIterator<PlutusScript>
541                                })
542                                .unwrap_or_else(|| {
543                                    Box::new(iter::empty()) as BoxedIterator<PlutusScript>
544                                });
545
546                            let v3_scripts = witness_set
547                                .plutus_v3_script
548                                .as_ref()
549                                .map(|set| {
550                                    Box::new(set.iter().cloned().map(PlutusScript::from))
551                                        as BoxedIterator<PlutusScript>
552                                })
553                                .unwrap_or_else(|| {
554                                    Box::new(iter::empty()) as BoxedIterator<PlutusScript>
555                                });
556
557                            let plutus_scripts = v1_scripts.chain(v2_scripts).chain(v3_scripts);
558
559                            f.debug_list()
560                                .entries(plutus_scripts.map(pretty::ViaDisplay))
561                                .finish()
562                        }),
563                    );
564                }
565
566                if let Some(datums) = witness_set.plutus_data.as_ref() {
567                    debug_struct.field(
568                        "datums",
569                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
570                            f.debug_list()
571                                .entries(
572                                    datums
573                                        .iter()
574                                        .cloned()
575                                        .map(PlutusData::from)
576                                        .map(pretty::ViaDisplay),
577                                )
578                                .finish()
579                        }),
580                    );
581                }
582
583                if let Some(redeemers) = witness_set.redeemer.as_ref() {
584                    debug_struct.field(
585                        "redeemers",
586                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| match redeemers {
587                            pallas::Redeemers::List(_) => panic!(
588                                "found redeemers encoded as list; shouldn't be possible with this builder."
589                            ),
590                            pallas::Redeemers::Map(map) => {
591                                let mut redeemers = f.debug_map();
592                                for (key, value) in map.iter() {
593                                    redeemers.entry(
594                                        &pretty::ViaDisplay(RedeemerPointer::from(key.clone())),
595                                        &pretty::Fmt(|f: &mut fmt::Formatter<'_>| {
596                                            f.debug_tuple("Redeemer")
597                                                .field(&pretty::ViaDisplay(PlutusData::from(
598                                                    value.data.clone(),
599                                                )))
600                                                .field(&pretty::ViaDisplay(ExecutionUnits::from(
601                                                    value.ex_units,
602                                                )))
603                                                .finish()
604                                        }),
605                                    );
606                                }
607                                redeemers.finish()
608                            }
609                        }),
610                    );
611                }
612
613                debug_struct.finish()
614            })
615        )
616    }
617}
618
619impl<State: IsTransactionBodyState> Transaction<State> {
620    /// The transaction identifier, as a _blake2b-256_ hash digest of its serialised body.
621    ///
622    /// <div class="warning">While the method can be called at any time, any change on the
623    /// transaction body will alter the id. It is only stable when the state is
624    /// [`state::ReadyForSigning`]</div>
625    pub fn id(&self) -> Hash<32> {
626        let mut bytes = Vec::new();
627        let _ = cbor::encode(&self.inner.transaction_body, &mut bytes);
628        Hash::from(pallas::Hasher::<256>::hash(&bytes))
629    }
630
631    pub fn fee(&self) -> u64 {
632        self.inner.transaction_body.fee
633    }
634
635    pub fn total_collateral(&self) -> u64 {
636        self.inner
637            .transaction_body
638            .total_collateral
639            .unwrap_or_default()
640    }
641
642    /// The declared transaction inputs, which are spent in case of successful transaction.
643    pub fn inputs(&self) -> Box<dyn Iterator<Item = Input> + '_> {
644        Box::new(
645            self.inner
646                .transaction_body
647                .inputs
648                .deref()
649                .iter()
650                .cloned()
651                .map(Input::from),
652        )
653    }
654
655    /// The declared transaction collaterals, which are spent in case of failed transaction.
656    pub fn collaterals(&self) -> Box<dyn Iterator<Item = Input> + '_> {
657        self.inner
658            .transaction_body
659            .collateral
660            .as_ref()
661            .map(|xs| Box::new(xs.iter().cloned().map(Input::from)) as BoxedIterator<'_, Input>)
662            .unwrap_or_else(|| Box::new(iter::empty()) as BoxedIterator<'_, Input>)
663    }
664
665    /// The declared transaction reference inputs, which are never spent but contribute to the
666    /// script context for smart-contract execution.
667    pub fn reference_inputs(&self) -> Box<dyn Iterator<Item = Input> + '_> {
668        self.inner
669            .transaction_body
670            .reference_inputs
671            .as_ref()
672            .map(|xs| Box::new(xs.iter().cloned().map(Input::from)) as BoxedIterator<'_, Input>)
673            .unwrap_or_else(|| Box::new(iter::empty()) as BoxedIterator<'_, Input>)
674    }
675
676    pub fn mint(&self) -> Value<i64> {
677        self.inner
678            .transaction_body
679            .mint
680            .as_ref()
681            .map(Value::from)
682            .unwrap_or_default()
683    }
684
685    /// The declared transaction outputs, which are produced in case of successful transaction.
686    pub fn outputs(&self) -> Box<dyn Iterator<Item = Output> + '_> {
687        Box::new(
688            self.inner
689                .transaction_body
690                .outputs
691                .iter()
692                .cloned()
693                .map(Output::try_from)
694                .collect::<Result<Vec<_>, _>>()
695                .expect("transaction contains invalid outputs; should be impossible at this point.")
696                .into_iter(),
697        )
698    }
699
700    /// View this transaction as a UTxO, mapping each output to its corresponding input reference.
701    pub fn as_resolved_inputs(&self) -> BTreeMap<Input, Output> {
702        let id = self.id();
703        self.outputs()
704            .enumerate()
705            .fold(BTreeMap::new(), |mut resolved_inputs, (ix, output)| {
706                resolved_inputs.insert(Input::new(id, ix as u64), output);
707                resolved_inputs
708            })
709    }
710
711    /// The list of signatories explicitly listed in the transaction body, and visible to any
712    /// underlying validator script. This is necessary a subset of the all signatories but the
713    /// total set of inferred signatories may be larger due do transaction inputs.
714    ///
715    /// In the wild, this may also be called:
716    ///
717    /// - 'required_signers' (e.g. in the 'official' CDDL: <https://github.com/IntersectMBO/cardano-ledger/blob/232511b0fa01cd848cd7a569d1acc322124cf9b8/eras/conway/impl/cddl-files/conway.cddl#L142>)
718    /// - 'extra_signatories' (e.g. in Aiken's stdlib: https://aiken-lang.github.io/stdlib/cardano/transaction.html#Transaction
719    ///
720    fn specified_signatories(&self) -> Box<dyn Iterator<Item = Hash<28>> + '_> {
721        self.inner
722            .transaction_body
723            .required_signers
724            .as_ref()
725            .map(|xs| Box::new(xs.deref().iter().map(<Hash<_>>::from)) as BoxedIterator<'_, _>)
726            .unwrap_or_else(|| Box::new(iter::empty()) as BoxedIterator<'_, _>)
727    }
728}
729
730// -------------------------------------------------------------------- Internal
731
732impl<State: IsTransactionBodyState> Transaction<State> {
733    /// The list of required signatories on the transaction, solely inferred from inputs,
734    /// collaterals and explicitly specified signers.
735    ///
736    /// FIXME:
737    ///
738    /// - account for signers from native scripts
739    /// - account for signers from certificates
740    /// - account for signers from votes
741    /// - account for signers from withdrawals
742    fn required_signatories(
743        &self,
744        resolved_inputs: &BTreeMap<Input, Output>,
745    ) -> anyhow::Result<BTreeSet<Hash<28>>> {
746        let body = &self.inner.transaction_body;
747
748        debug_assert!(
749            body.certificates.is_none(),
750            "found certificates in transaction: not supported yet",
751        );
752
753        debug_assert!(
754            body.withdrawals.is_none(),
755            "found withdrawals in transaction: not supported yet",
756        );
757
758        debug_assert!(
759            body.voting_procedures.is_none(),
760            "found votes in transaction: not supported yet",
761        );
762
763        Ok(self
764            .specified_signatories()
765            .chain(
766                self.inputs()
767                    .chain(self.collaterals())
768                    .map(|input| {
769                        let output =
770                            resolved_inputs
771                                .get(&input)
772                                .ok_or(anyhow!("unknown = {input}").context(
773                                    "unknown output for specified input or collateral input; found in transaction but not provided in resolved set",
774                                ))?;
775                        Ok::<_, anyhow::Error>(output)
776                    })
777                    .collect::<Result<Vec<_>, _>>()?
778                    .into_iter()
779                    .filter_map(|output| {
780                        let address = output.address();
781                        let address = address.as_shelley()?;
782                        address.payment().as_key()
783                    }),
784            )
785            .collect::<BTreeSet<_>>())
786    }
787
788    /// The set of scripts that must be executed and pass for the transaction to be valid. These
789    /// are specifically relevant to someone building a transaction as they each require a specific
790    /// redeemer and each have execution costs.
791    ///
792    /// Those scripts are distinct from all scripts available in the transaction since one may
793    /// introduce scripts via reference inputs or via the witness set, even if their (valid)
794    /// execution isn't required.
795    ///
796    /// 'required_scripts' may, therefore, come from multiple sources:
797    ///
798    /// - inputs (for script-locked inputs)
799    /// - certificates (for script-based credentials)
800    /// - mint (for minting/burning scripts)
801    /// - withdrawals (for script-based credentials)
802    /// - proposals (for any defined constitution guardrails)
803    /// - votes (for script-based credentials)
804    ///
805    /// FIXME: the function is currently partial, as certificates, withdrawals, votes and proposals
806    /// aren't implemented.
807    fn required_scripts(
808        &self,
809        resolved_inputs: &BTreeMap<Input, Output>,
810    ) -> BTreeMap<RedeemerPointer, Hash<28>> {
811        let from_inputs = self
812            .inputs()
813            .enumerate()
814            .filter_map(|(index, input)| Some((index, resolved_inputs.get(&input)?)))
815            .filter_map(|(index, output)| {
816                let payment_credential = output.address().as_shelley()?.payment();
817                Some((index, payment_credential.as_script()?))
818            })
819            .map(|(index, hash)| (RedeemerPointer::from_spend(index as u32), hash));
820
821        let from_mint = self
822            .inner
823            .transaction_body
824            .mint
825            .as_ref()
826            .map(|assets| {
827                Box::new(assets.iter().enumerate().map(|(index, (script_hash, _))| {
828                    (
829                        RedeemerPointer::from_mint(index as u32),
830                        Hash::from(script_hash),
831                    )
832                })) as Box<dyn Iterator<Item = (RedeemerPointer, Hash<28>)>>
833            })
834            .unwrap_or_else(|| {
835                Box::new(std::iter::empty())
836                    as Box<dyn Iterator<Item = (RedeemerPointer, Hash<28>)>>
837            });
838
839        let body = &self.inner.transaction_body;
840
841        debug_assert!(
842            body.certificates.is_none(),
843            "found certificates in transaction: not supported yet",
844        );
845
846        debug_assert!(
847            body.withdrawals.is_none(),
848            "found withdrawals in transaction: not supported yet",
849        );
850
851        debug_assert!(
852            body.voting_procedures.is_none(),
853            "found votes in transaction: not supported yet",
854        );
855
856        debug_assert!(
857            body.proposal_procedures.is_none(),
858            "found proposals in transaction: not supported yet",
859        );
860
861        std::iter::empty()
862            .chain(from_inputs)
863            .chain(from_mint)
864            .collect()
865    }
866
867    /// Pre-condition: this assumes and only support Plutus V3.
868    fn script_integrity_hash(&self, params: &ProtocolParameters) -> Option<Hash<32>> {
869        debug_assert!(
870            self.inner
871                .transaction_witness_set
872                .plutus_v1_script
873                .is_none(),
874            "found plutus v1 scripts in the transaction witness set; not supported yet"
875        );
876
877        debug_assert!(
878            self.inner
879                .transaction_witness_set
880                .plutus_v2_script
881                .is_none(),
882            "found plutus v2 scripts in the transaction witness set; not supported yet"
883        );
884
885        let redeemers = self.inner.transaction_witness_set.redeemer.as_ref();
886
887        let datums = self.inner.transaction_witness_set.plutus_data.as_ref();
888
889        if redeemers.is_none() && datums.is_none() {
890            return None;
891        }
892
893        let mut preimage: Vec<u8> = Vec::new();
894        if let Some(redeemers) = redeemers {
895            cbor::encode(redeemers, &mut preimage).unwrap();
896        }
897
898        if let Some(datums) = datums {
899            cbor::encode(datums, &mut preimage).unwrap();
900        }
901
902        cbor::encode(
903            pallas::NonEmptyKeyValuePairs::Def(vec![(
904                PlutusVersion::V3,
905                params.plutus_v3_cost_model(),
906            )]),
907            &mut preimage,
908        )
909        .unwrap();
910
911        Some(Hash::from(pallas::Hasher::<256>::hash(&preimage)))
912    }
913}
914
915impl Transaction<state::InConstruction> {
916    fn with_change_output(&mut self, change: Value<u64>) -> anyhow::Result<()> {
917        let min_change_value =
918            Output::new(Address::default(), change.clone()).min_acceptable_value();
919
920        if change.lovelace() < min_change_value {
921            return Err(
922                anyhow!("not enough funds to create a sufficiently large change output").context(
923                    format!(
924                        "current value={} lovelace, minimum required={}",
925                        change.lovelace(),
926                        min_change_value
927                    ),
928                ),
929            );
930        }
931
932        let mut outputs = mem::take(&mut self.inner.transaction_body.outputs)
933            .into_iter()
934            .map(Output::try_from)
935            .collect::<Result<VecDeque<_>, _>>()?;
936
937        mem::take(&mut self.change_strategy).apply(change, &mut outputs)?;
938
939        self.with_outputs(outputs);
940
941        Ok(())
942    }
943
944    fn with_redeemers(
945        &mut self,
946        discard_if: impl Fn(pallas::RedeemerTag) -> bool,
947        redeemers: BTreeMap<RedeemerPointer, PlutusData>,
948    ) -> &mut Self {
949        let redeemers = into_pallas_redeemers(redeemers);
950
951        let new_redeemers = if let Some(existing_redeemers) =
952            mem::take(&mut self.inner.transaction_witness_set.redeemer)
953        {
954            let existing_redeemers = without_existing_redeemers(existing_redeemers, discard_if);
955            Box::new(existing_redeemers.chain(redeemers))
956                as Box<dyn Iterator<Item = (pallas::RedeemersKey, pallas::RedeemersValue)>>
957        } else {
958            Box::new(redeemers)
959                as Box<dyn Iterator<Item = (pallas::RedeemersKey, pallas::RedeemersValue)>>
960        };
961
962        self.inner.transaction_witness_set.redeemer =
963            pallas::NonEmptyKeyValuePairs::from_vec(new_redeemers.collect())
964                .map(pallas::Redeemers::from);
965
966        self
967    }
968
969    fn with_script_integrity_hash(
970        &mut self,
971        required_scripts: &BTreeMap<RedeemerPointer, Hash<28>>,
972        params: &ProtocolParameters,
973    ) -> anyhow::Result<()> {
974        if let Some(hash) = self.script_integrity_hash(params) {
975            self.inner.transaction_body.script_data_hash = Some(pallas::Hash::from(hash));
976        } else if !required_scripts.is_empty() {
977            let mut scripts = required_scripts.iter();
978
979            let (ptr, hash) = scripts.next().unwrap(); // Safe because it's not empty
980            let mut err = anyhow!("required_scripts = {ptr} -> {hash}");
981            for (ptr, hash) in scripts {
982                err = err.context(format!("required_scripts = {ptr} -> {hash}"));
983            }
984
985            return Err(err.context("couldn't compute required script integrity hash: datums and redeemers are missing from the transaction."));
986        }
987
988        Ok(())
989    }
990
991    fn with_execution_units(
992        &mut self,
993        redeemers: &mut BTreeMap<RedeemerPointer, ExecutionUnits>,
994    ) -> anyhow::Result<()> {
995        if let Some(declared_redeemers) =
996            std::mem::take(&mut self.inner.transaction_witness_set.redeemer)
997        {
998            match declared_redeemers {
999                pallas::Redeemers::List(..) => {
1000                    unreachable!("found redeemers encoded as list: impossible with this library.")
1001                }
1002
1003                pallas::Redeemers::Map(kv) => {
1004                    self.inner.transaction_witness_set.redeemer =
1005                        pallas::NonEmptyKeyValuePairs::from_vec(
1006                            kv.into_iter()
1007                                .map(|(key, mut value)| {
1008                                    let ptr = RedeemerPointer::from(key.clone());
1009                                    // If we have already computed the correct execution units
1010                                    // for that redeemer in a previous round, we can adjust
1011                                    // them.
1012                                    if let Some(ex_units) = redeemers.remove(&ptr) {
1013                                        value.ex_units = pallas::ExUnits::from(ex_units);
1014                                    }
1015                                    (key, value)
1016                                })
1017                                .collect(),
1018                        )
1019                        .map(pallas::Redeemers::from)
1020                }
1021            }
1022        }
1023
1024        // We should technically have consumed all redeemers.
1025        if !redeemers.is_empty() {
1026            return Err(
1027                anyhow!("extraneous redeemers in transaction; not required by any script").context(
1028                    format!(
1029                        "extra={:?}",
1030                        redeemers
1031                            .keys()
1032                            .map(|ptr| ptr.to_string())
1033                            .collect::<Vec<_>>()
1034                    ),
1035                ),
1036            );
1037        }
1038
1039        Ok(())
1040    }
1041
1042    fn with_change(&mut self, resolved_inputs: &BTreeMap<Input, Output>) -> anyhow::Result<()> {
1043        let mut change = Value::default();
1044
1045        // Add inputs to the change balance
1046        self.inputs().try_fold(&mut change, |total_input, input| {
1047            let output = resolved_inputs.get(&input).ok_or_else(|| {
1048                anyhow!("unknown input, not present in resolved set")
1049                    .context(format!("input={input}"))
1050            })?;
1051
1052            Ok::<_, anyhow::Error>(total_input.add(output.value()))
1053        })?;
1054
1055        // Partition mint quantities between mint & burn
1056        let (mint, burn) = self.mint().assets().clone().into_iter().fold(
1057            (BTreeMap::new(), BTreeMap::new()),
1058            |(mut mint, mut burn), (script_hash, assets)| {
1059                let mut minted_assets = BTreeMap::new();
1060                let mut burned_assets = BTreeMap::new();
1061
1062                for (asset_name, quantity) in assets {
1063                    if quantity > 0 {
1064                        minted_assets.insert(asset_name, quantity as u64);
1065                    } else {
1066                        burned_assets.insert(asset_name, (-quantity) as u64);
1067                    }
1068                }
1069
1070                if !minted_assets.is_empty() {
1071                    mint.insert(script_hash, minted_assets);
1072                }
1073
1074                if !burned_assets.is_empty() {
1075                    burn.insert(script_hash, burned_assets);
1076                }
1077
1078                (mint, burn)
1079            },
1080        );
1081
1082        // Add minted tokens to the change balance
1083        change.add(&Value::default().with_assets(mint));
1084
1085        // Subtract burned tokens from the change balance
1086        change
1087            .checked_sub(&Value::default().with_assets(burn))
1088            .map_err(|e| e.context("insufficient balance; spending more than available"))?;
1089
1090        // Subtract all outputs from the change balance
1091        self.outputs()
1092            .try_fold(&mut change, |total_output, output| {
1093                total_output.checked_sub(output.value())
1094            })
1095            .map_err(|e| e.context("insufficient balance; spending more than available"))?;
1096
1097        // Subtract the transaction fee as well
1098        change
1099            .checked_sub(&Value::new(self.fee()))
1100            .map_err(|e| e.context("insufficient balance; spending more than available"))?;
1101
1102        let body = &self.inner.transaction_body;
1103
1104        debug_assert!(
1105            body.certificates.is_none(),
1106            "found certificates in transaction: not supported yet",
1107        );
1108
1109        debug_assert!(
1110            body.withdrawals.is_none(),
1111            "found withdrawals in transaction: not supported yet",
1112        );
1113
1114        debug_assert!(
1115            body.treasury_value.is_none(),
1116            "found treasury donation in transaction: not supported yet",
1117        );
1118
1119        debug_assert!(
1120            body.proposal_procedures.is_none(),
1121            "found proposals in transaction: not supported yet",
1122        );
1123
1124        if !change.is_empty() {
1125            self.with_change_output(change)?;
1126        }
1127
1128        Ok(())
1129    }
1130
1131    fn with_collateral_return(
1132        &mut self,
1133        resolved_inputs: &BTreeMap<Input, Output>,
1134        params: &ProtocolParameters,
1135    ) -> anyhow::Result<()> {
1136        let (mut total_collateral_value, opt_return_address): (Value<u64>, Option<Address<_>>) =
1137            self.collaterals()
1138                .map(|input| {
1139                    resolved_inputs.get(&input).ok_or_else(|| {
1140                        anyhow!("unknown collateral input").context(format!("reference={input}"))
1141                    })
1142                })
1143                .try_fold(
1144                    (Value::new(0), None),
1145                    |(mut total, address), maybe_output| {
1146                        let output = maybe_output?;
1147                        total.add(output.value());
1148                        // It is arbitrary, but we use the source address of the first collateral as the
1149                        // destination of the collateral change. Collaterals can't be script, so this is
1150                        // relatively safe as the ledger enforces that the key is known at the time the
1151                        // transaction is constructed.
1152                        Ok::<_, anyhow::Error>((
1153                            total,
1154                            address.or_else(|| Some(output.address().to_owned())),
1155                        ))
1156                    },
1157                )?;
1158
1159        if let Some(return_address) = opt_return_address {
1160            let minimum_collateral = params.minimum_collateral(self.fee());
1161
1162            total_collateral_value
1163                .checked_sub(&Value::new(minimum_collateral))
1164                .map_err(|e| e.context("insufficient collateral inputs"))?;
1165
1166            self.inner.transaction_body.total_collateral = Some(minimum_collateral);
1167            self.inner.transaction_body.collateral_return = Some(pallas::TransactionOutput::from(
1168                // A bit misleading but 'total_collateral_value' now refers to the total amount brought
1169                // in, minus the the minimum required by the protocol, left out.
1170                Output::new(return_address, total_collateral_value),
1171            ));
1172        }
1173
1174        Ok(())
1175    }
1176}
1177
1178/// Obtain a more friendly representation of Pallas' redeemers, without any redeemer matching the
1179/// given predicate.
1180fn without_existing_redeemers(
1181    redeemers: pallas::Redeemers,
1182    predicate: impl Fn(pallas::RedeemerTag) -> bool,
1183) -> impl Iterator<Item = (pallas::RedeemersKey, pallas::RedeemersValue)> {
1184    match redeemers {
1185        pallas::Redeemers::List(..) => {
1186            unreachable!("found redeemers encoded as list: impossible with this library.")
1187        }
1188        pallas::Redeemers::Map(kv) => kv.into_iter().filter(move |(k, _)| !predicate(k.tag)),
1189    }
1190}
1191
1192fn into_pallas_redeemers(
1193    redeemers: BTreeMap<RedeemerPointer, PlutusData>,
1194) -> impl Iterator<Item = (pallas::RedeemersKey, pallas::RedeemersValue)> {
1195    redeemers.into_iter().map(|(ptr, data)| {
1196        let key = pallas::RedeemersKey::from(ptr);
1197
1198        let value = pallas::RedeemersValue {
1199            data: pallas::PlutusData::from(data),
1200            ex_units: pallas::ExUnits::from(ExecutionUnits::default()),
1201        };
1202
1203        (key, value)
1204    })
1205}
1206
1207// -------------------------------------------------------------------- Encoding
1208
1209impl<C, State: IsTransactionBodyState> cbor::Encode<C> for Transaction<State> {
1210    fn encode<W: cbor::encode::write::Write>(
1211        &self,
1212        e: &mut cbor::Encoder<W>,
1213        ctx: &mut C,
1214    ) -> Result<(), cbor::encode::Error<W::Error>> {
1215        e.encode_with(&self.inner, ctx)?;
1216        Ok(())
1217    }
1218}
1219
1220impl<'d, C> cbor::Decode<'d, C> for Transaction<state::Unknown> {
1221    fn decode(d: &mut cbor::Decoder<'d>, ctx: &mut C) -> Result<Self, cbor::decode::Error> {
1222        Ok(Self {
1223            inner: d.decode_with(ctx)?,
1224            state: PhantomData,
1225            change_strategy: (),
1226        })
1227    }
1228}
1229
1230impl<'d, C> cbor::Decode<'d, C> for Transaction<state::ReadyForSigning> {
1231    fn decode(d: &mut cbor::Decoder<'d>, ctx: &mut C) -> Result<Self, cbor::decode::Error> {
1232        Ok(Self {
1233            inner: d.decode_with(ctx)?,
1234            state: PhantomData,
1235            change_strategy: (),
1236        })
1237    }
1238}
1239
1240// ------------------------------------------------------------------------ WASM
1241
1242#[cfg(test)]
1243mod tests {
1244    use crate::{SigningKey, Transaction, cbor, transaction::state::*};
1245    use indoc::indoc;
1246
1247    #[test]
1248    fn display_transaction_1() {
1249        let mut transaction: Transaction<ReadyForSigning> = cbor::decode(
1250            &hex::decode(
1251                "84a300d9010281825820c984c8bf52a141254c714c905b2d27b432d4b546f815fbc\
1252                 2fea7b9da6e490324030182a30058390082c1729d5fd44124a6ae72bcdb86b6e827\
1253                 aac6a74301e4003c092e6f4af57b0c9ff6ca5218967d1e7a3f572d7cd277d73468d\
1254                 3b2fca56572011a001092a803d818558203525101010023259800a518a4d1365640\
1255                 04ae69a20058390082c1729d5fd44124a6ae72bcdb86b6e827aac6a74301e4003c0\
1256                 92e6f4af57b0c9ff6ca5218967d1e7a3f572d7cd277d73468d3b2fca56572011a00\
1257                 a208bb021a00029755a0f5f6\
1258                ",
1259            )
1260            .unwrap(),
1261        )
1262        .unwrap();
1263
1264        let signing_key = SigningKey::from([
1265            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1266            0, 0, 0,
1267        ]);
1268        transaction.sign(&signing_key);
1269
1270        assert_eq!(
1271            transaction.to_string(),
1272            indoc! {"
1273                Transaction (id = 036fd8d808d4a87737cbb0ed1e61b08ce753323e94fc118c5eefabee6a8e04a5) {
1274                    inputs: [
1275                        Input(c984c8bf52a141254c714c905b2d27b432d4b546f815fbc2fea7b9da6e490324#3),
1276                    ],
1277                    outputs: [
1278                        Output {
1279                            address: addr_test1qzpvzu5atl2yzf9x4eetekuxkm5z02kx5apsreqq8syjum6274ase8lkeffp39narear74ed0nf804e5drfm9l99v4eq3ecz8t,
1280                            value: Value {
1281                                lovelace: 1086120,
1282                            },
1283                            script: v3(bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777),
1284                        },
1285                        Output {
1286                            address: addr_test1qzpvzu5atl2yzf9x4eetekuxkm5z02kx5apsreqq8syjum6274ase8lkeffp39narear74ed0nf804e5drfm9l99v4eq3ecz8t,
1287                            value: Value {
1288                                lovelace: 10619067,
1289                            },
1290                        },
1291                    ],
1292                    fee: 169813,
1293                    validity: ]-∞; +∞[,
1294                    signatures: {
1295                        3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29: d739204915ea986ce309662cadfab44f8ffb9b0c10c6ade3839e2c5b11a6ba738ee2cbb1365ab714312fb79af0effb98c54ec92c88c99967e1e6cc87b56dc90e,
1296                    },
1297                }"
1298            },
1299        );
1300    }
1301
1302    #[test]
1303    fn display_transaction_2() {
1304        let transaction: Transaction<ReadyForSigning> = cbor::decode(
1305            &hex::decode(
1306                "84a700d9010283825820036fd8d808d4a87737cbb0ed1e61b08ce753323e94fc118\
1307                 c5eefabee6a8e04a5008258203522a630e91e631f56897be2898e059478c300f4bb\
1308                 8dd7891549a191b4bf1090008258208d56891b4638203175c488e19d630bfbc8af2\
1309                 85353aeeb1053d54a3c371b7a40010181a20058390082c1729d5fd44124a6ae72bc\
1310                 db86b6e827aac6a74301e4003c092e6f4af57b0c9ff6ca5218967d1e7a3f572d7cd\
1311                 277d73468d3b2fca56572011a00aab370021a0002b1ef0b5820d37acc9c984616d9\
1312                 d15825afeaf7d266e5bde38fdd4df4f8b2312703022d474d0dd90102818258208d5\
1313                 6891b4638203175c488e19d630bfbc8af285353aeeb1053d54a3c371b7a400110a2\
1314                 0058390082c1729d5fd44124a6ae72bcdb86b6e827aac6a74301e4003c092e6f4af\
1315                 57b0c9ff6ca5218967d1e7a3f572d7cd277d73468d3b2fca56572011a004f245b11\
1316                 1a00040ae7a105a18200018280821906411a0004d2f5f5f6\
1317                ",
1318            )
1319            .unwrap(),
1320        )
1321        .unwrap();
1322
1323        assert_eq!(
1324            transaction.to_string(),
1325            indoc! {"
1326                Transaction (id = cd8c5bf00ab490d57c82ebf6364e4a6337dc214d635e8c392deaa7e4b98ed6ea) {
1327                    inputs: [
1328                        Input(036fd8d808d4a87737cbb0ed1e61b08ce753323e94fc118c5eefabee6a8e04a5#0),
1329                        Input(3522a630e91e631f56897be2898e059478c300f4bb8dd7891549a191b4bf1090#0),
1330                        Input(8d56891b4638203175c488e19d630bfbc8af285353aeeb1053d54a3c371b7a40#1),
1331                    ],
1332                    outputs: [
1333                        Output {
1334                            address: addr_test1qzpvzu5atl2yzf9x4eetekuxkm5z02kx5apsreqq8syjum6274ase8lkeffp39narear74ed0nf804e5drfm9l99v4eq3ecz8t,
1335                            value: Value {
1336                                lovelace: 11187056,
1337                            },
1338                        },
1339                    ],
1340                    fee: 176623,
1341                    validity: ]-∞; +∞[,
1342                    script_integrity_hash: d37acc9c984616d9d15825afeaf7d266e5bde38fdd4df4f8b2312703022d474d,
1343                    collaterals: [
1344                        Input(8d56891b4638203175c488e19d630bfbc8af285353aeeb1053d54a3c371b7a40#1),
1345                    ],
1346                    collateral_return: Output {
1347                        address: addr_test1qzpvzu5atl2yzf9x4eetekuxkm5z02kx5apsreqq8syjum6274ase8lkeffp39narear74ed0nf804e5drfm9l99v4eq3ecz8t,
1348                        value: Value {
1349                            lovelace: 5186651,
1350                        },
1351                    },
1352                    total_collateral: 264935,
1353                    redeemers: {
1354                        Spend(1): Redeemer(
1355                            CBOR(80),
1356                            ExecutionUnits {
1357                                mem: 1601,
1358                                cpu: 316149,
1359                            },
1360                        ),
1361                    },
1362                }"
1363            },
1364        );
1365    }
1366}