Skip to main content

sui_rpc/client/
staking_rewards.rs

1use futures::future::try_join_all;
2use prost_types::FieldMask;
3use sui_sdk_types::Address;
4
5use crate::field::FieldMaskUtil;
6use crate::proto::sui::rpc::v2::Argument;
7use crate::proto::sui::rpc::v2::GetObjectRequest;
8use crate::proto::sui::rpc::v2::Input;
9use crate::proto::sui::rpc::v2::ListOwnedObjectsRequest;
10use crate::proto::sui::rpc::v2::MoveCall;
11use crate::proto::sui::rpc::v2::Object;
12use crate::proto::sui::rpc::v2::ProgrammableTransaction;
13use crate::proto::sui::rpc::v2::SimulateTransactionRequest;
14use crate::proto::sui::rpc::v2::Transaction;
15use crate::proto::sui::rpc::v2::simulate_transaction_request::TransactionChecks;
16
17use super::Client;
18use super::Result;
19
20// Programmable transaction validation requires the command count to be strictly less than the
21// protocol's 1,024-command limit.
22const MAX_REWARDS_PER_PTB: usize = 1023;
23
24#[derive(Debug)]
25pub struct DelegatedStake {
26    /// ObjectId of this StakedSui delegation.
27    pub staked_sui_id: Address,
28    /// Validator's Address.
29    pub validator_address: Address,
30    /// Staking pool object id.
31    pub staking_pool: Address,
32    /// The epoch at which the stake becomes active.
33    pub activation_epoch: u64,
34    /// The staked SUI tokens.
35    pub principal: u64,
36    /// The accrued rewards.
37    pub rewards: u64,
38}
39
40#[derive(serde::Deserialize, Debug)]
41struct StakedSui {
42    id: Address,
43    /// ID of the staking pool we are staking with.
44    pool_id: Address,
45    /// The epoch at which the stake becomes active.
46    stake_activation_epoch: u64,
47    /// The staked SUI tokens.
48    principal: u64,
49}
50
51impl Client {
52    pub async fn get_delegated_stake(&mut self, staked_sui_id: &Address) -> Result<DelegatedStake> {
53        let maybe_staked_sui = self
54            .ledger_client()
55            .get_object(
56                GetObjectRequest::new(staked_sui_id)
57                    .with_read_mask(FieldMask::from_str("contents")),
58            )
59            .await?
60            .into_inner()
61            .object
62            .unwrap_or_default();
63
64        let mut stakes = self
65            .try_create_delegated_stake_info(&[maybe_staked_sui])
66            .await?;
67        Ok(stakes.remove(0))
68    }
69
70    pub async fn list_delegated_stake(&mut self, address: &Address) -> Result<Vec<DelegatedStake>> {
71        const STAKED_SUI_TYPE: &str = "0x3::staking_pool::StakedSui";
72
73        let mut delegated_stakes = Vec::new();
74
75        let mut list_request = ListOwnedObjectsRequest::default()
76            .with_owner(address)
77            .with_page_size(500u32)
78            .with_read_mask(FieldMask::from_str("contents"))
79            .with_object_type(STAKED_SUI_TYPE);
80
81        loop {
82            let response = self
83                .state_client()
84                .list_owned_objects(list_request.clone())
85                .await?
86                .into_inner();
87
88            // with the fetched StakedSui objects, attempt to calculate the rewards and create a
89            // DelegatedStake for each.
90            delegated_stakes.extend(
91                self.try_create_delegated_stake_info(&response.objects)
92                    .await?,
93            );
94
95            // If there are no more pages then we can break, otherwise update the page_token for
96            // the next request
97            if response.next_page_token.is_none() {
98                break;
99            } else {
100                list_request.page_token = response.next_page_token;
101            }
102        }
103
104        Ok(delegated_stakes)
105    }
106
107    async fn try_create_delegated_stake_info(
108        &mut self,
109        maybe_staked_sui: &[Object],
110    ) -> Result<Vec<DelegatedStake>> {
111        let staked_suis = maybe_staked_sui
112            .iter()
113            .map(|o| {
114                o.contents()
115                    .deserialize::<StakedSui>()
116                    .map_err(Into::into)
117                    .map_err(tonic::Status::from_error)
118            })
119            .collect::<Result<Vec<StakedSui>>>()?;
120
121        let ids = staked_suis.iter().map(|s| s.id).collect::<Vec<_>>();
122        let pool_ids = staked_suis.iter().map(|s| s.pool_id).collect::<Vec<_>>();
123
124        let rewards = self.calculate_rewards(&ids).await?;
125        let validator_addresses = self.get_validator_address_by_pool_id(&pool_ids).await?;
126
127        Ok(staked_suis
128            .into_iter()
129            .zip(rewards)
130            .zip(validator_addresses)
131            .map(
132                |((staked_sui, (_id, rewards)), (_pool_id, validator_address))| DelegatedStake {
133                    staked_sui_id: staked_sui.id,
134                    validator_address,
135                    staking_pool: staked_sui.pool_id,
136                    activation_epoch: staked_sui.stake_activation_epoch,
137                    principal: staked_sui.principal,
138                    rewards,
139                },
140            )
141            .collect())
142    }
143
144    pub async fn calculate_rewards(
145        &mut self,
146        staked_sui_ids: &[Address],
147    ) -> Result<Vec<(Address, u64)>> {
148        let batches = staked_sui_ids.chunks(MAX_REWARDS_PER_PTB).map(|batch| {
149            let mut client = self.clone();
150            async move { client.calculate_rewards_batch(batch).await }
151        });
152
153        Ok(try_join_all(batches).await?.into_iter().flatten().collect())
154    }
155
156    async fn calculate_rewards_batch(
157        &mut self,
158        staked_sui_ids: &[Address],
159    ) -> Result<Vec<(Address, u64)>> {
160        let mut ptb = ProgrammableTransaction::default()
161            .with_inputs(vec![Input::default().with_object_id("0x5")]);
162        let system_object = Argument::new_input(0);
163
164        for id in staked_sui_ids {
165            let staked_sui = Argument::new_input(ptb.inputs.len() as u16);
166
167            ptb.inputs.push(Input::default().with_object_id(id));
168
169            ptb.commands.push(
170                MoveCall::default()
171                    .with_package("0x3")
172                    .with_module("sui_system")
173                    .with_function("calculate_rewards")
174                    .with_arguments(vec![system_object, staked_sui])
175                    .into(),
176            );
177        }
178
179        let transaction = Transaction::default().with_kind(ptb).with_sender("0x0");
180
181        let resp = self
182            .execution_client()
183            .simulate_transaction(
184                SimulateTransactionRequest::new(transaction)
185                    .with_read_mask(FieldMask::from_paths([
186                        "command_outputs.return_values.value",
187                        "transaction.effects.status",
188                    ]))
189                    .with_checks(TransactionChecks::Disabled),
190            )
191            .await?
192            .into_inner();
193
194        if !resp.transaction().effects().status().success() {
195            return Err(tonic::Status::from_error(
196                "transaction execution failed".into(),
197            ));
198        }
199
200        if staked_sui_ids.len() != resp.command_outputs.len() {
201            return Err(tonic::Status::from_error(
202                "missing transaction command_outputs".into(),
203            ));
204        }
205
206        let mut rewards = Vec::with_capacity(staked_sui_ids.len());
207
208        for (id, output) in staked_sui_ids.iter().zip(resp.command_outputs) {
209            let bcs_rewards = output
210                .return_values
211                .first()
212                .and_then(|o| o.value_opt())
213                .ok_or_else(|| tonic::Status::from_error("missing bcs".into()))?;
214
215            let reward =
216                if bcs_rewards.name() == "u64" && bcs_rewards.value().len() == size_of::<u64>() {
217                    u64::from_le_bytes(bcs_rewards.value().try_into().unwrap())
218                } else {
219                    return Err(tonic::Status::from_error("missing rewards".into()));
220                };
221            rewards.push((*id, reward));
222        }
223
224        Ok(rewards)
225    }
226
227    pub async fn get_validator_address_by_pool_id(
228        &mut self,
229        pool_ids: &[Address],
230    ) -> Result<Vec<(Address, Address)>> {
231        let mut ptb = ProgrammableTransaction::default()
232            .with_inputs(vec![Input::default().with_object_id("0x5")]);
233        let system_object = Argument::new_input(0);
234
235        for id in pool_ids {
236            let pool_id = Argument::new_input(ptb.inputs.len() as u16);
237
238            ptb.inputs
239                .push(Input::default().with_pure(id.into_inner().to_vec()));
240
241            ptb.commands.push(
242                MoveCall::default()
243                    .with_package("0x3")
244                    .with_module("sui_system")
245                    .with_function("validator_address_by_pool_id")
246                    .with_arguments(vec![system_object, pool_id])
247                    .into(),
248            );
249        }
250
251        let transaction = Transaction::default().with_kind(ptb).with_sender("0x0");
252
253        // Boxed because this helper is inlined into the futures of
254        // get_delegated_stake and list_delegated_stake: the simulate call
255        // chain is by far the largest part of their state machines, and
256        // keeping it on the heap keeps those futures small for callers.
257        let mut execution_client = self.execution_client();
258        let simulate = Box::pin(
259            execution_client.simulate_transaction(
260                SimulateTransactionRequest::new(transaction)
261                    .with_read_mask(FieldMask::from_paths([
262                        "command_outputs.return_values.value",
263                        "transaction.effects.status",
264                    ]))
265                    .with_checks(TransactionChecks::Disabled),
266            ),
267        );
268        let resp = simulate.await?.into_inner();
269
270        if !resp.transaction().effects().status().success() {
271            return Err(tonic::Status::from_error(
272                "transaction execution failed".into(),
273            ));
274        }
275
276        if pool_ids.len() != resp.command_outputs.len() {
277            return Err(tonic::Status::from_error(
278                "missing transaction command_outputs".into(),
279            ));
280        }
281
282        let mut addresses = Vec::with_capacity(pool_ids.len());
283
284        for (id, output) in pool_ids.iter().zip(resp.command_outputs) {
285            let validator_address = output
286                .return_values
287                .first()
288                .and_then(|o| o.value_opt())
289                .ok_or_else(|| tonic::Status::from_error("missing bcs".into()))?;
290
291            let address = if validator_address.name() == "address"
292                && validator_address.value().len() == Address::LENGTH
293            {
294                Address::from_bytes(validator_address.value())
295                    .map_err(|e| tonic::Status::from_error(e.into()))?
296            } else {
297                return Err(tonic::Status::from_error("missing address".into()));
298            };
299            addresses.push((*id, address));
300        }
301
302        Ok(addresses)
303    }
304}