iota_rest_api/transactions/
mod.rs1mod execution;
6pub use execution::{
7 EffectsFinality, ExecuteTransaction, ExecuteTransactionQueryParameters, SimulateTransaction,
8 SimulateTransactionQueryParameters, TransactionExecutionResponse,
9 TransactionSimulationResponse,
10};
11
12mod resolve;
13use axum::{
14 extract::{Path, Query, State},
15 http::StatusCode,
16};
17use iota_sdk2::types::{
18 CheckpointSequenceNumber, Transaction, TransactionDigest, TransactionEffects,
19 TransactionEvents, UserSignature,
20};
21pub use resolve::{
22 ResolveTransaction, ResolveTransactionQueryParameters, ResolveTransactionResponse,
23};
24use tap::Pipe;
25
26use crate::{
27 Direction, Page, RestError, RestService, Result,
28 accept::AcceptFormat,
29 openapi::{ApiEndpoint, OperationBuilder, ResponseBuilder, RouteHandler},
30 reader::StateReader,
31 response::ResponseContent,
32};
33
34pub struct GetTransaction;
35
36impl ApiEndpoint<RestService> for GetTransaction {
37 fn method(&self) -> axum::http::Method {
38 axum::http::Method::GET
39 }
40
41 fn path(&self) -> &'static str {
42 "/transactions/{transaction}"
43 }
44
45 fn operation(
46 &self,
47 generator: &mut schemars::gen::SchemaGenerator,
48 ) -> openapiv3::v3_1::Operation {
49 OperationBuilder::new()
50 .tag("Transactions")
51 .operation_id("GetTransaction")
52 .path_parameter::<TransactionDigest>("transaction", generator)
53 .response(
54 200,
55 ResponseBuilder::new()
56 .json_content::<TransactionResponse>(generator)
57 .bcs_content()
58 .build(),
59 )
60 .response(404, ResponseBuilder::new().build())
61 .build()
62 }
63
64 fn handler(&self) -> RouteHandler<RestService> {
65 RouteHandler::new(self.method(), get_transaction)
66 }
67}
68
69async fn get_transaction(
70 Path(transaction_digest): Path<TransactionDigest>,
71 accept: AcceptFormat,
72 State(state): State<StateReader>,
73) -> Result<ResponseContent<TransactionResponse>> {
74 let response = state.get_transaction_response(transaction_digest)?;
75
76 match accept {
77 AcceptFormat::Json => ResponseContent::Json(response),
78 AcceptFormat::Bcs => ResponseContent::Bcs(response),
79 }
80 .pipe(Ok)
81}
82
83#[serde_with::serde_as]
84#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
85pub struct TransactionResponse {
86 pub digest: TransactionDigest,
87 pub transaction: Transaction,
88 pub signatures: Vec<UserSignature>,
89 pub effects: TransactionEffects,
90 pub events: Option<TransactionEvents>,
91 #[serde_as(
92 as = "Option<iota_types::iota_serde::Readable<iota_types::iota_serde::BigInt<u64>, _>>"
93 )]
94 #[schemars(with = "Option<crate::_schemars::U64>")]
95 pub checkpoint: Option<u64>,
96 #[serde_as(
97 as = "Option<iota_types::iota_serde::Readable<iota_types::iota_serde::BigInt<u64>, _>>"
98 )]
99 #[schemars(with = "Option<crate::_schemars::U64>")]
100 pub timestamp_ms: Option<u64>,
101}
102
103#[derive(Debug)]
104pub struct TransactionNotFoundError(pub TransactionDigest);
105
106impl std::fmt::Display for TransactionNotFoundError {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "Transaction {} not found", self.0)
109 }
110}
111
112impl std::error::Error for TransactionNotFoundError {}
113
114impl From<TransactionNotFoundError> for crate::RestError {
115 fn from(value: TransactionNotFoundError) -> Self {
116 Self::new(axum::http::StatusCode::NOT_FOUND, value.to_string())
117 }
118}
119
120pub struct ListTransactions;
121
122impl ApiEndpoint<RestService> for ListTransactions {
123 fn method(&self) -> axum::http::Method {
124 axum::http::Method::GET
125 }
126
127 fn path(&self) -> &'static str {
128 "/transactions"
129 }
130
131 fn operation(
132 &self,
133 generator: &mut schemars::gen::SchemaGenerator,
134 ) -> openapiv3::v3_1::Operation {
135 OperationBuilder::new()
136 .tag("Transactions")
137 .operation_id("ListTransactions")
138 .query_parameters::<ListTransactionsQueryParameters>(generator)
139 .response(
140 200,
141 ResponseBuilder::new()
142 .json_content::<Vec<TransactionResponse>>(generator)
143 .bcs_content()
144 .header::<String>(crate::types::X_IOTA_CURSOR, generator)
145 .build(),
146 )
147 .response(410, ResponseBuilder::new().build())
148 .build()
149 }
150
151 fn handler(&self) -> RouteHandler<RestService> {
152 RouteHandler::new(self.method(), list_transactions)
153 }
154}
155
156async fn list_transactions(
157 Query(parameters): Query<ListTransactionsQueryParameters>,
158 accept: AcceptFormat,
159 State(state): State<StateReader>,
160) -> Result<Page<TransactionResponse, TransactionCursor>> {
161 let latest_checkpoint = state.inner().get_latest_checkpoint()?.sequence_number;
162 let oldest_checkpoint = state.inner().get_lowest_available_checkpoint()?;
163 let limit = parameters.limit();
164 let start = parameters.start(latest_checkpoint);
165 let direction = parameters.direction();
166
167 if start.checkpoint < oldest_checkpoint {
168 return Err(RestError::new(
169 StatusCode::GONE,
170 "Old transactions have been pruned",
171 ));
172 }
173
174 let mut next_cursor = None;
175 let transactions = state
176 .transaction_iter(direction, (start.checkpoint, start.index))
177 .take(limit)
178 .map(|entry| {
179 let (cursor_info, digest) = entry?;
180 next_cursor = cursor_info.next_cursor;
181 state
182 .get_transaction(digest.into())
183 .map(|(transaction, effects, events)| TransactionResponse {
184 digest: transaction.transaction.digest(),
185 transaction: transaction.transaction,
186 signatures: transaction.signatures,
187 effects,
188 events,
189 checkpoint: Some(cursor_info.checkpoint),
190 timestamp_ms: Some(cursor_info.timestamp_ms),
191 })
192 })
193 .collect::<Result<_, _>>()?;
194
195 let entries = match accept {
196 AcceptFormat::Json => ResponseContent::Json(transactions),
197 AcceptFormat::Bcs => ResponseContent::Bcs(transactions),
198 };
199
200 let cursor = next_cursor.and_then(|(checkpoint, index)| {
201 if checkpoint < oldest_checkpoint {
202 None
203 } else {
204 Some(TransactionCursor { checkpoint, index })
205 }
206 });
207
208 Ok(Page { entries, cursor })
209}
210
211#[derive(Debug, Copy, Clone)]
222pub struct TransactionCursor {
223 checkpoint: CheckpointSequenceNumber,
224 index: Option<usize>,
225}
226
227impl std::fmt::Display for TransactionCursor {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 write!(f, "{}", self.checkpoint)?;
230 if let Some(index) = self.index {
231 write!(f, ".{index}")?;
232 }
233 Ok(())
234 }
235}
236
237impl std::str::FromStr for TransactionCursor {
238 type Err = std::num::ParseIntError;
239
240 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
241 if let Some((checkpoint, index)) = s.split_once('.') {
242 Self {
243 checkpoint: checkpoint.parse()?,
244 index: Some(index.parse()?),
245 }
246 } else {
247 Self {
248 checkpoint: s.parse()?,
249 index: None,
250 }
251 }
252 .pipe(Ok)
253 }
254}
255
256impl<'de> serde::Deserialize<'de> for TransactionCursor {
257 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
258 where
259 D: serde::Deserializer<'de>,
260 {
261 use serde_with::DeserializeAs;
262 serde_with::DisplayFromStr::deserialize_as(deserializer)
263 }
264}
265
266impl serde::Serialize for TransactionCursor {
267 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
268 where
269 S: serde::Serializer,
270 {
271 use serde_with::SerializeAs;
272 serde_with::DisplayFromStr::serialize_as(self, serializer)
273 }
274}
275
276#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
277pub struct ListTransactionsQueryParameters {
278 pub limit: Option<u32>,
279 #[schemars(with = "Option<String>")]
280 pub start: Option<TransactionCursor>,
281 pub direction: Option<Direction>,
282}
283
284impl ListTransactionsQueryParameters {
285 pub fn limit(&self) -> usize {
286 self.limit
287 .map(|l| (l as usize).clamp(1, crate::MAX_PAGE_SIZE))
288 .unwrap_or(crate::DEFAULT_PAGE_SIZE)
289 }
290
291 pub fn start(&self, default: CheckpointSequenceNumber) -> TransactionCursor {
292 self.start.unwrap_or(TransactionCursor {
293 checkpoint: default,
294 index: None,
295 })
296 }
297
298 pub fn direction(&self) -> Direction {
299 self.direction.unwrap_or(Direction::Descending)
300 }
301}