iota_rest_api/transactions/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5mod 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/// A Cursor that points at a specific transaction in history.
212///
213/// Has the format of: `<checkpoint>[.<index>]`
214/// where `<checkpoint>` is the sequence number of a checkpoint and `<index>` is
215/// the index of a transaction in the particular checkpoint.
216///
217/// `index` is optional and if omitted iteration will start at the first or last
218/// transaction in a checkpoint based on the provided `Direction`:
219///   - Direction::Ascending - first
220///   - Direction::Descending - last
221#[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}