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