Skip to main content

sui_cluster_test/test_case/
coin_merge_split_test.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{TestCaseImpl, TestContext, helper::ObjectChecker};
5use async_trait::async_trait;
6use sui_rpc_api::client::ExecutedTransaction;
7use sui_types::base_types::{ObjectID, SuiAddress};
8use sui_types::effects::TransactionEffectsAPI;
9use sui_types::object::Owner;
10use tracing::{debug, info};
11
12pub struct CoinMergeSplitTest;
13
14#[async_trait]
15impl TestCaseImpl for CoinMergeSplitTest {
16    fn name(&self) -> &'static str {
17        "CoinMergeSplit"
18    }
19
20    fn description(&self) -> &'static str {
21        "Test merge and split SUI coins"
22    }
23
24    async fn run(&self, ctx: &mut TestContext) -> Result<(), anyhow::Error> {
25        let mut sui_objs = ctx.get_sui_from_faucet(Some(1)).await;
26        let gas_obj = sui_objs.swap_remove(0);
27        let gas_obj_id = *gas_obj.id();
28
29        let signer = ctx.get_wallet_address();
30        let mut sui_objs_2 = ctx.get_sui_from_faucet(Some(1)).await;
31
32        let primary_coin = sui_objs_2.swap_remove(0);
33        let primary_coin_id = *primary_coin.id();
34        let original_value = primary_coin.value();
35
36        // Split
37        info!("Testing coin split.");
38        let amounts = vec![1, (original_value - 2) / 2];
39
40        let response = Self::split_coin(ctx, signer, primary_coin_id, amounts, gas_obj_id).await;
41        // The created coins are read straight from the native effects
42        // (`LedgerService`).
43        let new_coins: Vec<ObjectID> = response
44            .effects
45            .created()
46            .iter()
47            .map(|(obj_ref, _owner)| obj_ref.0)
48            .collect();
49        assert!(
50            !new_coins.is_empty(),
51            "Expected the split to create new coins"
52        );
53
54        for coin_id in &new_coins {
55            let _ = ObjectChecker::new(*coin_id)
56                .owner(Owner::AddressOwner(signer))
57                .check_into_gas_coin(&ctx.get_grpc_client())
58                .await;
59        }
60
61        // Merge
62        info!("Testing coin merge.");
63        let mut coins_merged = Vec::new();
64        // We on purpose linearize the merge operations, otherwise the primary
65        // coin may be locked. Both the primary coin and the gas coin refs are
66        // refreshed before every mutation.
67        for coin_to_merge in &new_coins {
68            debug!(
69                "Merging coin {} back to {}.",
70                coin_to_merge, primary_coin_id
71            );
72            let response =
73                Self::merge_coin(ctx, signer, primary_coin_id, *coin_to_merge, gas_obj_id).await;
74            // The merged-in coin must be reported as deleted by the effects.
75            let deleted: Vec<ObjectID> = response.effects.deleted().iter().map(|r| r.0).collect();
76            assert!(
77                deleted.contains(coin_to_merge),
78                "Merged coin {coin_to_merge} should be deleted, deleted set: {deleted:?}",
79            );
80            coins_merged.push(*coin_to_merge);
81        }
82
83        // Owner still owns the primary coin, and its value is restored.
84        debug!("Verifying owner still owns the primary coin {primary_coin_id}");
85        let primary_after_merge = ObjectChecker::new(primary_coin_id)
86            .owner(Owner::AddressOwner(signer))
87            .check_into_gas_coin(&ctx.get_grpc_client())
88            .await;
89        assert_eq!(
90            primary_after_merge.value(),
91            original_value,
92            "Split-then-merge yields unexpected coin value, expect {}, got {}",
93            original_value,
94            primary_after_merge.value(),
95        );
96        Ok(())
97    }
98}
99
100impl CoinMergeSplitTest {
101    async fn merge_coin(
102        ctx: &TestContext,
103        signer: SuiAddress,
104        primary_coin: ObjectID,
105        coin_to_merge: ObjectID,
106        gas_obj_id: ObjectID,
107    ) -> ExecutedTransaction {
108        let gas_ref = ctx.current_object_ref(gas_obj_id).await;
109        let builder = ctx.get_grpc_client().transaction_builder();
110        let data = builder
111            .merge_coins(
112                signer,
113                primary_coin,
114                coin_to_merge,
115                Some(gas_ref.0),
116                20_000_000,
117            )
118            .await
119            .unwrap();
120        ctx.sign_and_execute(data, "coin merge").await
121    }
122
123    async fn split_coin(
124        ctx: &TestContext,
125        signer: SuiAddress,
126        primary_coin: ObjectID,
127        amounts: Vec<u64>,
128        gas_obj_id: ObjectID,
129    ) -> ExecutedTransaction {
130        let gas_ref = ctx.current_object_ref(gas_obj_id).await;
131        let builder = ctx.get_grpc_client().transaction_builder();
132        let data = builder
133            .split_coin(signer, primary_coin, amounts, Some(gas_ref.0), 20_000_000)
134            .await
135            .unwrap();
136        ctx.sign_and_execute(data, "coin split").await
137    }
138}