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 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
31/// 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)
32mod 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        // Online endpoints
81        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        // Online endpoints
115        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}