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 {
165 ObjectImpl(&self.super_.super_).status().await
166 }
167
168 pub(crate) async fn digest(&self) -> Option<String> {
171 ObjectImpl(&self.super_.super_).digest().await
172 }
173
174 pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
176 ObjectImpl(&self.super_.super_).owner(ctx).await
177 }
178
179 pub(crate) async fn previous_transaction_block(
181 &self,
182 ctx: &Context<'_>,
183 ) -> Result<Option<TransactionBlock>> {
184 ObjectImpl(&self.super_.super_)
185 .previous_transaction_block(ctx)
186 .await
187 }
188
189 pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
193 ObjectImpl(&self.super_.super_).storage_rebate().await
194 }
195
196 #[graphql(
222 complexity = "first.or(last).unwrap_or(DEFAULT_PAGE_SIZE as u64) as usize * child_complexity"
223 )]
224 pub(crate) async fn received_transaction_blocks(
225 &self,
226 ctx: &Context<'_>,
227 first: Option<u64>,
228 after: Option<transaction_block::Cursor>,
229 last: Option<u64>,
230 before: Option<transaction_block::Cursor>,
231 filter: Option<TransactionBlockFilter>,
232 scan_limit: Option<u64>,
233 ) -> Result<ScanConnection<String, TransactionBlock>> {
234 ObjectImpl(&self.super_.super_)
235 .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
236 .await
237 }
238
239 pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
241 ObjectImpl(&self.super_.super_).bcs().await
242 }
243
244 pub(crate) async fn contents(&self) -> Option<MoveValue> {
248 MoveObjectImpl(&self.super_).contents().await
249 }
250
251 pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
255 ObjectImpl(&self.super_.super_).display(ctx).await
256 }
257
258 pub(crate) async fn dynamic_field(
265 &self,
266 ctx: &Context<'_>,
267 name: DynamicFieldName,
268 ) -> Result<Option<DynamicField>> {
269 OwnerImpl::from(&self.super_.super_)
270 .dynamic_field(ctx, name, Some(self.super_.root_version()))
271 .await
272 }
273
274 pub(crate) async fn dynamic_object_field(
283 &self,
284 ctx: &Context<'_>,
285 name: DynamicFieldName,
286 ) -> Result<Option<DynamicField>> {
287 OwnerImpl::from(&self.super_.super_)
288 .dynamic_object_field(ctx, name, Some(self.super_.root_version()))
289 .await
290 }
291
292 pub(crate) async fn dynamic_fields(
297 &self,
298 ctx: &Context<'_>,
299 first: Option<u64>,
300 after: Option<object::Cursor>,
301 last: Option<u64>,
302 before: Option<object::Cursor>,
303 ) -> Result<Connection<String, DynamicField>> {
304 OwnerImpl::from(&self.super_.super_)
305 .dynamic_fields(
306 ctx,
307 first,
308 after,
309 last,
310 before,
311 Some(self.super_.root_version()),
312 )
313 .await
314 }
315
316 async fn decimals(&self) -> Option<u8> {
318 Some(self.native.decimals)
319 }
320
321 async fn name(&self) -> Option<&str> {
323 Some(&self.native.name)
324 }
325
326 async fn symbol(&self) -> Option<&str> {
328 Some(&self.native.symbol)
329 }
330
331 async fn description(&self) -> Option<&str> {
333 Some(&self.native.description)
334 }
335
336 async fn icon_url(&self) -> Option<&str> {
337 self.native.icon_url.as_deref()
338 }
339
340 async fn supply(&self, ctx: &Context<'_>) -> Result<Option<BigInt>> {
342 let type_params = self.super_.native.struct_tag().type_params();
343 let Some(coin_type) = type_params.last().cloned() else {
344 return Ok(None);
345 };
346
347 let supply = CoinMetadata::query_total_supply(
348 ctx,
349 coin_type,
350 self.super_.super_.checkpoint_viewed_at,
351 )
352 .await
353 .extend()?;
354
355 Ok(supply.map(BigInt::from))
356 }
357}
358
359impl CoinMetadata {
360 pub(crate) async fn query(
363 db: &Db,
364 coin_type: TypeTag,
365 checkpoint_viewed_at: u64,
366 ) -> Result<Option<CoinMetadata>, Error> {
367 let TypeTag::Struct(coin_struct) = coin_type else {
368 return Ok(None);
371 };
372
373 let metadata_type = StructTag::new_coin_metadata(*coin_struct);
374 let Some(object) = Object::query_singleton(db, metadata_type, checkpoint_viewed_at).await?
375 else {
376 return Ok(None);
377 };
378
379 let move_object = MoveObject::try_from(&object).map_err(|_| {
380 Error::Internal(format!(
381 "Expected {} to be CoinMetadata, but it is not an object.",
382 object.address,
383 ))
384 })?;
385
386 let coin_metadata = CoinMetadata::try_from(&move_object).map_err(|_| {
387 Error::Internal(format!(
388 "Expected {} to be CoinMetadata, but it is not.",
389 object.address,
390 ))
391 })?;
392
393 Ok(Some(coin_metadata))
394 }
395
396 pub(crate) async fn query_total_supply(
397 ctx: &Context<'_>,
398 coin_type: TypeTag,
399 checkpoint_viewed_at: u64,
400 ) -> Result<Option<u64>, Error> {
401 let TypeTag::Struct(coin_struct) = coin_type else {
402 return Ok(None);
405 };
406
407 Ok(Some(if coin_struct.is_gas() {
408 let pg_manager = ctx.data_unchecked::<PgManager>();
409
410 let state = pg_manager.fetch_iota_system_state(None).await?;
411
412 state.iota_total_supply()
413 } else {
414 let cap_type = StructTag::new_treasury_cap(*coin_struct);
415
416 let db = ctx.data_unchecked();
417
418 let Some(object) = Object::query_singleton(db, cap_type, checkpoint_viewed_at).await?
419 else {
420 return Ok(None);
421 };
422
423 let Some(native) = object.native_impl() else {
424 return Ok(None);
425 };
426
427 let treasury_cap = TreasuryCap::try_from(native.clone()).map_err(|e| {
428 Error::Internal(format!(
429 "Error while deserializing treasury cap {}: {e}",
430 object.address,
431 ))
432 })?;
433
434 treasury_cap.total_supply.value
435 }))
436 }
437}
438
439impl TryFrom<&MoveObject> for CoinMetadata {
440 type Error = CoinMetadataDowncastError;
441
442 fn try_from(move_object: &MoveObject) -> Result<Self, Self::Error> {
443 if !move_object.native.struct_tag().is_coin_metadata() {
444 return Err(CoinMetadataDowncastError::NotCoinMetadata);
445 }
446
447 Ok(Self {
448 super_: move_object.clone(),
449 native: bcs::from_bytes(move_object.native.contents())
450 .map_err(CoinMetadataDowncastError::Bcs)?,
451 })
452 }
453}