sui_graphql_rpc/types/
stake.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::connection::ScanConnection;
5use crate::error::Error;
6use crate::{context_data::db_data_provider::PgManager, data::Db};
7
8use super::balance::{self, Balance};
9use super::base64::Base64;
10use super::coin::Coin;
11use super::cursor::Page;
12use super::display::DisplayEntry;
13use super::dynamic_field::{DynamicField, DynamicFieldName};
14use super::move_object::MoveObjectImpl;
15use super::move_value::MoveValue;
16use super::object::{Object, ObjectFilter, ObjectImpl, ObjectOwner, ObjectStatus};
17use super::owner::OwnerImpl;
18use super::suins_registration::{DomainFormat, SuinsRegistration};
19use super::transaction_block::{self, TransactionBlock, TransactionBlockFilter};
20use super::type_filter::ExactTypeFilter;
21use super::uint53::UInt53;
22use super::{
23    big_int::BigInt, epoch::Epoch, move_object::MoveObject, object, sui_address::SuiAddress,
24};
25use async_graphql::connection::Connection;
26use async_graphql::*;
27use move_core_types::language_storage::StructTag;
28use sui_json_rpc_types::{Stake as RpcStakedSui, StakeStatus as RpcStakeStatus};
29use sui_types::base_types::MoveObjectType;
30use sui_types::governance::StakedSui as NativeStakedSui;
31
32#[derive(Copy, Clone, Enum, PartialEq, Eq)]
33/// The stake's possible status: active, pending, or unstaked.
34pub(crate) enum StakeStatus {
35    /// The stake object is active in a staking pool and it is generating rewards.
36    Active,
37    /// The stake awaits to join a staking pool in the next epoch.
38    Pending,
39    /// The stake is no longer active in any staking pool.
40    Unstaked,
41}
42
43pub(crate) enum StakedSuiDowncastError {
44    NotAStakedSui,
45    Bcs(bcs::Error),
46}
47
48#[derive(Clone)]
49pub(crate) struct StakedSui {
50    /// Representation of this StakedSui as a generic Move Object.
51    pub super_: MoveObject,
52
53    /// Deserialized representation of the Move Object's contents as a
54    /// `0x3::staking_pool::StakedSui`.
55    pub native: NativeStakedSui,
56}
57
58/// Represents a `0x3::staking_pool::StakedSui` Move object on-chain.
59#[Object]
60impl StakedSui {
61    pub(crate) async fn address(&self) -> SuiAddress {
62        OwnerImpl::from(&self.super_.super_).address().await
63    }
64
65    /// Objects owned by this object, optionally `filter`-ed.
66    pub(crate) async fn objects(
67        &self,
68        ctx: &Context<'_>,
69        first: Option<u64>,
70        after: Option<object::Cursor>,
71        last: Option<u64>,
72        before: Option<object::Cursor>,
73        filter: Option<ObjectFilter>,
74    ) -> Result<Connection<String, MoveObject>> {
75        OwnerImpl::from(&self.super_.super_)
76            .objects(ctx, first, after, last, before, filter)
77            .await
78    }
79
80    /// Total balance of all coins with marker type owned by this object. If type is not supplied,
81    /// it defaults to `0x2::sui::SUI`.
82    pub(crate) async fn balance(
83        &self,
84        ctx: &Context<'_>,
85        type_: Option<ExactTypeFilter>,
86    ) -> Result<Option<Balance>> {
87        OwnerImpl::from(&self.super_.super_)
88            .balance(ctx, type_)
89            .await
90    }
91
92    /// The balances of all coin types owned by this object.
93    pub(crate) async fn balances(
94        &self,
95        ctx: &Context<'_>,
96        first: Option<u64>,
97        after: Option<balance::Cursor>,
98        last: Option<u64>,
99        before: Option<balance::Cursor>,
100    ) -> Result<Connection<String, Balance>> {
101        OwnerImpl::from(&self.super_.super_)
102            .balances(ctx, first, after, last, before)
103            .await
104    }
105
106    /// The coin objects for this object.
107    ///
108    ///`type` is a filter on the coin's type parameter, defaulting to `0x2::sui::SUI`.
109    pub(crate) async fn coins(
110        &self,
111        ctx: &Context<'_>,
112        first: Option<u64>,
113        after: Option<object::Cursor>,
114        last: Option<u64>,
115        before: Option<object::Cursor>,
116        type_: Option<ExactTypeFilter>,
117    ) -> Result<Connection<String, Coin>> {
118        OwnerImpl::from(&self.super_.super_)
119            .coins(ctx, first, after, last, before, type_)
120            .await
121    }
122
123    /// The `0x3::staking_pool::StakedSui` objects owned by this object.
124    pub(crate) async fn staked_suis(
125        &self,
126        ctx: &Context<'_>,
127        first: Option<u64>,
128        after: Option<object::Cursor>,
129        last: Option<u64>,
130        before: Option<object::Cursor>,
131    ) -> Result<Connection<String, StakedSui>> {
132        OwnerImpl::from(&self.super_.super_)
133            .staked_suis(ctx, first, after, last, before)
134            .await
135    }
136
137    /// The domain explicitly configured as the default domain pointing to this object.
138    pub(crate) async fn default_suins_name(
139        &self,
140        ctx: &Context<'_>,
141        format: Option<DomainFormat>,
142    ) -> Result<Option<String>> {
143        OwnerImpl::from(&self.super_.super_)
144            .default_suins_name(ctx, format)
145            .await
146    }
147
148    /// The SuinsRegistration NFTs owned by this object. These grant the owner the capability to
149    /// manage the associated domain.
150    pub(crate) async fn suins_registrations(
151        &self,
152        ctx: &Context<'_>,
153        first: Option<u64>,
154        after: Option<object::Cursor>,
155        last: Option<u64>,
156        before: Option<object::Cursor>,
157    ) -> Result<Connection<String, SuinsRegistration>> {
158        OwnerImpl::from(&self.super_.super_)
159            .suins_registrations(ctx, first, after, last, before)
160            .await
161    }
162
163    pub(crate) async fn version(&self) -> UInt53 {
164        ObjectImpl(&self.super_.super_).version().await
165    }
166
167    /// The current status of the object as read from the off-chain store. The possible states are:
168    /// NOT_INDEXED, the object is loaded from serialized data, such as the contents of a genesis or
169    /// system package upgrade transaction. LIVE, the version returned is the most recent for the
170    /// object, and it is not deleted or wrapped at that version. HISTORICAL, the object was
171    /// referenced at a specific version or checkpoint, so is fetched from historical tables and may
172    /// not be the latest version of the object. WRAPPED_OR_DELETED, the object is deleted or
173    /// wrapped and only partial 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 string.
179    pub(crate) async fn digest(&self) -> Option<String> {
180        ObjectImpl(&self.super_.super_).digest().await
181    }
182
183    /// The owner type of this object: Immutable, Shared, Parent, Address
184    pub(crate) async fn owner(&self) -> Option<ObjectOwner> {
185        ObjectImpl(&self.super_.super_).owner().await
186    }
187
188    /// The transaction block that created this version of the object.
189    pub(crate) async fn previous_transaction_block(
190        &self,
191        ctx: &Context<'_>,
192    ) -> Result<Option<TransactionBlock>> {
193        ObjectImpl(&self.super_.super_)
194            .previous_transaction_block(ctx)
195            .await
196    }
197
198    /// The amount of SUI we would rebate if this object gets deleted or mutated. This number is
199    /// recalculated based on the present storage gas price.
200    pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
201        ObjectImpl(&self.super_.super_).storage_rebate().await
202    }
203
204    /// The transaction blocks that sent objects to this object.
205    ///
206    /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of
207    /// results. It is required for queries that apply more than two complex filters (on function,
208    /// kind, sender, recipient, input object, changed object, or ids), and can be at most
209    /// `serviceConfig.maxScanLimit`.
210    ///
211    /// When the scan limit is reached the page will be returned even if it has fewer than `first`
212    /// results when paginating forward (`last` when paginating backwards). If there are more
213    /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to
214    /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last
215    /// transaction that was scanned as opposed to the last (or first) transaction in the page.
216    ///
217    /// Requesting the next (or previous) page after this cursor will resume the search, scanning
218    /// the next `scanLimit` many transactions in the direction of pagination, and so on until all
219    /// transactions in the scanning range have been visited.
220    ///
221    /// By default, the scanning range includes all transactions known to GraphQL, but it can be
222    /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`,
223    /// `afterCheckpoint` and `atCheckpoint` filters.
224    pub(crate) async fn received_transaction_blocks(
225        &self,
226        ctx: &Context<'_>,
227        first: Option<u64>,
228        after: Option<transaction_block::Cursor>,
229        last: Option<u64>,
230        before: Option<transaction_block::Cursor>,
231        filter: Option<TransactionBlockFilter>,
232        scan_limit: Option<u64>,
233    ) -> Result<ScanConnection<String, TransactionBlock>> {
234        ObjectImpl(&self.super_.super_)
235            .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
236            .await
237    }
238
239    /// The Base64-encoded BCS serialization of the object's content.
240    pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
241        ObjectImpl(&self.super_.super_).bcs().await
242    }
243
244    /// Displays the contents of the Move object in a JSON string and through GraphQL types. Also
245    /// provides the flat representation of the type signature, and the BCS of the corresponding
246    /// data.
247    pub(crate) async fn contents(&self) -> Option<MoveValue> {
248        MoveObjectImpl(&self.super_).contents().await
249    }
250
251    /// Determines whether a transaction can transfer this object, using the TransferObjects
252    /// transaction command or `sui::transfer::public_transfer`, both of which require the object to
253    /// have the `key` and `store` abilities.
254    pub(crate) async fn has_public_transfer(&self, ctx: &Context<'_>) -> Result<bool> {
255        MoveObjectImpl(&self.super_).has_public_transfer(ctx).await
256    }
257
258    /// The set of named templates defined on-chain for the type of this object, to be handled
259    /// off-chain. The server substitutes data from the object into these templates to generate a
260    /// 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 Move values whose
266    /// type have `copy`, `drop`, and `store`, and are specified using their type, and their BCS
267    /// contents, Base64 encoded.
268    ///
269    /// Dynamic fields on wrapped objects can be accessed by using the same API under the Owner
270    /// 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 arbitrary Move values
282    /// whose type have `copy`, `drop`, and `store`, and are specified using their type, and their
283    /// BCS contents, Base64 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 under the Owner
287    /// 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 under the Owner
301    /// 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    /// A stake can be pending, active, or unstaked
323    async fn stake_status(&self, ctx: &Context<'_>) -> Result<StakeStatus> {
324        Ok(match self.rpc_stake(ctx).await.extend()?.status {
325            RpcStakeStatus::Pending => StakeStatus::Pending,
326            RpcStakeStatus::Active { .. } => StakeStatus::Active,
327            RpcStakeStatus::Unstaked => StakeStatus::Unstaked,
328        })
329    }
330
331    /// The epoch at which this stake became active.
332    async fn activated_epoch(&self, ctx: &Context<'_>) -> Result<Option<Epoch>> {
333        Epoch::query(
334            ctx,
335            Some(self.native.activation_epoch()),
336            self.super_.super_.checkpoint_viewed_at,
337        )
338        .await
339        .extend()
340    }
341
342    /// The epoch at which this object was requested to join a stake pool.
343    async fn requested_epoch(&self, ctx: &Context<'_>) -> Result<Option<Epoch>> {
344        Epoch::query(
345            ctx,
346            Some(self.native.request_epoch()),
347            self.super_.super_.checkpoint_viewed_at,
348        )
349        .await
350        .extend()
351    }
352
353    /// The object id of the validator staking pool this stake belongs to.
354    async fn pool_id(&self) -> Option<SuiAddress> {
355        Some(self.native.pool_id().into())
356    }
357
358    /// The SUI that was initially staked.
359    async fn principal(&self) -> Option<BigInt> {
360        Some(BigInt::from(self.native.principal()))
361    }
362
363    /// The estimated reward for this stake object, calculated as:
364    ///
365    ///  principal * (initial_stake_rate / current_stake_rate - 1.0)
366    ///
367    /// Or 0, if this value is negative, where:
368    ///
369    /// - `initial_stake_rate` is the stake rate at the epoch this stake was activated at.
370    /// - `current_stake_rate` is the stake rate in the current epoch.
371    ///
372    /// This value is only available if the stake is active.
373    async fn estimated_reward(&self, ctx: &Context<'_>) -> Result<Option<BigInt>, Error> {
374        let RpcStakeStatus::Active { estimated_reward } = self.rpc_stake(ctx).await?.status else {
375            return Ok(None);
376        };
377
378        Ok(Some(BigInt::from(estimated_reward)))
379    }
380}
381
382impl StakedSui {
383    /// Query the database for a `page` of Staked SUI. The page uses the same cursor type as is used
384    /// for `Object`, and is further filtered to a particular `owner`.
385    ///
386    /// `checkpoint_viewed_at` represents the checkpoint sequence number at which this page was
387    /// queried for. Each entity returned in the connection will inherit this checkpoint, so that
388    /// when viewing that entity's state, it will be as if it was read at the same checkpoint.
389    pub(crate) async fn paginate(
390        db: &Db,
391        page: Page<object::Cursor>,
392        owner: SuiAddress,
393        checkpoint_viewed_at: u64,
394    ) -> Result<Connection<String, StakedSui>, Error> {
395        let type_: StructTag = MoveObjectType::staked_sui().into();
396
397        let filter = ObjectFilter {
398            type_: Some(type_.into()),
399            owner: Some(owner),
400            ..Default::default()
401        };
402
403        Object::paginate_subtype(db, page, filter, checkpoint_viewed_at, |object| {
404            let address = object.address;
405            let move_object = MoveObject::try_from(&object).map_err(|_| {
406                Error::Internal(format!(
407                    "Expected {address} to be a StakedSui, but it's not a Move Object.",
408                ))
409            })?;
410
411            StakedSui::try_from(&move_object).map_err(|_| {
412                Error::Internal(format!(
413                    "Expected {address} to be a StakedSui, but it is not."
414                ))
415            })
416        })
417        .await
418    }
419
420    /// The JSON-RPC representation of a StakedSui so that we can "cheat" to implement fields that
421    /// are not yet implemented directly for GraphQL.
422    ///
423    /// TODO: Make this obsolete
424    async fn rpc_stake(&self, ctx: &Context<'_>) -> Result<RpcStakedSui, Error> {
425        ctx.data_unchecked::<PgManager>()
426            .fetch_rpc_staked_sui(self.native.clone())
427            .await
428    }
429}
430
431impl TryFrom<&MoveObject> for StakedSui {
432    type Error = StakedSuiDowncastError;
433
434    fn try_from(move_object: &MoveObject) -> Result<Self, Self::Error> {
435        if !move_object.native.is_staked_sui() {
436            return Err(StakedSuiDowncastError::NotAStakedSui);
437        }
438
439        Ok(Self {
440            super_: move_object.clone(),
441            native: bcs::from_bytes(move_object.native.contents())
442                .map_err(StakedSuiDowncastError::Bcs)?,
443        })
444    }
445}