1use {
5 crate::{
6 move_bindings::sui_framework::{object::ID, table_vec::TableVec},
7 nexus::error::NexusError,
8 sui::{self, traits::FieldMaskUtil},
9 },
10 anyhow::{anyhow, bail, ensure, Context as _},
11 serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize},
12 std::{
13 collections::{HashMap, HashSet},
14 hash::Hash,
15 sync::Arc,
16 },
17};
18
19lazy_static::lazy_static! {
20 static ref DYNAMIC_FIELD_LIST_REQUESTS: prometheus::CounterVec =
21 prometheus::register_counter_vec!(
22 "nexus_dynamic_field_list_requests_total",
23 "ListDynamicFields requests issued by each typed crawler operation",
24 &["operation"],
25 )
26 .unwrap();
27}
28
29fn observe_dynamic_field_list(operation: &'static str) {
30 DYNAMIC_FIELD_LIST_REQUESTS
31 .with_label_values(&[operation])
32 .inc();
33}
34
35#[derive(Debug, Deserialize)]
36struct DynamicFieldNameBcs<K> {
37 #[allow(unused)]
38 id: sui::types::Address,
39 name: K,
40}
41
42#[derive(Debug, Deserialize, Serialize)]
43struct DynamicFieldValue<K, V> {
44 #[allow(unused)]
45 id: sui::types::Address,
46 #[allow(unused)]
47 name: K,
48 value: V,
49}
50
51#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
56struct DynamicObjectFieldName<K> {
57 name: K,
58}
59
60fn parse_dynamic_field_name<K>(bytes: &[u8]) -> Result<K, bcs::Error>
61where
62 K: DeserializeOwned,
63{
64 bcs::from_bytes::<K>(bytes)
65 .or_else(|_| bcs::from_bytes::<DynamicFieldNameBcs<K>>(bytes).map(|field| field.name))
66}
67
68pub(crate) fn derive_dynamic_field_id<K>(
69 parent_id: sui::types::Address,
70 key: &K,
71 key_type: &sui::types::TypeTag,
72) -> anyhow::Result<sui::types::Address>
73where
74 K: Serialize,
75{
76 let key_bytes = bcs::to_bytes(key).context("Could not encode dynamic field key")?;
77 Ok(parent_id.derive_dynamic_child_id(key_type, &key_bytes))
78}
79
80fn validate_dynamic_field<K, V>(
81 field_id: sui::types::Address,
82 key: &K,
83 field: &DynamicFieldValue<K, V>,
84) -> anyhow::Result<()>
85where
86 K: Eq,
87{
88 if field.id != field_id {
89 bail!("Dynamic field '{field_id}' decoded with an unexpected embedded ID");
90 }
91 if &field.name != key {
92 bail!("Dynamic field '{field_id}' decoded with an unexpected key");
93 }
94 Ok(())
95}
96
97enum FinalizedObjectChange {
98 Unchanged,
99 Written(Box<sui::grpc::Object>),
100 Removed,
101}
102
103fn validate_executed_transaction(
104 executed: &sui::grpc::ExecutedTransaction,
105 transaction: sui::types::Digest,
106) -> anyhow::Result<()> {
107 let observed = executed
108 .digest_opt()
109 .ok_or_else(|| anyhow!("Transaction '{transaction}' response has no digest"))?
110 .parse::<sui::types::Digest>()
111 .map_err(|error| {
112 anyhow!("Transaction '{transaction}' response has an invalid digest: {error}")
113 })?;
114 if observed != transaction {
115 bail!("Requested transaction '{transaction}', received transaction '{observed}'");
116 }
117 Ok(())
118}
119
120fn finalized_transaction_object_change(
121 objects: &[sui::grpc::Object],
122 transaction: sui::types::Digest,
123 object_id: sui::types::Address,
124) -> anyhow::Result<FinalizedObjectChange> {
125 let mut mentioned = false;
126 let mut output = None;
127 for object in objects {
128 if Crawler::parse_object_id(object)? != object_id {
129 continue;
130 }
131 mentioned = true;
132 let previous_transaction = object
133 .previous_transaction_opt()
134 .ok_or_else(|| {
135 anyhow!(
136 "Finalized transaction '{transaction}' returned object '{object_id}' without \
137 its previous transaction"
138 )
139 })?
140 .parse::<sui::types::Digest>()
141 .map_err(|error| {
142 anyhow!(
143 "Finalized transaction '{transaction}' returned object '{object_id}' with an \
144 invalid previous transaction: {error}"
145 )
146 })?;
147 if previous_transaction == transaction && output.replace(object.clone()).is_some() {
148 bail!(
149 "Finalized transaction '{transaction}' returned output object '{object_id}' \
150 more than once"
151 );
152 }
153 }
154
155 match (mentioned, output) {
156 (_, Some(output)) => Ok(FinalizedObjectChange::Written(Box::new(output))),
157 (true, None) => Ok(FinalizedObjectChange::Removed),
158 (false, None) => Ok(FinalizedObjectChange::Unchanged),
159 }
160}
161
162fn reject_removed_state_object(
163 transaction: sui::types::Digest,
164 object_id: sui::types::Address,
165 role: &str,
166) -> ObjectStateSnapshotError {
167 ObjectStateSnapshotError::Invalid(format!(
168 "Finalized transaction '{transaction}' removed the {role} object '{object_id}'"
169 ))
170}
171
172fn dynamic_object_field_wrapper_type(key_type: &sui::types::TypeTag) -> sui::types::TypeTag {
173 sui::types::TypeTag::Struct(Box::new(sui::types::StructTag::new(
174 sui::types::Address::from_static("0x2"),
175 sui::types::Identifier::from_static("dynamic_object_field"),
176 sui::types::Identifier::from_static("Wrapper"),
177 vec![key_type.clone()],
178 )))
179}
180
181#[derive(Clone)]
183pub struct Crawler {
184 client: Arc<sui::grpc::Client>,
185 state_catalog: Arc<sui::grpc::Client>,
186}
187
188#[derive(Debug)]
189pub struct DynamicObjectFieldReference<K> {
190 pub name: K,
191 pub field_id: sui::types::Address,
192 pub child_id: sui::types::Address,
193}
194
195#[derive(Clone, Debug)]
196pub struct DynamicFieldReference<K> {
197 pub name: K,
198 pub field_id: sui::types::Address,
199}
200
201#[derive(Clone, Debug, Eq, PartialEq)]
207pub struct DynamicFieldMetadata {
208 pub field_id: sui::types::Address,
210 pub key_type: sui::types::TypeTag,
212 pub value_type: sui::types::TypeTag,
214}
215
216#[derive(Clone, Debug, Eq, PartialEq)]
221pub struct ObjectMetadata {
222 pub object_id: sui::types::Address,
224 pub owner: sui::types::Owner,
226 pub object_type: sui::types::StructTag,
228}
229
230#[derive(Clone, Debug)]
236pub(crate) struct ObjectStateSnapshot {
237 pub(crate) object: ObjectMetadata,
238 pub(crate) witness: DynamicFieldMetadata,
239 pub(crate) inner: DynamicFieldMetadata,
240 anchor_object: sui::grpc::Object,
241 inner_object: sui::grpc::Object,
242}
243
244impl ObjectStateSnapshot {
245 pub(crate) fn inner_object_reference(&self) -> anyhow::Result<sui::types::ObjectReference> {
246 let object_id = Crawler::parse_object_id(&self.inner_object)?;
247 ensure!(
248 object_id == self.inner.field_id,
249 "State inner object '{}' does not match expected field '{}'",
250 object_id,
251 self.inner.field_id,
252 );
253 let version = self
254 .inner_object
255 .version_opt()
256 .ok_or_else(|| anyhow!("Version missing for state inner object '{object_id}'"))?;
257 let digest = self
258 .inner_object
259 .digest_opt()
260 .ok_or_else(|| anyhow!("Digest missing for state inner object '{object_id}'"))?
261 .parse()
262 .map_err(|_| anyhow!("Invalid digest for state inner object '{object_id}'"))?;
263
264 Ok(sui::types::ObjectReference::new(object_id, version, digest))
265 }
266}
267
268#[derive(Debug)]
270pub(crate) enum ObjectStateSnapshotError {
271 Rpc(anyhow::Error),
273 Invalid(String),
275}
276
277#[derive(Debug)]
278struct ObjectStateObjects {
279 anchor: sui::grpc::Object,
280 witness: sui::grpc::Object,
281 inner: sui::grpc::Object,
282}
283
284fn parse_dynamic_field_metadata(
285 parent_id: sui::types::Address,
286 field: &sui::grpc::DynamicField,
287) -> anyhow::Result<Option<DynamicFieldMetadata>> {
288 match field.kind {
289 Some(kind) if kind == sui::grpc::dynamic_field::DynamicFieldKind::Object as i32 => {
290 return Ok(None);
291 }
292 Some(kind) if kind != sui::grpc::dynamic_field::DynamicFieldKind::Field as i32 => {
293 bail!("Dynamic field for parent '{parent_id}' has unknown kind '{kind}'");
294 }
295 _ => {}
296 }
297
298 let field_id = field
299 .field_id_opt()
300 .ok_or_else(|| anyhow!("Dynamic field ID missing for parent '{parent_id}'"))?
301 .parse()
302 .map_err(|error| anyhow!("Could not parse dynamic field ID: {error}"))?;
303 let object_type = field
304 .field_object_opt()
305 .and_then(|object| object.object_type_opt())
306 .ok_or_else(|| anyhow!("Dynamic field '{field_id}' has no field object type"))?;
307 let (key_type, value_type) = parse_dynamic_field_type(field_id, object_type)?;
308 if let Some(reported_value_type) = field.value_type_opt() {
309 let reported_value_type =
310 reported_value_type
311 .parse::<sui::types::TypeTag>()
312 .map_err(|error| {
313 anyhow!("Dynamic field '{field_id}' has invalid reported value type: {error}")
314 })?;
315 if reported_value_type != value_type {
316 bail!(
317 "Dynamic field '{field_id}' reports value type '{reported_value_type}', but its \
318 field object contains '{value_type}'"
319 );
320 }
321 }
322
323 Ok(Some(DynamicFieldMetadata {
324 field_id,
325 key_type,
326 value_type,
327 }))
328}
329
330fn parse_dynamic_field_type(
331 field_id: sui::types::Address,
332 object_type: &str,
333) -> anyhow::Result<(sui::types::TypeTag, sui::types::TypeTag)> {
334 let field_type = object_type
335 .parse::<sui::types::StructTag>()
336 .map_err(|error| anyhow!("Dynamic field '{field_id}' has invalid type: {error}"))?;
337 if *field_type.address() != sui::types::Address::from_static("0x2")
338 || field_type.module().as_str() != "dynamic_field"
339 || field_type.name().as_str() != "Field"
340 || field_type.type_params().len() != 2
341 {
342 bail!(
343 "Dynamic field '{field_id}' has object type '{field_type}', expected \
344 '0x2::dynamic_field::Field<K, V>'"
345 );
346 }
347
348 let key_type = field_type.type_params()[0].clone();
349 let value_type = field_type.type_params()[1].clone();
350 Ok((key_type, value_type))
351}
352
353#[derive(Clone, Debug, PartialEq, Eq)]
355pub struct DynamicFieldPage<K, V> {
356 data: Vec<(K, V)>,
357 next_cursor: Option<Vec<u8>>,
358}
359
360impl<K, V> DynamicFieldPage<K, V> {
361 pub fn data(&self) -> &[(K, V)] {
363 &self.data
364 }
365
366 pub fn next_cursor(&self) -> Option<&[u8]> {
368 self.next_cursor.as_deref()
369 }
370
371 pub fn into_parts(self) -> (Vec<(K, V)>, Option<Vec<u8>>) {
373 (self.data, self.next_cursor)
374 }
375}
376
377#[derive(Clone, Debug)]
379pub struct OwnedObjectPage<T> {
380 data: Vec<Response<T>>,
381 next_cursor: Option<Vec<u8>>,
382}
383
384fn is_owned_by_address(owner: &sui::types::Owner, address: sui::types::Address) -> bool {
385 match owner {
386 sui::types::Owner::Address(owner) | sui::types::Owner::ConsensusAddress { owner, .. } => {
387 *owner == address
388 }
389 _ => false,
390 }
391}
392
393fn matches_expected_owner(observed: &sui::types::Owner, expected: &sui::types::Owner) -> bool {
394 match expected {
395 sui::types::Owner::Address(address) => is_owned_by_address(observed, *address),
396 _ => observed == expected,
397 }
398}
399
400impl<T> OwnedObjectPage<T> {
401 pub fn data(&self) -> &[Response<T>] {
403 &self.data
404 }
405
406 pub fn next_cursor(&self) -> Option<&[u8]> {
408 self.next_cursor.as_deref()
409 }
410
411 pub fn into_parts(self) -> (Vec<Response<T>>, Option<Vec<u8>>) {
413 (self.data, self.next_cursor)
414 }
415}
416
417#[derive(Clone, Debug, PartialEq, Eq)]
420pub struct ObjectUpdateReference {
421 pub owner: sui::types::Owner,
422 pub object_type: sui::types::StructTag,
423 pub version: sui::types::Version,
424 pub digest: sui::types::Digest,
425 pub previous_transaction: sui::types::Digest,
426}
427
428#[derive(Clone, Debug)]
430pub struct TransactionUpdate {
431 pub digest: sui::types::Digest,
432 pub checkpoint: u64,
433 pub effects: sui::types::TransactionEffectsV2,
434 pub events: Vec<sui::types::Event>,
435}
436
437impl Crawler {
438 pub fn new(client: Arc<sui::grpc::Client>) -> Self {
439 Self {
440 state_catalog: Arc::clone(&client),
441 client,
442 }
443 }
444
445 pub fn with_state_catalog(
451 client: Arc<sui::grpc::Client>,
452 state_catalog: Arc<sui::grpc::Client>,
453 ) -> Self {
454 Self {
455 client,
456 state_catalog,
457 }
458 }
459
460 pub(crate) fn grpc_client(&self) -> Arc<sui::grpc::Client> {
461 Arc::clone(&self.client)
462 }
463
464 pub(crate) fn clone_grpc_client(&self) -> sui::grpc::Client {
465 self.client.as_ref().clone()
466 }
467
468 fn clone_state_catalog_client(&self) -> sui::grpc::Client {
469 self.state_catalog.as_ref().clone()
470 }
471
472 pub async fn observe_object_metadata(
481 &self,
482 object_id: sui::types::Address,
483 ) -> anyhow::Result<ObjectMetadata> {
484 let object = self
485 .fetch_object(
486 object_id,
487 sui::grpc::FieldMask::from_paths(["object_id", "owner", "object_type"]),
488 )
489 .await?;
490 Self::parse_observed_object_metadata(object_id, &object)
491 }
492
493 fn parse_observed_object_metadata(
494 object_id: sui::types::Address,
495 object: &sui::grpc::Object,
496 ) -> anyhow::Result<ObjectMetadata> {
497 let returned_id = Self::parse_object_id(object)?;
498 if returned_id != object_id {
499 bail!("Requested object '{object_id}', received object '{returned_id}'");
500 }
501 let owner = object
502 .owner_opt()
503 .ok_or_else(|| anyhow!("Owner missing for object '{object_id}'"))?
504 .try_into()
505 .map_err(|_| anyhow!("Could not parse owner for object '{object_id}'"))?;
506 let object_type = object
507 .object_type_opt()
508 .ok_or_else(|| anyhow!("Object type missing for object '{object_id}'"))?
509 .parse()
510 .map_err(|error| anyhow!("Could not parse type for object '{object_id}': {error}"))?;
511
512 Ok(ObjectMetadata {
513 object_id,
514 owner,
515 object_type,
516 })
517 }
518
519 pub(crate) async fn observe_object_state_fields(
526 &self,
527 object_id: sui::types::Address,
528 witness_field_id: sui::types::Address,
529 inner_field_id: sui::types::Address,
530 ) -> anyhow::Result<Option<(ObjectMetadata, Vec<DynamicFieldMetadata>)>> {
531 let field_mask = sui::grpc::FieldMask::from_paths(["object_id", "owner", "object_type"]);
532 let Some(objects) = self
533 .fetch_object_state_objects(object_id, witness_field_id, inner_field_id, field_mask)
534 .await?
535 else {
536 return Ok(None);
537 };
538
539 let object = Self::parse_observed_object_metadata(object_id, &objects.anchor)?;
540 let witness =
541 Self::parse_state_field_metadata(object_id, witness_field_id, &objects.witness)?;
542 let inner = Self::parse_state_field_metadata(object_id, inner_field_id, &objects.inner)?;
543
544 Ok(Some((object, vec![witness, inner])))
545 }
546
547 pub(crate) async fn object_state_snapshot(
553 &self,
554 object_id: sui::types::Address,
555 witness_field_id: sui::types::Address,
556 inner_field_id: sui::types::Address,
557 ) -> Result<Option<ObjectStateSnapshot>, ObjectStateSnapshotError> {
558 let field_mask = sui::grpc::FieldMask::from_paths([
559 "object_id",
560 "owner",
561 "object_type",
562 "version",
563 "digest",
564 "balance",
565 "contents",
566 ]);
567 let Some(objects) = self
568 .fetch_object_state_objects(object_id, witness_field_id, inner_field_id, field_mask)
569 .await
570 .map_err(ObjectStateSnapshotError::Rpc)?
571 else {
572 return Ok(None);
573 };
574 let invalid = |error: anyhow::Error| ObjectStateSnapshotError::Invalid(error.to_string());
575 let object =
576 Self::parse_observed_object_metadata(object_id, &objects.anchor).map_err(invalid)?;
577 let witness =
578 Self::parse_state_field_metadata(object_id, witness_field_id, &objects.witness)
579 .map_err(invalid)?;
580 let inner = Self::parse_state_field_metadata(object_id, inner_field_id, &objects.inner)
581 .map_err(invalid)?;
582
583 Ok(Some(ObjectStateSnapshot {
584 object,
585 witness,
586 inner,
587 anchor_object: objects.anchor,
588 inner_object: objects.inner,
589 }))
590 }
591
592 pub(crate) fn object_state_snapshot_from_transaction_view(
599 &self,
600 object_id: sui::types::Address,
601 witness_field_id: sui::types::Address,
602 inner_field_id: sui::types::Address,
603 executed: &sui::grpc::ExecutedTransaction,
604 transaction: sui::types::Digest,
605 ) -> Result<Option<ObjectStateSnapshot>, ObjectStateSnapshotError> {
606 let invalid = |error: anyhow::Error| ObjectStateSnapshotError::Invalid(error.to_string());
607 validate_executed_transaction(executed, transaction).map_err(invalid)?;
608
609 let find =
610 |id| -> anyhow::Result<Option<sui::grpc::Object>> {
611 let mut matches = executed.objects().objects.iter().filter_map(|object| {
612 match Self::parse_object_id(object) {
613 Ok(returned) if returned == id => Some(Ok(object.clone())),
614 Ok(_) => None,
615 Err(error) => Some(Err(error)),
616 }
617 });
618 let Some(object) = matches.next().transpose()? else {
619 return Ok(None);
620 };
621 ensure!(
622 matches.next().transpose()?.is_none(),
623 "Causal transaction view contains object '{id}' more than once"
624 );
625 Ok(Some(object))
626 };
627
628 let Some(anchor) = find(object_id).map_err(invalid)? else {
629 return Ok(None);
630 };
631 let Some(witness_object) = find(witness_field_id).map_err(invalid)? else {
632 return Ok(None);
633 };
634 let Some(inner_object) = find(inner_field_id).map_err(invalid)? else {
635 return Ok(None);
636 };
637
638 let object = Self::parse_observed_object_metadata(object_id, &anchor).map_err(invalid)?;
639 let witness =
640 Self::parse_state_field_metadata(object_id, witness_field_id, &witness_object)
641 .map_err(invalid)?;
642 let inner = Self::parse_state_field_metadata(object_id, inner_field_id, &inner_object)
643 .map_err(invalid)?;
644
645 Ok(Some(ObjectStateSnapshot {
646 object,
647 witness,
648 inner,
649 anchor_object: anchor,
650 inner_object,
651 }))
652 }
653
654 async fn fetch_object_state_objects(
655 &self,
656 object_id: sui::types::Address,
657 witness_field_id: sui::types::Address,
658 inner_field_id: sui::types::Address,
659 field_mask: sui::grpc::FieldMask,
660 ) -> anyhow::Result<Option<ObjectStateObjects>> {
661 let object_ids = [object_id, witness_field_id, inner_field_id];
662 let results = self.fetch_object_results(&object_ids, field_mask).await?;
663 if results.len() != object_ids.len() {
664 bail!(
665 "Batch object response contained {} results for {} state objects",
666 results.len(),
667 object_ids.len()
668 );
669 }
670
671 let mut results = results.into_iter();
672 let anchor = results
673 .next()
674 .expect("response length was validated")
675 .to_result()
676 .map_err(|status| {
677 if status.code == i32::from(tonic::Code::NotFound) {
678 anyhow::Error::new(NexusError::ObjectNotFound { object: object_id })
679 } else {
680 anyhow!("Could not fetch object '{object_id}': {}", status.message)
681 }
682 })?;
683 let field = |field_id, result: sui::grpc::GetObjectResult| match result.to_result() {
684 Ok(field) => Ok(Some(field)),
685 Err(status) if status.code == i32::from(tonic::Code::NotFound) => Ok(None),
686 Err(status) => Err(anyhow!(
687 "Could not fetch dynamic field '{field_id}': {}",
688 status.message
689 )),
690 };
691 let Some(witness) = field(
692 witness_field_id,
693 results.next().expect("response length was validated"),
694 )?
695 else {
696 return Ok(None);
697 };
698 let Some(inner) = field(
699 inner_field_id,
700 results.next().expect("response length was validated"),
701 )?
702 else {
703 return Ok(None);
704 };
705
706 Ok(Some(ObjectStateObjects {
707 anchor,
708 witness,
709 inner,
710 }))
711 }
712
713 fn parse_state_field_metadata(
714 parent_id: sui::types::Address,
715 field_id: sui::types::Address,
716 field: &sui::grpc::Object,
717 ) -> anyhow::Result<DynamicFieldMetadata> {
718 let returned_id = Self::parse_object_id(field)?;
719 if returned_id != field_id {
720 bail!("Requested dynamic field '{field_id}', received '{returned_id}'");
721 }
722 let owner: sui::types::Owner = field
723 .owner_opt()
724 .ok_or_else(|| anyhow!("Owner missing for dynamic field '{field_id}'"))?
725 .try_into()
726 .map_err(|_| anyhow!("Could not parse owner for dynamic field '{field_id}'"))?;
727 if owner != sui::types::Owner::Object(parent_id) {
728 bail!(
729 "Dynamic field '{field_id}' has owner '{owner:?}', expected object '{parent_id}'"
730 );
731 }
732 let object_type = field
733 .object_type_opt()
734 .ok_or_else(|| anyhow!("Object type missing for dynamic field '{field_id}'"))?;
735 let (key_type, value_type) = parse_dynamic_field_type(field_id, object_type)?;
736 Ok(DynamicFieldMetadata {
737 field_id,
738 key_type,
739 value_type,
740 })
741 }
742
743 pub(crate) fn decode_object_state_snapshot<A, K, V>(
745 &self,
746 snapshot: &ObjectStateSnapshot,
747 key: &K,
748 ) -> anyhow::Result<(Response<A>, V)>
749 where
750 A: DeserializeOwned,
751 K: DeserializeOwned + Eq,
752 V: DeserializeOwned,
753 {
754 let object_id = snapshot.object.object_id;
755 let (owner, digest, version, balance) =
756 self.parse_object_metadata(object_id, &snapshot.anchor_object)?;
757 let data = self.parse_object_contents_bcs::<A>(&snapshot.anchor_object)?;
758 let field =
759 self.parse_object_contents_bcs::<DynamicFieldValue<K, V>>(&snapshot.inner_object)?;
760 validate_dynamic_field(snapshot.inner.field_id, key, &field)?;
761 Ok((
762 Response {
763 object_id,
764 owner,
765 version,
766 data,
767 digest,
768 balance,
769 },
770 field.value,
771 ))
772 }
773
774 pub async fn get_dynamic_field_metadata(
785 &self,
786 parent_id: sui::types::Address,
787 ) -> anyhow::Result<Vec<DynamicFieldMetadata>> {
788 let field_mask = sui::grpc::FieldMask::from_paths([
789 "kind",
790 "field_id",
791 "field_object.object_type",
792 "value_type",
793 ]);
794 let mut metadata = Vec::new();
795 let mut page_token = None;
796 let client = self.clone_state_catalog_client();
797
798 loop {
799 observe_dynamic_field_list("state_metadata");
800 let mut request = sui::grpc::ListDynamicFieldsRequest::default()
801 .with_parent(parent_id)
802 .with_page_size(1000)
803 .with_read_mask(field_mask.clone());
804 if let Some(token) = page_token {
805 request = request.with_page_token(token);
806 }
807
808 let response = sui::grpc::retry_read(|| {
809 let mut client = client.clone();
810 let request = request.clone();
811 async move { client.state_client().list_dynamic_fields(request).await }
812 })
813 .await
814 .map(|response| response.into_inner())
815 .map_err(anyhow::Error::new)
816 .with_context(|| format!("Could not fetch dynamic fields for parent '{parent_id}'"))?;
817 page_token = response.next_page_token;
818 metadata.extend(
819 response
820 .dynamic_fields
821 .iter()
822 .map(|field| parse_dynamic_field_metadata(parent_id, field))
823 .collect::<anyhow::Result<Vec<_>>>()?
824 .into_iter()
825 .flatten(),
826 );
827
828 if page_token.is_none() {
829 break;
830 }
831 }
832
833 Ok(metadata)
834 }
835
836 pub async fn get_package(
838 &self,
839 package_id: sui::types::Address,
840 ) -> anyhow::Result<sui::grpc::Package> {
841 let request = sui::grpc::GetPackageRequest::default().with_package_id(package_id);
842 let mut client = self.clone_grpc_client();
843 client
844 .package_client()
845 .get_package(request)
846 .await
847 .map_err(|e| anyhow!("Could not fetch package '{package_id}': {e}"))?
848 .into_inner()
849 .package
850 .ok_or_else(|| anyhow!("Package '{package_id}' was not returned"))
851 }
852
853 async fn get_object_parsed<T>(
854 &self,
855 object_id: sui::types::Address,
856 field_mask: sui::grpc::FieldMask,
857 parse_data: fn(&Crawler, &sui::grpc::Object) -> anyhow::Result<T>,
858 ) -> anyhow::Result<Response<T>>
859 where
860 T: DeserializeOwned,
861 {
862 let object = self.fetch_object(object_id, field_mask).await?;
863
864 let (owner, digest, version, balance) = self.parse_object_metadata(object_id, &object)?;
865 let data = parse_data(self, &object)?;
866
867 Ok(Response {
868 object_id,
869 owner,
870 version,
871 data,
872 digest,
873 balance,
874 })
875 }
876
877 async fn get_objects_parsed<T>(
878 &self,
879 object_ids: &[sui::types::Address],
880 field_mask: sui::grpc::FieldMask,
881 parse_data: fn(&Crawler, &sui::grpc::Object) -> anyhow::Result<T>,
882 ) -> anyhow::Result<Vec<Response<T>>>
883 where
884 T: DeserializeOwned,
885 {
886 let objects = self.fetch_objects(object_ids, field_mask).await?;
887
888 objects
889 .into_iter()
890 .map(|object| {
891 let object_id = Self::parse_object_id(&object)?;
892 let (owner, digest, version, balance) =
893 self.parse_object_metadata(object_id, &object)?;
894 let data = parse_data(self, &object)?;
895
896 Ok(Response {
897 object_id,
898 owner,
899 version,
900 data,
901 digest,
902 balance,
903 })
904 })
905 .collect()
906 }
907
908 async fn get_optional_objects_parsed<T>(
909 &self,
910 object_ids: &[sui::types::Address],
911 field_mask: sui::grpc::FieldMask,
912 parse_data: fn(&Crawler, &sui::grpc::Object) -> anyhow::Result<T>,
913 ) -> anyhow::Result<Vec<Option<Response<T>>>>
914 where
915 T: DeserializeOwned,
916 {
917 let results = self.fetch_object_results(object_ids, field_mask).await?;
918 if results.len() != object_ids.len() {
919 bail!(
920 "Batch object response contained {} results for {} requests",
921 results.len(),
922 object_ids.len()
923 );
924 }
925
926 object_ids
927 .iter()
928 .copied()
929 .zip(results)
930 .map(|(requested_id, result)| {
931 let object = match result.to_result() {
932 Ok(object) => object,
933 Err(status) if status.code == i32::from(tonic::Code::NotFound) => {
934 return Ok(None);
935 }
936 Err(status) => {
937 bail!(
938 "Could not fetch object '{requested_id}': {}",
939 status.message
940 );
941 }
942 };
943 let object_id = Self::parse_object_id(&object)?;
944 if object_id != requested_id {
945 bail!("Requested object '{requested_id}', received object '{object_id}'");
946 }
947 let (owner, digest, version, balance) =
948 self.parse_object_metadata(object_id, &object)?;
949 let data = parse_data(self, &object)?;
950 Ok(Some(Response {
951 object_id,
952 owner,
953 version,
954 data,
955 digest,
956 balance,
957 }))
958 })
959 .collect()
960 }
961
962 fn parse_object_id(object: &sui::grpc::Object) -> anyhow::Result<sui::types::Address> {
963 object
964 .object_id_opt()
965 .ok_or_else(|| anyhow!("Object ID missing"))?
966 .parse()
967 .map_err(|_| anyhow!("Could not parse object ID"))
968 }
969
970 pub async fn get_object<T>(&self, object_id: sui::types::Address) -> anyhow::Result<Response<T>>
972 where
973 T: DeserializeOwned,
974 {
975 let field_mask = sui::grpc::FieldMask::from_paths([
976 "object_id",
977 "owner",
978 "version",
979 "digest",
980 "balance",
981 "contents",
982 ]);
983
984 self.get_object_parsed(object_id, field_mask, Self::parse_object_contents_bcs::<T>)
985 .await
986 }
987
988 pub(crate) async fn get_object_with_dynamic_field<A, K, V>(
994 &self,
995 object_id: sui::types::Address,
996 expected_object_type: &sui::types::StructTag,
997 field_id: sui::types::Address,
998 expected_value_type: &sui::types::TypeTag,
999 key: &K,
1000 ) -> anyhow::Result<(Response<A>, V)>
1001 where
1002 A: DeserializeOwned,
1003 K: DeserializeOwned + Eq,
1004 V: DeserializeOwned,
1005 {
1006 let field_mask = sui::grpc::FieldMask::from_paths([
1007 "object_id",
1008 "owner",
1009 "object_type",
1010 "version",
1011 "digest",
1012 "balance",
1013 "contents",
1014 ]);
1015 let requested = [object_id, field_id];
1016 let results = self.fetch_object_results(&requested, field_mask).await?;
1017 if results.len() != requested.len() {
1018 bail!(
1019 "Batch object response contained {} results for {} requests",
1020 results.len(),
1021 requested.len()
1022 );
1023 }
1024 let mut results = results.into_iter();
1025 let object = results
1026 .next()
1027 .expect("response length was validated")
1028 .to_result()
1029 .map_err(|status| {
1030 anyhow!("Could not fetch object '{object_id}': {}", status.message)
1031 })?;
1032 let field = results
1033 .next()
1034 .expect("response length was validated")
1035 .to_result()
1036 .map_err(|status| {
1037 anyhow!(
1038 "Could not fetch dynamic field '{field_id}': {}",
1039 status.message
1040 )
1041 })?;
1042
1043 let returned_object_id = Self::parse_object_id(&object)?;
1044 if returned_object_id != object_id {
1045 bail!("Requested object '{object_id}', received object '{returned_object_id}'");
1046 }
1047 let object_type = object
1048 .object_type_opt()
1049 .ok_or_else(|| anyhow!("Object type missing for object '{object_id}'"))?
1050 .parse::<sui::types::StructTag>()
1051 .map_err(|error| anyhow!("Could not parse type for object '{object_id}': {error}"))?;
1052 if &object_type != expected_object_type {
1053 bail!(
1054 "Object '{object_id}' has type '{object_type}', expected '{expected_object_type}'"
1055 );
1056 }
1057
1058 let returned_field_id = Self::parse_object_id(&field)?;
1059 if returned_field_id != field_id {
1060 bail!("Requested dynamic field '{field_id}', received '{returned_field_id}'");
1061 }
1062 let field_owner: sui::types::Owner = field
1063 .owner_opt()
1064 .ok_or_else(|| anyhow!("Owner missing for dynamic field '{field_id}'"))?
1065 .try_into()
1066 .map_err(|_| anyhow!("Could not parse owner for dynamic field '{field_id}'"))?;
1067 if field_owner != sui::types::Owner::Object(object_id) {
1068 bail!(
1069 "Dynamic field '{field_id}' has owner '{field_owner:?}', expected object \
1070 '{object_id}'"
1071 );
1072 }
1073 let field_type = field
1074 .object_type_opt()
1075 .ok_or_else(|| anyhow!("Object type missing for dynamic field '{field_id}'"))?;
1076 let (_, value_type) = parse_dynamic_field_type(field_id, field_type)?;
1077 if &value_type != expected_value_type {
1078 bail!(
1079 "Dynamic field '{field_id}' has value type '{value_type}', expected \
1080 '{expected_value_type}'"
1081 );
1082 }
1083
1084 let (owner, digest, version, balance) = self.parse_object_metadata(object_id, &object)?;
1085 let data = self.parse_object_contents_bcs::<A>(&object)?;
1086 let field = self.parse_object_contents_bcs::<DynamicFieldValue<K, V>>(&field)?;
1087 validate_dynamic_field(field_id, key, &field)?;
1088 Ok((
1089 Response::<A> {
1090 object_id,
1091 owner,
1092 version,
1093 data,
1094 digest,
1095 balance,
1096 },
1097 field.value,
1098 ))
1099 }
1100
1101 pub async fn get_optional_object<T>(
1108 &self,
1109 object_id: sui::types::Address,
1110 ) -> anyhow::Result<Option<Response<T>>>
1111 where
1112 T: DeserializeOwned,
1113 {
1114 let field_mask = sui::grpc::FieldMask::from_paths([
1115 "object_id",
1116 "owner",
1117 "version",
1118 "digest",
1119 "balance",
1120 "contents",
1121 ]);
1122 let Some(object) = self.fetch_optional_object(object_id, field_mask).await? else {
1123 return Ok(None);
1124 };
1125 let returned_id = Self::parse_object_id(&object)?;
1126 if returned_id != object_id {
1127 bail!("Requested object '{object_id}', received object '{returned_id}'");
1128 }
1129 let (owner, digest, version, balance) = self.parse_object_metadata(object_id, &object)?;
1130 let data = self.parse_object_contents_bcs::<T>(&object)?;
1131 Ok(Some(Response {
1132 object_id,
1133 owner,
1134 version,
1135 data,
1136 digest,
1137 balance,
1138 }))
1139 }
1140
1141 pub async fn get_objects<T>(
1143 &self,
1144 object_ids: &[sui::types::Address],
1145 ) -> anyhow::Result<Vec<Response<T>>>
1146 where
1147 T: DeserializeOwned,
1148 {
1149 let field_mask = sui::grpc::FieldMask::from_paths([
1150 "object_id",
1151 "owner",
1152 "version",
1153 "digest",
1154 "balance",
1155 "contents",
1156 ]);
1157
1158 self.get_objects_parsed(object_ids, field_mask, Self::parse_object_contents_bcs::<T>)
1159 .await
1160 }
1161
1162 pub async fn get_optional_objects<T>(
1172 &self,
1173 object_ids: &[sui::types::Address],
1174 ) -> anyhow::Result<Vec<Option<Response<T>>>>
1175 where
1176 T: DeserializeOwned,
1177 {
1178 if object_ids.is_empty() {
1179 return Ok(Vec::new());
1180 }
1181
1182 let field_mask = sui::grpc::FieldMask::from_paths([
1183 "object_id",
1184 "owner",
1185 "version",
1186 "digest",
1187 "balance",
1188 "contents",
1189 ]);
1190
1191 self.get_optional_objects_parsed(
1192 object_ids,
1193 field_mask,
1194 Self::parse_object_contents_bcs::<T>,
1195 )
1196 .await
1197 }
1198
1199 pub async fn get_chain_id(&self) -> anyhow::Result<String> {
1206 let digest = self.get_chain_digest().await?;
1207 Ok(hex::encode(&digest.as_bytes()[..4]))
1208 }
1209
1210 pub async fn get_chain_digest(&self) -> anyhow::Result<sui::types::Digest> {
1216 let mut client = self.clone_grpc_client();
1217 let response = client
1218 .ledger_client()
1219 .get_service_info(sui::grpc::GetServiceInfoRequest::default())
1220 .await
1221 .map_err(|e| anyhow!("failed to fetch service info from the connected RPC: {e}"))?;
1222 let base58 = response
1223 .into_inner()
1224 .chain_id
1225 .ok_or_else(|| anyhow!("connected RPC did not return a chain id in service info"))?;
1226 sui::types::Digest::from_base58(&base58)
1227 .map_err(|e| anyhow!("connected RPC returned an unparsable chain id '{base58}': {e}"))
1228 }
1229
1230 pub async fn get_object_metadata(
1232 &self,
1233 object_id: sui::types::Address,
1234 ) -> anyhow::Result<Response<()>> {
1235 let field_mask = sui::grpc::FieldMask::from_paths([
1236 "object_id",
1237 "owner",
1238 "version",
1239 "digest",
1240 "balance",
1241 ]);
1242
1243 let object = self.fetch_object(object_id, field_mask).await?;
1244 let (owner, digest, version, balance) = self.parse_object_metadata(object_id, &object)?;
1245
1246 Ok(Response {
1247 object_id,
1248 owner,
1249 version,
1250 data: (),
1251 digest,
1252 balance,
1253 })
1254 }
1255
1256 pub async fn get_object_update_reference(
1260 &self,
1261 object_id: sui::types::Address,
1262 version: Option<sui::types::Version>,
1263 ) -> anyhow::Result<ObjectUpdateReference> {
1264 let mut request = sui::grpc::GetObjectRequest::default()
1265 .with_object_id(object_id)
1266 .with_read_mask(sui::grpc::FieldMask::from_paths([
1267 "object_id",
1268 "owner",
1269 "object_type",
1270 "version",
1271 "digest",
1272 "previous_transaction",
1273 ]));
1274 if let Some(version) = version {
1275 request = request.with_version(version);
1276 }
1277
1278 let client = self.clone_grpc_client();
1279 let object = sui::grpc::retry_read(|| {
1280 let mut client = client.clone();
1281 let request = request.clone();
1282 async move { client.ledger_client().get_object(request).await }
1283 })
1284 .await
1285 .map(|response| response.into_inner().object)
1286 .with_context(|| {
1287 let version = version
1288 .map(|version| format!(" at version {version}"))
1289 .unwrap_or_default();
1290 format!("Could not fetch object '{object_id}'{version}")
1291 })?
1292 .ok_or_else(|| {
1293 let version = version
1294 .map(|version| format!(" at version {version}"))
1295 .unwrap_or_default();
1296 anyhow!("Object '{object_id}'{version} not found")
1297 })?;
1298
1299 let (owner, digest, observed_version, _) =
1300 self.parse_object_metadata(object_id, &object)?;
1301 let object_type = object
1302 .object_type_opt()
1303 .ok_or_else(|| anyhow!("Object type missing for object '{object_id}'"))?
1304 .parse()
1305 .map_err(|e| anyhow!("Could not parse object type for object '{object_id}': {e}"))?;
1306 if version.is_some_and(|requested| requested != observed_version) {
1307 bail!(
1308 "Requested object '{object_id}' at version {}, received version {observed_version}",
1309 version.expect("checked as some")
1310 );
1311 }
1312 let previous_transaction = object
1313 .previous_transaction_opt()
1314 .ok_or_else(|| {
1315 anyhow!(
1316 "Object '{object_id}' at version {observed_version} has no previous_transaction"
1317 )
1318 })?
1319 .parse()
1320 .map_err(|e| {
1321 anyhow!(
1322 "Object '{object_id}' at version {observed_version} has an invalid previous_transaction: {e}"
1323 )
1324 })?;
1325
1326 Ok(ObjectUpdateReference {
1327 owner,
1328 object_type,
1329 version: observed_version,
1330 digest,
1331 previous_transaction,
1332 })
1333 }
1334
1335 pub async fn get_transaction_update(
1337 &self,
1338 digest: sui::types::Digest,
1339 ) -> anyhow::Result<TransactionUpdate> {
1340 let request = sui::grpc::GetTransactionRequest::default()
1341 .with_digest(digest.to_string())
1342 .with_read_mask(sui::grpc::FieldMask::from_paths([
1343 "digest",
1344 "checkpoint",
1345 "effects.bcs",
1346 "events.events",
1347 ]));
1348 let client = self.clone_grpc_client();
1349 let transaction = sui::grpc::retry_read(|| {
1350 let mut client = client.clone();
1351 let request = request.clone();
1352 async move { client.ledger_client().get_transaction(request).await }
1353 })
1354 .await
1355 .map(|response| response.into_inner().transaction)
1356 .with_context(|| format!("Could not fetch transaction '{digest}'"))?
1357 .ok_or_else(|| anyhow!("Transaction '{digest}' not found"))?;
1358
1359 let observed_digest = transaction
1360 .digest_opt()
1361 .ok_or_else(|| anyhow!("Failed to get Executed Transaction for digest '{digest}'"))?
1362 .parse()
1363 .map_err(|e| anyhow!("Transaction '{digest}' response has an invalid digest: {e}"))?;
1364 if observed_digest != digest {
1365 bail!("Requested transaction '{digest}', received transaction '{observed_digest}'");
1366 }
1367 let checkpoint = transaction
1368 .checkpoint_opt()
1369 .ok_or_else(|| anyhow!("Transaction '{digest}' response has no checkpoint"))?;
1370
1371 let effects = match sui::types::TransactionEffects::try_from(transaction.effects())
1372 .map_err(|e| anyhow!("Could not decode effects for transaction '{digest}': {e}"))?
1373 {
1374 sui::types::TransactionEffects::V2(effects) => *effects,
1375 sui::types::TransactionEffects::V1(_) => {
1376 bail!("Transaction '{digest}' returned unsupported V1 effects")
1377 }
1378 };
1379 if effects.transaction_digest != observed_digest {
1380 bail!(
1381 "Transaction '{observed_digest}' response contains effects for transaction '{}'; expected the effects transaction digest to match the requested transaction '{digest}'",
1382 effects.transaction_digest
1383 );
1384 }
1385 let events = sui::types::TransactionEvents::try_from(transaction.events())
1386 .map_err(|e| anyhow!("Could not decode events for transaction '{digest}': {e}"))?
1387 .0;
1388
1389 Ok(TransactionUpdate {
1390 digest: observed_digest,
1391 checkpoint,
1392 effects,
1393 events,
1394 })
1395 }
1396
1397 pub async fn get_transaction_objects(
1407 &self,
1408 transaction: sui::types::Digest,
1409 ) -> anyhow::Result<sui::grpc::ExecutedTransaction> {
1410 let request = sui::grpc::GetTransactionRequest::default()
1411 .with_digest(transaction.to_string())
1412 .with_read_mask(sui::grpc::FieldMask::from_paths([
1413 "digest",
1414 "objects.objects.object_id",
1415 "objects.objects.owner",
1416 "objects.objects.version",
1417 "objects.objects.digest",
1418 "objects.objects.balance",
1419 "objects.objects.object_type",
1420 "objects.objects.contents",
1421 "objects.objects.previous_transaction",
1422 ]));
1423 let client = self.clone_grpc_client();
1424 let executed = sui::grpc::retry_read(|| {
1425 let mut client = client.clone();
1426 let request = request.clone();
1427 async move { client.ledger_client().get_transaction(request).await }
1428 })
1429 .await
1430 .map(|response| response.into_inner().transaction)
1431 .with_context(|| format!("Could not fetch transaction '{transaction}'"))?
1432 .ok_or_else(|| anyhow!("Transaction '{transaction}' not found"))?;
1433 validate_executed_transaction(&executed, transaction)?;
1434 Ok(executed)
1435 }
1436
1437 pub fn transaction_output_object<T>(
1447 &self,
1448 executed: &sui::grpc::ExecutedTransaction,
1449 transaction: sui::types::Digest,
1450 object_id: sui::types::Address,
1451 ) -> anyhow::Result<Response<T>>
1452 where
1453 T: DeserializeOwned,
1454 {
1455 validate_executed_transaction(executed, transaction)?;
1456
1457 let mut output = executed.objects().objects.iter().filter(|object| {
1458 Self::parse_object_id(object).ok() == Some(object_id)
1459 && object
1460 .previous_transaction_opt()
1461 .and_then(|digest| digest.parse::<sui::types::Digest>().ok())
1462 == Some(transaction)
1463 });
1464 let object = output.next().ok_or_else(|| {
1465 anyhow!("Transaction '{transaction}' did not write object '{object_id}'")
1466 })?;
1467 if output.next().is_some() {
1468 bail!("Transaction '{transaction}' returned object '{object_id}' more than once");
1469 }
1470 let (owner, digest, version, balance) = self.parse_object_metadata(object_id, object)?;
1471 let data = self.parse_object_contents_bcs::<T>(object)?;
1472
1473 Ok(Response {
1474 object_id,
1475 owner,
1476 version,
1477 data,
1478 digest,
1479 balance,
1480 })
1481 }
1482
1483 pub fn transaction_shared_initial_version(
1498 &self,
1499 executed: &sui::grpc::ExecutedTransaction,
1500 transaction: sui::types::Digest,
1501 object_id: sui::types::Address,
1502 expected_type: &sui::types::StructTag,
1503 ) -> anyhow::Result<sui::types::Version> {
1504 validate_executed_transaction(executed, transaction)?;
1505
1506 let mut initial_version = None;
1507 for object in executed
1508 .objects()
1509 .objects
1510 .iter()
1511 .filter(|object| Self::parse_object_id(object).ok() == Some(object_id))
1512 {
1513 let object_type = object
1514 .object_type_opt()
1515 .ok_or_else(|| anyhow!("Object type missing for transaction object '{object_id}'"))?
1516 .parse::<sui::types::StructTag>()
1517 .map_err(|error| {
1518 anyhow!("Transaction object '{object_id}' has an invalid type: {error}")
1519 })?;
1520 ensure!(
1521 &object_type == expected_type,
1522 "Transaction object '{object_id}' has type '{object_type}', expected '{expected_type}'"
1523 );
1524 let (owner, _, _, _) = self.parse_object_metadata(object_id, object)?;
1525 let observed = match owner {
1526 sui::types::Owner::Shared(version) => version,
1527 sui::types::Owner::ConsensusAddress { start_version, .. } => start_version,
1528 owner => bail!(
1529 "Transaction object '{object_id}' is not shared; observed owner {owner:?}"
1530 ),
1531 };
1532 match initial_version {
1533 Some(expected) => ensure!(
1534 observed == expected,
1535 "Transaction object '{object_id}' has inconsistent shared start versions {expected} and {observed}"
1536 ),
1537 None => initial_version = Some(observed),
1538 }
1539 }
1540
1541 initial_version.ok_or_else(|| {
1542 anyhow!("Transaction '{transaction}' does not name shared object '{object_id}'")
1543 })
1544 }
1545
1546 pub async fn get_transaction_output_object<T>(
1553 &self,
1554 transaction: sui::types::Digest,
1555 object_id: sui::types::Address,
1556 ) -> anyhow::Result<Response<T>>
1557 where
1558 T: DeserializeOwned,
1559 {
1560 let executed = self.get_transaction_objects(transaction).await?;
1561 self.transaction_output_object(&executed, transaction, object_id)
1562 }
1563
1564 pub(crate) fn transaction_dynamic_field_outputs<K, V>(
1565 &self,
1566 executed: &sui::grpc::ExecutedTransaction,
1567 transaction: sui::types::Digest,
1568 key: &K,
1569 expected_key_type: &sui::types::TypeTag,
1570 expected_value_type: &sui::types::TypeTag,
1571 ) -> anyhow::Result<Vec<TransactionStateOutput<V>>>
1572 where
1573 K: DeserializeOwned + Eq + Serialize,
1574 V: DeserializeOwned,
1575 {
1576 validate_executed_transaction(executed, transaction)?;
1577 let mut outputs = Vec::new();
1578 for object in &executed.objects().objects {
1579 let object_id = Self::parse_object_id(object)?;
1580 let previous_transaction = object
1581 .previous_transaction_opt()
1582 .ok_or_else(|| {
1583 anyhow!(
1584 "Transaction '{transaction}' returned object '{object_id}' without its \
1585 previous transaction"
1586 )
1587 })?
1588 .parse::<sui::types::Digest>()
1589 .map_err(|error| {
1590 anyhow!(
1591 "Transaction '{transaction}' returned object '{object_id}' with an \
1592 invalid previous transaction: {error}"
1593 )
1594 })?;
1595 if previous_transaction != transaction {
1596 continue;
1597 }
1598 if let Some(output) = self.decode_transaction_dynamic_field(
1599 object,
1600 object_id,
1601 key,
1602 expected_key_type,
1603 expected_value_type,
1604 )? {
1605 outputs.push(output);
1606 }
1607 }
1608 Ok(outputs)
1609 }
1610
1611 pub(crate) fn causal_dynamic_field_values<K, V>(
1619 &self,
1620 executed: &sui::grpc::ExecutedTransaction,
1621 transaction: sui::types::Digest,
1622 key: &K,
1623 expected_key_type: &sui::types::TypeTag,
1624 expected_value_type: &sui::types::TypeTag,
1625 ) -> anyhow::Result<Vec<TransactionStateOutput<V>>>
1626 where
1627 K: DeserializeOwned + Eq + Serialize,
1628 V: DeserializeOwned,
1629 {
1630 validate_executed_transaction(executed, transaction)?;
1631 let mut positions = HashMap::new();
1632 let mut paired = HashSet::new();
1633 let mut visible = Vec::new();
1634 for object in &executed.objects().objects {
1635 let object_id = Self::parse_object_id(object)?;
1636 let previous_transaction = object
1637 .previous_transaction_opt()
1638 .ok_or_else(|| {
1639 anyhow!(
1640 "Causal transaction view returned object '{object_id}' without its \
1641 previous transaction"
1642 )
1643 })?
1644 .parse::<sui::types::Digest>()
1645 .map_err(|error| {
1646 anyhow!(
1647 "Causal transaction view returned object '{object_id}' with an invalid \
1648 previous transaction: {error}"
1649 )
1650 })?;
1651 let root_output = previous_transaction == transaction;
1652
1653 let Some(index) = positions.get(&object_id).copied() else {
1654 positions.insert(object_id, visible.len());
1655 visible.push((object, root_output));
1656 continue;
1657 };
1658
1659 ensure!(
1660 paired.insert(object_id),
1661 "Causal transaction view contains more than an input and output version of \
1662 object '{object_id}'"
1663 );
1664 let (selected, selected_is_root_output) = visible[index];
1665 ensure!(
1666 selected_is_root_output != root_output,
1667 "Causal transaction view contains ambiguous versions of object '{object_id}'"
1668 );
1669 let (input, output) = if root_output {
1670 (selected, object)
1671 } else {
1672 (object, selected)
1673 };
1674 let input_version = input
1675 .version_opt()
1676 .ok_or_else(|| anyhow!("Version missing for causal input object '{object_id}'"))?;
1677 let output_version = output
1678 .version_opt()
1679 .ok_or_else(|| anyhow!("Version missing for causal output object '{object_id}'"))?;
1680 ensure!(
1681 output_version > input_version,
1682 "Causal output object '{object_id}' has version {output_version}, not newer than \
1683 input version {input_version}"
1684 );
1685 if root_output {
1686 visible[index] = (object, true);
1687 }
1688 }
1689
1690 let mut values = Vec::new();
1691 for (object, _) in visible {
1692 let object_id = Self::parse_object_id(object)?;
1693 if let Some(value) = self.decode_transaction_dynamic_field(
1694 object,
1695 object_id,
1696 key,
1697 expected_key_type,
1698 expected_value_type,
1699 )? {
1700 values.push(value);
1701 }
1702 }
1703 Ok(values)
1704 }
1705
1706 fn decode_transaction_dynamic_field<K, V>(
1707 &self,
1708 object: &sui::grpc::Object,
1709 object_id: sui::types::Address,
1710 key: &K,
1711 expected_key_type: &sui::types::TypeTag,
1712 expected_value_type: &sui::types::TypeTag,
1713 ) -> anyhow::Result<Option<TransactionStateOutput<V>>>
1714 where
1715 K: DeserializeOwned + Eq + Serialize,
1716 V: DeserializeOwned,
1717 {
1718 let object_type = object
1719 .object_type_opt()
1720 .ok_or_else(|| anyhow!("Object type missing for transaction object '{object_id}'"))?
1721 .parse::<sui::types::StructTag>()
1722 .map_err(|error| {
1723 anyhow!("Transaction object '{object_id}' has an invalid type: {error}")
1724 })?;
1725 if *object_type.address() != sui::types::Address::from_static("0x2")
1726 || object_type.module().as_str() != "dynamic_field"
1727 || object_type.name().as_str() != "Field"
1728 {
1729 return Ok(None);
1730 }
1731 if object_type.type_params().len() != 2 {
1732 bail!("Dynamic field '{object_id}' does not have two type arguments");
1733 }
1734 if &object_type.type_params()[0] != expected_key_type
1735 || &object_type.type_params()[1] != expected_value_type
1736 {
1737 return Ok(None);
1738 }
1739
1740 let (owner, digest, version, _) = self.parse_object_metadata(object_id, object)?;
1741 let sui::types::Owner::Object(parent_id) = owner else {
1742 bail!("Dynamic field '{object_id}' is not owned by an object");
1743 };
1744 let expected_field_id = derive_dynamic_field_id(parent_id, key, expected_key_type)?;
1745 if object_id != expected_field_id {
1746 bail!("Dynamic field '{object_id}' does not derive from parent '{parent_id}'");
1747 }
1748 let field = self.parse_object_contents_bcs::<DynamicFieldValue<K, V>>(object)?;
1749 validate_dynamic_field(object_id, key, &field)?;
1750 Ok(Some(TransactionStateOutput {
1751 object_id: parent_id,
1752 field_ref: sui::types::ObjectReference::new(object_id, version, digest),
1753 data: field.value,
1754 }))
1755 }
1756
1757 pub(crate) fn advance_object_state_snapshot(
1767 &self,
1768 basis: &ObjectStateSnapshot,
1769 executed: &sui::grpc::ExecutedTransaction,
1770 transaction: sui::types::Digest,
1771 ) -> Result<Option<ObjectStateSnapshot>, ObjectStateSnapshotError> {
1772 let invalid = |error: anyhow::Error| ObjectStateSnapshotError::Invalid(error.to_string());
1773 let observed_transaction = executed
1774 .digest_opt()
1775 .ok_or_else(|| anyhow!("Finalized transaction response has no digest"))
1776 .and_then(|digest| {
1777 digest.parse::<sui::types::Digest>().map_err(|error| {
1778 anyhow!("Finalized transaction response has an invalid digest: {error}")
1779 })
1780 })
1781 .map_err(invalid)?;
1782 if observed_transaction != transaction {
1783 return Err(ObjectStateSnapshotError::Invalid(format!(
1784 "Finalized transaction response names '{observed_transaction}', expected \
1785 '{transaction}'"
1786 )));
1787 }
1788 let objects = &executed.objects().objects;
1789 let object_id = basis.object.object_id;
1790 let witness_field_id = basis.witness.field_id;
1791 let inner_field_id = basis.inner.field_id;
1792 let change =
1793 |id| finalized_transaction_object_change(objects, transaction, id).map_err(invalid);
1794
1795 let inner_object = match change(inner_field_id)? {
1796 FinalizedObjectChange::Written(object) => *object,
1797 FinalizedObjectChange::Unchanged => return Ok(None),
1798 FinalizedObjectChange::Removed => {
1799 return Err(reject_removed_state_object(
1800 transaction,
1801 inner_field_id,
1802 "inner field",
1803 ));
1804 }
1805 };
1806 let inner = Self::parse_state_field_metadata(object_id, inner_field_id, &inner_object)
1807 .map_err(invalid)?;
1808
1809 let (object, anchor_object) = match change(object_id)? {
1810 FinalizedObjectChange::Written(anchor) => {
1811 let metadata =
1812 Self::parse_observed_object_metadata(object_id, &anchor).map_err(invalid)?;
1813 (metadata, *anchor)
1814 }
1815 FinalizedObjectChange::Unchanged => (basis.object.clone(), basis.anchor_object.clone()),
1816 FinalizedObjectChange::Removed => {
1817 return Err(reject_removed_state_object(
1818 transaction,
1819 object_id,
1820 "anchor",
1821 ));
1822 }
1823 };
1824 let witness = match change(witness_field_id)? {
1825 FinalizedObjectChange::Written(witness) => {
1826 Self::parse_state_field_metadata(object_id, witness_field_id, &witness)
1827 .map_err(invalid)?
1828 }
1829 FinalizedObjectChange::Unchanged => basis.witness.clone(),
1830 FinalizedObjectChange::Removed => {
1831 return Err(reject_removed_state_object(
1832 transaction,
1833 witness_field_id,
1834 "witness field",
1835 ));
1836 }
1837 };
1838
1839 Ok(Some(ObjectStateSnapshot {
1840 object,
1841 witness,
1842 inner,
1843 anchor_object,
1844 inner_object,
1845 }))
1846 }
1847
1848 pub async fn get_objects_metadata(
1850 &self,
1851 object_ids: &[sui::types::Address],
1852 ) -> anyhow::Result<Vec<Response<()>>> {
1853 let field_mask = sui::grpc::FieldMask::from_paths([
1854 "object_id",
1855 "owner",
1856 "version",
1857 "digest",
1858 "balance",
1859 ]);
1860
1861 let objects = self.fetch_objects(object_ids, field_mask).await?;
1862
1863 objects
1864 .into_iter()
1865 .map(|object| {
1866 let object_id = Self::parse_object_id(&object)?;
1867
1868 let (owner, digest, version, balance) =
1869 self.parse_object_metadata(object_id, &object)?;
1870
1871 Ok(Response {
1872 object_id,
1873 owner,
1874 version,
1875 data: (),
1876 digest,
1877 balance,
1878 })
1879 })
1880 .collect()
1881 }
1882
1883 pub async fn fetch_coins_for_address_by_type(
1888 &self,
1889 owner: sui::types::Address,
1890 object_type: sui::types::StructTag,
1891 ) -> anyhow::Result<Vec<(sui::types::ObjectReference, u64)>> {
1892 let field_mask = sui::grpc::FieldMask::from_paths([
1893 "object_id",
1894 "owner",
1895 "version",
1896 "digest",
1897 "balance",
1898 "object_type",
1899 ]);
1900 let mut results = Vec::new();
1901 let mut page_token = None;
1902 let mut client = self.clone_grpc_client();
1903
1904 loop {
1905 let mut request = sui::grpc::ListOwnedObjectsRequest::default()
1906 .with_owner(owner)
1907 .with_page_size(1000)
1908 .with_object_type(object_type.clone())
1909 .with_read_mask(field_mask.clone());
1910
1911 if let Some(token) = page_token.clone() {
1912 request = request.with_page_token(token);
1913 }
1914
1915 let response = client
1916 .state_client()
1917 .list_owned_objects(request)
1918 .await
1919 .map(|response| response.into_inner())
1920 .map_err(|e| {
1921 anyhow!("Could not fetch coins of type '{object_type}' owned by '{owner}': {e}")
1922 })?;
1923
1924 page_token = response.next_page_token;
1925 results.extend(response.objects.iter().filter_map(|object| {
1926 Self::parse_owned_coin_with_type(object, owner, &object_type)
1927 }));
1928
1929 if page_token.is_none() {
1930 break;
1931 }
1932 }
1933
1934 Ok(results)
1935 }
1936
1937 pub async fn get_owned_objects<T>(
1939 &self,
1940 owner: sui::types::Address,
1941 object_type: sui::types::StructTag,
1942 ) -> anyhow::Result<Vec<Response<T>>>
1943 where
1944 T: DeserializeOwned,
1945 {
1946 let mut results = Vec::new();
1947 let mut cursor = None;
1948
1949 loop {
1950 let page = self
1951 .get_owned_object_page(owner, object_type.clone(), cursor, 1000)
1952 .await?;
1953 let (data, next_cursor) = page.into_parts();
1954 results.extend(data);
1955 cursor = next_cursor;
1956 if cursor.is_none() {
1957 break;
1958 }
1959 }
1960
1961 Ok(results)
1962 }
1963
1964 pub async fn get_object_owned_objects<T>(
1966 &self,
1967 parent_id: sui::types::Address,
1968 object_type: sui::types::StructTag,
1969 ) -> anyhow::Result<Vec<Response<T>>>
1970 where
1971 T: DeserializeOwned,
1972 {
1973 let mut results = Vec::new();
1974 let mut cursor = None;
1975
1976 loop {
1977 let page = self
1978 .get_object_owned_object_page(parent_id, object_type.clone(), cursor, 1000)
1979 .await?;
1980 let (data, next_cursor) = page.into_parts();
1981 results.extend(data);
1982 cursor = next_cursor;
1983 if cursor.is_none() {
1984 break;
1985 }
1986 }
1987
1988 Ok(results)
1989 }
1990
1991 pub async fn get_owned_object_page<T>(
2002 &self,
2003 owner: sui::types::Address,
2004 object_type: sui::types::StructTag,
2005 cursor: Option<Vec<u8>>,
2006 limit: usize,
2007 ) -> anyhow::Result<OwnedObjectPage<T>>
2008 where
2009 T: DeserializeOwned,
2010 {
2011 self.get_typed_owned_object_page(
2012 sui::types::Owner::Address(owner),
2013 object_type,
2014 cursor,
2015 limit,
2016 )
2017 .await
2018 }
2019
2020 pub async fn get_object_owned_object_page<T>(
2025 &self,
2026 parent_id: sui::types::Address,
2027 object_type: sui::types::StructTag,
2028 cursor: Option<Vec<u8>>,
2029 limit: usize,
2030 ) -> anyhow::Result<OwnedObjectPage<T>>
2031 where
2032 T: DeserializeOwned,
2033 {
2034 self.get_typed_owned_object_page(
2035 sui::types::Owner::Object(parent_id),
2036 object_type,
2037 cursor,
2038 limit,
2039 )
2040 .await
2041 }
2042
2043 async fn get_typed_owned_object_page<T>(
2044 &self,
2045 expected_owner: sui::types::Owner,
2046 object_type: sui::types::StructTag,
2047 cursor: Option<Vec<u8>>,
2048 limit: usize,
2049 ) -> anyhow::Result<OwnedObjectPage<T>>
2050 where
2051 T: DeserializeOwned,
2052 {
2053 let request_owner = match &expected_owner {
2054 sui::types::Owner::Address(owner) | sui::types::Owner::Object(owner) => *owner,
2055 _ => bail!("typed owned-object reads require an address or object owner"),
2056 };
2057 let page_size =
2058 u32::try_from(limit).context("owned object page limit does not fit in u32")?;
2059 if page_size == 0 {
2060 bail!("owned object page limit must be greater than zero");
2061 }
2062
2063 let field_mask = sui::grpc::FieldMask::from_paths([
2064 "object_id",
2065 "owner",
2066 "object_type",
2067 "version",
2068 "digest",
2069 "balance",
2070 "contents",
2071 ]);
2072 let mut request = sui::grpc::ListOwnedObjectsRequest::default()
2073 .with_owner(request_owner)
2074 .with_page_size(page_size)
2075 .with_object_type(object_type.clone())
2076 .with_read_mask(field_mask);
2077 if let Some(cursor) = cursor {
2078 request = request.with_page_token(cursor);
2079 }
2080
2081 let mut client = self.clone_grpc_client();
2082 let response = client
2083 .state_client()
2084 .list_owned_objects(request)
2085 .await
2086 .map(|response| response.into_inner())
2087 .map_err(|error| {
2088 anyhow!(
2089 "Could not fetch objects of type '{object_type}' owned by \
2090 '{expected_owner:?}': {error}"
2091 )
2092 })?;
2093
2094 let mut data = Vec::with_capacity(response.objects.len());
2095 for object in response.objects {
2096 let object_id = Self::parse_object_id(&object)?;
2097 let (observed_owner, digest, version, balance) =
2098 self.parse_object_metadata(object_id, &object)?;
2099 if !matches_expected_owner(&observed_owner, &expected_owner) {
2100 bail!(
2101 "Object '{object_id}' has owner '{observed_owner:?}', expected \
2102 '{expected_owner:?}'"
2103 );
2104 }
2105 let observed_type = object
2106 .object_type_opt()
2107 .ok_or_else(|| anyhow!("Object type missing for object '{object_id}'"))?
2108 .parse::<sui::types::StructTag>()
2109 .map_err(|error| {
2110 anyhow!("Could not parse object type for object '{object_id}': {error}")
2111 })?;
2112 if observed_type != object_type {
2113 bail!("Object '{object_id}' has type '{observed_type}', expected '{object_type}'");
2114 }
2115 let decoded = Self::parse_object_contents_bcs::<T>(self, &object)?;
2116 data.push(Response {
2117 object_id,
2118 owner: observed_owner,
2119 version,
2120 data: decoded,
2121 digest,
2122 balance,
2123 });
2124 }
2125
2126 Ok(OwnedObjectPage {
2127 data,
2128 next_cursor: response.next_page_token.map(|cursor| cursor.to_vec()),
2129 })
2130 }
2131
2132 pub async fn get_dynamic_fields<K, V>(
2135 &self,
2136 parent_id: sui::types::Address,
2137 expected_size: usize,
2138 ) -> anyhow::Result<HashMap<K, V>>
2139 where
2140 K: Eq + Hash + DeserializeOwned,
2141 V: DeserializeOwned,
2142 {
2143 let names_and_ids = self
2144 .fetch_dynamic_fields::<K>(parent_id, expected_size)
2145 .await?;
2146
2147 let mut name_by_field_id = HashMap::with_capacity(names_and_ids.len());
2148 let mut field_ids = Vec::with_capacity(names_and_ids.len());
2149
2150 for (name, _child_id, field_id) in names_and_ids {
2151 let Some(field_id) = field_id else {
2152 bail!("Dynamic field ID missing for dynamic map");
2153 };
2154
2155 if name_by_field_id.insert(field_id, name).is_some() {
2156 bail!("Duplicate dynamic field ID '{field_id}' for dynamic map");
2157 }
2158
2159 field_ids.push(field_id);
2160 }
2161
2162 let field_objects = self
2163 .get_objects::<DynamicFieldValue<K, V>>(&field_ids)
2164 .await?;
2165
2166 let mut out = HashMap::with_capacity(field_objects.len());
2167 for obj in field_objects {
2168 let name = name_by_field_id.remove(&obj.object_id).ok_or_else(|| {
2169 anyhow!(
2170 "Unexpected dynamic field ID '{}' for dynamic map",
2171 obj.object_id
2172 )
2173 })?;
2174
2175 out.insert(name, obj.data.value);
2176 }
2177
2178 Ok(out)
2179 }
2180
2181 pub async fn get_dynamic_field_refs_matching_key<K>(
2186 &self,
2187 parent_id: sui::types::Address,
2188 ) -> anyhow::Result<Vec<DynamicFieldReference<K>>>
2189 where
2190 K: Eq + Hash + DeserializeOwned,
2191 {
2192 Ok(self
2193 .fetch_dynamic_fields::<K>(parent_id, 0)
2194 .await?
2195 .into_iter()
2196 .filter_map(|(name, _child_id, field_id)| {
2197 Some(DynamicFieldReference {
2198 name,
2199 field_id: field_id?,
2200 })
2201 })
2202 .collect())
2203 }
2204
2205 pub async fn get_dynamic_field_value_by_id<K, V>(
2207 &self,
2208 field_id: sui::types::Address,
2209 ) -> anyhow::Result<V>
2210 where
2211 K: DeserializeOwned,
2212 V: DeserializeOwned,
2213 {
2214 Ok(self
2215 .get_object::<DynamicFieldValue<K, V>>(field_id)
2216 .await?
2217 .data
2218 .value)
2219 }
2220
2221 pub async fn get_dynamic_field_by_key<K, V>(
2233 &self,
2234 parent_id: sui::types::Address,
2235 key: K,
2236 key_type: &sui::types::TypeTag,
2237 ) -> anyhow::Result<Option<V>>
2238 where
2239 K: Eq + Serialize + DeserializeOwned,
2240 V: DeserializeOwned,
2241 {
2242 let field_id = derive_dynamic_field_id(parent_id, &key, key_type)?;
2243 let Some(field) = self
2244 .get_optional_object::<DynamicFieldValue<K, V>>(field_id)
2245 .await?
2246 else {
2247 return Ok(None);
2248 };
2249 validate_dynamic_field(field_id, &key, &field.data)?;
2250 Ok(Some(field.data.value))
2251 }
2252
2253 pub async fn get_dynamic_fields_by_keys<K, V, I>(
2267 &self,
2268 parent_id: sui::types::Address,
2269 keys: I,
2270 key_type: &sui::types::TypeTag,
2271 ) -> anyhow::Result<HashMap<K, V>>
2272 where
2273 K: Eq + Hash + Serialize + DeserializeOwned,
2274 V: DeserializeOwned,
2275 I: IntoIterator<Item = K>,
2276 {
2277 let mut index_by_field_id = HashMap::<sui::types::Address, usize>::new();
2278 let mut requests = Vec::<(sui::types::Address, K)>::new();
2279
2280 for key in keys {
2281 let field_id = derive_dynamic_field_id(parent_id, &key, key_type)?;
2282 if let Some(index) = index_by_field_id.get(&field_id).copied() {
2283 if requests[index].1 != key {
2284 bail!("Distinct dynamic field keys derived the same object ID '{field_id}'");
2285 }
2286 continue;
2287 }
2288 index_by_field_id.insert(field_id, requests.len());
2289 requests.push((field_id, key));
2290 }
2291
2292 let field_ids = requests
2293 .iter()
2294 .map(|(field_id, _)| *field_id)
2295 .collect::<Vec<_>>();
2296 let responses = self
2297 .get_optional_objects::<DynamicFieldValue<K, V>>(&field_ids)
2298 .await?;
2299 let mut values = HashMap::with_capacity(responses.len());
2300
2301 for ((field_id, key), response) in requests.into_iter().zip(responses) {
2302 let Some(response) = response else {
2303 continue;
2304 };
2305 validate_dynamic_field(field_id, &key, &response.data)?;
2306 values.insert(key, response.data.value);
2307 }
2308
2309 Ok(values)
2310 }
2311
2312 pub async fn get_dynamic_field_page_matching_types<K, V>(
2322 &self,
2323 parent_id: sui::types::Address,
2324 cursor: Option<Vec<u8>>,
2325 limit: usize,
2326 value_type_suffix: &str,
2327 ) -> anyhow::Result<DynamicFieldPage<K, V>>
2328 where
2329 K: DeserializeOwned,
2330 V: DeserializeOwned,
2331 {
2332 let page_size =
2333 u32::try_from(limit).context("dynamic field page limit does not fit in u32")?;
2334 if page_size == 0 {
2335 bail!("dynamic field page limit must be greater than zero");
2336 }
2337
2338 let field_mask = sui::grpc::FieldMask::from_paths(["name", "field_id", "value_type"]);
2339 let mut request = sui::grpc::ListDynamicFieldsRequest::default()
2340 .with_parent(parent_id)
2341 .with_page_size(page_size)
2342 .with_read_mask(field_mask);
2343 if let Some(cursor) = cursor {
2344 request = request.with_page_token(cursor);
2345 }
2346
2347 observe_dynamic_field_list("typed_page");
2348 let client = self.clone_state_catalog_client();
2349 let response = sui::grpc::retry_read(|| {
2350 let mut client = client.clone();
2351 let request = request.clone();
2352 async move { client.state_client().list_dynamic_fields(request).await }
2353 })
2354 .await
2355 .map(|response| response.into_inner())
2356 .map_err(|error| {
2357 anyhow!("Could not fetch dynamic fields for parent '{parent_id}': {error}")
2358 })?;
2359
2360 let next_cursor = response.next_page_token.map(|cursor| cursor.to_vec());
2361 let mut field_ids = Vec::new();
2362 for field in response.dynamic_fields {
2363 if !field
2364 .value_type
2365 .as_deref()
2366 .is_some_and(|value_type| value_type.ends_with(value_type_suffix))
2367 {
2368 continue;
2369 }
2370 let Some(name) = field.name_opt() else {
2371 continue;
2372 };
2373 if parse_dynamic_field_name::<K>(name.value()).is_err() {
2374 continue;
2375 }
2376 let field_id = field
2377 .field_id_opt()
2378 .ok_or_else(|| anyhow!("Dynamic field ID missing for parent '{parent_id}'"))?
2379 .parse()
2380 .map_err(|_| anyhow!("Could not parse field ID for dynamic field"))?;
2381 field_ids.push(field_id);
2382 }
2383
2384 let data = if field_ids.is_empty() {
2385 Vec::new()
2386 } else {
2387 self.get_objects::<DynamicFieldValue<K, V>>(&field_ids)
2388 .await?
2389 .into_iter()
2390 .map(|field| (field.data.name, field.data.value))
2391 .collect()
2392 };
2393
2394 Ok(DynamicFieldPage { data, next_cursor })
2395 }
2396
2397 pub async fn get_dynamic_field_object_values<K, V>(
2401 &self,
2402 parent_id: sui::types::Address,
2403 ) -> anyhow::Result<Vec<V>>
2404 where
2405 K: DeserializeOwned,
2406 V: DeserializeOwned,
2407 {
2408 let names_and_ids = self.fetch_dynamic_fields_untyped(parent_id).await?;
2409 let mut field_ids = Vec::with_capacity(names_and_ids.len());
2410 for field_id in names_and_ids {
2411 field_ids.push(field_id);
2412 }
2413
2414 Ok(self
2415 .get_objects::<DynamicFieldValue<K, V>>(&field_ids)
2416 .await?
2417 .into_iter()
2418 .map(|response| response.data.value)
2419 .collect())
2420 }
2421
2422 pub async fn get_dynamic_field_values<V>(
2424 &self,
2425 parent_id: sui::types::Address,
2426 ) -> anyhow::Result<Vec<(Option<String>, V)>>
2427 where
2428 V: DeserializeOwned,
2429 {
2430 let mut results = Vec::new();
2431 let mut page_token = None;
2432 let field_mask =
2433 sui::grpc::FieldMask::from_paths(["field_id", "kind", "value", "value_type"]);
2434 let client = self.clone_state_catalog_client();
2435
2436 loop {
2437 observe_dynamic_field_list("values");
2438 let mut request = sui::grpc::ListDynamicFieldsRequest::default()
2439 .with_parent(parent_id)
2440 .with_page_size(1000)
2441 .with_read_mask(field_mask.clone());
2442
2443 if let Some(token) = page_token.clone() {
2444 request = request.with_page_token(token);
2445 }
2446
2447 let response = sui::grpc::retry_read(|| {
2448 let mut client = client.clone();
2449 let request = request.clone();
2450 async move { client.state_client().list_dynamic_fields(request).await }
2451 })
2452 .await
2453 .map(|r| r.into_inner())
2454 .map_err(|e| anyhow!("Could not fetch dynamic fields for parent '{parent_id}': {e}"))?;
2455
2456 page_token = response.next_page_token;
2457
2458 for field in response.dynamic_fields {
2459 let value = field.value_opt().ok_or_else(|| {
2460 anyhow!("Dynamic field value missing for parent '{parent_id}'")
2461 })?;
2462 let Some(bytes) = value.value_opt() else {
2463 bail!("Dynamic field value BCS missing for parent '{parent_id}'");
2464 };
2465 let decoded = bcs::from_bytes::<V>(bytes).map_err(|e| {
2466 anyhow!("Could not parse dynamic field value for parent '{parent_id}': {e}")
2467 })?;
2468 results.push((field.value_type, decoded));
2469 }
2470
2471 if page_token.is_none() {
2472 break;
2473 }
2474 }
2475
2476 Ok(results)
2477 }
2478
2479 pub async fn get_dynamic_object_fields<K, V>(
2482 &self,
2483 parent_id: sui::types::Address,
2484 ) -> anyhow::Result<HashMap<K, Response<V>>>
2485 where
2486 K: Eq + Hash + DeserializeOwned,
2487 V: DeserializeOwned,
2488 {
2489 let names_and_ids = self.fetch_dynamic_fields::<K>(parent_id, 0).await?;
2490 let mut names = Vec::with_capacity(names_and_ids.len());
2491 let mut child_ids = Vec::with_capacity(names_and_ids.len());
2492
2493 for (name, child_id, _) in names_and_ids {
2494 let Some(child_id) = child_id else {
2495 bail!("Dynamic object field child ID missing for parent '{parent_id}'");
2496 };
2497 names.push(name);
2498 child_ids.push(child_id);
2499 }
2500
2501 let child_objects = self.get_objects::<V>(&child_ids).await?;
2502 Ok(names.into_iter().zip(child_objects).collect())
2503 }
2504
2505 pub async fn get_dynamic_object_field_by_key<K, V>(
2519 &self,
2520 parent_id: sui::types::Address,
2521 key: K,
2522 key_type: &sui::types::TypeTag,
2523 ) -> anyhow::Result<Option<Response<V>>>
2524 where
2525 K: Eq + Serialize + DeserializeOwned,
2526 V: DeserializeOwned,
2527 {
2528 let wrapper_key = DynamicObjectFieldName { name: key };
2529 let wrapper_type = dynamic_object_field_wrapper_type(key_type);
2530 let Some(child_id) = self
2531 .get_dynamic_field_by_key::<DynamicObjectFieldName<K>, ID>(
2532 parent_id,
2533 wrapper_key,
2534 &wrapper_type,
2535 )
2536 .await?
2537 else {
2538 return Ok(None);
2539 };
2540
2541 let child_id = child_id.bytes;
2542 self.get_optional_object(child_id)
2543 .await?
2544 .map(Some)
2545 .ok_or_else(|| anyhow!("Dynamic object field child '{child_id}' not found"))
2546 }
2547
2548 pub async fn get_dynamic_object_field_refs_matching_key<K>(
2552 &self,
2553 parent_id: sui::types::Address,
2554 ) -> anyhow::Result<Vec<DynamicObjectFieldReference<K>>>
2555 where
2556 K: Eq + Hash + DeserializeOwned,
2557 {
2558 Ok(self
2559 .fetch_dynamic_fields::<K>(parent_id, 0)
2560 .await?
2561 .into_iter()
2562 .filter_map(|(name, child_id, field_id)| {
2563 Some(DynamicObjectFieldReference {
2564 name,
2565 field_id: field_id?,
2566 child_id: child_id?,
2567 })
2568 })
2569 .collect())
2570 }
2571
2572 pub async fn get_dynamic_object_field_child_ids(
2574 &self,
2575 parent_id: sui::types::Address,
2576 ) -> anyhow::Result<Vec<sui::types::Address>> {
2577 let mut child_ids = Vec::new();
2578 let mut page_token = None;
2579 let field_mask = sui::grpc::FieldMask::from_paths(["child_id"]);
2580 let client = self.clone_state_catalog_client();
2581
2582 loop {
2583 observe_dynamic_field_list("object_children");
2584 let mut request = sui::grpc::ListDynamicFieldsRequest::default()
2585 .with_parent(parent_id)
2586 .with_page_size(1000)
2587 .with_read_mask(field_mask.clone());
2588
2589 if let Some(token) = page_token.clone() {
2590 request = request.with_page_token(token);
2591 }
2592
2593 let response = sui::grpc::retry_read(|| {
2594 let mut client = client.clone();
2595 let request = request.clone();
2596 async move { client.state_client().list_dynamic_fields(request).await }
2597 })
2598 .await
2599 .map(|r| r.into_inner())
2600 .map_err(|e| anyhow!("Could not fetch dynamic fields for parent '{parent_id}': {e}"))?;
2601
2602 page_token = response.next_page_token;
2603
2604 for field in response.dynamic_fields {
2605 let child_id = field
2606 .child_id_opt()
2607 .map(|id| id.parse())
2608 .transpose()
2609 .map_err(|_| anyhow!("Could not parse child ID for dynamic field"))?;
2610
2611 if let Some(child_id) = child_id {
2612 child_ids.push(child_id);
2613 }
2614 }
2615
2616 if page_token.is_none() {
2617 break;
2618 }
2619 }
2620
2621 Ok(child_ids)
2622 }
2623
2624 pub async fn get_table_vec<T>(&self, parent: &TableVec<T>) -> anyhow::Result<Vec<T>>
2634 where
2635 T: DeserializeOwned,
2636 {
2637 let indexes = 0..parent.size_u64();
2638 let mut values = self
2639 .get_dynamic_fields_by_keys(parent.id(), indexes.clone(), &sui::types::TypeTag::U64)
2640 .await?;
2641
2642 indexes
2643 .map(|index| {
2644 values
2645 .remove(&index)
2646 .ok_or_else(|| anyhow!("Missing TableVec element {index}"))
2647 })
2648 .collect()
2649 }
2650
2651 async fn fetch_object(
2653 &self,
2654 object_id: sui::types::Address,
2655 field_mask: sui::grpc::FieldMask,
2656 ) -> anyhow::Result<sui::grpc::Object> {
2657 self.fetch_optional_object(object_id, field_mask)
2658 .await?
2659 .ok_or_else(|| anyhow::Error::new(NexusError::ObjectNotFound { object: object_id }))
2660 }
2661
2662 async fn fetch_optional_object(
2663 &self,
2664 object_id: sui::types::Address,
2665 field_mask: sui::grpc::FieldMask,
2666 ) -> anyhow::Result<Option<sui::grpc::Object>> {
2667 let client = self.clone_grpc_client();
2668
2669 let request = sui::grpc::GetObjectRequest::default()
2670 .with_object_id(object_id)
2671 .with_read_mask(field_mask);
2672
2673 match sui::grpc::retry_read(|| {
2674 let mut client = client.clone();
2675 let request = request.clone();
2676 async move { client.ledger_client().get_object(request).await }
2677 })
2678 .await
2679 {
2680 Ok(response) => Ok(response.into_inner().object),
2681 Err(status) if status.code() == tonic::Code::NotFound => Ok(None),
2682 Err(status) => Err(anyhow::Error::new(status))
2683 .with_context(|| format!("Could not fetch object '{object_id}'")),
2684 }
2685 }
2686
2687 async fn fetch_objects(
2689 &self,
2690 object_ids: &[sui::types::Address],
2691 field_mask: sui::grpc::FieldMask,
2692 ) -> anyhow::Result<Vec<sui::grpc::Object>> {
2693 let results = self.fetch_object_results(object_ids, field_mask).await?;
2694
2695 object_ids
2696 .iter()
2697 .copied()
2698 .zip(results)
2699 .map(|(object_id, result)| {
2700 result.to_result().map_err(|status| {
2701 anyhow!("Could not fetch object '{object_id}': {}", status.message)
2702 })
2703 })
2704 .collect()
2705 }
2706
2707 async fn fetch_object_results(
2708 &self,
2709 object_ids: &[sui::types::Address],
2710 field_mask: sui::grpc::FieldMask,
2711 ) -> anyhow::Result<Vec<sui::grpc::GetObjectResult>> {
2712 let request = {
2713 let mut req = sui::grpc::BatchGetObjectsRequest::default();
2714
2715 req.set_requests(
2716 object_ids
2717 .iter()
2718 .map(|&id| {
2719 sui::grpc::GetObjectRequest::default()
2720 .with_object_id(id)
2721 .with_read_mask(field_mask.clone())
2722 })
2723 .collect(),
2724 );
2725
2726 req.set_read_mask(field_mask);
2727
2728 req
2729 };
2730
2731 let client = self.clone_grpc_client();
2732
2733 let response = sui::grpc::retry_read(|| {
2734 let mut client = client.clone();
2735 let request = request.clone();
2736 async move { client.ledger_client().batch_get_objects(request).await }
2737 })
2738 .await
2739 .map(|r| r.into_inner())
2740 .map_err(|e| anyhow!("Could not fetch objects: {e}"))?;
2741
2742 Ok(response.objects)
2743 }
2744
2745 async fn fetch_dynamic_fields<K>(
2749 &self,
2750 parent_id: sui::types::Address,
2751 expected_size: usize,
2752 ) -> anyhow::Result<Vec<(K, Option<sui::types::Address>, Option<sui::types::Address>)>>
2753 where
2754 K: Eq + Hash + DeserializeOwned,
2755 {
2756 let mut results = Vec::with_capacity(expected_size);
2757 let mut page_token = None;
2758 let field_mask = sui::grpc::FieldMask::from_paths(["name", "child_id", "field_id"]);
2759 let client = self.clone_state_catalog_client();
2760
2761 loop {
2762 observe_dynamic_field_list("typed_values");
2763 let mut request = sui::grpc::ListDynamicFieldsRequest::default()
2764 .with_parent(parent_id)
2765 .with_page_size(1000)
2766 .with_read_mask(field_mask.clone());
2767
2768 if let Some(token) = page_token.clone() {
2769 request = request.with_page_token(token);
2770 }
2771
2772 let response = sui::grpc::retry_read(|| {
2773 let mut client = client.clone();
2774 let request = request.clone();
2775 async move { client.state_client().list_dynamic_fields(request).await }
2776 })
2777 .await
2778 .map(|r| r.into_inner())
2779 .map_err(|e| anyhow!("Could not fetch dynamic fields for parent '{parent_id}': {e}"))?;
2780
2781 page_token = response.next_page_token;
2782
2783 for field in response.dynamic_fields {
2784 let name = parse_dynamic_field_name::<K>(
2786 field
2787 .name_opt()
2788 .ok_or_else(|| {
2789 anyhow!("Dynamic field name missing for parent '{parent_id}'")
2790 })?
2791 .value(),
2792 )
2793 .map_err(|e| {
2794 anyhow!("Could not parse dynamic field name for parent '{parent_id}': {e}")
2795 })?;
2796
2797 let field_id = field
2798 .field_id_opt()
2799 .map(|id| id.parse())
2800 .transpose()
2801 .map_err(|_| anyhow!("Could not parse field ID for dynamic field"))?;
2802
2803 let child_id = field
2804 .child_id_opt()
2805 .map(|id| id.parse())
2806 .transpose()
2807 .map_err(|_| anyhow!("Could not parse child ID for dynamic field"))?;
2808
2809 results.push((name, child_id, field_id));
2810 }
2811
2812 if page_token.is_none() {
2813 break;
2814 }
2815 }
2816
2817 Ok(results)
2818 }
2819
2820 async fn fetch_dynamic_fields_untyped(
2821 &self,
2822 parent_id: sui::types::Address,
2823 ) -> anyhow::Result<Vec<sui::types::Address>> {
2824 let mut results = Vec::new();
2825 let mut page_token = None;
2826 let field_mask = sui::grpc::FieldMask::from_paths(["field_id"]);
2827 let client = self.clone_state_catalog_client();
2828
2829 loop {
2830 observe_dynamic_field_list("field_ids");
2831 let mut request = sui::grpc::ListDynamicFieldsRequest::default()
2832 .with_parent(parent_id)
2833 .with_page_size(1000)
2834 .with_read_mask(field_mask.clone());
2835
2836 if let Some(token) = page_token.clone() {
2837 request = request.with_page_token(token);
2838 }
2839
2840 let response = sui::grpc::retry_read(|| {
2841 let mut client = client.clone();
2842 let request = request.clone();
2843 async move { client.state_client().list_dynamic_fields(request).await }
2844 })
2845 .await
2846 .map(|r| r.into_inner())
2847 .map_err(|e| anyhow!("Could not fetch dynamic fields for parent '{parent_id}': {e}"))?;
2848
2849 page_token = response.next_page_token;
2850
2851 for field in response.dynamic_fields {
2852 let field_id = field
2853 .field_id_opt()
2854 .ok_or_else(|| anyhow!("Dynamic field ID missing for parent '{parent_id}'"))?
2855 .parse()
2856 .map_err(|_| anyhow!("Could not parse field ID for dynamic field"))?;
2857 results.push(field_id);
2858 }
2859
2860 if page_token.is_none() {
2861 break;
2862 }
2863 }
2864
2865 Ok(results)
2866 }
2867
2868 fn parse_object_metadata(
2870 &self,
2871 object_id: sui::types::Address,
2872 object: &sui::grpc::Object,
2873 ) -> anyhow::Result<(
2874 sui::types::Owner,
2875 sui::types::Digest,
2876 sui::types::Version,
2877 Option<u64>,
2878 )> {
2879 let owner = object
2880 .owner_opt()
2881 .ok_or_else(|| anyhow!("Owner missing for object '{object_id}'"))?
2882 .try_into()
2883 .map_err(|_| anyhow!("Could not parse owner for object '{object_id}'"))?;
2884
2885 let digest = object
2886 .digest_opt()
2887 .ok_or_else(|| anyhow!("Digest missing for object '{object_id}'"))?
2888 .parse()
2889 .map_err(|_| anyhow!("Could not parse digest for object '{object_id}'"))?;
2890
2891 let version = object
2892 .version_opt()
2893 .ok_or_else(|| anyhow!("Version missing for object '{object_id}'"))?;
2894
2895 let balance = object.balance_opt();
2896
2897 Ok((owner, digest, version, balance))
2898 }
2899
2900 fn parse_owned_coin_with_type(
2901 object: &sui::grpc::Object,
2902 expected_owner: sui::types::Address,
2903 expected_type: &sui::types::StructTag,
2904 ) -> Option<(sui::types::ObjectReference, u64)> {
2905 let owner: sui::types::Owner = object.owner_opt()?.try_into().ok()?;
2906 if owner != sui::types::Owner::Address(expected_owner) {
2907 return None;
2908 }
2909
2910 let object_type = object
2911 .object_type_opt()?
2912 .parse::<sui::types::StructTag>()
2913 .ok()?;
2914 if object_type != *expected_type {
2915 return None;
2916 }
2917
2918 Some((
2919 sui::types::ObjectReference::new(
2920 object.object_id_opt()?.parse().ok()?,
2921 object.version_opt()?,
2922 object.digest_opt()?.parse().ok()?,
2923 ),
2924 object.balance_opt().unwrap_or(0),
2925 ))
2926 }
2927
2928 fn parse_object_contents_bcs<T>(&self, object: &sui::grpc::Object) -> anyhow::Result<T>
2929 where
2930 T: DeserializeOwned,
2931 {
2932 let Some(contents) = object.contents_opt() else {
2933 bail!("Object contents missing");
2934 };
2935
2936 let Some(bytes) = contents.value_opt() else {
2937 bail!("Object BCS contents missing");
2938 };
2939
2940 bcs::from_bytes::<T>(bytes).map_err(|e| {
2941 anyhow!(
2942 "Could not parse object contents BCS as `{ty}` (object id `{id}`, {len} bytes): {e}",
2943 ty = std::any::type_name::<T>(),
2944 id = object
2945 .object_id_opt()
2946 .map(ToString::to_string)
2947 .unwrap_or_else(|| "<unknown>".into()),
2948 len = bytes.len(),
2949 )
2950 })
2951 }
2952}
2953
2954#[derive(Clone, Debug)]
2957pub struct Response<T> {
2958 pub object_id: sui::types::Address,
2959 pub owner: sui::types::Owner,
2960 pub version: sui::types::Version,
2961 pub data: T,
2962 pub digest: sui::types::Digest,
2963 pub balance: Option<u64>,
2965}
2966
2967#[derive(Clone, Debug)]
2972pub struct TransactionStateOutput<T> {
2973 pub object_id: sui::types::Address,
2974 pub field_ref: sui::types::ObjectReference,
2975 pub data: T,
2976}
2977
2978impl<T> Response<T> {
2979 pub fn is_shared(&self) -> bool {
2981 matches!(
2982 self.owner,
2983 sui::types::Owner::Shared(_) | sui::types::Owner::ConsensusAddress { .. }
2984 )
2985 }
2986
2987 pub fn is_immutable(&self) -> bool {
2989 matches!(self.owner, sui::types::Owner::Immutable)
2990 }
2991
2992 pub fn get_initial_version(&self) -> sui::types::Version {
2995 match self.owner {
2996 sui::types::Owner::Shared(version) => version,
2997 sui::types::Owner::ConsensusAddress { start_version, .. } => start_version,
2998 _ => self.version,
2999 }
3000 }
3001
3002 pub fn object_ref(&self) -> sui::types::ObjectReference {
3004 sui::types::ObjectReference::new(self.object_id, self.get_initial_version(), self.digest)
3005 }
3006}
3007
3008#[derive(Clone, Debug, Deserialize)]
3013pub struct Set<T>
3014where
3015 T: Eq + Hash,
3016{
3017 contents: HashSet<T>,
3018}
3019
3020impl<T> Set<T>
3021where
3022 T: Eq + Hash,
3023{
3024 pub fn new() -> Self {
3025 Self {
3026 contents: HashSet::new(),
3027 }
3028 }
3029
3030 pub fn into_inner(self) -> HashSet<T> {
3031 self.contents
3032 }
3033
3034 pub fn inner(&self) -> &HashSet<T> {
3035 &self.contents
3036 }
3037
3038 pub fn inner_mut(&mut self) -> &mut HashSet<T> {
3039 &mut self.contents
3040 }
3041
3042 pub fn is_empty(&self) -> bool {
3043 self.contents.is_empty()
3044 }
3045}
3046
3047impl<T> Default for Set<T>
3048where
3049 T: Eq + Hash,
3050{
3051 fn default() -> Self {
3052 Self::new()
3053 }
3054}
3055
3056impl<T> From<HashSet<T>> for Set<T>
3057where
3058 T: Eq + Hash,
3059{
3060 fn from(contents: HashSet<T>) -> Self {
3061 Self { contents }
3062 }
3063}
3064
3065impl<T> FromIterator<T> for Set<T>
3066where
3067 T: Eq + Hash,
3068{
3069 fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
3070 Self {
3071 contents: HashSet::from_iter(iter),
3072 }
3073 }
3074}
3075
3076impl<T> Serialize for Set<T>
3077where
3078 T: Eq + Hash + Serialize,
3079{
3080 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3081 where
3082 S: serde::Serializer,
3083 {
3084 #[derive(Serialize)]
3085 struct Wrapper<'a, T> {
3086 contents: Vec<&'a T>,
3087 }
3088
3089 Wrapper {
3090 contents: self.contents.iter().collect(),
3091 }
3092 .serialize(serializer)
3093 }
3094}
3095
3096#[derive(Clone, Debug, PartialEq, Eq)]
3100pub struct Map<K, V>
3101where
3102 K: Eq + Hash,
3103{
3104 contents: HashMap<K, V>,
3105}
3106
3107impl<K, V> Map<K, V>
3108where
3109 K: Eq + Hash,
3110{
3111 pub fn new() -> Self {
3112 Self {
3113 contents: HashMap::new(),
3114 }
3115 }
3116
3117 pub fn from_map(contents: HashMap<K, V>) -> Self {
3118 Self { contents }
3119 }
3120
3121 pub fn into_inner(self) -> HashMap<K, V> {
3122 self.contents
3123 }
3124
3125 pub fn into_map(self) -> HashMap<K, V> {
3126 self.into_inner()
3127 }
3128
3129 pub fn inner(&self) -> &HashMap<K, V> {
3130 &self.contents
3131 }
3132
3133 pub fn inner_mut(&mut self) -> &mut HashMap<K, V> {
3134 &mut self.contents
3135 }
3136}
3137
3138impl<K, V> Default for Map<K, V>
3139where
3140 K: Eq + Hash,
3141{
3142 fn default() -> Self {
3143 Self::new()
3144 }
3145}
3146
3147impl<K, V> From<HashMap<K, V>> for Map<K, V>
3148where
3149 K: Eq + Hash,
3150{
3151 fn from(contents: HashMap<K, V>) -> Self {
3152 Self { contents }
3153 }
3154}
3155
3156impl<K, V> FromIterator<(K, V)> for Map<K, V>
3157where
3158 K: Eq + Hash,
3159{
3160 fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
3161 Self {
3162 contents: HashMap::from_iter(iter),
3163 }
3164 }
3165}
3166
3167impl<K, V> Serialize for Map<K, V>
3168where
3169 K: Eq + Hash + Serialize,
3170 V: Serialize,
3171{
3172 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3173 where
3174 S: serde::Serializer,
3175 {
3176 #[derive(Serialize)]
3177 struct PairRef<'a, K, V> {
3178 key: &'a K,
3179 value: &'a V,
3180 }
3181
3182 #[derive(Serialize)]
3183 struct Wrapper<'a, K, V> {
3184 contents: Vec<PairRef<'a, K, V>>,
3185 }
3186
3187 let contents = self
3188 .contents
3189 .iter()
3190 .map(|(key, value)| PairRef { key, value })
3191 .collect();
3192
3193 Wrapper { contents }.serialize(serializer)
3194 }
3195}
3196
3197impl<'de, K, V> Deserialize<'de> for Map<K, V>
3198where
3199 K: Eq + Hash + DeserializeOwned,
3200 V: DeserializeOwned,
3201{
3202 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3203 where
3204 D: Deserializer<'de>,
3205 {
3206 #[derive(Deserialize)]
3207 #[serde(untagged)]
3208 enum Wrapper<K, V>
3209 where
3210 K: Eq + Hash,
3211 {
3212 Pairs { contents: Vec<Pair<K, V>> },
3213 Map { contents: HashMap<K, V> },
3214 }
3215
3216 match Wrapper::<K, V>::deserialize(deserializer)? {
3217 Wrapper::Pairs { contents } => Ok(Self {
3218 contents: contents
3219 .into_iter()
3220 .map(|Pair { key, value }| (key, value))
3221 .collect(),
3222 }),
3223 Wrapper::Map { contents } => Ok(Self { contents }),
3224 }
3225 }
3226}
3227
3228#[derive(Clone, Debug, Deserialize)]
3230struct Pair<K, V> {
3231 #[serde(alias = "key", alias = "name")]
3232 key: K,
3233 value: V,
3234}
3235
3236#[cfg(test)]
3237mod tests {
3238 use {
3239 super::*,
3240 crate::test_utils::sui_mocks,
3241 mockall::predicate::always,
3242 serde::{Deserialize, Serialize},
3243 std::sync::{Arc, Barrier},
3244 };
3245
3246 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3247 struct TestValue {
3248 value: u64,
3249 }
3250
3251 #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3252 struct TestKey {
3253 name: String,
3254 }
3255
3256 #[derive(Serialize)]
3257 struct TestDynamicObjectFieldName<K> {
3258 name: K,
3259 }
3260
3261 fn test_value_tag() -> sui::types::StructTag {
3262 sui::types::StructTag::new(
3263 sui::types::Address::from_static("0x1"),
3264 sui::types::Identifier::from_static("test"),
3265 sui::types::Identifier::from_static("TestValue"),
3266 vec![],
3267 )
3268 }
3269
3270 fn object_with_bcs<T>(
3271 object_ref: sui::types::ObjectReference,
3272 owner: sui::types::Owner,
3273 value: &T,
3274 ) -> sui::grpc::Object
3275 where
3276 T: Serialize,
3277 {
3278 let mut object = sui::grpc::Object::default();
3279 object.set_object_id(object_ref.object_id().to_string());
3280 object.set_owner(sui::grpc::Owner::from(owner));
3281 object.set_digest(*object_ref.digest());
3282 object.set_version(object_ref.version());
3283 let mut contents = sui::grpc::Bcs::default();
3284 contents.set_value(bcs::to_bytes(value).expect("object value serializes"));
3285 object.contents = Some(contents);
3286 object
3287 }
3288
3289 fn typed_object_with_bcs<T>(
3290 object_ref: sui::types::ObjectReference,
3291 owner: sui::types::Owner,
3292 object_type: &sui::types::StructTag,
3293 value: &T,
3294 ) -> sui::grpc::Object
3295 where
3296 T: Serialize,
3297 {
3298 let mut object = object_with_bcs(object_ref, owner, value);
3299 object.set_object_type(object_type.to_string());
3300 object
3301 }
3302
3303 #[test]
3304 fn dynamic_field_metadata_reports_unknown_value_type_without_bcs_decode() {
3305 let parent_id = sui::types::Address::from_static("0x91");
3306 let field_id = sui::types::Address::from_static("0x92");
3307 let key_type = "0xa1::object_state::Inner"
3308 .parse::<sui::types::TypeTag>()
3309 .unwrap();
3310 let value_type = "0xf0::future::UnknownInner"
3311 .parse::<sui::types::TypeTag>()
3312 .unwrap();
3313 let field_type = sui::types::StructTag::new(
3314 sui::types::Address::from_static("0x2"),
3315 sui::types::Identifier::from_static("dynamic_field"),
3316 sui::types::Identifier::from_static("Field"),
3317 vec![key_type.clone(), value_type.clone()],
3318 );
3319 let mut field_object = sui::grpc::Object::default();
3320 field_object.set_object_type(field_type.to_string());
3321 let mut invalid_value = sui::grpc::Bcs::default();
3322 invalid_value.set_value(vec![0xff]);
3323 let mut field = sui::grpc::DynamicField::default();
3324 field.set_field_id(field_id.to_string());
3325 field.set_field_object(field_object);
3326 field.set_value_type(value_type.to_string());
3327 field.set_value(invalid_value);
3328
3329 let metadata = parse_dynamic_field_metadata(parent_id, &field)
3330 .unwrap()
3331 .unwrap();
3332
3333 assert_eq!(metadata.field_id, field_id);
3334 assert_eq!(metadata.key_type, key_type);
3335 assert_eq!(metadata.value_type, value_type);
3336 }
3337
3338 #[test]
3339 fn dynamic_field_metadata_rejects_an_incomplete_field_shape() {
3340 let parent_id = sui::types::Address::from_static("0x91");
3341 let field_id = sui::types::Address::from_static("0x92");
3342 let mut field_object = sui::grpc::Object::default();
3343 field_object.set_object_type("0x2::dynamic_field::Field<u64>".to_owned());
3344 let mut field = sui::grpc::DynamicField::default();
3345 field.set_field_id(field_id.to_string());
3346 field.set_field_object(field_object);
3347
3348 let error = parse_dynamic_field_metadata(parent_id, &field)
3349 .unwrap_err()
3350 .to_string();
3351
3352 assert!(error.contains("Field<K, V>"));
3353 }
3354
3355 #[test]
3356 fn dynamic_object_field_metadata_is_excluded_from_ordinary_state_metadata() {
3357 let parent_id = sui::types::Address::from_static("0x91");
3358 let field_id = sui::types::Address::from_static("0x92");
3359 let key_type = "0xa1::task::AuthorizationKey"
3360 .parse::<sui::types::TypeTag>()
3361 .unwrap();
3362 let wrapper_type = dynamic_object_field_wrapper_type(&key_type);
3363 let child_type = "0xb1::authorization::AgentSkillAuthorization"
3364 .parse::<sui::types::TypeTag>()
3365 .unwrap();
3366 let field_type = sui::types::StructTag::new(
3367 sui::types::Address::from_static("0x2"),
3368 sui::types::Identifier::from_static("dynamic_field"),
3369 sui::types::Identifier::from_static("Field"),
3370 vec![
3371 wrapper_type,
3372 sui::types::TypeTag::Struct(Box::new(sui::types::StructTag::new(
3373 sui::types::Address::from_static("0x2"),
3374 sui::types::Identifier::from_static("object"),
3375 sui::types::Identifier::from_static("ID"),
3376 vec![],
3377 ))),
3378 ],
3379 );
3380 let mut field_object = sui::grpc::Object::default();
3381 field_object.set_object_type(field_type.to_string());
3382 let mut field = sui::grpc::DynamicField::default();
3383 field.set_kind(sui::grpc::dynamic_field::DynamicFieldKind::Object);
3384 field.set_field_id(field_id.to_string());
3385 field.set_field_object(field_object);
3386 field.set_value_type(child_type.to_string());
3387
3388 let metadata = parse_dynamic_field_metadata(parent_id, &field).unwrap();
3389
3390 assert!(metadata.is_none());
3391 }
3392
3393 fn coin_object(
3394 object_ref: sui::types::ObjectReference,
3395 owner: sui::types::Owner,
3396 balance: u64,
3397 object_type: &sui::types::StructTag,
3398 ) -> sui::grpc::Object {
3399 let mut object = sui::grpc::Object::default();
3400 object.set_object_id(*object_ref.object_id());
3401 object.set_owner(sui::grpc::Owner::from(owner));
3402 object.set_digest(*object_ref.digest());
3403 object.set_version(object_ref.version());
3404 object.set_balance(balance);
3405 object.set_object_type(object_type.to_string());
3406 object
3407 }
3408
3409 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
3410 async fn inner_client_clones_allow_independent_requests_to_progress_concurrently() {
3411 let owner = sui::types::Address::from_static("0xa");
3412 let object_type = sui::types::StructTag::gas_coin();
3413 let metadata_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x10"));
3414 let coin_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x11"));
3415 let request_barrier = Arc::new(Barrier::new(2));
3416 let mut ledger_service_mock = sui_mocks::grpc::MockLedgerService::new();
3417 ledger_service_mock
3418 .expect_get_object()
3419 .times(1)
3420 .return_once({
3421 let request_barrier = Arc::clone(&request_barrier);
3422 let metadata_ref = metadata_ref.clone();
3423 move |_request| {
3424 request_barrier.wait();
3425 let object = object_with_bcs(
3426 metadata_ref,
3427 sui::types::Owner::Immutable,
3428 &TestValue { value: 1 },
3429 );
3430 let mut response = sui::grpc::GetObjectResponse::default();
3431 response.set_object(object);
3432 Ok(tonic::Response::new(response))
3433 }
3434 });
3435 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
3436 state_service_mock
3437 .expect_list_owned_objects()
3438 .times(1)
3439 .return_once({
3440 let request_barrier = Arc::clone(&request_barrier);
3441 let coin_ref = coin_ref.clone();
3442 let object_type = object_type.clone();
3443 move |_request| {
3444 request_barrier.wait();
3445 let mut response = sui::grpc::ListOwnedObjectsResponse::default();
3446 response.set_objects(vec![coin_object(
3447 coin_ref,
3448 sui::types::Owner::Address(owner),
3449 50,
3450 &object_type,
3451 )]);
3452 Ok(tonic::Response::new(response))
3453 }
3454 });
3455 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3456 ledger_service_mock: Some(ledger_service_mock),
3457 state_service_mock: Some(state_service_mock),
3458 ..Default::default()
3459 });
3460 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
3461
3462 let (metadata, coins) = tokio::time::timeout(std::time::Duration::from_secs(2), async {
3463 tokio::join!(
3464 crawler.get_object_metadata(*metadata_ref.object_id()),
3465 crawler.fetch_coins_for_address_by_type(owner, object_type),
3466 )
3467 })
3468 .await
3469 .expect("independent RPCs should not wait on a shared client mutex");
3470
3471 assert_eq!(
3472 metadata
3473 .expect("metadata request should succeed")
3474 .object_ref(),
3475 metadata_ref
3476 );
3477 assert_eq!(
3478 coins.expect("coin request should succeed"),
3479 vec![(coin_ref, 50)]
3480 );
3481 }
3482
3483 #[tokio::test]
3484 async fn preparation_retries_only_the_failed_object_read() {
3485 let good_id = sui::types::Address::from_static("0x91");
3486 let flaky_id = sui::types::Address::from_static("0x92");
3487 let mut ledger = sui_mocks::grpc::MockLedgerService::new();
3488 ledger
3489 .expect_get_object()
3490 .withf(move |request| request.get_ref().object_id() == good_id.to_string())
3491 .times(1)
3492 .returning(move |_| {
3493 let object = object_with_bcs(
3494 sui_mocks::object_ref_for_id(good_id),
3495 sui::types::Owner::Immutable,
3496 &TestValue { value: 1 },
3497 );
3498 Ok(tonic::Response::new(
3499 sui::grpc::GetObjectResponse::default().with_object(object),
3500 ))
3501 });
3502 let mut attempts = 0;
3503 ledger
3504 .expect_get_object()
3505 .withf(move |request| request.get_ref().object_id() == flaky_id.to_string())
3506 .times(3)
3507 .returning(move |_| {
3508 attempts += 1;
3509 if attempts < 3 {
3510 return Err(tonic::Status::unavailable("offline"));
3511 }
3512 let object = object_with_bcs(
3513 sui_mocks::object_ref_for_id(flaky_id),
3514 sui::types::Owner::Immutable,
3515 &TestValue { value: 2 },
3516 );
3517 Ok(tonic::Response::new(
3518 sui::grpc::GetObjectResponse::default().with_object(object),
3519 ))
3520 });
3521 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3522 ledger_service_mock: Some(ledger),
3523 ..Default::default()
3524 });
3525 sui::grpc::set_retry_request_budget(
3526 &rpc_url,
3527 std::num::NonZeroU32::new(20).unwrap(),
3528 std::num::NonZeroU32::new(1).unwrap(),
3529 )
3530 .unwrap();
3531 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).unwrap()));
3532 let (good, flaky) = sui::grpc::with_read_retry_until(
3533 tokio::time::Instant::now() + std::time::Duration::from_secs(5),
3534 async {
3535 tokio::try_join!(
3536 crawler.get_object::<TestValue>(good_id),
3537 crawler.get_object::<TestValue>(flaky_id)
3538 )
3539 },
3540 )
3541 .await
3542 .unwrap();
3543 assert_eq!(good.data.value, 1);
3544 assert_eq!(flaky.data.value, 2);
3545 }
3546
3547 #[tokio::test]
3548 async fn optional_object_batch_preserves_missing_positions() {
3549 let first_id = sui::types::Address::from_static("0x71");
3550 let missing_id = sui::types::Address::from_static("0x72");
3551 let last_id = sui::types::Address::from_static("0x73");
3552 let requested_ids = [first_id, missing_id, last_id];
3553 let first_ref = sui_mocks::object_ref_for_id(first_id);
3554 let last_ref = sui_mocks::object_ref_for_id(last_id);
3555 let expected_ids = requested_ids.map(|id| id.to_string());
3556 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
3557 ledger_service
3558 .expect_batch_get_objects()
3559 .times(1)
3560 .return_once(move |request| {
3561 let actual_ids = request
3562 .get_ref()
3563 .requests
3564 .iter()
3565 .map(|request| request.object_id.clone().expect("object ID"))
3566 .collect::<Vec<_>>();
3567 assert_eq!(actual_ids, expected_ids);
3568
3569 let missing = sui_rpc::proto::google::rpc::Status {
3570 code: tonic::Code::NotFound.into(),
3571 message: "object not found".to_owned(),
3572 ..Default::default()
3573 };
3574 let response = sui::grpc::BatchGetObjectsResponse::new(vec![
3575 sui::grpc::GetObjectResult::new_object(object_with_bcs(
3576 first_ref,
3577 sui::types::Owner::Shared(1),
3578 &TestValue { value: 3 },
3579 )),
3580 sui::grpc::GetObjectResult::new_error(missing),
3581 sui::grpc::GetObjectResult::new_object(object_with_bcs(
3582 last_ref,
3583 sui::types::Owner::Shared(1),
3584 &TestValue { value: 5 },
3585 )),
3586 ]);
3587 Ok(tonic::Response::new(response))
3588 });
3589
3590 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3591 ledger_service_mock: Some(ledger_service),
3592 ..Default::default()
3593 });
3594 let client = sui::grpc::client(rpc_url).expect("mock client");
3595 let crawler = Crawler::new(Arc::new(client));
3596
3597 let objects = crawler
3598 .get_optional_objects::<TestValue>(&requested_ids)
3599 .await
3600 .expect("optional objects load");
3601
3602 assert_eq!(
3603 objects[0].as_ref().map(|object| &object.data),
3604 Some(&TestValue { value: 3 })
3605 );
3606 assert!(objects[1].is_none());
3607 assert_eq!(
3608 objects[2].as_ref().map(|object| &object.data),
3609 Some(&TestValue { value: 5 })
3610 );
3611 }
3612
3613 #[tokio::test]
3614 async fn fetch_coins_for_address_by_type_reads_every_page() {
3615 let owner = sui::types::Address::from_static("0xa");
3616 let object_type = sui::types::StructTag::gas_coin();
3617 let first_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x10"));
3618 let second_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x11"));
3619 let responses = vec![
3620 (
3621 vec![coin_object(
3622 first_ref.clone(),
3623 sui::types::Owner::Address(owner),
3624 70,
3625 &object_type,
3626 )],
3627 Some(Vec::from(&b"page-2"[..])),
3628 ),
3629 (
3630 vec![coin_object(
3631 second_ref.clone(),
3632 sui::types::Owner::Address(owner),
3633 30,
3634 &object_type,
3635 )],
3636 None,
3637 ),
3638 ];
3639 let mut responses = responses.into_iter();
3640 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
3641 state_service_mock
3642 .expect_list_owned_objects()
3643 .times(2)
3644 .with(always())
3645 .returning(move |_request| {
3646 let (objects, next_page_token) = responses.next().expect("typed coin page");
3647 let mut response = sui::grpc::ListOwnedObjectsResponse::default();
3648 response.set_objects(objects);
3649 response.next_page_token = next_page_token.map(Into::into);
3650 Ok(tonic::Response::new(response))
3651 });
3652
3653 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3654 state_service_mock: Some(state_service_mock),
3655 ..Default::default()
3656 });
3657 let client = sui::grpc::Client::new(rpc_url).expect("mock client");
3658 let crawler = Crawler::new(Arc::new(client));
3659
3660 let coins = crawler
3661 .fetch_coins_for_address_by_type(owner, object_type)
3662 .await
3663 .expect("typed coins load");
3664
3665 assert_eq!(coins, vec![(first_ref, 70), (second_ref, 30)]);
3666 }
3667
3668 #[tokio::test]
3669 async fn fetch_coins_for_address_by_type_excludes_wrong_owner_and_type() {
3670 let owner = sui::types::Address::from_static("0xa");
3671 let other_owner = sui::types::Address::from_static("0xb");
3672 let object_type = sui::types::StructTag::gas_coin();
3673 let valid_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x20"));
3674 let wrong_owner_ref =
3675 sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x21"));
3676 let wrong_type_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x22"));
3677 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
3678 state_service_mock
3679 .expect_list_owned_objects()
3680 .times(1)
3681 .with(always())
3682 .return_once({
3683 let valid_ref = valid_ref.clone();
3684 let object_type = object_type.clone();
3685 move |_request| {
3686 let mut response = sui::grpc::ListOwnedObjectsResponse::default();
3687 response.set_objects(vec![
3688 coin_object(
3689 valid_ref,
3690 sui::types::Owner::Address(owner),
3691 50,
3692 &object_type,
3693 ),
3694 coin_object(
3695 wrong_owner_ref,
3696 sui::types::Owner::Address(other_owner),
3697 60,
3698 &object_type,
3699 ),
3700 coin_object(
3701 wrong_type_ref,
3702 sui::types::Owner::Address(owner),
3703 70,
3704 &test_value_tag(),
3705 ),
3706 ]);
3707 Ok(tonic::Response::new(response))
3708 }
3709 });
3710
3711 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3712 state_service_mock: Some(state_service_mock),
3713 ..Default::default()
3714 });
3715 let client = sui::grpc::Client::new(rpc_url).expect("mock client");
3716 let crawler = Crawler::new(Arc::new(client));
3717
3718 let coins = crawler
3719 .fetch_coins_for_address_by_type(owner, object_type)
3720 .await
3721 .expect("typed coins load");
3722
3723 assert_eq!(coins, vec![(valid_ref, 50)]);
3724 }
3725
3726 #[tokio::test]
3727 async fn get_owned_objects_pages_and_deserializes_bcs() {
3728 let owner = sui::types::Address::from_static("0xa");
3729 let first_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x10"));
3730 let second_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x11"));
3731 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
3732 let first_object = typed_object_with_bcs(
3733 first_ref.clone(),
3734 sui::types::Owner::Address(owner),
3735 &test_value_tag(),
3736 &TestValue { value: 7 },
3737 );
3738 let second_object = typed_object_with_bcs(
3739 second_ref.clone(),
3740 sui::types::Owner::Address(owner),
3741 &test_value_tag(),
3742 &TestValue { value: 9 },
3743 );
3744 let responses: Vec<(Vec<sui::grpc::Object>, Option<Vec<u8>>)> = vec![
3745 (vec![first_object], Some(Vec::from(&b"page-2"[..]))),
3746 (vec![second_object], None),
3747 ];
3748 let mut responses = responses.into_iter();
3749 state_service_mock
3750 .expect_list_owned_objects()
3751 .times(2)
3752 .with(always())
3753 .returning(move |_request| {
3754 let (objects, next_page_token) = responses.next().expect("owned object page");
3755 let mut response = sui::grpc::ListOwnedObjectsResponse::default();
3756 response.set_objects(objects);
3757 response.next_page_token = next_page_token.map(Into::into);
3758 Ok(tonic::Response::new(response))
3759 });
3760
3761 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3762 state_service_mock: Some(state_service_mock),
3763 ..Default::default()
3764 });
3765 let client = sui::grpc::client(rpc_url).expect("mock client");
3766 let crawler = Crawler::new(Arc::new(client));
3767
3768 let objects = crawler
3769 .get_owned_objects::<TestValue>(owner, test_value_tag())
3770 .await
3771 .expect("owned objects load");
3772
3773 assert_eq!(objects.len(), 2);
3774 assert_eq!(objects[0].object_id, *first_ref.object_id());
3775 assert_eq!(objects[0].data, TestValue { value: 7 });
3776 assert_eq!(objects[1].object_id, *second_ref.object_id());
3777 assert_eq!(objects[1].data, TestValue { value: 9 });
3778 }
3779
3780 #[tokio::test]
3781 async fn owned_object_page_preserves_filters_and_both_cursors() {
3782 let owner = sui::types::Address::from_static("0xa");
3783 let object_type = test_value_tag();
3784 let object_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x10"));
3785 let request_cursor = Vec::from(&b"request-cursor"[..]);
3786 let response_cursor = Vec::from(&b"response-cursor"[..]);
3787 let expected_owner = owner.to_string();
3788 let expected_type = object_type.to_string();
3789 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
3790 state_service_mock
3791 .expect_list_owned_objects()
3792 .times(1)
3793 .return_once({
3794 let object_type = object_type.clone();
3795 let object_ref = object_ref.clone();
3796 let request_cursor = request_cursor.clone();
3797 let response_cursor = response_cursor.clone();
3798 move |request| {
3799 let request = request.get_ref();
3800 assert_eq!(request.owner.as_deref(), Some(expected_owner.as_str()));
3801 assert_eq!(request.object_type.as_deref(), Some(expected_type.as_str()));
3802 assert_eq!(request.page_size, Some(2));
3803 assert_eq!(
3804 request.page_token.as_deref(),
3805 Some(request_cursor.as_slice())
3806 );
3807
3808 let mut response = sui::grpc::ListOwnedObjectsResponse::default();
3809 response.set_objects(vec![typed_object_with_bcs(
3810 object_ref,
3811 sui::types::Owner::ConsensusAddress {
3812 start_version: 5,
3813 owner,
3814 },
3815 &object_type,
3816 &TestValue { value: 42 },
3817 )]);
3818 response.next_page_token = Some(response_cursor.into());
3819 Ok(tonic::Response::new(response))
3820 }
3821 });
3822
3823 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3824 state_service_mock: Some(state_service_mock),
3825 ..Default::default()
3826 });
3827 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
3828
3829 let page = crawler
3830 .get_owned_object_page::<TestValue>(owner, object_type, Some(request_cursor), 2)
3831 .await
3832 .expect("owned object page loads");
3833
3834 assert_eq!(page.data().len(), 1);
3835 assert_eq!(page.data()[0].object_id, *object_ref.object_id());
3836 assert_eq!(page.data()[0].data, TestValue { value: 42 });
3837 assert_eq!(
3838 page.data()[0].owner,
3839 sui::types::Owner::ConsensusAddress {
3840 start_version: 5,
3841 owner,
3842 }
3843 );
3844 assert_eq!(page.next_cursor(), Some(response_cursor.as_slice()));
3845 }
3846
3847 #[tokio::test]
3848 async fn object_owned_objects_page_and_validate_the_exact_parent() {
3849 let parent_id = sui::types::Address::from_static("0xa");
3850 let object_type = test_value_tag();
3851 let first_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x20"));
3852 let second_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x21"));
3853 let expected_owner = parent_id.to_string();
3854 let expected_type = object_type.to_string();
3855 let responses = vec![
3856 (
3857 typed_object_with_bcs(
3858 first_ref.clone(),
3859 sui::types::Owner::Object(parent_id),
3860 &object_type,
3861 &TestValue { value: 7 },
3862 ),
3863 Some(Vec::from(&b"page-2"[..])),
3864 ),
3865 (
3866 typed_object_with_bcs(
3867 second_ref.clone(),
3868 sui::types::Owner::Object(parent_id),
3869 &object_type,
3870 &TestValue { value: 9 },
3871 ),
3872 None,
3873 ),
3874 ];
3875 let mut responses = responses.into_iter();
3876 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
3877 state_service_mock
3878 .expect_list_owned_objects()
3879 .times(2)
3880 .returning(move |request| {
3881 let request = request.get_ref();
3882 assert_eq!(request.owner.as_deref(), Some(expected_owner.as_str()));
3883 assert_eq!(request.object_type.as_deref(), Some(expected_type.as_str()));
3884 let (object, next_page_token) = responses.next().expect("object-owned page");
3885 let mut response = sui::grpc::ListOwnedObjectsResponse::default();
3886 response.set_objects(vec![object]);
3887 response.next_page_token = next_page_token.map(Into::into);
3888 Ok(tonic::Response::new(response))
3889 });
3890
3891 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3892 state_service_mock: Some(state_service_mock),
3893 ..Default::default()
3894 });
3895 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
3896
3897 let objects = crawler
3898 .get_object_owned_objects::<TestValue>(parent_id, object_type)
3899 .await
3900 .expect("object-owned objects load");
3901
3902 assert_eq!(objects.len(), 2);
3903 assert_eq!(objects[0].object_id, *first_ref.object_id());
3904 assert_eq!(objects[1].object_id, *second_ref.object_id());
3905 assert!(objects
3906 .iter()
3907 .all(|object| object.owner == sui::types::Owner::Object(parent_id)));
3908 }
3909
3910 #[tokio::test]
3911 async fn object_owned_object_page_rejects_address_ownership() {
3912 let parent_id = sui::types::Address::from_static("0xa");
3913 let object_type = test_value_tag();
3914 let object_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x20"));
3915 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
3916 state_service_mock
3917 .expect_list_owned_objects()
3918 .times(1)
3919 .return_once({
3920 let object_type = object_type.clone();
3921 move |_| {
3922 let mut response = sui::grpc::ListOwnedObjectsResponse::default();
3923 response.set_objects(vec![typed_object_with_bcs(
3924 object_ref,
3925 sui::types::Owner::Address(parent_id),
3926 &object_type,
3927 &TestValue { value: 7 },
3928 )]);
3929 Ok(tonic::Response::new(response))
3930 }
3931 });
3932
3933 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3934 state_service_mock: Some(state_service_mock),
3935 ..Default::default()
3936 });
3937 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
3938
3939 let error = crawler
3940 .get_object_owned_object_page::<TestValue>(parent_id, object_type, None, 1)
3941 .await
3942 .expect_err("address ownership must be rejected");
3943 assert!(error.to_string().contains("expected 'Object"));
3944 }
3945
3946 #[tokio::test]
3947 async fn object_owned_object_page_rejects_the_wrong_type() {
3948 let parent_id = sui::types::Address::from_static("0xa");
3949 let expected_type = test_value_tag();
3950 let wrong_type = sui::types::StructTag::new(
3951 sui::types::Address::from_static("0xa1"),
3952 sui::types::Identifier::from_static("test"),
3953 sui::types::Identifier::from_static("WrongValue"),
3954 vec![],
3955 );
3956 let object_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x20"));
3957 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
3958 state_service_mock
3959 .expect_list_owned_objects()
3960 .times(1)
3961 .return_once(move |_| {
3962 let mut response = sui::grpc::ListOwnedObjectsResponse::default();
3963 response.set_objects(vec![typed_object_with_bcs(
3964 object_ref,
3965 sui::types::Owner::Object(parent_id),
3966 &wrong_type,
3967 &TestValue { value: 7 },
3968 )]);
3969 Ok(tonic::Response::new(response))
3970 });
3971
3972 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
3973 state_service_mock: Some(state_service_mock),
3974 ..Default::default()
3975 });
3976 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
3977
3978 let error = crawler
3979 .get_object_owned_object_page::<TestValue>(parent_id, expected_type.clone(), None, 1)
3980 .await
3981 .expect_err("wrong type must be rejected");
3982 assert!(error.to_string().contains(&expected_type.to_string()));
3983 }
3984
3985 #[tokio::test]
3986 async fn dynamic_object_field_by_key_fetches_only_the_wrapper_and_child() {
3987 let parent_id = sui::types::Address::from_static("0x40");
3988 let child_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x42"));
3989 let key = TestKey {
3990 name: "primary".to_string(),
3991 };
3992 let key_type = sui::types::TypeTag::Struct(Box::new(sui::types::StructTag::new(
3993 sui::types::Address::from_static("0xa1"),
3994 sui::types::Identifier::from_static("test"),
3995 sui::types::Identifier::from_static("TestKey"),
3996 vec![],
3997 )));
3998 let wrapper_type = sui::types::TypeTag::Struct(Box::new(sui::types::StructTag::new(
3999 sui::types::Address::from_static("0x2"),
4000 sui::types::Identifier::from_static("dynamic_object_field"),
4001 sui::types::Identifier::from_static("Wrapper"),
4002 vec![key_type.clone()],
4003 )));
4004 let wrapper_key = TestDynamicObjectFieldName { name: key.clone() };
4005 let field_id = parent_id.derive_dynamic_child_id(
4006 &wrapper_type,
4007 &bcs::to_bytes(&wrapper_key).expect("wrapper key serializes"),
4008 );
4009 let mut ledger_service_mock = sui_mocks::grpc::MockLedgerService::new();
4010 sui_mocks::grpc::mock_get_object_bcs(
4011 &mut ledger_service_mock,
4012 sui_mocks::object_ref_for_id(field_id),
4013 sui::types::Owner::Object(parent_id),
4014 bcs::to_bytes(&DynamicFieldValue {
4015 id: field_id,
4016 name: wrapper_key,
4017 value: crate::move_bindings::sui_framework::object::ID::new(*child_ref.object_id()),
4018 })
4019 .expect("wrapper field serializes"),
4020 );
4021 sui_mocks::grpc::mock_get_object_bcs(
4022 &mut ledger_service_mock,
4023 child_ref.clone(),
4024 sui::types::Owner::Shared(child_ref.version()),
4025 bcs::to_bytes(&TestValue { value: 11 }).expect("test value serializes"),
4026 );
4027
4028 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4029 ledger_service_mock: Some(ledger_service_mock),
4030 ..Default::default()
4031 });
4032 let client = sui::grpc::client(rpc_url).expect("mock client");
4033 let crawler = Crawler::new(Arc::new(client));
4034
4035 let object = crawler
4036 .get_dynamic_object_field_by_key::<TestKey, TestValue>(parent_id, key, &key_type)
4037 .await
4038 .expect("dynamic object field request succeeds")
4039 .expect("dynamic object field exists");
4040
4041 assert_eq!(object.object_id, *child_ref.object_id());
4042 assert_eq!(object.data, TestValue { value: 11 });
4043 }
4044
4045 #[tokio::test]
4046 async fn dynamic_object_field_by_key_rejects_a_child_identity_mismatch() {
4047 let parent_id = sui::types::Address::from_static("0x40");
4048 let child_id = sui::types::Address::from_static("0x42");
4049 let returned_child_ref =
4050 sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x43"));
4051 let key = TestKey {
4052 name: "primary".to_string(),
4053 };
4054 let key_type = sui::types::TypeTag::Struct(Box::new(sui::types::StructTag::new(
4055 sui::types::Address::from_static("0xa1"),
4056 sui::types::Identifier::from_static("test"),
4057 sui::types::Identifier::from_static("TestKey"),
4058 vec![],
4059 )));
4060 let wrapper_type = dynamic_object_field_wrapper_type(&key_type);
4061 let wrapper_key = TestDynamicObjectFieldName { name: key.clone() };
4062 let field_id = parent_id.derive_dynamic_child_id(
4063 &wrapper_type,
4064 &bcs::to_bytes(&wrapper_key).expect("wrapper key serializes"),
4065 );
4066 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
4067 sui_mocks::grpc::mock_get_object_bcs(
4068 &mut ledger_service,
4069 sui_mocks::object_ref_for_id(field_id),
4070 sui::types::Owner::Object(parent_id),
4071 bcs::to_bytes(&DynamicFieldValue {
4072 id: field_id,
4073 name: wrapper_key,
4074 value: crate::move_bindings::sui_framework::object::ID::new(child_id),
4075 })
4076 .expect("wrapper field serializes"),
4077 );
4078 ledger_service
4079 .expect_get_object()
4080 .withf(move |request| {
4081 request.get_ref().object_id.as_deref() == Some(child_id.to_string().as_str())
4082 })
4083 .times(1)
4084 .return_once(move |_| {
4085 let mut response = sui::grpc::GetObjectResponse::default();
4086 response.set_object(object_with_bcs(
4087 returned_child_ref,
4088 sui::types::Owner::Shared(1),
4089 &TestValue { value: 11 },
4090 ));
4091 Ok(tonic::Response::new(response))
4092 });
4093
4094 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4095 ledger_service_mock: Some(ledger_service),
4096 ..Default::default()
4097 });
4098 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
4099
4100 let error = crawler
4101 .get_dynamic_object_field_by_key::<TestKey, TestValue>(parent_id, key, &key_type)
4102 .await
4103 .expect_err("mismatched child identity must fail");
4104
4105 assert!(
4106 error.to_string().contains("received object"),
4107 "unexpected error: {error}"
4108 );
4109 }
4110
4111 #[tokio::test]
4112 async fn get_dynamic_fields_decodes_field_object_bcs() {
4113 let parent_id = sui::types::Address::from_static("0x70");
4114 let key = TestKey {
4115 name: "wanted".to_string(),
4116 };
4117 let field_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x71"));
4118 let field = DynamicFieldValue {
4119 id: *field_ref.object_id(),
4120 name: key.clone(),
4121 value: TestValue { value: 42 },
4122 };
4123 let field_type = sui::types::StructTag::new(
4124 sui::types::Address::from_static("0x2"),
4125 sui::types::Identifier::from_static("dynamic_field"),
4126 sui::types::Identifier::from_static("Field"),
4127 vec![],
4128 );
4129
4130 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
4131 sui_mocks::grpc::mock_list_dynamic_fields(
4132 &mut state_service_mock,
4133 vec![(key.clone(), *field_ref.object_id())],
4134 );
4135 let mut ledger_service_mock = sui_mocks::grpc::MockLedgerService::new();
4136 sui_mocks::grpc::mock_get_objects_bcs(
4137 &mut ledger_service_mock,
4138 vec![(
4139 field_ref.clone(),
4140 sui::types::Owner::Shared(1),
4141 bcs::to_bytes(&field).expect("dynamic field bcs"),
4142 field_type,
4143 )],
4144 );
4145
4146 let catalog_rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4147 state_service_mock: Some(state_service_mock),
4148 ..Default::default()
4149 });
4150 let live_rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4151 ledger_service_mock: Some(ledger_service_mock),
4152 ..Default::default()
4153 });
4154 let live = Arc::new(sui::grpc::client(live_rpc_url).expect("live mock client"));
4155 let catalog = Arc::new(sui::grpc::client(catalog_rpc_url).expect("catalog mock client"));
4156 let crawler = Crawler::with_state_catalog(live, catalog);
4157
4158 let fields = crawler
4159 .get_dynamic_fields::<TestKey, TestValue>(parent_id, 1)
4160 .await
4161 .expect("dynamic field value decodes");
4162
4163 assert_eq!(fields.get(&key), Some(&TestValue { value: 42 }));
4164 }
4165
4166 #[tokio::test]
4167 async fn get_dynamic_field_by_key_derives_the_field_identity() {
4168 let parent_id = sui::types::Address::from_static("0x70");
4169 let key = 7_u64;
4170 let key_type = sui::types::TypeTag::U64;
4171 let field_id = parent_id
4172 .derive_dynamic_child_id(&key_type, &bcs::to_bytes(&key).expect("key serializes"));
4173 let field_ref = sui_mocks::object_ref_for_id(field_id);
4174 let field = DynamicFieldValue {
4175 id: field_id,
4176 name: key,
4177 value: TestValue { value: 42 },
4178 };
4179 let mut ledger_service_mock = sui_mocks::grpc::MockLedgerService::new();
4180 sui_mocks::grpc::mock_get_object_bcs(
4181 &mut ledger_service_mock,
4182 field_ref,
4183 sui::types::Owner::Shared(1),
4184 bcs::to_bytes(&field).expect("dynamic field serializes"),
4185 );
4186
4187 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4188 ledger_service_mock: Some(ledger_service_mock),
4189 ..Default::default()
4190 });
4191 let client = sui::grpc::client(rpc_url).expect("mock client");
4192 let crawler = Crawler::new(Arc::new(client));
4193
4194 let value = crawler
4195 .get_dynamic_field_by_key::<u64, TestValue>(parent_id, key, &key_type)
4196 .await
4197 .expect("dynamic field loads");
4198
4199 assert_eq!(value, Some(TestValue { value: 42 }));
4200 }
4201
4202 #[tokio::test]
4203 async fn dynamic_field_by_key_rejects_an_embedded_id_mismatch() {
4204 let parent_id = sui::types::Address::from_static("0x70");
4205 let key = 7_u64;
4206 let key_type = sui::types::TypeTag::U64;
4207 let field_id = parent_id
4208 .derive_dynamic_child_id(&key_type, &bcs::to_bytes(&key).expect("key serializes"));
4209 let field_ref = sui_mocks::object_ref_for_id(field_id);
4210 let field = DynamicFieldValue {
4211 id: sui::types::Address::from_static("0x71"),
4212 name: key,
4213 value: TestValue { value: 42 },
4214 };
4215 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
4216 sui_mocks::grpc::mock_get_object_bcs(
4217 &mut ledger_service,
4218 field_ref,
4219 sui::types::Owner::Object(parent_id),
4220 bcs::to_bytes(&field).expect("dynamic field serializes"),
4221 );
4222
4223 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4224 ledger_service_mock: Some(ledger_service),
4225 ..Default::default()
4226 });
4227 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
4228
4229 let error = crawler
4230 .get_dynamic_field_by_key::<u64, TestValue>(parent_id, key, &key_type)
4231 .await
4232 .expect_err("mismatched embedded ID must fail");
4233
4234 assert!(
4235 error.to_string().contains("unexpected embedded ID"),
4236 "unexpected error: {error}"
4237 );
4238 }
4239
4240 #[tokio::test]
4241 async fn dynamic_fields_by_keys_fetch_only_distinct_derived_ids() {
4242 let parent_id = sui::types::Address::from_static("0x70");
4243 let keys = [7_u64, 7_u64, 9_u64];
4244 let key_type = sui::types::TypeTag::U64;
4245 let requested_keys = [7_u64, 9_u64];
4246 let requested_ids = requested_keys.map(|key| {
4247 parent_id
4248 .derive_dynamic_child_id(&key_type, &bcs::to_bytes(&key).expect("key serializes"))
4249 });
4250
4251 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
4252 ledger_service
4253 .expect_batch_get_objects()
4254 .times(1)
4255 .return_once(move |request| {
4256 let actual_ids = request
4257 .get_ref()
4258 .requests
4259 .iter()
4260 .map(|request| {
4261 request
4262 .object_id
4263 .as_deref()
4264 .expect("object ID")
4265 .parse()
4266 .expect("valid object ID")
4267 })
4268 .collect::<Vec<sui::types::Address>>();
4269 assert_eq!(actual_ids, requested_ids);
4270
4271 let objects = requested_keys
4272 .into_iter()
4273 .zip(requested_ids)
4274 .map(|(name, object_id)| {
4275 sui::grpc::GetObjectResult::new_object(object_with_bcs(
4276 sui_mocks::object_ref_for_id(object_id),
4277 sui::types::Owner::Object(parent_id),
4278 &DynamicFieldValue {
4279 id: object_id,
4280 name,
4281 value: TestValue { value: name },
4282 },
4283 ))
4284 })
4285 .collect();
4286 Ok(tonic::Response::new(
4287 sui::grpc::BatchGetObjectsResponse::new(objects),
4288 ))
4289 });
4290
4291 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4292 ledger_service_mock: Some(ledger_service),
4293 ..Default::default()
4294 });
4295 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
4296
4297 let fields = crawler
4298 .get_dynamic_fields_by_keys::<u64, TestValue, _>(parent_id, keys, &key_type)
4299 .await
4300 .expect("exact fields load");
4301
4302 assert_eq!(fields.len(), 2);
4303 assert_eq!(fields[&7].value, 7);
4304 assert_eq!(fields[&9].value, 9);
4305 }
4306
4307 #[tokio::test]
4308 async fn dynamic_fields_by_keys_omit_missing_fields() {
4309 let parent_id = sui::types::Address::from_static("0x70");
4310 let key_type = sui::types::TypeTag::U64;
4311 let keys = [7_u64, 9_u64];
4312 let field_ids = keys.map(|key| {
4313 parent_id
4314 .derive_dynamic_child_id(&key_type, &bcs::to_bytes(&key).expect("key serializes"))
4315 });
4316 let present_id = field_ids[0];
4317
4318 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
4319 ledger_service
4320 .expect_batch_get_objects()
4321 .times(1)
4322 .return_once(move |_| {
4323 let missing = sui_rpc::proto::google::rpc::Status {
4324 code: tonic::Code::NotFound.into(),
4325 message: "object not found".to_owned(),
4326 ..Default::default()
4327 };
4328 Ok(tonic::Response::new(
4329 sui::grpc::BatchGetObjectsResponse::new(vec![
4330 sui::grpc::GetObjectResult::new_object(object_with_bcs(
4331 sui_mocks::object_ref_for_id(present_id),
4332 sui::types::Owner::Object(parent_id),
4333 &DynamicFieldValue {
4334 id: present_id,
4335 name: 7_u64,
4336 value: TestValue { value: 7 },
4337 },
4338 )),
4339 sui::grpc::GetObjectResult::new_error(missing),
4340 ]),
4341 ))
4342 });
4343
4344 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4345 ledger_service_mock: Some(ledger_service),
4346 ..Default::default()
4347 });
4348 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
4349
4350 let fields = crawler
4351 .get_dynamic_fields_by_keys::<u64, TestValue, _>(parent_id, keys, &key_type)
4352 .await
4353 .expect("exact fields load");
4354
4355 assert_eq!(fields, HashMap::from([(7, TestValue { value: 7 })]));
4356 }
4357
4358 #[tokio::test]
4359 async fn dynamic_fields_by_keys_skip_rpc_for_empty_input() {
4360 let rpc_url = sui_mocks::grpc::mock_server(Default::default());
4361 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
4362
4363 let fields = crawler
4364 .get_dynamic_fields_by_keys::<u64, TestValue, _>(
4365 sui::types::Address::from_static("0x70"),
4366 [],
4367 &sui::types::TypeTag::U64,
4368 )
4369 .await
4370 .expect("empty request succeeds");
4371
4372 assert!(fields.is_empty());
4373 }
4374
4375 #[tokio::test]
4376 async fn dynamic_fields_by_keys_reject_a_decoded_key_mismatch() {
4377 let parent_id = sui::types::Address::from_static("0x70");
4378 let key_type = sui::types::TypeTag::U64;
4379 let requested_key = 7_u64;
4380 let field_id = parent_id.derive_dynamic_child_id(
4381 &key_type,
4382 &bcs::to_bytes(&requested_key).expect("key serializes"),
4383 );
4384
4385 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
4386 ledger_service
4387 .expect_batch_get_objects()
4388 .times(1)
4389 .return_once(move |_| {
4390 Ok(tonic::Response::new(
4391 sui::grpc::BatchGetObjectsResponse::new(vec![
4392 sui::grpc::GetObjectResult::new_object(object_with_bcs(
4393 sui_mocks::object_ref_for_id(field_id),
4394 sui::types::Owner::Object(parent_id),
4395 &DynamicFieldValue {
4396 id: field_id,
4397 name: 9_u64,
4398 value: TestValue { value: 9 },
4399 },
4400 )),
4401 ]),
4402 ))
4403 });
4404
4405 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4406 ledger_service_mock: Some(ledger_service),
4407 ..Default::default()
4408 });
4409 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
4410
4411 let error = crawler
4412 .get_dynamic_fields_by_keys::<u64, TestValue, _>(parent_id, [requested_key], &key_type)
4413 .await
4414 .expect_err("mismatched key must fail");
4415
4416 assert!(
4417 error.to_string().contains("unexpected key"),
4418 "unexpected error: {error}"
4419 );
4420 }
4421
4422 #[tokio::test]
4423 async fn dynamic_fields_by_keys_reject_an_embedded_id_mismatch() {
4424 let parent_id = sui::types::Address::from_static("0x70");
4425 let key_type = sui::types::TypeTag::U64;
4426 let requested_key = 7_u64;
4427 let field_id = parent_id.derive_dynamic_child_id(
4428 &key_type,
4429 &bcs::to_bytes(&requested_key).expect("key serializes"),
4430 );
4431
4432 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
4433 ledger_service
4434 .expect_batch_get_objects()
4435 .times(1)
4436 .return_once(move |_| {
4437 Ok(tonic::Response::new(
4438 sui::grpc::BatchGetObjectsResponse::new(vec![
4439 sui::grpc::GetObjectResult::new_object(object_with_bcs(
4440 sui_mocks::object_ref_for_id(field_id),
4441 sui::types::Owner::Object(parent_id),
4442 &DynamicFieldValue {
4443 id: sui::types::Address::from_static("0x71"),
4444 name: requested_key,
4445 value: TestValue { value: 7 },
4446 },
4447 )),
4448 ]),
4449 ))
4450 });
4451
4452 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4453 ledger_service_mock: Some(ledger_service),
4454 ..Default::default()
4455 });
4456 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
4457
4458 let error = crawler
4459 .get_dynamic_fields_by_keys::<u64, TestValue, _>(parent_id, [requested_key], &key_type)
4460 .await
4461 .expect_err("mismatched embedded ID must fail");
4462
4463 assert!(
4464 error.to_string().contains("unexpected embedded ID"),
4465 "unexpected error: {error}"
4466 );
4467 }
4468
4469 #[tokio::test]
4470 async fn table_vec_fetches_exact_index_range_without_listing_fields() {
4471 let parent_id = sui::types::Address::from_static("0x70");
4472 let table = TableVec::<TestValue>::new(parent_id, 2);
4473 let indexes = [0_u64, 1_u64];
4474 let field_ids = indexes.map(|index| {
4475 parent_id.derive_dynamic_child_id(
4476 &sui::types::TypeTag::U64,
4477 &bcs::to_bytes(&index).expect("index serializes"),
4478 )
4479 });
4480
4481 let mut ledger_service = sui_mocks::grpc::MockLedgerService::new();
4482 ledger_service
4483 .expect_batch_get_objects()
4484 .times(1)
4485 .return_once(move |request| {
4486 let actual_ids = request
4487 .get_ref()
4488 .requests
4489 .iter()
4490 .map(|request| {
4491 request
4492 .object_id
4493 .as_deref()
4494 .expect("object ID")
4495 .parse()
4496 .expect("valid object ID")
4497 })
4498 .collect::<Vec<sui::types::Address>>();
4499 assert_eq!(actual_ids, field_ids);
4500
4501 let objects = indexes
4502 .into_iter()
4503 .zip(field_ids)
4504 .map(|(index, field_id)| {
4505 sui::grpc::GetObjectResult::new_object(object_with_bcs(
4506 sui_mocks::object_ref_for_id(field_id),
4507 sui::types::Owner::Object(parent_id),
4508 &DynamicFieldValue {
4509 id: field_id,
4510 name: index,
4511 value: TestValue { value: index + 3 },
4512 },
4513 ))
4514 })
4515 .collect();
4516 Ok(tonic::Response::new(
4517 sui::grpc::BatchGetObjectsResponse::new(objects),
4518 ))
4519 });
4520
4521 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4522 ledger_service_mock: Some(ledger_service),
4523 ..Default::default()
4524 });
4525 let crawler = Crawler::new(Arc::new(sui::grpc::client(rpc_url).expect("mock client")));
4526
4527 let values = crawler
4528 .get_table_vec(&table)
4529 .await
4530 .expect("table vector loads");
4531
4532 assert_eq!(values, [TestValue { value: 3 }, TestValue { value: 4 }]);
4533 }
4534
4535 #[tokio::test]
4536 async fn dynamic_field_page_forwards_both_cursors_unchanged() {
4537 let parent_id = sui::types::Address::from_static("0x70");
4538 let key = TestKey {
4539 name: "occurrence".to_owned(),
4540 };
4541 let field_ref = sui_mocks::object_ref_for_id(sui::types::Address::from_static("0x71"));
4542 let request_cursor = Vec::from(&b"request-cursor"[..]);
4543 let response_cursor = Vec::from(&b"response-cursor"[..]);
4544 let expected_parent = parent_id.to_string();
4545 let listed_field_ref = field_ref.clone();
4546 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
4547 state_service_mock
4548 .expect_list_dynamic_fields()
4549 .times(1)
4550 .return_once({
4551 let key = key.clone();
4552 let expected_parent = expected_parent.clone();
4553 let request_cursor = request_cursor.clone();
4554 let response_cursor = response_cursor.clone();
4555 move |request| {
4556 let request = request.get_ref();
4557 assert_eq!(request.parent.as_deref(), Some(expected_parent.as_str()));
4558 assert_eq!(request.page_size, Some(2));
4559 assert_eq!(
4560 request.page_token.as_deref(),
4561 Some(request_cursor.as_slice())
4562 );
4563
4564 let mut field = sui::grpc::DynamicField::default();
4565 field.set_field_id(*listed_field_ref.object_id());
4566 let mut name = sui::grpc::Bcs::default();
4567 name.set_value(bcs::to_bytes(&key).expect("key serializes"));
4568 field.set_name(name);
4569 field.set_value_type("0xa5::task::OccurrenceRecord");
4570 let mut response = sui::grpc::ListDynamicFieldsResponse::default();
4571 response.set_dynamic_fields(vec![field]);
4572 response.next_page_token = Some(response_cursor.into());
4573 Ok(tonic::Response::new(response))
4574 }
4575 });
4576 let mut ledger_service_mock = sui_mocks::grpc::MockLedgerService::new();
4577 sui_mocks::grpc::mock_get_dynamic_table_values_bcs(
4578 &mut ledger_service_mock,
4579 vec![(
4580 field_ref,
4581 sui::types::Owner::Shared(1),
4582 key.clone(),
4583 TestValue { value: 42 },
4584 )],
4585 );
4586
4587 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4588 ledger_service_mock: Some(ledger_service_mock),
4589 state_service_mock: Some(state_service_mock),
4590 ..Default::default()
4591 });
4592 let client = sui::grpc::client(rpc_url).expect("mock client");
4593 let crawler = Crawler::new(Arc::new(client));
4594
4595 let page = crawler
4596 .get_dynamic_field_page_matching_types::<TestKey, TestValue>(
4597 parent_id,
4598 Some(request_cursor),
4599 2,
4600 "::task::OccurrenceRecord",
4601 )
4602 .await
4603 .expect("dynamic field page loads");
4604
4605 assert_eq!(page.data(), &[(key, TestValue { value: 42 })]);
4606 assert_eq!(page.next_cursor(), Some(response_cursor.as_slice()));
4607 }
4608
4609 #[tokio::test]
4610 async fn get_dynamic_field_values_pages_and_decodes_values() {
4611 let parent_id = sui::types::Address::from_static("0x70");
4612 let mut state_service_mock = sui_mocks::grpc::MockStateService::new();
4613 type DynamicFieldTestResponse = (Vec<(&'static str, TestValue)>, Option<Vec<u8>>);
4614 let responses: Vec<DynamicFieldTestResponse> = vec![
4615 (
4616 vec![("test::First", TestValue { value: 3 })],
4617 Some(Vec::from(&b"page-2"[..])),
4618 ),
4619 (vec![("test::Second", TestValue { value: 5 })], None),
4620 ];
4621 let mut responses = responses.into_iter();
4622 state_service_mock
4623 .expect_list_dynamic_fields()
4624 .times(2)
4625 .with(always())
4626 .returning(move |_request| {
4627 let (fields, next_page_token) = responses.next().expect("dynamic field page");
4628 let mut response = sui::grpc::ListDynamicFieldsResponse::default();
4629 response.set_dynamic_fields(
4630 fields
4631 .into_iter()
4632 .map(|(value_type, value)| {
4633 let mut field = sui::grpc::DynamicField::default();
4634 let mut bcs_value = sui::grpc::Bcs::default();
4635 bcs_value.set_value(bcs::to_bytes(&value).expect("value bcs"));
4636 field.set_value(bcs_value);
4637 field.set_value_type(value_type);
4638 field
4639 })
4640 .collect(),
4641 );
4642 response.next_page_token = next_page_token.map(Into::into);
4643 Ok(tonic::Response::new(response))
4644 });
4645
4646 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4647 state_service_mock: Some(state_service_mock),
4648 ..Default::default()
4649 });
4650 let client = sui::grpc::client(rpc_url).expect("mock client");
4651 let crawler = Crawler::new(Arc::new(client));
4652
4653 let values = crawler
4654 .get_dynamic_field_values::<TestValue>(parent_id)
4655 .await
4656 .expect("dynamic field values load");
4657
4658 assert_eq!(
4659 values,
4660 vec![
4661 (Some("test::First".to_string()), TestValue { value: 3 }),
4662 (Some("test::Second".to_string()), TestValue { value: 5 }),
4663 ]
4664 );
4665 }
4666
4667 #[tokio::test]
4668 async fn get_transaction_update_rejects_mismatched_effects_digest() {
4669 let mut rng = rand::thread_rng();
4670 let requested_digest = sui::types::Digest::generate(&mut rng);
4671 let effects_digest = sui::types::Digest::generate(&mut rng);
4672 let effects =
4673 sui::types::TransactionEffects::V2(Box::new(sui::types::TransactionEffectsV2 {
4674 status: sui::types::ExecutionStatus::Success,
4675 epoch: 1,
4676 gas_used: sui::types::GasCostSummary {
4677 computation_cost: 0,
4678 storage_cost: 0,
4679 storage_rebate: 0,
4680 non_refundable_storage_fee: 0,
4681 },
4682 transaction_digest: effects_digest,
4683 gas_object_index: None,
4684 events_digest: None,
4685 dependencies: vec![],
4686 lamport_version: 1,
4687 changed_objects: vec![],
4688 unchanged_consensus_objects: vec![],
4689 auxiliary_data_digest: None,
4690 }));
4691 let mut ledger_service_mock = sui_mocks::grpc::MockLedgerService::new();
4692 ledger_service_mock
4693 .expect_get_transaction()
4694 .times(1)
4695 .returning(move |request| {
4696 assert_eq!(
4697 request.get_ref().digest_opt(),
4698 Some(requested_digest.to_string().as_str())
4699 );
4700 let mut grpc_effects = sui::grpc::TransactionEffects::default();
4701 grpc_effects.set_bcs(bcs::to_bytes(&effects).expect("effects serialize"));
4702 let mut transaction = sui::grpc::ExecutedTransaction::default();
4703 transaction.set_digest(requested_digest);
4704 transaction.set_checkpoint(1);
4705 transaction.set_effects(grpc_effects);
4706 let mut response = sui::grpc::GetTransactionResponse::default();
4707 response.set_transaction(transaction);
4708 Ok(tonic::Response::new(response))
4709 });
4710
4711 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4712 ledger_service_mock: Some(ledger_service_mock),
4713 ..Default::default()
4714 });
4715 let client = sui::grpc::client(rpc_url).expect("mock client");
4716 let crawler = Crawler::new(Arc::new(client));
4717
4718 let error = crawler
4719 .get_transaction_update(requested_digest)
4720 .await
4721 .expect_err("mismatched effects digest must fail permanently");
4722 let message = error.to_string();
4723 assert!(message.contains(&requested_digest.to_string()));
4724 assert!(message.contains(&effects_digest.to_string()));
4725 assert!(message.contains("effects transaction digest"));
4726 }
4727
4728 #[tokio::test]
4729 async fn get_transaction_update_retains_checkpoint() {
4730 let mut rng = rand::thread_rng();
4731 let digest = sui::types::Digest::generate(&mut rng);
4732 let effects =
4733 sui::types::TransactionEffects::V2(Box::new(sui::types::TransactionEffectsV2 {
4734 status: sui::types::ExecutionStatus::Success,
4735 epoch: 1,
4736 gas_used: sui::types::GasCostSummary {
4737 computation_cost: 0,
4738 storage_cost: 0,
4739 storage_rebate: 0,
4740 non_refundable_storage_fee: 0,
4741 },
4742 transaction_digest: digest,
4743 gas_object_index: None,
4744 events_digest: None,
4745 dependencies: vec![],
4746 lamport_version: 1,
4747 changed_objects: vec![],
4748 unchanged_consensus_objects: vec![],
4749 auxiliary_data_digest: None,
4750 }));
4751 let mut ledger_service_mock = sui_mocks::grpc::MockLedgerService::new();
4752 ledger_service_mock
4753 .expect_get_transaction()
4754 .times(1)
4755 .returning(move |_| {
4756 let mut grpc_effects = sui::grpc::TransactionEffects::default();
4757 grpc_effects.set_bcs(bcs::to_bytes(&effects).expect("effects serialize"));
4758 let mut grpc_events = sui::grpc::TransactionEvents::default();
4759 grpc_events.set_events(vec![]);
4760 let mut transaction = sui::grpc::ExecutedTransaction::default();
4761 transaction.set_digest(digest);
4762 transaction.set_checkpoint(42);
4763 transaction.set_effects(grpc_effects);
4764 transaction.set_events(grpc_events);
4765 let mut response = sui::grpc::GetTransactionResponse::default();
4766 response.set_transaction(transaction);
4767 Ok(tonic::Response::new(response))
4768 });
4769
4770 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4771 ledger_service_mock: Some(ledger_service_mock),
4772 ..Default::default()
4773 });
4774 let client = sui::grpc::client(rpc_url).expect("mock client");
4775 let crawler = Crawler::new(Arc::new(client));
4776
4777 let update = crawler
4778 .get_transaction_update(digest)
4779 .await
4780 .expect("transaction update loads");
4781
4782 assert_eq!(update.checkpoint, 42);
4783 }
4784
4785 #[tokio::test]
4786 async fn transaction_output_object_selects_the_written_version() {
4787 let mut rng = rand::thread_rng();
4788 let transaction = sui::types::Digest::generate(&mut rng);
4789 let earlier_transaction = sui::types::Digest::generate(&mut rng);
4790 let object_id = sui::types::Address::generate(&mut rng);
4791 let owner = sui::types::Owner::Object(sui::types::Address::generate(&mut rng));
4792 let input_ref =
4793 sui::types::ObjectReference::new(object_id, 1, sui::types::Digest::generate(&mut rng));
4794 let output_ref =
4795 sui::types::ObjectReference::new(object_id, 2, sui::types::Digest::generate(&mut rng));
4796 let mut input = object_with_bcs(input_ref, owner, &TestValue { value: 3 });
4797 input.set_previous_transaction(earlier_transaction);
4798 let mut output = object_with_bcs(output_ref.clone(), owner, &TestValue { value: 5 });
4799 output.set_previous_transaction(transaction);
4800
4801 let mut ledger_service_mock = sui_mocks::grpc::MockLedgerService::new();
4802 ledger_service_mock
4803 .expect_get_transaction()
4804 .withf(move |request| {
4805 request.get_ref().digest_opt() == Some(transaction.to_string().as_str())
4806 && request.get_ref().read_mask.as_ref().is_some_and(|mask| {
4807 mask.paths
4808 .iter()
4809 .any(|path| path == "objects.objects.previous_transaction")
4810 })
4811 })
4812 .times(1)
4813 .returning(move |_| {
4814 let mut objects = sui::grpc::ObjectSet::default();
4815 objects.set_objects(vec![input.clone(), output.clone()]);
4816 let mut executed = sui::grpc::ExecutedTransaction::default();
4817 executed.set_digest(transaction);
4818 executed.set_objects(objects);
4819 let mut response = sui::grpc::GetTransactionResponse::default();
4820 response.set_transaction(executed);
4821 Ok(tonic::Response::new(response))
4822 });
4823 let rpc_url = sui_mocks::grpc::mock_server(sui_mocks::grpc::ServerMocks {
4824 ledger_service_mock: Some(ledger_service_mock),
4825 ..Default::default()
4826 });
4827 let client = sui::grpc::client(rpc_url).expect("mock client");
4828 let crawler = Crawler::new(Arc::new(client));
4829
4830 let observed = crawler
4831 .get_transaction_output_object::<TestValue>(transaction, object_id)
4832 .await
4833 .expect("transaction output object loads");
4834
4835 assert_eq!(observed.object_ref(), output_ref);
4836 assert_eq!(observed.owner, owner);
4837 assert_eq!(observed.data, TestValue { value: 5 });
4838 }
4839
4840 #[tokio::test]
4841 async fn transaction_shared_initial_version_requires_exact_type_and_owner() {
4842 let mut rng = rand::thread_rng();
4843 let transaction = sui::types::Digest::generate(&mut rng);
4844 let earlier_transaction = sui::types::Digest::generate(&mut rng);
4845 let object_id = sui::types::Address::generate(&mut rng);
4846 let object_type = test_value_tag();
4847 let input_ref =
4848 sui::types::ObjectReference::new(object_id, 9, sui::types::Digest::generate(&mut rng));
4849 let output_ref =
4850 sui::types::ObjectReference::new(object_id, 10, sui::types::Digest::generate(&mut rng));
4851 let owner = sui::types::Owner::Shared(7);
4852 let mut input =
4853 typed_object_with_bcs(input_ref, owner, &object_type, &TestValue { value: 3 });
4854 input.set_previous_transaction(earlier_transaction);
4855 let mut output =
4856 typed_object_with_bcs(output_ref, owner, &object_type, &TestValue { value: 5 });
4857 output.set_previous_transaction(transaction);
4858
4859 let mut executed = sui::grpc::ExecutedTransaction::default();
4860 executed.set_digest(transaction);
4861 executed.set_objects(
4862 sui::grpc::ObjectSet::default().with_objects(vec![input.clone(), output.clone()]),
4863 );
4864 let crawler = Crawler::new(Arc::new(
4865 sui::grpc::client(sui_mocks::grpc::mock_server(Default::default()))
4866 .expect("mock client builds"),
4867 ));
4868
4869 assert_eq!(
4870 crawler
4871 .transaction_shared_initial_version(
4872 &executed,
4873 transaction,
4874 object_id,
4875 &object_type,
4876 )
4877 .expect("causal shared input validates"),
4878 7,
4879 );
4880
4881 output.set_owner(sui::grpc::Owner::from(sui::types::Owner::Address(
4882 sui::types::Address::generate(&mut rng),
4883 )));
4884 executed
4885 .set_objects(sui::grpc::ObjectSet::default().with_objects(vec![input.clone(), output]));
4886 assert!(crawler
4887 .transaction_shared_initial_version(&executed, transaction, object_id, &object_type,)
4888 .expect_err("address-owned output must be rejected")
4889 .to_string()
4890 .contains("is not shared"));
4891
4892 let wrong_type = sui::types::StructTag::gas_coin();
4893 assert!(crawler
4894 .transaction_shared_initial_version(&executed, transaction, object_id, &wrong_type,)
4895 .expect_err("wrong expected type must be rejected")
4896 .to_string()
4897 .contains("expected"));
4898 }
4899
4900 #[tokio::test]
4901 async fn transaction_dynamic_field_outputs_and_causal_values_select_root_output() {
4902 let mut rng = rand::thread_rng();
4903 let transaction = sui::types::Digest::generate(&mut rng);
4904 let earlier_transaction = sui::types::Digest::generate(&mut rng);
4905 let parent_id = sui::types::Address::generate(&mut rng);
4906 let wrong_parent = sui::types::Address::generate(&mut rng);
4907 let key = TestKey {
4908 name: "inner".to_owned(),
4909 };
4910 let key_type = sui::types::TypeTag::Struct(Box::new(sui::types::StructTag::new(
4911 sui::types::Address::from_static("0x1"),
4912 sui::types::Identifier::from_static("state"),
4913 sui::types::Identifier::from_static("Inner"),
4914 vec![],
4915 )));
4916 let value_type = sui::types::TypeTag::Struct(Box::new(test_value_tag()));
4917 let field_type = sui::types::StructTag::new(
4918 sui::types::Address::from_static("0x2"),
4919 sui::types::Identifier::from_static("dynamic_field"),
4920 sui::types::Identifier::from_static("Field"),
4921 vec![key_type.clone(), value_type.clone()],
4922 );
4923 let field_id = derive_dynamic_field_id(parent_id, &key, &key_type).unwrap();
4924 let input_ref =
4925 sui::types::ObjectReference::new(field_id, 1, sui::types::Digest::generate(&mut rng));
4926 let output_ref =
4927 sui::types::ObjectReference::new(field_id, 2, sui::types::Digest::generate(&mut rng));
4928 let mut input = typed_object_with_bcs(
4929 input_ref,
4930 sui::types::Owner::Object(parent_id),
4931 &field_type,
4932 &DynamicFieldValue {
4933 id: field_id,
4934 name: key.clone(),
4935 value: TestValue { value: 3 },
4936 },
4937 );
4938 input.set_previous_transaction(earlier_transaction);
4939 let inherited_input = input.clone();
4940 let mut output = typed_object_with_bcs(
4941 output_ref.clone(),
4942 sui::types::Owner::Object(parent_id),
4943 &field_type,
4944 &DynamicFieldValue {
4945 id: field_id,
4946 name: key.clone(),
4947 value: TestValue { value: 5 },
4948 },
4949 );
4950 output.set_previous_transaction(transaction);
4951
4952 let mut executed = sui::grpc::ExecutedTransaction::default();
4953 executed.set_digest(transaction);
4954 let mut objects = sui::grpc::ObjectSet::default();
4955 objects.set_objects(vec![input, output.clone()]);
4956 executed.set_objects(objects);
4957 let rpc_url = sui_mocks::grpc::mock_server(Default::default());
4958 let crawler = Crawler::new(Arc::new(
4959 sui::grpc::client(rpc_url).expect("mock client builds"),
4960 ));
4961
4962 let observed = crawler
4963 .transaction_dynamic_field_outputs::<TestKey, TestValue>(
4964 &executed,
4965 transaction,
4966 &key,
4967 &key_type,
4968 &value_type,
4969 )
4970 .expect("exact output decodes");
4971 assert_eq!(observed.len(), 1);
4972 assert_eq!(observed[0].object_id, parent_id);
4973 assert_eq!(observed[0].field_ref, output_ref);
4974 assert_eq!(observed[0].data, TestValue { value: 5 });
4975
4976 let causal = crawler
4977 .causal_dynamic_field_values::<TestKey, TestValue>(
4978 &executed,
4979 transaction,
4980 &key,
4981 &key_type,
4982 &value_type,
4983 )
4984 .expect("the root output is the visible causal value");
4985 assert_eq!(causal.len(), 1);
4986 assert_eq!(causal[0].object_id, parent_id);
4987 assert_eq!(causal[0].field_ref, output_ref);
4988 assert_eq!(causal[0].data, TestValue { value: 5 });
4989
4990 let mut inherited_output = output.clone();
4991 inherited_output.set_previous_transaction(earlier_transaction);
4992 executed.set_objects(
4993 sui::grpc::ObjectSet::default().with_objects(vec![inherited_input, inherited_output]),
4994 );
4995 let error = crawler
4996 .causal_dynamic_field_values::<TestKey, TestValue>(
4997 &executed,
4998 transaction,
4999 &key,
5000 &key_type,
5001 &value_type,
5002 )
5003 .expect_err("two inherited versions have no unique visible value");
5004 assert!(error.to_string().contains("ambiguous versions"));
5005
5006 output.set_owner(sui::grpc::Owner::from(sui::types::Owner::Object(
5007 wrong_parent,
5008 )));
5009 let mut objects = sui::grpc::ObjectSet::default();
5010 objects.set_objects(vec![output]);
5011 executed.set_objects(objects);
5012 let error = crawler
5013 .transaction_dynamic_field_outputs::<TestKey, TestValue>(
5014 &executed,
5015 transaction,
5016 &key,
5017 &key_type,
5018 &value_type,
5019 )
5020 .expect_err("wrong parent cannot validate the deterministic field ID");
5021 assert!(error.to_string().contains("does not derive from parent"));
5022 }
5023
5024 #[tokio::test]
5025 async fn causal_dynamic_field_values_include_inherited_state() {
5026 let mut rng = rand::thread_rng();
5027 let transaction = sui::types::Digest::generate(&mut rng);
5028 let ancestor = sui::types::Digest::generate(&mut rng);
5029 let parents = [
5030 sui::types::Address::generate(&mut rng),
5031 sui::types::Address::generate(&mut rng),
5032 ];
5033 let key = TestKey {
5034 name: "inner".to_owned(),
5035 };
5036 let key_type = sui::types::TypeTag::Struct(Box::new(sui::types::StructTag::new(
5037 sui::types::Address::from_static("0x1"),
5038 sui::types::Identifier::from_static("state"),
5039 sui::types::Identifier::from_static("Inner"),
5040 vec![],
5041 )));
5042 let value_type = sui::types::TypeTag::Struct(Box::new(test_value_tag()));
5043 let field_type = sui::types::StructTag::new(
5044 sui::types::Address::from_static("0x2"),
5045 sui::types::Identifier::from_static("dynamic_field"),
5046 sui::types::Identifier::from_static("Field"),
5047 vec![key_type.clone(), value_type.clone()],
5048 );
5049 let objects = parents
5050 .iter()
5051 .copied()
5052 .zip([ancestor, transaction])
5053 .zip([3, 5])
5054 .map(|((parent, previous), value)| {
5055 let field_id = derive_dynamic_field_id(parent, &key, &key_type).unwrap();
5056 let object_ref = sui::types::ObjectReference::new(
5057 field_id,
5058 1,
5059 sui::types::Digest::generate(&mut rng),
5060 );
5061 let mut object = typed_object_with_bcs(
5062 object_ref,
5063 sui::types::Owner::Object(parent),
5064 &field_type,
5065 &DynamicFieldValue {
5066 id: field_id,
5067 name: key.clone(),
5068 value: TestValue { value },
5069 },
5070 );
5071 object.set_previous_transaction(previous);
5072 object
5073 })
5074 .collect::<Vec<_>>();
5075 let mut executed = sui::grpc::ExecutedTransaction::default();
5076 executed.set_digest(transaction);
5077 executed.set_objects(sui::grpc::ObjectSet::default().with_objects(objects));
5078 let rpc_url = sui_mocks::grpc::mock_server(Default::default());
5079 let crawler = Crawler::new(Arc::new(
5080 sui::grpc::client(rpc_url).expect("mock client builds"),
5081 ));
5082
5083 let values = crawler
5084 .causal_dynamic_field_values::<TestKey, TestValue>(
5085 &executed,
5086 transaction,
5087 &key,
5088 &key_type,
5089 &value_type,
5090 )
5091 .expect("complete causal view decodes");
5092 assert_eq!(
5093 values
5094 .iter()
5095 .map(|value| (value.object_id, value.data.value))
5096 .collect::<Vec<_>>(),
5097 vec![(parents[0], 3), (parents[1], 5)]
5098 );
5099 }
5100}