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