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