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 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/// A Cursor that points at a specific transaction in history.
213///
214/// Has the format of: `<checkpoint>[.<index>]`
215/// where `<checkpoint>` is the sequence number of a checkpoint and `<index>` is
216/// the index of a transaction in the particular checkpoint.
217///
218/// `index` is optional and if omitted iteration will start at the first or last
219/// transaction in a checkpoint based on the provided `Direction`:
220///   - Direction::Ascending - first
221///   - Direction::Descending - last
222#[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}