sui_rpc/client/v2/
staking_rewards.rs

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