iota_cluster_test/
helper.rs

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