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