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 jsonrpsee::rpc_params;
7use sui_json_rpc_types::{SuiTransactionBlockEffectsAPI, SuiTransactionBlockResponse};
8use sui_types::base_types::{ObjectID, SuiAddress};
9use sui_types::object::Owner;
10use sui_types::sui_serde::BigInt;
11use tracing::{debug, info};
12
13pub struct CoinMergeSplitTest;
14
15#[async_trait]
16impl TestCaseImpl for CoinMergeSplitTest {
17    fn name(&self) -> &'static str {
18        "CoinMergeSplit"
19    }
20
21    fn description(&self) -> &'static str {
22        "Test merge and split SUI coins"
23    }
24
25    async fn run(&self, ctx: &mut TestContext) -> Result<(), anyhow::Error> {
26        let mut sui_objs = ctx.get_sui_from_faucet(Some(1)).await;
27        let gas_obj = sui_objs.swap_remove(0);
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.into(), ((original_value - 2) / 2).into()];
39
40        let response =
41            Self::split_coin(ctx, signer, *primary_coin.id(), amounts, *gas_obj.id()).await;
42        let tx_digest = response.digest;
43        let new_coins = response.effects.as_ref().unwrap().created();
44
45        // Verify fullnode observes the txn
46        ctx.let_fullnode_sync(vec![tx_digest], 5).await;
47
48        let _ = futures::future::join_all(
49            new_coins
50                .iter()
51                .map(|coin_ref| {
52                    ObjectChecker::new(coin_ref.reference.object_id)
53                        .owner(Owner::AddressOwner(signer))
54                        .check_into_gas_coin(ctx.get_fullnode_client())
55                })
56                .collect::<Vec<_>>(),
57        )
58        .await;
59
60        // Merge
61        info!("Testing coin merge.");
62        let mut coins_merged = Vec::new();
63        let mut txes = Vec::new();
64        // We on purpose linearize the merge operations, otherwise the primary coin may be locked
65        for new_coin in new_coins {
66            let coin_to_merge = new_coin.reference.object_id;
67            debug!(
68                "Merging coin {} back to {}.",
69                coin_to_merge, primary_coin_id
70            );
71            let response =
72                Self::merge_coin(ctx, signer, primary_coin_id, coin_to_merge, *gas_obj.id()).await;
73            debug!("Verifying the merged coin {} is deleted.", coin_to_merge);
74            coins_merged.push(coin_to_merge);
75            txes.push(response.digest);
76        }
77
78        // Verify fullnode observes the txn
79        ctx.let_fullnode_sync(txes, 5).await;
80
81        let _ = futures::future::join_all(
82            coins_merged
83                .iter()
84                .map(|obj_id| {
85                    ObjectChecker::new(*obj_id)
86                        .owner(Owner::AddressOwner(signer))
87                        .deleted()
88                        .check(ctx.get_fullnode_client())
89                })
90                .collect::<Vec<_>>(),
91        )
92        .await
93        .into_iter()
94        .collect::<Vec<_>>();
95
96        // Owner still owns the primary coin
97        debug!(
98            "Verifying owner still owns the primary coin {}",
99            *primary_coin.id()
100        );
101        let primary_after_merge = ObjectChecker::new(primary_coin_id)
102            .owner(Owner::AddressOwner(ctx.get_wallet_address()))
103            .check_into_gas_coin(ctx.get_fullnode_client())
104            .await;
105        assert_eq!(
106            primary_after_merge.value(),
107            original_value,
108            "Split-then-merge yields unexpected coin value, expect {}, got {}",
109            original_value,
110            primary_after_merge.value(),
111        );
112        Ok(())
113    }
114}
115
116impl CoinMergeSplitTest {
117    async fn merge_coin(
118        ctx: &TestContext,
119        signer: SuiAddress,
120        primary_coin: ObjectID,
121        coin_to_merge: ObjectID,
122        gas_obj_id: ObjectID,
123    ) -> SuiTransactionBlockResponse {
124        let params = rpc_params![
125            signer,
126            primary_coin,
127            coin_to_merge,
128            Some(gas_obj_id),
129            (20_000_000).to_string()
130        ];
131
132        let data = ctx
133            .build_transaction_remotely("unsafe_mergeCoins", params)
134            .await
135            .unwrap();
136
137        ctx.sign_and_execute(data, "coin merge").await
138    }
139
140    async fn split_coin(
141        ctx: &TestContext,
142        signer: SuiAddress,
143        primary_coin: ObjectID,
144        amounts: Vec<BigInt<u64>>,
145        gas_obj_id: ObjectID,
146    ) -> SuiTransactionBlockResponse {
147        let params = rpc_params![
148            signer,
149            primary_coin,
150            amounts,
151            Some(gas_obj_id),
152            (20_000_000).to_string()
153        ];
154
155        let data = ctx
156            .build_transaction_remotely("unsafe_splitCoin", params)
157            .await
158            .unwrap();
159
160        ctx.sign_and_execute(data, "coin merge").await
161    }
162}