cardano_sdk/cardano/transaction/
builder.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    ExecutionUnits, Hash, Input, Output, ProtocolParameters, RedeemerPointer, Transaction,
7    cardano::transaction::{IsTransactionBodyState, state},
8    cbor::{self, ToCbor},
9    pallas,
10};
11use anyhow::anyhow;
12
13use std::{
14    collections::{BTreeMap, BTreeSet},
15    marker::PhantomData,
16    mem,
17};
18use uplc::tx::SlotConfig;
19
20// ```cddl
21// vkeywitness = [ vkey, signature ]
22// ```
23const SIZE_OF_KEY_WITNESS: u64 = 1 // 1 byte for the 2-tuple declaration
24    + (32 + 2) // 32 bytes of verification & 2 bytes of CBOR bytestring declaration
25    + (64 + 2); // 64 bytes of signature + 2 bytes of CBOR bytestring declaration
26
27// - 1 bytes for the map key(s).
28//
29// - 3 bytes for the declaration of a CBOR-Set, 1 for the tag itself, and 2 for the tag index.
30//
31// - 1 to 3 bytes for the witness lists declaration. The size varies based on the number of
32//   witnesses. For more than 255 witnesses, the size will be encoded over 3 bytes and allow up to
33//   65535 witnesses, which should be enough...
34//
35// TODO: Note that we then multiply that size by 2 to cope with both standard and byron witnesses. In
36// practice, We could potentially distinguish based on the type of witness, but that's more work.
37const SIZE_OF_KEY_WITNESSES_OVERHEAD: u64 = 2 * (1 + 3 + 3);
38
39impl Transaction<state::InConstruction> {
40    /// Build a transaction by repeatedly executing some building logic with different fee and execution
41    /// units settings. Stops when a fixed point is reached.
42    ///
43    /// The final transaction has corresponding fees, execution units, collateral return and script
44    /// integrity hash set.
45    ///
46    /// # notes
47    ///
48    /// - any input specified in the transaction (either as input, reference input or collateral) must be present in the provided `resolved_inputs`.
49    /// - `with_change_strategy` must **always** be present for there's no default change strategy.
50    /// - while it is allowed (for whatever reasons), you aren't expected to fiddle with fees here;
51    ///   this is entirely managed by the `build` loop.
52    ///
53    /// # examples
54    ///
55    /// ## A simple transaction from a single input to a single recipient, with change.
56    ///
57    /// ```rust
58    /// # use cardano_sdk::{ChangeStrategy, ProtocolParameters, Transaction, input, output, value, address};
59    /// # use std::collections::btree_map::BTreeMap;
60    /// # use indoc::indoc;
61    /// // The available UTxO, typically fetched from a blockchain provider or an indexer.
62    /// let resolved_inputs = BTreeMap::from([
63    ///   (
64    ///     input!("32b5e793d26af181cb837ab7470ba6e10e15ff638088bc6b099bb22b54b4796c", 1),
65    ///     output!(
66    ///       "addr1vx7n46v3kk40ejh7tjnswk9ax65m97rj74lk6wsllg8twacak3e47",
67    ///       value!(10_000_000),
68    ///     ),
69    ///   )
70    /// ]);
71    ///
72    /// // Send a minimum lovelace value to an arbitrary address, and the change back to the
73    /// // sender.
74    /// assert_eq!(
75    ///   Transaction::build(&ProtocolParameters::mainnet(), &resolved_inputs, |tx| tx
76    ///     .with_inputs(vec![
77    ///       input!("32b5e793d26af181cb837ab7470ba6e10e15ff638088bc6b099bb22b54b4796c", 1, _),
78    ///     ])
79    ///     .with_outputs(vec![
80    ///       output!("addr1wyhcwt6h7mf6rlaqadmhh5awnyd44t7v4lju5ur430fk4xczzq8aw"),
81    ///     ])
82    ///     .with_change_strategy(ChangeStrategy::as_last_output(
83    ///       address!("addr1vx7n46v3kk40ejh7tjnswk9ax65m97rj74lk6wsllg8twacak3e47"),
84    ///     ))
85    ///     .ok()
86    ///   ).unwrap().to_string(),
87    ///   indoc!{"
88    ///     Transaction (id = 6f8e53f61fe0a709e1c895c1bd9487e555779de0902ad25377d4de1df48f08b8) {
89    ///         inputs: [
90    ///             Input(32b5e793d26af181cb837ab7470ba6e10e15ff638088bc6b099bb22b54b4796c#1),
91    ///         ],
92    ///         outputs: [
93    ///             Output {
94    ///                 address: addr1wyhcwt6h7mf6rlaqadmhh5awnyd44t7v4lju5ur430fk4xczzq8aw,
95    ///                 value: Value {
96    ///                     lovelace: 857690,
97    ///                 },
98    ///             },
99    ///             Output {
100    ///                 address: addr1vx7n46v3kk40ejh7tjnswk9ax65m97rj74lk6wsllg8twacak3e47,
101    ///                 value: Value {
102    ///                     lovelace: 8976061,
103    ///                 },
104    ///             },
105    ///         ],
106    ///         fee: 166249,
107    ///         validity: ]-∞; +∞[,
108    ///     }"
109    ///   }
110    /// );
111    /// ```
112    ///
113    /// ## Minting assets using a plutus script.
114    ///
115    ///
116    /// ```rust
117    /// # use cardano_sdk::{ChangeStrategy, PlutusData, PlutusVersion, ProtocolParameters, SlotBound, Transaction};
118    /// # use cardano_sdk::{address, assets, input, output, plutus_script, value};
119    /// # use std::collections::btree_map::BTreeMap;
120    /// # use indoc::indoc;
121    /// let resolved_inputs = BTreeMap::from([
122    ///   (
123    ///     input!("32b5e793d26af181cb837ab7470ba6e10e15ff638088bc6b099bb22b54b4796c", 1),
124    ///     output!(
125    ///       "addr1vx7n46v3kk40ejh7tjnswk9ax65m97rj74lk6wsllg8twacak3e47",
126    ///       value!(10_000_000),
127    ///     ),
128    ///   )
129    /// ]);
130    ///
131    /// // Notice the absence of outputs and of collateral return. We let the builder handler those for us.
132    /// assert_eq!(
133    ///   Transaction::build(&ProtocolParameters::mainnet(), &resolved_inputs, |tx| tx
134    ///     .with_inputs(vec![
135    ///       input!("32b5e793d26af181cb837ab7470ba6e10e15ff638088bc6b099bb22b54b4796c", 1, _),
136    ///     ])
137    ///     .with_collaterals(vec![
138    ///       input!("32b5e793d26af181cb837ab7470ba6e10e15ff638088bc6b099bb22b54b4796c", 1),
139    ///     ])
140    ///     .with_change_strategy(ChangeStrategy::as_last_output(
141    ///       address!("addr1vx7n46v3kk40ejh7tjnswk9ax65m97rj74lk6wsllg8twacak3e47"),
142    ///     ))
143    ///     .with_mint(assets!(
144    ///       (
145    ///         "bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777",
146    ///         "7768617465766572",
147    ///         100_i64,
148    ///         PlutusData::list::<PlutusData>([]),
149    ///       ),
150    ///     ))
151    ///     .with_validity_interval(SlotBound::None, SlotBound::Exclusive(123456789))
152    ///     .with_plutus_scripts(vec![
153    ///       plutus_script!(PlutusVersion::V3, "5101010023259800a518a4d136564004ae69")
154    ///     ])
155    ///     .ok()
156    ///   ).unwrap().to_string(),
157    ///   indoc!{"
158    ///     Transaction (id = 07fbde0af6eaceb28a59f764e442002382181af756707e0ce0325354d2b26fac) {
159    ///         inputs: [
160    ///             Input(32b5e793d26af181cb837ab7470ba6e10e15ff638088bc6b099bb22b54b4796c#1),
161    ///         ],
162    ///         outputs: [
163    ///             Output {
164    ///                 address: addr1vx7n46v3kk40ejh7tjnswk9ax65m97rj74lk6wsllg8twacak3e47,
165    ///                 value: Value {
166    ///                     lovelace: 9824087,
167    ///                     assets: {
168    ///                         bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777: {
169    ///                             whatever: 100,
170    ///                         },
171    ///                     },
172    ///                 },
173    ///             },
174    ///         ],
175    ///         fee: 175913,
176    ///         validity: ]-∞; 123456789[,
177    ///         mint: Value {
178    ///             lovelace: 0,
179    ///             assets: {
180    ///                 bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777: {
181    ///                     whatever: 100,
182    ///                 },
183    ///             },
184    ///         },
185    ///         script_integrity_hash: b5a66ea46c7628f9b151d6e029f322058d9dff3793a98f3cfed6e21ed7064c4f,
186    ///         collaterals: [
187    ///             Input(32b5e793d26af181cb837ab7470ba6e10e15ff638088bc6b099bb22b54b4796c#1),
188    ///         ],
189    ///         collateral_return: Output {
190    ///             address: addr1vx7n46v3kk40ejh7tjnswk9ax65m97rj74lk6wsllg8twacak3e47,
191    ///             value: Value {
192    ///                 lovelace: 9736130,
193    ///             },
194    ///         },
195    ///         total_collateral: 263870,
196    ///         scripts: [
197    ///             v3(bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777),
198    ///         ],
199    ///         redeemers: {
200    ///             Mint(0): Redeemer(
201    ///                 CBOR(80),
202    ///                 ExecutionUnits {
203    ///                     mem: 1601,
204    ///                     cpu: 316149,
205    ///                 },
206    ///             ),
207    ///         },
208    ///     }"
209    ///   },
210    /// );
211    pub fn build<F>(
212        params: &ProtocolParameters,
213        resolved_inputs: &BTreeMap<Input, Output>,
214        build: F,
215    ) -> anyhow::Result<Transaction<state::ReadyForSigning>>
216    where
217        F: Fn(&mut Self) -> anyhow::Result<&mut Self>,
218    {
219        let mut attempts: usize = 0;
220        let mut fee: u64 = 0;
221        let mut tx: Self;
222        let mut redeemers: BTreeMap<RedeemerPointer, ExecutionUnits> = BTreeMap::new();
223        let mut serialized_tx: Vec<u8> = Vec::new();
224
225        loop {
226            tx = Transaction::default();
227
228            build(tx.with_fee(fee))?;
229
230            let required_scripts = tx.required_scripts(resolved_inputs);
231
232            // Add a change output according to the user's chosen strategy.
233            tx.with_change(resolved_inputs)?;
234
235            // Compute & add total collateral + collateral return
236            tx.with_collateral_return(resolved_inputs, params)?;
237
238            // Adjust execution units calculated in previous iteration for all redeemers.
239            tx.with_execution_units(&mut redeemers)?;
240
241            // Add the script integrity hash, so that it counts towards the transaction size.
242            tx.with_script_integrity_hash(&required_scripts, params)?;
243
244            // Explicitly fails when there's no collaterals, but that Plutus scripts were found.
245            // This informs the user of the builder that they did something wrong and forgot to set
246            // one or more collateral.
247            fail_on_missing_collateral(&required_scripts, tx.collaterals())?;
248
249            // Fails when any output is below its minimum Ada value. We specifically call this
250            // after 'with_change' so that it's possible for users to create outputs that are
251            // initially too small but get repleted when distributing change.
252            //
253            // The builder shall never produce an invalid change output that is too small (i.e. the
254            // 'change' given to the change strategy callback is always sufficient to cover for a
255            // full new output). But users may pre-define such outputs; hence the safeguard here.
256            fail_on_insufficiently_funded_outputs(tx.outputs())?;
257
258            // Serialise the transaction to compute its fee.
259            serialized_tx.clear();
260            cbor::encode(&tx, &mut serialized_tx).unwrap();
261
262            // Re-compute execution units for all scripts, if any.
263            let (total_inline_scripts_size, uplc_resolved_inputs) =
264                into_uplc_inputs(&tx, resolved_inputs)?;
265            redeemers = evaluate_plutus_scripts(
266                &serialized_tx,
267                uplc_resolved_inputs.clone(),
268                &required_scripts,
269                params,
270            )
271            .inspect_err(|err| {
272                eprintln!("{}", err);
273                dump_tx_context(&tx.id(), &serialized_tx, &uplc_resolved_inputs);
274            })?;
275
276            // This estimation is a best-effort and assumes that one (non-script) input requires one signature
277            // witness. This means that it possibly UNDER-estimate fees for Native-script-locked inputs;
278            //
279            // We don't have a solution for it at the moment.
280            let estimated_fee = {
281                let num_signatories = tx.required_signatories(resolved_inputs)?.len() as u64;
282                let estimated_size = serialized_tx.len() as u64
283                    + SIZE_OF_KEY_WITNESSES_OVERHEAD
284                    + SIZE_OF_KEY_WITNESS * num_signatories;
285
286                params.base_fee(estimated_size)
287                    + params.referenced_scripts_fee(total_inline_scripts_size)
288                    + total_execution_cost(params, &redeemers)
289            };
290
291            attempts += 1;
292
293            // Check if we've reached a fixed point, or start over.
294            if fee >= estimated_fee {
295                break;
296            } else if attempts >= 3 {
297                return Err(anyhow!("transaction = {}", hex::encode(&serialized_tx))
298                    .context(format!("fee = {fee}, estimated_fee = {estimated_fee}"))
299                    .context(
300                        "failed to build transaction: did not converge after three attempts",
301                    ));
302            } else {
303                fee = estimated_fee;
304            }
305        }
306
307        Ok(Transaction {
308            inner: tx.inner,
309            change_strategy: (),
310            state: PhantomData,
311        })
312    }
313}
314
315// --------------------------------------------------------------------- Helpers
316
317fn total_execution_cost<'a>(
318    params: &'_ ProtocolParameters,
319    redeemers: impl IntoIterator<Item = (&'a RedeemerPointer, &'a ExecutionUnits)>,
320) -> u64 {
321    redeemers.into_iter().fold(0, |acc, (_, ex_units)| {
322        acc + params.price_mem(ex_units.mem()) + params.price_cpu(ex_units.cpu())
323    })
324}
325
326/// Resolve specified inputs and reference inputs, and convert them into a format suitable for the
327/// UPLC VM evaluation. Also returns the sum of the size of any inline scripts found in those
328/// (counting multiple times the size of repeated scripts).
329fn into_uplc_inputs<State: IsTransactionBodyState>(
330    tx: &Transaction<State>,
331    resolved_inputs: &BTreeMap<Input, Output>,
332) -> anyhow::Result<(u64, Vec<uplc::tx::ResolvedInput>)> {
333    // Ensures that only 'known' inputs contribute to the evaluation; in case the user
334    // added extra inputs to the provided UTxO which do not get correctly captured in
335    // the transaction; causing the evaluation to possibly wrongly succeed.
336    let known_inputs = tx
337        .inputs()
338        .chain(tx.reference_inputs())
339        .collect::<BTreeSet<_>>();
340
341    for input in known_inputs.iter() {
342        if resolved_inputs.get(input).is_none() {
343            return Err(anyhow!("unknown = {input}")
344                .context("unknown output for specified input or reference input; found in transaction but not in provided resolved set"));
345        }
346    }
347
348    let mut total_inline_scripts_size = 0;
349
350    let uplc_resolved_inputs = resolved_inputs
351        .iter()
352        .filter_map(|(i, o)| {
353            if known_inputs.contains(i) {
354                if let Some(script) = o.script() {
355                    total_inline_scripts_size += script.size();
356                }
357
358                Some(uplc::tx::ResolvedInput {
359                    input: pallas::TransactionInput::from((*i).clone()),
360                    output: pallas::TransactionOutput::from((*o).clone()),
361                })
362            } else {
363                None
364            }
365        })
366        .collect();
367
368    Ok((total_inline_scripts_size, uplc_resolved_inputs))
369}
370
371fn fail_on_missing_collateral<T>(
372    redeemers: &BTreeMap<RedeemerPointer, T>,
373    collaterals: impl Iterator<Item = Input>,
374) -> anyhow::Result<()> {
375    let mut ptrs = redeemers.keys();
376    if let Some(ptr) = ptrs.next()
377        && collaterals.count() == 0
378    {
379        let mut err = anyhow!("at {}", ptr);
380        for ptr in ptrs {
381            err = err.context(format!("at {ptr}"));
382        }
383
384        return Err(err.context(
385            "no collaterals set, but the transaction requires at least one phase-2 script execution",
386        ));
387    }
388
389    Ok(())
390}
391
392fn fail_on_insufficiently_funded_outputs(
393    outputs: impl Iterator<Item = Output>,
394) -> anyhow::Result<()> {
395    let mut err_opt: Option<anyhow::Error> = None;
396
397    for (ix, output) in outputs.enumerate() {
398        let allocated = output.value().lovelace();
399        let minimum_required = output.min_acceptable_value();
400        if allocated < minimum_required {
401            if let Some(err) = mem::take(&mut err_opt) {
402                err_opt = Some(err.context(format!("at output index {ix}: allocated={allocated}, minimum required={minimum_required}")));
403            } else {
404                err_opt = Some(anyhow!(
405                    "at output index {ix}: allocated={allocated}, minimum required={minimum_required}"
406                ));
407            }
408        }
409    }
410
411    if let Some(err) = err_opt {
412        return Err(
413            err.context("insufficiently provisioned output(s): not enough lovelace allocated")
414        );
415    }
416
417    Ok(())
418}
419
420fn evaluate_plutus_scripts(
421    serialized_tx: &[u8],
422    resolved_inputs: Vec<uplc::tx::ResolvedInput>,
423    required_scripts: &BTreeMap<RedeemerPointer, Hash<28>>,
424    params: &ProtocolParameters,
425) -> anyhow::Result<BTreeMap<RedeemerPointer, ExecutionUnits>> {
426    if !required_scripts.is_empty() {
427        // Convert to Pallas' MintedTx. Since there's no public access to the constructor of
428        // 'MintedTx', we have to serialize the transaction, and deserialize it back into a
429        // MintedTx directly.
430        //
431        // We need a MintedTx because that is the API expected from 'eval_phase_two'.
432        //
433        // TODO:
434        //   Either:
435        //    - Provide better constructors' on Pallas' side;
436        //    - Adjust the 'eval_phase_two' API in the uplc crate, because there's no reason to
437        //      require a MintedTx specifically.
438        let minted_tx = cbor::decode(serialized_tx).unwrap();
439
440        return Ok(uplc::tx::eval_phase_two(
441            &minted_tx,
442            resolved_inputs.as_slice(),
443            None,
444            None,
445            &SlotConfig::from(params),
446            false,
447            |_| (),
448        )
449        .map_err(|e| anyhow!("required scripts = {required_scripts:?}").context(format!("{e:?}")))?
450        .into_iter()
451        // FIXME: The second element in the resulting pair contains the evaluation result.
452        // We shall make sure that it is passing, and if it isn't, we should fail with a
453        // proper error including the evaluation traces.
454        .map(|(redeemer, _eval_result)| {
455            (
456                RedeemerPointer::from(pallas::RedeemersKey {
457                    tag: redeemer.tag,
458                    index: redeemer.index,
459                }),
460                ExecutionUnits::from(redeemer.ex_units),
461            )
462        })
463        .collect());
464    };
465
466    Ok(BTreeMap::new())
467}
468
469/// Internal helper to handle the platform-specific writing logic.
470fn write(filename: &str, content: String) {
471    #[cfg(all(not(target_family = "wasm"), debug_assertions))]
472    {
473        // On native debug, write to the filesystem.
474        _ = std::fs::write(filename, &content);
475    }
476
477    #[cfg(any(target_family = "wasm", not(debug_assertions)))]
478    {
479        // On WASM or release, log the "file" metadata followed by the content.
480        log::info!("File: {}", filename);
481        log::info!("{}", content);
482    }
483}
484
485/// Dumps transaction data for inspection.
486/// Note that this occurs only if the log level sufficiently verbose (info or debug)
487fn dump_tx_context(tx_id: &Hash<32>, serialized_tx: &[u8], inputs: &[uplc::tx::ResolvedInput]) {
488    if log::log_enabled!(log::Level::Info) {
489        let dump_base = format!("./tx-dump-{:.6}", hex::encode(tx_id));
490
491        // Dump Serialized TX
492        write(&format!("{}-tx.txt", dump_base), hex::encode(serialized_tx));
493
494        // Dump Inputs
495        let inputs_cbor = inputs
496            .iter()
497            .map(|ri| ri.input.clone())
498            .collect::<Vec<_>>()
499            .to_cbor();
500        write(
501            &format!("{}-inputs.txt", dump_base),
502            hex::encode(inputs_cbor),
503        );
504
505        // Dump Outputs
506        let outputs_cbor = inputs
507            .iter()
508            .map(|ri| ri.output.clone())
509            .collect::<Vec<_>>()
510            .to_cbor();
511        write(
512            &format!("{}-outputs.txt", dump_base),
513            hex::encode(outputs_cbor),
514        );
515
516        #[cfg(all(not(target_family = "wasm"), debug_assertions))]
517        eprintln!("Debug: Transaction context dumped to {}*", dump_base);
518    } else {
519        #[cfg(all(not(target_family = "wasm"), debug_assertions))]
520        eprintln!("Dump tx skipped. Set log::level = Info to enable");
521    }
522}
523
524// ----------------------------------------------------------------------- Tests
525
526#[cfg(test)]
527mod tests {
528    use crate::{
529        Address, ChangeStrategy, Hash, Input, Output, PlutusData, PlutusScript, PlutusVersion,
530        ProtocolParameters, SlotBound, Transaction, address, address::kind::*, address_test,
531        assets, cbor::ToCbor, hash, input, key_credential, output, plutus_data, plutus_script,
532        script_credential, value,
533    };
534    use indoc::indoc;
535    use std::{cell::LazyCell, collections::BTreeMap, sync::LazyLock};
536
537    /// Some fixture parameters, simply mimicking PreProd's parameters.
538    pub static FIXTURE_PROTOCOL_PARAMETERS: LazyLock<ProtocolParameters> =
539        LazyLock::new(ProtocolParameters::preprod);
540
541    #[allow(clippy::declare_interior_mutable_const)]
542    const ALWAYS_SUCCEED_ADDRESS: LazyCell<Address<Any>> = LazyCell::new(|| {
543        Address::from(address_test!(script_credential!(
544            "bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777"
545        )))
546    });
547
548    #[allow(clippy::declare_interior_mutable_const)]
549    const ALWAYS_SUCCEED_SCRIPT: LazyCell<PlutusScript> =
550        LazyCell::new(|| plutus_script!(PlutusVersion::V3, "5101010023259800a518a4d136564004ae69"));
551
552    #[test]
553    fn with_validity_interval() {
554        let resolved_inputs = [(
555            input!(
556                "0000000000000000000000000000000000000000000000000000000000000000",
557                0,
558            ),
559            output!(
560                "addr1vx7n46v3kk40ejh7tjnswk9ax65m97rj74lk6wsllg8twacak3e47",
561                value!(10_000_000),
562            ),
563        )];
564
565        // With a missing datum hash.
566        let result = Transaction::build(
567            &FIXTURE_PROTOCOL_PARAMETERS,
568            &BTreeMap::from(resolved_inputs.clone()),
569            |tx| {
570                tx.with_inputs(vec![input!(
571                    "0000000000000000000000000000000000000000000000000000000000000000",
572                    0,
573                    PlutusData::list::<PlutusData>([]),
574                )])
575                .with_validity_interval(
576                    SlotBound::Inclusive(14),
577                    SlotBound::Inclusive(42),
578                )
579                .with_change_strategy(ChangeStrategy::as_last_output(
580                    address!("addr1qxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytun36yuhgl049rxhhuckm2lpq3rmz5dcraddyl45d6xgvqqsp504c")
581                ))
582                .ok()
583            },
584        ).unwrap_or_else(|e| panic!("{e:?}"));
585
586        assert_eq!(
587            result.to_string(),
588            indoc! {
589                "Transaction (id = 454f5ca4f75209540851329deb6d08cb759836f5bbec0b11d3de38b4030aaf76) {
590                     inputs: [
591                         Input(0000000000000000000000000000000000000000000000000000000000000000#0),
592                     ],
593                     outputs: [
594                         Output {
595                             address: addr1qxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytun36yuhgl049rxhhuckm2lpq3rmz5dcraddyl45d6xgvqqsp504c,
596                             value: Value {
597                                 lovelace: 9832035,
598                             },
599                         },
600                     ],
601                     fee: 167965,
602                     validity: [14; 43[,
603                     script_integrity_hash: 6bdd58f9ba5f3134e0c9509063c3292110582042096294d394c9913301b2dc5e,
604                     redeemers: {
605                         Spend(0): Redeemer(
606                             CBOR(80),
607                             ExecutionUnits {
608                                 mem: 0,
609                                 cpu: 0,
610                             },
611                         ),
612                     },
613                 }"
614            }
615        );
616    }
617
618    #[test]
619    fn spend_from_datum_hash() {
620        let always_succeed_script = ALWAYS_SUCCEED_SCRIPT;
621        let always_succeed_address = ALWAYS_SUCCEED_ADDRESS;
622
623        let resolved_inputs = [
624            (
625                input!(
626                    "d62db0b98b6df96645eec19d4728b385592fc531736abd987eb6490510c5ba50",
627                    0,
628                ),
629                Output::new(always_succeed_address.clone(), value!(102049379)).with_datum_hash(
630                    hash!("747a61e363e1fbee6a0ce234320a55bae7262ed62aa7e979d5d390339be3dd18"),
631                ),
632            ),
633            (
634                input!(
635                    "0000000000000000000000000000000000000000000000000000000000000000",
636                    1,
637                ),
638                output!(
639                    "addr1vxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytckt7nh9",
640                    value!(10000000),
641                ),
642            ),
643        ];
644
645        // With a missing datum hash.
646        let result = Transaction::build(
647            &FIXTURE_PROTOCOL_PARAMETERS,
648            &BTreeMap::from(resolved_inputs.clone()),
649            |tx| {
650                tx.with_inputs(vec![input!(
651                    "d62db0b98b6df96645eec19d4728b385592fc531736abd987eb6490510c5ba50",
652                    0,
653                    PlutusData::list::<PlutusData>([]),
654                )])
655                .with_collaterals(vec![input!(
656                    "0000000000000000000000000000000000000000000000000000000000000000",
657                    1
658                )])
659                .with_change_strategy(ChangeStrategy::as_last_output(
660                    address!("addr1qxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytun36yuhgl049rxhhuckm2lpq3rmz5dcraddyl45d6xgvqqsp504c")
661                ))
662                .with_plutus_scripts(vec![always_succeed_script.clone()])
663                .ok()
664            },
665        );
666
667        let debug = format!("{result:#?}");
668
669        assert!(
670            result.is_err_and(|e| e.to_string().contains("MissingRequiredDatum")),
671            "{debug}",
672        );
673
674        // Providing the datum hash
675        let result = Transaction::build(
676            &FIXTURE_PROTOCOL_PARAMETERS,
677            &BTreeMap::from(resolved_inputs.clone()),
678            |tx| {
679                tx.with_inputs(vec![input!(
680                    "d62db0b98b6df96645eec19d4728b385592fc531736abd987eb6490510c5ba50",
681                    0,
682                    PlutusData::list::<PlutusData>([]),
683                )])
684                .with_collaterals(vec![input!(
685                    "0000000000000000000000000000000000000000000000000000000000000000",
686                    1
687                )])
688                .with_change_strategy(ChangeStrategy::as_last_output(
689                    address!("addr1qxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytun36yuhgl049rxhhuckm2lpq3rmz5dcraddyl45d6xgvqqsp504c")
690                ))
691                .with_plutus_scripts(vec![always_succeed_script.clone()])
692                .with_datums(vec![
693                    plutus_data!(
694                        "d8799fd8799fd87a9f581c1eae96baf29e27682ea3f815aba361a0c\
695                         6059d45e4bfbe95bbd2f44affffd8799f4040ffd8799f581cf66d78\
696                         b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b6988044694\
697                         55448ff1a9deac9cb1b00000033accac2401a02311bec18641864d8\
698                         799f190e52ffd87980ff"
699                    )
700                ])
701                .ok()
702            },
703        );
704
705        assert_eq!(
706            result
707                .map(|tx| tx.to_string())
708                .unwrap_or_else(|e| e.to_string()),
709            indoc! {"
710                Transaction (id = 3bd44ee7393607ab23ac97bc0928cce42edf7316195d301308fc346877d8a55d) {
711                    inputs: [
712                        Input(d62db0b98b6df96645eec19d4728b385592fc531736abd987eb6490510c5ba50#0),
713                    ],
714                    outputs: [
715                        Output {
716                            address: addr1qxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytun36yuhgl049rxhhuckm2lpq3rmz5dcraddyl45d6xgvqqsp504c,
717                            value: Value {
718                                lovelace: 101870870,
719                            },
720                        },
721                    ],
722                    fee: 178509,
723                    validity: ]-∞; +∞[,
724                    script_integrity_hash: 3b2ff5d0ea6d2fa720d12f01d71e015306d77524c750df84b2106bbe0919a4e2,
725                    collaterals: [
726                        Input(0000000000000000000000000000000000000000000000000000000000000000#1),
727                    ],
728                    collateral_return: Output {
729                        address: addr1vxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytckt7nh9,
730                        value: Value {
731                            lovelace: 9732236,
732                        },
733                    },
734                    total_collateral: 267764,
735                    scripts: [
736                        v3(bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777),
737                    ],
738                    datums: [
739                        CBOR(d8799fd8799fd87a9f581c1eae96baf29e27682ea3f815aba361a0c6059d45e4bfbe95bbd2f44affffd8799f4040ffd8799f581cf66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b698804469455448ff1a9deac9cb1b00000033accac2401a02311bec18641864d8799f190e52ffd87980ff),
740                    ],
741                    redeemers: {
742                        Spend(0): Redeemer(
743                            CBOR(80),
744                            ExecutionUnits {
745                                mem: 1601,
746                                cpu: 316149,
747                            },
748                        ),
749                    },
750                }"
751            },
752        );
753    }
754
755    #[test]
756    fn min_lovelace_value_with_nft() {
757        let resolved_inputs = [(
758            input!(
759                "d62db0b98b6df96645eec19d4728b385592fc531736abd987eb6490510c5ba50",
760                0
761            ),
762            output!(
763                "addr1qxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytun36yuhgl049rxhhuckm2lpq3rmz5dcraddyl45d6xgvqqsp504c",
764                value!(
765                    102049379,
766                    (
767                        "279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f",
768                        "534e454b",
769                        1
770                    ),
771                )
772            ),
773        )];
774
775        let result = Transaction::build(
776            &FIXTURE_PROTOCOL_PARAMETERS,
777            &BTreeMap::from(resolved_inputs.clone()),
778            |tx| {
779                tx.with_inputs(vec![input!(
780                    "d62db0b98b6df96645eec19d4728b385592fc531736abd987eb6490510c5ba50",
781                    0,
782                    _
783                )])
784                .with_outputs(vec![
785                    output!("addr1qxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytun36yuhgl049rxhhuckm2lpq3rmz5dcraddyl45d6xgvqqsp504c").with_assets(
786                        assets!(
787                            (
788                                "279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f",
789                                "534e454b",
790                                1,
791                            )
792                        ),
793                    ),
794                ])
795                .with_change_strategy(ChangeStrategy::as_last_output(
796                    address!("addr1qxu84ftxpzh3zd8p9awp2ytwzk5exj0fxcj7paur4kd4ytun36yuhgl049rxhhuckm2lpq3rmz5dcraddyl45d6xgvqqsp504c")
797                ))
798                .ok()
799            },
800        );
801
802        assert!(result.is_ok(), "{result:#?}");
803    }
804
805    #[test]
806    fn full_lifecycle() {
807        let always_succeed_script = ALWAYS_SUCCEED_SCRIPT;
808        let always_succeed_address = ALWAYS_SUCCEED_ADDRESS;
809
810        let my_address: Address<Any> = address_test!(
811            "addr_test1qzpvzu5atl2yzf9x4eetekuxkm5z02kx5apsreqq8syjum6274ase8lkeffp39narear74ed0nf804e5drfm9l99v4eq3ecz8t"
812        );
813
814        let mut resolved_inputs = BTreeMap::from([(
815            input!(
816                "c984c8bf52a141254c714c905b2d27b432d4b546f815fbc2fea7b9da6e490324",
817                3
818            ),
819            Output::new(my_address.clone(), value!(11875000)),
820        )]);
821
822        // https://preprod.cardanoscan.io/transaction/036fd8d808d4a87737cbb0ed1e61b08ce753323e94fc118c5eefabee6a8e04a5
823        let deploy_script =
824            Transaction::build(&FIXTURE_PROTOCOL_PARAMETERS, &resolved_inputs, |tx| {
825                tx.with_inputs(vec![input!(
826                    "c984c8bf52a141254c714c905b2d27b432d4b546f815fbc2fea7b9da6e490324",
827                    3,
828                    _
829                )])
830                .with_outputs(vec![
831                    Output::to(my_address.clone())
832                        .with_plutus_script(always_succeed_script.clone()),
833                ])
834                .with_change_strategy(ChangeStrategy::as_last_output(my_address.clone()))
835                .ok()
836            })
837            .unwrap();
838
839        assert_eq!(
840            hex::encode(deploy_script.to_cbor()),
841            "84a300d9010281825820c984c8bf52a141254c714c905b2d27b432d4b546f815fbc\
842             2fea7b9da6e490324030182a30058390082c1729d5fd44124a6ae72bcdb86b6e827\
843             aac6a74301e4003c092e6f4af57b0c9ff6ca5218967d1e7a3f572d7cd277d73468d\
844             3b2fca56572011a001092a803d818558203525101010023259800a518a4d1365640\
845             04ae69a20058390082c1729d5fd44124a6ae72bcdb86b6e827aac6a74301e4003c0\
846             92e6f4af57b0c9ff6ca5218967d1e7a3f572d7cd277d73468d3b2fca56572011a00\
847             a208bb021a00029755a0f5f6",
848            "deploy_script no longer matches expected bytes."
849        );
850
851        resolved_inputs.append(&mut deploy_script.as_resolved_inputs());
852
853        // https://preprod.cardanoscan.io/transaction/8d56891b4638203175c488e19d630bfbc8af285353aeeb1053d54a3c371b7a40
854        let pay_to_script =
855            Transaction::build(&FIXTURE_PROTOCOL_PARAMETERS, &resolved_inputs, |tx| {
856                tx.with_inputs(vec![(Input::new(deploy_script.id(), 1), None)])
857                    .with_outputs(vec![Output::new(
858                        always_succeed_address.clone(),
859                        value!(5_000_000),
860                    )])
861                    .with_change_strategy(ChangeStrategy::as_last_output(my_address.clone()))
862                    .ok()
863            })
864            .unwrap();
865
866        assert_eq!(
867            hex::encode(pay_to_script.to_cbor()),
868            "84a300d9010281825820036fd8d808d4a87737cbb0ed1e61b08ce753323e94fc118\
869             c5eefabee6a8e04a5010182a200581d70bd3ae991b5aafccafe5ca70758bd36a9b2\
870             f872f57f6d3a1ffa0eb777011a004c4b40a20058390082c1729d5fd44124a6ae72b\
871             cdb86b6e827aac6a74301e4003c092e6f4af57b0c9ff6ca5218967d1e7a3f572d7c\
872             d277d73468d3b2fca56572011a00532f42021a00028e39a0f5f6",
873            "pay_to_script no longer matches expected bytes."
874        );
875
876        resolved_inputs.append(&mut pay_to_script.as_resolved_inputs());
877
878        // https://preprod.cardanoscan.io/transaction/3522a630e91e631f56897be2898e059478c300f4bb8dd7891549a191b4bf1090
879        let spend_from_script =
880            Transaction::build(&FIXTURE_PROTOCOL_PARAMETERS, &resolved_inputs, |tx| {
881                tx.with_inputs(vec![(
882                    Input::new(pay_to_script.id(), 0),
883                    Some(PlutusData::list::<PlutusData>([])),
884                )])
885                .with_reference_inputs(vec![(Input::new(deploy_script.id(), 0))])
886                .with_collaterals(vec![Input::new(pay_to_script.id(), 1)])
887                .with_change_strategy(ChangeStrategy::as_last_output(
888                    always_succeed_address.clone(),
889                ))
890                .ok()
891            })
892            .unwrap();
893
894        assert_eq!(
895            hex::encode(spend_from_script.to_cbor()),
896            "84a800d90102818258208d56891b4638203175c488e19d630bfbc8af285353aeeb1\
897             053d54a3c371b7a40000181a200581d70bd3ae991b5aafccafe5ca70758bd36a9b2\
898             f872f57f6d3a1ffa0eb777011a0049a375021a0002a7cb0b5820d545623b07e425a\
899             55262585d2b5e8aaee16230fd1434e790fa4511da4bf8a4550dd90102818258208d\
900             56891b4638203175c488e19d630bfbc8af285353aeeb1053d54a3c371b7a400110a\
901             20058390082c1729d5fd44124a6ae72bcdb86b6e827aac6a74301e4003c092e6f4a\
902             f57b0c9ff6ca5218967d1e7a3f572d7cd277d73468d3b2fca56572011a004f33911\
903             11a0003fbb112d9010281825820036fd8d808d4a87737cbb0ed1e61b08ce753323e\
904             94fc118c5eefabee6a8e04a500a105a18200008280821906411a0004d2f5f5f6",
905            "spend_from_script no longer matches expected bytes."
906        );
907
908        resolved_inputs.append(&mut spend_from_script.as_resolved_inputs());
909
910        // https://preprod.cardanoscan.io/transaction/cd8c5bf00ab490d57c82ebf6364e4a6337dc214d635e8c392deaa7e4b98ed6ea
911        let unpublish_script =
912            Transaction::build(&FIXTURE_PROTOCOL_PARAMETERS, &resolved_inputs, |tx| {
913                tx.with_inputs(vec![
914                    (
915                        Input::new(spend_from_script.id(), 0),
916                        Some(PlutusData::list::<PlutusData>([])),
917                    ),
918                    (Input::new(deploy_script.id(), 0), None),
919                    (Input::new(pay_to_script.id(), 1), None),
920                ])
921                .with_collaterals(vec![Input::new(pay_to_script.id(), 1)])
922                .with_change_strategy(ChangeStrategy::as_last_output(my_address.clone()))
923                .ok()
924            })
925            .unwrap();
926
927        assert_eq!(
928            hex::encode(unpublish_script.to_cbor()),
929            "84a700d9010283825820036fd8d808d4a87737cbb0ed1e61b08ce753323e94fc118\
930             c5eefabee6a8e04a5008258203522a630e91e631f56897be2898e059478c300f4bb\
931             8dd7891549a191b4bf1090008258208d56891b4638203175c488e19d630bfbc8af2\
932             85353aeeb1053d54a3c371b7a40010181a20058390082c1729d5fd44124a6ae72bc\
933             db86b6e827aac6a74301e4003c092e6f4af57b0c9ff6ca5218967d1e7a3f572d7cd\
934             277d73468d3b2fca56572011a00aab370021a0002b1ef0b5820d37acc9c984616d9\
935             d15825afeaf7d266e5bde38fdd4df4f8b2312703022d474d0dd90102818258208d5\
936             6891b4638203175c488e19d630bfbc8af285353aeeb1053d54a3c371b7a400110a2\
937             0058390082c1729d5fd44124a6ae72bcdb86b6e827aac6a74301e4003c092e6f4af\
938             57b0c9ff6ca5218967d1e7a3f572d7cd277d73468d3b2fca56572011a004f245b11\
939             1a00040ae7a105a18200018280821906411a0004d2f5f5f6",
940            "unpublish_script no longer matches expected bytes."
941        );
942    }
943
944    /// This test confirms that when a script fails and log leve is info, then the `dump_tx_context`
945    /// function is triggered.
946    ///
947    /// Since the function has side effects, the test is ignored by default.
948    ///
949    /// "Why not just tidy up after?" Because having the test available
950    /// to generate example output is also helpful.
951    ///
952    /// **Usage**: This test is ignored by default as it writes files to the disk.
953    /// Run with `cargo test test_tx_dump -- --ignored --nocapture`.
954    ///
955    /// Then:
956    ///
957    /// ```bash
958    ///     aiken tx simulate \
959    ///         tx-dump-097acb-tx.txt \
960    ///         tx-dump-097acb-inputs.txt \
961    ///         tx-dump-097acb-outputs.txt \
962    ///         --zero-time 1666656000000 \
963    ///         --zero-slot 0 \
964    ///         --script-override
965    ///         "22c9a103ed3f2fa97c982d76d6e2af50c5d54ac306983b196c8fcdab:<your-script>" \
966    ///         --blueprint ../../../kernel/plutus.json
967    /// ```
968    ///
969    /// Unless your override is always succeed or similar, this will probably fail.
970    /// But instructively.
971    #[test]
972    #[ignore]
973    fn test_tx_dump() {
974        let _ = env_logger::builder()
975            .filter_level(log::LevelFilter::Info)
976            .is_test(true)
977            .try_init();
978
979        let crash_script = plutus_script!(PlutusVersion::V3, "5001010023259800b452689b2b20025735");
980        let crash_address = Address::from(address_test!(script_credential!(
981            "22c9a103ed3f2fa97c982d76d6e2af50c5d54ac306983b196c8fcdab"
982        )));
983        let script_utxo = (
984            Input::new(Hash::<32>::new([0; 32]), 0),
985            Output::new(crash_address, value!(5_000_000))
986                .with_datum(PlutusData::list::<PlutusData>([])),
987        );
988        let user_address = Address::from(address_test!(key_credential!(
989            "00000000000000000000000000000000000000000000000000000000"
990        )));
991        let user_utxo = (
992            Input::new(Hash::<32>::new([1; 32]), 0),
993            Output::new(user_address.clone(), value!(5_000_000)),
994        );
995        let ref_utxo = (
996            Input::new(Hash::<32>::new([2; 32]), 0),
997            Output::new(user_address.clone(), value!(20_000_000)).with_plutus_script(crash_script),
998        );
999
1000        let resolved_inputs = [script_utxo.clone(), user_utxo.clone(), ref_utxo.clone()];
1001
1002        let result = Transaction::build(
1003            &FIXTURE_PROTOCOL_PARAMETERS,
1004            &BTreeMap::from(resolved_inputs.clone()),
1005            |tx| {
1006                tx.with_inputs(vec![
1007                    (
1008                        script_utxo.0.clone(),
1009                        Some(PlutusData::list::<PlutusData>([])),
1010                    ),
1011                    (user_utxo.0.clone(), None),
1012                ])
1013                .with_collaterals(vec![user_utxo.0.clone()])
1014                .with_reference_inputs(vec![ref_utxo.0.clone()])
1015                .with_outputs(vec![])
1016                .with_change_strategy(ChangeStrategy::as_last_output(user_address.clone()))
1017                .ok()
1018            },
1019        );
1020
1021        assert!(
1022            result.is_err(),
1023            "Transaction should have failed due to 'crash' script"
1024        );
1025        log::info!(
1026            "Test finished. Check your local directory (Native) or Console (WASM) for hex dumps."
1027        );
1028    }
1029}