Skip to main content

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