Skip to main content

iota_rest_kv/routes/
kv_store.rs

1// Copyright (c) 2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::num::NonZeroUsize;
5
6use axum::{
7    Json,
8    body::Body,
9    extract::{Path, Query, State},
10    http::StatusCode,
11    response::IntoResponse,
12};
13use iota_kvstore::client::TransactionSequenceNumber;
14use iota_sdk_types::Address;
15use iota_storage::http_key_value_store::{ItemType, Key};
16use serde::Deserialize;
17
18use crate::{
19    bigtable::{ObjectRangeKeyBound, ObjectsBeforeVersionRequest},
20    errors::ApiError,
21    extractors::ExtractPath,
22    types::SharedRestServerAppState,
23};
24
25const BEFORE_VERSION_REQUIRES_OB_ERROR_MSG: &str =
26    "`before_version` query parameter is only valid for `ob` item types";
27
28/// Request payload for multi_get_objects_post containing list of keys.
29#[derive(Deserialize, Debug)]
30pub(crate) struct MultiGetRequest {
31    /// List of base64url-encoded keys to retrieve.
32    pub(crate) keys: Vec<String>,
33}
34
35/// Extracts the `?before_version` query parameter
36#[derive(Deserialize, Debug, Default)]
37pub(crate) struct BeforeVersion {
38    #[serde(default)]
39    pub(crate) before_version: bool,
40}
41
42/// Retrieves data associated with a given key from the KV store as raw
43/// [`Bytes`](bytes::Bytes).
44///
45/// # Query Parameters
46///
47/// * `before_version` (optional, default `false`): only valid when `item_type`
48///   is [`ItemType::Object`]. When `true`, returns the latest stored version
49///   strictly less than the version encoded in the key. Returns `400 Bad
50///   Request` if used with any other [`ItemType`].
51///
52/// # Returns
53///
54/// * If the key exists, the data is returned as a [`Bytes`](bytes::Bytes)
55///   stream with a `200 OK` status code.
56/// * If the key does not exist, a `404 Not Found` status code is returned with
57///   an empty body.
58/// * If an error occurs while interacting with the KV store, an `500 internal
59///   server error` is returned.
60pub async fn data_as_bytes(
61    State(app_state): State<SharedRestServerAppState>,
62    ExtractPath(key): ExtractPath,
63    Query(BeforeVersion { before_version }): Query<BeforeVersion>,
64) -> Result<impl IntoResponse, ApiError> {
65    tracing::debug!(?key, before_version, "get item");
66
67    if before_version {
68        let range = ObjectRangeKeyBound::try_from(key)
69            .map_err(|_| ApiError::BadRequest(BEFORE_VERSION_REQUIRES_OB_ERROR_MSG.into()))?;
70
71        let response = app_state
72            .kv_store_client
73            .object_before_version(range)
74            .await?;
75
76        return Ok(response.map_or_else(
77            || (StatusCode::NOT_FOUND, Body::empty()).into_response(),
78            |bytes| bytes.into_response(),
79        ));
80    }
81
82    app_state
83        .kv_store_client
84        .get(key)
85        .await
86        .map(|res| match res {
87            Some(bytes) => bytes.into_response(),
88            None => (StatusCode::NOT_FOUND, Body::empty()).into_response(),
89        })
90}
91
92/// Retrieves multiple objects via POST request with JSON payload.
93///
94/// # Path Parameters
95///
96/// - `item_type`: The type of items to get (e.g., "cs", "cc", "tx")
97///
98/// # Query Parameters
99///
100/// * `before_version` (optional, default `false`): only valid when `item_type`
101///   is [`ItemType::Object`]. When `true`, returns the latest stored version
102///   strictly less than the version encoded in each key. Returns `400 Bad
103///   Request` if used with any other [`ItemType`].
104///
105/// # Request Body
106///
107/// JSON object with `keys` field:
108///
109/// ```json
110/// {
111///   "keys": ["AAEAAAAAAAAA", "AAIAAAAAAAAA", "AAMAAAAAAAAA"]
112/// }
113/// ```
114///
115/// Where:
116/// - `keys`: Array of base64url-encoded keys for given `item_type`. The same
117///   kind of key and encoding user would use in single item GET request.
118///
119/// # Returns
120///
121/// * If successful, returns a BCS-serialized
122///   [`Vec`]<[`Option`]<[`Bytes`](bytes::Bytes)>> with a `200 OK` status code.
123///   The vector has the same length and order as the `keys` list in the request
124///   body. Each entry is `Some(bytes)` if the key was found, or `None` if the
125///   key was not found.
126///  * If no keys are provided or the number of keys exceeds the configured
127///    `multiget_max_items` limit, a `400 bad request error` is returned.
128/// * If the keys cannot be parsed, a `400 bad request error` is returned.
129/// * If an error occurs while interacting with the KV store, an `500 internal
130///   server error` is returned.
131pub async fn multi_get_data(
132    State(app_state): State<SharedRestServerAppState>,
133    Path(item_type): Path<ItemType>,
134    Query(BeforeVersion { before_version }): Query<BeforeVersion>,
135    Json(payload): Json<MultiGetRequest>,
136) -> Result<impl IntoResponse, ApiError> {
137    if payload.keys.is_empty() {
138        return Err(ApiError::BadRequest("no keys provided".into()));
139    }
140
141    if payload.keys.len() > app_state.multiget_max_items.get() {
142        return Err(ApiError::BadRequest(format!(
143            "too many keys: requested {}, maximum allowed is {}",
144            payload.keys.len(),
145            app_state.multiget_max_items
146        )));
147    }
148
149    tracing::debug!(
150        %item_type,
151        num_keys = payload.keys.len(),
152        before_version,
153        "multi-get items"
154    );
155
156    let item_type_str = item_type.to_string();
157    let keys = payload
158        .keys
159        .iter()
160        .map(|encoded_key| {
161            Key::new(item_type_str.as_str(), encoded_key.as_str())
162                .map_err(|err| ApiError::BadRequest(format!("invalid key '{encoded_key}': {err}")))
163        })
164        .collect::<Result<Vec<Key>, ApiError>>()?;
165
166    let results = if before_version {
167        let request = ObjectsBeforeVersionRequest::try_from(keys)
168            .map_err(|_| ApiError::BadRequest(BEFORE_VERSION_REQUIRES_OB_ERROR_MSG.into()))?;
169        app_state
170            .kv_store_client
171            .objects_before_version(request)
172            .await?
173    } else {
174        app_state.kv_store_client.get_items(keys).await?
175    };
176
177    let bcs_data = bcs::to_bytes(&results)
178        .map_err(Into::into)
179        .map_err(ApiError::InternalServerError)?;
180    Ok(bcs_data.into_response())
181}
182
183#[derive(Deserialize, Debug)]
184pub(crate) struct TransactionDigestsByAddressQuery {
185    pub(crate) cursor: Option<TransactionSequenceNumber>,
186    pub(crate) limit: Option<NonZeroUsize>,
187    #[serde(default)]
188    pub(crate) oldest_first: bool,
189}
190
191/// Retrieves a paginated list of transactions that affect a given address.
192///
193/// An address is considered "affected" by a transaction if it appears as the
194/// sender, a recipient, or the gas payer.
195///
196/// # Path Parameters
197///
198/// * `address`: Base64-url-encoded [`Address`].
199///
200/// # Query Parameters
201///
202/// * `cursor` (optional): The [`TransactionSequenceNumber`] used as an
203///   exclusive pagination boundary. Omit for the first request.
204/// * `limit` (optional): The maximum number of results to return. Defaults to
205///   the server's configured `multiget_max_items` when omitted.
206/// * `oldest_first` (optional, default `false`):
207///   - `true`: Ascending sequence order (oldest first).
208///   - `false`: Descending sequence order (newest first).
209///
210/// # Responses
211///
212/// * `200 OK`: A BCS-encoded `Vec<(TransactionSequenceNumber,
213///   TransactionDigest)>`. Returns an empty list when no transaction digests
214///   are found in the range scan.
215/// * `400 Bad Request`: Returned if the provided `address` is malformed or
216///   invalid.
217/// * `500 Internal Server Error`: Returned if an error occurs interacting with
218///   the KV store.
219pub async fn transaction_digests_by_address(
220    State(app_state): State<SharedRestServerAppState>,
221    Path(address): Path<String>,
222    Query(query): Query<TransactionDigestsByAddressQuery>,
223) -> Result<impl IntoResponse, ApiError> {
224    let address = base64_url::decode(&address)
225        .map_err(|_| ApiError::BadRequest("address is not valid base64-url".into()))?;
226
227    let address = Address::from_bytes(&address)
228        .map_err(|_| ApiError::BadRequest("invalid address".into()))?;
229
230    let TransactionDigestsByAddressQuery {
231        cursor,
232        limit,
233        oldest_first,
234    } = query;
235
236    tracing::debug!(
237        %address,
238        ?cursor,
239        ?limit,
240        oldest_first,
241        "get transaction digests by address"
242    );
243
244    let max_limit = app_state.multiget_max_items.get();
245    let limit = limit.map_or(max_limit, |l| l.get());
246
247    if limit > max_limit {
248        return Err(ApiError::BadRequest(format!(
249            "limit too large: maximum allowed is {max_limit}",
250        )));
251    }
252
253    let transactions = app_state
254        .kv_store_client
255        .transactions_by_address(address, cursor, limit, oldest_first)
256        .await?;
257
258    let bcs_data = bcs::to_bytes(&transactions)
259        .map_err(Into::into)
260        .map_err(ApiError::InternalServerError)?;
261    Ok(bcs_data.into_response())
262}