1use 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
25pub struct Transaction<State: IsTransactionBodyState> {
38 inner: pallas::Tx,
39 change_strategy: State::ChangeStrategy,
40 state: PhantomData<State>,
41}
42
43impl 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 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
281impl 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 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 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
332impl<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 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 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 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 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 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 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 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
730impl<State: IsTransactionBodyState> Transaction<State> {
733 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 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 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(); 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 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 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 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 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 change.add(&Value::default().with_assets(mint));
1084
1085 change
1087 .checked_sub(&Value::default().with_assets(burn))
1088 .map_err(|e| e.context("insufficient balance; spending more than available"))?;
1089
1090 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 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 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 Output::new(return_address, total_collateral_value),
1171 ));
1172 }
1173
1174 Ok(())
1175 }
1176}
1177
1178fn 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
1207impl<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#[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}