1use 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
20const SIZE_OF_KEY_WITNESS: u64 = 1 + (32 + 2) + (64 + 2); const SIZE_OF_KEY_WITNESSES_OVERHEAD: u64 = 2 * (1 + 3 + 3);
38
39impl Transaction<state::InConstruction> {
40 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 tx.with_change(resolved_inputs)?;
234
235 tx.with_collateral_return(resolved_inputs, params)?;
237
238 tx.with_execution_units(&mut redeemers)?;
240
241 tx.with_script_integrity_hash(&required_scripts, params)?;
243
244 fail_on_missing_collateral(&required_scripts, tx.collaterals())?;
248
249 fail_on_insufficiently_funded_outputs(tx.outputs())?;
257
258 serialized_tx.clear();
260 cbor::encode(&tx, &mut serialized_tx).unwrap();
261
262 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 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 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
315fn 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
326fn into_uplc_inputs<State: IsTransactionBodyState>(
330 tx: &Transaction<State>,
331 resolved_inputs: &BTreeMap<Input, Output>,
332) -> anyhow::Result<(u64, Vec<uplc::tx::ResolvedInput>)> {
333 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 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 .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
469fn write(filename: &str, content: String) {
471 #[cfg(all(not(target_family = "wasm"), debug_assertions))]
472 {
473 _ = std::fs::write(filename, &content);
475 }
476
477 #[cfg(any(target_family = "wasm", not(debug_assertions)))]
478 {
479 log::info!("File: {}", filename);
481 log::info!("{}", content);
482 }
483}
484
485fn 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 write(&format!("{}-tx.txt", dump_base), hex::encode(serialized_tx));
493
494 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 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#[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 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 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 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 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 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 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 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 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 #[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}