iota_types/
test_checkpoint_data_builder.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::{BTreeMap, BTreeSet, HashMap};
6
7use iota_protocol_config::ProtocolConfig;
8use move_core_types::{
9    ident_str,
10    language_storage::{StructTag, TypeTag},
11};
12use tap::Pipe;
13
14use crate::{
15    IOTA_SYSTEM_ADDRESS,
16    base_types::{
17        ExecutionDigests, IotaAddress, ObjectID, ObjectRef, SequenceNumber, dbg_addr,
18        random_object_ref,
19    },
20    committee::Committee,
21    digests::TransactionDigest,
22    effects::{TestEffectsBuilder, TransactionEffectsAPI, TransactionEvents},
23    event::{Event, SystemEpochInfoEventV2},
24    full_checkpoint_content::{CheckpointData, CheckpointTransaction},
25    gas_coin::GAS,
26    message_envelope::Message,
27    messages_checkpoint::{
28        CertifiedCheckpointSummary, CheckpointContents, CheckpointSummary, EndOfEpochData,
29    },
30    object::{GAS_VALUE_FOR_TESTING, MoveObject, Object, Owner},
31    programmable_transaction_builder::ProgrammableTransactionBuilder,
32    transaction::{
33        EndOfEpochTransactionKind, SenderSignedData, Transaction, TransactionData, TransactionKind,
34    },
35};
36
37/// A builder for creating test checkpoint data.
38/// Once initialized, the builder can be used to build multiple checkpoints.
39/// Call `start_transaction` to begin creating a new transaction.
40/// Call `finish_transaction` to complete the current transaction and add it to
41/// the current checkpoint. After all transactions are added, call
42/// `build_checkpoint` to get the final checkpoint data. This will also
43/// increment the stored checkpoint sequence number. Start the above process
44/// again to build the next checkpoint. NOTE: The generated checkpoint data is
45/// not guaranteed to be semantically valid or consistent. For instance, all
46/// object digests will be randomly set. It focuses on providing a way to
47/// generate various shaped test data for testing purposes.
48/// If you need to test the validity of the checkpoint data, you should use
49/// Simulacrum instead.
50pub struct TestCheckpointDataBuilder {
51    /// Map of all live objects in the state.
52    live_objects: HashMap<ObjectID, Object>,
53    /// Map of all wrapped objects in the state.
54    wrapped_objects: HashMap<ObjectID, Object>,
55    /// A map from sender addresses to gas objects they own.
56    /// These are created automatically when a transaction is started.
57    /// Users of this builder should not need to worry about them.
58    gas_map: HashMap<IotaAddress, ObjectID>,
59
60    /// The current checkpoint builder.
61    /// It is initialized when the builder is created, and is reset when
62    /// `build_checkpoint` is called.
63    checkpoint_builder: CheckpointBuilder,
64}
65
66struct CheckpointBuilder {
67    /// Checkpoint number for the current checkpoint we are building.
68    checkpoint: u64,
69    /// Epoch number for the current checkpoint we are building.
70    epoch: u64,
71    /// Counter for the total number of transactions added to the builder.
72    network_total_transactions: u64,
73    /// Transactions that have been added to the current checkpoint.
74    transactions: Vec<CheckpointTransaction>,
75    /// The current transaction being built.
76    next_transaction: Option<TransactionBuilder>,
77}
78
79struct TransactionBuilder {
80    sender_idx: u8,
81    gas: ObjectRef,
82    move_calls: Vec<(ObjectID, &'static str, &'static str)>,
83    created_objects: BTreeMap<ObjectID, Object>,
84    mutated_objects: BTreeMap<ObjectID, Object>,
85    unwrapped_objects: BTreeSet<ObjectID>,
86    wrapped_objects: BTreeSet<ObjectID>,
87    deleted_objects: BTreeSet<ObjectID>,
88    events: Option<Vec<Event>>,
89}
90
91impl TransactionBuilder {
92    pub fn new(sender_idx: u8, gas: ObjectRef) -> Self {
93        Self {
94            sender_idx,
95            gas,
96            move_calls: vec![],
97            created_objects: BTreeMap::new(),
98            mutated_objects: BTreeMap::new(),
99            unwrapped_objects: BTreeSet::new(),
100            wrapped_objects: BTreeSet::new(),
101            deleted_objects: BTreeSet::new(),
102            events: None,
103        }
104    }
105}
106
107impl TestCheckpointDataBuilder {
108    pub fn new(checkpoint: u64) -> Self {
109        Self {
110            live_objects: HashMap::new(),
111            wrapped_objects: HashMap::new(),
112            gas_map: HashMap::new(),
113            checkpoint_builder: CheckpointBuilder {
114                checkpoint,
115                epoch: 0,
116                network_total_transactions: 0,
117                transactions: vec![],
118                next_transaction: None,
119            },
120        }
121    }
122
123    /// Set the epoch for the checkpoint.
124    pub fn with_epoch(mut self, epoch: u64) -> Self {
125        self.checkpoint_builder.epoch = epoch;
126        self
127    }
128
129    /// Start creating a new transaction.
130    /// `sender_idx` is a convenient representation of the sender's address.
131    /// A proper IotaAddress will be derived from it.
132    /// It will also create a gas object for the sender if it doesn't already
133    /// exist in the live object map. You do not need to create the gas
134    /// object yourself.
135    pub fn start_transaction(mut self, sender_idx: u8) -> Self {
136        assert!(self.checkpoint_builder.next_transaction.is_none());
137        let sender = Self::derive_address(sender_idx);
138        let gas_id = self.gas_map.entry(sender).or_insert_with(|| {
139            let gas = Object::with_owner_for_testing(sender);
140            let id = gas.id();
141            self.live_objects.insert(id, gas);
142            id
143        });
144        let gas_ref = self
145            .live_objects
146            .get(gas_id)
147            .cloned()
148            .unwrap()
149            .compute_object_reference();
150        self.checkpoint_builder.next_transaction =
151            Some(TransactionBuilder::new(sender_idx, gas_ref));
152        self
153    }
154
155    /// Create a new object in the transaction.
156    /// `object_idx` is a convenient representation of the object's ID.
157    /// The object will be created as a IOTA coin object, with default balance,
158    /// and the transaction sender as its owner.
159    pub fn create_owned_object(self, object_idx: u64) -> Self {
160        self.create_iota_object(object_idx, GAS_VALUE_FOR_TESTING)
161    }
162
163    /// Create a new shared object in the transaction.
164    /// `object_idx` is a convenient representation of the object's ID.
165    /// The object will be created as a IOTA coin object, with default balance,
166    /// and it is a shared object.
167    pub fn create_shared_object(self, object_idx: u64) -> Self {
168        self.create_coin_object_with_owner(
169            object_idx,
170            Owner::Shared {
171                initial_shared_version: SequenceNumber::MIN_VALID_INCL,
172            },
173            GAS_VALUE_FOR_TESTING,
174            GAS::type_tag(),
175        )
176    }
177
178    /// Create a new IOTA coin object in the transaction.
179    /// `object_idx` is a convenient representation of the object's ID.
180    /// `balance` is the amount of IOTA to be created.
181    pub fn create_iota_object(self, object_idx: u64, balance: u64) -> Self {
182        let sender_idx = self
183            .checkpoint_builder
184            .next_transaction
185            .as_ref()
186            .unwrap()
187            .sender_idx;
188        self.create_coin_object(object_idx, sender_idx, balance, GAS::type_tag())
189    }
190
191    /// Create a new coin object in the transaction.
192    /// `object_idx` is a convenient representation of the object's ID.
193    /// `owner_idx` is a convenient representation of the object's owner's
194    /// address. `balance` is the amount of IOTA to be created.
195    /// `coin_type` is the type of the coin to be created.
196    pub fn create_coin_object(
197        self,
198        object_idx: u64,
199        owner_idx: u8,
200        balance: u64,
201        coin_type: TypeTag,
202    ) -> Self {
203        self.create_coin_object_with_owner(
204            object_idx,
205            Owner::AddressOwner(Self::derive_address(owner_idx)),
206            balance,
207            coin_type,
208        )
209    }
210
211    fn create_coin_object_with_owner(
212        mut self,
213        object_idx: u64,
214        owner: Owner,
215        balance: u64,
216        coin_type: TypeTag,
217    ) -> Self {
218        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
219        let object_id = Self::derive_object_id(object_idx);
220        assert!(
221            !self.live_objects.contains_key(&object_id),
222            "Object already exists: {object_id}. Please use a different object index.",
223        );
224        let move_object = MoveObject::new_coin(
225            coin_type,
226            // version doesn't matter since we will set it to the lamport version when we finalize
227            // the transaction
228            SequenceNumber::MIN_VALID_INCL,
229            object_id,
230            balance,
231        );
232        let object = Object::new_move(move_object, owner, TransactionDigest::ZERO);
233        tx_builder.created_objects.insert(object_id, object);
234        self
235    }
236
237    /// Mutate an existing object in the transaction.
238    /// `object_idx` is a convenient representation of the object's ID.
239    pub fn mutate_object(mut self, object_idx: u64) -> Self {
240        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
241        let object_id = Self::derive_object_id(object_idx);
242        let object = self
243            .live_objects
244            .get(&object_id)
245            .cloned()
246            .expect("Mutating an object that doesn't exist");
247        tx_builder.mutated_objects.insert(object_id, object);
248        self
249    }
250
251    /// Transfer an existing object to a new owner.
252    /// `object_idx` is a convenient representation of the object's ID.
253    /// `recipient_idx` is a convenient representation of the recipient's
254    /// address.
255    pub fn transfer_object(self, object_idx: u64, recipient_idx: u8) -> Self {
256        self.change_object_owner(
257            object_idx,
258            Owner::AddressOwner(Self::derive_address(recipient_idx)),
259        )
260    }
261
262    /// Change the owner of an existing object.
263    /// `object_idx` is a convenient representation of the object's ID.
264    /// `owner` is the new owner of the object.
265    pub fn change_object_owner(mut self, object_idx: u64, owner: Owner) -> Self {
266        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
267        let object_id = Self::derive_object_id(object_idx);
268        let mut object = self.live_objects.get(&object_id).unwrap().clone();
269        object.owner = owner;
270        tx_builder.mutated_objects.insert(object_id, object);
271        self
272    }
273
274    /// Transfer part of an existing coin object's balance to a new owner.
275    /// `object_idx` is a convenient representation of the object's ID.
276    /// `new_object_idx` is a convenient representation of the new object's ID.
277    /// `recipient_idx` is a convenient representation of the recipient's
278    /// address. `amount` is the amount of balance to be transferred.
279    pub fn transfer_coin_balance(
280        mut self,
281        object_idx: u64,
282        new_object_idx: u64,
283        recipient_idx: u8,
284        amount: u64,
285    ) -> Self {
286        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
287        let object_id = Self::derive_object_id(object_idx);
288        let mut object = self
289            .live_objects
290            .get(&object_id)
291            .cloned()
292            .expect("Mutating an object that does not exist");
293        let coin_type = object.coin_type_maybe().unwrap();
294        // Withdraw balance from coin object.
295        let move_object = object.data.try_as_move_mut().unwrap();
296        let old_balance = move_object.get_coin_value_unsafe();
297        let new_balance = old_balance - amount;
298        move_object.set_coin_value_unsafe(new_balance);
299        tx_builder.mutated_objects.insert(object_id, object);
300
301        // Deposit balance into new coin object.
302        self.create_coin_object(new_object_idx, recipient_idx, amount, coin_type)
303    }
304
305    /// Wrap an existing object in the transaction.
306    /// `object_idx` is a convenient representation of the object's ID.
307    pub fn wrap_object(mut self, object_idx: u64) -> Self {
308        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
309        let object_id = Self::derive_object_id(object_idx);
310        assert!(self.live_objects.contains_key(&object_id));
311        tx_builder.wrapped_objects.insert(object_id);
312        self
313    }
314
315    /// Unwrap an existing object from the transaction.
316    /// `object_idx` is a convenient representation of the object's ID.
317    pub fn unwrap_object(mut self, object_idx: u64) -> Self {
318        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
319        let object_id = Self::derive_object_id(object_idx);
320        assert!(self.wrapped_objects.contains_key(&object_id));
321        tx_builder.unwrapped_objects.insert(object_id);
322        self
323    }
324
325    /// Delete an existing object from the transaction.
326    /// `object_idx` is a convenient representation of the object's ID.
327    pub fn delete_object(mut self, object_idx: u64) -> Self {
328        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
329        let object_id = Self::derive_object_id(object_idx);
330        assert!(self.live_objects.contains_key(&object_id));
331        tx_builder.deleted_objects.insert(object_id);
332        self
333    }
334
335    /// Add events to the transaction.
336    /// `events` is a vector of events to be added to the transaction.
337    pub fn with_events(mut self, events: Vec<Event>) -> Self {
338        self.checkpoint_builder
339            .next_transaction
340            .as_mut()
341            .unwrap()
342            .events = Some(events);
343        self
344    }
345
346    /// Add a move call PTB command to the transaction.
347    /// `package` is the ID of the package to be called.
348    /// `module` is the name of the module to be called.
349    /// `function` is the name of the function to be called.
350    pub fn add_move_call(
351        mut self,
352        package: ObjectID,
353        module: &'static str,
354        function: &'static str,
355    ) -> Self {
356        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
357        tx_builder.move_calls.push((package, module, function));
358        self
359    }
360
361    /// Complete the current transaction and add it to the checkpoint.
362    /// This will also finalize all the object changes, and reflect them in the
363    /// live object map.
364    pub fn finish_transaction(mut self) -> Self {
365        let TransactionBuilder {
366            sender_idx,
367            gas,
368            move_calls,
369            created_objects,
370            mutated_objects,
371            unwrapped_objects,
372            wrapped_objects,
373            deleted_objects,
374            events,
375        } = self.checkpoint_builder.next_transaction.take().unwrap();
376        let sender = Self::derive_address(sender_idx);
377        let events = events.map(|events| TransactionEvents { data: events });
378        let events_digest = events.as_ref().map(|events| events.digest());
379        let mut pt_builder = ProgrammableTransactionBuilder::new();
380        for (package, module, function) in move_calls {
381            pt_builder
382                .move_call(
383                    package,
384                    ident_str!(module).to_owned(),
385                    ident_str!(function).to_owned(),
386                    vec![],
387                    vec![],
388                )
389                .unwrap();
390        }
391        let pt = pt_builder.finish();
392        let tx_data = TransactionData::new(
393            TransactionKind::ProgrammableTransaction(pt),
394            sender,
395            gas,
396            1,
397            1,
398        );
399        let tx = Transaction::new(SenderSignedData::new(tx_data, vec![]));
400        let wrapped_objects: Vec<_> = wrapped_objects
401            .into_iter()
402            .map(|id| self.live_objects.remove(&id).unwrap())
403            .collect();
404        let deleted_objects: Vec<_> = deleted_objects
405            .into_iter()
406            .map(|id| self.live_objects.remove(&id).unwrap())
407            .collect();
408        let unwrapped_objects: Vec<_> = unwrapped_objects
409            .into_iter()
410            .map(|id| self.wrapped_objects.remove(&id).unwrap())
411            .collect();
412        let mut effects_builder = TestEffectsBuilder::new(tx.data())
413            .with_created_objects(created_objects.iter().map(|(id, o)| (*id, *o.owner())))
414            .with_mutated_objects(
415                mutated_objects
416                    .iter()
417                    .map(|(id, o)| (*id, o.version(), *o.owner())),
418            )
419            .with_wrapped_objects(wrapped_objects.iter().map(|o| (o.id(), o.version())))
420            .with_unwrapped_objects(unwrapped_objects.iter().map(|o| (o.id(), *o.owner())))
421            .with_deleted_objects(deleted_objects.iter().map(|o| (o.id(), o.version())));
422        if let Some(events_digest) = &events_digest {
423            effects_builder = effects_builder.with_events_digest(*events_digest);
424        }
425        let effects = effects_builder.build();
426        let lamport_version = effects.lamport_version();
427        let input_objects: Vec<_> = mutated_objects
428            .keys()
429            .map(|id| self.live_objects.get(id).unwrap().clone())
430            .chain(deleted_objects.clone())
431            .chain(wrapped_objects.clone())
432            .chain(std::iter::once(
433                self.live_objects.get(&gas.0).unwrap().clone(),
434            ))
435            .collect();
436        let output_objects: Vec<_> = created_objects
437            .values()
438            .cloned()
439            .chain(mutated_objects.values().cloned())
440            .chain(unwrapped_objects.clone())
441            .chain(std::iter::once(
442                self.live_objects.get(&gas.0).cloned().unwrap(),
443            ))
444            .map(|mut o| {
445                o.data
446                    .try_as_move_mut()
447                    .unwrap()
448                    .increment_version_to(lamport_version);
449                o
450            })
451            .collect();
452        self.live_objects
453            .extend(output_objects.iter().map(|o| (o.id(), o.clone())));
454        self.wrapped_objects
455            .extend(wrapped_objects.iter().map(|o| (o.id(), o.clone())));
456        self.checkpoint_builder
457            .transactions
458            .push(CheckpointTransaction {
459                transaction: tx,
460                effects,
461                events,
462                input_objects,
463                output_objects,
464            });
465        self
466    }
467
468    /// Creates a transaction that advances the epoch, adds it to the
469    /// checkpoint, and then builds the checkpoint. This increments the
470    /// stored checkpoint sequence number and epoch. If `safe_mode` is true,
471    /// the epoch end transaction will not include the `SystemEpochInfoEvent`.
472    pub fn advance_epoch(&mut self, safe_mode: bool) -> CheckpointData {
473        let (committee, _) = Committee::new_simple_test_committee();
474        let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
475        let tx_kind = EndOfEpochTransactionKind::new_change_epoch(
476            self.checkpoint_builder.epoch + 1,
477            protocol_config.version,
478            Default::default(),
479            Default::default(),
480            Default::default(),
481            Default::default(),
482            Default::default(),
483            Default::default(),
484        );
485
486        // TODO: need the system state object wrapper and dynamic field object to
487        // "correctly" mock advancing epoch, at least to satisfy kv_epoch_starts
488        // pipeline.
489        let end_of_epoch_tx = TransactionData::new(
490            TransactionKind::EndOfEpochTransaction(vec![tx_kind]),
491            IotaAddress::default(),
492            random_object_ref(),
493            1,
494            1,
495        )
496        .pipe(|data| SenderSignedData::new(data, vec![]))
497        .pipe(Transaction::new);
498
499        let events = if !safe_mode {
500            let system_epoch_info_event = SystemEpochInfoEventV2 {
501                epoch: self.checkpoint_builder.epoch,
502                protocol_version: protocol_config.version.as_u64(),
503                ..Default::default()
504            };
505            let struct_tag = StructTag {
506                address: IOTA_SYSTEM_ADDRESS,
507                module: ident_str!("iota_system_state_inner").to_owned(),
508                name: ident_str!("SystemEpochInfoEvent").to_owned(),
509                type_params: vec![],
510            };
511            Some(vec![Event::new(
512                &IOTA_SYSTEM_ADDRESS,
513                ident_str!("iota_system_state_inner"),
514                TestCheckpointDataBuilder::derive_address(0),
515                struct_tag,
516                bcs::to_bytes(&system_epoch_info_event).unwrap(),
517            )])
518        } else {
519            None
520        };
521
522        let transaction_events = events.map(|events| TransactionEvents { data: events });
523
524        // Similar to calling self.finish_transaction()
525        self.checkpoint_builder
526            .transactions
527            .push(CheckpointTransaction {
528                transaction: end_of_epoch_tx,
529                effects: Default::default(),
530                events: transaction_events,
531                input_objects: vec![],
532                output_objects: vec![],
533            });
534
535        // Call build_checkpoint() to finalize the checkpoint and then populate the
536        // checkpoint with additional end of epoch data.
537        let mut checkpoint = self.build_checkpoint();
538        let end_of_epoch_data = EndOfEpochData {
539            next_epoch_committee: committee.voting_rights.clone(),
540            next_epoch_protocol_version: protocol_config.version,
541            epoch_commitments: vec![],
542            // Do not simulate supply changes in tests.
543            epoch_supply_change: 0,
544        };
545        checkpoint.checkpoint_summary.end_of_epoch_data = Some(end_of_epoch_data);
546        self.checkpoint_builder.epoch += 1;
547        checkpoint
548    }
549
550    /// Build the checkpoint data using all the transactions added to the
551    /// builder so far. This will also increment the stored checkpoint
552    /// sequence number.
553    pub fn build_checkpoint(&mut self) -> CheckpointData {
554        assert!(self.checkpoint_builder.next_transaction.is_none());
555        let transactions = std::mem::take(&mut self.checkpoint_builder.transactions);
556        let contents = CheckpointContents::new_with_digests_only_for_tests(
557            transactions
558                .iter()
559                .map(|tx| ExecutionDigests::new(*tx.transaction.digest(), tx.effects.digest())),
560        );
561
562        self.checkpoint_builder.network_total_transactions += transactions.len() as u64;
563
564        let checkpoint_summary = CheckpointSummary::new(
565            &ProtocolConfig::get_for_max_version_UNSAFE(),
566            self.checkpoint_builder.epoch,
567            self.checkpoint_builder.checkpoint,
568            self.checkpoint_builder.network_total_transactions,
569            &contents,
570            None,
571            Default::default(),
572            None,
573            0,
574            vec![],
575        );
576
577        let (committee, keys) = Committee::new_simple_test_committee();
578
579        let checkpoint_cert = CertifiedCheckpointSummary::new_from_keypairs_for_testing(
580            checkpoint_summary,
581            &keys,
582            &committee,
583        );
584
585        self.checkpoint_builder.checkpoint += 1;
586        CheckpointData {
587            checkpoint_summary: checkpoint_cert,
588            checkpoint_contents: contents,
589            transactions,
590        }
591    }
592
593    /// Derive an object ID from an index. This is used to conveniently
594    /// represent an object's ID. We ensure that the bytes of object IDs
595    /// have a stable order that is the same as object_idx.
596    pub fn derive_object_id(object_idx: u64) -> ObjectID {
597        // We achieve this by setting the first 8 bytes of the object ID to the
598        // object_idx.
599        let mut bytes = [0; ObjectID::LENGTH];
600        bytes[0..8].copy_from_slice(&object_idx.to_le_bytes());
601        ObjectID::from_bytes(bytes).unwrap()
602    }
603
604    /// Derive an address from an index.
605    pub fn derive_address(address_idx: u8) -> IotaAddress {
606        dbg_addr(address_idx)
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use std::str::FromStr;
613
614    use move_core_types::ident_str;
615
616    use super::*;
617    use crate::transaction::{Command, ProgrammableMoveCall, TransactionDataAPI};
618    #[test]
619    fn test_basic_checkpoint_builder() {
620        // Create a checkpoint with a single transaction that does nothing.
621        let checkpoint = TestCheckpointDataBuilder::new(1)
622            .with_epoch(5)
623            .start_transaction(0)
624            .finish_transaction()
625            .build_checkpoint();
626
627        assert_eq!(*checkpoint.checkpoint_summary.sequence_number(), 1);
628        assert_eq!(checkpoint.checkpoint_summary.epoch, 5);
629        assert_eq!(checkpoint.transactions.len(), 1);
630        let tx = &checkpoint.transactions[0];
631        assert_eq!(
632            tx.transaction.sender_address(),
633            TestCheckpointDataBuilder::derive_address(0)
634        );
635        assert_eq!(tx.effects.mutated().len(), 1); // gas object
636        assert_eq!(tx.effects.deleted().len(), 0);
637        assert_eq!(tx.effects.created().len(), 0);
638        assert_eq!(tx.input_objects.len(), 1);
639        assert_eq!(tx.output_objects.len(), 1);
640    }
641
642    #[test]
643    fn test_multiple_transactions() {
644        let checkpoint = TestCheckpointDataBuilder::new(1)
645            .start_transaction(0)
646            .finish_transaction()
647            .start_transaction(1)
648            .finish_transaction()
649            .start_transaction(2)
650            .finish_transaction()
651            .build_checkpoint();
652
653        assert_eq!(checkpoint.transactions.len(), 3);
654
655        // Verify transactions have different senders (since we used 0, 1, 2 as sender
656        // indices above).
657        let senders: Vec<_> = checkpoint
658            .transactions
659            .iter()
660            .map(|tx| tx.transaction.transaction_data().sender())
661            .collect();
662        assert_eq!(
663            senders,
664            vec![
665                TestCheckpointDataBuilder::derive_address(0),
666                TestCheckpointDataBuilder::derive_address(1),
667                TestCheckpointDataBuilder::derive_address(2)
668            ]
669        );
670    }
671
672    #[test]
673    fn test_object_creation() {
674        let checkpoint = TestCheckpointDataBuilder::new(1)
675            .start_transaction(0)
676            .create_owned_object(0)
677            .finish_transaction()
678            .build_checkpoint();
679
680        let tx = &checkpoint.transactions[0];
681        let created_obj_id = TestCheckpointDataBuilder::derive_object_id(0);
682
683        // Verify the newly created object appears in output objects
684        assert!(
685            tx.output_objects
686                .iter()
687                .any(|obj| obj.id() == created_obj_id)
688        );
689
690        // Verify effects show object creation
691        assert!(
692            tx.effects
693                .created()
694                .iter()
695                .any(|((id, ..), owner)| *id == created_obj_id
696                    && owner.get_owner_address().unwrap()
697                        == TestCheckpointDataBuilder::derive_address(0))
698        );
699    }
700
701    #[test]
702    fn test_object_mutation() {
703        let checkpoint = TestCheckpointDataBuilder::new(1)
704            .start_transaction(0)
705            .create_owned_object(0)
706            .finish_transaction()
707            .start_transaction(0)
708            .mutate_object(0)
709            .finish_transaction()
710            .build_checkpoint();
711
712        let tx = &checkpoint.transactions[1];
713        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
714
715        // Verify object appears in both input and output objects
716        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
717        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id));
718
719        // Verify effects show object mutation
720        assert!(
721            tx.effects
722                .mutated()
723                .iter()
724                .any(|((id, ..), _)| *id == obj_id)
725        );
726    }
727
728    #[test]
729    fn test_object_deletion() {
730        let checkpoint = TestCheckpointDataBuilder::new(1)
731            .start_transaction(0)
732            .create_owned_object(0)
733            .finish_transaction()
734            .start_transaction(0)
735            .delete_object(0)
736            .finish_transaction()
737            .build_checkpoint();
738
739        let tx = &checkpoint.transactions[1];
740        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
741
742        // Verify object appears in input objects but not output
743        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
744        assert!(!tx.output_objects.iter().any(|obj| obj.id() == obj_id));
745
746        // Verify effects show object deletion
747        assert!(tx.effects.deleted().iter().any(|(id, ..)| *id == obj_id));
748    }
749
750    #[test]
751    fn test_object_wrapping() {
752        let checkpoint = TestCheckpointDataBuilder::new(1)
753            .start_transaction(0)
754            .create_owned_object(0)
755            .finish_transaction()
756            .start_transaction(0)
757            .wrap_object(0)
758            .finish_transaction()
759            .start_transaction(0)
760            .unwrap_object(0)
761            .finish_transaction()
762            .build_checkpoint();
763
764        let tx = &checkpoint.transactions[1];
765        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
766
767        // Verify object appears in input objects but not output
768        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
769        assert!(!tx.output_objects.iter().any(|obj| obj.id() == obj_id));
770
771        // Verify effects show object wrapping
772        assert!(tx.effects.wrapped().iter().any(|(id, ..)| *id == obj_id));
773
774        let tx = &checkpoint.transactions[2];
775
776        // Verify object appears in output objects but not input
777        assert!(!tx.input_objects.iter().any(|obj| obj.id() == obj_id));
778        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id));
779
780        // Verify effects show object unwrapping
781        assert!(
782            tx.effects
783                .unwrapped()
784                .iter()
785                .any(|((id, ..), _)| *id == obj_id)
786        );
787    }
788
789    #[test]
790    fn test_object_transfer() {
791        let checkpoint = TestCheckpointDataBuilder::new(1)
792            .start_transaction(0)
793            .create_owned_object(0)
794            .finish_transaction()
795            .start_transaction(1)
796            .transfer_object(0, 1)
797            .finish_transaction()
798            .build_checkpoint();
799
800        let tx = &checkpoint.transactions[1];
801        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
802
803        // Verify object appears in input and output objects
804        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
805        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id));
806
807        // Verify effects show object transfer
808        assert!(
809            tx.effects
810                .mutated()
811                .iter()
812                .any(|((id, ..), owner)| *id == obj_id
813                    && owner.get_owner_address().unwrap()
814                        == TestCheckpointDataBuilder::derive_address(1))
815        );
816    }
817
818    #[test]
819    fn test_shared_object() {
820        let checkpoint = TestCheckpointDataBuilder::new(1)
821            .start_transaction(0)
822            .create_shared_object(0)
823            .finish_transaction()
824            .build_checkpoint();
825
826        let tx = &checkpoint.transactions[0];
827        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
828
829        // Verify object appears in output objects and is shared
830        assert!(
831            tx.output_objects
832                .iter()
833                .any(|obj| obj.id() == obj_id && obj.owner().is_shared())
834        );
835    }
836
837    #[test]
838    fn test_freeze_object() {
839        let checkpoint = TestCheckpointDataBuilder::new(1)
840            .start_transaction(0)
841            .create_owned_object(0)
842            .finish_transaction()
843            .start_transaction(0)
844            .change_object_owner(0, Owner::Immutable)
845            .finish_transaction()
846            .build_checkpoint();
847
848        let tx = &checkpoint.transactions[1];
849        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
850
851        // Verify object appears in output objects and is immutable
852        assert!(
853            tx.output_objects
854                .iter()
855                .any(|obj| obj.id() == obj_id && obj.owner().is_immutable())
856        );
857    }
858
859    #[test]
860    fn test_iota_balance_transfer() {
861        let checkpoint = TestCheckpointDataBuilder::new(1)
862            .start_transaction(0)
863            .create_iota_object(0, 100)
864            .finish_transaction()
865            .start_transaction(1)
866            .transfer_coin_balance(0, 1, 1, 10)
867            .finish_transaction()
868            .build_checkpoint();
869
870        let tx = &checkpoint.transactions[0];
871        let obj_id0 = TestCheckpointDataBuilder::derive_object_id(0);
872
873        // Verify the newly created object appears in output objects and is a gas coin
874        // with 100 NANOS.
875        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0
876            && obj.is_gas_coin()
877            && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 100));
878
879        let tx = &checkpoint.transactions[1];
880        let obj_id1 = TestCheckpointDataBuilder::derive_object_id(1);
881
882        // Verify the original IOTA coin now has 90 NANOS after the transfer.
883        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0
884            && obj.is_gas_coin()
885            && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 90));
886
887        // Verify the split out IOTA coin has 10 NANOS.
888        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id1
889            && obj.is_gas_coin()
890            && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 10));
891    }
892
893    #[test]
894    fn test_coin_balance_transfer() {
895        let type_tag = TypeTag::from_str("0x100::a::b").unwrap();
896        let checkpoint = TestCheckpointDataBuilder::new(1)
897            .start_transaction(0)
898            .create_coin_object(0, 0, 100, type_tag.clone())
899            .finish_transaction()
900            .start_transaction(1)
901            .transfer_coin_balance(0, 1, 1, 10)
902            .finish_transaction()
903            .build_checkpoint();
904
905        let tx = &checkpoint.transactions[1];
906        let obj_id0 = TestCheckpointDataBuilder::derive_object_id(0);
907        let obj_id1 = TestCheckpointDataBuilder::derive_object_id(1);
908
909        // Verify the original coin now has 90 balance after the transfer.
910        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0
911            && obj.coin_type_maybe().unwrap() == type_tag
912            && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 90));
913
914        // Verify the split out coin has 10 balance, with the same type tag.
915        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id1
916            && obj.coin_type_maybe().unwrap() == type_tag
917            && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 10));
918    }
919
920    #[test]
921    fn test_events() {
922        let checkpoint = TestCheckpointDataBuilder::new(1)
923            .start_transaction(0)
924            .with_events(vec![Event::new(
925                &ObjectID::ZERO,
926                ident_str!("test"),
927                TestCheckpointDataBuilder::derive_address(0),
928                GAS::type_(),
929                vec![],
930            )])
931            .finish_transaction()
932            .build_checkpoint();
933        let tx = &checkpoint.transactions[0];
934
935        // Verify the transaction has an events digest
936        assert!(tx.effects.events_digest().is_some());
937
938        // Verify the transaction has a single event
939        assert_eq!(tx.events.as_ref().unwrap().data.len(), 1);
940    }
941
942    #[test]
943    fn test_move_call() {
944        let checkpoint = TestCheckpointDataBuilder::new(1)
945            .start_transaction(0)
946            .add_move_call(ObjectID::ZERO, "test", "test")
947            .finish_transaction()
948            .build_checkpoint();
949        let tx = &checkpoint.transactions[0];
950
951        // Verify the transaction has a move call matching the arguments provided.
952        assert!(
953            tx.transaction
954                .transaction_data()
955                .kind()
956                .iter_commands()
957                .any(|cmd| {
958                    cmd == &Command::MoveCall(Box::new(ProgrammableMoveCall {
959                        package: ObjectID::ZERO,
960                        module: "test".to_string(),
961                        function: "test".to_string(),
962                        type_arguments: vec![],
963                        arguments: vec![],
964                    }))
965                })
966        );
967    }
968
969    #[test]
970    fn test_multiple_checkpoints() {
971        let mut builder = TestCheckpointDataBuilder::new(1)
972            .start_transaction(0)
973            .create_owned_object(0)
974            .finish_transaction();
975        let checkpoint1 = builder.build_checkpoint();
976        builder = builder
977            .start_transaction(0)
978            .mutate_object(0)
979            .finish_transaction();
980        let checkpoint2 = builder.build_checkpoint();
981        builder = builder
982            .start_transaction(0)
983            .delete_object(0)
984            .finish_transaction();
985        let checkpoint3 = builder.build_checkpoint();
986
987        // Verify the sequence numbers are consecutive.
988        assert_eq!(checkpoint1.checkpoint_summary.sequence_number, 1);
989        assert_eq!(checkpoint2.checkpoint_summary.sequence_number, 2);
990        assert_eq!(checkpoint3.checkpoint_summary.sequence_number, 3);
991    }
992}