sui_cluster_test/
helper.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use anyhow::bail;
5use move_core_types::language_storage::TypeTag;
6use sui_json_rpc_types::{BalanceChange, SuiData, SuiObjectData, SuiObjectDataOptions};
7use sui_sdk::SuiClient;
8use sui_types::error::SuiObjectResponseError;
9use sui_types::gas_coin::GasCoin;
10use sui_types::{base_types::ObjectID, object::Owner, parse_sui_type_tag};
11use tracing::{debug, trace};
12
13/// A util struct that helps verify Sui Object.
14/// Use builder style to construct the conditions.
15/// When optionals fields are not set, related checks are omitted.
16/// Consuming functions such as `check` perform the check and panics if
17/// verification results are unexpected. `check_into_object` and
18/// `check_into_gas_coin` expect to get a `SuiObjectData` and `GasCoin`
19/// respectfully.
20#[derive(Debug)]
21pub struct ObjectChecker {
22    object_id: ObjectID,
23    owner: Option<Owner>,
24    is_deleted: bool,
25    is_sui_coin: Option<bool>,
26}
27
28impl ObjectChecker {
29    pub fn new(object_id: ObjectID) -> ObjectChecker {
30        Self {
31            object_id,
32            owner: None,
33            is_deleted: false, // default to exist
34            is_sui_coin: None,
35        }
36    }
37
38    pub fn owner(mut self, owner: Owner) -> Self {
39        self.owner = Some(owner);
40        self
41    }
42
43    pub fn deleted(mut self) -> Self {
44        self.is_deleted = true;
45        self
46    }
47
48    pub fn is_sui_coin(mut self, is_sui_coin: bool) -> Self {
49        self.is_sui_coin = Some(is_sui_coin);
50        self
51    }
52
53    pub async fn check_into_gas_coin(self, client: &SuiClient) -> GasCoin {
54        if self.is_sui_coin == Some(false) {
55            panic!("'check_into_gas_coin' shouldn't be called with 'is_sui_coin' set as false");
56        }
57        self.is_sui_coin(true)
58            .check(client)
59            .await
60            .unwrap()
61            .into_gas_coin()
62    }
63
64    pub async fn check_into_object(self, client: &SuiClient) -> SuiObjectData {
65        self.check(client).await.unwrap().into_object()
66    }
67
68    pub async fn check(self, client: &SuiClient) -> Result<CheckerResultObject, anyhow::Error> {
69        debug!(?self);
70
71        let object_id = self.object_id;
72        let object_info = client
73            .read_api()
74            .get_object_with_options(
75                object_id,
76                SuiObjectDataOptions::new()
77                    .with_type()
78                    .with_owner()
79                    .with_bcs(),
80            )
81            .await
82            .or_else(|err| bail!("Failed to get object info (id: {}), err: {err}", object_id))?;
83
84        trace!("getting object {object_id}, info :: {object_info:?}");
85
86        match (object_info.data, object_info.error) {
87            (None, Some(SuiObjectResponseError::NotExists { object_id })) => {
88                panic!(
89                    "Node can't find gas object {} with client {:?}",
90                    object_id,
91                    client.read_api()
92                )
93            }
94            (
95                None,
96                Some(SuiObjectResponseError::DynamicFieldNotFound {
97                    parent_object_id: object_id,
98                }),
99            ) => {
100                panic!(
101                    "Node can't find dynamic field for {} with client {:?}",
102                    object_id,
103                    client.read_api()
104                )
105            }
106            (
107                None,
108                Some(SuiObjectResponseError::Deleted {
109                    object_id,
110                    version: _,
111                    digest: _,
112                }),
113            ) => {
114                if !self.is_deleted {
115                    panic!("Gas object {} was deleted", object_id);
116                }
117                Ok(CheckerResultObject::new(None, None))
118            }
119            (Some(object), _) => {
120                if self.is_deleted {
121                    panic!("Expect Gas object {} deleted, but it is not", object_id);
122                }
123                if let Some(owner) = self.owner {
124                    let object_owner = object
125                        .owner
126                        .clone()
127                        .unwrap_or_else(|| panic!("Object {} does not have owner", object_id));
128                    assert_eq!(
129                        object_owner, owner,
130                        "Gas coin {} does not belong to {}, but {}",
131                        object_id, owner, object_owner
132                    );
133                }
134                if self.is_sui_coin == Some(true) {
135                    let move_obj = object
136                        .bcs
137                        .as_ref()
138                        .unwrap_or_else(|| panic!("Object {} does not have bcs data", object_id))
139                        .try_as_move()
140                        .unwrap_or_else(|| panic!("Object {} is not a move object", object_id));
141
142                    let gas_coin = move_obj.deserialize()?;
143                    return Ok(CheckerResultObject::new(Some(gas_coin), Some(object)));
144                }
145                Ok(CheckerResultObject::new(None, Some(object)))
146            }
147            (None, Some(SuiObjectResponseError::DisplayError { error })) => {
148                panic!("Display Error: {error:?}");
149            }
150            (None, None) | (None, Some(SuiObjectResponseError::Unknown)) => {
151                panic!("Unexpected response: object not found and no specific error provided");
152            }
153        }
154    }
155}
156
157pub struct CheckerResultObject {
158    gas_coin: Option<GasCoin>,
159    object: Option<SuiObjectData>,
160}
161
162impl CheckerResultObject {
163    pub fn new(gas_coin: Option<GasCoin>, object: Option<SuiObjectData>) -> Self {
164        Self { gas_coin, object }
165    }
166    pub fn into_gas_coin(self) -> GasCoin {
167        self.gas_coin.unwrap()
168    }
169    pub fn into_object(self) -> SuiObjectData {
170        self.object.unwrap()
171    }
172}
173
174#[macro_export]
175macro_rules! assert_eq_if_present {
176    ($left:expr, $right:expr, $($arg:tt)+) => {
177        match (&$left, &$right) {
178            (Some(left_val), right_val) => {
179                 if !(&left_val == right_val) {
180                    panic!("{} does not match, left: {:?}, right: {:?}", $($arg)+, left_val, right_val);
181                }
182            }
183            _ => ()
184        }
185    };
186}
187
188#[derive(Default, Debug)]
189pub struct BalanceChangeChecker {
190    owner: Option<Owner>,
191    coin_type: Option<TypeTag>,
192    amount: Option<i128>,
193}
194
195impl BalanceChangeChecker {
196    pub fn new() -> Self {
197        Default::default()
198    }
199
200    pub fn owner(mut self, owner: Owner) -> Self {
201        self.owner = Some(owner);
202        self
203    }
204    pub fn coin_type(mut self, coin_type: &str) -> Self {
205        self.coin_type = Some(parse_sui_type_tag(coin_type).unwrap());
206        self
207    }
208
209    pub fn amount(mut self, amount: i128) -> Self {
210        self.amount = Some(amount);
211        self
212    }
213
214    pub fn check(self, event: &BalanceChange) {
215        let BalanceChange {
216            owner,
217            coin_type,
218            amount,
219        } = event;
220
221        assert_eq_if_present!(self.owner, owner, "owner");
222        assert_eq_if_present!(self.coin_type, coin_type, "coin_type");
223        assert_eq_if_present!(self.amount, amount, "version");
224    }
225}