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