sui_graphql_rpc/types/
coin.rs

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