sui_indexer_alt_reader/
fullnode_client.rs1use anyhow::Context;
5use anyhow::anyhow;
6use async_graphql::dataloader::DataLoader;
7use futures::future::try_join_all;
8use prometheus::Registry;
9use prost_types::FieldMask;
10use sui_rpc::Client;
11use sui_rpc::field::FieldMaskUtil;
12use sui_rpc::proto::sui::rpc::v2 as proto;
13use sui_sdk_types::Address;
14use sui_types::signature::GenericSignature;
15use sui_types::transaction::Transaction;
16use sui_types::transaction::TransactionData;
17use tracing::instrument;
18use url::Url;
19
20use crate::metrics::GrpcMetricsLayer;
21
22const MAX_REWARDS_PER_PTB: usize = 1023;
25
26#[derive(clap::Args, Debug, Clone, Default)]
27pub struct FullnodeArgs {
28 #[clap(long)]
31 pub(crate) fullnode_rpc_url: Option<Url>,
32}
33
34#[derive(Clone)]
36pub struct FullnodeClient {
37 client: Client,
38}
39
40#[derive(thiserror::Error, Debug)]
41pub enum Error {
42 #[error(transparent)]
43 Internal(#[from] anyhow::Error),
44
45 #[error(transparent)]
46 GrpcExecutionError(#[from] tonic::Status),
47}
48
49impl FullnodeArgs {
50 pub fn new(url: Url) -> Self {
51 Self {
52 fullnode_rpc_url: Some(url),
53 }
54 }
55}
56
57impl FullnodeClient {
58 pub async fn new(
59 prefix: Option<&str>,
60 args: FullnodeArgs,
61 registry: &Registry,
62 ) -> Result<Option<Self>, Error> {
63 let Some(url) = args.fullnode_rpc_url else {
64 return Ok(None);
65 };
66
67 let client = Client::new(url.to_string())
68 .context("Failed to create client for gRPC endpoint")?
69 .request_layer(GrpcMetricsLayer::new(
70 prefix.unwrap_or("fullnode"),
71 registry,
72 ));
73
74 Ok(Some(Self { client }))
75 }
76
77 pub fn as_data_loader(&self) -> DataLoader<Self> {
78 DataLoader::new(self.clone(), tokio::spawn)
79 }
80
81 #[instrument(skip(self, transaction_data, signatures, read_mask), level = "debug")]
83 pub async fn execute_transaction(
84 &self,
85 transaction_data: TransactionData,
86 signatures: Vec<GenericSignature>,
87 read_mask: FieldMask,
88 ) -> Result<proto::ExecuteTransactionResponse, Error> {
89 let transaction = Transaction::from_generic_sig_data(transaction_data, signatures);
90
91 let signatures = transaction
92 .inner()
93 .tx_signatures
94 .iter()
95 .map(|signature| {
96 let mut message = proto::UserSignature::default();
97 message.bcs = Some(signature.as_ref().to_vec().into());
98 message
99 })
100 .collect();
101
102 let request = proto::ExecuteTransactionRequest::new({
103 let mut tx = proto::Transaction::default();
104 tx.bcs = Some(
105 proto::Bcs::serialize(&transaction.inner().intent_message.value)
106 .context("Failed to serialize transaction")?,
107 );
108 tx
109 })
110 .with_signatures(signatures)
111 .with_read_mask(read_mask);
112
113 self.client
114 .clone()
115 .execution_client()
116 .execute_transaction(request)
117 .await
118 .map(|r| r.into_inner())
119 .map_err(Into::into)
120 }
121
122 #[instrument(skip(self, transaction, read_mask), level = "debug")]
128 pub async fn simulate_transaction(
129 &self,
130 transaction: proto::Transaction,
131 checks_enabled: bool,
132 do_gas_selection: bool,
133 read_mask: FieldMask,
134 ) -> Result<proto::SimulateTransactionResponse, Error> {
135 use proto::simulate_transaction_request::TransactionChecks;
136
137 let checks = if checks_enabled {
138 TransactionChecks::Enabled
139 } else {
140 TransactionChecks::Disabled
141 };
142
143 let request = proto::SimulateTransactionRequest::new(transaction)
144 .with_read_mask(read_mask)
145 .with_checks(checks)
146 .with_do_gas_selection(do_gas_selection);
147
148 self.client
149 .clone()
150 .execution_client()
151 .simulate_transaction(request)
152 .await
153 .map(|r| r.into_inner())
154 .map_err(Into::into)
155 }
156
157 pub async fn calculate_rewards(&self, staked_sui_ids: &[Address]) -> Result<Vec<u64>, Error> {
160 let batches = staked_sui_ids
161 .chunks(MAX_REWARDS_PER_PTB)
162 .map(|batch| self.calculate_rewards_batch(batch));
163
164 Ok(try_join_all(batches).await?.into_iter().flatten().collect())
165 }
166
167 pub async fn get_validator_address_by_pool_id(
171 &self,
172 pool_ids: &[Address],
173 ) -> Result<Vec<Address>, Error> {
174 let mut ptb = proto::ProgrammableTransaction::default()
175 .with_inputs(vec![proto::Input::default().with_object_id("0x5")]);
176 let system_object = proto::Argument::new_input(0);
177
178 for id in pool_ids {
179 let pool_id = proto::Argument::new_input(ptb.inputs.len() as u16);
180 ptb.inputs
181 .push(proto::Input::default().with_pure(id.into_inner().to_vec()));
182 ptb.commands.push(
183 proto::MoveCall::default()
184 .with_package("0x3")
185 .with_module("sui_system")
186 .with_function("validator_address_by_pool_id")
187 .with_arguments(vec![system_object, pool_id])
188 .into(),
189 );
190 }
191
192 let transaction = proto::Transaction::default()
193 .with_kind(ptb)
194 .with_sender("0x0");
195
196 let resp = self
197 .simulate_transaction(
198 transaction,
199 false,
200 false,
201 FieldMask::from_paths([
202 "command_outputs.return_values.value",
203 "transaction.effects.status",
204 ]),
205 )
206 .await?;
207
208 if !resp.transaction().effects().status().success() {
209 return Err(Error::Internal(anyhow!("transaction execution failed")));
210 }
211
212 if pool_ids.len() != resp.command_outputs.len() {
213 return Err(Error::Internal(anyhow!(
214 "Mismatch between transaction inputs and command_outputs"
215 )));
216 }
217
218 resp.command_outputs
219 .iter()
220 .map(|output| {
221 let bcs_address = output
224 .return_values
225 .first()
226 .and_then(|o| o.value_opt())
227 .ok_or_else(|| Error::Internal(anyhow!("missing address bcs")))?;
228
229 Address::from_bytes(bcs_address.value())
230 .map_err(|e| Error::Internal(anyhow!("Failed to deserialize address: {e}")))
231 })
232 .collect()
233 }
234
235 async fn calculate_rewards_batch(&self, staked_sui_ids: &[Address]) -> Result<Vec<u64>, Error> {
236 let mut ptb = proto::ProgrammableTransaction::default()
237 .with_inputs(vec![proto::Input::default().with_object_id("0x5")]);
238 let system_object = proto::Argument::new_input(0);
239
240 for id in staked_sui_ids {
241 let staked_sui = proto::Argument::new_input(ptb.inputs.len() as u16);
242 ptb.inputs.push(proto::Input::default().with_object_id(id));
243 ptb.commands.push(
244 proto::MoveCall::default()
245 .with_package("0x3")
246 .with_module("sui_system")
247 .with_function("calculate_rewards")
248 .with_arguments(vec![system_object, staked_sui])
249 .into(),
250 );
251 }
252
253 let transaction = proto::Transaction::default()
254 .with_kind(ptb)
255 .with_sender("0x0");
256
257 let resp = self
258 .simulate_transaction(
259 transaction,
260 false,
261 false,
262 FieldMask::from_paths([
263 "command_outputs.return_values.value",
264 "transaction.effects.status",
265 ]),
266 )
267 .await?;
268
269 if !resp.transaction().effects().status().success() {
270 return Err(Error::Internal(anyhow!("transaction execution failed")));
271 }
272
273 if staked_sui_ids.len() != resp.command_outputs.len() {
274 return Err(Error::Internal(anyhow!(
275 "missing transaction command_outputs"
276 )));
277 }
278
279 resp.command_outputs
280 .iter()
281 .map(|output| {
282 let bcs_rewards = output
284 .return_values
285 .first()
286 .and_then(|o| o.value_opt())
287 .ok_or_else(|| Error::Internal(anyhow!("missing rewards bcs")))?;
288
289 bcs::from_bytes::<u64>(bcs_rewards.value())
290 .map_err(|e| Error::Internal(anyhow!("Failed to deserialize rewards: {e}")))
291 })
292 .collect()
293 }
294}
295
296impl From<Error> for crate::error::Error {
297 fn from(e: Error) -> Self {
298 match e {
299 Error::Internal(err) => err.into(),
300 Error::GrpcExecutionError(status) => status.into(),
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 async fn fn_client(url: Option<&str>) -> Result<Option<FullnodeClient>, Error> {
310 let registry = Registry::new();
311 let args = FullnodeArgs {
312 fullnode_rpc_url: url.map(|u| Url::parse(u).unwrap()),
313 };
314 FullnodeClient::new(None, args, ®istry).await
315 }
316
317 #[tokio::test]
318 async fn no_url_means_not_configured() {
319 let client = fn_client(None).await.unwrap();
320 assert!(client.is_none());
321 }
322
323 #[tokio::test]
324 async fn http_url_creates_client() {
325 assert!(
326 fn_client(Some("http://localhost:9000"))
327 .await
328 .unwrap()
329 .is_some()
330 );
331 }
332
333 #[tokio::test]
334 async fn https_url_creates_client() {
335 assert!(
336 fn_client(Some("https://fn.example.com"))
337 .await
338 .unwrap()
339 .is_some()
340 );
341 }
342}