iota_cluster_test/
helper.rs1use 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#[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, 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}