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::v2::Argument;
6use crate::proto::sui::rpc::v2::GetObjectRequest;
7use crate::proto::sui::rpc::v2::Input;
8use crate::proto::sui::rpc::v2::ListOwnedObjectsRequest;
9use crate::proto::sui::rpc::v2::MoveCall;
10use crate::proto::sui::rpc::v2::Object;
11use crate::proto::sui::rpc::v2::ProgrammableTransaction;
12use crate::proto::sui::rpc::v2::SimulateTransactionRequest;
13use crate::proto::sui::rpc::v2::Transaction;
14use crate::proto::sui::rpc::v2::simulate_transaction_request::TransactionChecks;
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(
51                GetObjectRequest::new(staked_sui_id)
52                    .with_read_mask(FieldMask::from_str("contents")),
53            )
54            .await?
55            .into_inner()
56            .object
57            .unwrap_or_default();
58
59        let mut stakes = self
60            .try_create_delegated_stake_info(&[maybe_staked_sui])
61            .await?;
62        Ok(stakes.remove(0))
63    }
64
65    pub async fn list_delegated_stake(&mut self, address: &Address) -> Result<Vec<DelegatedStake>> {
66        const STAKED_SUI_TYPE: &str = "0x3::staking_pool::StakedSui";
67
68        let mut delegated_stakes = Vec::new();
69
70        let mut list_request = ListOwnedObjectsRequest::default()
71            .with_owner(address)
72            .with_page_size(500u32)
73            .with_read_mask(FieldMask::from_str("contents"))
74            .with_object_type(STAKED_SUI_TYPE);
75
76        loop {
77            let response = self
78                .state_client()
79                .list_owned_objects(list_request.clone())
80                .await?
81                .into_inner();
82
83            // with the fetched StakedSui objects, attempt to calculate the rewards and create a
84            // DelegatedStake for each.
85            delegated_stakes.extend(
86                self.try_create_delegated_stake_info(&response.objects)
87                    .await?,
88            );
89
90            // If there are no more pages then we can break, otherwise update the page_token for
91            // the next request
92            if response.next_page_token.is_none() {
93                break;
94            } else {
95                list_request.page_token = response.next_page_token;
96            }
97        }
98
99        Ok(delegated_stakes)
100    }
101
102    async fn try_create_delegated_stake_info(
103        &mut self,
104        maybe_staked_sui: &[Object],
105    ) -> Result<Vec<DelegatedStake>> {
106        let staked_suis = maybe_staked_sui
107            .iter()
108            .map(|o| {
109                o.contents()
110                    .deserialize::<StakedSui>()
111                    .map_err(Into::into)
112                    .map_err(tonic::Status::from_error)
113            })
114            .collect::<Result<Vec<StakedSui>>>()?;
115
116        let ids = staked_suis.iter().map(|s| s.id).collect::<Vec<_>>();
117        let pool_ids = staked_suis.iter().map(|s| s.pool_id).collect::<Vec<_>>();
118
119        let rewards = self.calculate_rewards(&ids).await?;
120        let validator_addresses = self.get_validator_address_by_pool_id(&pool_ids).await?;
121
122        Ok(staked_suis
123            .into_iter()
124            .zip(rewards)
125            .zip(validator_addresses)
126            .map(
127                |((staked_sui, (_id, rewards)), (_pool_id, validator_address))| DelegatedStake {
128                    staked_sui_id: staked_sui.id,
129                    validator_address,
130                    staking_pool: staked_sui.pool_id,
131                    activation_epoch: staked_sui.stake_activation_epoch,
132                    principal: staked_sui.principal,
133                    rewards,
134                },
135            )
136            .collect())
137    }
138
139    async fn calculate_rewards(
140        &mut self,
141        staked_sui_ids: &[Address],
142    ) -> Result<Vec<(Address, u64)>> {
143        let mut ptb = ProgrammableTransaction::default()
144            .with_inputs(vec![Input::default().with_object_id("0x5")]);
145        let system_object = Argument::new_input(0);
146
147        for id in staked_sui_ids {
148            let staked_sui = Argument::new_input(ptb.inputs.len() as u16);
149
150            ptb.inputs.push(Input::default().with_object_id(id));
151
152            ptb.commands.push(
153                MoveCall::default()
154                    .with_package("0x3")
155                    .with_module("sui_system")
156                    .with_function("calculate_rewards")
157                    .with_arguments(vec![system_object, staked_sui])
158                    .into(),
159            );
160        }
161
162        let transaction = Transaction::default().with_kind(ptb).with_sender("0x0");
163
164        let resp = self
165            .execution_client()
166            .simulate_transaction(
167                SimulateTransactionRequest::new(transaction)
168                    .with_read_mask(FieldMask::from_paths([
169                        "command_outputs.return_values.value",
170                        "transaction.effects.status",
171                    ]))
172                    .with_checks(TransactionChecks::Disabled),
173            )
174            .await?
175            .into_inner();
176
177        if !resp.transaction().effects().status().success() {
178            return Err(tonic::Status::from_error(
179                "transaction execution failed".into(),
180            ));
181        }
182
183        if staked_sui_ids.len() != resp.command_outputs.len() {
184            return Err(tonic::Status::from_error(
185                "missing transaction command_outputs".into(),
186            ));
187        }
188
189        let mut rewards = Vec::with_capacity(staked_sui_ids.len());
190
191        for (id, output) in staked_sui_ids.iter().zip(resp.command_outputs) {
192            let bcs_rewards = output
193                .return_values
194                .first()
195                .and_then(|o| o.value_opt())
196                .ok_or_else(|| tonic::Status::from_error("missing bcs".into()))?;
197
198            let reward =
199                if bcs_rewards.name() == "u64" && bcs_rewards.value().len() == size_of::<u64>() {
200                    u64::from_le_bytes(bcs_rewards.value().try_into().unwrap())
201                } else {
202                    return Err(tonic::Status::from_error("missing rewards".into()));
203                };
204            rewards.push((*id, reward));
205        }
206
207        Ok(rewards)
208    }
209
210    async fn get_validator_address_by_pool_id(
211        &mut self,
212        pool_ids: &[Address],
213    ) -> Result<Vec<(Address, Address)>> {
214        let mut ptb = ProgrammableTransaction::default()
215            .with_inputs(vec![Input::default().with_object_id("0x5")]);
216        let system_object = Argument::new_input(0);
217
218        for id in pool_ids {
219            let pool_id = Argument::new_input(ptb.inputs.len() as u16);
220
221            ptb.inputs
222                .push(Input::default().with_pure(id.into_inner().to_vec()));
223
224            ptb.commands.push(
225                MoveCall::default()
226                    .with_package("0x3")
227                    .with_module("sui_system")
228                    .with_function("validator_address_by_pool_id")
229                    .with_arguments(vec![system_object, pool_id])
230                    .into(),
231            );
232        }
233
234        let transaction = Transaction::default().with_kind(ptb).with_sender("0x0");
235
236        let resp = self
237            .execution_client()
238            .simulate_transaction(
239                SimulateTransactionRequest::new(transaction)
240                    .with_read_mask(FieldMask::from_paths([
241                        "command_outputs.return_values.value",
242                        "transaction.effects.status",
243                    ]))
244                    .with_checks(TransactionChecks::Disabled),
245            )
246            .await?
247            .into_inner();
248
249        if !resp.transaction().effects().status().success() {
250            return Err(tonic::Status::from_error(
251                "transaction execution failed".into(),
252            ));
253        }
254
255        if pool_ids.len() != resp.command_outputs.len() {
256            return Err(tonic::Status::from_error(
257                "missing transaction command_outputs".into(),
258            ));
259        }
260
261        let mut addresses = Vec::with_capacity(pool_ids.len());
262
263        for (id, output) in pool_ids.iter().zip(resp.command_outputs) {
264            let validator_address = output
265                .return_values
266                .first()
267                .and_then(|o| o.value_opt())
268                .ok_or_else(|| tonic::Status::from_error("missing bcs".into()))?;
269
270            let address = if validator_address.name() == "address"
271                && validator_address.value().len() == Address::LENGTH
272            {
273                Address::from_bytes(validator_address.value())
274                    .map_err(|e| tonic::Status::from_error(e.into()))?
275            } else {
276                return Err(tonic::Status::from_error("missing address".into()));
277            };
278            addresses.push((*id, address));
279        }
280
281        Ok(addresses)
282    }
283}