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