1use std::future::Future;
5use std::sync::Arc;
6use std::time::Duration;
7
8use anyhow::Context;
9use anyhow::anyhow;
10use prometheus::Registry;
11use sui_indexer_alt_consistent_api::proto::rpc::consistent::v1alpha::consistent_service_client::ConsistentServiceClient;
12use sui_types::base_types::ObjectDigest;
13use sui_types::base_types::ObjectID;
14use sui_types::base_types::ObjectRef;
15use sui_types::base_types::SequenceNumber;
16use tonic::transport::Channel;
17use tracing::instrument;
18use url::Url;
19
20pub use sui_indexer_alt_consistent_api::proto::rpc::consistent::v1alpha as proto;
21
22use crate::metrics::ConsistentReaderMetrics;
23
24#[derive(clap::Args, Debug, Clone, Default)]
25pub struct ConsistentReaderArgs {
26 #[arg(long)]
28 pub consistent_store_url: Option<Url>,
29
30 #[arg(long)]
32 pub consistent_store_statement_timeout_ms: Option<u64>,
33}
34
35#[derive(Clone)]
37pub struct ConsistentReader {
38 client: Option<Client>,
39 timeout: Option<Duration>,
40 metrics: Arc<ConsistentReaderMetrics>,
41}
42
43pub struct Page<T> {
45 pub results: Vec<Edge<T>>,
46 pub has_previous_page: bool,
47 pub has_next_page: bool,
48}
49
50pub struct Edge<T> {
53 pub token: Vec<u8>,
54 pub value: T,
55}
56
57type Client = ConsistentServiceClient<Channel>;
58
59#[derive(thiserror::Error, Debug)]
60pub enum Error {
61 #[error(transparent)]
62 Internal(#[from] anyhow::Error),
63
64 #[error("{}", .0.message())]
65 OutOfRange(#[source] tonic::Status),
66
67 #[error("Consistent store client not configured")]
68 NotConfigured,
69}
70
71impl ConsistentReaderArgs {
72 pub fn statement_timeout(&self) -> Option<Duration> {
73 self.consistent_store_statement_timeout_ms
74 .map(Duration::from_millis)
75 }
76}
77
78impl ConsistentReader {
79 pub async fn new(
80 prefix: Option<&str>,
81 args: ConsistentReaderArgs,
82 registry: &Registry,
83 ) -> Result<Self, Error> {
84 let client = if let Some(url) = &args.consistent_store_url {
85 let mut endpoint = Channel::from_shared(url.to_string())
86 .context("Failed to create channel for gRPC endpoint")?;
87
88 if let Some(timeout) = args.statement_timeout() {
89 endpoint = endpoint.timeout(timeout);
90 }
91
92 let channel = endpoint.connect_lazy();
93
94 Some(ConsistentServiceClient::new(channel))
95 } else {
96 None
97 };
98
99 let timeout = args.statement_timeout();
100 let metrics = ConsistentReaderMetrics::new(prefix, registry);
101
102 Ok(Self {
103 client,
104 timeout,
105 metrics,
106 })
107 }
108
109 #[instrument(skip(self), level = "debug")]
111 pub async fn available_range(
112 &self,
113 checkpoint: u64,
114 ) -> Result<proto::AvailableRangeResponse, Error> {
115 self.request(
116 "available_range",
117 Some(checkpoint),
118 |mut client, request| async move { client.available_range(request).await },
119 proto::AvailableRangeRequest {},
120 )
121 .await
122 }
123
124 #[instrument(skip(self), level = "debug")]
126 pub async fn batch_get_balances(
127 &self,
128 checkpoint: u64,
129 requests: Vec<(String, String)>,
130 ) -> Result<Vec<proto::Balance>, Error> {
131 let response = self
132 .request(
133 "batch_get_balances",
134 Some(checkpoint),
135 |mut client, request| async move { client.batch_get_balances(request).await },
136 proto::BatchGetBalancesRequest {
137 requests: requests
138 .into_iter()
139 .map(|(owner, coin_type)| proto::GetBalanceRequest {
140 owner: Some(owner),
141 coin_type: Some(coin_type),
142 })
143 .collect(),
144 },
145 )
146 .await?;
147
148 let mut results = vec![];
149 for balance in response.balances {
150 results.push(balance);
151 }
152
153 Ok(results)
154 }
155
156 #[instrument(skip(self), level = "debug")]
158 pub async fn get_balance(
159 &self,
160 checkpoint: Option<u64>,
161 address: String,
162 coin_type: String,
163 ) -> Result<proto::Balance, Error> {
164 self.request(
165 "get_balance",
166 checkpoint,
167 |mut client, request| async move { client.get_balance(request).await },
168 proto::GetBalanceRequest {
169 owner: Some(address),
170 coin_type: Some(coin_type),
171 },
172 )
173 .await
174 }
175
176 #[instrument(skip(self), level = "debug")]
178 pub async fn list_balances(
179 &self,
180 checkpoint: Option<u64>,
181 address: String,
182 page_size: Option<u32>,
183 after_token: Option<Vec<u8>>,
184 before_token: Option<Vec<u8>>,
185 is_from_front: bool,
186 ) -> Result<Page<proto::Balance>, Error> {
187 let response = self
188 .request(
189 "list_balances",
190 checkpoint,
191 |mut client, request| async move { client.list_balances(request).await },
192 proto::ListBalancesRequest {
193 owner: Some(address),
194 page_size,
195 after_token: after_token.map(Into::into),
196 before_token: before_token.map(Into::into),
197 end: if is_from_front {
198 Some(proto::End::Front.into())
199 } else {
200 Some(proto::End::Back.into())
201 },
202 },
203 )
204 .await?;
205
206 let has_next_page = response.has_next_page();
207 let has_previous_page = response.has_previous_page();
208
209 let results = response
210 .balances
211 .into_iter()
212 .map(|b| Edge {
213 token: b.page_token.clone().unwrap_or_default().into(),
214 value: b,
215 })
216 .collect();
217
218 Ok(Page {
219 results,
220 has_next_page,
221 has_previous_page,
222 })
223 }
224
225 #[instrument(skip(self), level = "debug")]
227 pub async fn list_objects_by_type(
228 &self,
229 checkpoint: Option<u64>,
230 object_type: String,
231 page_size: Option<u32>,
232 after_token: Option<Vec<u8>>,
233 before_token: Option<Vec<u8>>,
234 is_from_front: bool,
235 ) -> Result<Page<ObjectRef>, Error> {
236 let response = self
237 .request(
238 "list_objects_by_type",
239 checkpoint,
240 |mut client, request| async move { client.list_objects_by_type(request).await },
241 proto::ListObjectsByTypeRequest {
242 object_type: Some(object_type),
243 page_size,
244 after_token: after_token.map(Into::into),
245 before_token: before_token.map(Into::into),
246 end: if is_from_front {
247 Some(proto::End::Front.into())
248 } else {
249 Some(proto::End::Back.into())
250 },
251 },
252 )
253 .await?;
254
255 let has_next_page = response.has_next_page();
256 let has_previous_page = response.has_previous_page();
257
258 let results = response
259 .objects
260 .into_iter()
261 .map(TryFrom::try_from)
262 .collect::<Result<Vec<_>, _>>()?;
263
264 Ok(Page {
265 results,
266 has_next_page,
267 has_previous_page,
268 })
269 }
270
271 #[instrument(skip(self), level = "debug")]
275 pub async fn list_owned_objects(
276 &self,
277 checkpoint: Option<u64>,
278 kind: proto::owner::OwnerKind,
279 address: Option<String>,
280 object_type: Option<String>,
281 page_size: Option<u32>,
282 after_token: Option<Vec<u8>>,
283 before_token: Option<Vec<u8>>,
284 is_from_front: bool,
285 ) -> Result<Page<ObjectRef>, Error> {
286 let response = self
287 .request(
288 "list_owned_objects",
289 checkpoint,
290 |mut client, request| async move { client.list_owned_objects(request).await },
291 proto::ListOwnedObjectsRequest {
292 owner: Some(proto::Owner {
293 kind: Some(kind.into()),
294 address,
295 }),
296 object_type,
297 page_size,
298 after_token: after_token.map(Into::into),
299 before_token: before_token.map(Into::into),
300 end: if is_from_front {
301 Some(proto::End::Front.into())
302 } else {
303 Some(proto::End::Back.into())
304 },
305 },
306 )
307 .await?;
308
309 let has_next_page = response.has_next_page();
310 let has_previous_page = response.has_previous_page();
311
312 let results = response
313 .objects
314 .into_iter()
315 .map(TryFrom::try_from)
316 .collect::<Result<Vec<_>, _>>()?;
317
318 Ok(Page {
319 results,
320 has_next_page,
321 has_previous_page,
322 })
323 }
324
325 async fn request<I, O, Fut, F>(
326 &self,
327 method: &str,
328 checkpoint: Option<u64>,
329 response: F,
330 input: I,
331 ) -> Result<O, Error>
332 where
333 F: FnOnce(Client, tonic::Request<I>) -> Fut,
334 Fut: Future<Output = Result<tonic::Response<O>, tonic::Status>>,
335 {
336 let Some(client) = self.client.clone() else {
337 return Err(Error::NotConfigured);
338 };
339
340 self.metrics
341 .requests_received
342 .with_label_values(&[method])
343 .inc();
344
345 let _timer = self
346 .metrics
347 .latency
348 .with_label_values(&[method])
349 .start_timer();
350
351 let mut request = tonic::Request::new(input);
352
353 if let Some(timeout) = self.timeout {
354 request.set_timeout(timeout);
355 }
356
357 if let Some(checkpoint) = checkpoint {
358 request.metadata_mut().insert(
359 proto::CHECKPOINT_HEIGHT_METADATA,
360 checkpoint
361 .to_string()
362 .parse()
363 .with_context(|| format!("Invalid checkpoint {checkpoint}"))?,
364 );
365 }
366
367 let response = response(client, request)
368 .await
369 .map(|r| r.into_inner())
370 .map_err(Into::into);
371
372 if response.is_ok() {
373 self.metrics
374 .requests_succeeded
375 .with_label_values(&[method])
376 .inc();
377 } else {
378 self.metrics
379 .requests_failed
380 .with_label_values(&[method])
381 .inc()
382 }
383
384 response
385 }
386}
387
388impl TryFrom<proto::Object> for Edge<ObjectRef> {
389 type Error = Error;
390
391 fn try_from(proto: proto::Object) -> Result<Self, Error> {
392 let object_id: ObjectID = proto
393 .object_id
394 .context("object ID missing")?
395 .parse()
396 .context("invalid object ID")?;
397
398 let digest: ObjectDigest = proto
399 .digest
400 .context("digest missing")?
401 .parse()
402 .context("invalid digest")?;
403
404 let version: SequenceNumber = proto.version.context("version missing")?.into();
405 let token: Vec<u8> = proto.page_token.unwrap_or_default().into();
406
407 Ok(Edge {
408 token,
409 value: (object_id, version, digest),
410 })
411 }
412}
413
414impl From<tonic::Status> for Error {
415 fn from(status: tonic::Status) -> Self {
416 match status.code() {
417 tonic::Code::OutOfRange => Error::OutOfRange(status),
418 _ => Error::Internal(anyhow!(status.code()).context(status.message().to_string())),
419 }
420 }
421}