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