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