1mod v1;
6
7pub use v1::{
8 ChangedObject, ObjectIn, ObjectOut, TransactionEffectsV1, UnchangedSharedKind,
9 UnchangedSharedObject,
10};
11
12use crate::{ObjectDigest, ObjectId, ObjectReference, ObjectVersion, Version};
13
14#[derive(Clone, Debug, Eq, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
25#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
26#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
27#[non_exhaustive]
28pub enum TransactionEffects {
29 V1(Box<TransactionEffectsV1>),
30}
31
32impl TransactionEffects {
33 crate::def_is_as_into_opt!(V1(Box<TransactionEffectsV1>));
34}
35
36impl crate::TreeDisplay for TransactionEffects {
37 fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
38 w.enum_name("Transaction Effects");
39 match self {
40 Self::V1(v1) => v1.fmt_tree(w),
41 }
42 }
43}
44
45#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub enum InputSharedObject {
52 Mutate(ObjectReference),
54 ReadOnly(ObjectReference),
56 ReadDeleted(ObjectVersion),
58 MutateDeleted(ObjectVersion),
60 Canceled(ObjectVersion),
63}
64
65impl InputSharedObject {
66 pub fn object_reference(&self) -> ObjectReference {
70 match self {
71 Self::Mutate(reference) | Self::ReadOnly(reference) => *reference,
72 Self::ReadDeleted(object) | Self::MutateDeleted(object) => ObjectReference::new(
73 object.object_id,
74 object.version,
75 ObjectDigest::OBJECT_DELETED,
76 ),
77 Self::Canceled(object) => ObjectReference::new(
78 object.object_id,
79 object.version,
80 ObjectDigest::OBJECT_CANCELED,
81 ),
82 }
83 }
84}
85
86impl crate::TreeDisplay for InputSharedObject {
87 fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
88 w.header("Input Shared Object")?;
89 let kind = match self {
90 Self::Mutate(_) => "Mutate",
91 Self::ReadOnly(_) => "Read Only",
92 Self::ReadDeleted(_) => "Read Deleted",
93 Self::MutateDeleted(_) => "Mutate Deleted",
94 Self::Canceled(_) => "Canceled",
95 };
96 w.leaf("Kind", &kind, false)?;
97 w.child("Reference", &self.object_reference(), true)
98 }
99}
100
101#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
107pub enum WriteKind {
108 Mutate,
110 Create,
112 Unwrap,
115}
116
117impl std::fmt::Display for WriteKind {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 let text = match self {
120 Self::Mutate => "Mutate",
121 Self::Create => "Create",
122 Self::Unwrap => "Unwrap",
123 };
124 f.write_str(text)
125 }
126}
127
128#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub enum ObjectRemoveKind {
135 Delete,
137 Wrap,
139}
140
141impl std::fmt::Display for ObjectRemoveKind {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 let text = match self {
144 Self::Delete => "Delete",
145 Self::Wrap => "Wrap",
146 };
147 f.write_str(text)
148 }
149}
150
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub struct ObjectChange {
159 pub object_id: ObjectId,
161 pub input_version: Option<Version>,
163 pub input_digest: Option<ObjectDigest>,
165 pub output_version: Option<Version>,
167 pub output_digest: Option<ObjectDigest>,
169 pub id_operation: IdOperation,
171}
172
173impl crate::TreeDisplay for ObjectChange {
174 fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
175 w.header("Object Change")?;
176 w.leaf("Object ID", &self.object_id, false)?;
177 w.option_leaf("Input Version", &self.input_version, false)?;
178 w.option_leaf("Input Digest", &self.input_digest, false)?;
179 w.option_leaf("Output Version", &self.output_version, false)?;
180 w.option_leaf("Output Digest", &self.output_digest, false)?;
181 w.leaf("ID Operation", &self.id_operation, true)
182 }
183}
184
185crate::impl_tree_display!(TransactionEffects, InputSharedObject, ObjectChange);
186
187#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::Display)]
199#[cfg_attr(
200 feature = "serde",
201 derive(serde::Deserialize, serde::Serialize),
202 serde(rename_all = "lowercase")
203)]
204#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
205#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
206#[non_exhaustive]
207pub enum IdOperation {
208 None,
209 Created,
210 Deleted,
211}
212
213impl IdOperation {
214 crate::def_is!(None, Created, Deleted);
215}
216
217#[cfg(all(feature = "serde", test))]
218mod tests {
219 use base64ct::{Base64, Encoding};
220 #[cfg(target_arch = "wasm32")]
221 use wasm_bindgen_test::wasm_bindgen_test as test;
222
223 use super::{ObjectOut, TransactionEffects};
224
225 const GENESIS_EFFECTS: &str = include_str!("fixtures/genesis-transaction-effects");
227 const SPONSOR_TX_EFFECTS: &str = include_str!("fixtures/sponsor-tx-effects");
228
229 #[test]
230 fn effects_fixtures() {
231 for fixture in [GENESIS_EFFECTS, SPONSOR_TX_EFFECTS] {
232 let fixture = Base64::decode_vec(fixture.trim()).unwrap();
233 let fx: TransactionEffects = bcs::from_bytes(&fixture).unwrap();
234 assert_eq!(bcs::to_bytes(&fx).unwrap(), fixture);
235
236 let json = serde_json::to_string_pretty(&fx).unwrap();
237 println!("{json}");
238 assert_eq!(fx, serde_json::from_str(&json).unwrap());
239 }
240 }
241
242 #[test]
245 fn input_shared_objects_span_changed_and_unchanged() {
246 use crate::{
247 ChangedObject, IdOperation, InputSharedObject, ObjectDigest, ObjectId, ObjectIn,
248 ObjectOut, ObjectVersion, Owner, TransactionEffectsV1, UnchangedSharedKind,
249 UnchangedSharedObject, Version,
250 };
251
252 let mutated = ObjectId::new([1; 32]);
253 let read_only = ObjectId::new([2; 32]);
254 let read_deleted = ObjectId::new([3; 32]);
255 let mutate_deleted = ObjectId::new([4; 32]);
256 let canceled = ObjectId::new([5; 32]);
257 let per_epoch_config = ObjectId::new([6; 32]);
258 let owned = ObjectId::new([7; 32]);
259 let digest = ObjectDigest::new([8; 32]);
260 let version = Version::from_u64(3);
261
262 let shared_input = |object_id, owner| ChangedObject {
263 object_id,
264 input_state: ObjectIn::Data {
265 version,
266 digest,
267 owner,
268 },
269 output_state: ObjectOut::ObjectWrite { digest, owner },
270 id_operation: IdOperation::None,
271 };
272 let unchanged = |object_id, kind| UnchangedSharedObject { object_id, kind };
273
274 let effects = TransactionEffectsV1 {
275 status: crate::ExecutionStatus::Success,
276 epoch: 0,
277 gas_cost_summary: crate::GasCostSummary::default(),
278 transaction_digest: crate::TransactionDigest::default(),
279 gas_object_index: None,
280 events_digest: None,
281 dependencies: Vec::new(),
282 lamport_version: Version::from_u64(4),
283 changed_objects: vec![
284 shared_input(mutated, Owner::Shared(version)),
285 shared_input(owned, Owner::Address(crate::Address::ZERO)),
287 ],
288 unchanged_shared_objects: vec![
289 unchanged(
290 read_only,
291 UnchangedSharedKind::ReadOnlyRoot { version, digest },
292 ),
293 unchanged(read_deleted, UnchangedSharedKind::ReadDeleted { version }),
294 unchanged(
295 mutate_deleted,
296 UnchangedSharedKind::MutateDeleted { version },
297 ),
298 unchanged(canceled, UnchangedSharedKind::Canceled { version }),
299 unchanged(per_epoch_config, UnchangedSharedKind::PerEpochConfig),
300 ],
301 auxiliary_data_digest: None,
302 };
303
304 let object = |object_id| ObjectVersion::new(object_id, version);
305 assert_eq!(
306 effects.input_shared_objects(),
307 vec![
308 InputSharedObject::Mutate(crate::ObjectReference::new(mutated, version, digest)),
309 InputSharedObject::ReadOnly(crate::ObjectReference::new(
310 read_only, version, digest
311 )),
312 InputSharedObject::ReadDeleted(object(read_deleted)),
313 InputSharedObject::MutateDeleted(object(mutate_deleted)),
314 InputSharedObject::Canceled(object(canceled)),
315 ],
316 );
317
318 assert!(
321 InputSharedObject::ReadDeleted(object(read_deleted))
322 .object_reference()
323 .digest
324 .is_deleted()
325 );
326 assert_eq!(
327 InputSharedObject::Canceled(object(canceled))
328 .object_reference()
329 .digest,
330 ObjectDigest::OBJECT_CANCELED,
331 );
332 }
333
334 #[test]
338 fn object_changes_keep_a_package_at_its_own_version() {
339 use crate::{
340 ChangedObject, IdOperation, ObjectDigest, ObjectId, ObjectIn, ObjectOut, Owner,
341 TransactionEffectsV1, Version,
342 };
343
344 let lamport_version = Version::from_u64(9);
345 let package_version = Version::from_u64(7);
346 let digest = ObjectDigest::new([1; 32]);
347 let object = ObjectId::new([2; 32]);
348 let package = ObjectId::new([3; 32]);
349
350 let effects = TransactionEffectsV1 {
351 status: crate::ExecutionStatus::Success,
352 epoch: 0,
353 gas_cost_summary: crate::GasCostSummary::default(),
354 transaction_digest: crate::TransactionDigest::default(),
355 gas_object_index: None,
356 events_digest: None,
357 dependencies: Vec::new(),
358 lamport_version,
359 changed_objects: vec![
360 ChangedObject {
361 object_id: object,
362 input_state: ObjectIn::Missing,
363 output_state: ObjectOut::ObjectWrite {
364 digest,
365 owner: Owner::Address(crate::Address::ZERO),
366 },
367 id_operation: IdOperation::Created,
368 },
369 ChangedObject {
370 object_id: package,
371 input_state: ObjectIn::Missing,
372 output_state: ObjectOut::PackageWrite {
373 version: package_version,
374 digest,
375 },
376 id_operation: IdOperation::Created,
377 },
378 ],
379 unchanged_shared_objects: Vec::new(),
380 auxiliary_data_digest: None,
381 };
382
383 let changes = effects.object_changes();
384 assert_eq!(changes[0].output_version, Some(lamport_version));
385 assert_eq!(changes[1].output_version, Some(package_version));
386
387 assert_eq!(
389 effects
390 .created()
391 .into_iter()
392 .map(|owned| owned.reference.version)
393 .collect::<Vec<_>>(),
394 vec![lamport_version, package_version],
395 );
396 }
397
398 #[test]
401 fn object_changes_resolve_each_side() {
402 for fixture in [GENESIS_EFFECTS, SPONSOR_TX_EFFECTS] {
403 let effects: TransactionEffects =
404 bcs::from_bytes(&Base64::decode_vec(fixture.trim()).unwrap()).unwrap();
405 let fx = effects.as_v1();
406
407 let changes = fx.object_changes();
408 assert_eq!(changes.len(), fx.changed_objects.len());
409
410 for (change, changed) in changes.iter().zip(&fx.changed_objects) {
411 assert_eq!(change.object_id, changed.object_id);
412 assert_eq!(change.id_operation, changed.id_operation);
413 assert_eq!(change.input_version, changed.input_state.opt_version());
414 assert_eq!(change.input_digest, changed.input_state.opt_digest());
415 assert_eq!(
416 change.input_version.is_some(),
417 change.input_digest.is_some()
418 );
419 assert_eq!(
420 change.output_version.is_some(),
421 change.output_digest.is_some()
422 );
423
424 match changed.output_state {
427 ObjectOut::ObjectWrite { digest, .. } => {
428 assert_eq!(change.output_version, Some(fx.lamport_version));
429 assert_eq!(change.output_digest, Some(digest));
430 }
431 ObjectOut::PackageWrite { version, digest } => {
432 assert_eq!(change.output_version, Some(version));
433 assert_eq!(change.output_digest, Some(digest));
434 }
435 _ => assert_eq!(change.output_version, None),
436 }
437 }
438 }
439 }
440
441 #[test]
444 fn a_package_write_is_neither_unwrapped_nor_gas() {
445 use crate::{
446 ChangedObject, IdOperation, ObjectDigest, ObjectId, ObjectIn, ObjectOut,
447 TransactionEffectsV1, Version,
448 };
449
450 let package = ObjectId::new([1; 32]);
451 let effects = TransactionEffectsV1 {
452 status: crate::ExecutionStatus::Success,
453 epoch: 0,
454 gas_cost_summary: crate::GasCostSummary::default(),
455 transaction_digest: crate::TransactionDigest::default(),
456 gas_object_index: Some(0),
458 events_digest: None,
459 dependencies: Vec::new(),
460 lamport_version: Version::from_u64(2),
461 changed_objects: vec![ChangedObject {
462 object_id: package,
463 input_state: ObjectIn::Missing,
464 output_state: ObjectOut::PackageWrite {
465 version: Version::from_u64(1),
466 digest: ObjectDigest::new([2; 32]),
467 },
468 id_operation: IdOperation::None,
471 }],
472 unchanged_shared_objects: Vec::new(),
473 auxiliary_data_digest: None,
474 };
475
476 assert!(effects.unwrapped().is_empty());
477 assert!(effects.gas_object().is_none());
478 }
479
480 #[test]
484 fn object_sets_report_each_kind() {
485 use crate::{
486 ChangedObject, IdOperation, ObjectDigest, ObjectId, ObjectIn, ObjectOut,
487 ObjectReference, ObjectRemoveKind, Owner, TransactionEffectsV1, Version, WriteKind,
488 };
489
490 let lamport_version = Version::from_u64(9);
491 let old_version = Version::from_u64(4);
492 let digest = ObjectDigest::new([7; 32]);
493 let owner = Owner::Address(crate::Address::ZERO);
494
495 let created = ObjectId::new([1; 32]);
496 let mutated = ObjectId::new([2; 32]);
497 let unwrapped = ObjectId::new([3; 32]);
498 let deleted = ObjectId::new([4; 32]);
499 let wrapped = ObjectId::new([5; 32]);
500 let unwrapped_then_deleted = ObjectId::new([6; 32]);
501
502 let entry = |object_id, input_state, output_state, id_operation| ChangedObject {
503 object_id,
504 input_state,
505 output_state,
506 id_operation,
507 };
508 let existed = || ObjectIn::Data {
509 version: old_version,
510 digest,
511 owner,
512 };
513 let written = || ObjectOut::ObjectWrite { digest, owner };
514
515 let effects = TransactionEffectsV1 {
516 status: crate::ExecutionStatus::Success,
517 epoch: 0,
518 gas_cost_summary: crate::GasCostSummary::default(),
519 transaction_digest: crate::TransactionDigest::default(),
520 gas_object_index: None,
521 events_digest: None,
522 dependencies: Vec::new(),
523 lamport_version,
524 changed_objects: vec![
525 entry(created, ObjectIn::Missing, written(), IdOperation::Created),
526 entry(mutated, existed(), written(), IdOperation::None),
527 entry(unwrapped, ObjectIn::Missing, written(), IdOperation::None),
528 entry(deleted, existed(), ObjectOut::Missing, IdOperation::Deleted),
529 entry(wrapped, existed(), ObjectOut::Missing, IdOperation::None),
530 entry(
531 unwrapped_then_deleted,
532 ObjectIn::Missing,
533 ObjectOut::Missing,
534 IdOperation::Deleted,
535 ),
536 ],
537 unchanged_shared_objects: Vec::new(),
538 auxiliary_data_digest: None,
539 };
540
541 let ids = |objects: Vec<crate::OwnedObjectReference>| {
542 objects
543 .into_iter()
544 .map(|owned| owned.reference.object_id)
545 .collect::<Vec<_>>()
546 };
547 let removed_ids = |references: Vec<ObjectReference>| {
548 references
549 .into_iter()
550 .map(|reference| reference.object_id)
551 .collect::<Vec<_>>()
552 };
553
554 assert_eq!(ids(effects.created()), vec![created]);
555 assert_eq!(ids(effects.mutated()), vec![mutated]);
556 assert_eq!(ids(effects.unwrapped()), vec![unwrapped]);
557 assert_eq!(removed_ids(effects.deleted()), vec![deleted]);
558 assert_eq!(removed_ids(effects.wrapped()), vec![wrapped]);
559 assert_eq!(
560 removed_ids(effects.unwrapped_then_deleted()),
561 vec![unwrapped_then_deleted]
562 );
563
564 assert_eq!(
566 effects
567 .all_changed_objects()
568 .into_iter()
569 .map(|(object, kind)| (object.reference.object_id, kind))
570 .collect::<Vec<_>>(),
571 vec![
572 (mutated, WriteKind::Mutate),
573 (created, WriteKind::Create),
574 (unwrapped, WriteKind::Unwrap),
575 ]
576 );
577
578 assert_eq!(
581 effects
582 .all_removed_objects()
583 .into_iter()
584 .map(|(reference, kind)| (reference.object_id, kind))
585 .collect::<Vec<_>>(),
586 vec![
587 (deleted, ObjectRemoveKind::Delete),
588 (wrapped, ObjectRemoveKind::Wrap),
589 ]
590 );
591
592 for (reference, kind) in effects.all_removed_objects() {
595 assert_eq!(reference.version, lamport_version);
596 match kind {
597 ObjectRemoveKind::Delete => assert!(reference.digest.is_deleted()),
598 ObjectRemoveKind::Wrap => assert!(reference.digest.is_wrapped()),
599 }
600 }
601 for reference in effects.unwrapped_then_deleted() {
602 assert!(reference.digest.is_deleted());
603 }
604 }
605
606 #[test]
609 fn tagged_unions_cover_the_object_sets() {
610 use crate::{ObjectRemoveKind, WriteKind};
611
612 for fixture in [GENESIS_EFFECTS, SPONSOR_TX_EFFECTS] {
613 let effects: TransactionEffects =
614 bcs::from_bytes(&Base64::decode_vec(fixture.trim()).unwrap()).unwrap();
615 let fx = effects.as_v1();
616
617 let changed = fx.all_changed_objects();
618 assert_eq!(
619 changed.len(),
620 fx.mutated().len() + fx.created().len() + fx.unwrapped().len()
621 );
622 assert!(!changed.is_empty(), "the fixture must change objects");
623 let of_kind = |kind| {
624 changed
625 .iter()
626 .filter(|(_, k)| *k == kind)
627 .map(|(object, _)| *object)
628 .collect::<Vec<_>>()
629 };
630 assert_eq!(of_kind(WriteKind::Mutate), fx.mutated());
631 assert_eq!(of_kind(WriteKind::Create), fx.created());
632 assert_eq!(of_kind(WriteKind::Unwrap), fx.unwrapped());
633
634 let removed = fx.all_removed_objects();
635 assert_eq!(removed.len(), fx.deleted().len() + fx.wrapped().len());
636 let removed_of_kind = |kind| {
637 removed
638 .iter()
639 .filter(|(_, k)| *k == kind)
640 .map(|(reference, _)| *reference)
641 .collect::<Vec<_>>()
642 };
643 assert_eq!(removed_of_kind(ObjectRemoveKind::Delete), fx.deleted());
644 assert_eq!(removed_of_kind(ObjectRemoveKind::Wrap), fx.wrapped());
645
646 for reference in fx.unwrapped_then_deleted() {
649 assert!(!removed.iter().any(|(removed, _)| *removed == reference));
650 }
651 }
652 }
653
654 #[test]
657 fn object_sets_partition_the_changed_objects() {
658 for fixture in [GENESIS_EFFECTS, SPONSOR_TX_EFFECTS] {
659 let effects: TransactionEffects =
660 bcs::from_bytes(&Base64::decode_vec(fixture.trim()).unwrap()).unwrap();
661 let fx = effects.as_v1();
662
663 let owned = fx
664 .created()
665 .into_iter()
666 .chain(fx.mutated())
667 .chain(fx.unwrapped())
668 .map(|owned| owned.reference.object_id);
669 let removed = fx
670 .deleted()
671 .into_iter()
672 .chain(fx.unwrapped_then_deleted())
673 .chain(fx.wrapped())
674 .map(|object_ref| object_ref.object_id);
675 let reported: Vec<_> = owned.chain(removed).collect();
676
677 assert!(!reported.is_empty(), "the fixture must change objects");
678 let unique: std::collections::BTreeSet<_> = reported.iter().collect();
679 assert_eq!(unique.len(), reported.len(), "an object was reported twice");
680 assert_eq!(
681 unique,
682 fx.changed_objects
683 .iter()
684 .map(|changed| &changed.object_id)
685 .collect(),
686 );
687 }
688 }
689
690 #[test]
694 fn object_sets_agree_with_the_raw_effects() {
695 for fixture in [GENESIS_EFFECTS, SPONSOR_TX_EFFECTS] {
696 let effects: TransactionEffects =
697 bcs::from_bytes(&Base64::decode_vec(fixture.trim()).unwrap()).unwrap();
698 let fx = effects.as_v1();
699
700 for object_ref in fx.deleted().into_iter().chain(fx.unwrapped_then_deleted()) {
701 assert!(object_ref.digest.is_deleted());
702 assert_eq!(object_ref.version, fx.lamport_version);
703 }
704 for object_ref in fx.wrapped() {
705 assert!(object_ref.digest.is_wrapped());
706 }
707
708 let modified: Vec<_> = fx
709 .modified_at_versions()
710 .into_iter()
711 .map(|modified| modified.object_id)
712 .collect();
713 let old_metadata: Vec<_> = fx
714 .old_object_metadata()
715 .into_iter()
716 .map(|owned| owned.reference.object_id)
717 .collect();
718 assert_eq!(modified, old_metadata);
719 }
720 }
721
722 #[test]
725 fn gas_object_is_absent_without_a_gas_payment() {
726 let effects: TransactionEffects =
727 bcs::from_bytes(&Base64::decode_vec(SPONSOR_TX_EFFECTS.trim()).unwrap()).unwrap();
728 let sponsored = effects.as_v1();
729 let gas = sponsored.gas_object().expect("a sponsored tx pays gas");
730 assert_eq!(gas.reference.version, sponsored.lamport_version);
731 assert!(
732 sponsored
733 .mutated()
734 .iter()
735 .any(|owned| owned.reference == gas.reference),
736 "the gas object is reported as mutated"
737 );
738
739 let genesis: TransactionEffects =
740 bcs::from_bytes(&Base64::decode_vec(GENESIS_EFFECTS.trim()).unwrap()).unwrap();
741 assert!(genesis.as_v1().gas_object().is_none());
742 }
743}