1mod v1;
2mod v2;
3
4pub use v1::ModifiedAtVersion;
5pub use v1::ObjectReferenceWithOwner;
6pub use v1::TransactionEffectsV1;
7pub use v2::AccumulatorOperation;
8pub use v2::AccumulatorValue;
9pub use v2::AccumulatorWrite;
10pub use v2::ChangedObject;
11pub use v2::IdOperation;
12pub use v2::ObjectIn;
13pub use v2::ObjectOut;
14pub use v2::TransactionEffectsV2;
15pub use v2::UnchangedConsensusKind;
16pub use v2::UnchangedConsensusObject;
17
18use crate::Address;
19use crate::Digest;
20use crate::execution_status::ExecutionStatus;
21use crate::object::Owner;
22use crate::object::Version;
23
24#[derive(Eq, PartialEq, Clone, Debug)]
35#[cfg_attr(
36 feature = "serde",
37 derive(serde_derive::Serialize, serde_derive::Deserialize)
38)]
39#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
40pub enum TransactionEffects {
41 V1(Box<TransactionEffectsV1>),
42 V2(Box<TransactionEffectsV2>),
43}
44
45impl TransactionEffects {
46 pub fn status(&self) -> &ExecutionStatus {
48 match self {
49 TransactionEffects::V1(e) => e.status(),
50 TransactionEffects::V2(e) => e.status(),
51 }
52 }
53
54 pub fn epoch(&self) -> u64 {
56 match self {
57 TransactionEffects::V1(e) => e.epoch(),
58 TransactionEffects::V2(e) => e.epoch(),
59 }
60 }
61
62 pub fn gas_summary(&self) -> &crate::gas::GasCostSummary {
64 match self {
65 TransactionEffects::V1(e) => e.gas_summary(),
66 TransactionEffects::V2(e) => e.gas_summary(),
67 }
68 }
69
70 pub fn transaction_digest(&self) -> &Digest {
72 match self {
73 TransactionEffects::V1(e) => &e.transaction_digest,
74 TransactionEffects::V2(e) => &e.transaction_digest,
75 }
76 }
77
78 pub fn events_digest(&self) -> Option<&Digest> {
81 match self {
82 TransactionEffects::V1(e) => e.events_digest.as_ref(),
83 TransactionEffects::V2(e) => e.events_digest.as_ref(),
84 }
85 }
86
87 pub fn object_changes(&self) -> impl Iterator<Item = ObjectChange<'_>> + '_ {
109 ObjectChanges::new(self)
110 }
111}
112
113#[derive(Copy, Clone, Debug, PartialEq, Eq)]
130#[non_exhaustive]
131pub enum ObjectChange<'a> {
132 Created {
134 object_id: &'a Address,
135 output_version: Version,
136 output_digest: &'a Digest,
137 output_owner: &'a Owner,
138 },
139
140 Mutated {
143 object_id: &'a Address,
144 input_version: Option<Version>,
151 input_digest: Option<&'a Digest>,
153 input_owner: Option<&'a Owner>,
155 output_version: Version,
156 output_digest: &'a Digest,
157 output_owner: &'a Owner,
158 },
159
160 Unwrapped {
163 object_id: &'a Address,
164 output_version: Version,
165 output_digest: &'a Digest,
166 output_owner: &'a Owner,
167 },
168
169 Deleted {
171 object_id: &'a Address,
172 input_version: Option<Version>,
175 input_digest: Option<&'a Digest>,
176 input_owner: Option<&'a Owner>,
177 },
178
179 Wrapped {
182 object_id: &'a Address,
183 input_version: Option<Version>,
186 input_digest: Option<&'a Digest>,
187 input_owner: Option<&'a Owner>,
188 },
189
190 UnwrappedThenDeleted { object_id: &'a Address },
193}
194
195impl<'a> ObjectChange<'a> {
196 pub fn object_id(&self) -> &'a Address {
198 match self {
199 Self::Created { object_id, .. }
200 | Self::Mutated { object_id, .. }
201 | Self::Unwrapped { object_id, .. }
202 | Self::Deleted { object_id, .. }
203 | Self::Wrapped { object_id, .. }
204 | Self::UnwrappedThenDeleted { object_id } => object_id,
205 }
206 }
207}
208
209enum ObjectChanges<'a> {
217 V1 {
218 modified_at: &'a [ModifiedAtVersion],
226 created: std::slice::Iter<'a, ObjectReferenceWithOwner>,
227 mutated: std::slice::Iter<'a, ObjectReferenceWithOwner>,
228 unwrapped: std::slice::Iter<'a, ObjectReferenceWithOwner>,
229 deleted: std::slice::Iter<'a, crate::ObjectReference>,
230 unwrapped_then_deleted: std::slice::Iter<'a, crate::ObjectReference>,
231 wrapped: std::slice::Iter<'a, crate::ObjectReference>,
232 },
233 V2 {
234 lamport_version: Version,
235 inner: std::slice::Iter<'a, ChangedObject>,
236 },
237}
238
239fn find_modified_at(modified_at: &[ModifiedAtVersion], id: &Address) -> Option<Version> {
241 modified_at
242 .iter()
243 .find(|m| &m.object_id == id)
244 .map(|m| m.version)
245}
246
247impl<'a> ObjectChanges<'a> {
248 fn new(effects: &'a TransactionEffects) -> Self {
249 match effects {
250 TransactionEffects::V1(e) => Self::V1 {
251 modified_at: &e.modified_at_versions,
252 created: e.created.iter(),
253 mutated: e.mutated.iter(),
254 unwrapped: e.unwrapped.iter(),
255 deleted: e.deleted.iter(),
256 unwrapped_then_deleted: e.unwrapped_then_deleted.iter(),
257 wrapped: e.wrapped.iter(),
258 },
259 TransactionEffects::V2(e) => Self::V2 {
260 lamport_version: e.lamport_version,
261 inner: e.changed_objects.iter(),
262 },
263 }
264 }
265}
266
267impl<'a> Iterator for ObjectChanges<'a> {
268 type Item = ObjectChange<'a>;
269
270 fn next(&mut self) -> Option<Self::Item> {
271 match self {
272 Self::V1 {
273 modified_at,
274 created,
275 mutated,
276 unwrapped,
277 deleted,
278 unwrapped_then_deleted,
279 wrapped,
280 } => {
281 if let Some(o) = created.next() {
282 return Some(ObjectChange::Created {
283 object_id: o.reference.object_id(),
284 output_version: o.reference.version(),
285 output_digest: o.reference.digest(),
286 output_owner: &o.owner,
287 });
288 }
289 if let Some(o) = mutated.next() {
290 let object_id = o.reference.object_id();
291 return Some(ObjectChange::Mutated {
292 object_id,
293 input_version: find_modified_at(modified_at, object_id),
294 input_digest: None,
295 input_owner: None,
296 output_version: o.reference.version(),
297 output_digest: o.reference.digest(),
298 output_owner: &o.owner,
299 });
300 }
301 if let Some(o) = unwrapped.next() {
302 return Some(ObjectChange::Unwrapped {
303 object_id: o.reference.object_id(),
304 output_version: o.reference.version(),
305 output_digest: o.reference.digest(),
306 output_owner: &o.owner,
307 });
308 }
309 if let Some(r) = deleted.next() {
310 let object_id = r.object_id();
311 return Some(ObjectChange::Deleted {
312 object_id,
313 input_version: find_modified_at(modified_at, object_id),
314 input_digest: None,
315 input_owner: None,
316 });
317 }
318 if let Some(r) = unwrapped_then_deleted.next() {
319 return Some(ObjectChange::UnwrappedThenDeleted {
320 object_id: r.object_id(),
321 });
322 }
323 if let Some(r) = wrapped.next() {
324 let object_id = r.object_id();
325 return Some(ObjectChange::Wrapped {
326 object_id,
327 input_version: find_modified_at(modified_at, object_id),
328 input_digest: None,
329 input_owner: None,
330 });
331 }
332 None
333 }
334 Self::V2 {
335 lamport_version,
336 inner,
337 } => {
338 for changed in inner.by_ref() {
339 if let Some(change) = v2_object_change(changed, *lamport_version) {
340 return Some(change);
341 }
342 }
343 None
344 }
345 }
346 }
347}
348
349static IMMUTABLE_OWNER: Owner = Owner::Immutable;
354
355fn v2_object_change<'a>(
361 changed: &'a ChangedObject,
362 lamport_version: Version,
363) -> Option<ObjectChange<'a>> {
364 let object_id = &changed.object_id;
365 match (
366 &changed.input_state,
367 &changed.output_state,
368 changed.id_operation,
369 ) {
370 (ObjectIn::NotExist, ObjectOut::ObjectWrite { digest, owner }, IdOperation::Created) => {
372 Some(ObjectChange::Created {
373 object_id,
374 output_version: lamport_version,
375 output_digest: digest,
376 output_owner: owner,
377 })
378 }
379
380 (ObjectIn::NotExist, ObjectOut::PackageWrite { version, digest }, IdOperation::Created) => {
384 Some(ObjectChange::Created {
385 object_id,
386 output_version: *version,
387 output_digest: digest,
388 output_owner: &IMMUTABLE_OWNER,
389 })
390 }
391
392 (
394 ObjectIn::Exist {
395 version: input_version,
396 digest: input_digest,
397 owner: input_owner,
398 },
399 ObjectOut::ObjectWrite { digest, owner },
400 IdOperation::None,
401 ) => Some(ObjectChange::Mutated {
402 object_id,
403 input_version: Some(*input_version),
404 input_digest: Some(input_digest),
405 input_owner: Some(input_owner),
406 output_version: lamport_version,
407 output_digest: digest,
408 output_owner: owner,
409 }),
410
411 (
415 ObjectIn::Exist {
416 version: input_version,
417 digest: input_digest,
418 owner: input_owner,
419 },
420 ObjectOut::PackageWrite { version, digest },
421 IdOperation::None,
422 ) => Some(ObjectChange::Mutated {
423 object_id,
424 input_version: Some(*input_version),
425 input_digest: Some(input_digest),
426 input_owner: Some(input_owner),
427 output_version: *version,
428 output_digest: digest,
429 output_owner: &IMMUTABLE_OWNER,
430 }),
431
432 (ObjectIn::NotExist, ObjectOut::ObjectWrite { digest, owner }, IdOperation::None) => {
434 Some(ObjectChange::Unwrapped {
435 object_id,
436 output_version: lamport_version,
437 output_digest: digest,
438 output_owner: owner,
439 })
440 }
441
442 (
444 ObjectIn::Exist {
445 version: input_version,
446 digest: input_digest,
447 owner: input_owner,
448 },
449 ObjectOut::NotExist,
450 IdOperation::Deleted,
451 ) => Some(ObjectChange::Deleted {
452 object_id,
453 input_version: Some(*input_version),
454 input_digest: Some(input_digest),
455 input_owner: Some(input_owner),
456 }),
457
458 (ObjectIn::NotExist, ObjectOut::NotExist, IdOperation::Deleted) => {
460 Some(ObjectChange::UnwrappedThenDeleted { object_id })
461 }
462
463 (
465 ObjectIn::Exist {
466 version: input_version,
467 digest: input_digest,
468 owner: input_owner,
469 },
470 ObjectOut::NotExist,
471 IdOperation::None,
472 ) => Some(ObjectChange::Wrapped {
473 object_id,
474 input_version: Some(*input_version),
475 input_digest: Some(input_digest),
476 input_owner: Some(input_owner),
477 }),
478
479 (_, ObjectOut::AccumulatorWrite(_), _) => None,
483
484 _ => None,
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::ObjectChange;
493 use super::ObjectIn;
494 use super::ObjectOut;
495 use super::TransactionEffects;
496
497 use base64ct::Base64;
498 use base64ct::Encoding;
499
500 #[cfg(target_arch = "wasm32")]
501 use wasm_bindgen_test::wasm_bindgen_test as test;
502
503 const GENESIS_EFFECTS: &str = include_str!("fixtures/genesis-transaction-effects");
504 const PYTH_WORMHOLE_V2: &str = include_str!("fixtures/pyth-wormhole-v2");
505
506 fn decode(fixture: &str) -> TransactionEffects {
507 let bytes = Base64::decode_vec(fixture.trim()).unwrap();
508 bcs::from_bytes(&bytes).unwrap()
509 }
510
511 #[test]
512 fn effects_fixtures() {
513 for fixture in [GENESIS_EFFECTS, PYTH_WORMHOLE_V2] {
514 let bytes = Base64::decode_vec(fixture.trim()).unwrap();
515 let fx: TransactionEffects = bcs::from_bytes(&bytes).unwrap();
516 assert_eq!(bcs::to_bytes(&fx).unwrap(), bytes);
517
518 let json = serde_json::to_string_pretty(&fx).unwrap();
519 println!("{json}");
520 assert_eq!(fx, serde_json::from_str(&json).unwrap());
521 }
522 }
523
524 #[test]
528 fn v1_object_changes_match_fields() {
529 let fx = decode(GENESIS_EFFECTS);
530 let TransactionEffects::V1(v1) = &fx else {
531 panic!("expected V1 fixture");
532 };
533
534 assert_eq!(fx.transaction_digest(), &v1.transaction_digest);
535 assert_eq!(fx.events_digest(), v1.events_digest.as_ref());
536
537 let changes: Vec<ObjectChange<'_>> = fx.object_changes().collect();
538 let expected_total = v1.created.len()
539 + v1.mutated.len()
540 + v1.unwrapped.len()
541 + v1.deleted.len()
542 + v1.unwrapped_then_deleted.len()
543 + v1.wrapped.len();
544 assert_eq!(changes.len(), expected_total);
545
546 for (i, change) in changes.iter().copied().take(v1.created.len()).enumerate() {
548 let expected = &v1.created[i];
549 match change {
550 ObjectChange::Created {
551 object_id,
552 output_version,
553 output_digest,
554 output_owner,
555 } => {
556 assert_eq!(object_id, expected.reference.object_id());
557 assert_eq!(output_version, expected.reference.version());
558 assert_eq!(output_digest, expected.reference.digest());
559 assert_eq!(*output_owner, expected.owner);
560 }
561 other => panic!("expected Created at index {i}, got {other:?}"),
562 }
563 }
564
565 for change in changes.iter().copied() {
567 match change {
568 ObjectChange::Mutated {
569 input_digest,
570 input_owner,
571 ..
572 }
573 | ObjectChange::Deleted {
574 input_digest,
575 input_owner,
576 ..
577 }
578 | ObjectChange::Wrapped {
579 input_digest,
580 input_owner,
581 ..
582 } => {
583 assert!(input_digest.is_none(), "V1 has no input digest");
584 assert!(input_owner.is_none(), "V1 has no input owner");
585 }
586 _ => {}
587 }
588 }
589 }
590
591 #[test]
595 fn v2_object_changes_match_fields() {
596 let fx = decode(PYTH_WORMHOLE_V2);
597 let TransactionEffects::V2(v2) = &fx else {
598 panic!("expected V2 fixture");
599 };
600
601 assert_eq!(fx.transaction_digest(), &v2.transaction_digest);
602 assert_eq!(fx.events_digest(), v2.events_digest.as_ref());
603
604 let changes: Vec<ObjectChange<'_>> = fx.object_changes().collect();
605 let expected_count = v2
607 .changed_objects
608 .iter()
609 .filter(|c| !matches!(c.output_state, ObjectOut::AccumulatorWrite(_)))
610 .count();
611 assert_eq!(changes.len(), expected_count);
612
613 let mut saw_object_write = false;
614 let raws = v2
615 .changed_objects
616 .iter()
617 .filter(|c| !matches!(c.output_state, ObjectOut::AccumulatorWrite(_)));
618 for (raw, change) in raws.zip(changes.iter().copied()) {
619 match (&raw.input_state, &raw.output_state) {
620 (
621 ObjectIn::Exist {
622 version,
623 digest,
624 owner,
625 },
626 ObjectOut::ObjectWrite { .. },
627 ) => {
628 saw_object_write = true;
629 let ObjectChange::Mutated {
630 input_version,
631 input_digest,
632 input_owner,
633 output_version,
634 ..
635 } = change
636 else {
637 panic!("expected Mutated, got {change:?}");
638 };
639 assert_eq!(input_version, Some(*version));
640 assert_eq!(input_digest, Some(digest));
641 assert_eq!(input_owner, Some(owner));
642 assert_eq!(output_version, v2.lamport_version);
643 }
644 (ObjectIn::NotExist, ObjectOut::ObjectWrite { .. }) => {
645 saw_object_write = true;
646 match change {
647 ObjectChange::Created { output_version, .. }
648 | ObjectChange::Unwrapped { output_version, .. } => {
649 assert_eq!(output_version, v2.lamport_version);
650 }
651 other => panic!("expected Created or Unwrapped, got {other:?}"),
652 }
653 }
654 _ => {}
655 }
656 }
657 assert!(saw_object_write, "fixture should exercise ObjectWrite");
658 }
659
660 #[test]
662 fn object_change_object_id_covers_every_variant() {
663 let v1 = decode(GENESIS_EFFECTS);
664 let v2 = decode(PYTH_WORMHOLE_V2);
665
666 for change in v1.object_changes().chain(v2.object_changes()) {
667 let id_from_accessor = change.object_id();
668 let id_from_match = match change {
669 ObjectChange::Created { object_id, .. }
670 | ObjectChange::Mutated { object_id, .. }
671 | ObjectChange::Unwrapped { object_id, .. }
672 | ObjectChange::Deleted { object_id, .. }
673 | ObjectChange::Wrapped { object_id, .. }
674 | ObjectChange::UnwrappedThenDeleted { object_id } => object_id,
675 };
676 assert_eq!(id_from_accessor, id_from_match);
677 }
678 }
679}