sui_rosetta/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
28/// This lib implements the Mesh online and offline server defined by the [Mesh API Spec](https://docs.cdp.coinbase.com/mesh/mesh-api-spec/api-reference)
29mod 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        // Online endpoints
66        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        // Online endpoints
100        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}