1use std::{collections::HashMap, fmt, iter, sync::Arc};
4
5use halo2::pasta::pallas;
6
7mod auth_digest;
8mod hash;
9mod joinsplit;
10mod lock_time;
11mod memo;
12mod serialize;
13mod sighash;
14mod txid;
15mod unmined;
16
17pub mod builder;
18
19#[cfg(any(test, feature = "proptest-impl"))]
20#[allow(clippy::unwrap_in_result)]
21pub mod arbitrary;
22#[cfg(test)]
23mod tests;
24
25pub use auth_digest::AuthDigest;
26pub use hash::{Hash, WtxId};
27pub use joinsplit::JoinSplitData;
28pub use lock_time::LockTime;
29pub use memo::Memo;
30use redjubjub::{Binding, Signature};
31pub use sapling::FieldNotPresent;
32pub use serialize::{
33 SerializedTransaction, MIN_TRANSPARENT_TX_SIZE, MIN_TRANSPARENT_TX_V4_SIZE,
34 MIN_TRANSPARENT_TX_V5_SIZE,
35};
36pub use sighash::{HashType, SigHash, SigHasher};
37pub use unmined::{
38 zip317, UnminedTx, UnminedTxId, VerifiedUnminedTx, MEMPOOL_TRANSACTION_COST_THRESHOLD,
39};
40use zcash_protocol::consensus;
41
42#[cfg(feature = "tx_v6")]
43use crate::parameters::TX_V6_VERSION_GROUP_ID;
44use crate::{
45 amount::{Amount, Error as AmountError, NegativeAllowed, NonNegative},
46 block, orchard,
47 parameters::{
48 Network, NetworkUpgrade, OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID,
49 TX_V5_VERSION_GROUP_ID,
50 },
51 primitives::{ed25519, Bctv14Proof, Groth16Proof},
52 sapling,
53 serialization::ZcashSerialize,
54 sprout,
55 transparent::{
56 self, outputs_from_utxos,
57 CoinbaseSpendRestriction::{self, *},
58 },
59 value_balance::{ValueBalance, ValueBalanceError},
60 Error,
61};
62
63#[derive(Clone, Debug, PartialEq, Eq)]
75#[cfg_attr(
76 any(test, feature = "proptest-impl", feature = "elasticsearch"),
77 derive(Serialize)
78)]
79pub enum Transaction {
80 V1 {
82 inputs: Vec<transparent::Input>,
84 outputs: Vec<transparent::Output>,
86 lock_time: LockTime,
89 },
90 V2 {
92 inputs: Vec<transparent::Input>,
94 outputs: Vec<transparent::Output>,
96 lock_time: LockTime,
99 joinsplit_data: Option<JoinSplitData<Bctv14Proof>>,
101 },
102 V3 {
104 inputs: Vec<transparent::Input>,
106 outputs: Vec<transparent::Output>,
108 lock_time: LockTime,
111 expiry_height: block::Height,
113 joinsplit_data: Option<JoinSplitData<Bctv14Proof>>,
115 },
116 V4 {
118 inputs: Vec<transparent::Input>,
120 outputs: Vec<transparent::Output>,
122 lock_time: LockTime,
125 expiry_height: block::Height,
127 joinsplit_data: Option<JoinSplitData<Groth16Proof>>,
129 sapling_shielded_data: Option<sapling::ShieldedData<sapling::PerSpendAnchor>>,
131 },
132 V5 {
134 network_upgrade: NetworkUpgrade,
138 lock_time: LockTime,
141 expiry_height: block::Height,
143 inputs: Vec<transparent::Input>,
145 outputs: Vec<transparent::Output>,
147 sapling_shielded_data: Option<sapling::ShieldedData<sapling::SharedAnchor>>,
149 orchard_shielded_data: Option<orchard::ShieldedData>,
151 },
152 #[cfg(feature = "tx_v6")]
154 V6 {
155 network_upgrade: NetworkUpgrade,
159 lock_time: LockTime,
162 expiry_height: block::Height,
164 inputs: Vec<transparent::Input>,
166 outputs: Vec<transparent::Output>,
168 sapling_shielded_data: Option<sapling::ShieldedData<sapling::SharedAnchor>>,
170 orchard_shielded_data: Option<orchard::ShieldedData>,
172 },
174}
175
176impl fmt::Display for Transaction {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 let mut fmter = f.debug_struct("Transaction");
179
180 fmter.field("version", &self.version());
181
182 if let Some(network_upgrade) = self.network_upgrade() {
183 fmter.field("network_upgrade", &network_upgrade);
184 }
185
186 if let Some(lock_time) = self.lock_time() {
187 fmter.field("lock_time", &lock_time);
188 }
189
190 if let Some(expiry_height) = self.expiry_height() {
191 fmter.field("expiry_height", &expiry_height);
192 }
193
194 fmter.field("transparent_inputs", &self.inputs().len());
195 fmter.field("transparent_outputs", &self.outputs().len());
196 fmter.field("sprout_joinsplits", &self.joinsplit_count());
197 fmter.field("sapling_spends", &self.sapling_spends_per_anchor().count());
198 fmter.field("sapling_outputs", &self.sapling_outputs().count());
199 fmter.field("orchard_actions", &self.orchard_actions().count());
200
201 fmter.field("unmined_id", &self.unmined_id());
202
203 fmter.finish()
204 }
205}
206
207impl Transaction {
208 pub fn hash(&self) -> Hash {
215 Hash::from(self)
216 }
217
218 pub fn unmined_id(&self) -> UnminedTxId {
223 UnminedTxId::from(self)
224 }
225
226 pub fn sighash(
252 &self,
253 nu: NetworkUpgrade,
254 hash_type: sighash::HashType,
255 all_previous_outputs: Arc<Vec<transparent::Output>>,
256 input_index_script_code: Option<(usize, Vec<u8>)>,
257 ) -> Result<SigHash, Error> {
258 Ok(sighash::SigHasher::new(self, nu, all_previous_outputs)?
259 .sighash(hash_type, input_index_script_code))
260 }
261
262 pub fn sighasher(
264 &self,
265 nu: NetworkUpgrade,
266 all_previous_outputs: Arc<Vec<transparent::Output>>,
267 ) -> Result<sighash::SigHasher, Error> {
268 sighash::SigHasher::new(self, nu, all_previous_outputs)
269 }
270
271 pub fn auth_digest(&self) -> Option<AuthDigest> {
278 match self {
279 Transaction::V1 { .. }
280 | Transaction::V2 { .. }
281 | Transaction::V3 { .. }
282 | Transaction::V4 { .. } => None,
283 Transaction::V5 { .. } => Some(AuthDigest::from(self)),
284 #[cfg(feature = "tx_v6")]
285 Transaction::V6 { .. } => Some(AuthDigest::from(self)),
286 }
287 }
288
289 pub fn has_transparent_inputs(&self) -> bool {
293 !self.inputs().is_empty()
294 }
295
296 pub fn has_transparent_outputs(&self) -> bool {
298 !self.outputs().is_empty()
299 }
300
301 pub fn has_transparent_inputs_or_outputs(&self) -> bool {
303 self.has_transparent_inputs() || self.has_transparent_outputs()
304 }
305
306 pub fn has_transparent_or_shielded_inputs(&self) -> bool {
308 self.has_transparent_inputs() || self.has_shielded_inputs()
309 }
310
311 pub fn has_shielded_inputs(&self) -> bool {
315 self.joinsplit_count() > 0
316 || self.sapling_spends_per_anchor().count() > 0
317 || (self.orchard_actions().count() > 0
318 && self
319 .orchard_flags()
320 .unwrap_or_else(orchard::Flags::empty)
321 .contains(orchard::Flags::ENABLE_SPENDS))
322 }
323
324 pub fn has_shielded_outputs(&self) -> bool {
328 self.joinsplit_count() > 0
329 || self.sapling_outputs().count() > 0
330 || (self.orchard_actions().count() > 0
331 && self
332 .orchard_flags()
333 .unwrap_or_else(orchard::Flags::empty)
334 .contains(orchard::Flags::ENABLE_OUTPUTS))
335 }
336
337 pub fn has_transparent_or_shielded_outputs(&self) -> bool {
339 self.has_transparent_outputs() || self.has_shielded_outputs()
340 }
341
342 pub fn has_enough_orchard_flags(&self) -> bool {
344 if self.version() < 5 || self.orchard_actions().count() == 0 {
345 return true;
346 }
347 self.orchard_flags()
348 .unwrap_or_else(orchard::Flags::empty)
349 .intersects(orchard::Flags::ENABLE_SPENDS | orchard::Flags::ENABLE_OUTPUTS)
350 }
351
352 pub fn coinbase_spend_restriction(
355 &self,
356 network: &Network,
357 spend_height: block::Height,
358 ) -> CoinbaseSpendRestriction {
359 if self.outputs().is_empty() || network.should_allow_unshielded_coinbase_spends() {
360 CheckCoinbaseMaturity { spend_height }
363 } else {
364 DisallowCoinbaseSpend
365 }
366 }
367
368 pub fn is_overwintered(&self) -> bool {
372 match self {
373 Transaction::V1 { .. } | Transaction::V2 { .. } => false,
374 Transaction::V3 { .. } | Transaction::V4 { .. } | Transaction::V5 { .. } => true,
375 #[cfg(feature = "tx_v6")]
376 Transaction::V6 { .. } => true,
377 }
378 }
379
380 pub fn version(&self) -> u32 {
392 match self {
393 Transaction::V1 { .. } => 1,
394 Transaction::V2 { .. } => 2,
395 Transaction::V3 { .. } => 3,
396 Transaction::V4 { .. } => 4,
397 Transaction::V5 { .. } => 5,
398 #[cfg(feature = "tx_v6")]
399 Transaction::V6 { .. } => 6,
400 }
401 }
402
403 pub fn lock_time(&self) -> Option<LockTime> {
405 let lock_time = match self {
406 Transaction::V1 { lock_time, .. }
407 | Transaction::V2 { lock_time, .. }
408 | Transaction::V3 { lock_time, .. }
409 | Transaction::V4 { lock_time, .. }
410 | Transaction::V5 { lock_time, .. } => *lock_time,
411 #[cfg(feature = "tx_v6")]
412 Transaction::V6 { lock_time, .. } => *lock_time,
413 };
414
415 if lock_time == LockTime::unlocked() {
422 return None;
423 }
424
425 let has_sequence_number_enabling_lock_time = self
440 .inputs()
441 .iter()
442 .map(transparent::Input::sequence)
443 .any(|sequence_number| sequence_number != u32::MAX);
444
445 if has_sequence_number_enabling_lock_time {
446 Some(lock_time)
447 } else {
448 None
449 }
450 }
451
452 pub fn raw_lock_time(&self) -> u32 {
454 let lock_time = match self {
455 Transaction::V1 { lock_time, .. }
456 | Transaction::V2 { lock_time, .. }
457 | Transaction::V3 { lock_time, .. }
458 | Transaction::V4 { lock_time, .. }
459 | Transaction::V5 { lock_time, .. } => *lock_time,
460 #[cfg(feature = "tx_v6")]
461 Transaction::V6 { lock_time, .. } => *lock_time,
462 };
463 let mut lock_time_bytes = Vec::new();
464 lock_time
465 .zcash_serialize(&mut lock_time_bytes)
466 .expect("lock_time should serialize");
467 u32::from_le_bytes(
468 lock_time_bytes
469 .try_into()
470 .expect("should serialize as 4 bytes"),
471 )
472 }
473
474 pub fn lock_time_is_time(&self) -> bool {
478 if let Some(lock_time) = self.lock_time() {
479 return lock_time.is_time();
480 }
481
482 false
483 }
484
485 pub fn expiry_height(&self) -> Option<block::Height> {
487 match self {
488 Transaction::V1 { .. } | Transaction::V2 { .. } => None,
489 Transaction::V3 { expiry_height, .. }
490 | Transaction::V4 { expiry_height, .. }
491 | Transaction::V5 { expiry_height, .. } => match expiry_height {
492 block::Height(0) => None,
496 block::Height(expiry_height) => Some(block::Height(*expiry_height)),
497 },
498 #[cfg(feature = "tx_v6")]
499 Transaction::V6 { expiry_height, .. } => match expiry_height {
500 block::Height(0) => None,
505 block::Height(expiry_height) => Some(block::Height(*expiry_height)),
506 },
507 }
508 }
509
510 pub fn network_upgrade(&self) -> Option<NetworkUpgrade> {
515 match self {
516 Transaction::V1 { .. }
517 | Transaction::V2 { .. }
518 | Transaction::V3 { .. }
519 | Transaction::V4 { .. } => None,
520 Transaction::V5 {
521 network_upgrade, ..
522 } => Some(*network_upgrade),
523 #[cfg(feature = "tx_v6")]
524 Transaction::V6 {
525 network_upgrade, ..
526 } => Some(*network_upgrade),
527 }
528 }
529
530 pub fn inputs(&self) -> &[transparent::Input] {
534 match self {
535 Transaction::V1 { ref inputs, .. } => inputs,
536 Transaction::V2 { ref inputs, .. } => inputs,
537 Transaction::V3 { ref inputs, .. } => inputs,
538 Transaction::V4 { ref inputs, .. } => inputs,
539 Transaction::V5 { ref inputs, .. } => inputs,
540 #[cfg(feature = "tx_v6")]
541 Transaction::V6 { ref inputs, .. } => inputs,
542 }
543 }
544
545 pub fn spent_outpoints(&self) -> impl Iterator<Item = transparent::OutPoint> + '_ {
547 self.inputs()
548 .iter()
549 .filter_map(transparent::Input::outpoint)
550 }
551
552 pub fn outputs(&self) -> &[transparent::Output] {
554 match self {
555 Transaction::V1 { ref outputs, .. } => outputs,
556 Transaction::V2 { ref outputs, .. } => outputs,
557 Transaction::V3 { ref outputs, .. } => outputs,
558 Transaction::V4 { ref outputs, .. } => outputs,
559 Transaction::V5 { ref outputs, .. } => outputs,
560 #[cfg(feature = "tx_v6")]
561 Transaction::V6 { ref outputs, .. } => outputs,
562 }
563 }
564
565 pub fn is_coinbase(&self) -> bool {
569 self.inputs().len() == 1
570 && matches!(
571 self.inputs().first(),
572 Some(transparent::Input::Coinbase { .. })
573 )
574 }
575
576 pub fn is_valid_non_coinbase(&self) -> bool {
583 self.inputs()
584 .iter()
585 .all(|input| matches!(input, transparent::Input::PrevOut { .. }))
586 }
587
588 pub fn sprout_groth16_joinsplits(
592 &self,
593 ) -> Box<dyn Iterator<Item = &sprout::JoinSplit<Groth16Proof>> + '_> {
594 match self {
595 Transaction::V4 {
597 joinsplit_data: Some(joinsplit_data),
598 ..
599 } => Box::new(joinsplit_data.joinsplits()),
600
601 Transaction::V1 { .. }
603 | Transaction::V2 { .. }
604 | Transaction::V3 { .. }
605 | Transaction::V4 {
606 joinsplit_data: None,
607 ..
608 }
609 | Transaction::V5 { .. } => Box::new(std::iter::empty()),
610 #[cfg(feature = "tx_v6")]
611 Transaction::V6 { .. } => Box::new(std::iter::empty()),
612 }
613 }
614
615 pub fn sprout_joinsplits(&self) -> Box<dyn Iterator<Item = sprout::GenericJoinSplit> + '_> {
617 match self {
618 Transaction::V2 {
620 joinsplit_data: Some(joinsplit_data),
621 ..
622 }
623 | Transaction::V3 {
624 joinsplit_data: Some(joinsplit_data),
625 ..
626 } => Box::new(joinsplit_data.joinsplits().map(|js| js.clone().into())),
627 Transaction::V4 {
629 joinsplit_data: Some(joinsplit_data),
630 ..
631 } => Box::new(joinsplit_data.joinsplits().map(|js| js.clone().into())),
632 Transaction::V1 { .. }
634 | Transaction::V2 {
635 joinsplit_data: None,
636 ..
637 }
638 | Transaction::V3 {
639 joinsplit_data: None,
640 ..
641 }
642 | Transaction::V4 {
643 joinsplit_data: None,
644 ..
645 }
646 | Transaction::V5 { .. } => Box::new(std::iter::empty()),
647 #[cfg(feature = "tx_v6")]
648 Transaction::V6 { .. } => Box::new(std::iter::empty()),
649 }
650 }
651
652 pub fn joinsplit_count(&self) -> usize {
654 match self {
655 Transaction::V2 {
657 joinsplit_data: Some(joinsplit_data),
658 ..
659 }
660 | Transaction::V3 {
661 joinsplit_data: Some(joinsplit_data),
662 ..
663 } => joinsplit_data.joinsplits().count(),
664 Transaction::V4 {
666 joinsplit_data: Some(joinsplit_data),
667 ..
668 } => joinsplit_data.joinsplits().count(),
669 Transaction::V1 { .. }
671 | Transaction::V2 {
672 joinsplit_data: None,
673 ..
674 }
675 | Transaction::V3 {
676 joinsplit_data: None,
677 ..
678 }
679 | Transaction::V4 {
680 joinsplit_data: None,
681 ..
682 }
683 | Transaction::V5 { .. } => 0,
684 #[cfg(feature = "tx_v6")]
685 Transaction::V6 { .. } => 0,
686 }
687 }
688
689 pub fn sprout_nullifiers(&self) -> Box<dyn Iterator<Item = &sprout::Nullifier> + '_> {
691 match self {
696 Transaction::V2 {
698 joinsplit_data: Some(joinsplit_data),
699 ..
700 }
701 | Transaction::V3 {
702 joinsplit_data: Some(joinsplit_data),
703 ..
704 } => Box::new(joinsplit_data.nullifiers()),
705 Transaction::V4 {
707 joinsplit_data: Some(joinsplit_data),
708 ..
709 } => Box::new(joinsplit_data.nullifiers()),
710 Transaction::V1 { .. }
712 | Transaction::V2 {
713 joinsplit_data: None,
714 ..
715 }
716 | Transaction::V3 {
717 joinsplit_data: None,
718 ..
719 }
720 | Transaction::V4 {
721 joinsplit_data: None,
722 ..
723 }
724 | Transaction::V5 { .. } => Box::new(std::iter::empty()),
725 #[cfg(feature = "tx_v6")]
726 Transaction::V6 { .. } => Box::new(std::iter::empty()),
727 }
728 }
729
730 pub fn sprout_joinsplit_pub_key(&self) -> Option<ed25519::VerificationKeyBytes> {
733 match self {
734 Transaction::V2 {
736 joinsplit_data: Some(joinsplit_data),
737 ..
738 }
739 | Transaction::V3 {
740 joinsplit_data: Some(joinsplit_data),
741 ..
742 } => Some(joinsplit_data.pub_key),
743 Transaction::V4 {
745 joinsplit_data: Some(joinsplit_data),
746 ..
747 } => Some(joinsplit_data.pub_key),
748 Transaction::V1 { .. }
750 | Transaction::V2 {
751 joinsplit_data: None,
752 ..
753 }
754 | Transaction::V3 {
755 joinsplit_data: None,
756 ..
757 }
758 | Transaction::V4 {
759 joinsplit_data: None,
760 ..
761 }
762 | Transaction::V5 { .. } => None,
763 #[cfg(feature = "tx_v6")]
764 Transaction::V6 { .. } => None,
765 }
766 }
767
768 pub fn has_sprout_joinsplit_data(&self) -> bool {
770 match self {
771 Transaction::V1 { .. } | Transaction::V5 { .. } => false,
773 #[cfg(feature = "tx_v6")]
774 Transaction::V6 { .. } => false,
775
776 Transaction::V2 { joinsplit_data, .. } | Transaction::V3 { joinsplit_data, .. } => {
778 joinsplit_data.is_some()
779 }
780
781 Transaction::V4 { joinsplit_data, .. } => joinsplit_data.is_some(),
783 }
784 }
785
786 pub fn sprout_note_commitments(
788 &self,
789 ) -> Box<dyn Iterator<Item = &sprout::commitment::NoteCommitment> + '_> {
790 match self {
791 Transaction::V2 {
793 joinsplit_data: Some(joinsplit_data),
794 ..
795 }
796 | Transaction::V3 {
797 joinsplit_data: Some(joinsplit_data),
798 ..
799 } => Box::new(joinsplit_data.note_commitments()),
800
801 Transaction::V4 {
803 joinsplit_data: Some(joinsplit_data),
804 ..
805 } => Box::new(joinsplit_data.note_commitments()),
806
807 Transaction::V2 {
809 joinsplit_data: None,
810 ..
811 }
812 | Transaction::V3 {
813 joinsplit_data: None,
814 ..
815 }
816 | Transaction::V4 {
817 joinsplit_data: None,
818 ..
819 }
820 | Transaction::V1 { .. }
821 | Transaction::V5 { .. } => Box::new(std::iter::empty()),
822 #[cfg(feature = "tx_v6")]
823 Transaction::V6 { .. } => Box::new(std::iter::empty()),
824 }
825 }
826
827 pub fn sapling_anchors(&self) -> Box<dyn Iterator<Item = sapling::tree::Root> + '_> {
832 match self {
835 Transaction::V4 {
836 sapling_shielded_data: Some(sapling_shielded_data),
837 ..
838 } => Box::new(sapling_shielded_data.anchors()),
839
840 Transaction::V5 {
841 sapling_shielded_data: Some(sapling_shielded_data),
842 ..
843 } => Box::new(sapling_shielded_data.anchors()),
844
845 #[cfg(feature = "tx_v6")]
846 Transaction::V6 {
847 sapling_shielded_data: Some(sapling_shielded_data),
848 ..
849 } => Box::new(sapling_shielded_data.anchors()),
850
851 Transaction::V1 { .. }
853 | Transaction::V2 { .. }
854 | Transaction::V3 { .. }
855 | Transaction::V4 {
856 sapling_shielded_data: None,
857 ..
858 }
859 | Transaction::V5 {
860 sapling_shielded_data: None,
861 ..
862 } => Box::new(std::iter::empty()),
863 #[cfg(feature = "tx_v6")]
864 Transaction::V6 {
865 sapling_shielded_data: None,
866 ..
867 } => Box::new(std::iter::empty()),
868 }
869 }
870
871 pub fn sapling_spends_per_anchor(
882 &self,
883 ) -> Box<dyn Iterator<Item = sapling::Spend<sapling::PerSpendAnchor>> + '_> {
884 match self {
885 Transaction::V4 {
886 sapling_shielded_data: Some(sapling_shielded_data),
887 ..
888 } => Box::new(sapling_shielded_data.spends_per_anchor()),
889 Transaction::V5 {
890 sapling_shielded_data: Some(sapling_shielded_data),
891 ..
892 } => Box::new(sapling_shielded_data.spends_per_anchor()),
893 #[cfg(feature = "tx_v6")]
894 Transaction::V6 {
895 sapling_shielded_data: Some(sapling_shielded_data),
896 ..
897 } => Box::new(sapling_shielded_data.spends_per_anchor()),
898
899 Transaction::V1 { .. }
901 | Transaction::V2 { .. }
902 | Transaction::V3 { .. }
903 | Transaction::V4 {
904 sapling_shielded_data: None,
905 ..
906 }
907 | Transaction::V5 {
908 sapling_shielded_data: None,
909 ..
910 } => Box::new(std::iter::empty()),
911 #[cfg(feature = "tx_v6")]
912 Transaction::V6 {
913 sapling_shielded_data: None,
914 ..
915 } => Box::new(std::iter::empty()),
916 }
917 }
918
919 pub fn sapling_outputs(&self) -> Box<dyn Iterator<Item = &sapling::Output> + '_> {
922 match self {
923 Transaction::V4 {
924 sapling_shielded_data: Some(sapling_shielded_data),
925 ..
926 } => Box::new(sapling_shielded_data.outputs()),
927 Transaction::V5 {
928 sapling_shielded_data: Some(sapling_shielded_data),
929 ..
930 } => Box::new(sapling_shielded_data.outputs()),
931 #[cfg(feature = "tx_v6")]
932 Transaction::V6 {
933 sapling_shielded_data: Some(sapling_shielded_data),
934 ..
935 } => Box::new(sapling_shielded_data.outputs()),
936
937 Transaction::V1 { .. }
939 | Transaction::V2 { .. }
940 | Transaction::V3 { .. }
941 | Transaction::V4 {
942 sapling_shielded_data: None,
943 ..
944 }
945 | Transaction::V5 {
946 sapling_shielded_data: None,
947 ..
948 } => Box::new(std::iter::empty()),
949 #[cfg(feature = "tx_v6")]
950 Transaction::V6 {
951 sapling_shielded_data: None,
952 ..
953 } => Box::new(std::iter::empty()),
954 }
955 }
956
957 pub fn sapling_nullifiers(&self) -> Box<dyn Iterator<Item = &sapling::Nullifier> + '_> {
959 match self {
962 Transaction::V4 {
964 sapling_shielded_data: Some(sapling_shielded_data),
965 ..
966 } => Box::new(sapling_shielded_data.nullifiers()),
967 Transaction::V5 {
968 sapling_shielded_data: Some(sapling_shielded_data),
969 ..
970 } => Box::new(sapling_shielded_data.nullifiers()),
971 #[cfg(feature = "tx_v6")]
972 Transaction::V6 {
973 sapling_shielded_data: Some(sapling_shielded_data),
974 ..
975 } => Box::new(sapling_shielded_data.nullifiers()),
976
977 Transaction::V1 { .. }
979 | Transaction::V2 { .. }
980 | Transaction::V3 { .. }
981 | Transaction::V4 {
982 sapling_shielded_data: None,
983 ..
984 }
985 | Transaction::V5 {
986 sapling_shielded_data: None,
987 ..
988 } => Box::new(std::iter::empty()),
989 #[cfg(feature = "tx_v6")]
990 Transaction::V6 {
991 sapling_shielded_data: None,
992 ..
993 } => Box::new(std::iter::empty()),
994 }
995 }
996
997 pub fn sapling_note_commitments(
999 &self,
1000 ) -> Box<dyn Iterator<Item = &sapling_crypto::note::ExtractedNoteCommitment> + '_> {
1001 match self {
1004 Transaction::V4 {
1006 sapling_shielded_data: Some(sapling_shielded_data),
1007 ..
1008 } => Box::new(sapling_shielded_data.note_commitments()),
1009 Transaction::V5 {
1010 sapling_shielded_data: Some(sapling_shielded_data),
1011 ..
1012 } => Box::new(sapling_shielded_data.note_commitments()),
1013 #[cfg(feature = "tx_v6")]
1014 Transaction::V6 {
1015 sapling_shielded_data: Some(sapling_shielded_data),
1016 ..
1017 } => Box::new(sapling_shielded_data.note_commitments()),
1018
1019 Transaction::V1 { .. }
1021 | Transaction::V2 { .. }
1022 | Transaction::V3 { .. }
1023 | Transaction::V4 {
1024 sapling_shielded_data: None,
1025 ..
1026 }
1027 | Transaction::V5 {
1028 sapling_shielded_data: None,
1029 ..
1030 } => Box::new(std::iter::empty()),
1031 #[cfg(feature = "tx_v6")]
1032 Transaction::V6 {
1033 sapling_shielded_data: None,
1034 ..
1035 } => Box::new(std::iter::empty()),
1036 }
1037 }
1038
1039 pub fn has_sapling_shielded_data(&self) -> bool {
1041 match self {
1042 Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => false,
1043 Transaction::V4 {
1044 sapling_shielded_data,
1045 ..
1046 } => sapling_shielded_data.is_some(),
1047 Transaction::V5 {
1048 sapling_shielded_data,
1049 ..
1050 } => sapling_shielded_data.is_some(),
1051 #[cfg(feature = "tx_v6")]
1052 Transaction::V6 {
1053 sapling_shielded_data,
1054 ..
1055 } => sapling_shielded_data.is_some(),
1056 }
1057 }
1058
1059 pub fn orchard_shielded_data(&self) -> Option<&orchard::ShieldedData> {
1064 match self {
1065 Transaction::V5 {
1067 orchard_shielded_data,
1068 ..
1069 } => orchard_shielded_data.as_ref(),
1070 #[cfg(feature = "tx_v6")]
1071 Transaction::V6 {
1072 orchard_shielded_data,
1073 ..
1074 } => orchard_shielded_data.as_ref(),
1075
1076 Transaction::V1 { .. }
1078 | Transaction::V2 { .. }
1079 | Transaction::V3 { .. }
1080 | Transaction::V4 { .. } => None,
1081 }
1082 }
1083
1084 pub fn orchard_actions(&self) -> impl Iterator<Item = &orchard::Action> {
1087 self.orchard_shielded_data()
1088 .into_iter()
1089 .flat_map(orchard::ShieldedData::actions)
1090 }
1091
1092 pub fn orchard_nullifiers(&self) -> impl Iterator<Item = &orchard::Nullifier> {
1095 self.orchard_shielded_data()
1096 .into_iter()
1097 .flat_map(orchard::ShieldedData::nullifiers)
1098 }
1099
1100 pub fn orchard_note_commitments(&self) -> impl Iterator<Item = &pallas::Base> {
1103 self.orchard_shielded_data()
1104 .into_iter()
1105 .flat_map(orchard::ShieldedData::note_commitments)
1106 }
1107
1108 pub fn orchard_flags(&self) -> Option<orchard::shielded_data::Flags> {
1111 self.orchard_shielded_data()
1112 .map(|orchard_shielded_data| orchard_shielded_data.flags)
1113 }
1114
1115 pub fn has_orchard_shielded_data(&self) -> bool {
1118 self.orchard_shielded_data().is_some()
1119 }
1120
1121 #[allow(clippy::unwrap_in_result)]
1128 fn transparent_value_balance_from_outputs(
1129 &self,
1130 outputs: &HashMap<transparent::OutPoint, transparent::Output>,
1131 ) -> Result<ValueBalance<NegativeAllowed>, ValueBalanceError> {
1132 let input_value = self
1133 .inputs()
1134 .iter()
1135 .map(|i| i.value_from_outputs(outputs))
1136 .sum::<Result<Amount<NonNegative>, AmountError>>()
1137 .map_err(ValueBalanceError::Transparent)?
1138 .constrain()
1139 .expect("conversion from NonNegative to NegativeAllowed is always valid");
1140
1141 let output_value = self
1142 .outputs()
1143 .iter()
1144 .map(|o| o.value())
1145 .sum::<Result<Amount<NonNegative>, AmountError>>()
1146 .map_err(ValueBalanceError::Transparent)?
1147 .constrain()
1148 .expect("conversion from NonNegative to NegativeAllowed is always valid");
1149
1150 (input_value - output_value)
1151 .map(ValueBalance::from_transparent_amount)
1152 .map_err(ValueBalanceError::Transparent)
1153 }
1154
1155 pub fn output_values_to_sprout(&self) -> Box<dyn Iterator<Item = &Amount<NonNegative>> + '_> {
1161 match self {
1162 Transaction::V2 {
1164 joinsplit_data: Some(joinsplit_data),
1165 ..
1166 }
1167 | Transaction::V3 {
1168 joinsplit_data: Some(joinsplit_data),
1169 ..
1170 } => Box::new(
1171 joinsplit_data
1172 .joinsplits()
1173 .map(|joinsplit| &joinsplit.vpub_old),
1174 ),
1175 Transaction::V4 {
1177 joinsplit_data: Some(joinsplit_data),
1178 ..
1179 } => Box::new(
1180 joinsplit_data
1181 .joinsplits()
1182 .map(|joinsplit| &joinsplit.vpub_old),
1183 ),
1184 Transaction::V1 { .. }
1186 | Transaction::V2 {
1187 joinsplit_data: None,
1188 ..
1189 }
1190 | Transaction::V3 {
1191 joinsplit_data: None,
1192 ..
1193 }
1194 | Transaction::V4 {
1195 joinsplit_data: None,
1196 ..
1197 }
1198 | Transaction::V5 { .. } => Box::new(std::iter::empty()),
1199 #[cfg(feature = "tx_v6")]
1200 Transaction::V6 { .. } => Box::new(std::iter::empty()),
1201 }
1202 }
1203
1204 pub fn input_values_from_sprout(&self) -> Box<dyn Iterator<Item = &Amount<NonNegative>> + '_> {
1210 match self {
1211 Transaction::V2 {
1213 joinsplit_data: Some(joinsplit_data),
1214 ..
1215 }
1216 | Transaction::V3 {
1217 joinsplit_data: Some(joinsplit_data),
1218 ..
1219 } => Box::new(
1220 joinsplit_data
1221 .joinsplits()
1222 .map(|joinsplit| &joinsplit.vpub_new),
1223 ),
1224 Transaction::V4 {
1226 joinsplit_data: Some(joinsplit_data),
1227 ..
1228 } => Box::new(
1229 joinsplit_data
1230 .joinsplits()
1231 .map(|joinsplit| &joinsplit.vpub_new),
1232 ),
1233 Transaction::V1 { .. }
1235 | Transaction::V2 {
1236 joinsplit_data: None,
1237 ..
1238 }
1239 | Transaction::V3 {
1240 joinsplit_data: None,
1241 ..
1242 }
1243 | Transaction::V4 {
1244 joinsplit_data: None,
1245 ..
1246 }
1247 | Transaction::V5 { .. } => Box::new(std::iter::empty()),
1248 #[cfg(feature = "tx_v6")]
1249 Transaction::V6 { .. } => Box::new(std::iter::empty()),
1250 }
1251 }
1252
1253 fn sprout_joinsplit_value_balances(
1262 &self,
1263 ) -> impl Iterator<Item = ValueBalance<NegativeAllowed>> + '_ {
1264 let joinsplit_value_balances = match self {
1265 Transaction::V2 {
1266 joinsplit_data: Some(joinsplit_data),
1267 ..
1268 }
1269 | Transaction::V3 {
1270 joinsplit_data: Some(joinsplit_data),
1271 ..
1272 } => joinsplit_data.joinsplit_value_balances(),
1273 Transaction::V4 {
1274 joinsplit_data: Some(joinsplit_data),
1275 ..
1276 } => joinsplit_data.joinsplit_value_balances(),
1277 Transaction::V1 { .. }
1278 | Transaction::V2 {
1279 joinsplit_data: None,
1280 ..
1281 }
1282 | Transaction::V3 {
1283 joinsplit_data: None,
1284 ..
1285 }
1286 | Transaction::V4 {
1287 joinsplit_data: None,
1288 ..
1289 }
1290 | Transaction::V5 { .. } => Box::new(iter::empty()),
1291 #[cfg(feature = "tx_v6")]
1292 Transaction::V6 { .. } => Box::new(iter::empty()),
1293 };
1294
1295 joinsplit_value_balances.map(ValueBalance::from_sprout_amount)
1296 }
1297
1298 fn sprout_value_balance(&self) -> Result<ValueBalance<NegativeAllowed>, ValueBalanceError> {
1310 self.sprout_joinsplit_value_balances().sum()
1311 }
1312
1313 pub fn sapling_value_balance(&self) -> ValueBalance<NegativeAllowed> {
1325 let sapling_value_balance = match self {
1326 Transaction::V4 {
1327 sapling_shielded_data: Some(sapling_shielded_data),
1328 ..
1329 } => sapling_shielded_data.value_balance,
1330 Transaction::V5 {
1331 sapling_shielded_data: Some(sapling_shielded_data),
1332 ..
1333 } => sapling_shielded_data.value_balance,
1334 #[cfg(feature = "tx_v6")]
1335 Transaction::V6 {
1336 sapling_shielded_data: Some(sapling_shielded_data),
1337 ..
1338 } => sapling_shielded_data.value_balance,
1339
1340 Transaction::V1 { .. }
1341 | Transaction::V2 { .. }
1342 | Transaction::V3 { .. }
1343 | Transaction::V4 {
1344 sapling_shielded_data: None,
1345 ..
1346 }
1347 | Transaction::V5 {
1348 sapling_shielded_data: None,
1349 ..
1350 } => Amount::zero(),
1351 #[cfg(feature = "tx_v6")]
1352 Transaction::V6 {
1353 sapling_shielded_data: None,
1354 ..
1355 } => Amount::zero(),
1356 };
1357
1358 ValueBalance::from_sapling_amount(sapling_value_balance)
1359 }
1360
1361 pub fn sapling_binding_sig(&self) -> Option<Signature<Binding>> {
1366 match self {
1367 Transaction::V4 {
1368 sapling_shielded_data: Some(sapling_shielded_data),
1369 ..
1370 } => Some(sapling_shielded_data.binding_sig),
1371 Transaction::V5 {
1372 sapling_shielded_data: Some(sapling_shielded_data),
1373 ..
1374 } => Some(sapling_shielded_data.binding_sig),
1375 #[cfg(feature = "tx_v6")]
1376 Transaction::V6 {
1377 sapling_shielded_data: Some(sapling_shielded_data),
1378 ..
1379 } => Some(sapling_shielded_data.binding_sig),
1380 _ => None,
1381 }
1382 }
1383
1384 pub fn joinsplit_pub_key(&self) -> Option<ed25519::VerificationKeyBytes> {
1392 match self {
1393 Transaction::V2 {
1394 joinsplit_data: Some(joinsplit_data),
1395 ..
1396 } => Some(joinsplit_data.pub_key),
1397 Transaction::V3 {
1398 joinsplit_data: Some(joinsplit_data),
1399 ..
1400 } => Some(joinsplit_data.pub_key),
1401 Transaction::V4 {
1402 joinsplit_data: Some(joinsplit_data),
1403 ..
1404 } => Some(joinsplit_data.pub_key),
1405 _ => None,
1406 }
1407 }
1408
1409 pub fn joinsplit_sig(&self) -> Option<ed25519::Signature> {
1417 match self {
1418 Transaction::V2 {
1419 joinsplit_data: Some(joinsplit_data),
1420 ..
1421 } => Some(joinsplit_data.sig),
1422 Transaction::V3 {
1423 joinsplit_data: Some(joinsplit_data),
1424 ..
1425 } => Some(joinsplit_data.sig),
1426 Transaction::V4 {
1427 joinsplit_data: Some(joinsplit_data),
1428 ..
1429 } => Some(joinsplit_data.sig),
1430 _ => None,
1431 }
1432 }
1433
1434 pub fn orchard_value_balance(&self) -> ValueBalance<NegativeAllowed> {
1446 let orchard_value_balance = self
1447 .orchard_shielded_data()
1448 .map(|shielded_data| shielded_data.value_balance)
1449 .unwrap_or_else(Amount::zero);
1450
1451 ValueBalance::from_orchard_amount(orchard_value_balance)
1452 }
1453
1454 pub(crate) fn value_balance_from_outputs(
1456 &self,
1457 outputs: &HashMap<transparent::OutPoint, transparent::Output>,
1458 ) -> Result<ValueBalance<NegativeAllowed>, ValueBalanceError> {
1459 self.transparent_value_balance_from_outputs(outputs)?
1460 + self.sprout_value_balance()?
1461 + self.sapling_value_balance()
1462 + self.orchard_value_balance()
1463 }
1464
1465 pub fn value_balance(
1486 &self,
1487 utxos: &HashMap<transparent::OutPoint, transparent::Utxo>,
1488 ) -> Result<ValueBalance<NegativeAllowed>, ValueBalanceError> {
1489 self.value_balance_from_outputs(&outputs_from_utxos(utxos.clone()))
1490 }
1491
1492 pub(crate) fn to_librustzcash(
1498 &self,
1499 nu: NetworkUpgrade,
1500 ) -> Result<zcash_primitives::transaction::Transaction, crate::Error> {
1501 if self.network_upgrade().is_some_and(|tx_nu| tx_nu != nu) {
1502 return Err(crate::Error::InvalidConsensusBranchId);
1503 }
1504
1505 let Some(branch_id) = nu.branch_id() else {
1506 return Err(crate::Error::InvalidConsensusBranchId);
1507 };
1508
1509 let Ok(branch_id) = consensus::BranchId::try_from(branch_id) else {
1510 return Err(crate::Error::InvalidConsensusBranchId);
1511 };
1512
1513 Ok(zcash_primitives::transaction::Transaction::read(
1514 &self.zcash_serialize_to_vec()?[..],
1515 branch_id,
1516 )?)
1517 }
1518
1519 pub fn has_shielded_data(&self) -> bool {
1523 self.has_shielded_inputs() || self.has_shielded_outputs()
1524 }
1525
1526 pub fn version_group_id(&self) -> Option<u32> {
1528 match self {
1532 Transaction::V1 { .. } | Transaction::V2 { .. } => None,
1533 Transaction::V3 { .. } => Some(OVERWINTER_VERSION_GROUP_ID),
1534 Transaction::V4 { .. } => Some(SAPLING_VERSION_GROUP_ID),
1535 Transaction::V5 { .. } => Some(TX_V5_VERSION_GROUP_ID),
1536 #[cfg(feature = "tx_v6")]
1537 Transaction::V6 { .. } => Some(TX_V6_VERSION_GROUP_ID),
1538 }
1539 }
1540}
1541
1542#[cfg(any(test, feature = "proptest-impl"))]
1543impl Transaction {
1544 pub fn update_network_upgrade(&mut self, nu: NetworkUpgrade) -> Result<(), &str> {
1550 match self {
1551 Transaction::V1 { .. }
1552 | Transaction::V2 { .. }
1553 | Transaction::V3 { .. }
1554 | Transaction::V4 { .. } => Err(
1555 "Updating the network upgrade for V1, V2, V3 and V4 transactions is not possible.",
1556 ),
1557 Transaction::V5 {
1558 ref mut network_upgrade,
1559 ..
1560 } => {
1561 *network_upgrade = nu;
1562 Ok(())
1563 }
1564 #[cfg(feature = "tx_v6")]
1565 Transaction::V6 {
1566 ref mut network_upgrade,
1567 ..
1568 } => {
1569 *network_upgrade = nu;
1570 Ok(())
1571 }
1572 }
1573 }
1574
1575 pub fn expiry_height_mut(&mut self) -> &mut block::Height {
1581 match self {
1582 Transaction::V1 { .. } | Transaction::V2 { .. } => {
1583 panic!("v1 and v2 transactions are not supported")
1584 }
1585 Transaction::V3 {
1586 ref mut expiry_height,
1587 ..
1588 }
1589 | Transaction::V4 {
1590 ref mut expiry_height,
1591 ..
1592 }
1593 | Transaction::V5 {
1594 ref mut expiry_height,
1595 ..
1596 } => expiry_height,
1597 #[cfg(feature = "tx_v6")]
1598 Transaction::V6 {
1599 ref mut expiry_height,
1600 ..
1601 } => expiry_height,
1602 }
1603 }
1604
1605 pub fn inputs_mut(&mut self) -> &mut Vec<transparent::Input> {
1607 match self {
1608 Transaction::V1 { ref mut inputs, .. } => inputs,
1609 Transaction::V2 { ref mut inputs, .. } => inputs,
1610 Transaction::V3 { ref mut inputs, .. } => inputs,
1611 Transaction::V4 { ref mut inputs, .. } => inputs,
1612 Transaction::V5 { ref mut inputs, .. } => inputs,
1613 #[cfg(feature = "tx_v6")]
1614 Transaction::V6 { ref mut inputs, .. } => inputs,
1615 }
1616 }
1617
1618 pub fn orchard_value_balance_mut(&mut self) -> Option<&mut Amount<NegativeAllowed>> {
1623 self.orchard_shielded_data_mut()
1624 .map(|shielded_data| &mut shielded_data.value_balance)
1625 }
1626
1627 pub fn sapling_value_balance_mut(&mut self) -> Option<&mut Amount<NegativeAllowed>> {
1632 match self {
1633 Transaction::V4 {
1634 sapling_shielded_data: Some(sapling_shielded_data),
1635 ..
1636 } => Some(&mut sapling_shielded_data.value_balance),
1637 Transaction::V5 {
1638 sapling_shielded_data: Some(sapling_shielded_data),
1639 ..
1640 } => Some(&mut sapling_shielded_data.value_balance),
1641 #[cfg(feature = "tx_v6")]
1642 Transaction::V6 {
1643 sapling_shielded_data: Some(sapling_shielded_data),
1644 ..
1645 } => Some(&mut sapling_shielded_data.value_balance),
1646 Transaction::V1 { .. }
1647 | Transaction::V2 { .. }
1648 | Transaction::V3 { .. }
1649 | Transaction::V4 {
1650 sapling_shielded_data: None,
1651 ..
1652 }
1653 | Transaction::V5 {
1654 sapling_shielded_data: None,
1655 ..
1656 } => None,
1657 #[cfg(feature = "tx_v6")]
1658 Transaction::V6 {
1659 sapling_shielded_data: None,
1660 ..
1661 } => None,
1662 }
1663 }
1664
1665 pub fn input_values_from_sprout_mut(
1670 &mut self,
1671 ) -> Box<dyn Iterator<Item = &mut Amount<NonNegative>> + '_> {
1672 match self {
1673 Transaction::V2 {
1675 joinsplit_data: Some(joinsplit_data),
1676 ..
1677 }
1678 | Transaction::V3 {
1679 joinsplit_data: Some(joinsplit_data),
1680 ..
1681 } => Box::new(
1682 joinsplit_data
1683 .joinsplits_mut()
1684 .map(|joinsplit| &mut joinsplit.vpub_new),
1685 ),
1686 Transaction::V4 {
1688 joinsplit_data: Some(joinsplit_data),
1689 ..
1690 } => Box::new(
1691 joinsplit_data
1692 .joinsplits_mut()
1693 .map(|joinsplit| &mut joinsplit.vpub_new),
1694 ),
1695 Transaction::V1 { .. }
1697 | Transaction::V2 {
1698 joinsplit_data: None,
1699 ..
1700 }
1701 | Transaction::V3 {
1702 joinsplit_data: None,
1703 ..
1704 }
1705 | Transaction::V4 {
1706 joinsplit_data: None,
1707 ..
1708 }
1709 | Transaction::V5 { .. } => Box::new(std::iter::empty()),
1710 #[cfg(feature = "tx_v6")]
1711 Transaction::V6 { .. } => Box::new(std::iter::empty()),
1712 }
1713 }
1714
1715 pub fn output_values_to_sprout_mut(
1720 &mut self,
1721 ) -> Box<dyn Iterator<Item = &mut Amount<NonNegative>> + '_> {
1722 match self {
1723 Transaction::V2 {
1725 joinsplit_data: Some(joinsplit_data),
1726 ..
1727 }
1728 | Transaction::V3 {
1729 joinsplit_data: Some(joinsplit_data),
1730 ..
1731 } => Box::new(
1732 joinsplit_data
1733 .joinsplits_mut()
1734 .map(|joinsplit| &mut joinsplit.vpub_old),
1735 ),
1736 Transaction::V4 {
1738 joinsplit_data: Some(joinsplit_data),
1739 ..
1740 } => Box::new(
1741 joinsplit_data
1742 .joinsplits_mut()
1743 .map(|joinsplit| &mut joinsplit.vpub_old),
1744 ),
1745 Transaction::V1 { .. }
1747 | Transaction::V2 {
1748 joinsplit_data: None,
1749 ..
1750 }
1751 | Transaction::V3 {
1752 joinsplit_data: None,
1753 ..
1754 }
1755 | Transaction::V4 {
1756 joinsplit_data: None,
1757 ..
1758 }
1759 | Transaction::V5 { .. } => Box::new(std::iter::empty()),
1760 #[cfg(feature = "tx_v6")]
1761 Transaction::V6 { .. } => Box::new(std::iter::empty()),
1762 }
1763 }
1764
1765 pub fn output_values_mut(&mut self) -> impl Iterator<Item = &mut Amount<NonNegative>> {
1767 self.outputs_mut()
1768 .iter_mut()
1769 .map(|output| &mut output.value)
1770 }
1771
1772 pub fn orchard_shielded_data_mut(&mut self) -> Option<&mut orchard::ShieldedData> {
1775 match self {
1776 Transaction::V5 {
1777 orchard_shielded_data: Some(orchard_shielded_data),
1778 ..
1779 } => Some(orchard_shielded_data),
1780 #[cfg(feature = "tx_v6")]
1781 Transaction::V6 {
1782 orchard_shielded_data: Some(orchard_shielded_data),
1783 ..
1784 } => Some(orchard_shielded_data),
1785
1786 Transaction::V1 { .. }
1787 | Transaction::V2 { .. }
1788 | Transaction::V3 { .. }
1789 | Transaction::V4 { .. }
1790 | Transaction::V5 {
1791 orchard_shielded_data: None,
1792 ..
1793 } => None,
1794 #[cfg(feature = "tx_v6")]
1795 Transaction::V6 {
1796 orchard_shielded_data: None,
1797 ..
1798 } => None,
1799 }
1800 }
1801
1802 pub fn outputs_mut(&mut self) -> &mut Vec<transparent::Output> {
1804 match self {
1805 Transaction::V1 {
1806 ref mut outputs, ..
1807 } => outputs,
1808 Transaction::V2 {
1809 ref mut outputs, ..
1810 } => outputs,
1811 Transaction::V3 {
1812 ref mut outputs, ..
1813 } => outputs,
1814 Transaction::V4 {
1815 ref mut outputs, ..
1816 } => outputs,
1817 Transaction::V5 {
1818 ref mut outputs, ..
1819 } => outputs,
1820 #[cfg(feature = "tx_v6")]
1821 Transaction::V6 {
1822 ref mut outputs, ..
1823 } => outputs,
1824 }
1825 }
1826}