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