sui_rpc/client/
staking_rewards.rs

1use prost_types::FieldMask;
2use sui_sdk_types::Address;
3
4use crate::field::FieldMaskUtil;
5use crate::proto::sui::rpc::v2beta2::simulate_transaction_request::TransactionChecks;
6use crate::proto::sui::rpc::v2beta2::Argument;
7use crate::proto::sui::rpc::v2beta2::GetObjectRequest;
8use crate::proto::sui::rpc::v2beta2::Input;
9use crate::proto::sui::rpc::v2beta2::ListOwnedObjectsRequest;
10use crate::proto::sui::rpc::v2beta2::MoveCall;
11use crate::proto::sui::rpc::v2beta2::Object;
12use crate::proto::sui::rpc::v2beta2::ProgrammableTransaction;
13use crate::proto::sui::rpc::v2beta2::SimulateTransactionRequest;
14use crate::proto::sui::rpc::v2beta2::Transaction;
15
16use super::Client;
17use super::Result;
18
19#[derive(Debug)]
20pub struct DelegatedStake {
21    /// ObjectId of this StakedSui delegation.
22    pub staked_sui_id: Address,
23    /// Validator's Address.
24    pub validator_address: Address,
25    /// Staking pool object id.
26    pub staking_pool: Address,
27    /// The epoch at which the stake becomes active.
28    pub activation_epoch: u64,
29    /// The staked SUI tokens.
30    pub principal: u64,
31    /// The accrued rewards.
32    pub rewards: u64,
33}
34
35#[derive(serde::Deserialize, Debug)]
36struct StakedSui {
37    id: Address,
38    /// ID of the staking pool we are staking with.
39    pool_id: Address,
40    /// The epoch at which the stake becomes active.
41    stake_activation_epoch: u64,
42    /// The staked SUI tokens.
43    principal: u64,
44}
45
46impl Client {
47    pub async fn get_delegated_stake(&mut self, staked_sui_id: &Address) -> Result<DelegatedStake> {
48        let maybe_staked_sui = self
49            .ledger_client()
50            .get_object(GetObjectRequest {
51                object_id: Some(staked_sui_id.to_string()),
52                version: None,
53                read_mask: Some(FieldMask::from_str("contents")),
54            })
55            .await?
56            .into_inner()
57            .object
58            .unwrap_or_default();
59
60        let mut stakes = self
61            .try_create_delegated_stake_info(&[maybe_staked_sui])
62            .await?;
63        Ok(stakes.remove(0))
64    }
65
66    pub async fn list_delegated_stake(&mut self, address: &Address) -> Result<Vec<DelegatedStake>> {
67        const STAKED_SUI_TYPE: &str = "0x3::staking_pool::StakedSui";
68
69        let mut delegated_stakes = Vec::new();
70
71        let mut list_request = ListOwnedObjectsRequest {
72            owner: Some(address.to_string()),
73            page_size: Some(500),
74            page_token: None,
75            read_mask: Some(FieldMask::from_str("contents")),
76            object_type: Some(STAKED_SUI_TYPE.to_owned()),
77        };
78
79        loop {
80            let response = self
81                .live_data_client()
82                .list_owned_objects(list_request.clone())
83                .await?
84                .into_inner();
85
86            // with the fetched StakedSui objects, attempt to calculate the rewards and create a
87            // DelegatedStake for each.
88            delegated_stakes.extend(
89                self.try_create_delegated_stake_info(&response.objects)
90                    .await?,
91            );
92
93            // If there are no more pages then we can break, otherwise update the page_token for
94            // the next request
95            if response.next_page_token.is_none() {
96                break;
97            } else {
98                list_request.page_token = response.next_page_token;
99            }
100        }
101
102        Ok(delegated_stakes)
103    }
104
105    async fn try_create_delegated_stake_info(
106        &mut self,
107        maybe_staked_sui: &[Object],
108    ) -> Result<Vec<DelegatedStake>> {
109        let staked_suis = maybe_staked_sui
110            .iter()
111            .map(|o| {
112                o.contents
113                    .clone() // Avoid clone probably with better getters
114                    .unwrap_or_default()
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    async fn calculate_rewards(
145        &mut self,
146        staked_sui_ids: &[Address],
147    ) -> Result<Vec<(Address, u64)>> {
148        let mut ptb = ProgrammableTransaction {
149            inputs: vec![Input {
150                object_id: Some("0x5".into()),
151                ..Default::default()
152            }],
153            commands: vec![],
154        };
155        let system_object = Argument::new_input(0);
156
157        for id in staked_sui_ids {
158            let staked_sui = Argument::new_input(ptb.inputs.len() as u16);
159
160            ptb.inputs.push(Input {
161                object_id: Some(id.to_string()),
162                ..Default::default()
163            });
164
165            ptb.commands.push(
166                MoveCall {
167                    package: Some("0x3".to_owned()),
168                    module: Some("sui_system".to_owned()),
169                    function: Some("calculate_rewards".to_owned()),
170                    type_arguments: vec![],
171                    arguments: vec![system_object, staked_sui],
172                }
173                .into(),
174            );
175        }
176
177        let transaction = Transaction {
178            kind: Some(ptb.into()),
179            sender: Some("0x0".into()),
180            ..Default::default()
181        };
182
183        let resp = self
184            .live_data_client()
185            .simulate_transaction(SimulateTransactionRequest {
186                transaction: Some(transaction),
187                read_mask: Some(FieldMask::from_paths([
188                    "outputs.return_values.value",
189                    "transaction.effects.status",
190                ])),
191                checks: Some(TransactionChecks::Disabled as _),
192                ..Default::default()
193            })
194            .await?
195            .into_inner();
196
197        if !resp
198            .transaction
199            .as_ref()
200            .and_then(|t| t.effects.as_ref().and_then(|e| e.status.as_ref()))
201            .is_some_and(|s| s.success())
202        {
203            return Err(tonic::Status::from_error(
204                "transaction execution failed".into(),
205            ));
206        }
207
208        if staked_sui_ids.len() != resp.outputs.len() {
209            return Err(tonic::Status::from_error(
210                "missing transaction command outputs".into(),
211            ));
212        }
213
214        let mut rewards = Vec::with_capacity(staked_sui_ids.len());
215
216        for (id, output) in staked_sui_ids.iter().zip(resp.outputs) {
217            let bcs_rewards = output
218                .return_values
219                .first()
220                .and_then(|o| o.value.as_ref())
221                .ok_or_else(|| tonic::Status::from_error("missing bcs".into()))?;
222
223            let reward =
224                if bcs_rewards.name() == "u64" && bcs_rewards.value().len() == size_of::<u64>() {
225                    u64::from_le_bytes(bcs_rewards.value().try_into().unwrap())
226                } else {
227                    return Err(tonic::Status::from_error("missing rewards".into()));
228                };
229            rewards.push((*id, reward));
230        }
231
232        Ok(rewards)
233    }
234
235    async fn get_validator_address_by_pool_id(
236        &mut self,
237        pool_ids: &[Address],
238    ) -> Result<Vec<(Address, Address)>> {
239        let mut ptb = ProgrammableTransaction {
240            inputs: vec![Input {
241                object_id: Some("0x5".into()),
242                ..Default::default()
243            }],
244            commands: vec![],
245        };
246        let system_object = Argument::new_input(0);
247
248        for id in pool_ids {
249            let pool_id = Argument::new_input(ptb.inputs.len() as u16);
250
251            ptb.inputs.push(Input {
252                pure: Some(id.into_inner().to_vec().into()),
253                ..Default::default()
254            });
255
256            ptb.commands.push(
257                MoveCall {
258                    package: Some("0x3".to_owned()),
259                    module: Some("sui_system".to_owned()),
260                    function: Some("validator_address_by_pool_id".to_owned()),
261                    type_arguments: vec![],
262                    arguments: vec![system_object, pool_id],
263                }
264                .into(),
265            );
266        }
267
268        let transaction = Transaction {
269            kind: Some(ptb.into()),
270            sender: Some("0x0".into()),
271            ..Default::default()
272        };
273
274        let resp = self
275            .live_data_client()
276            .simulate_transaction(SimulateTransactionRequest {
277                transaction: Some(transaction),
278                read_mask: Some(FieldMask::from_paths([
279                    "outputs.return_values.value",
280                    "transaction.effects.status",
281                ])),
282                checks: Some(TransactionChecks::Disabled as _),
283                ..Default::default()
284            })
285            .await?
286            .into_inner();
287
288        if !resp
289            .transaction
290            .as_ref()
291            .and_then(|t| t.effects.as_ref().and_then(|e| e.status.as_ref()))
292            .is_some_and(|s| s.success())
293        {
294            return Err(tonic::Status::from_error(
295                "transaction execution failed".into(),
296            ));
297        }
298
299        if pool_ids.len() != resp.outputs.len() {
300            return Err(tonic::Status::from_error(
301                "missing transaction command outputs".into(),
302            ));
303        }
304
305        let mut addresses = Vec::with_capacity(pool_ids.len());
306
307        for (id, output) in pool_ids.iter().zip(resp.outputs) {
308            let validator_address = output
309                .return_values
310                .first()
311                .and_then(|o| o.value.as_ref())
312                .ok_or_else(|| tonic::Status::from_error("missing bcs".into()))?;
313
314            let address = if validator_address.name() == "address"
315                && validator_address.value().len() == Address::LENGTH
316            {
317                Address::from_bytes(validator_address.value())
318                    .map_err(|e| tonic::Status::from_error(e.into()))?
319            } else {
320                return Err(tonic::Status::from_error("missing address".into()));
321            };
322            addresses.push((*id, address));
323        }
324
325        Ok(addresses)
326    }
327}