1use async_graphql::{connection::Connection, *};
6use iota_names::config::IotaNamesConfig;
7use iota_sdk_types::ObjectData;
8use iota_types::object::MoveObject as NativeMoveObject;
9
10use crate::{
11 config::DEFAULT_PAGE_SIZE,
12 connection::ScanConnection,
13 data::Db,
14 error::Error,
15 types::{
16 balance::{self, Balance},
17 base64::Base64,
18 big_int::BigInt,
19 coin::{Coin, CoinDowncastError},
20 coin_metadata::{CoinMetadata, CoinMetadataDowncastError},
21 cursor::Page,
22 display::DisplayEntry,
23 dynamic_field::{DynamicField, DynamicFieldName},
24 iota_address::IotaAddress,
25 iota_names_registration::{NameFormat, NameRegistration, NameRegistrationDowncastError},
26 move_type::MoveType,
27 move_value::MoveValue,
28 object::{self, Object, ObjectFilter, ObjectImpl, ObjectLookup, ObjectOwner, ObjectStatus},
29 owner::OwnerImpl,
30 stake::{StakedIota, StakedIotaDowncastError},
31 transaction_block::{self, TransactionBlock, TransactionBlockFilter},
32 type_filter::ExactTypeFilter,
33 uint53::UInt53,
34 },
35};
36
37#[derive(Clone)]
38pub(crate) struct MoveObject {
39 pub super_: Object,
41
42 pub native: NativeMoveObject,
45}
46
47pub(crate) struct MoveObjectImpl<'o>(pub &'o MoveObject);
49
50pub(crate) enum MoveObjectDowncastError {
51 NotAMoveObject,
52}
53
54#[expect(clippy::duplicated_attributes)]
57#[derive(Interface)]
58#[graphql(
59 name = "IMoveObject",
60 field(
61 name = "contents",
62 ty = "Option<MoveValue>",
63 desc = "Displays the contents of the Move object in a JSON string and through GraphQL \
64 types. Also provides the flat representation of the type signature, and the BCS of \
65 the corresponding data."
66 ),
67 field(
68 name = "display",
69 ty = "Option<Vec<DisplayEntry>>",
70 desc = "The set of named templates defined on-chain for the type of this object, to be \
71 handled off-chain. The server substitutes data from the object into these \
72 templates to generate a display string per template."
73 ),
74 field(
75 name = "dynamic_field",
76 arg(name = "name", ty = "DynamicFieldName"),
77 ty = "Option<DynamicField>",
78 desc = "Access a dynamic field on an object using its name. Names are arbitrary Move \
79 values whose type have `copy`, `drop`, and `store`, and are specified using their \
80 type, and their BCS contents, Base64 encoded.\n\n\
81 Dynamic fields on wrapped objects can be accessed by using the same API under the \
82 Ownertype."
83 ),
84 field(
85 name = "dynamic_object_field",
86 arg(name = "name", ty = "DynamicFieldName"),
87 ty = "Option<DynamicField>",
88 desc = "Access a dynamic object field on an object using its name. Names are arbitrary \
89 Move values whose type have `copy`, `drop`, and `store`, and are specified using \
90 their type, and their BCS contents, Base64 encoded. The value of a dynamic object \
91 field can also be accessed off-chain directly via its address (e.g. using \
92 `Query.object`).\n\n\
93 Dynamic fields on wrapped objects can be accessed by using the same API under the \
94 Owner type."
95 ),
96 field(
97 name = "dynamic_fields",
98 arg(name = "first", ty = "Option<u64>"),
99 arg(name = "after", ty = "Option<object::Cursor>"),
100 arg(name = "last", ty = "Option<u64>"),
101 arg(name = "before", ty = "Option<object::Cursor>"),
102 ty = "Connection<String, DynamicField>",
103 desc = "The dynamic fields and dynamic object fields on an object.\n\n\
104 Dynamic fields on wrapped objects can be accessed by using the same API under the \
105 Owner type."
106 )
107)]
108pub(crate) enum IMoveObject {
109 MoveObject(MoveObject),
110 Coin(Coin),
111 CoinMetadata(CoinMetadata),
112 StakedIota(StakedIota),
113 NameRegistration(NameRegistration),
114}
115
116#[Object]
120impl MoveObject {
121 pub(crate) async fn address(&self) -> IotaAddress {
122 OwnerImpl::from(&self.super_).address().await
123 }
124
125 pub(crate) async fn objects(
127 &self,
128 ctx: &Context<'_>,
129 first: Option<u64>,
130 after: Option<object::Cursor>,
131 last: Option<u64>,
132 before: Option<object::Cursor>,
133 filter: Option<ObjectFilter>,
134 ) -> Result<Connection<String, MoveObject>> {
135 OwnerImpl::from(&self.super_)
136 .objects(ctx, first, after, last, before, filter)
137 .await
138 }
139
140 pub(crate) async fn balance(
143 &self,
144 ctx: &Context<'_>,
145 type_: Option<ExactTypeFilter>,
146 ) -> Result<Option<Balance>> {
147 OwnerImpl::from(&self.super_).balance(ctx, type_).await
148 }
149
150 pub(crate) async fn balances(
152 &self,
153 ctx: &Context<'_>,
154 first: Option<u64>,
155 after: Option<balance::Cursor>,
156 last: Option<u64>,
157 before: Option<balance::Cursor>,
158 ) -> Result<Connection<String, Balance>> {
159 OwnerImpl::from(&self.super_)
160 .balances(ctx, first, after, last, before)
161 .await
162 }
163
164 pub(crate) async fn coins(
169 &self,
170 ctx: &Context<'_>,
171 first: Option<u64>,
172 after: Option<object::Cursor>,
173 last: Option<u64>,
174 before: Option<object::Cursor>,
175 type_: Option<ExactTypeFilter>,
176 ) -> Result<Connection<String, Coin>> {
177 OwnerImpl::from(&self.super_)
178 .coins(ctx, first, after, last, before, type_)
179 .await
180 }
181
182 pub(crate) async fn staked_iotas(
184 &self,
185 ctx: &Context<'_>,
186 first: Option<u64>,
187 after: Option<object::Cursor>,
188 last: Option<u64>,
189 before: Option<object::Cursor>,
190 ) -> Result<Connection<String, StakedIota>> {
191 OwnerImpl::from(&self.super_)
192 .staked_iotas(ctx, first, after, last, before)
193 .await
194 }
195
196 pub(crate) async fn iota_names_default_name(
199 &self,
200 ctx: &Context<'_>,
201 format: Option<NameFormat>,
202 ) -> Result<Option<String>> {
203 OwnerImpl::from(&self.super_)
204 .iota_names_default_name(ctx, format)
205 .await
206 }
207
208 pub(crate) async fn iota_names_registrations(
211 &self,
212 ctx: &Context<'_>,
213 first: Option<u64>,
214 after: Option<object::Cursor>,
215 last: Option<u64>,
216 before: Option<object::Cursor>,
217 ) -> Result<Connection<String, NameRegistration>> {
218 OwnerImpl::from(&self.super_)
219 .iota_names_registrations(ctx, first, after, last, before)
220 .await
221 }
222
223 pub(crate) async fn version(&self) -> UInt53 {
224 ObjectImpl(&self.super_).version().await
225 }
226
227 pub(crate) async fn status(&self) -> ObjectStatus {
234 ObjectImpl(&self.super_).status().await
235 }
236
237 pub(crate) async fn digest(&self) -> Option<String> {
240 ObjectImpl(&self.super_).digest().await
241 }
242
243 pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
245 ObjectImpl(&self.super_).owner(ctx).await
246 }
247
248 pub(crate) async fn previous_transaction_block(
250 &self,
251 ctx: &Context<'_>,
252 ) -> Result<Option<TransactionBlock>> {
253 ObjectImpl(&self.super_)
254 .previous_transaction_block(ctx)
255 .await
256 }
257
258 pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
262 ObjectImpl(&self.super_).storage_rebate().await
263 }
264
265 #[graphql(
291 complexity = "first.or(last).unwrap_or(DEFAULT_PAGE_SIZE as u64) as usize * child_complexity"
292 )]
293 pub(crate) async fn received_transaction_blocks(
294 &self,
295 ctx: &Context<'_>,
296 first: Option<u64>,
297 after: Option<transaction_block::Cursor>,
298 last: Option<u64>,
299 before: Option<transaction_block::Cursor>,
300 filter: Option<TransactionBlockFilter>,
301 scan_limit: Option<u64>,
302 ) -> Result<ScanConnection<String, TransactionBlock>> {
303 ObjectImpl(&self.super_)
304 .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
305 .await
306 }
307
308 pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
310 ObjectImpl(&self.super_).bcs().await
311 }
312
313 pub(crate) async fn contents(&self) -> Option<MoveValue> {
317 MoveObjectImpl(self).contents().await
318 }
319
320 pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
324 ObjectImpl(&self.super_).display(ctx).await
325 }
326
327 pub(crate) async fn dynamic_field(
334 &self,
335 ctx: &Context<'_>,
336 name: DynamicFieldName,
337 ) -> Result<Option<DynamicField>> {
338 OwnerImpl::from(&self.super_)
339 .dynamic_field(ctx, name, Some(self.root_version()))
340 .await
341 }
342
343 pub(crate) async fn dynamic_object_field(
352 &self,
353 ctx: &Context<'_>,
354 name: DynamicFieldName,
355 ) -> Result<Option<DynamicField>> {
356 OwnerImpl::from(&self.super_)
357 .dynamic_object_field(ctx, name, Some(self.root_version()))
358 .await
359 }
360
361 pub(crate) async fn dynamic_fields(
366 &self,
367 ctx: &Context<'_>,
368 first: Option<u64>,
369 after: Option<object::Cursor>,
370 last: Option<u64>,
371 before: Option<object::Cursor>,
372 ) -> Result<Connection<String, DynamicField>> {
373 OwnerImpl::from(&self.super_)
374 .dynamic_fields(ctx, first, after, last, before, Some(self.root_version()))
375 .await
376 }
377
378 async fn as_coin(&self) -> Result<Option<Coin>> {
380 match Coin::try_from(self) {
381 Ok(coin) => Ok(Some(coin)),
382 Err(CoinDowncastError::NotACoin) => Ok(None),
383 Err(CoinDowncastError::Bcs(e)) => {
384 Err(Error::Internal(format!("Failed to deserialize Coin: {e}"))).extend()
385 }
386 }
387 }
388
389 async fn as_staked_iota(&self) -> Result<Option<StakedIota>> {
392 match StakedIota::try_from(self) {
393 Ok(coin) => Ok(Some(coin)),
394 Err(StakedIotaDowncastError::NotAStakedIota) => Ok(None),
395 Err(StakedIotaDowncastError::Bcs(e)) => Err(Error::Internal(format!(
396 "Failed to deserialize StakedIota: {e}"
397 )))
398 .extend(),
399 }
400 }
401
402 async fn as_coin_metadata(&self) -> Result<Option<CoinMetadata>> {
404 match CoinMetadata::try_from(self) {
405 Ok(metadata) => Ok(Some(metadata)),
406 Err(CoinMetadataDowncastError::NotCoinMetadata) => Ok(None),
407 Err(CoinMetadataDowncastError::Bcs(e)) => Err(Error::Internal(format!(
408 "Failed to deserialize CoinMetadata: {e}"
409 )))
410 .extend(),
411 }
412 }
413
414 async fn as_iota_names_registration(
416 &self,
417 ctx: &Context<'_>,
418 ) -> Result<Option<NameRegistration>> {
419 let cfg: &IotaNamesConfig = ctx.data_unchecked();
420 let tag = NameRegistration::type_(cfg.package_address.into());
421
422 match NameRegistration::try_from(self, &tag) {
423 Ok(registration) => Ok(Some(registration)),
424 Err(NameRegistrationDowncastError::NotAnNameRegistration) => Ok(None),
425 Err(NameRegistrationDowncastError::Bcs(e)) => Err(Error::Internal(format!(
426 "Failed to deserialize
427 NameRegistration: {e}",
428 )))
429 .extend(),
430 }
431 }
432}
433
434impl MoveObjectImpl<'_> {
435 pub(crate) async fn contents(&self) -> Option<MoveValue> {
436 let type_ = self.0.native.type_tag();
437 Some(MoveValue::new(type_, self.0.native.contents().into()))
438 }
439 pub(crate) async fn has_public_transfer(&self, ctx: &Context<'_>) -> Result<bool> {
440 let type_: MoveType = self.0.native.struct_tag().clone().into();
441 let set = type_.abilities_impl(ctx.data_unchecked()).await.extend()?;
442 Ok(set.is_some_and(|s| s.has_key() && s.has_store()))
443 }
444}
445
446impl MoveObject {
447 pub(crate) async fn query(
448 ctx: &Context<'_>,
449 address: IotaAddress,
450 key: ObjectLookup,
451 ) -> Result<Option<Self>, Error> {
452 let Some(object) = Object::query(ctx, address, key).await? else {
453 return Ok(None);
454 };
455
456 match MoveObject::try_from(&object) {
457 Ok(object) => Ok(Some(object)),
458 Err(MoveObjectDowncastError::NotAMoveObject) => {
459 Err(Error::Internal(format!("{address} is not a Move object")))?
460 }
461 }
462 }
463
464 pub(crate) async fn paginate(
471 db: &Db,
472 page: Page<object::Cursor>,
473 filter: ObjectFilter,
474 checkpoint_viewed_at: u64,
475 ) -> Result<Connection<String, MoveObject>, Error> {
476 Object::paginate_subtype(db, page, filter, checkpoint_viewed_at, |object| {
477 let address = object.address;
478 MoveObject::try_from(&object).map_err(|_| {
479 Error::Internal(format!(
480 "Expected {address} to be a Move object, but it's not."
481 ))
482 })
483 })
484 .await
485 }
486
487 pub(crate) fn root_version(&self) -> u64 {
491 self.super_.root_version()
492 }
493}
494
495impl TryFrom<&Object> for MoveObject {
496 type Error = MoveObjectDowncastError;
497
498 fn try_from(object: &Object) -> Result<Self, Self::Error> {
499 let native = object.native_impl();
500
501 if let ObjectData::Struct(move_object) = &native.data {
502 Ok(Self {
503 super_: object.clone(),
504 native: move_object.clone(),
505 })
506 } else {
507 Err(MoveObjectDowncastError::NotAMoveObject)
508 }
509 }
510}