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