iota_cluster_test/test_case/
coin_merge_split_test.rs

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