1mod addressable_entity_identifier;
2mod approval;
3mod approvals_hash;
4mod deploy;
5mod error;
6mod execution_info;
7mod initiator_addr;
8#[cfg(any(feature = "std", test, feature = "testing"))]
9mod initiator_addr_and_secret_key;
10mod package_identifier;
11mod pricing_mode;
12mod runtime_args;
13mod serialization;
14mod transaction_entry_point;
15mod transaction_hash;
16mod transaction_id;
17mod transaction_invocation_target;
18mod transaction_scheduling;
19mod transaction_target;
20mod transaction_v1;
21mod transfer_target;
22
23#[cfg(feature = "json-schema")]
24use crate::URef;
25use alloc::{
26 collections::BTreeSet,
27 string::{String, ToString},
28 vec::Vec,
29};
30use core::fmt::{self, Debug, Display, Formatter};
31#[cfg(feature = "datasize")]
32use datasize::DataSize;
33#[cfg(feature = "json-schema")]
34use once_cell::sync::Lazy;
35#[cfg(any(all(feature = "std", feature = "testing"), test))]
36use rand::Rng;
37#[cfg(feature = "json-schema")]
38use schemars::JsonSchema;
39#[cfg(any(feature = "std", test))]
40use serde::{de, ser, Deserializer, Serializer};
41#[cfg(any(feature = "std", test))]
42use serde::{Deserialize, Serialize};
43#[cfg(any(feature = "std", test))]
44use serde_bytes::ByteBuf;
45#[cfg(any(feature = "std", test))]
46use std::hash::Hash;
47#[cfg(any(feature = "std", test))]
48use thiserror::Error;
49use tracing::error;
50#[cfg(any(feature = "std", test))]
51pub use transaction_v1::calculate_transaction_lane;
52#[cfg(any(feature = "std", test))]
53use transaction_v1::TransactionV1Json;
54
55#[cfg(any(all(feature = "std", feature = "testing"), test))]
56use crate::testing::TestRng;
57use crate::{
58 account::AccountHash,
59 bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH},
60 Digest, Phase, SecretKey, TimeDiff, Timestamp,
61};
62#[cfg(any(feature = "std", test))]
63use crate::{Chainspec, Gas, Motes, TransactionV1Config};
64pub use addressable_entity_identifier::AddressableEntityIdentifier;
65pub use approval::Approval;
66pub use approvals_hash::ApprovalsHash;
67#[cfg(any(feature = "std", test))]
68pub use deploy::calculate_lane_id_for_deploy;
69pub use deploy::{
70 Deploy, DeployDecodeFromJsonError, DeployError, DeployExcessiveSizeError, DeployHash,
71 DeployHeader, DeployId, ExecutableDeployItem, ExecutableDeployItemIdentifier, InvalidDeploy,
72};
73pub use error::InvalidTransaction;
74pub use execution_info::ExecutionInfo;
75pub use initiator_addr::InitiatorAddr;
76#[cfg(any(feature = "std", feature = "testing", test))]
77pub(crate) use initiator_addr_and_secret_key::InitiatorAddrAndSecretKey;
78pub use package_identifier::PackageIdentifier;
79pub use pricing_mode::{PricingMode, PricingModeError};
80pub use runtime_args::{NamedArg, RuntimeArgs};
81pub use transaction_entry_point::TransactionEntryPoint;
82pub use transaction_hash::TransactionHash;
83pub use transaction_id::TransactionId;
84pub use transaction_invocation_target::TransactionInvocationTarget;
85pub use transaction_scheduling::TransactionScheduling;
86pub use transaction_target::{TransactionRuntimeParams, TransactionTarget};
87#[cfg(feature = "json-schema")]
88pub(crate) use transaction_v1::arg_handling;
89#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))]
90pub(crate) use transaction_v1::fields_container::FieldsContainer;
91pub use transaction_v1::{
92 InvalidTransactionV1, TransactionArgs, TransactionV1, TransactionV1DecodeFromJsonError,
93 TransactionV1Error, TransactionV1ExcessiveSizeError, TransactionV1Hash, TransactionV1Payload,
94};
95pub use transfer_target::TransferTarget;
96
97const DEPLOY_TAG: u8 = 0;
98const V1_TAG: u8 = 1;
99
100#[cfg(feature = "json-schema")]
101pub(super) static TRANSACTION: Lazy<Transaction> = Lazy::new(|| {
102 let secret_key = SecretKey::example();
103 let source = URef::from_formatted_str(
104 "uref-0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a-007",
105 )
106 .unwrap();
107 let target = URef::from_formatted_str(
108 "uref-1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b-000",
109 )
110 .unwrap();
111 let id = Some(999);
112 let amount = 30_000_000_000_u64;
113 let args = arg_handling::new_transfer_args(amount, Some(source), target, id).unwrap();
114 let container = FieldsContainer::new(
115 TransactionArgs::Named(args),
116 TransactionTarget::Native,
117 TransactionEntryPoint::Transfer,
118 TransactionScheduling::Standard,
119 );
120 let pricing_mode = PricingMode::Fixed {
121 gas_price_tolerance: 5,
122 additional_computation_factor: 0,
123 };
124 let initiator_addr_and_secret_key = InitiatorAddrAndSecretKey::SecretKey(secret_key);
125 let v1_txn = TransactionV1::build(
126 "casper-example".to_string(),
127 *Timestamp::example(),
128 TimeDiff::from_seconds(3_600),
129 pricing_mode,
130 container.to_map().unwrap(),
131 initiator_addr_and_secret_key,
132 );
133 Transaction::V1(v1_txn)
134});
135
136#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
138#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
139#[cfg_attr(feature = "datasize", derive(DataSize))]
140pub enum Transaction {
141 Deploy(Deploy),
143 #[cfg_attr(
145 feature = "json-schema",
146 serde(rename = "Version1"),
147 schemars(with = "TransactionV1Json")
148 )]
149 V1(TransactionV1),
150}
151
152impl Transaction {
153 pub fn from_deploy(deploy: Deploy) -> Self {
155 Transaction::Deploy(deploy)
156 }
157
158 pub fn from_v1(v1: TransactionV1) -> Self {
160 Transaction::V1(v1)
161 }
162
163 pub fn hash(&self) -> TransactionHash {
165 match self {
166 Transaction::Deploy(deploy) => TransactionHash::from(*deploy.hash()),
167 Transaction::V1(txn) => TransactionHash::from(*txn.hash()),
168 }
169 }
170
171 pub fn size_estimate(&self) -> usize {
173 match self {
174 Transaction::Deploy(deploy) => deploy.serialized_length(),
175 Transaction::V1(v1) => v1.serialized_length(),
176 }
177 }
178
179 pub fn timestamp(&self) -> Timestamp {
181 match self {
182 Transaction::Deploy(deploy) => deploy.header().timestamp(),
183 Transaction::V1(v1) => v1.payload().timestamp(),
184 }
185 }
186
187 pub fn ttl(&self) -> TimeDiff {
189 match self {
190 Transaction::Deploy(deploy) => deploy.header().ttl(),
191 Transaction::V1(v1) => v1.payload().ttl(),
192 }
193 }
194
195 pub fn verify(&self) -> Result<(), InvalidTransaction> {
198 match self {
199 Transaction::Deploy(deploy) => deploy.is_valid().map_err(Into::into),
200 Transaction::V1(v1) => v1.verify().map_err(Into::into),
201 }
202 }
203
204 pub fn sign(&mut self, secret_key: &SecretKey) {
206 match self {
207 Transaction::Deploy(deploy) => deploy.sign(secret_key),
208 Transaction::V1(v1) => v1.sign(secret_key),
209 }
210 }
211
212 pub fn approvals(&self) -> BTreeSet<Approval> {
214 match self {
215 Transaction::Deploy(deploy) => deploy.approvals().clone(),
216 Transaction::V1(v1) => v1.approvals().clone(),
217 }
218 }
219
220 pub fn compute_approvals_hash(&self) -> Result<ApprovalsHash, bytesrepr::Error> {
222 let approvals_hash = match self {
223 Transaction::Deploy(deploy) => deploy.compute_approvals_hash()?,
224 Transaction::V1(txn) => txn.compute_approvals_hash()?,
225 };
226 Ok(approvals_hash)
227 }
228
229 pub fn chain_name(&self) -> String {
231 match self {
232 Transaction::Deploy(txn) => txn.chain_name().to_string(),
233 Transaction::V1(txn) => txn.chain_name().to_string(),
234 }
235 }
236
237 pub fn is_standard_payment(&self) -> bool {
243 match self {
244 Transaction::Deploy(txn) => txn.session().is_standard_payment(Phase::Payment),
245 Transaction::V1(txn) => match txn.pricing_mode() {
246 PricingMode::PaymentLimited {
247 standard_payment, ..
248 } => *standard_payment,
249 _ => true,
250 },
251 }
252 }
253
254 pub fn compute_id(&self) -> TransactionId {
257 match self {
258 Transaction::Deploy(deploy) => {
259 let deploy_hash = *deploy.hash();
260 let approvals_hash = deploy.compute_approvals_hash().unwrap_or_else(|error| {
261 error!(%error, "failed to serialize deploy approvals");
262 ApprovalsHash::from(Digest::default())
263 });
264 TransactionId::new(TransactionHash::Deploy(deploy_hash), approvals_hash)
265 }
266 Transaction::V1(txn) => {
267 let txn_hash = *txn.hash();
268 let approvals_hash = txn.compute_approvals_hash().unwrap_or_else(|error| {
269 error!(%error, "failed to serialize transaction approvals");
270 ApprovalsHash::from(Digest::default())
271 });
272 TransactionId::new(TransactionHash::V1(txn_hash), approvals_hash)
273 }
274 }
275 }
276
277 pub fn initiator_addr(&self) -> InitiatorAddr {
279 match self {
280 Transaction::Deploy(deploy) => InitiatorAddr::PublicKey(deploy.account().clone()),
281 Transaction::V1(txn) => txn.initiator_addr().clone(),
282 }
283 }
284
285 pub fn expired(&self, current_instant: Timestamp) -> bool {
287 match self {
288 Transaction::Deploy(deploy) => deploy.expired(current_instant),
289 Transaction::V1(txn) => txn.expired(current_instant),
290 }
291 }
292
293 pub fn expires(&self) -> Timestamp {
295 match self {
296 Transaction::Deploy(deploy) => deploy.header().expires(),
297 Transaction::V1(txn) => txn.payload().expires(),
298 }
299 }
300
301 pub fn signers(&self) -> BTreeSet<AccountHash> {
303 match self {
304 Transaction::Deploy(deploy) => deploy
305 .approvals()
306 .iter()
307 .map(|approval| approval.signer().to_account_hash())
308 .collect(),
309 Transaction::V1(txn) => txn
310 .approvals()
311 .iter()
312 .map(|approval| approval.signer().to_account_hash())
313 .collect(),
314 }
315 }
316
317 #[doc(hidden)]
322 pub fn with_approvals(self, approvals: BTreeSet<Approval>) -> Self {
323 match self {
324 Transaction::Deploy(deploy) => Transaction::Deploy(deploy.with_approvals(approvals)),
325 Transaction::V1(transaction_v1) => {
326 Transaction::V1(transaction_v1.with_approvals(approvals))
327 }
328 }
329 }
330
331 pub fn as_transaction_v1(&self) -> Option<&TransactionV1> {
333 match self {
334 Transaction::Deploy(_) => None,
335 Transaction::V1(v1) => Some(v1),
336 }
337 }
338
339 pub fn authorization_keys(&self) -> BTreeSet<AccountHash> {
341 match self {
342 Transaction::Deploy(deploy) => deploy
343 .approvals()
344 .iter()
345 .map(|approval| approval.signer().to_account_hash())
346 .collect(),
347 Transaction::V1(transaction_v1) => transaction_v1
348 .approvals()
349 .iter()
350 .map(|approval| approval.signer().to_account_hash())
351 .collect(),
352 }
353 }
354
355 pub fn is_legacy_transaction(&self) -> bool {
357 match self {
358 Transaction::Deploy(_) => true,
359 Transaction::V1(_) => false,
360 }
361 }
362
363 #[cfg(any(all(feature = "std", feature = "testing"), test))]
364 pub fn gas_limit(&self, chainspec: &Chainspec, lane_id: u8) -> Result<Gas, InvalidTransaction> {
366 match self {
367 Transaction::Deploy(deploy) => deploy
368 .gas_limit(chainspec)
369 .map_err(InvalidTransaction::from),
370 Transaction::V1(v1) => {
371 let pricing_mode = v1.pricing_mode();
372 pricing_mode
373 .gas_limit(chainspec, lane_id)
374 .map_err(InvalidTransaction::from)
375 }
376 }
377 }
378
379 #[cfg(any(all(feature = "std", feature = "testing"), test))]
380 pub fn gas_cost(
383 &self,
384 chainspec: &Chainspec,
385 lane_id: u8,
386 gas_price: u8,
387 ) -> Result<Motes, InvalidTransaction> {
388 match self {
389 Transaction::Deploy(deploy) => deploy
390 .gas_cost(chainspec, gas_price)
391 .map_err(InvalidTransaction::from),
392 Transaction::V1(v1) => {
393 let pricing_mode = v1.pricing_mode();
394 pricing_mode
395 .gas_cost(chainspec, lane_id, gas_price)
396 .map_err(InvalidTransaction::from)
397 }
398 }
399 }
400
401 #[doc(hidden)]
403 #[cfg(feature = "json-schema")]
404 pub fn example() -> &'static Self {
405 &TRANSACTION
406 }
407
408 #[cfg(any(all(feature = "std", feature = "testing"), test))]
410 pub fn random(rng: &mut TestRng) -> Self {
411 if rng.gen() {
412 Transaction::Deploy(Deploy::random_valid_native_transfer(rng))
413 } else {
414 Transaction::V1(TransactionV1::random(rng))
415 }
416 }
417}
418
419#[cfg(any(feature = "std", test))]
420impl Serialize for Transaction {
421 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
422 if serializer.is_human_readable() {
423 TransactionJson::try_from(self.clone())
424 .map_err(|error| ser::Error::custom(format!("{:?}", error)))?
425 .serialize(serializer)
426 } else {
427 let bytes = self
428 .to_bytes()
429 .map_err(|error| ser::Error::custom(format!("{:?}", error)))?;
430 ByteBuf::from(bytes).serialize(serializer)
431 }
432 }
433}
434
435#[cfg(any(feature = "std", test))]
436impl<'de> Deserialize<'de> for Transaction {
437 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
438 if deserializer.is_human_readable() {
439 let json_helper = TransactionJson::deserialize(deserializer)?;
440 Transaction::try_from(json_helper)
441 .map_err(|error| de::Error::custom(format!("{:?}", error)))
442 } else {
443 let bytes = ByteBuf::deserialize(deserializer)?.into_vec();
444 bytesrepr::deserialize::<Transaction>(bytes)
445 .map_err(|error| de::Error::custom(format!("{:?}", error)))
446 }
447 }
448}
449
450#[cfg(any(feature = "std", test))]
452#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
453#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
454#[serde(deny_unknown_fields)]
455enum TransactionJson {
456 Deploy(Deploy),
458 #[serde(rename = "Version1")]
460 V1(TransactionV1Json),
461}
462
463#[cfg(any(feature = "std", test))]
464#[derive(Error, Debug)]
465enum TransactionJsonError {
466 #[error("{0}")]
467 FailedToMap(String),
468}
469
470#[cfg(any(feature = "std", test))]
471impl TryFrom<TransactionJson> for Transaction {
472 type Error = TransactionJsonError;
473 fn try_from(transaction: TransactionJson) -> Result<Self, Self::Error> {
474 match transaction {
475 TransactionJson::Deploy(deploy) => Ok(Transaction::Deploy(deploy)),
476 TransactionJson::V1(v1) => {
477 TransactionV1::try_from(v1)
478 .map(Transaction::V1)
479 .map_err(|error| {
480 TransactionJsonError::FailedToMap(format!(
481 "Failed to map TransactionJson::V1 to Transaction::V1, err: {}",
482 error
483 ))
484 })
485 }
486 }
487 }
488}
489
490#[cfg(any(feature = "std", test))]
491impl TryFrom<Transaction> for TransactionJson {
492 type Error = TransactionJsonError;
493 fn try_from(transaction: Transaction) -> Result<Self, Self::Error> {
494 match transaction {
495 Transaction::Deploy(deploy) => Ok(TransactionJson::Deploy(deploy)),
496 Transaction::V1(v1) => TransactionV1Json::try_from(v1)
497 .map(TransactionJson::V1)
498 .map_err(|error| {
499 TransactionJsonError::FailedToMap(format!(
500 "Failed to map Transaction::V1 to TransactionJson::V1, err: {}",
501 error
502 ))
503 }),
504 }
505 }
506}
507#[cfg(any(feature = "std", test))]
509pub trait GasLimited {
510 type Error;
512
513 const GAS_PRICE_FLOOR: u8 = 1;
515
516 fn gas_cost(&self, chainspec: &Chainspec, gas_price: u8) -> Result<Motes, Self::Error>;
519
520 fn gas_limit(&self, chainspec: &Chainspec) -> Result<Gas, Self::Error>;
522
523 fn gas_price_tolerance(&self) -> Result<u8, Self::Error>;
525}
526
527impl From<Deploy> for Transaction {
528 fn from(deploy: Deploy) -> Self {
529 Self::Deploy(deploy)
530 }
531}
532
533impl From<TransactionV1> for Transaction {
534 fn from(txn: TransactionV1) -> Self {
535 Self::V1(txn)
536 }
537}
538
539impl ToBytes for Transaction {
540 fn to_bytes(&self) -> Result<Vec<u8>, bytesrepr::Error> {
541 let mut buffer = bytesrepr::allocate_buffer(self)?;
542 self.write_bytes(&mut buffer)?;
543 Ok(buffer)
544 }
545
546 fn serialized_length(&self) -> usize {
547 U8_SERIALIZED_LENGTH
548 + match self {
549 Transaction::Deploy(deploy) => deploy.serialized_length(),
550 Transaction::V1(txn) => txn.serialized_length(),
551 }
552 }
553
554 fn write_bytes(&self, writer: &mut Vec<u8>) -> Result<(), bytesrepr::Error> {
555 match self {
556 Transaction::Deploy(deploy) => {
557 DEPLOY_TAG.write_bytes(writer)?;
558 deploy.write_bytes(writer)
559 }
560 Transaction::V1(txn) => {
561 V1_TAG.write_bytes(writer)?;
562 txn.write_bytes(writer)
563 }
564 }
565 }
566}
567
568impl FromBytes for Transaction {
569 fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> {
570 let (tag, remainder) = u8::from_bytes(bytes)?;
571 match tag {
572 DEPLOY_TAG => {
573 let (deploy, remainder) = Deploy::from_bytes(remainder)?;
574 Ok((Transaction::Deploy(deploy), remainder))
575 }
576 V1_TAG => {
577 let (txn, remainder) = TransactionV1::from_bytes(remainder)?;
578 Ok((Transaction::V1(txn), remainder))
579 }
580 _ => Err(bytesrepr::Error::Formatting),
581 }
582 }
583}
584
585impl Display for Transaction {
586 fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
587 match self {
588 Transaction::Deploy(deploy) => Display::fmt(deploy, formatter),
589 Transaction::V1(txn) => Display::fmt(txn, formatter),
590 }
591 }
592}
593
594#[cfg(any(feature = "std", test))]
595pub(crate) enum GetLaneError {
596 NoLaneMatch,
597 PricingModeNotSupported,
598}
599
600#[cfg(any(feature = "std", test))]
601impl From<GetLaneError> for InvalidTransactionV1 {
602 fn from(value: GetLaneError) -> Self {
603 match value {
604 GetLaneError::NoLaneMatch => InvalidTransactionV1::NoLaneMatch,
605 GetLaneError::PricingModeNotSupported => InvalidTransactionV1::PricingModeNotSupported,
606 }
607 }
608}
609
610#[cfg(any(feature = "std", test))]
611impl From<GetLaneError> for InvalidDeploy {
612 fn from(value: GetLaneError) -> Self {
613 match value {
614 GetLaneError::NoLaneMatch => InvalidDeploy::NoLaneMatch,
615 GetLaneError::PricingModeNotSupported => InvalidDeploy::PricingModeNotSupported,
616 }
617 }
618}
619
620#[cfg(any(feature = "std", test))]
621pub(crate) fn get_lane_for_non_install_wasm(
622 config: &TransactionV1Config,
623 pricing_mode: &PricingMode,
624 transaction_size: u64,
625 runtime_args_size: u64,
626) -> Result<u8, GetLaneError> {
627 match pricing_mode {
628 PricingMode::PaymentLimited { payment_amount, .. } => config
629 .get_wasm_lane_id_by_payment_limited(
630 *payment_amount,
631 transaction_size,
632 runtime_args_size,
633 )
634 .ok_or(GetLaneError::NoLaneMatch),
635 PricingMode::Fixed {
636 additional_computation_factor,
637 ..
638 } => config
639 .get_wasm_lane_id_by_size(
640 transaction_size,
641 *additional_computation_factor,
642 runtime_args_size,
643 )
644 .ok_or(GetLaneError::NoLaneMatch),
645 PricingMode::Prepaid { .. } => Err(GetLaneError::PricingModeNotSupported),
646 }
647}
648
649#[cfg(any(feature = "testing", feature = "gens", test))]
651pub mod gens {
652 use super::*;
653 use proptest::{
654 array,
655 prelude::{Arbitrary, Strategy},
656 };
657
658 pub fn deploy_hash_arb() -> impl Strategy<Value = DeployHash> {
664 array::uniform32(<u8>::arbitrary()).prop_map(DeployHash::from_raw)
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 use crate::testing::TestRng;
672
673 #[test]
674 fn json_roundtrip() {
675 let rng = &mut TestRng::new();
676
677 let transaction = Transaction::from(Deploy::random(rng));
678 let json_string = serde_json::to_string_pretty(&transaction).unwrap();
679 let decoded = serde_json::from_str(&json_string).unwrap();
680 assert_eq!(transaction, decoded);
681
682 let transaction = Transaction::from(TransactionV1::random(rng));
683 let json_string = serde_json::to_string_pretty(&transaction).unwrap();
684 let decoded = serde_json::from_str(&json_string).unwrap();
685 assert_eq!(transaction, decoded);
686 }
687
688 #[test]
689 fn bincode_roundtrip() {
690 let rng = &mut TestRng::new();
691
692 let transaction = Transaction::from(Deploy::random(rng));
693 let serialized = bincode::serialize(&transaction).unwrap();
694 let deserialized = bincode::deserialize(&serialized).unwrap();
695 assert_eq!(transaction, deserialized);
696
697 let transaction = Transaction::from(TransactionV1::random(rng));
698 let serialized = bincode::serialize(&transaction).unwrap();
699 let deserialized = bincode::deserialize(&serialized).unwrap();
700 assert_eq!(transaction, deserialized);
701 }
702
703 #[test]
704 fn bytesrepr_roundtrip() {
705 let rng = &mut TestRng::new();
706
707 let transaction = Transaction::from(Deploy::random(rng));
708 bytesrepr::test_serialization_roundtrip(&transaction);
709
710 let transaction = Transaction::from(TransactionV1::random(rng));
711 bytesrepr::test_serialization_roundtrip(&transaction);
712 }
713}
714
715#[cfg(test)]
716mod proptests {
717 use super::*;
718 use crate::{
719 bytesrepr,
720 gens::{legal_transaction_arb, transaction_arb},
721 };
722 use proptest::prelude::*;
723
724 proptest! {
725 #[test]
726 fn bytesrepr_roundtrip(transaction in transaction_arb()) {
727 bytesrepr::test_serialization_roundtrip(&transaction);
728 }
729
730 #[test]
731 fn json_roundtrip(transaction in legal_transaction_arb()) {
732 let json_string = serde_json::to_string_pretty(&transaction).unwrap();
733 let decoded = serde_json::from_str::<Transaction>(&json_string).unwrap();
734 assert_eq!(transaction, decoded);
735 }
736 }
737}