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