1use std::sync::Arc;
4
5use crate::methods::arrayhex;
6use chrono::{DateTime, Utc};
7use derive_getters::Getters;
8use derive_new::new;
9use hex::ToHex;
10
11use zebra_chain::{
12 amount::{self, Amount, NegativeOrZero, NonNegative},
13 block::{self, merkle::AUTH_DIGEST_PLACEHOLDER, Height},
14 orchard,
15 parameters::Network,
16 primitives::ed25519,
17 sapling::ValueCommitment,
18 serialization::ZcashSerialize,
19 transaction::{self, SerializedTransaction, Transaction, UnminedTx, VerifiedUnminedTx},
20 transparent::Script,
21};
22use zebra_consensus::groth16::Description;
23use zebra_script::Sigops;
24use zebra_state::IntoDisk;
25
26use super::super::opthex;
27use super::zec::Zec;
28
29#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
31#[serde(bound = "FeeConstraint: amount::Constraint + Clone")]
32pub struct TransactionTemplate<FeeConstraint>
33where
34 FeeConstraint: amount::Constraint + Clone + Copy,
35{
36 #[serde(with = "hex")]
38 pub(crate) data: SerializedTransaction,
39
40 #[serde(with = "hex")]
42 #[getter(copy)]
43 pub(crate) hash: transaction::Hash,
44
45 #[serde(rename = "authdigest")]
47 #[serde(with = "hex")]
48 #[getter(copy)]
49 pub(crate) auth_digest: transaction::AuthDigest,
50
51 pub(crate) depends: Vec<u16>,
58
59 #[getter(copy)]
65 pub(crate) fee: Amount<FeeConstraint>,
66
67 pub(crate) sigops: u32,
69
70 pub(crate) required: bool,
74}
75
76impl From<&VerifiedUnminedTx> for TransactionTemplate<NonNegative> {
78 fn from(tx: &VerifiedUnminedTx) -> Self {
79 assert!(
80 !tx.transaction.transaction.is_coinbase(),
81 "unexpected coinbase transaction in mempool"
82 );
83
84 Self {
85 data: tx.transaction.transaction.as_ref().into(),
86 hash: tx.transaction.id.mined_id(),
87 auth_digest: tx
88 .transaction
89 .id
90 .auth_digest()
91 .unwrap_or(AUTH_DIGEST_PLACEHOLDER),
92
93 depends: Vec::new(),
95
96 fee: tx.miner_fee,
97
98 sigops: tx.sigops,
99
100 required: false,
102 }
103 }
104}
105
106impl From<VerifiedUnminedTx> for TransactionTemplate<NonNegative> {
107 fn from(tx: VerifiedUnminedTx) -> Self {
108 Self::from(&tx)
109 }
110}
111
112impl TransactionTemplate<NegativeOrZero> {
113 pub fn from_coinbase(tx: &UnminedTx, miner_fee: Amount<NonNegative>) -> Self {
119 assert!(
120 tx.transaction.is_coinbase(),
121 "invalid generated coinbase transaction: \
122 must have exactly one input, which must be a coinbase input",
123 );
124
125 let miner_fee = (-miner_fee)
126 .constrain()
127 .expect("negating a NonNegative amount always results in a valid NegativeOrZero");
128
129 Self {
130 data: tx.transaction.as_ref().into(),
131 hash: tx.id.mined_id(),
132 auth_digest: tx.id.auth_digest().unwrap_or(AUTH_DIGEST_PLACEHOLDER),
133
134 depends: Vec::new(),
136
137 fee: miner_fee,
138
139 sigops: tx.sigops().expect("sigops count should be valid"),
140
141 required: true,
143 }
144 }
145}
146
147#[allow(clippy::too_many_arguments)]
150#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
151pub struct TransactionObject {
152 #[serde(skip_serializing_if = "Option::is_none")]
155 #[getter(copy)]
156 pub(crate) in_active_chain: Option<bool>,
157 #[serde(with = "hex")]
159 pub(crate) hex: SerializedTransaction,
160 #[serde(skip_serializing_if = "Option::is_none")]
163 #[getter(copy)]
164 pub(crate) height: Option<i32>,
165 #[serde(skip_serializing_if = "Option::is_none")]
169 #[getter(copy)]
170 pub(crate) confirmations: Option<u32>,
171
172 #[serde(rename = "vin")]
174 pub(crate) inputs: Vec<Input>,
175
176 #[serde(rename = "vout")]
178 pub(crate) outputs: Vec<Output>,
179
180 #[serde(rename = "vShieldedSpend")]
182 pub(crate) shielded_spends: Vec<ShieldedSpend>,
183
184 #[serde(rename = "vShieldedOutput")]
186 pub(crate) shielded_outputs: Vec<ShieldedOutput>,
187
188 #[serde(rename = "vjoinsplit")]
190 pub(crate) joinsplits: Vec<JoinSplit>,
191
192 #[serde(
194 skip_serializing_if = "Option::is_none",
195 with = "opthex",
196 default,
197 rename = "bindingSig"
198 )]
199 #[getter(copy)]
200 pub(crate) binding_sig: Option<[u8; 64]>,
201
202 #[serde(
204 skip_serializing_if = "Option::is_none",
205 with = "opthex",
206 default,
207 rename = "joinSplitPubKey"
208 )]
209 #[getter(copy)]
210 pub(crate) joinsplit_pub_key: Option<[u8; 32]>,
211
212 #[serde(
214 skip_serializing_if = "Option::is_none",
215 with = "opthex",
216 default,
217 rename = "joinSplitSig"
218 )]
219 #[getter(copy)]
220 pub(crate) joinsplit_sig: Option<[u8; ed25519::Signature::BYTE_SIZE]>,
221
222 #[serde(rename = "orchard", skip_serializing_if = "Option::is_none")]
224 pub(crate) orchard: Option<Orchard>,
225
226 #[serde(rename = "valueBalance", skip_serializing_if = "Option::is_none")]
228 #[getter(copy)]
229 pub(crate) value_balance: Option<f64>,
230
231 #[serde(rename = "valueBalanceZat", skip_serializing_if = "Option::is_none")]
233 #[getter(copy)]
234 pub(crate) value_balance_zat: Option<i64>,
235
236 #[serde(skip_serializing_if = "Option::is_none")]
238 #[getter(copy)]
239 pub(crate) size: Option<i64>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
243 #[getter(copy)]
244 pub(crate) time: Option<i64>,
245
246 #[serde(with = "hex")]
248 #[getter(copy)]
249 pub txid: transaction::Hash,
250
251 #[serde(
254 rename = "authdigest",
255 with = "opthex",
256 skip_serializing_if = "Option::is_none",
257 default
258 )]
259 #[getter(copy)]
260 pub(crate) auth_digest: Option<transaction::AuthDigest>,
261
262 pub(crate) overwintered: bool,
264
265 pub(crate) version: u32,
267
268 #[serde(
270 rename = "versiongroupid",
271 with = "opthex",
272 skip_serializing_if = "Option::is_none",
273 default
274 )]
275 pub(crate) version_group_id: Option<Vec<u8>>,
276
277 #[serde(rename = "locktime")]
279 pub(crate) lock_time: u32,
280
281 #[serde(rename = "expiryheight", skip_serializing_if = "Option::is_none")]
283 #[getter(copy)]
284 pub(crate) expiry_height: Option<Height>,
285
286 #[serde(
288 rename = "blockhash",
289 with = "opthex",
290 skip_serializing_if = "Option::is_none",
291 default
292 )]
293 #[getter(copy)]
294 pub(crate) block_hash: Option<block::Hash>,
295
296 #[serde(rename = "blocktime", skip_serializing_if = "Option::is_none")]
298 #[getter(copy)]
299 pub(crate) block_time: Option<i64>,
300}
301
302#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
304#[serde(untagged)]
305pub enum Input {
306 Coinbase {
308 #[serde(with = "hex")]
310 coinbase: Vec<u8>,
311 sequence: u32,
313 },
314 NonCoinbase {
316 txid: String,
318 vout: u32,
320 #[serde(rename = "scriptSig")]
322 script_sig: ScriptSig,
323 sequence: u32,
325 #[serde(skip_serializing_if = "Option::is_none")]
327 value: Option<f64>,
328 #[serde(rename = "valueSat", skip_serializing_if = "Option::is_none")]
330 value_zat: Option<i64>,
331 #[serde(skip_serializing_if = "Option::is_none")]
333 address: Option<String>,
334 },
335}
336
337#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
339pub struct Output {
340 value: f64,
342 #[serde(rename = "valueZat")]
344 value_zat: i64,
345 n: u32,
347 #[serde(rename = "scriptPubKey")]
349 script_pub_key: ScriptPubKey,
350}
351
352#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
354pub struct ScriptPubKey {
355 asm: String,
358 #[serde(with = "hex")]
360 hex: Script,
361 #[serde(rename = "reqSigs")]
363 #[serde(default)]
364 #[serde(skip_serializing_if = "Option::is_none")]
365 #[getter(copy)]
366 req_sigs: Option<u32>,
367 r#type: String,
370 #[serde(default)]
372 #[serde(skip_serializing_if = "Option::is_none")]
373 addresses: Option<Vec<String>>,
374}
375
376#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
378pub struct ScriptSig {
379 asm: String,
382 hex: Script,
384}
385
386#[allow(clippy::too_many_arguments)]
388#[serde_with::serde_as]
389#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
390pub struct JoinSplit {
391 #[serde(rename = "vpub_old")]
393 old_public_value: f64,
394 #[serde(rename = "vpub_oldZat")]
396 old_public_value_zat: i64,
397 #[serde(rename = "vpub_new")]
399 new_public_value: f64,
400 #[serde(rename = "vpub_newZat")]
402 new_public_value_zat: i64,
403 #[serde(with = "hex")]
405 #[getter(copy)]
406 anchor: [u8; 32],
407 #[serde_as(as = "Vec<serde_with::hex::Hex>")]
409 nullifiers: Vec<[u8; 32]>,
410 #[serde_as(as = "Vec<serde_with::hex::Hex>")]
412 commitments: Vec<[u8; 32]>,
413 #[serde(rename = "onetimePubKey")]
415 #[serde(with = "hex")]
416 #[getter(copy)]
417 one_time_pubkey: [u8; 32],
418 #[serde(rename = "randomSeed")]
420 #[serde(with = "hex")]
421 #[getter(copy)]
422 random_seed: [u8; 32],
423 #[serde_as(as = "Vec<serde_with::hex::Hex>")]
425 macs: Vec<[u8; 32]>,
426 #[serde(with = "hex")]
428 proof: Vec<u8>,
429 #[serde_as(as = "Vec<serde_with::hex::Hex>")]
431 ciphertexts: Vec<Vec<u8>>,
432}
433
434#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
436pub struct ShieldedSpend {
437 #[serde(with = "hex")]
439 #[getter(skip)]
440 cv: ValueCommitment,
441 #[serde(with = "hex")]
443 #[getter(copy)]
444 anchor: [u8; 32],
445 #[serde(with = "hex")]
447 #[getter(copy)]
448 nullifier: [u8; 32],
449 #[serde(with = "hex")]
451 #[getter(copy)]
452 rk: [u8; 32],
453 #[serde(with = "hex")]
455 #[getter(copy)]
456 proof: [u8; 192],
457 #[serde(rename = "spendAuthSig", with = "hex")]
459 #[getter(copy)]
460 spend_auth_sig: [u8; 64],
461}
462
463impl ShieldedSpend {
465 pub fn cv(&self) -> ValueCommitment {
467 self.cv.clone()
468 }
469}
470
471#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
473pub struct ShieldedOutput {
474 #[serde(with = "hex")]
476 #[getter(skip)]
477 cv: ValueCommitment,
478 #[serde(rename = "cmu", with = "hex")]
480 cm_u: [u8; 32],
481 #[serde(rename = "ephemeralKey", with = "hex")]
483 ephemeral_key: [u8; 32],
484 #[serde(rename = "encCiphertext", with = "arrayhex")]
486 enc_ciphertext: [u8; 580],
487 #[serde(rename = "outCiphertext", with = "hex")]
489 out_ciphertext: [u8; 80],
490 #[serde(with = "hex")]
492 proof: [u8; 192],
493}
494
495impl ShieldedOutput {
497 pub fn cv(&self) -> ValueCommitment {
499 self.cv.clone()
500 }
501}
502
503#[serde_with::serde_as]
505#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
506pub struct Orchard {
507 actions: Vec<OrchardAction>,
509 #[serde(rename = "valueBalance")]
511 value_balance: f64,
512 #[serde(rename = "valueBalanceZat")]
514 value_balance_zat: i64,
515 #[serde(skip_serializing_if = "Option::is_none")]
517 flags: Option<OrchardFlags>,
518 #[serde_as(as = "Option<serde_with::hex::Hex>")]
520 #[serde(skip_serializing_if = "Option::is_none")]
521 #[getter(copy)]
522 anchor: Option<[u8; 32]>,
523 #[serde_as(as = "Option<serde_with::hex::Hex>")]
525 #[serde(skip_serializing_if = "Option::is_none")]
526 proof: Option<Vec<u8>>,
527 #[serde(rename = "bindingSig")]
529 #[serde(skip_serializing_if = "Option::is_none")]
530 #[serde_as(as = "Option<serde_with::hex::Hex>")]
531 #[getter(copy)]
532 binding_sig: Option<[u8; 64]>,
533}
534
535#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
537pub struct OrchardFlags {
538 #[serde(rename = "enableOutputs")]
540 enable_outputs: bool,
541 #[serde(rename = "enableSpends")]
543 enable_spends: bool,
544}
545
546#[allow(clippy::too_many_arguments)]
548#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
549pub struct OrchardAction {
550 #[serde(with = "hex")]
552 cv: [u8; 32],
553 #[serde(with = "hex")]
555 nullifier: [u8; 32],
556 #[serde(with = "hex")]
558 rk: [u8; 32],
559 #[serde(rename = "cmx", with = "hex")]
561 cm_x: [u8; 32],
562 #[serde(rename = "ephemeralKey", with = "hex")]
564 ephemeral_key: [u8; 32],
565 #[serde(rename = "encCiphertext", with = "arrayhex")]
567 enc_ciphertext: [u8; 580],
568 #[serde(rename = "spendAuthSig", with = "hex")]
570 spend_auth_sig: [u8; 64],
571 #[serde(rename = "outCiphertext", with = "hex")]
573 out_ciphertext: [u8; 80],
574}
575
576impl Default for TransactionObject {
577 fn default() -> Self {
578 Self {
579 hex: SerializedTransaction::from(
580 [0u8; zebra_chain::transaction::MIN_TRANSPARENT_TX_SIZE as usize].to_vec(),
581 ),
582 height: Option::default(),
583 confirmations: Option::default(),
584 inputs: Vec::new(),
585 outputs: Vec::new(),
586 shielded_spends: Vec::new(),
587 shielded_outputs: Vec::new(),
588 joinsplits: Vec::new(),
589 orchard: None,
590 binding_sig: None,
591 joinsplit_pub_key: None,
592 joinsplit_sig: None,
593 value_balance: None,
594 value_balance_zat: None,
595 size: None,
596 time: None,
597 txid: transaction::Hash::from([0u8; 32]),
598 in_active_chain: None,
599 auth_digest: None,
600 overwintered: false,
601 version: 4,
602 version_group_id: None,
603 lock_time: 0,
604 expiry_height: None,
605 block_hash: None,
606 block_time: None,
607 }
608 }
609}
610
611impl TransactionObject {
612 #[allow(clippy::unwrap_in_result)]
614 #[allow(clippy::too_many_arguments)]
615 pub fn from_transaction(
616 tx: Arc<Transaction>,
617 height: Option<block::Height>,
618 confirmations: Option<u32>,
619 network: &Network,
620 block_time: Option<DateTime<Utc>>,
621 block_hash: Option<block::Hash>,
622 in_active_chain: Option<bool>,
623 txid: transaction::Hash,
624 ) -> Self {
625 let block_time = block_time.map(|bt| bt.timestamp());
626 Self {
627 hex: tx.clone().into(),
628 height: if in_active_chain.unwrap_or_default() {
629 height.map(|height| height.0 as i32)
630 } else if block_hash.is_some() {
631 Some(-1)
633 } else {
634 None
636 },
637 confirmations: if in_active_chain.unwrap_or_default() {
638 confirmations
639 } else if block_hash.is_some() {
640 Some(0)
642 } else {
643 None
645 },
646 inputs: tx
647 .inputs()
648 .iter()
649 .map(|input| match input {
650 zebra_chain::transparent::Input::Coinbase { sequence, .. } => Input::Coinbase {
651 coinbase: input
652 .coinbase_script()
653 .expect("we know it is a valid coinbase script"),
654 sequence: *sequence,
655 },
656 zebra_chain::transparent::Input::PrevOut {
657 sequence,
658 unlock_script,
659 outpoint,
660 } => Input::NonCoinbase {
661 txid: outpoint.hash.encode_hex(),
662 vout: outpoint.index,
663 script_sig: ScriptSig {
664 asm: "".to_string(),
665 hex: unlock_script.clone(),
666 },
667 sequence: *sequence,
668 value: None,
669 value_zat: None,
670 address: None,
671 },
672 })
673 .collect(),
674 outputs: tx
675 .outputs()
676 .iter()
677 .enumerate()
678 .map(|output| {
679 let (addresses, req_sigs) = output
681 .1
682 .address(network)
683 .map(|address| (vec![address.to_string()], 1))
684 .unzip();
685
686 Output {
687 value: Zec::from(output.1.value).lossy_zec(),
688 value_zat: output.1.value.zatoshis(),
689 n: output.0 as u32,
690 script_pub_key: ScriptPubKey {
691 asm: "".to_string(),
693 hex: output.1.lock_script.clone(),
694 req_sigs,
695 r#type: "".to_string(),
697 addresses,
698 },
699 }
700 })
701 .collect(),
702 shielded_spends: tx
703 .sapling_spends_per_anchor()
704 .map(|spend| {
705 let mut anchor = spend.per_spend_anchor.as_bytes();
706 anchor.reverse();
707
708 let mut nullifier = spend.nullifier.as_bytes();
709 nullifier.reverse();
710
711 let mut rk: [u8; 32] = spend.clone().rk.into();
712 rk.reverse();
713
714 let spend_auth_sig: [u8; 64] = spend.spend_auth_sig.into();
715
716 ShieldedSpend {
717 cv: spend.cv.clone(),
718 anchor,
719 nullifier,
720 rk,
721 proof: spend.proof().0,
722 spend_auth_sig,
723 }
724 })
725 .collect(),
726 shielded_outputs: tx
727 .sapling_outputs()
728 .map(|output| {
729 let mut cm_u: [u8; 32] = output.cm_u.to_bytes();
730 cm_u.reverse();
731 let mut ephemeral_key: [u8; 32] = output.ephemeral_key.into();
732 ephemeral_key.reverse();
733 let enc_ciphertext: [u8; 580] = output.enc_ciphertext.into();
734 let out_ciphertext: [u8; 80] = output.out_ciphertext.into();
735
736 ShieldedOutput {
737 cv: output.cv.clone(),
738 cm_u,
739 ephemeral_key,
740 enc_ciphertext,
741 out_ciphertext,
742 proof: output.proof().0,
743 }
744 })
745 .collect(),
746 joinsplits: tx
747 .sprout_joinsplits()
748 .map(|joinsplit| {
749 let mut ephemeral_key_bytes: [u8; 32] = joinsplit.ephemeral_key.to_bytes();
750 ephemeral_key_bytes.reverse();
751
752 JoinSplit {
753 old_public_value: Zec::from(joinsplit.vpub_old).lossy_zec(),
754 old_public_value_zat: joinsplit.vpub_old.zatoshis(),
755 new_public_value: Zec::from(joinsplit.vpub_new).lossy_zec(),
756 new_public_value_zat: joinsplit.vpub_new.zatoshis(),
757 anchor: joinsplit.anchor.bytes_in_display_order(),
758 nullifiers: joinsplit
759 .nullifiers
760 .iter()
761 .map(|n| n.bytes_in_display_order())
762 .collect(),
763 commitments: joinsplit
764 .commitments
765 .iter()
766 .map(|c| c.bytes_in_display_order())
767 .collect(),
768 one_time_pubkey: ephemeral_key_bytes,
769 random_seed: joinsplit.random_seed.bytes_in_display_order(),
770 macs: joinsplit
771 .vmacs
772 .iter()
773 .map(|m| m.bytes_in_display_order())
774 .collect(),
775 proof: joinsplit.zkproof.unwrap_or_default(),
776 ciphertexts: joinsplit
777 .enc_ciphertexts
778 .iter()
779 .map(|c| c.zcash_serialize_to_vec().unwrap_or_default())
780 .collect(),
781 }
782 })
783 .collect(),
784 value_balance: Some(Zec::from(tx.sapling_value_balance().sapling_amount()).lossy_zec()),
785 value_balance_zat: Some(tx.sapling_value_balance().sapling_amount().zatoshis()),
786 orchard: Some(Orchard {
787 actions: tx
788 .orchard_actions()
789 .collect::<Vec<_>>()
790 .iter()
791 .map(|action| {
792 let spend_auth_sig: [u8; 64] = tx
793 .orchard_shielded_data()
794 .and_then(|shielded_data| {
795 shielded_data
796 .actions
797 .iter()
798 .find(|authorized_action| authorized_action.action == **action)
799 .map(|authorized_action| {
800 authorized_action.spend_auth_sig.into()
801 })
802 })
803 .unwrap_or([0; 64]);
804
805 let cv: [u8; 32] = action.cv.into();
806 let nullifier: [u8; 32] = action.nullifier.into();
807 let rk: [u8; 32] = action.rk.into();
808 let cm_x: [u8; 32] = action.cm_x.into();
809 let ephemeral_key: [u8; 32] = action.ephemeral_key.into();
810 let enc_ciphertext: [u8; 580] = action.enc_ciphertext.into();
811 let out_ciphertext: [u8; 80] = action.out_ciphertext.into();
812
813 OrchardAction {
814 cv,
815 nullifier,
816 rk,
817 cm_x,
818 ephemeral_key,
819 enc_ciphertext,
820 spend_auth_sig,
821 out_ciphertext,
822 }
823 })
824 .collect(),
825 value_balance: Zec::from(tx.orchard_value_balance().orchard_amount()).lossy_zec(),
826 value_balance_zat: tx.orchard_value_balance().orchard_amount().zatoshis(),
827 flags: tx.orchard_shielded_data().map(|data| {
828 OrchardFlags::new(
829 data.flags.contains(orchard::Flags::ENABLE_OUTPUTS),
830 data.flags.contains(orchard::Flags::ENABLE_SPENDS),
831 )
832 }),
833 anchor: tx
834 .orchard_shielded_data()
835 .map(|data| data.shared_anchor.bytes_in_display_order()),
836 proof: tx
837 .orchard_shielded_data()
838 .map(|data| data.proof.bytes_in_display_order()),
839 binding_sig: tx
840 .orchard_shielded_data()
841 .map(|data| data.binding_sig.into()),
842 }),
843 binding_sig: tx.sapling_binding_sig().map(|raw_sig| raw_sig.into()),
844 joinsplit_pub_key: tx.joinsplit_pub_key().map(|raw_key| {
845 let mut key: [u8; 32] = raw_key.into();
847 key.reverse();
848 key
849 }),
850 joinsplit_sig: tx.joinsplit_sig().map(|raw_sig| raw_sig.into()),
851 size: tx.as_bytes().len().try_into().ok(),
852 time: block_time,
853 txid,
854 in_active_chain,
855 auth_digest: tx.auth_digest(),
856 overwintered: tx.is_overwintered(),
857 version: tx.version(),
858 version_group_id: tx.version_group_id().map(|id| id.to_be_bytes().to_vec()),
859 lock_time: tx.raw_lock_time(),
860 expiry_height: tx.expiry_height(),
861 block_hash,
862 block_time,
863 }
864 }
865}