1use futures::Stream;
4use sui_graphql_macros::Response;
5use sui_sdk_types::Address;
6use sui_sdk_types::Object;
7
8use super::Client;
9use crate::bcs::Bcs;
10use crate::error::Error;
11use crate::pagination::Page;
12use crate::pagination::PageInfo;
13use crate::pagination::paginate;
14
15impl Client {
16 pub async fn get_object(&self, object_id: Address) -> Result<Option<Object>, Error> {
42 #[derive(Response)]
43 struct Response {
44 #[field(path = "object?.objectBcs?")]
45 object: Option<Bcs<Object>>,
46 }
47
48 const QUERY: &str = r#"
49 query($id: SuiAddress!) {
50 object(address: $id) {
51 objectBcs
52 }
53 }
54 "#;
55
56 let variables = serde_json::json!({ "id": object_id });
57
58 let response = self.query::<Response>(QUERY, variables).await?;
59
60 Ok(response.into_data().and_then(|d| d.object).map(|b| b.0))
61 }
62
63 pub async fn get_object_at_version(
65 &self,
66 object_id: Address,
67 version: u64,
68 ) -> Result<Option<Object>, Error> {
69 #[derive(Response)]
70 struct Response {
71 #[field(path = "object?.objectBcs?")]
72 object: Option<Bcs<Object>>,
73 }
74
75 const QUERY: &str = r#"
76 query($id: SuiAddress!, $version: UInt53) {
77 object(address: $id, version: $version) {
78 objectBcs
79 }
80 }
81 "#;
82
83 let variables = serde_json::json!({
84 "id": object_id,
85 "version": version,
86 });
87
88 let response = self.query::<Response>(QUERY, variables).await?;
89
90 Ok(response.into_data().and_then(|d| d.object).map(|b| b.0))
91 }
92
93 pub async fn get_object_at_checkpoint(
97 &self,
98 object_id: Address,
99 checkpoint: u64,
100 ) -> Result<Option<Object>, Error> {
101 #[derive(Response)]
102 struct Response {
103 #[field(path = "object?.objectBcs?")]
104 object: Option<Bcs<Object>>,
105 }
106
107 const QUERY: &str = r#"
108 query($id: SuiAddress!, $atCheckpoint: UInt53) {
109 object(address: $id, atCheckpoint: $atCheckpoint) {
110 objectBcs
111 }
112 }
113 "#;
114
115 let variables = serde_json::json!({
116 "id": object_id,
117 "atCheckpoint": checkpoint,
118 });
119
120 let response = self.query::<Response>(QUERY, variables).await?;
121
122 Ok(response.into_data().and_then(|d| d.object).map(|b| b.0))
123 }
124
125 pub async fn get_object_with_root_version(
131 &self,
132 object_id: Address,
133 root_version: u64,
134 ) -> Result<Option<Object>, Error> {
135 #[derive(Response)]
136 struct Response {
137 #[field(path = "object?.objectBcs?")]
138 object: Option<Bcs<Object>>,
139 }
140
141 const QUERY: &str = r#"
142 query($id: SuiAddress!, $rootVersion: UInt53) {
143 object(address: $id, rootVersion: $rootVersion) {
144 objectBcs
145 }
146 }
147 "#;
148
149 let variables = serde_json::json!({
150 "id": object_id,
151 "rootVersion": root_version,
152 });
153
154 let response = self.query::<Response>(QUERY, variables).await?;
155
156 Ok(response.into_data().and_then(|d| d.object).map(|b| b.0))
157 }
158
159 pub fn list_objects(&self, owner: Address) -> impl Stream<Item = Result<Object, Error>> + '_ {
184 let client = self.clone();
185 paginate(move |cursor| {
186 let client = client.clone();
187 async move { client.fetch_objects_page(owner, cursor.as_deref()).await }
188 })
189 }
190
191 async fn fetch_objects_page(
193 &self,
194 owner: Address,
195 cursor: Option<&str>,
196 ) -> Result<Page<Object>, Error> {
197 #[derive(Response)]
198 struct Response {
199 #[field(path = "objects?.pageInfo?")]
200 page_info: Option<PageInfo>,
201 #[field(path = "objects?.nodes?[].objectBcs")]
202 objects: Option<Vec<Bcs<Object>>>,
203 }
204
205 const QUERY: &str = r#"
206 query($owner: SuiAddress!, $after: String) {
207 objects(filter: { owner: $owner }, after: $after) {
208 pageInfo {
209 hasNextPage
210 endCursor
211 }
212 nodes {
213 objectBcs
214 }
215 }
216 }
217 "#;
218
219 let variables = serde_json::json!({
220 "owner": owner,
221 "after": cursor,
222 });
223
224 let response = self.query::<Response>(QUERY, variables).await?;
225
226 let data = response.into_data();
227 let page_info = data
228 .as_ref()
229 .and_then(|d| d.page_info.clone())
230 .unwrap_or_default();
231
232 let objects = data
233 .and_then(|d| d.objects)
234 .unwrap_or_default()
235 .into_iter()
236 .map(|b| b.0)
237 .collect();
238
239 Ok(Page {
240 items: objects,
241 has_next_page: page_info.has_next_page,
242 end_cursor: page_info.end_cursor,
243 ..Default::default()
244 })
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use futures::StreamExt;
252 use std::sync::Arc;
253 use std::sync::atomic::AtomicUsize;
254 use std::sync::atomic::Ordering;
255 use wiremock::Mock;
256 use wiremock::MockServer;
257 use wiremock::ResponseTemplate;
258 use wiremock::matchers::method;
259 use wiremock::matchers::path;
260
261 fn test_object_bcs() -> String {
263 use base64ct::Base64;
264 use base64ct::Encoding;
265
266 const SUI_COIN_BCS: &[u8] = &[
268 0, 1, 1, 32, 79, 43, 0, 0, 0, 0, 0, 40, 35, 95, 175, 213, 151, 87, 206, 190, 35, 131,
269 79, 35, 254, 22, 15, 181, 40, 108, 28, 77, 68, 229, 107, 254, 191, 160, 196, 186, 42,
270 2, 122, 53, 52, 133, 199, 58, 0, 0, 0, 0, 0, 79, 255, 208, 0, 85, 34, 190, 75, 192, 41,
271 114, 76, 127, 15, 110, 215, 9, 58, 107, 243, 160, 155, 144, 230, 47, 97, 220, 21, 24,
272 30, 26, 62, 32, 17, 197, 192, 38, 64, 173, 142, 143, 49, 111, 15, 211, 92, 84, 48, 160,
273 243, 102, 229, 253, 251, 137, 210, 101, 119, 173, 228, 51, 141, 20, 15, 85, 96, 19, 15,
274 0, 0, 0, 0, 0,
275 ];
276 Base64::encode_string(SUI_COIN_BCS)
277 }
278
279 #[tokio::test]
280 async fn test_get_object_not_found() {
281 let mock_server = MockServer::start().await;
282
283 Mock::given(method("POST"))
284 .and(path("/"))
285 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
286 "data": {
287 "object": null
288 }
289 })))
290 .mount(&mock_server)
291 .await;
292
293 let client = Client::new(&mock_server.uri()).unwrap();
294 let object_id: Address = "0x5".parse().unwrap();
295
296 let result = client.get_object(object_id).await;
297 assert!(result.is_ok());
298 assert!(result.unwrap().is_none());
299 }
300
301 #[tokio::test]
302 async fn test_get_object_found() {
303 let mock_server = MockServer::start().await;
304
305 Mock::given(method("POST"))
306 .and(path("/"))
307 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
308 "data": {
309 "object": {
310 "objectBcs": test_object_bcs()
311 }
312 }
313 })))
314 .mount(&mock_server)
315 .await;
316
317 let client = Client::new(&mock_server.uri()).unwrap();
318 let object_id: Address = "0x5".parse().unwrap();
319
320 let result = client.get_object(object_id).await;
321 assert!(result.is_ok());
322 assert!(result.unwrap().is_some());
323 }
324
325 #[tokio::test]
326 async fn test_list_objects_empty() {
327 let mock_server = MockServer::start().await;
328
329 Mock::given(method("POST"))
330 .and(path("/"))
331 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
332 "data": {
333 "objects": {
334 "pageInfo": {
335 "hasNextPage": false,
336 "endCursor": null
337 },
338 "nodes": []
339 }
340 }
341 })))
342 .mount(&mock_server)
343 .await;
344
345 let client = Client::new(&mock_server.uri()).unwrap();
346 let owner: Address = "0x1".parse().unwrap();
347
348 let stream = client.list_objects(owner);
349 let objects: Vec<_> = futures::StreamExt::collect(stream).await;
350
351 assert!(objects.is_empty());
352 }
353
354 #[tokio::test]
355 async fn test_list_objects_with_pagination() {
356 let mock_server = MockServer::start().await;
357 let call_count = Arc::new(AtomicUsize::new(0));
358 let call_count_clone = call_count.clone();
359
360 Mock::given(method("POST"))
361 .and(path("/"))
362 .respond_with(move |_req: &wiremock::Request| {
363 let count = call_count_clone.fetch_add(1, Ordering::SeqCst);
364 match count {
365 0 => ResponseTemplate::new(200).set_body_json(serde_json::json!({
367 "data": {
368 "objects": {
369 "pageInfo": {
370 "hasNextPage": true,
371 "endCursor": "cursor1"
372 },
373 "nodes": [
374 { "objectBcs": test_object_bcs() },
375 { "objectBcs": test_object_bcs() },
376 { "objectBcs": test_object_bcs() }
377 ]
378 }
379 }
380 })),
381 1 => ResponseTemplate::new(200).set_body_json(serde_json::json!({
383 "data": {
384 "objects": {
385 "pageInfo": {
386 "hasNextPage": false,
387 "endCursor": null
388 },
389 "nodes": [
390 { "objectBcs": test_object_bcs() },
391 { "objectBcs": test_object_bcs() }
392 ]
393 }
394 }
395 })),
396 _ => ResponseTemplate::new(200).set_body_json(serde_json::json!({
397 "data": { "objects": { "pageInfo": { "hasNextPage": false, "endCursor": null }, "nodes": [] } }
398 })),
399 }
400 })
401 .mount(&mock_server)
402 .await;
403
404 let client = Client::new(&mock_server.uri()).unwrap();
405 let owner: Address = "0x1".parse().unwrap();
406
407 let stream = client.list_objects(owner);
408 let objects: Vec<_> = futures::StreamExt::collect(stream).await;
409
410 assert_eq!(objects.len(), 5);
412 assert_eq!(call_count.load(Ordering::SeqCst), 2);
413
414 for result in objects {
415 assert!(result.is_ok());
416 }
417 }
418
419 #[tokio::test]
420 async fn test_list_objects_partial_consumption() {
421 let mock_server = MockServer::start().await;
422 let call_count = Arc::new(AtomicUsize::new(0));
423 let call_count_clone = call_count.clone();
424
425 Mock::given(method("POST"))
426 .and(path("/"))
427 .respond_with(move |_req: &wiremock::Request| {
428 let count = call_count_clone.fetch_add(1, Ordering::SeqCst);
429 match count {
430 0 => ResponseTemplate::new(200).set_body_json(serde_json::json!({
432 "data": {
433 "objects": {
434 "pageInfo": {
435 "hasNextPage": true,
436 "endCursor": "cursor1"
437 },
438 "nodes": [
439 { "objectBcs": test_object_bcs() },
440 { "objectBcs": test_object_bcs() },
441 { "objectBcs": test_object_bcs() }
442 ]
443 }
444 }
445 })),
446 1 => ResponseTemplate::new(200).set_body_json(serde_json::json!({
448 "data": {
449 "objects": {
450 "pageInfo": {
451 "hasNextPage": false,
452 "endCursor": null
453 },
454 "nodes": [
455 { "objectBcs": test_object_bcs() },
456 { "objectBcs": test_object_bcs() }
457 ]
458 }
459 }
460 })),
461 _ => panic!("unexpected page request"),
462 }
463 })
464 .mount(&mock_server)
465 .await;
466
467 let client = Client::new(&mock_server.uri()).unwrap();
468 let owner: Address = "0x1".parse().unwrap();
469
470 let stream = client.list_objects(owner).take(3);
472 let objects: Vec<_> = stream.collect().await;
473
474 assert_eq!(objects.len(), 3);
476 assert_eq!(call_count.load(Ordering::SeqCst), 1);
477
478 for result in objects {
479 assert!(result.is_ok());
480 }
481 }
482}