1use futures::Stream;
4use serde::Serialize;
5use sui_graphql_macros::Response;
6use sui_sdk_types::Address;
7use sui_sdk_types::TypeTag;
8
9use super::Client;
10use crate::bcs::Bcs;
11use crate::error::Error;
12use crate::move_value::MoveObject;
13use crate::move_value::MoveValue;
14use crate::pagination::Page;
15use crate::pagination::PageInfo;
16use crate::pagination::paginate;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Format {
21 Json,
23 Bcs,
25}
26
27pub struct DynamicFieldsRequest<'a> {
33 client: &'a Client,
34 parent: Address,
35 formats: Vec<Format>,
36}
37
38impl<'a> DynamicFieldsRequest<'a> {
39 pub fn format(mut self, f: Format) -> Self {
42 if !self.formats.contains(&f) {
43 self.formats.push(f);
44 }
45 self
46 }
47
48 pub fn list(self) -> impl Stream<Item = Result<DynamicField, Error>> + 'a {
50 let client = self.client.clone();
51 let formats = self.formats;
52 let parent = self.parent;
53
54 paginate(move |cursor| {
55 let client = client.clone();
56 let formats = formats.clone();
57 async move {
58 client
59 .fetch_dynamic_fields_page_with_formats(parent, cursor.as_deref(), &formats)
60 .await
61 }
62 })
63 }
64}
65
66pub struct DynamicFieldRequest<'a, N> {
68 client: &'a Client,
69 parent: Address,
70 name_type: TypeTag,
71 name: Bcs<N>,
72 field_type: DynamicFieldType,
73 formats: Vec<Format>,
74}
75
76impl<'a, N: Serialize> DynamicFieldRequest<'a, N> {
77 pub fn format(mut self, f: Format) -> Self {
80 if !self.formats.contains(&f) {
81 self.formats.push(f);
82 }
83 self
84 }
85
86 pub async fn get(self) -> Result<Option<DynamicField>, Error> {
88 self.client
89 .fetch_single_dynamic_field(
90 self.parent,
91 self.name_type,
92 self.name,
93 self.field_type,
94 &self.formats,
95 )
96 .await
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106enum DynamicFieldType {
107 Field,
109 Object,
111}
112
113#[derive(Debug, Clone, Response)]
115#[response(root_type = "DynamicFieldValue")]
116pub enum DynamicFieldValue {
117 MoveValue(MoveValue),
118 MoveObject(MoveObject),
119}
120
121#[derive(Debug, Clone, Response)]
123#[response(root_type = "DynamicField")]
124#[non_exhaustive]
125pub struct DynamicField {
126 #[field(path = "name")]
128 pub name: MoveValue,
129 #[field(path = "value")]
131 pub value: DynamicFieldValue,
132}
133
134impl Client {
135 pub fn dynamic_fields(&self, parent: Address) -> DynamicFieldsRequest<'_> {
137 DynamicFieldsRequest {
138 client: self,
139 parent,
140 formats: vec![],
141 }
142 }
143
144 pub fn dynamic_field<N: Serialize>(
146 &self,
147 parent: Address,
148 name_type: TypeTag,
149 name: Bcs<N>,
150 ) -> DynamicFieldRequest<'_, N> {
151 DynamicFieldRequest {
152 client: self,
153 parent,
154 name_type,
155 name,
156 field_type: DynamicFieldType::Field,
157 formats: vec![],
158 }
159 }
160
161 pub fn dynamic_object_field<N: Serialize>(
163 &self,
164 parent: Address,
165 name_type: TypeTag,
166 name: Bcs<N>,
167 ) -> DynamicFieldRequest<'_, N> {
168 DynamicFieldRequest {
169 client: self,
170 parent,
171 name_type,
172 name,
173 field_type: DynamicFieldType::Object,
174 formats: vec![],
175 }
176 }
177
178 async fn fetch_dynamic_fields_page_with_formats(
180 &self,
181 parent: Address,
182 cursor: Option<&str>,
183 formats: &[Format],
184 ) -> Result<Page<DynamicField>, Error> {
185 #[derive(Response)]
186 struct Response {
187 #[field(path = "object?.dynamicFields?.nodes?[]")]
188 nodes: Option<Vec<DynamicField>>,
189 #[field(path = "object?.dynamicFields?.pageInfo?")]
190 page_info: Option<PageInfo>,
191 }
192
193 const QUERY: &str = r#"
194 fragment MoveValueFields on MoveValue {
195 type { repr }
196 json @include(if: $withJson)
197 bcs @include(if: $withBcs)
198 }
199 query($parent: SuiAddress!, $cursor: String, $withJson: Boolean!, $withBcs: Boolean!) {
200 object(address: $parent) {
201 dynamicFields(after: $cursor) {
202 nodes {
203 name { ...MoveValueFields }
204 value {
205 __typename
206 ... on MoveValue { ...MoveValueFields }
207 ... on MoveObject {
208 address
209 contents { ...MoveValueFields }
210 }
211 }
212 }
213 pageInfo {
214 hasNextPage
215 endCursor
216 }
217 }
218 }
219 }
220 "#;
221
222 let with_json = formats.contains(&Format::Json);
223 let with_bcs = formats.is_empty() || formats.contains(&Format::Bcs);
224 let variables = serde_json::json!({
225 "parent": parent,
226 "cursor": cursor,
227 "withJson": with_json,
228 "withBcs": with_bcs,
229 });
230
231 let response = self.query::<Response>(QUERY, variables).await?;
232
233 let Some(data) = response.into_data() else {
234 return Ok(Page::default());
235 };
236
237 let page_info = data.page_info.unwrap_or_default();
238
239 Ok(Page {
240 items: data.nodes.unwrap_or_default(),
241 has_next_page: page_info.has_next_page,
242 end_cursor: page_info.end_cursor,
243 ..Default::default()
244 })
245 }
246
247 async fn fetch_single_dynamic_field<N: Serialize>(
249 &self,
250 parent: Address,
251 name_type: TypeTag,
252 name: Bcs<N>,
253 field_type: DynamicFieldType,
254 formats: &[Format],
255 ) -> Result<Option<DynamicField>, Error> {
256 #[derive(Response)]
257 struct DynamicFieldResponse {
258 #[field(path = "object?.dynamicField?")]
259 field: Option<DynamicField>,
260 }
261
262 #[derive(Response)]
263 struct DynamicObjectFieldResponse {
264 #[field(path = "object?.dynamicObjectField?")]
265 field: Option<DynamicField>,
266 }
267
268 const DYNAMIC_FIELD_QUERY: &str = r#"
269 fragment MoveValueFields on MoveValue {
270 type { repr }
271 json @include(if: $withJson)
272 bcs @include(if: $withBcs)
273 }
274 query($parent: SuiAddress!, $name: DynamicFieldName!, $withJson: Boolean!, $withBcs: Boolean!) {
275 object(address: $parent) {
276 dynamicField(name: $name) {
277 name { ...MoveValueFields }
278 value {
279 ... on MoveValue { ...MoveValueFields }
280 ... on MoveObject {
281 contents { ...MoveValueFields }
282 }
283 }
284 }
285 }
286 }
287 "#;
288
289 const DYNAMIC_OBJECT_FIELD_QUERY: &str = r#"
290 fragment MoveValueFields on MoveValue {
291 type { repr }
292 json @include(if: $withJson)
293 bcs @include(if: $withBcs)
294 }
295 query($parent: SuiAddress!, $name: DynamicFieldName!, $withJson: Boolean!, $withBcs: Boolean!) {
296 object(address: $parent) {
297 dynamicObjectField(name: $name) {
298 name { ...MoveValueFields }
299 value {
300 ... on MoveValue { ...MoveValueFields }
301 ... on MoveObject {
302 contents { ...MoveValueFields }
303 }
304 }
305 }
306 }
307 }
308 "#;
309
310 let with_json = formats.contains(&Format::Json);
311 let with_bcs = formats.is_empty() || formats.contains(&Format::Bcs);
312 let variables = serde_json::json!({
313 "parent": parent,
314 "name": {
315 "type": name_type.to_string(),
316 "bcs": name,
317 },
318 "withJson": with_json,
319 "withBcs": with_bcs,
320 });
321
322 match field_type {
323 DynamicFieldType::Field => {
324 let response = self
325 .query::<DynamicFieldResponse>(DYNAMIC_FIELD_QUERY, variables)
326 .await?;
327 Ok(response.into_data().and_then(|d| d.field))
328 }
329 DynamicFieldType::Object => {
330 let response = self
331 .query::<DynamicObjectFieldResponse>(DYNAMIC_OBJECT_FIELD_QUERY, variables)
332 .await?;
333 Ok(response.into_data().and_then(|d| d.field))
334 }
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use futures::StreamExt;
343 use std::pin::pin;
344 use sui_sdk_types::TypeTag;
345 use wiremock::Mock;
346 use wiremock::MockServer;
347 use wiremock::ResponseTemplate;
348 use wiremock::matchers::method;
349 use wiremock::matchers::path;
350
351 #[tokio::test]
352 async fn test_dynamic_fields_empty() {
353 let mock_server = MockServer::start().await;
354
355 Mock::given(method("POST"))
356 .and(path("/"))
357 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
358 "data": {
359 "object": {
360 "dynamicFields": {
361 "nodes": [],
362 "pageInfo": {
363 "hasNextPage": false,
364 "endCursor": null
365 }
366 }
367 }
368 }
369 })))
370 .mount(&mock_server)
371 .await;
372
373 let client = Client::new(&mock_server.uri()).unwrap();
374
375 let parent: Address = "0x123".parse().unwrap();
376 let mut stream = pin!(client.dynamic_fields(parent).list());
377 let result = stream.next().await;
378 assert!(result.is_none());
379 }
380
381 #[tokio::test]
382 async fn test_dynamic_fields_with_json_format() {
383 let mock_server = MockServer::start().await;
384
385 Mock::given(method("POST"))
386 .and(path("/"))
387 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
388 "data": {
389 "object": {
390 "dynamicFields": {
391 "nodes": [
392 {
393 "name": {
394 "type": { "repr": "u64" },
395 "json": "123"
396 },
397 "value": {
398 "__typename": "MoveValue",
399 "type": { "repr": "0x2::coin::Coin<0x2::sui::SUI>" },
400 "json": { "balance": "1000" }
401 }
402 },
403 {
404 "name": {
405 "type": { "repr": "0x2::kiosk::Listing" },
406 "json": { "id": "0xabc" }
407 },
408 "value": {
409 "__typename": "MoveObject",
410 "address": "0x0000000000000000000000000000000000000000000000000000000000000def",
411 "contents": {
412 "type": { "repr": "0x2::kiosk::Item" },
413 "json": { "price": "500" }
414 }
415 }
416 }
417 ],
418 "pageInfo": {
419 "hasNextPage": false,
420 "endCursor": null
421 }
422 }
423 }
424 }
425 })))
426 .mount(&mock_server)
427 .await;
428
429 let client = Client::new(&mock_server.uri()).unwrap();
430
431 let parent: Address = "0x123".parse().unwrap();
432 let mut stream = pin!(client.dynamic_fields(parent).format(Format::Json).list());
433
434 let field1 = stream.next().await.unwrap().unwrap();
436 assert_eq!(field1.name.type_tag, TypeTag::U64);
437 assert!(field1.name.json.is_some());
438 assert!(field1.name.bcs.is_none()); assert!(matches!(field1.value, DynamicFieldValue::MoveValue(_)));
440
441 let field2 = stream.next().await.unwrap().unwrap();
443 assert_eq!(
444 field2.name.type_tag,
445 "0x2::kiosk::Listing".parse::<TypeTag>().unwrap()
446 );
447 assert!(matches!(field2.value, DynamicFieldValue::MoveObject(_)));
448
449 assert!(stream.next().await.is_none());
451 }
452
453 #[tokio::test]
454 async fn test_dynamic_fields_with_default_bcs() {
455 let mock_server = MockServer::start().await;
456
457 Mock::given(method("POST"))
458 .and(path("/"))
459 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
460 "data": {
461 "object": {
462 "dynamicFields": {
463 "nodes": [
464 {
465 "name": {
466 "type": { "repr": "u64" },
467 "bcs": "ewAAAAAAAAA="
468 },
469 "value": {
470 "__typename": "MoveValue",
471 "type": { "repr": "bool" },
472 "bcs": "AQ=="
473 }
474 }
475 ],
476 "pageInfo": {
477 "hasNextPage": false,
478 "endCursor": null
479 }
480 }
481 }
482 }
483 })))
484 .mount(&mock_server)
485 .await;
486
487 let client = Client::new(&mock_server.uri()).unwrap();
488
489 let parent: Address = "0x123".parse().unwrap();
490 let mut stream = pin!(client.dynamic_fields(parent).list());
492
493 let field = stream.next().await.unwrap().unwrap();
494 assert_eq!(field.name.type_tag, TypeTag::U64);
495 assert!(field.name.bcs.is_some());
496 assert!(field.name.json.is_none()); }
498
499 #[tokio::test]
500 async fn test_dynamic_fields_object_not_found() {
501 let mock_server = MockServer::start().await;
502
503 Mock::given(method("POST"))
504 .and(path("/"))
505 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
506 "data": {
507 "object": null
508 }
509 })))
510 .mount(&mock_server)
511 .await;
512
513 let client = Client::new(&mock_server.uri()).unwrap();
514
515 let parent: Address = "0x999".parse().unwrap();
516 let mut stream = pin!(client.dynamic_fields(parent).list());
517 let result = stream.next().await;
518 assert!(result.is_none());
519 }
520
521 #[tokio::test]
522 async fn test_dynamic_field_fetch() {
523 let mock_server = MockServer::start().await;
524
525 Mock::given(method("POST"))
526 .and(path("/"))
527 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
528 "data": {
529 "object": {
530 "dynamicField": {
531 "name": {
532 "type": { "repr": "u64" },
533 "json": "123",
534 "bcs": "ewAAAAAAAAA="
535 },
536 "value": {
537 "__typename": "MoveValue",
538 "type": { "repr": "bool" },
539 "json": true,
540 "bcs": "AQ=="
541 }
542 }
543 }
544 }
545 })))
546 .mount(&mock_server)
547 .await;
548
549 let client = Client::new(&mock_server.uri()).unwrap();
550 let parent: Address = "0x123".parse().unwrap();
551 let name_type: TypeTag = "u64".parse().unwrap();
552
553 let field = client
554 .dynamic_field(parent, name_type, Bcs(123u64))
555 .format(Format::Json)
556 .format(Format::Bcs)
557 .get()
558 .await
559 .unwrap();
560
561 assert!(field.is_some());
562 let field = field.unwrap();
563 assert_eq!(field.name.type_tag, TypeTag::U64);
564 assert!(field.name.json.is_some());
565 assert!(field.name.bcs.is_some());
566 assert!(matches!(field.value, DynamicFieldValue::MoveValue(_)));
567 }
568
569 #[tokio::test]
570 async fn test_dynamic_field_object_type() {
571 let mock_server = MockServer::start().await;
572
573 Mock::given(method("POST"))
574 .and(path("/"))
575 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
576 "data": {
577 "object": {
578 "dynamicObjectField": {
579 "name": {
580 "type": { "repr": "0x1::string::String" },
581 "json": "my_key"
582 },
583 "value": {
584 "__typename": "MoveObject",
585 "address": "0x0000000000000000000000000000000000000000000000000000000000000abc",
586 "contents": {
587 "type": { "repr": "0x2::coin::Coin<0x2::sui::SUI>" },
588 "json": { "balance": "1000" }
589 }
590 }
591 }
592 }
593 }
594 })))
595 .mount(&mock_server)
596 .await;
597
598 let client = Client::new(&mock_server.uri()).unwrap();
599 let parent: Address = "0x123".parse().unwrap();
600 let name_type: TypeTag = "0x1::string::String".parse().unwrap();
601
602 let field = client
603 .dynamic_object_field(parent, name_type, Bcs("my_key"))
604 .format(Format::Json)
605 .get()
606 .await
607 .unwrap();
608
609 assert!(field.is_some());
610 let field = field.unwrap();
611 let DynamicFieldValue::MoveObject(ref obj) = field.value else {
612 panic!("expected MoveObject variant");
613 };
614 assert_eq!(
615 obj.contents.type_tag,
616 "0x2::coin::Coin<0x2::sui::SUI>".parse::<TypeTag>().unwrap()
617 );
618 }
619
620 #[tokio::test]
621 async fn test_dynamic_field_not_found() {
622 let mock_server = MockServer::start().await;
623
624 Mock::given(method("POST"))
625 .and(path("/"))
626 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
627 "data": {
628 "object": {
629 "dynamicField": null
630 }
631 }
632 })))
633 .mount(&mock_server)
634 .await;
635
636 let client = Client::new(&mock_server.uri()).unwrap();
637 let parent: Address = "0x123".parse().unwrap();
638 let name_type: TypeTag = "u64".parse().unwrap();
639
640 let field = client
641 .dynamic_field(parent, name_type, Bcs(999u64))
642 .get()
643 .await
644 .unwrap();
645
646 assert!(field.is_none());
647 }
648}