1use std::net::SocketAddr;
5use std::num::NonZeroUsize;
6use std::string::ToString;
7use std::sync::Arc;
8
9use axum::routing::post;
10use axum::{Extension, Router};
11use lru::LruCache;
12use move_core_types::language_storage::TypeTag;
13use once_cell::sync::Lazy;
14use tokio::sync::Mutex;
15use tracing::info;
16
17use sui_rpc::client::Client;
18use sui_rpc::proto::sui::rpc::v2::GetCoinInfoRequest;
19use sui_sdk_types::{StructTag, TypeTag as SDKTypeTag};
20
21use crate::errors::Error;
22use crate::errors::Error::MissingMetadata;
23
24pub use crate::errors::Error as RosettaError;
25use crate::state::{CheckpointBlockProvider, OnlineServerContext};
26use crate::types::{Currency, CurrencyMetadata, SuiEnv};
27
28mod account;
30mod block;
31mod construction;
32pub mod errors;
33mod network;
34pub mod operations;
35mod state;
36pub mod types;
37
38pub static SUI: Lazy<Currency> = Lazy::new(|| Currency {
39 symbol: "SUI".to_string(),
40 decimals: 9,
41 metadata: CurrencyMetadata {
42 coin_type: SDKTypeTag::from(StructTag::sui()).to_string(),
43 },
44});
45
46pub struct RosettaOnlineServer {
47 env: SuiEnv,
48 context: OnlineServerContext,
49}
50
51impl RosettaOnlineServer {
52 pub fn new(env: SuiEnv, client: Client) -> Self {
53 let coin_cache = CoinMetadataCache::new(client.clone(), NonZeroUsize::new(1000).unwrap());
54 let blocks = Arc::new(CheckpointBlockProvider::new(
55 client.clone(),
56 coin_cache.clone(),
57 ));
58 Self {
59 env,
60 context: OnlineServerContext::new(client, blocks, coin_cache),
61 }
62 }
63
64 pub async fn serve(self, addr: SocketAddr) {
65 let app = Router::new()
67 .route("/account/balance", post(account::balance))
68 .route("/account/coins", post(account::coins))
69 .route("/block", post(block::block))
70 .route("/block/transaction", post(block::transaction))
71 .route("/construction/submit", post(construction::submit))
72 .route("/construction/metadata", post(construction::metadata))
73 .route("/network/status", post(network::status))
74 .route("/network/list", post(network::list))
75 .route("/network/options", post(network::options))
76 .layer(Extension(self.env))
77 .with_state(self.context);
78
79 let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
80
81 info!(
82 "Sui Rosetta online server listening on {}",
83 listener.local_addr().unwrap()
84 );
85 axum::serve(listener, app).await.unwrap();
86 }
87}
88
89pub struct RosettaOfflineServer {
90 env: SuiEnv,
91}
92
93impl RosettaOfflineServer {
94 pub fn new(env: SuiEnv) -> Self {
95 Self { env }
96 }
97
98 pub async fn serve(self, addr: SocketAddr) {
99 let app = Router::new()
101 .route("/construction/derive", post(construction::derive))
102 .route("/construction/payloads", post(construction::payloads))
103 .route("/construction/combine", post(construction::combine))
104 .route("/construction/preprocess", post(construction::preprocess))
105 .route("/construction/hash", post(construction::hash))
106 .route("/construction/parse", post(construction::parse))
107 .route("/network/list", post(network::list))
108 .route("/network/options", post(network::options))
109 .layer(Extension(self.env));
110 let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
111
112 info!(
113 "Sui Rosetta offline server listening on {}",
114 listener.local_addr().unwrap()
115 );
116 axum::serve(listener, app).await.unwrap();
117 }
118}
119
120#[derive(Clone)]
121pub struct CoinMetadataCache {
122 client: Client,
123 metadata: Arc<Mutex<LruCache<TypeTag, Currency>>>,
124}
125
126impl CoinMetadataCache {
127 pub fn new(client: Client, size: NonZeroUsize) -> Self {
128 Self {
129 client,
130 metadata: Arc::new(Mutex::new(LruCache::new(size))),
131 }
132 }
133
134 pub async fn get_currency(&self, type_tag: &TypeTag) -> Result<Currency, Error> {
135 let mut cache = self.metadata.lock().await;
136 if !cache.contains(type_tag) {
137 let mut client = self.client.clone();
138 let request = GetCoinInfoRequest::default().with_coin_type(type_tag.to_string());
139
140 let response = client
141 .state_client()
142 .get_coin_info(request)
143 .await?
144 .into_inner();
145
146 let (symbol, decimals) = response
147 .metadata
148 .and_then(|m| Some((m.symbol?, m.decimals?)))
149 .ok_or(MissingMetadata)?;
150
151 let ccy = Currency {
152 symbol,
153 decimals: decimals as u64,
154 metadata: CurrencyMetadata {
155 coin_type: type_tag.clone().to_canonical_string(true),
156 },
157 };
158 cache.push(type_tag.clone(), ccy);
159 }
160 cache.get(type_tag).cloned().ok_or(MissingMetadata)
161 }
162
163 pub async fn len(&self) -> usize {
164 self.metadata.lock().await.len()
165 }
166
167 pub async fn is_empty(&self) -> bool {
168 self.metadata.lock().await.is_empty()
169 }
170
171 pub async fn contains(&self, type_tag: &TypeTag) -> bool {
172 self.metadata.lock().await.contains(type_tag)
173 }
174}