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