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
51#[derive(Debug)]
52pub struct Page<T, C> {
53 pub entries: response::ResponseContent<Vec<T>>,
54 pub cursor: Option<C>,
55}
56
57pub const DEFAULT_PAGE_SIZE: usize = 50;
58pub const MAX_PAGE_SIZE: usize = 100;
59
60impl<T: serde::Serialize, C: std::fmt::Display> axum::response::IntoResponse for Page<T, C> {
61 fn into_response(self) -> axum::response::Response {
62 let cursor = self
63 .cursor
64 .map(|cursor| [(crate::types::X_IOTA_CURSOR, cursor.to_string())]);
65
66 (cursor, self.entries).into_response()
67 }
68}
69
70const ENDPOINTS: &[&dyn ApiEndpoint<RestService>] = &[
71 &info::GetNodeInfo,
72 &health::HealthCheck,
73 &accounts::ListAccountObjects,
74 &objects::GetObject,
75 &objects::GetObjectWithVersion,
76 &objects::ListDynamicFields,
77 &checkpoints::ListCheckpoints,
78 &checkpoints::GetCheckpoint,
79 &checkpoints::GetCheckpointFull,
80 &transactions::GetTransaction,
81 &transactions::ListTransactions,
82 &committee::GetCommittee,
83 &committee::GetLatestCommittee,
84 &system::GetSystemStateSummary,
85 &system::GetCurrentProtocolConfig,
86 &system::GetProtocolConfig,
87 &system::GetGasInfo,
88 &transactions::ExecuteTransaction,
89 &coins::GetCoinInfo,
90 &epochs::GetEpochLastCheckpoint,
91];
92
93#[derive(Clone)]
94pub struct RestService {
95 reader: StateReader,
96 executor: Option<Arc<dyn TransactionExecutor>>,
97 chain_id: iota_types::digests::ChainIdentifier,
98 software_version: &'static str,
99 metrics: Option<Arc<RestMetrics>>,
100}
101
102impl axum::extract::FromRef<RestService> for StateReader {
103 fn from_ref(input: &RestService) -> Self {
104 input.reader.clone()
105 }
106}
107
108impl axum::extract::FromRef<RestService> for Option<Arc<dyn TransactionExecutor>> {
109 fn from_ref(input: &RestService) -> Self {
110 input.executor.clone()
111 }
112}
113
114impl RestService {
115 pub fn new(reader: Arc<dyn RestStateReader>, software_version: &'static str) -> Self {
116 let chain_id = reader.get_chain_identifier().unwrap();
117 Self {
118 reader: StateReader::new(reader),
119 executor: None,
120 chain_id,
121 software_version,
122 metrics: None,
123 }
124 }
125
126 pub fn new_without_version(reader: Arc<dyn RestStateReader>) -> Self {
127 Self::new(reader, "unknown")
128 }
129
130 pub fn with_executor(&mut self, executor: Arc<dyn TransactionExecutor + Send + Sync>) {
131 self.executor = Some(executor);
132 }
133
134 pub fn with_metrics(&mut self, metrics: RestMetrics) {
135 self.metrics = Some(Arc::new(metrics));
136 }
137
138 pub fn chain_id(&self) -> iota_types::digests::ChainIdentifier {
139 self.chain_id
140 }
141
142 pub fn software_version(&self) -> &'static str {
143 self.software_version
144 }
145
146 pub fn into_router(self) -> Router {
147 let metrics = self.metrics.clone();
148
149 let mut api = openapi::Api::new(info());
150
151 api.register_endpoints(ENDPOINTS.to_owned());
152
153 Router::new()
154 .nest("/api/v1/", api.to_router().with_state(self.clone()))
155 .route("/api/v1", get(|| async { Redirect::permanent("/api/v1/") }))
156 .layer(axum::middleware::map_response_with_state(
157 self,
158 response::append_info_headers,
159 ))
160 .pipe(|router| {
161 if let Some(metrics) = metrics {
162 router.layer(CallbackLayer::new(
163 metrics::RestMetricsMakeCallbackHandler::new(metrics),
164 ))
165 } else {
166 router
167 }
168 })
169 }
170
171 pub async fn start_service(self, socket_address: std::net::SocketAddr) {
172 let listener = tokio::net::TcpListener::bind(socket_address).await.unwrap();
173 axum::serve(listener, self.into_router()).await.unwrap();
174 }
175}
176
177fn info() -> openapiv3::v3_1::Info {
178 use openapiv3::v3_1::{Contact, License};
179
180 openapiv3::v3_1::Info {
181 title: "IOTA Node API".to_owned(),
182 description: Some("REST API for interacting with the IOTA Blockchain".to_owned()),
183 contact: Some(Contact {
184 name: Some("IOTA Foundation".to_owned()),
185 url: Some("https://github.com/iotaledger/iota".to_owned()),
186 ..Default::default()
187 }),
188 license: Some(License {
189 name: "Apache 2.0".to_owned(),
190 url: Some("https://www.apache.org/licenses/LICENSE-2.0.html".to_owned()),
191 ..Default::default()
192 }),
193 version: "0.0.0".to_owned(),
194 ..Default::default()
195 }
196}
197
198mod _schemars {
199 use schemars::{
200 JsonSchema,
201 schema::{InstanceType, Metadata, SchemaObject},
202 };
203
204 pub(crate) struct U64;
205
206 impl JsonSchema for U64 {
207 fn schema_name() -> String {
208 "u64".to_owned()
209 }
210
211 fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
212 SchemaObject {
213 metadata: Some(Box::new(Metadata {
214 description: Some("Radix-10 encoded 64-bit unsigned integer".to_owned()),
215 ..Default::default()
216 })),
217 instance_type: Some(InstanceType::String.into()),
218 format: Some("u64".to_owned()),
219 ..Default::default()
220 }
221 .into()
222 }
223
224 fn is_referenceable() -> bool {
225 false
226 }
227 }
228}
229
230#[cfg(test)]
231mod test {
232 use super::*;
233
234 #[test]
235 fn openapi_spec() {
236 const OPENAPI_SPEC_FILE: &str =
237 concat!(env!("CARGO_MANIFEST_DIR"), "/openapi/openapi.json");
238
239 let openapi = {
240 let mut api = openapi::Api::new(info());
241
242 api.register_endpoints(ENDPOINTS.to_owned());
243 api.openapi()
244 };
245
246 let mut actual = serde_json::to_string_pretty(&openapi).unwrap();
247 actual.push('\n');
248
249 if std::env::var_os("UPDATE").is_some() {
251 std::fs::write(OPENAPI_SPEC_FILE, &actual).unwrap();
252 }
253
254 let expected = std::fs::read_to_string(OPENAPI_SPEC_FILE).unwrap();
255
256 let diff = diffy::create_patch(&expected, &actual);
257
258 if !diff.hunks().is_empty() {
259 let formatter = if std::io::IsTerminal::is_terminal(&std::io::stderr()) {
260 diffy::PatchFormatter::new().with_color()
261 } else {
262 diffy::PatchFormatter::new()
263 };
264 let header = "Generated and checked-in openapi spec does not match. \
265 Re-run with `UPDATE=1` to update expected format";
266 panic!("{header}\n\n{}", formatter.fmt_patch(&diff));
267 }
268 }
269
270 #[tokio::test]
271 async fn openapi_explorer() {
272 if std::env::var_os("OPENAPI_EXPLORER").is_none() {
274 return;
275 }
276
277 let openapi = {
278 let mut api = openapi::Api::new(info());
279 api.register_endpoints(ENDPOINTS.to_owned());
280 api.openapi()
281 };
282
283 let router = openapi::OpenApiDocument::new(openapi).into_router();
284
285 let listener = tokio::net::TcpListener::bind("127.0.0.1:8000")
286 .await
287 .unwrap();
288 axum::serve(listener, router).await.unwrap();
289 }
290}