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::{
7    BalanceChange, IotaData, IotaObjectData, IotaObjectDataOptions, IotaObjectResponseError,
8};
9use iota_sdk::IotaClient;
10use iota_types::{
11    base_types::{ObjectID, TypeTag},
12    gas_coin::GasCoin,
13    object::Owner,
14    parse_iota_type_tag,
15};
16use tracing::{debug, trace};
17
18/// A util struct that helps verify IOTA Object.
19/// Use builder style to construct the conditions.
20/// When optionals fields are not set, related checks are omitted.
21/// Consuming functions such as `check` perform the check and panics if
22/// verification results are unexpected. `check_into_object` and
23/// `check_into_gas_coin` expect to get a `IotaObjectData` and `GasCoin`
24/// respectfully.
25#[derive(Debug)]
26pub struct ObjectChecker {
27    object_id: ObjectID,
28    owner: Option<Owner>,
29    is_deleted: bool,
30    is_iota_coin: Option<bool>,
31}
32
33impl ObjectChecker {
34    pub fn new(object_id: ObjectID) -> ObjectChecker {
35        Self {
36            object_id,
37            owner: None,
38            is_deleted: false, // default to exist
39            is_iota_coin: None,
40        }
41    }
42
43    pub fn owner(mut self, owner: Owner) -> Self {
44        self.owner = Some(owner);
45        self
46    }
47
48    pub fn deleted(mut self) -> Self {
49        self.is_deleted = true;
50        self
51    }
52
53    pub fn is_iota_coin(mut self, is_iota_coin: bool) -> Self {
54        self.is_iota_coin = Some(is_iota_coin);
55        self
56    }
57
58    pub async fn check_into_gas_coin(self, client: &IotaClient) -> GasCoin {
59        if self.is_iota_coin == Some(false) {
60            panic!("'check_into_gas_coin' shouldn't be called with 'is_iota_coin' set as false");
61        }
62        self.is_iota_coin(true)
63            .check(client)
64            .await
65            .unwrap()
66            .into_gas_coin()
67    }
68
69    pub async fn check_into_object(self, client: &IotaClient) -> IotaObjectData {
70        self.check(client).await.unwrap().into_object()
71    }
72
73    pub async fn check(self, client: &IotaClient) -> Result<CheckerResultObject, anyhow::Error> {
74        debug!(?self);
75
76        let object_id = self.object_id;
77        let object_info = client
78            .read_api()
79            .get_object_with_options(
80                object_id,
81                IotaObjectDataOptions::new()
82                    .with_type()
83                    .with_owner()
84                    .with_bcs(),
85            )
86            .await
87            .or_else(|err| bail!("failed to get object info (id: {object_id}), err: {err}"))?;
88
89        trace!("getting object {object_id}, info :: {object_info:?}");
90
91        match (object_info.data, object_info.error) {
92            (None, Some(IotaObjectResponseError::NotExists { object_id })) => {
93                panic!(
94                    "node can't find gas object {object_id} with client {:?}",
95                    client.read_api()
96                )
97            }
98            (
99                None,
100                Some(IotaObjectResponseError::DynamicFieldNotFound {
101                    parent_object_id: object_id,
102                }),
103            ) => {
104                panic!(
105                    "node can't find dynamic field for {object_id} with client {:?}",
106                    client.read_api()
107                )
108            }
109            (
110                None,
111                Some(IotaObjectResponseError::Deleted {
112                    object_id,
113                    version: _,
114                    digest: _,
115                }),
116            ) => {
117                if !self.is_deleted {
118                    panic!("gas object {object_id} was deleted");
119                }
120                Ok(CheckerResultObject::new(None, None))
121            }
122            (Some(object), _) => {
123                if self.is_deleted {
124                    panic!("expect gas object {object_id} deleted, but it is not");
125                }
126                if let Some(owner) = self.owner {
127                    let object_owner = object
128                        .owner
129                        .unwrap_or_else(|| panic!("object {object_id} does not have owner"));
130                    assert_eq!(
131                        object_owner, owner,
132                        "gas coin {object_id} does not belong to {owner}, but {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 {object_id} does not have bcs data"))
140                        .try_as_move()
141                        .unwrap_or_else(|| panic!("object {object_id} is not a move object"));
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: {left_val:?}, right: {right_val:?}", $($arg)+);
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}