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::{NameFormat, NameRegistration},
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 name explicitly configured as the default name pointing to this
136    /// object.
137    pub(crate) async fn iota_names_default_name(
138        &self,
139        ctx: &Context<'_>,
140        format: Option<NameFormat>,
141    ) -> Result<Option<String>> {
142        OwnerImpl::from(&self.super_.super_)
143            .iota_names_default_name(ctx, format)
144            .await
145    }
146
147    /// The NameRegistration NFTs owned by this object. These grant the
148    /// owner the capability to manage the associated name.
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, NameRegistration>> {
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:
168    /// - NOT_INDEXED: The object is loaded from serialized data, such as the
169    ///   contents of a genesis or system package upgrade transaction.
170    /// - INDEXED: The object is retrieved from the off-chain index and
171    ///   represents the most recent or historical state of the object.
172    /// - WRAPPED_OR_DELETED: The object is deleted or wrapped and only partial
173    ///   information can be loaded.
174    pub(crate) async fn status(&self) -> ObjectStatus {
175        ObjectImpl(&self.super_.super_).status().await
176    }
177
178    /// 32-byte hash that identifies the object's contents, encoded as a Base58
179    /// string.
180    pub(crate) async fn digest(&self) -> Option<String> {
181        ObjectImpl(&self.super_.super_).digest().await
182    }
183
184    /// The owner type of this object: Immutable, Shared, Parent, Address
185    pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
186        ObjectImpl(&self.super_.super_).owner(ctx).await
187    }
188
189    /// The transaction block that created this version of the object.
190    pub(crate) async fn previous_transaction_block(
191        &self,
192        ctx: &Context<'_>,
193    ) -> Result<Option<TransactionBlock>> {
194        ObjectImpl(&self.super_.super_)
195            .previous_transaction_block(ctx)
196            .await
197    }
198
199    /// The amount of IOTA we would rebate if this object gets deleted or
200    /// mutated. This number is recalculated based on the present storage
201    /// gas price.
202    pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
203        ObjectImpl(&self.super_.super_).storage_rebate().await
204    }
205
206    /// The transaction blocks that sent objects to this object.
207    ///
208    /// `scanLimit` restricts the number of candidate transactions scanned when
209    /// gathering a page of results. It is required for queries that apply
210    /// more than two complex filters (on function, kind, sender, recipient,
211    /// input object, changed object, or ids), and can be at most
212    /// `serviceConfig.maxScanLimit`.
213    ///
214    /// When the scan limit is reached the page will be returned even if it has
215    /// fewer than `first` results when paginating forward (`last` when
216    /// paginating backwards). If there are more transactions to scan,
217    /// `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to
218    /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set
219    /// to the last transaction that was scanned as opposed to the last (or
220    /// first) transaction in the page.
221    ///
222    /// Requesting the next (or previous) page after this cursor will resume the
223    /// search, scanning the next `scanLimit` many transactions in the
224    /// direction of pagination, and so on until all transactions in the
225    /// scanning range have been visited.
226    ///
227    /// By default, the scanning range includes all transactions known to
228    /// GraphQL, but it can be restricted by the `after` and `before`
229    /// cursors, and the `beforeCheckpoint`, `afterCheckpoint` and
230    /// `atCheckpoint` filters.
231    pub(crate) async fn received_transaction_blocks(
232        &self,
233        ctx: &Context<'_>,
234        first: Option<u64>,
235        after: Option<transaction_block::Cursor>,
236        last: Option<u64>,
237        before: Option<transaction_block::Cursor>,
238        filter: Option<TransactionBlockFilter>,
239        scan_limit: Option<u64>,
240    ) -> Result<ScanConnection<String, TransactionBlock>> {
241        ObjectImpl(&self.super_.super_)
242            .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
243            .await
244    }
245
246    /// The Base64-encoded BCS serialization of the object's content.
247    pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
248        ObjectImpl(&self.super_.super_).bcs().await
249    }
250
251    /// Displays the contents of the Move object in a JSON string and through
252    /// GraphQL types. Also provides the flat representation of the type
253    /// signature, and the BCS of the corresponding data.
254    pub(crate) async fn contents(&self) -> Option<MoveValue> {
255        MoveObjectImpl(&self.super_).contents().await
256    }
257
258    /// The set of named templates defined on-chain for the type of this object,
259    /// to be handled off-chain. The server substitutes data from the object
260    /// into these templates to generate a display string per template.
261    pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
262        ObjectImpl(&self.super_.super_).display(ctx).await
263    }
264
265    /// Access a dynamic field on an object using its name. Names are arbitrary
266    /// Move values whose type have `copy`, `drop`, and `store`, and are
267    /// specified using their type, and their BCS contents, Base64 encoded.
268    ///
269    /// Dynamic fields on wrapped objects can be accessed by using the same API
270    /// under the Owner type.
271    pub(crate) async fn dynamic_field(
272        &self,
273        ctx: &Context<'_>,
274        name: DynamicFieldName,
275    ) -> Result<Option<DynamicField>> {
276        OwnerImpl::from(&self.super_.super_)
277            .dynamic_field(ctx, name, Some(self.super_.root_version()))
278            .await
279    }
280
281    /// Access a dynamic object field on an object using its name. Names are
282    /// arbitrary Move values whose type have `copy`, `drop`, and `store`,
283    /// and are specified using their type, and their BCS contents, Base64
284    /// encoded. The value of a dynamic object field can also be accessed
285    /// off-chain directly via its address (e.g. using `Query.object`).
286    ///
287    /// Dynamic fields on wrapped objects can be accessed by using the same API
288    /// under the Owner type.
289    pub(crate) async fn dynamic_object_field(
290        &self,
291        ctx: &Context<'_>,
292        name: DynamicFieldName,
293    ) -> Result<Option<DynamicField>> {
294        OwnerImpl::from(&self.super_.super_)
295            .dynamic_object_field(ctx, name, Some(self.super_.root_version()))
296            .await
297    }
298
299    /// The dynamic fields and dynamic object fields on an object.
300    ///
301    /// Dynamic fields on wrapped objects can be accessed by using the same API
302    /// under the Owner type.
303    pub(crate) async fn dynamic_fields(
304        &self,
305        ctx: &Context<'_>,
306        first: Option<u64>,
307        after: Option<object::Cursor>,
308        last: Option<u64>,
309        before: Option<object::Cursor>,
310    ) -> Result<Connection<String, DynamicField>> {
311        OwnerImpl::from(&self.super_.super_)
312            .dynamic_fields(
313                ctx,
314                first,
315                after,
316                last,
317                before,
318                Some(self.super_.root_version()),
319            )
320            .await
321    }
322
323    /// Balance of this coin object.
324    async fn coin_balance(&self) -> Option<BigInt> {
325        Some(BigInt::from(self.native.balance.value()))
326    }
327}
328
329impl Coin {
330    /// Query the database for a `page` of coins. The page uses the bytes of an
331    /// Object ID as the cursor, and can optionally be filtered by an owner.
332    pub(crate) async fn paginate(
333        db: &Db,
334        page: Page<object::Cursor>,
335        coin_type: TypeTag,
336        owner: Option<IotaAddress>,
337        checkpoint_viewed_at: u64,
338    ) -> Result<Connection<String, Coin>, Error> {
339        // If cursors are provided, defer to the `checkpoint_viewed_at` in the cursor if
340        // they are consistent. Otherwise, use the value from the parameter, or
341        // set to None. This is so that paginated queries are consistent with
342        // the previous query that created the cursor.
343        let cursor_viewed_at = page.validate_cursor_consistency()?;
344        let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at);
345
346        let Some((prev, next, results)) = db
347            .execute_repeatable(move |conn| {
348                let Some(range) = AvailableRange::result(conn, checkpoint_viewed_at)? else {
349                    return Ok::<_, diesel::result::Error>(None);
350                };
351
352                Ok(Some(page.paginate_raw_query::<StoredHistoryObject>(
353                    conn,
354                    checkpoint_viewed_at,
355                    coins_query(coin_type, owner, range, &page),
356                )?))
357            })
358            .await?
359        else {
360            return Err(Error::Client(
361                "Requested data is outside the available range".to_string(),
362            ));
363        };
364
365        let mut conn: Connection<String, Coin> = Connection::new(prev, next);
366
367        for stored in results {
368            // To maintain consistency, the returned cursor should have the same upper-bound
369            // as the checkpoint found on the cursor.
370            let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
371            let object =
372                Object::try_from_stored_history_object(stored, checkpoint_viewed_at, None)?;
373
374            let move_ = MoveObject::try_from(&object).map_err(|_| {
375                Error::Internal(format!(
376                    "Failed to deserialize as Move object: {}",
377                    object.address
378                ))
379            })?;
380
381            let coin = Coin::try_from(&move_).map_err(|_| {
382                Error::Internal(format!("Failed to deserialize as Coin: {}", object.address))
383            })?;
384
385            conn.edges.push(Edge::new(cursor, coin));
386        }
387
388        Ok(conn)
389    }
390}
391
392impl TryFrom<&MoveObject> for Coin {
393    type Error = CoinDowncastError;
394
395    fn try_from(move_object: &MoveObject) -> Result<Self, Self::Error> {
396        if !move_object.native.is_coin() {
397            return Err(CoinDowncastError::NotACoin);
398        }
399
400        Ok(Self {
401            super_: move_object.clone(),
402            native: bcs::from_bytes(move_object.native.contents())
403                .map_err(CoinDowncastError::Bcs)?,
404        })
405    }
406}
407
408/// Constructs a raw query to fetch objects from the database. Since there are
409/// no point lookups for the coin query, objects are filtered out if they
410/// satisfy the criteria but have a later version in the same checkpoint.
411fn coins_query(
412    coin_type: TypeTag,
413    owner: Option<IotaAddress>,
414    range: AvailableRange,
415    page: &Page<object::Cursor>,
416) -> RawQuery {
417    build_objects_query(
418        View::Consistent,
419        range,
420        page,
421        move |query| apply_filter(query, &coin_type, owner),
422        move |newer| newer,
423    )
424}
425
426fn apply_filter(mut query: RawQuery, coin_type: &TypeTag, owner: Option<IotaAddress>) -> RawQuery {
427    if let Some(owner) = owner {
428        query = filter!(
429            query,
430            format!(
431                "owner_id = '\\x{}'::bytea AND owner_type = {}",
432                hex::encode(owner.into_vec()),
433                OwnerType::Address as i16
434            )
435        );
436    }
437
438    query = filter!(
439        query,
440        "coin_type IS NOT NULL AND coin_type = {}",
441        coin_type.to_canonical_display(/* with_prefix */ true)
442    );
443
444    query
445}