Skip to main content

sui_cluster_test/test_case/
native_transfer_test.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use async_trait::async_trait;
5use tracing::info;
6
7use sui_rpc_api::client::ExecutedTransaction;
8use sui_types::{
9    base_types::{ObjectID, SuiAddress},
10    crypto::{AccountKeyPair, get_key_pair},
11    object::Owner,
12};
13
14use crate::{
15    TestCaseImpl, TestContext,
16    helper::{BalanceChangeChecker, ObjectChecker},
17};
18
19pub struct NativeTransferTest;
20
21#[async_trait]
22impl TestCaseImpl for NativeTransferTest {
23    fn name(&self) -> &'static str {
24        "NativeTransfer"
25    }
26
27    fn description(&self) -> &'static str {
28        "Test tranferring SUI coins natively"
29    }
30
31    async fn run(&self, ctx: &mut TestContext) -> Result<(), anyhow::Error> {
32        info!("Testing gas coin transfer");
33        let mut sui_objs = ctx.get_sui_from_faucet(Some(1)).await;
34        let gas_obj = ctx.get_sui_from_faucet(Some(1)).await.swap_remove(0);
35
36        let signer = ctx.get_wallet_address();
37        let (recipient_addr, _): (_, AccountKeyPair) = get_key_pair();
38        let gas_budget = 2_000_000;
39
40        // Test transfer object: move a whole SUI coin object to the recipient,
41        // paying for gas with a separate, explicitly-supplied gas coin (from the
42        // faucet response) so transaction construction is deterministic.
43        let obj_to_transfer: ObjectID = *sui_objs.swap_remove(0).id();
44        let gas_ref = ctx.current_object_ref(*gas_obj.id()).await;
45        let builder = ctx.get_grpc_client().transaction_builder();
46        let data = builder
47            .transfer_object(
48                signer,
49                obj_to_transfer,
50                Some(gas_ref.0),
51                gas_budget,
52                recipient_addr,
53            )
54            .await?;
55        let response = ctx.sign_and_execute(data, "coin transfer").await;
56        Self::examine_response(ctx, &response, signer, recipient_addr, obj_to_transfer).await;
57
58        // Test transfer of a second, distinct SUI coin object.
59        let mut sui_objs_2 = ctx.get_sui_from_faucet(Some(1)).await;
60        let obj_to_transfer_2 = *sui_objs_2.swap_remove(0).id();
61        // Refresh the gas ref: its version/digest changed after the first tx.
62        let gas_ref = ctx.current_object_ref(*gas_obj.id()).await;
63        let builder = ctx.get_grpc_client().transaction_builder();
64        let data = builder
65            .transfer_object(
66                signer,
67                obj_to_transfer_2,
68                Some(gas_ref.0),
69                gas_budget,
70                recipient_addr,
71            )
72            .await?;
73        let response = ctx.sign_and_execute(data, "coin transfer").await;
74
75        // Verify the SECOND transferred object (previously this asserted the
76        // first object's ID, which was already a no-op bug).
77        Self::examine_response(ctx, &response, signer, recipient_addr, obj_to_transfer_2).await;
78        Ok(())
79    }
80}
81
82impl NativeTransferTest {
83    async fn examine_response(
84        ctx: &TestContext,
85        response: &ExecutedTransaction,
86        signer: SuiAddress,
87        recipient: SuiAddress,
88        obj_to_transfer_id: ObjectID,
89    ) {
90        let mut balance_changes = response.balance_changes.clone();
91        // for transfer we only expect 2 balance changes, one for sender and one
92        // for recipient.
93        assert_eq!(
94            balance_changes.len(),
95            2,
96            "Expect 2 balance changes emitted, but got {}",
97            balance_changes.len()
98        );
99        // Order of balance change is not fixed so need to check whose balance
100        // comes first. This makes sure the recipient always comes first.
101        let signer_sdk: sui_sdk_types::Address = signer.into();
102        if balance_changes[0].address == signer_sdk {
103            balance_changes.reverse()
104        }
105        BalanceChangeChecker::new()
106            .address(recipient)
107            .coin_type("0x2::sui::SUI")
108            .check(&balance_changes.remove(0));
109        BalanceChangeChecker::new()
110            .address(signer)
111            .coin_type("0x2::sui::SUI")
112            .check(&balance_changes.remove(0));
113
114        // The executed transaction is already checkpointed; read the transferred
115        // object by ID and confirm the new owner (`LedgerService`). A failed
116        // read must fail the test — it is the ownership assertion itself.
117        ObjectChecker::new(obj_to_transfer_id)
118            .owner(Owner::AddressOwner(recipient))
119            .check(&ctx.get_grpc_client())
120            .await
121            .expect("transferred object must be readable and owned by the recipient");
122    }
123}