1use async_graphql::{connection::Connection, *};
6use iota_sdk_types::{StructTag, TypeTag};
7use iota_types::coin::{CoinMetadata as NativeCoinMetadata, TreasuryCap};
8
9use crate::{
10 config::DEFAULT_PAGE_SIZE,
11 connection::ScanConnection,
12 context_data::db_data_provider::PgManager,
13 data::Db,
14 error::Error,
15 types::{
16 balance::{self, Balance},
17 base64::Base64,
18 big_int::BigInt,
19 coin::Coin,
20 display::DisplayEntry,
21 dynamic_field::{DynamicField, DynamicFieldName},
22 iota_address::IotaAddress,
23 iota_names_registration::{NameFormat, NameRegistration},
24 move_object::{MoveObject, MoveObjectImpl},
25 move_value::MoveValue,
26 object::{self, Object, ObjectFilter, ObjectImpl, ObjectOwner, ObjectStatus},
27 owner::OwnerImpl,
28 stake::StakedIota,
29 transaction_block::{self, TransactionBlock, TransactionBlockFilter},
30 type_filter::ExactTypeFilter,
31 uint53::UInt53,
32 },
33};
34
35pub(crate) struct CoinMetadata {
36 pub super_: MoveObject,
37 pub native: NativeCoinMetadata,
38}
39
40pub(crate) enum CoinMetadataDowncastError {
41 NotCoinMetadata,
42 Bcs(bcs::Error),
43}
44
45#[Object]
47impl CoinMetadata {
48 pub(crate) async fn address(&self) -> IotaAddress {
49 OwnerImpl::from(&self.super_.super_).address().await
50 }
51
52 pub(crate) async fn objects(
54 &self,
55 ctx: &Context<'_>,
56 first: Option<u64>,
57 after: Option<object::Cursor>,
58 last: Option<u64>,
59 before: Option<object::Cursor>,
60 filter: Option<ObjectFilter>,
61 ) -> Result<Connection<String, MoveObject>> {
62 OwnerImpl::from(&self.super_.super_)
63 .objects(ctx, first, after, last, before, filter)
64 .await
65 }
66
67 pub(crate) async fn balance(
70 &self,
71 ctx: &Context<'_>,
72 type_: Option<ExactTypeFilter>,
73 ) -> Result<Option<Balance>> {
74 OwnerImpl::from(&self.super_.super_)
75 .balance(ctx, type_)
76 .await
77 }
78
79 pub(crate) async fn balances(
81 &self,
82 ctx: &Context<'_>,
83 first: Option<u64>,
84 after: Option<balance::Cursor>,
85 last: Option<u64>,
86 before: Option<balance::Cursor>,
87 ) -> Result<Connection<String, Balance>> {
88 OwnerImpl::from(&self.super_.super_)
89 .balances(ctx, first, after, last, before)
90 .await
91 }
92
93 pub(crate) async fn coins(
98 &self,
99 ctx: &Context<'_>,
100 first: Option<u64>,
101 after: Option<object::Cursor>,
102 last: Option<u64>,
103 before: Option<object::Cursor>,
104 type_: Option<ExactTypeFilter>,
105 ) -> Result<Connection<String, Coin>> {
106 OwnerImpl::from(&self.super_.super_)
107 .coins(ctx, first, after, last, before, type_)
108 .await
109 }
110
111 pub(crate) async fn staked_iotas(
113 &self,
114 ctx: &Context<'_>,
115 first: Option<u64>,
116 after: Option<object::Cursor>,
117 last: Option<u64>,
118 before: Option<object::Cursor>,
119 ) -> Result<Connection<String, StakedIota>> {
120 OwnerImpl::from(&self.super_.super_)
121 .staked_iotas(ctx, first, after, last, before)
122 .await
123 }
124
125 pub(crate) async fn iota_names_default_name(
128 &self,
129 ctx: &Context<'_>,
130 format: Option<NameFormat>,
131 ) -> Result<Option<String>> {
132 OwnerImpl::from(&self.super_.super_)
133 .iota_names_default_name(ctx, format)
134 .await
135 }
136
137 pub(crate) async fn iota_names_registrations(
140 &self,
141 ctx: &Context<'_>,
142 first: Option<u64>,
143 after: Option<object::Cursor>,
144 last: Option<u64>,
145 before: Option<object::Cursor>,
146 ) -> Result<Connection<String, NameRegistration>> {
147 OwnerImpl::from(&self.super_.super_)
148 .iota_names_registrations(ctx, first, after, last, before)
149 .await
150 }
151
152 pub(crate) async fn version(&self) -> UInt53 {
153 ObjectImpl(&self.super_.super_).version().await
154 }
155
156 pub(crate) async fn status(&self) -> ObjectStatus {
163 ObjectImpl(&self.super_.super_).status().await
164 }
165
166 pub(crate) async fn digest(&self) -> Option<String> {
169 ObjectImpl(&self.super_.super_).digest().await
170 }
171
172 pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
174 ObjectImpl(&self.super_.super_).owner(ctx).await
175 }
176
177 pub(crate) async fn previous_transaction_block(
179 &self,
180 ctx: &Context<'_>,
181 ) -> Result<Option<TransactionBlock>> {
182 ObjectImpl(&self.super_.super_)
183 .previous_transaction_block(ctx)
184 .await
185 }
186
187 pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
191 ObjectImpl(&self.super_.super_).storage_rebate().await
192 }
193
194 #[graphql(
225 complexity = "first.or(last).unwrap_or(DEFAULT_PAGE_SIZE as u64) as usize * child_complexity"
226 )]
227 pub(crate) async fn received_transaction_blocks(
228 &self,
229 ctx: &Context<'_>,
230 first: Option<u64>,
231 after: Option<transaction_block::Cursor>,
232 last: Option<u64>,
233 before: Option<transaction_block::Cursor>,
234 filter: Option<TransactionBlockFilter>,
235 #[graphql(
236 deprecation = "`scanLimit` will be removed with v1.38, along with the support for combining complex filters."
237 )]
238 scan_limit: Option<u64>,
239 ) -> Result<ScanConnection<String, TransactionBlock>> {
240 ObjectImpl(&self.super_.super_)
241 .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
242 .await
243 }
244
245 pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
247 ObjectImpl(&self.super_.super_).bcs().await
248 }
249
250 pub(crate) async fn contents(&self) -> Option<MoveValue> {
254 MoveObjectImpl(&self.super_).contents().await
255 }
256
257 pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
261 ObjectImpl(&self.super_.super_).display(ctx).await
262 }
263
264 pub(crate) async fn dynamic_field(
271 &self,
272 ctx: &Context<'_>,
273 name: DynamicFieldName,
274 ) -> Result<Option<DynamicField>> {
275 OwnerImpl::from(&self.super_.super_)
276 .dynamic_field(ctx, name, Some(self.super_.root_version()))
277 .await
278 }
279
280 pub(crate) async fn dynamic_object_field(
289 &self,
290 ctx: &Context<'_>,
291 name: DynamicFieldName,
292 ) -> Result<Option<DynamicField>> {
293 OwnerImpl::from(&self.super_.super_)
294 .dynamic_object_field(ctx, name, Some(self.super_.root_version()))
295 .await
296 }
297
298 pub(crate) async fn dynamic_fields(
303 &self,
304 ctx: &Context<'_>,
305 first: Option<u64>,
306 after: Option<object::Cursor>,
307 last: Option<u64>,
308 before: Option<object::Cursor>,
309 ) -> Result<Connection<String, DynamicField>> {
310 OwnerImpl::from(&self.super_.super_)
311 .dynamic_fields(
312 ctx,
313 first,
314 after,
315 last,
316 before,
317 Some(self.super_.root_version()),
318 )
319 .await
320 }
321
322 async fn decimals(&self) -> Option<u8> {
324 Some(self.native.decimals)
325 }
326
327 async fn name(&self) -> Option<&str> {
329 Some(&self.native.name)
330 }
331
332 async fn symbol(&self) -> Option<&str> {
334 Some(&self.native.symbol)
335 }
336
337 async fn description(&self) -> Option<&str> {
339 Some(&self.native.description)
340 }
341
342 async fn icon_url(&self) -> Option<&str> {
343 self.native.icon_url.as_deref()
344 }
345
346 async fn supply(&self, ctx: &Context<'_>) -> Result<Option<BigInt>> {
348 let type_params = self.super_.native.struct_tag().type_params();
349 let Some(coin_type) = type_params.last().cloned() else {
350 return Ok(None);
351 };
352
353 let supply = CoinMetadata::query_total_supply(
354 ctx,
355 coin_type,
356 self.super_.super_.checkpoint_viewed_at,
357 )
358 .await
359 .extend()?;
360
361 Ok(supply.map(BigInt::from))
362 }
363}
364
365impl CoinMetadata {
366 pub(crate) async fn query(
369 db: &Db,
370 coin_type: TypeTag,
371 checkpoint_viewed_at: u64,
372 ) -> Result<Option<CoinMetadata>, Error> {
373 let TypeTag::Struct(coin_struct) = coin_type else {
374 return Ok(None);
377 };
378
379 let metadata_type = StructTag::new_coin_metadata(*coin_struct);
380 let Some(object) = Object::query_singleton(db, metadata_type, checkpoint_viewed_at).await?
381 else {
382 return Ok(None);
383 };
384
385 let move_object = MoveObject::try_from(&object).map_err(|_| {
386 Error::Internal(format!(
387 "Expected {} to be CoinMetadata, but it is not an object.",
388 object.address,
389 ))
390 })?;
391
392 let coin_metadata = CoinMetadata::try_from(&move_object).map_err(|_| {
393 Error::Internal(format!(
394 "Expected {} to be CoinMetadata, but it is not.",
395 object.address,
396 ))
397 })?;
398
399 Ok(Some(coin_metadata))
400 }
401
402 pub(crate) async fn query_total_supply(
403 ctx: &Context<'_>,
404 coin_type: TypeTag,
405 checkpoint_viewed_at: u64,
406 ) -> Result<Option<u64>, Error> {
407 let TypeTag::Struct(coin_struct) = coin_type else {
408 return Ok(None);
411 };
412
413 Ok(Some(if coin_struct.is_gas() {
414 let pg_manager = ctx.data_unchecked::<PgManager>();
415
416 let state = pg_manager.fetch_iota_system_state(None).await?;
417
418 state.iota_total_supply()
419 } else {
420 let cap_type = StructTag::new_treasury_cap(*coin_struct);
421
422 let db = ctx.data_unchecked();
423
424 let Some(object) = Object::query_singleton(db, cap_type, checkpoint_viewed_at).await?
425 else {
426 return Ok(None);
427 };
428
429 let native = object.native_impl();
430 let treasury_cap = TreasuryCap::try_from(native.clone()).map_err(|e| {
431 Error::Internal(format!(
432 "Error while deserializing treasury cap {}: {e}",
433 object.address,
434 ))
435 })?;
436
437 treasury_cap.total_supply.value
438 }))
439 }
440}
441
442impl TryFrom<&MoveObject> for CoinMetadata {
443 type Error = CoinMetadataDowncastError;
444
445 fn try_from(move_object: &MoveObject) -> Result<Self, Self::Error> {
446 if !move_object.native.struct_tag().is_coin_metadata() {
447 return Err(CoinMetadataDowncastError::NotCoinMetadata);
448 }
449
450 Ok(Self {
451 super_: move_object.clone(),
452 native: bcs::from_bytes(move_object.native.contents())
453 .map_err(CoinMetadataDowncastError::Bcs)?,
454 })
455 }
456}