Skip to main content

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_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/// The metadata for a coin type.
46#[Object]
47impl CoinMetadata {
48    pub(crate) async fn address(&self) -> IotaAddress {
49        OwnerImpl::from(&self.super_.super_).address().await
50    }
51
52    /// Objects owned by this object, optionally `filter`-ed.
53    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    /// Total balance of all coins with marker type owned by this object. If
68    /// type is not supplied, it defaults to `0x2::iota::IOTA`.
69    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    /// The balances of all coin types owned by this object.
80    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    /// The coin objects for this object.
94    ///
95    /// `type` is a filter on the coin's type parameter, defaulting to
96    /// `0x2::iota::IOTA`.
97    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    /// The `0x3::staking_pool::StakedIota` objects owned by this object.
112    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    /// The name explicitly configured as the default name pointing to this
126    /// object.
127    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    /// The NameRegistration NFTs owned by this object. These grant the
138    /// owner the capability to manage the associated name.
139    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    /// The current status of the object as read from the off-chain store. The
157    /// possible states are:
158    /// - NOT_INDEXED: The object is loaded from serialized data, such as the
159    ///   contents of a genesis or system package upgrade transaction.
160    /// - INDEXED: The object is retrieved from the off-chain index and
161    ///   represents the most recent or historical state of the object.
162    pub(crate) async fn status(&self) -> ObjectStatus {
163        ObjectImpl(&self.super_.super_).status().await
164    }
165
166    /// 32-byte hash that identifies the object's contents, encoded as a Base58
167    /// string.
168    pub(crate) async fn digest(&self) -> Option<String> {
169        ObjectImpl(&self.super_.super_).digest().await
170    }
171
172    /// The owner type of this object: Immutable, Shared, Parent, Address
173    pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
174        ObjectImpl(&self.super_.super_).owner(ctx).await
175    }
176
177    /// The transaction block that created this version of the object.
178    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    /// The amount of IOTA we would rebate if this object gets deleted or
188    /// mutated. This number is recalculated based on the present storage
189    /// gas price.
190    pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
191        ObjectImpl(&self.super_.super_).storage_rebate().await
192    }
193
194    /// The transaction blocks that sent objects to this object.
195    ///
196    /// `scanLimit` restricts the number of candidate transactions scanned when
197    /// gathering a page of results. It is required for queries that apply two
198    /// or more complex filters (on function, affected address, recipient, input
199    /// object, changed object, or wrapped or deleted object), and can be at
200    /// most `serviceConfig.maxScanLimit`. A `kind` filter cannot be
201    /// combined with any of them.
202    ///
203    /// When the scan limit is reached the page will be returned even if it has
204    /// fewer than `first` results when paginating forward (`last` when
205    /// paginating backwards). If there are more transactions to scan,
206    /// `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to
207    /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set
208    /// to the last transaction that was scanned as opposed to the last (or
209    /// first) transaction in the page.
210    ///
211    /// Requesting the next (or previous) page after this cursor will resume the
212    /// search, scanning the next `scanLimit` many transactions in the
213    /// direction of pagination, and so on until all transactions in the
214    /// scanning range have been visited.
215    ///
216    /// By default, the scanning range includes all transactions known to
217    /// GraphQL, but it can be restricted by the `after` and `before`
218    /// cursors, and the `beforeCheckpoint`, `afterCheckpoint` and
219    /// `atCheckpoint` filters.
220    ///
221    /// DEPRECATION NOTICE: Support for the combination of two or more complex
222    /// filters as discussed above will stop with the v1.38 release. `scanLimit`
223    /// will thus become obsolete and will be removed as well.
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        #[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    /// The Base64-encoded BCS serialization of the object's content.
246    pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
247        ObjectImpl(&self.super_.super_).bcs().await
248    }
249
250    /// Displays the contents of the Move object in a JSON string and through
251    /// GraphQL types. Also provides the flat representation of the type
252    /// signature, and the BCS of the corresponding data.
253    pub(crate) async fn contents(&self) -> Option<MoveValue> {
254        MoveObjectImpl(&self.super_).contents().await
255    }
256
257    /// The set of named templates defined on-chain for the type of this object,
258    /// to be handled off-chain. The server substitutes data from the object
259    /// into these templates to generate a display string per template.
260    pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
261        ObjectImpl(&self.super_.super_).display(ctx).await
262    }
263
264    /// Access a dynamic field on an object using its name. Names are arbitrary
265    /// Move values whose type have `copy`, `drop`, and `store`, and are
266    /// specified using their type, and their BCS contents, Base64 encoded.
267    ///
268    /// Dynamic fields on wrapped objects can be accessed by using the same API
269    /// under the Owner type.
270    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    /// Access a dynamic object field on an object using its name. Names are
281    /// arbitrary Move values whose type have `copy`, `drop`, and `store`,
282    /// and are specified using their type, and their BCS contents, Base64
283    /// encoded. The value of a dynamic object field can also be accessed
284    /// off-chain directly via its address (e.g. using `Query.object`).
285    ///
286    /// Dynamic fields on wrapped objects can be accessed by using the same API
287    /// under the Owner type.
288    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    /// The dynamic fields and dynamic object fields on an object.
299    ///
300    /// Dynamic fields on wrapped objects can be accessed by using the same API
301    /// under the Owner type.
302    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    /// The number of decimal places used to represent the token.
323    async fn decimals(&self) -> Option<u8> {
324        Some(self.native.decimals)
325    }
326
327    /// Full, official name of the token.
328    async fn name(&self) -> Option<&str> {
329        Some(&self.native.name)
330    }
331
332    /// The token's identifying abbreviation.
333    async fn symbol(&self) -> Option<&str> {
334        Some(&self.native.symbol)
335    }
336
337    /// Optional description of the token, provided by the creator of the token.
338    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    /// The overall quantity of tokens that will be issued.
347    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    /// Read a `CoinMetadata` from the `db` for the coin whose inner type is
367    /// `coin_type`.
368    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            // If the type supplied is not metadata, we know it's not a valid coin type, so
375            // there won't be CoinMetadata for it.
376            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            // If the type supplied is not metadata, we know it's not a valid coin type, so
409            // there won't be CoinMetadata for it.
410            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}