mod v1;
mod v2;
pub use v1::ModifiedAtVersion;
pub use v1::ObjectReferenceWithOwner;
pub use v1::TransactionEffectsV1;
pub use v2::AccumulatorOperation;
pub use v2::AccumulatorValue;
pub use v2::AccumulatorWrite;
pub use v2::ChangedObject;
pub use v2::IdOperation;
pub use v2::ObjectIn;
pub use v2::ObjectOut;
pub use v2::TransactionEffectsV2;
pub use v2::UnchangedConsensusKind;
pub use v2::UnchangedConsensusObject;
use crate::Address;
use crate::Digest;
use crate::execution_status::ExecutionStatus;
use crate::object::Owner;
use crate::object::Version;
#[derive(Eq, PartialEq, Clone, Debug)]
#[cfg_attr(
feature = "serde",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
pub enum TransactionEffects {
V1(Box<TransactionEffectsV1>),
V2(Box<TransactionEffectsV2>),
}
impl TransactionEffects {
pub fn status(&self) -> &ExecutionStatus {
match self {
TransactionEffects::V1(e) => e.status(),
TransactionEffects::V2(e) => e.status(),
}
}
pub fn epoch(&self) -> u64 {
match self {
TransactionEffects::V1(e) => e.epoch(),
TransactionEffects::V2(e) => e.epoch(),
}
}
pub fn gas_summary(&self) -> &crate::gas::GasCostSummary {
match self {
TransactionEffects::V1(e) => e.gas_summary(),
TransactionEffects::V2(e) => e.gas_summary(),
}
}
pub fn transaction_digest(&self) -> &Digest {
match self {
TransactionEffects::V1(e) => &e.transaction_digest,
TransactionEffects::V2(e) => &e.transaction_digest,
}
}
pub fn events_digest(&self) -> Option<&Digest> {
match self {
TransactionEffects::V1(e) => e.events_digest.as_ref(),
TransactionEffects::V2(e) => e.events_digest.as_ref(),
}
}
pub fn object_changes(&self) -> impl Iterator<Item = ObjectChange<'_>> + '_ {
ObjectChanges::new(self)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ObjectChange<'a> {
Created {
object_id: &'a Address,
output_version: Version,
output_digest: &'a Digest,
output_owner: &'a Owner,
},
Mutated {
object_id: &'a Address,
input_version: Option<Version>,
input_digest: Option<&'a Digest>,
input_owner: Option<&'a Owner>,
output_version: Version,
output_digest: &'a Digest,
output_owner: &'a Owner,
},
Unwrapped {
object_id: &'a Address,
output_version: Version,
output_digest: &'a Digest,
output_owner: &'a Owner,
},
Deleted {
object_id: &'a Address,
input_version: Option<Version>,
input_digest: Option<&'a Digest>,
input_owner: Option<&'a Owner>,
},
Wrapped {
object_id: &'a Address,
input_version: Option<Version>,
input_digest: Option<&'a Digest>,
input_owner: Option<&'a Owner>,
},
UnwrappedThenDeleted { object_id: &'a Address },
}
impl<'a> ObjectChange<'a> {
pub fn object_id(&self) -> &'a Address {
match self {
Self::Created { object_id, .. }
| Self::Mutated { object_id, .. }
| Self::Unwrapped { object_id, .. }
| Self::Deleted { object_id, .. }
| Self::Wrapped { object_id, .. }
| Self::UnwrappedThenDeleted { object_id } => object_id,
}
}
}
enum ObjectChanges<'a> {
V1 {
modified_at: &'a [ModifiedAtVersion],
created: std::slice::Iter<'a, ObjectReferenceWithOwner>,
mutated: std::slice::Iter<'a, ObjectReferenceWithOwner>,
unwrapped: std::slice::Iter<'a, ObjectReferenceWithOwner>,
deleted: std::slice::Iter<'a, crate::ObjectReference>,
unwrapped_then_deleted: std::slice::Iter<'a, crate::ObjectReference>,
wrapped: std::slice::Iter<'a, crate::ObjectReference>,
},
V2 {
lamport_version: Version,
inner: std::slice::Iter<'a, ChangedObject>,
},
}
fn find_modified_at(modified_at: &[ModifiedAtVersion], id: &Address) -> Option<Version> {
modified_at
.iter()
.find(|m| &m.object_id == id)
.map(|m| m.version)
}
impl<'a> ObjectChanges<'a> {
fn new(effects: &'a TransactionEffects) -> Self {
match effects {
TransactionEffects::V1(e) => Self::V1 {
modified_at: &e.modified_at_versions,
created: e.created.iter(),
mutated: e.mutated.iter(),
unwrapped: e.unwrapped.iter(),
deleted: e.deleted.iter(),
unwrapped_then_deleted: e.unwrapped_then_deleted.iter(),
wrapped: e.wrapped.iter(),
},
TransactionEffects::V2(e) => Self::V2 {
lamport_version: e.lamport_version,
inner: e.changed_objects.iter(),
},
}
}
}
impl<'a> Iterator for ObjectChanges<'a> {
type Item = ObjectChange<'a>;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::V1 {
modified_at,
created,
mutated,
unwrapped,
deleted,
unwrapped_then_deleted,
wrapped,
} => {
if let Some(o) = created.next() {
return Some(ObjectChange::Created {
object_id: o.reference.object_id(),
output_version: o.reference.version(),
output_digest: o.reference.digest(),
output_owner: &o.owner,
});
}
if let Some(o) = mutated.next() {
let object_id = o.reference.object_id();
return Some(ObjectChange::Mutated {
object_id,
input_version: find_modified_at(modified_at, object_id),
input_digest: None,
input_owner: None,
output_version: o.reference.version(),
output_digest: o.reference.digest(),
output_owner: &o.owner,
});
}
if let Some(o) = unwrapped.next() {
return Some(ObjectChange::Unwrapped {
object_id: o.reference.object_id(),
output_version: o.reference.version(),
output_digest: o.reference.digest(),
output_owner: &o.owner,
});
}
if let Some(r) = deleted.next() {
let object_id = r.object_id();
return Some(ObjectChange::Deleted {
object_id,
input_version: find_modified_at(modified_at, object_id),
input_digest: None,
input_owner: None,
});
}
if let Some(r) = unwrapped_then_deleted.next() {
return Some(ObjectChange::UnwrappedThenDeleted {
object_id: r.object_id(),
});
}
if let Some(r) = wrapped.next() {
let object_id = r.object_id();
return Some(ObjectChange::Wrapped {
object_id,
input_version: find_modified_at(modified_at, object_id),
input_digest: None,
input_owner: None,
});
}
None
}
Self::V2 {
lamport_version,
inner,
} => {
for changed in inner.by_ref() {
if let Some(change) = v2_object_change(changed, *lamport_version) {
return Some(change);
}
}
None
}
}
}
}
static IMMUTABLE_OWNER: Owner = Owner::Immutable;
fn v2_object_change<'a>(
changed: &'a ChangedObject,
lamport_version: Version,
) -> Option<ObjectChange<'a>> {
let object_id = &changed.object_id;
match (
&changed.input_state,
&changed.output_state,
changed.id_operation,
) {
(ObjectIn::NotExist, ObjectOut::ObjectWrite { digest, owner }, IdOperation::Created) => {
Some(ObjectChange::Created {
object_id,
output_version: lamport_version,
output_digest: digest,
output_owner: owner,
})
}
(ObjectIn::NotExist, ObjectOut::PackageWrite { version, digest }, IdOperation::Created) => {
Some(ObjectChange::Created {
object_id,
output_version: *version,
output_digest: digest,
output_owner: &IMMUTABLE_OWNER,
})
}
(
ObjectIn::Exist {
version: input_version,
digest: input_digest,
owner: input_owner,
},
ObjectOut::ObjectWrite { digest, owner },
IdOperation::None,
) => Some(ObjectChange::Mutated {
object_id,
input_version: Some(*input_version),
input_digest: Some(input_digest),
input_owner: Some(input_owner),
output_version: lamport_version,
output_digest: digest,
output_owner: owner,
}),
(
ObjectIn::Exist {
version: input_version,
digest: input_digest,
owner: input_owner,
},
ObjectOut::PackageWrite { version, digest },
IdOperation::None,
) => Some(ObjectChange::Mutated {
object_id,
input_version: Some(*input_version),
input_digest: Some(input_digest),
input_owner: Some(input_owner),
output_version: *version,
output_digest: digest,
output_owner: &IMMUTABLE_OWNER,
}),
(ObjectIn::NotExist, ObjectOut::ObjectWrite { digest, owner }, IdOperation::None) => {
Some(ObjectChange::Unwrapped {
object_id,
output_version: lamport_version,
output_digest: digest,
output_owner: owner,
})
}
(
ObjectIn::Exist {
version: input_version,
digest: input_digest,
owner: input_owner,
},
ObjectOut::NotExist,
IdOperation::Deleted,
) => Some(ObjectChange::Deleted {
object_id,
input_version: Some(*input_version),
input_digest: Some(input_digest),
input_owner: Some(input_owner),
}),
(ObjectIn::NotExist, ObjectOut::NotExist, IdOperation::Deleted) => {
Some(ObjectChange::UnwrappedThenDeleted { object_id })
}
(
ObjectIn::Exist {
version: input_version,
digest: input_digest,
owner: input_owner,
},
ObjectOut::NotExist,
IdOperation::None,
) => Some(ObjectChange::Wrapped {
object_id,
input_version: Some(*input_version),
input_digest: Some(input_digest),
input_owner: Some(input_owner),
}),
(_, ObjectOut::AccumulatorWrite(_), _) => None,
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::ObjectChange;
use super::ObjectIn;
use super::ObjectOut;
use super::TransactionEffects;
use base64ct::Base64;
use base64ct::Encoding;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
const GENESIS_EFFECTS: &str = include_str!("fixtures/genesis-transaction-effects");
const PYTH_WORMHOLE_V2: &str = include_str!("fixtures/pyth-wormhole-v2");
fn decode(fixture: &str) -> TransactionEffects {
let bytes = Base64::decode_vec(fixture.trim()).unwrap();
bcs::from_bytes(&bytes).unwrap()
}
#[test]
fn effects_fixtures() {
for fixture in [GENESIS_EFFECTS, PYTH_WORMHOLE_V2] {
let bytes = Base64::decode_vec(fixture.trim()).unwrap();
let fx: TransactionEffects = bcs::from_bytes(&bytes).unwrap();
assert_eq!(bcs::to_bytes(&fx).unwrap(), bytes);
let json = serde_json::to_string_pretty(&fx).unwrap();
println!("{json}");
assert_eq!(fx, serde_json::from_str(&json).unwrap());
}
}
#[test]
fn v1_object_changes_match_fields() {
let fx = decode(GENESIS_EFFECTS);
let TransactionEffects::V1(v1) = &fx else {
panic!("expected V1 fixture");
};
assert_eq!(fx.transaction_digest(), &v1.transaction_digest);
assert_eq!(fx.events_digest(), v1.events_digest.as_ref());
let changes: Vec<ObjectChange<'_>> = fx.object_changes().collect();
let expected_total = v1.created.len()
+ v1.mutated.len()
+ v1.unwrapped.len()
+ v1.deleted.len()
+ v1.unwrapped_then_deleted.len()
+ v1.wrapped.len();
assert_eq!(changes.len(), expected_total);
for (i, change) in changes.iter().copied().take(v1.created.len()).enumerate() {
let expected = &v1.created[i];
match change {
ObjectChange::Created {
object_id,
output_version,
output_digest,
output_owner,
} => {
assert_eq!(object_id, expected.reference.object_id());
assert_eq!(output_version, expected.reference.version());
assert_eq!(output_digest, expected.reference.digest());
assert_eq!(*output_owner, expected.owner);
}
other => panic!("expected Created at index {i}, got {other:?}"),
}
}
for change in changes.iter().copied() {
match change {
ObjectChange::Mutated {
input_digest,
input_owner,
..
}
| ObjectChange::Deleted {
input_digest,
input_owner,
..
}
| ObjectChange::Wrapped {
input_digest,
input_owner,
..
} => {
assert!(input_digest.is_none(), "V1 has no input digest");
assert!(input_owner.is_none(), "V1 has no input owner");
}
_ => {}
}
}
}
#[test]
fn v2_object_changes_match_fields() {
let fx = decode(PYTH_WORMHOLE_V2);
let TransactionEffects::V2(v2) = &fx else {
panic!("expected V2 fixture");
};
assert_eq!(fx.transaction_digest(), &v2.transaction_digest);
assert_eq!(fx.events_digest(), v2.events_digest.as_ref());
let changes: Vec<ObjectChange<'_>> = fx.object_changes().collect();
let expected_count = v2
.changed_objects
.iter()
.filter(|c| !matches!(c.output_state, ObjectOut::AccumulatorWrite(_)))
.count();
assert_eq!(changes.len(), expected_count);
let mut saw_object_write = false;
let raws = v2
.changed_objects
.iter()
.filter(|c| !matches!(c.output_state, ObjectOut::AccumulatorWrite(_)));
for (raw, change) in raws.zip(changes.iter().copied()) {
match (&raw.input_state, &raw.output_state) {
(
ObjectIn::Exist {
version,
digest,
owner,
},
ObjectOut::ObjectWrite { .. },
) => {
saw_object_write = true;
let ObjectChange::Mutated {
input_version,
input_digest,
input_owner,
output_version,
..
} = change
else {
panic!("expected Mutated, got {change:?}");
};
assert_eq!(input_version, Some(*version));
assert_eq!(input_digest, Some(digest));
assert_eq!(input_owner, Some(owner));
assert_eq!(output_version, v2.lamport_version);
}
(ObjectIn::NotExist, ObjectOut::ObjectWrite { .. }) => {
saw_object_write = true;
match change {
ObjectChange::Created { output_version, .. }
| ObjectChange::Unwrapped { output_version, .. } => {
assert_eq!(output_version, v2.lamport_version);
}
other => panic!("expected Created or Unwrapped, got {other:?}"),
}
}
_ => {}
}
}
assert!(saw_object_write, "fixture should exercise ObjectWrite");
}
#[test]
fn object_change_object_id_covers_every_variant() {
let v1 = decode(GENESIS_EFFECTS);
let v2 = decode(PYTH_WORMHOLE_V2);
for change in v1.object_changes().chain(v2.object_changes()) {
let id_from_accessor = change.object_id();
let id_from_match = match change {
ObjectChange::Created { object_id, .. }
| ObjectChange::Mutated { object_id, .. }
| ObjectChange::Unwrapped { object_id, .. }
| ObjectChange::Deleted { object_id, .. }
| ObjectChange::Wrapped { object_id, .. }
| ObjectChange::UnwrappedThenDeleted { object_id } => object_id,
};
assert_eq!(id_from_accessor, id_from_match);
}
}
}