1use 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#[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, 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}