sui_rpc/client/
staking_rewards.rs1use 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
20const MAX_REWARDS_PER_PTB: usize = 1023;
23
24#[derive(Debug)]
25pub struct DelegatedStake {
26 pub staked_sui_id: Address,
28 pub validator_address: Address,
30 pub staking_pool: Address,
32 pub activation_epoch: u64,
34 pub principal: u64,
36 pub rewards: u64,
38}
39
40#[derive(serde::Deserialize, Debug)]
41struct StakedSui {
42 id: Address,
43 pool_id: Address,
45 stake_activation_epoch: u64,
47 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 delegated_stakes.extend(
91 self.try_create_delegated_stake_info(&response.objects)
92 .await?,
93 );
94
95 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 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}