iota_graphql_rpc/types/
coin_metadata.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use async_graphql::{connection::Connection, *};
6use iota_types::{
7    TypeTag,
8    coin::{CoinMetadata as NativeCoinMetadata, TreasuryCap},
9    gas_coin::GAS,
10};
11
12use crate::{
13    config::DEFAULT_PAGE_SIZE,
14    connection::ScanConnection,
15    context_data::db_data_provider::PgManager,
16    data::Db,
17    error::Error,
18    types::{
19        balance::{self, Balance},
20        base64::Base64,
21        big_int::BigInt,
22        coin::Coin,
23        display::DisplayEntry,
24        dynamic_field::{DynamicField, DynamicFieldName},
25        iota_address::IotaAddress,
26        iota_names_registration::{NameFormat, NameRegistration},
27        move_object::{MoveObject, MoveObjectImpl},
28        move_value::MoveValue,
29        object::{self, Object, ObjectFilter, ObjectImpl, ObjectOwner, ObjectStatus},
30        owner::OwnerImpl,
31        stake::StakedIota,
32        transaction_block::{self, TransactionBlock, TransactionBlockFilter},
33        type_filter::ExactTypeFilter,
34        uint53::UInt53,
35    },
36};
37
38pub(crate) struct CoinMetadata {
39    pub super_: MoveObject,
40    pub native: NativeCoinMetadata,
41}
42
43pub(crate) enum CoinMetadataDowncastError {
44    NotCoinMetadata,
45    Bcs(bcs::Error),
46}
47
48/// The metadata for a coin type.
49#[Object]
50impl CoinMetadata {
51    pub(crate) async fn address(&self) -> IotaAddress {
52        OwnerImpl::from(&self.super_.super_).address().await
53    }
54
55    /// Objects owned by this object, optionally `filter`-ed.
56    pub(crate) async fn objects(
57        &self,
58        ctx: &Context<'_>,
59        first: Option<u64>,
60        after: Option<object::Cursor>,
61        last: Option<u64>,
62        before: Option<object::Cursor>,
63        filter: Option<ObjectFilter>,
64    ) -> Result<Connection<String, MoveObject>> {
65        OwnerImpl::from(&self.super_.super_)
66            .objects(ctx, first, after, last, before, filter)
67            .await
68    }
69
70    /// Total balance of all coins with marker type owned by this object. If
71    /// type is not supplied, it defaults to `0x2::iota::IOTA`.
72    pub(crate) async fn balance(
73        &self,
74        ctx: &Context<'_>,
75        type_: Option<ExactTypeFilter>,
76    ) -> Result<Option<Balance>> {
77        OwnerImpl::from(&self.super_.super_)
78            .balance(ctx, type_)
79            .await
80    }
81
82    /// The balances of all coin types owned by this object.
83    pub(crate) async fn balances(
84        &self,
85        ctx: &Context<'_>,
86        first: Option<u64>,
87        after: Option<balance::Cursor>,
88        last: Option<u64>,
89        before: Option<balance::Cursor>,
90    ) -> Result<Connection<String, Balance>> {
91        OwnerImpl::from(&self.super_.super_)
92            .balances(ctx, first, after, last, before)
93            .await
94    }
95
96    /// The coin objects for this object.
97    ///
98    /// `type` is a filter on the coin's type parameter, defaulting to
99    /// `0x2::iota::IOTA`.
100    pub(crate) async fn coins(
101        &self,
102        ctx: &Context<'_>,
103        first: Option<u64>,
104        after: Option<object::Cursor>,
105        last: Option<u64>,
106        before: Option<object::Cursor>,
107        type_: Option<ExactTypeFilter>,
108    ) -> Result<Connection<String, Coin>> {
109        OwnerImpl::from(&self.super_.super_)
110            .coins(ctx, first, after, last, before, type_)
111            .await
112    }
113
114    /// The `0x3::staking_pool::StakedIota` objects owned by this object.
115    pub(crate) async fn staked_iotas(
116        &self,
117        ctx: &Context<'_>,
118        first: Option<u64>,
119        after: Option<object::Cursor>,
120        last: Option<u64>,
121        before: Option<object::Cursor>,
122    ) -> Result<Connection<String, StakedIota>> {
123        OwnerImpl::from(&self.super_.super_)
124            .staked_iotas(ctx, first, after, last, before)
125            .await
126    }
127
128    /// The name explicitly configured as the default name pointing to this
129    /// object.
130    pub(crate) async fn iota_names_default_name(
131        &self,
132        ctx: &Context<'_>,
133        format: Option<NameFormat>,
134    ) -> Result<Option<String>> {
135        OwnerImpl::from(&self.super_.super_)
136            .iota_names_default_name(ctx, format)
137            .await
138    }
139
140    /// The NameRegistration NFTs owned by this object. These grant the
141    /// owner the capability to manage the associated name.
142    pub(crate) async fn iota_names_registrations(
143        &self,
144        ctx: &Context<'_>,
145        first: Option<u64>,
146        after: Option<object::Cursor>,
147        last: Option<u64>,
148        before: Option<object::Cursor>,
149    ) -> Result<Connection<String, NameRegistration>> {
150        OwnerImpl::from(&self.super_.super_)
151            .iota_names_registrations(ctx, first, after, last, before)
152            .await
153    }
154
155    pub(crate) async fn version(&self) -> UInt53 {
156        ObjectImpl(&self.super_.super_).version().await
157    }
158
159    /// The current status of the object as read from the off-chain store. The
160    /// possible states are:
161    /// - NOT_INDEXED: The object is loaded from serialized data, such as the
162    ///   contents of a genesis or system package upgrade transaction.
163    /// - INDEXED: The object is retrieved from the off-chain index and
164    ///   represents the most recent or historical state of the object.
165    /// - WRAPPED_OR_DELETED: The object is deleted or wrapped and only partial
166    ///   information can be loaded.
167    pub(crate) async fn status(&self) -> ObjectStatus {
168        ObjectImpl(&self.super_.super_).status().await
169    }
170
171    /// 32-byte hash that identifies the object's contents, encoded as a Base58
172    /// string.
173    pub(crate) async fn digest(&self) -> Option<String> {
174        ObjectImpl(&self.super_.super_).digest().await
175    }
176
177    /// The owner type of this object: Immutable, Shared, Parent, Address
178    pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
179        ObjectImpl(&self.super_.super_).owner(ctx).await
180    }
181
182    /// The transaction block that created this version of the object.
183    pub(crate) async fn previous_transaction_block(
184        &self,
185        ctx: &Context<'_>,
186    ) -> Result<Option<TransactionBlock>> {
187        ObjectImpl(&self.super_.super_)
188            .previous_transaction_block(ctx)
189            .await
190    }
191
192    /// The amount of IOTA we would rebate if this object gets deleted or
193    /// mutated. This number is recalculated based on the present storage
194    /// gas price.
195    pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
196        ObjectImpl(&self.super_.super_).storage_rebate().await
197    }
198
199    /// The transaction blocks that sent objects to this object.
200    ///
201    /// `scanLimit` restricts the number of candidate transactions scanned when
202    /// gathering a page of results. It is required for queries that apply
203    /// more than two complex filters (on function, kind, sender, recipient,
204    /// input object, changed object, or ids), and can be at most
205    /// `serviceConfig.maxScanLimit`.
206    ///
207    /// When the scan limit is reached the page will be returned even if it has
208    /// fewer than `first` results when paginating forward (`last` when
209    /// paginating backwards). If there are more transactions to scan,
210    /// `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to
211    /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set
212    /// to the last transaction that was scanned as opposed to the last (or
213    /// first) transaction in the page.
214    ///
215    /// Requesting the next (or previous) page after this cursor will resume the
216    /// search, scanning the next `scanLimit` many transactions in the
217    /// direction of pagination, and so on until all transactions in the
218    /// scanning range have been visited.
219    ///
220    /// By default, the scanning range includes all transactions known to
221    /// GraphQL, but it can be restricted by the `after` and `before`
222    /// cursors, and the `beforeCheckpoint`, `afterCheckpoint` and
223    /// `atCheckpoint` filters.
224    #[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        scan_limit: Option<u64>,
236    ) -> Result<ScanConnection<String, TransactionBlock>> {
237        ObjectImpl(&self.super_.super_)
238            .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
239            .await
240    }
241
242    /// The Base64-encoded BCS serialization of the object's content.
243    pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
244        ObjectImpl(&self.super_.super_).bcs().await
245    }
246
247    /// Displays the contents of the Move object in a JSON string and through
248    /// GraphQL types. Also provides the flat representation of the type
249    /// signature, and the BCS of the corresponding data.
250    pub(crate) async fn contents(&self) -> Option<MoveValue> {
251        MoveObjectImpl(&self.super_).contents().await
252    }
253
254    /// The set of named templates defined on-chain for the type of this object,
255    /// to be handled off-chain. The server substitutes data from the object
256    /// into these templates to generate a display string per template.
257    pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
258        ObjectImpl(&self.super_.super_).display(ctx).await
259    }
260
261    /// Access a dynamic field on an object using its name. Names are arbitrary
262    /// Move values whose type have `copy`, `drop`, and `store`, and are
263    /// specified using their type, and their BCS contents, Base64 encoded.
264    ///
265    /// Dynamic fields on wrapped objects can be accessed by using the same API
266    /// under the Owner type.
267    pub(crate) async fn dynamic_field(
268        &self,
269        ctx: &Context<'_>,
270        name: DynamicFieldName,
271    ) -> Result<Option<DynamicField>> {
272        OwnerImpl::from(&self.super_.super_)
273            .dynamic_field(ctx, name, Some(self.super_.root_version()))
274            .await
275    }
276
277    /// Access a dynamic object field on an object using its name. Names are
278    /// arbitrary Move values whose type have `copy`, `drop`, and `store`,
279    /// and are specified using their type, and their BCS contents, Base64
280    /// encoded. The value of a dynamic object field can also be accessed
281    /// off-chain directly via its address (e.g. using `Query.object`).
282    ///
283    /// Dynamic fields on wrapped objects can be accessed by using the same API
284    /// under the Owner type.
285    pub(crate) async fn dynamic_object_field(
286        &self,
287        ctx: &Context<'_>,
288        name: DynamicFieldName,
289    ) -> Result<Option<DynamicField>> {
290        OwnerImpl::from(&self.super_.super_)
291            .dynamic_object_field(ctx, name, Some(self.super_.root_version()))
292            .await
293    }
294
295    /// The dynamic fields and dynamic object fields on an object.
296    ///
297    /// Dynamic fields on wrapped objects can be accessed by using the same API
298    /// under the Owner type.
299    pub(crate) async fn dynamic_fields(
300        &self,
301        ctx: &Context<'_>,
302        first: Option<u64>,
303        after: Option<object::Cursor>,
304        last: Option<u64>,
305        before: Option<object::Cursor>,
306    ) -> Result<Connection<String, DynamicField>> {
307        OwnerImpl::from(&self.super_.super_)
308            .dynamic_fields(
309                ctx,
310                first,
311                after,
312                last,
313                before,
314                Some(self.super_.root_version()),
315            )
316            .await
317    }
318
319    /// The number of decimal places used to represent the token.
320    async fn decimals(&self) -> Option<u8> {
321        Some(self.native.decimals)
322    }
323
324    /// Full, official name of the token.
325    async fn name(&self) -> Option<&str> {
326        Some(&self.native.name)
327    }
328
329    /// The token's identifying abbreviation.
330    async fn symbol(&self) -> Option<&str> {
331        Some(&self.native.symbol)
332    }
333
334    /// Optional description of the token, provided by the creator of the token.
335    async fn description(&self) -> Option<&str> {
336        Some(&self.native.description)
337    }
338
339    async fn icon_url(&self) -> Option<&str> {
340        self.native.icon_url.as_deref()
341    }
342
343    /// The overall quantity of tokens that will be issued.
344    async fn supply(&self, ctx: &Context<'_>) -> Result<Option<BigInt>> {
345        let mut type_params = self.super_.native.type_().type_params();
346        let Some(coin_type) = type_params.pop() else {
347            return Ok(None);
348        };
349
350        let supply = CoinMetadata::query_total_supply(
351            ctx,
352            coin_type,
353            self.super_.super_.checkpoint_viewed_at,
354        )
355        .await
356        .extend()?;
357
358        Ok(supply.map(BigInt::from))
359    }
360}
361
362impl CoinMetadata {
363    /// Read a `CoinMetadata` from the `db` for the coin whose inner type is
364    /// `coin_type`.
365    pub(crate) async fn query(
366        db: &Db,
367        coin_type: TypeTag,
368        checkpoint_viewed_at: u64,
369    ) -> Result<Option<CoinMetadata>, Error> {
370        let TypeTag::Struct(coin_struct) = coin_type else {
371            // If the type supplied is not metadata, we know it's not a valid coin type, so
372            // there won't be CoinMetadata for it.
373            return Ok(None);
374        };
375
376        let metadata_type = NativeCoinMetadata::type_(*coin_struct);
377        let Some(object) = Object::query_singleton(db, metadata_type, checkpoint_viewed_at).await?
378        else {
379            return Ok(None);
380        };
381
382        let move_object = MoveObject::try_from(&object).map_err(|_| {
383            Error::Internal(format!(
384                "Expected {} to be CoinMetadata, but it is not an object.",
385                object.address,
386            ))
387        })?;
388
389        let coin_metadata = CoinMetadata::try_from(&move_object).map_err(|_| {
390            Error::Internal(format!(
391                "Expected {} to be CoinMetadata, but it is not.",
392                object.address,
393            ))
394        })?;
395
396        Ok(Some(coin_metadata))
397    }
398
399    pub(crate) async fn query_total_supply(
400        ctx: &Context<'_>,
401        coin_type: TypeTag,
402        checkpoint_viewed_at: u64,
403    ) -> Result<Option<u64>, Error> {
404        let TypeTag::Struct(coin_struct) = coin_type else {
405            // If the type supplied is not metadata, we know it's not a valid coin type, so
406            // there won't be CoinMetadata for it.
407            return Ok(None);
408        };
409
410        Ok(Some(if GAS::is_gas(coin_struct.as_ref()) {
411            let pg_manager = ctx.data_unchecked::<PgManager>();
412
413            let state = pg_manager.fetch_iota_system_state(None).await?;
414
415            state.iota_total_supply()
416        } else {
417            let cap_type = TreasuryCap::type_(*coin_struct);
418
419            let db = ctx.data_unchecked();
420
421            let Some(object) = Object::query_singleton(db, cap_type, checkpoint_viewed_at).await?
422            else {
423                return Ok(None);
424            };
425
426            let Some(native) = object.native_impl() else {
427                return Ok(None);
428            };
429
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.type_().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}