sui_rpc/client/
staking_rewards.rs1use 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 pub staked_sui_id: Address,
23 pub validator_address: Address,
25 pub staking_pool: Address,
27 pub activation_epoch: u64,
29 pub principal: u64,
31 pub rewards: u64,
33}
34
35#[derive(serde::Deserialize, Debug)]
36struct StakedSui {
37 id: Address,
38 pool_id: Address,
40 stake_activation_epoch: u64,
42 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 delegated_stakes.extend(
89 self.try_create_delegated_stake_info(&response.objects)
90 .await?,
91 );
92
93 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() .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}