1use async_trait::async_trait;
5use chrono::DateTime;
6use futures::stream::{self, StreamExt, TryStreamExt};
7use prost_types::FieldMask;
8use std::str::FromStr;
9use std::sync::Arc;
10use sui_rpc::client::Client as GrpcClient;
11use sui_rpc::field::FieldMaskUtil;
12use sui_rpc::proto::sui::rpc::v2::{
13 Checkpoint, GetCheckpointRequest, GetServiceInfoRequest, get_checkpoint_request,
14};
15use sui_types::base_types::TransactionDigest;
16use sui_types::digests::CheckpointDigest;
17use sui_types::messages_checkpoint::CheckpointSequenceNumber;
18
19use sui_types::digests::ChainIdentifier;
20
21use crate::operations::Operations;
22use crate::types::{
23 Block, BlockHash, BlockIdentifier, BlockResponse, Transaction, TransactionIdentifier,
24};
25use crate::{CoinMetadataCache, Error};
26
27#[derive(Clone)]
28pub struct OnlineServerContext {
29 pub client: GrpcClient,
30 pub coin_metadata_cache: CoinMetadataCache,
31 pub chain_id: ChainIdentifier,
32 block_provider: Arc<dyn BlockProvider + Send + Sync>,
33}
34
35impl OnlineServerContext {
36 pub fn new(
37 client: GrpcClient,
38 block_provider: Arc<dyn BlockProvider + Send + Sync>,
39 coin_metadata_cache: CoinMetadataCache,
40 chain_id: ChainIdentifier,
41 ) -> Self {
42 Self {
43 client,
44 block_provider,
45 coin_metadata_cache,
46 chain_id,
47 }
48 }
49
50 pub fn blocks(&self) -> &(dyn BlockProvider + Sync + Send) {
51 &*self.block_provider
52 }
53}
54
55#[async_trait]
56pub trait BlockProvider {
57 async fn get_block_by_index(&self, index: u64) -> Result<BlockResponse, Error>;
58 async fn get_block_by_hash(&self, hash: BlockHash) -> Result<BlockResponse, Error>;
59 async fn current_block(&self) -> Result<BlockResponse, Error>;
60 async fn genesis_block_identifier(&self) -> Result<BlockIdentifier, Error>;
61 async fn oldest_block_identifier(&self) -> Result<BlockIdentifier, Error>;
62 async fn current_block_identifier(&self) -> Result<BlockIdentifier, Error>;
63}
64
65#[derive(Clone)]
66pub struct CheckpointBlockProvider {
67 client: GrpcClient,
68 coin_metadata_cache: CoinMetadataCache,
69}
70
71#[async_trait]
72impl BlockProvider for CheckpointBlockProvider {
73 async fn get_block_by_index(&self, index: u64) -> Result<BlockResponse, Error> {
74 let request = GetCheckpointRequest::by_sequence_number(index).with_read_mask(
75 FieldMask::from_paths([
76 "sequence_number",
77 "digest",
78 "summary.sequence_number",
79 "summary.previous_digest",
80 "summary.timestamp",
81 "transactions.digest",
82 "transactions.transaction.sender",
83 "transactions.transaction.gas_payment",
84 "transactions.transaction.kind",
85 "transactions.effects.gas_object",
86 "transactions.effects.gas_used",
87 "transactions.effects.status",
88 "transactions.balance_changes",
89 "transactions.events.events.event_type",
90 "transactions.events.events.json",
91 ]),
92 );
93
94 let mut client = self.client.clone();
95 let response = client
96 .ledger_client()
97 .get_checkpoint(request)
98 .await
99 .map_err(|e| Error::from(anyhow::anyhow!("Failed to get checkpoint: {}", e)))?
100 .into_inner();
101
102 let checkpoint = response
103 .checkpoint
104 .ok_or_else(|| Error::DataError("Checkpoint not found".to_string()))?;
105
106 self.create_block_response(checkpoint).await
107 }
108
109 async fn get_block_by_hash(&self, hash: BlockHash) -> Result<BlockResponse, Error> {
110 let mut request = GetCheckpointRequest::default().with_read_mask(FieldMask::from_paths([
111 "sequence_number",
112 "digest",
113 "summary.sequence_number",
114 "summary.previous_digest",
115 "summary.timestamp",
116 "transactions.digest",
117 "transactions.transaction.sender",
118 "transactions.transaction.gas_payment",
119 "transactions.transaction.kind",
120 "transactions.effects.gas_object",
121 "transactions.effects.gas_used",
122 "transactions.effects.status",
123 "transactions.balance_changes",
124 "transactions.events.events.event_type",
125 "transactions.events.events.json",
126 ]));
127 request.checkpoint_id = Some(get_checkpoint_request::CheckpointId::Digest(
128 hash.to_string(),
129 ));
130
131 let mut client = self.client.clone();
132 let response = client
133 .ledger_client()
134 .get_checkpoint(request)
135 .await?
136 .into_inner();
137 let checkpoint = response
138 .checkpoint
139 .ok_or_else(|| Error::DataError("Checkpoint not found".to_string()))?;
140
141 self.create_block_response(checkpoint).await
142 }
143
144 async fn current_block(&self) -> Result<BlockResponse, Error> {
145 let request = GetCheckpointRequest::latest().with_read_mask(FieldMask::from_paths([
146 "sequence_number",
147 "digest",
148 "summary.sequence_number",
149 "summary.previous_digest",
150 "summary.timestamp",
151 "transactions.digest",
152 "transactions.transaction.sender",
153 "transactions.transaction.gas_payment",
154 "transactions.transaction.kind",
155 "transactions.effects.gas_object",
156 "transactions.effects.gas_used",
157 "transactions.effects.status",
158 "transactions.balance_changes",
159 "transactions.events.events.event_type",
160 "transactions.events.events.json",
161 ]));
162
163 let mut client = self.client.clone();
164 let response = client
165 .ledger_client()
166 .get_checkpoint(request)
167 .await?
168 .into_inner();
169
170 let checkpoint = response
171 .checkpoint
172 .ok_or_else(|| Error::DataError("Checkpoint not found".to_string()))?;
173
174 self.create_block_response(checkpoint).await
175 }
176
177 async fn genesis_block_identifier(&self) -> Result<BlockIdentifier, Error> {
178 let response = self
179 .client
180 .clone()
181 .ledger_client()
182 .get_service_info(GetServiceInfoRequest::default())
183 .await?
184 .into_inner();
185 let chain_id = response
186 .chain_id
187 .ok_or_else(|| Error::DataError("Missing chain_id".to_string()))?;
188 let hash = CheckpointDigest::from_str(&chain_id)?;
189 Ok(BlockIdentifier { index: 0, hash })
190 }
191
192 async fn oldest_block_identifier(&self) -> Result<BlockIdentifier, Error> {
193 let response = self
194 .client
195 .clone()
196 .ledger_client()
197 .get_service_info(GetServiceInfoRequest::default())
198 .await?
199 .into_inner();
200 let lowest = response
201 .lowest_available_checkpoint
202 .ok_or_else(|| Error::DataError("Missing lowest_available_checkpoint".to_string()))?;
203 self.create_block_identifier(lowest).await
204 }
205
206 async fn current_block_identifier(&self) -> Result<BlockIdentifier, Error> {
207 let request = GetCheckpointRequest::latest()
208 .with_read_mask(FieldMask::from_paths(["sequence_number", "digest"]));
209
210 let response = self
211 .client
212 .clone()
213 .ledger_client()
214 .get_checkpoint(request)
215 .await?
216 .into_inner();
217
218 let checkpoint = response
219 .checkpoint
220 .ok_or_else(|| Error::DataError("Missing checkpoint".to_string()))?;
221
222 Ok(BlockIdentifier {
223 index: checkpoint.sequence_number(),
224 hash: CheckpointDigest::from_str(checkpoint.digest())?,
225 })
226 }
227}
228
229impl CheckpointBlockProvider {
230 pub fn new(client: GrpcClient, coin_metadata_cache: CoinMetadataCache) -> Self {
231 Self {
232 client,
233 coin_metadata_cache,
234 }
235 }
236
237 async fn create_block_response(&self, checkpoint: Checkpoint) -> Result<BlockResponse, Error> {
238 let summary = checkpoint.summary();
239 let index = summary.sequence_number();
240 let hash = CheckpointDigest::from_str(checkpoint.digest())?;
241 let previous_hash = if index == 0 {
243 hash
244 } else {
245 CheckpointDigest::from_str(summary.previous_digest())?
246 };
247 let timestamp_ms = summary
248 .timestamp
249 .ok_or_else(|| Error::DataError("Checkpoint timestamp is missing".to_string()))
250 .and_then(|ts| {
251 DateTime::from_timestamp(ts.seconds, ts.nanos as u32)
252 .ok_or_else(|| Error::DataError(format!("Invalid timestamp: {}", ts)))
253 })?
254 .timestamp_millis() as u64;
255
256 let transactions: Vec<Transaction> = stream::iter(checkpoint.transactions)
257 .map(|executed_tx| async move {
258 let digest = TransactionDigest::from_str(executed_tx.digest())?;
259 Ok::<_, Error>(Transaction {
260 transaction_identifier: TransactionIdentifier { hash: digest },
261 operations: Operations::try_from_executed_transaction(
264 executed_tx,
265 &self.coin_metadata_cache,
266 )
267 .await?,
268 related_transactions: vec![],
269 metadata: None,
270 })
271 })
272 .buffer_unordered(10)
276 .try_collect()
277 .await?;
278
279 let parent_block_identifier = if index == 0 {
280 BlockIdentifier { index, hash }
282 } else {
283 BlockIdentifier {
284 index: index - 1,
285 hash: previous_hash,
286 }
287 };
288
289 Ok(BlockResponse {
290 block: Block {
291 block_identifier: BlockIdentifier { index, hash },
292 parent_block_identifier,
293 timestamp: timestamp_ms,
294 transactions,
295 metadata: None,
296 },
297 other_transactions: vec![],
298 })
299 }
300
301 async fn create_block_identifier(
302 &self,
303 seq_number: CheckpointSequenceNumber,
304 ) -> Result<BlockIdentifier, Error> {
305 let grpc_request = GetCheckpointRequest::by_sequence_number(seq_number)
306 .with_read_mask(FieldMask::from_paths(["sequence_number", "digest"]));
307 let mut client = self.client.clone();
308 let response = client
309 .ledger_client()
310 .get_checkpoint(grpc_request)
311 .await?
312 .into_inner();
313
314 let checkpoint = response.checkpoint();
315 let index = checkpoint.sequence_number();
316 let hash = checkpoint.digest();
317
318 Ok(BlockIdentifier {
319 index,
320 hash: CheckpointDigest::from_str(hash)?,
321 })
322 }
323}
324
325#[cfg(test)]
326mod checkpoint_race_tests {
327 use super::*;
328 use crate::types::{
329 AccountBalanceRequest, AccountIdentifier, Currencies, Currency, NetworkIdentifier, SuiEnv,
330 };
331 use axum::extract::State;
332 use axum::{Extension, Json};
333 use axum_extra::extract::WithRejection;
334 use std::marker::PhantomData;
335 use std::num::NonZeroUsize;
336 use std::sync::atomic::{AtomicUsize, Ordering};
337 use sui_rpc::proto::sui::rpc::v2::ledger_service_server::{LedgerService, LedgerServiceServer};
338 use sui_rpc::proto::sui::rpc::v2::state_service_server::{StateService, StateServiceServer};
339 use sui_rpc::proto::sui::rpc::v2::{
340 Balance, CheckpointSummary, GetBalanceRequest, GetBalanceResponse, GetCheckpointResponse,
341 get_checkpoint_request,
342 };
343 use sui_types::base_types::SuiAddress;
344 use tonic::{Request, Response, Status};
345
346 const AHEAD_CHECKPOINT: u64 = 42;
347
348 #[derive(Clone)]
349 struct SplitHeightLedger {
350 latest_requests: Arc<AtomicUsize>,
351 sequence_requests: Arc<AtomicUsize>,
352 }
353
354 fn ahead_checkpoint() -> Checkpoint {
355 let mut summary = CheckpointSummary::default();
356 summary.sequence_number = Some(AHEAD_CHECKPOINT);
357 summary.previous_digest = Some(CheckpointDigest::new([1; 32]).to_string());
358 summary.timestamp = Some(prost_types::Timestamp {
359 seconds: 1,
360 nanos: 0,
361 });
362
363 let mut checkpoint = Checkpoint::default();
364 checkpoint.sequence_number = Some(AHEAD_CHECKPOINT);
365 checkpoint.digest = Some(CheckpointDigest::new([2; 32]).to_string());
366 checkpoint.summary = Some(summary);
367 checkpoint
368 }
369
370 #[tonic::async_trait]
371 impl LedgerService for SplitHeightLedger {
372 async fn get_checkpoint(
373 &self,
374 request: Request<GetCheckpointRequest>,
375 ) -> Result<Response<GetCheckpointResponse>, Status> {
376 match request.into_inner().checkpoint_id {
377 None => {
378 self.latest_requests.fetch_add(1, Ordering::SeqCst);
379 let mut response = GetCheckpointResponse::default();
380 response.checkpoint = Some(ahead_checkpoint());
381 Ok(Response::new(response))
382 }
383 Some(get_checkpoint_request::CheckpointId::SequenceNumber(AHEAD_CHECKPOINT)) => {
384 self.sequence_requests.fetch_add(1, Ordering::SeqCst);
385 Err(Status::not_found(format!(
386 "Checkpoint {AHEAD_CHECKPOINT} not found"
387 )))
388 }
389 Some(checkpoint_id) => Err(Status::invalid_argument(format!(
390 "unexpected checkpoint request: {checkpoint_id:?}"
391 ))),
392 }
393 }
394 }
395
396 #[tonic::async_trait]
397 impl StateService for SplitHeightLedger {
398 async fn get_balance(
399 &self,
400 _request: Request<GetBalanceRequest>,
401 ) -> Result<Response<GetBalanceResponse>, Status> {
402 let mut balance = Balance::default();
403 balance.balance = Some(100);
404 let mut response = GetBalanceResponse::default();
405 response.balance = Some(balance);
406 Ok(Response::new(response))
407 }
408 }
409
410 async fn start_split_height_ledger(
411 ledger: SplitHeightLedger,
412 ) -> (GrpcClient, tokio::task::JoinHandle<()>) {
413 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
414 let address = listener.local_addr().unwrap();
415 let incoming = stream::unfold(listener, |listener| async move {
416 let result = listener.accept().await.map(|(stream, _)| stream);
417 Some((result, listener))
418 });
419 let handle = tokio::spawn(async move {
420 tonic::transport::Server::builder()
421 .add_service(LedgerServiceServer::new(ledger.clone()))
422 .add_service(StateServiceServer::new(ledger))
423 .serve_with_incoming(incoming)
424 .await
425 .unwrap();
426 });
427 let client = GrpcClient::new(format!("http://{address}")).unwrap();
428 (client, handle)
429 }
430
431 #[tokio::test]
432 async fn current_block_uses_single_latest_request() {
433 let latest_requests = Arc::new(AtomicUsize::new(0));
434 let sequence_requests = Arc::new(AtomicUsize::new(0));
435 let ledger = SplitHeightLedger {
436 latest_requests: latest_requests.clone(),
437 sequence_requests: sequence_requests.clone(),
438 };
439 let (client, server) = start_split_height_ledger(ledger).await;
440 let coin_metadata_cache =
441 CoinMetadataCache::new(client.clone(), NonZeroUsize::new(1).unwrap());
442 let provider = CheckpointBlockProvider::new(client, coin_metadata_cache);
443
444 let response = provider.current_block().await.unwrap();
445
446 assert_eq!(response.block.block_identifier.index, AHEAD_CHECKPOINT);
447 assert_eq!(latest_requests.load(Ordering::SeqCst), 1);
448 assert_eq!(sequence_requests.load(Ordering::SeqCst), 0);
449
450 server.abort();
451 }
452
453 #[tokio::test]
454 async fn current_block_identifier_uses_single_latest_request() {
455 let latest_requests = Arc::new(AtomicUsize::new(0));
456 let sequence_requests = Arc::new(AtomicUsize::new(0));
457 let ledger = SplitHeightLedger {
458 latest_requests: latest_requests.clone(),
459 sequence_requests: sequence_requests.clone(),
460 };
461 let (client, server) = start_split_height_ledger(ledger).await;
462 let coin_metadata_cache =
463 CoinMetadataCache::new(client.clone(), NonZeroUsize::new(1).unwrap());
464 let provider = CheckpointBlockProvider::new(client, coin_metadata_cache);
465
466 let identifier = provider.current_block_identifier().await.unwrap();
467
468 assert_eq!(identifier.index, AHEAD_CHECKPOINT);
469 assert_eq!(latest_requests.load(Ordering::SeqCst), 1);
470 assert_eq!(sequence_requests.load(Ordering::SeqCst), 0);
471
472 server.abort();
473 }
474
475 #[tokio::test]
476 async fn account_balance_uses_single_latest_request() {
477 let latest_requests = Arc::new(AtomicUsize::new(0));
478 let sequence_requests = Arc::new(AtomicUsize::new(0));
479 let ledger = SplitHeightLedger {
480 latest_requests: latest_requests.clone(),
481 sequence_requests: sequence_requests.clone(),
482 };
483 let (client, server) = start_split_height_ledger(ledger).await;
484 let coin_metadata_cache =
485 CoinMetadataCache::new(client.clone(), NonZeroUsize::new(1).unwrap());
486 let block_provider = Arc::new(CheckpointBlockProvider::new(
487 client.clone(),
488 coin_metadata_cache.clone(),
489 ));
490 let context = OnlineServerContext::new(
491 client,
492 block_provider,
493 coin_metadata_cache,
494 ChainIdentifier::from(CheckpointDigest::new([3; 32])),
495 );
496 let request = AccountBalanceRequest {
497 network_identifier: NetworkIdentifier {
498 blockchain: "sui".to_string(),
499 network: SuiEnv::LocalNet,
500 },
501 account_identifier: AccountIdentifier {
502 address: SuiAddress::ZERO,
503 sub_account: None,
504 },
505 block_identifier: Default::default(),
506 currencies: Currencies(vec![Currency::default()]),
507 };
508
509 let response = crate::account::balance(
510 State(context),
511 Extension(SuiEnv::LocalNet),
512 WithRejection(Json(request), PhantomData),
513 )
514 .await
515 .unwrap();
516
517 assert_eq!(response.block_identifier.index, AHEAD_CHECKPOINT);
518 assert_eq!(response.balances[0].value, 100);
519 assert_eq!(latest_requests.load(Ordering::SeqCst), 1);
520 assert_eq!(sequence_requests.load(Ordering::SeqCst), 0);
521
522 server.abort();
523 }
524}