1use 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 &info::GetNodeInfo,
79 &health::HealthCheck,
80 &checkpoints::ListCheckpoints,
81 &checkpoints::GetCheckpoint,
82 &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 #[serde(skip_serializing_if = "Option::is_none")]
227 pub enable_unstable_apis: Option<bool>,
228
229 #[doc(hidden)]
232 #[serde(skip)]
233 pub _hidden: (),
234}
235
236impl Config {
237 pub fn enable_unstable_apis(&self) -> bool {
238 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 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 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}