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