Skip to main content

iota_sdk_types/effects/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5mod v1;
6
7pub use v1::{
8    ChangedObject, ObjectIn, ObjectOut, TransactionEffectsV1, UnchangedSharedKind,
9    UnchangedSharedObject,
10};
11
12use crate::{ObjectDigest, ObjectId, ObjectReference, ObjectVersion, Version};
13
14/// The output or effects of executing a transaction
15///
16/// # BCS
17///
18/// The BCS serialized form for this type is defined by the following ABNF:
19///
20/// ```text
21/// transaction-effects = %d00 transaction-effects-v1   ; V1
22/// ```
23#[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/// A shared object an executed transaction took as input.
46///
47/// Not a wire type: this is the effects' view of the shared objects a
48/// transaction was sequenced against, drawn from both the objects it changed
49/// and those it left unchanged.
50#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub enum InputSharedObject {
52    /// Taken mutably, and written back by the transaction.
53    Mutate(ObjectReference),
54    /// Read without being mutated.
55    ReadOnly(ObjectReference),
56    /// Read, but already deleted by an earlier transaction.
57    ReadDeleted(ObjectVersion),
58    /// Taken mutably, but already deleted by an earlier transaction.
59    MutateDeleted(ObjectVersion),
60    /// Taken by a transaction that consensus canceled; the version carries the
61    /// cancellation reason.
62    Canceled(ObjectVersion),
63}
64
65impl InputSharedObject {
66    /// The object's reference. A shared object that no longer exists is
67    /// reported at the version it was last known at, with the tombstone digest
68    /// for why it is gone.
69    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/// How an object came to be in the store after a transaction wrote it.
102///
103/// Not a wire type: this tags an object reported by
104/// [`TransactionEffectsV1::all_changed_objects`] with which of the object sets
105/// it came from.
106#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
107pub enum WriteKind {
108    /// The object existed already and the transaction changed its contents.
109    Mutate,
110    /// The transaction created the object.
111    Create,
112    /// The object was wrapped inside another object, and the transaction
113    /// restored it to the store.
114    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/// Why an object is no longer in the store after a transaction.
129///
130/// Not a wire type: this tags an object reported by
131/// [`TransactionEffectsV1::all_removed_objects`] with which of the object sets
132/// it came from.
133#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub enum ObjectRemoveKind {
135    /// The transaction deleted the object.
136    Delete,
137    /// The transaction wrapped the object inside another one.
138    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/// What an executed transaction did to one object, with the version and digest
152/// each side is at resolved.
153///
154/// Not a wire type: this is [`ChangedObject`] with the versions filled in,
155/// since a written object's version is the transaction's rather than one the
156/// entry carries. A `None` version means the object did not exist on that side.
157#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub struct ObjectChange {
159    /// The object's id.
160    pub object_id: ObjectId,
161    /// The version the object was at before the transaction, if it existed.
162    pub input_version: Option<Version>,
163    /// The digest the object had before the transaction, if it existed.
164    pub input_digest: Option<ObjectDigest>,
165    /// The version the object is at after the transaction, if it still exists.
166    pub output_version: Option<Version>,
167    /// The digest the object has after the transaction, if it still exists.
168    pub output_digest: Option<ObjectDigest>,
169    /// Whether the transaction created or deleted the object's id.
170    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/// Defines what happened to an ObjectId during execution
188///
189/// # BCS
190///
191/// The BCS serialized form for this type is defined by the following ABNF:
192///
193/// ```text
194/// id-operation = %d00   ; None
195///              / %d01   ; Created
196///              / %d02   ; Deleted
197/// ```
198#[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    // The files contain the bas64 encoded raw effects of transactions
226    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    /// Shared inputs are drawn from both the objects a transaction changed and
243    /// those it left unchanged, and a per-epoch config object is not one.
244    #[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                // An owned input is not a shared input.
286                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        // A shared object that no longer exists is reported with the tombstone
319        // digest saying why.
320        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    /// A written object takes the version the transaction assigned, while a
335    /// package keeps the version it was published or upgraded at. The fixtures
336    /// cannot tell these apart, since they publish at their lamport version.
337    #[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        // The same distinction reaches the object sets.
388        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    /// Each changed object is reported once, with the version and digest of
399    /// whichever sides it existed on.
400    #[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                // A written object takes the version this transaction assigned;
425                // a package keeps the version it was published or upgraded at.
426                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    /// A package write is only ever a create or a mutate: it is not reported as
442    /// an unwrap, and never as the gas object.
443    #[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            // The one entry stands in as the gas object, which it cannot be.
457            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                // Neither created nor deleted, which is what would otherwise
469                // read as an unwrap.
470                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    /// Every object set and both tagged unions, over effects that contain one
481    /// object of each kind. The fixtures hold only created and mutated objects,
482    /// so this is what covers the other four.
483    #[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        // The tags, and the order the sets are drawn in.
565        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        // An object unwrapped and then deleted was never in the store, so it is
579        // not a removal.
580        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        // Removals carry the tombstone digest for why the object is gone, at the
593        // version this transaction assigned.
594        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    /// The tagged unions are exactly the sets they are drawn from, so nothing
607    /// is dropped or double-counted, and each object carries the right tag.
608    #[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            // An object unwrapped and then deleted was never in the store, so it
647            // is not a removal.
648            for reference in fx.unwrapped_then_deleted() {
649                assert!(!removed.iter().any(|(removed, _)| *removed == reference));
650            }
651        }
652    }
653
654    /// Every changed object falls into exactly one of the reported sets, so the
655    /// sets partition `changed_objects` and never report an object twice.
656    #[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    /// Output objects other than packages are reported at the lamport version,
691    /// and the objects reported as modified are exactly those with a
692    /// pre-transaction state.
693    #[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    /// A transaction that pays for itself reports its gas object; the genesis
723    /// transaction is a system transaction, pays no gas, and reports none.
724    #[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}