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