iota_rest_api/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::sync::Arc;
6
7use axum::{Router, response::Redirect, routing::get};
8use iota_network_stack::callback::CallbackLayer;
9use iota_types::{storage::RestStateReader, transaction_executor::TransactionExecutor};
10use openapi::ApiEndpoint;
11use reader::StateReader;
12use tap::Pipe;
13
14pub mod accept;
15mod accounts;
16mod checkpoints;
17pub mod client;
18mod coins;
19mod committee;
20pub mod content_type;
21mod epochs;
22mod error;
23mod health;
24mod info;
25mod metrics;
26mod objects;
27pub mod openapi;
28mod reader;
29mod response;
30mod system;
31pub mod transactions;
32pub mod types;
33
34pub use client::Client;
35pub use error::{RestError, Result};
36pub use iota_types::full_checkpoint_content::{CheckpointData, CheckpointTransaction};
37pub use metrics::RestMetrics;
38pub use transactions::ExecuteTransactionQueryParameters;
39
40pub const TEXT_PLAIN_UTF_8: &str = "text/plain; charset=utf-8";
41pub const APPLICATION_BCS: &str = "application/bcs";
42pub const APPLICATION_JSON: &str = "application/json";
43
44#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
45#[serde(rename_all = "lowercase")]
46pub enum Direction {
47    Ascending,
48    Descending,
49}
50
51impl Direction {
52    pub fn is_descending(self) -> bool {
53        matches!(self, Self::Descending)
54    }
55}
56
57#[derive(Debug)]
58pub struct Page<T, C> {
59    pub entries: response::ResponseContent<Vec<T>>,
60    pub cursor: Option<C>,
61}
62
63pub const DEFAULT_PAGE_SIZE: usize = 50;
64pub const MAX_PAGE_SIZE: usize = 100;
65
66impl<T: serde::Serialize, C: std::fmt::Display> axum::response::IntoResponse for Page<T, C> {
67    fn into_response(self) -> axum::response::Response {
68        let cursor = self
69            .cursor
70            .map(|cursor| [(crate::types::X_IOTA_CURSOR, cursor.to_string())]);
71
72        (cursor, self.entries).into_response()
73    }
74}
75
76const ENDPOINTS: &[&dyn ApiEndpoint<RestService>] = &[
77    // stable APIs
78    &info::GetNodeInfo,
79    &health::HealthCheck,
80    &checkpoints::ListCheckpoints,
81    &checkpoints::GetCheckpoint,
82    // unstable APIs
83    &accounts::ListAccountObjects,
84    &objects::GetObject,
85    &objects::GetObjectWithVersion,
86    &objects::ListDynamicFields,
87    &checkpoints::GetFullCheckpoint,
88    &checkpoints::ListFullCheckpoints,
89    &transactions::GetTransaction,
90    &transactions::ListTransactions,
91    &committee::GetCommittee,
92    &committee::GetLatestCommittee,
93    &system::GetSystemStateSummary,
94    &system::GetCurrentProtocolConfig,
95    &system::GetProtocolConfig,
96    &system::GetGasInfo,
97    &transactions::ExecuteTransaction,
98    &transactions::SimulateTransaction,
99    &transactions::ResolveTransaction,
100    &coins::GetCoinInfo,
101    &epochs::GetEpochLastCheckpoint,
102];
103
104#[derive(Clone)]
105pub struct RestService {
106    reader: StateReader,
107    executor: Option<Arc<dyn TransactionExecutor>>,
108    chain_id: iota_types::digests::ChainIdentifier,
109    software_version: &'static str,
110    metrics: Option<Arc<RestMetrics>>,
111    config: Config,
112}
113
114impl axum::extract::FromRef<RestService> for StateReader {
115    fn from_ref(input: &RestService) -> Self {
116        input.reader.clone()
117    }
118}
119
120impl axum::extract::FromRef<RestService> for Option<Arc<dyn TransactionExecutor>> {
121    fn from_ref(input: &RestService) -> Self {
122        input.executor.clone()
123    }
124}
125
126impl RestService {
127    pub fn new(reader: Arc<dyn RestStateReader>, software_version: &'static str) -> Self {
128        let chain_id = reader.get_chain_identifier().unwrap();
129        Self {
130            reader: StateReader::new(reader),
131            executor: None,
132            chain_id,
133            software_version,
134            metrics: None,
135            config: Config::default(),
136        }
137    }
138
139    pub fn new_without_version(reader: Arc<dyn RestStateReader>) -> Self {
140        Self::new(reader, "unknown")
141    }
142
143    pub fn with_config(&mut self, config: Config) {
144        self.config = config;
145    }
146
147    pub fn with_executor(&mut self, executor: Arc<dyn TransactionExecutor + Send + Sync>) {
148        self.executor = Some(executor);
149    }
150
151    pub fn with_metrics(&mut self, metrics: RestMetrics) {
152        self.metrics = Some(Arc::new(metrics));
153    }
154
155    pub fn chain_id(&self) -> iota_types::digests::ChainIdentifier {
156        self.chain_id
157    }
158
159    pub fn software_version(&self) -> &'static str {
160        self.software_version
161    }
162
163    pub fn into_router(self) -> Router {
164        let metrics = self.metrics.clone();
165
166        let mut api = openapi::Api::new(info(self.software_version()));
167
168        api.register_endpoints(
169            ENDPOINTS
170                .iter()
171                .copied()
172                .filter(|endpoint| endpoint.stable() || self.config.enable_unstable_apis()),
173        );
174
175        Router::new()
176            .nest("/api/v1/", api.to_router().with_state(self.clone()))
177            .route("/api/v1", get(|| async { Redirect::permanent("/api/v1/") }))
178            .layer(axum::middleware::map_response_with_state(
179                self,
180                response::append_info_headers,
181            ))
182            .pipe(|router| {
183                if let Some(metrics) = metrics {
184                    router.layer(CallbackLayer::new(
185                        metrics::RestMetricsMakeCallbackHandler::new(metrics),
186                    ))
187                } else {
188                    router
189                }
190            })
191    }
192
193    pub async fn start_service(self, socket_address: std::net::SocketAddr) {
194        let listener = tokio::net::TcpListener::bind(socket_address).await.unwrap();
195        axum::serve(listener, self.into_router()).await.unwrap();
196    }
197}
198
199fn info(version: &'static str) -> openapiv3::v3_1::Info {
200    use openapiv3::v3_1::{Contact, License};
201
202    openapiv3::v3_1::Info {
203        title: "IOTA Node API".to_owned(),
204        description: Some("REST API for interacting with the IOTA Blockchain".to_owned()),
205        contact: Some(Contact {
206            name: Some("IOTA Foundation".to_owned()),
207            url: Some("https://github.com/iotaledger/iota".to_owned()),
208            ..Default::default()
209        }),
210        license: Some(License {
211            name: "Apache 2.0".to_owned(),
212            url: Some("https://www.apache.org/licenses/LICENSE-2.0.html".to_owned()),
213            ..Default::default()
214        }),
215        version: version.to_owned(),
216        ..Default::default()
217    }
218}
219
220#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
221#[serde(rename_all = "kebab-case")]
222pub struct Config {
223    /// Enable serving of unstable APIs
224    ///
225    /// Defaults to `false`, with unstable APIs being disabled
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub enable_unstable_apis: Option<bool>,
228
229    // Only include this till we have another field that isn't set with a non-default value for
230    // testing
231    #[doc(hidden)]
232    #[serde(skip)]
233    pub _hidden: (),
234}
235
236impl Config {
237    pub fn enable_unstable_apis(&self) -> bool {
238        // TODO
239        // Until the rest service as a whole is "stabilized" with a sane set of default
240        // stable apis, have the default be to enable all apis
241        self.enable_unstable_apis.unwrap_or(true)
242    }
243}
244
245mod _schemars {
246    use schemars::{
247        JsonSchema,
248        schema::{InstanceType, Metadata, SchemaObject},
249    };
250
251    pub(crate) struct U64;
252
253    impl JsonSchema for U64 {
254        fn schema_name() -> String {
255            "u64".to_owned()
256        }
257
258        fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
259            SchemaObject {
260                metadata: Some(Box::new(Metadata {
261                    description: Some("Radix-10 encoded 64-bit unsigned integer".to_owned()),
262                    ..Default::default()
263                })),
264                instance_type: Some(InstanceType::String.into()),
265                format: Some("u64".to_owned()),
266                ..Default::default()
267            }
268            .into()
269        }
270
271        fn is_referenceable() -> bool {
272            false
273        }
274    }
275}
276
277#[cfg(test)]
278mod test {
279    use super::*;
280
281    #[test]
282    fn openapi_spec() {
283        const OPENAPI_SPEC_FILE: &str =
284            concat!(env!("CARGO_MANIFEST_DIR"), "/openapi/openapi.json");
285
286        let openapi = {
287            let mut api = openapi::Api::new(info("unknown"));
288
289            api.register_endpoints(ENDPOINTS.iter().copied());
290            api.openapi()
291        };
292
293        let mut actual = serde_json::to_string_pretty(&openapi).unwrap();
294        actual.push('\n');
295
296        // Update the expected format
297        if std::env::var_os("UPDATE").is_some() {
298            std::fs::write(OPENAPI_SPEC_FILE, &actual).unwrap();
299        }
300
301        let expected = std::fs::read_to_string(OPENAPI_SPEC_FILE).unwrap();
302
303        let diff = diffy::create_patch(&expected, &actual);
304
305        if !diff.hunks().is_empty() {
306            let formatter = if std::io::IsTerminal::is_terminal(&std::io::stderr()) {
307                diffy::PatchFormatter::new().with_color()
308            } else {
309                diffy::PatchFormatter::new()
310            };
311            let header = "Generated and checked-in openapi spec does not match. \
312                          Re-run with `UPDATE=1` to update expected format";
313            panic!("{header}\n\n{}", formatter.fmt_patch(&diff));
314        }
315    }
316
317    #[tokio::test]
318    async fn openapi_explorer() {
319        // Unless env var is set, just early return
320        if std::env::var_os("OPENAPI_EXPLORER").is_none() {
321            return;
322        }
323
324        let openapi = {
325            let mut api = openapi::Api::new(info("unknown"));
326            api.register_endpoints(ENDPOINTS.to_owned());
327            api.openapi()
328        };
329
330        let router = openapi::OpenApiDocument::new(openapi).into_router();
331
332        let listener = tokio::net::TcpListener::bind("127.0.0.1:8000")
333            .await
334            .unwrap();
335        axum::serve(listener, router).await.unwrap();
336    }
337}