sui_indexer_alt_jsonrpc/api/transactions/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use futures::future;
5use jsonrpsee::core::RpcResult;
6use jsonrpsee::proc_macros::rpc;
7use mysten_common::ZipDebugEqIteratorExt;
8use sui_json_rpc_types::Page;
9use sui_json_rpc_types::SuiTransactionBlockResponse;
10use sui_json_rpc_types::SuiTransactionBlockResponseOptions;
11use sui_open_rpc::Module;
12use sui_open_rpc_macros::open_rpc;
13use sui_types::digests::TransactionDigest;
14
15use crate::api::rpc_module::RpcModule;
16use crate::api::transactions::error::Error;
17use crate::api::transactions::filter::SuiTransactionBlockResponseQuery;
18use crate::context::Context;
19use crate::error::InternalContext;
20use crate::error::RpcError;
21use crate::error::rpc_bail;
22
23mod error;
24mod filter;
25mod response;
26
27#[open_rpc(namespace = "sui", tag = "Transactions API")]
28#[rpc(server, namespace = "sui")]
29trait TransactionsApi {
30    /// Fetch a transaction by its transaction digest.
31    #[method(name = "getTransactionBlock")]
32    async fn get_transaction_block(
33        &self,
34        /// The digest of the queried transaction.
35        digest: TransactionDigest,
36        /// Options controlling the output format.
37        options: Option<SuiTransactionBlockResponseOptions>,
38    ) -> RpcResult<SuiTransactionBlockResponse>;
39}
40
41#[open_rpc(namespace = "suix", tag = "Query Transactions API")]
42#[rpc(server, namespace = "suix")]
43trait QueryTransactionsApi {
44    /// Query transactions based on their properties (sender, affected addresses, function calls,
45    /// etc). Returns a paginated list of transactions.
46    ///
47    /// If a cursor is provided, the query will start from the transaction after the one pointed to
48    /// by this cursor, otherwise pagination starts from the first transaction that meets the query
49    /// criteria.
50    ///
51    /// The definition of "first" transaction is changed by the `descending_order` parameter, which
52    /// is optional, and defaults to false, meaning that the oldest transaction is shown first.
53    ///
54    /// The size of each page is controlled by the `limit` parameter.
55    #[method(name = "queryTransactionBlocks")]
56    async fn query_transaction_blocks(
57        &self,
58        /// The query criteria, and the output options.
59        query: SuiTransactionBlockResponseQuery,
60        /// Cursor to start paginating from.
61        cursor: Option<String>,
62        /// Maximum number of transactions to return per page.
63        limit: Option<usize>,
64        /// Order of results, defaulting to ascending order (false), by sequence on-chain.
65        descending_order: Option<bool>,
66    ) -> RpcResult<Page<SuiTransactionBlockResponse, String>>;
67}
68
69pub(crate) struct Transactions(pub Context);
70
71pub(crate) struct QueryTransactions(pub Context);
72
73#[async_trait::async_trait]
74impl TransactionsApiServer for Transactions {
75    async fn get_transaction_block(
76        &self,
77        digest: TransactionDigest,
78        options: Option<SuiTransactionBlockResponseOptions>,
79    ) -> RpcResult<SuiTransactionBlockResponse> {
80        let Self(ctx) = self;
81        Ok(
82            response::transaction(ctx, digest, &options.unwrap_or_default())
83                .await
84                .with_internal_context(|| format!("Failed to get transaction {digest}"))?,
85        )
86    }
87}
88
89#[async_trait::async_trait]
90impl QueryTransactionsApiServer for QueryTransactions {
91    async fn query_transaction_blocks(
92        &self,
93        query: SuiTransactionBlockResponseQuery,
94        cursor: Option<String>,
95        limit: Option<usize>,
96        descending_order: Option<bool>,
97    ) -> RpcResult<Page<SuiTransactionBlockResponse, String>> {
98        let Self(ctx) = self;
99
100        let Page {
101            data: digests,
102            next_cursor,
103            has_next_page,
104        } = filter::transactions(ctx, &query.filter, cursor.clone(), limit, descending_order)
105            .await?;
106
107        let options = query.options.unwrap_or_default();
108
109        let tx_futures = digests.iter().map(|d| {
110            async {
111                let mut tx = response::transaction(ctx, *d, &options).await;
112
113                let config = &ctx.config().transactions;
114                let mut interval = tokio::time::interval(std::time::Duration::from_millis(
115                    config.tx_retry_interval_ms,
116                ));
117
118                let mut retries = 0;
119                for _ in 0..config.tx_retry_count {
120                    // Retry only if the error is an invalid params error, which can only be due to
121                    // the transaction not being found in the kv store or tx balance changes table.
122                    if let Err(RpcError::InvalidParams(
123                        _e @ (Error::BalanceChangesNotFound(_) | Error::NotFound(_)),
124                    )) = tx
125                    {
126                        interval.tick().await;
127                        retries += 1;
128                        tx = response::transaction(ctx, *d, &options).await;
129                        ctx.metrics()
130                            .read_retries
131                            .with_label_values(&["tx_response"])
132                            .inc();
133                    } else {
134                        break;
135                    }
136                }
137
138                ctx.metrics()
139                    .read_retries_per_request
140                    .with_label_values(&["tx_response"])
141                    .observe(retries as f64);
142                tx
143            }
144        });
145
146        let data = future::join_all(tx_futures)
147            .await
148            .into_iter()
149            .zip_debug_eq(digests)
150            .map(|(r, d)| {
151                if let Err(RpcError::InvalidParams(e @ Error::NotFound(_))) = r {
152                    rpc_bail!(e)
153                } else {
154                    r.with_internal_context(|| format!("Failed to get transaction {d}"))
155                }
156            })
157            .collect::<Result<Vec<_>, _>>()?;
158
159        Ok(Page {
160            data,
161            next_cursor: next_cursor.or(cursor),
162            has_next_page,
163        })
164    }
165}
166
167impl RpcModule for Transactions {
168    fn schema(&self) -> Module {
169        TransactionsApiOpenRpc::module_doc()
170    }
171
172    fn into_impl(self) -> jsonrpsee::RpcModule<Self> {
173        self.into_rpc()
174    }
175}
176
177impl RpcModule for QueryTransactions {
178    fn schema(&self) -> Module {
179        QueryTransactionsApiOpenRpc::module_doc()
180    }
181
182    fn into_impl(self) -> jsonrpsee::RpcModule<Self> {
183        self.into_rpc()
184    }
185}