use axum::Json;
use axum::extract::{Query, State};
use miden_protocol::block::BlockNumber;
use miden_protocol::utils::serde::Serializable;
use serde::{Deserialize, Serialize};
use crate::StoredPrivateRecord;
use crate::db::{ListTransactionsParams, ListedTransaction};
use crate::server::admin_service::ValidatorAdminService;
use crate::server::admin_service::error::ApiError;
pub(super) const DEFAULT_PAGE_LIMIT: usize = 100;
pub(super) const MAX_PAGE_LIMIT: usize = 1000;
pub(super) const MAX_RECORD_PAGE_LIMIT: usize = 100;
#[derive(Debug, Default, Deserialize)]
pub(super) struct ListTransactionsQuery {
pub(super) limit: Option<usize>,
#[serde(default)]
pub(super) include_records: bool,
pub(super) block_from: Option<u32>,
pub(super) tx_index_from: Option<u32>,
pub(super) block_to: Option<u32>,
}
#[derive(Debug, Deserialize, Serialize)]
pub(super) struct ListedValidatedTransaction {
pub(super) transaction_id: String,
pub(super) block_num: u32,
pub(super) block_tx_index: u32,
pub(super) key_epoch: String,
pub(super) setup_context_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) record: Option<PrivateRecordPayload>,
}
impl From<ListedTransaction> for ListedValidatedTransaction {
fn from(item: ListedTransaction) -> Self {
Self {
transaction_id: hex::encode(item.transaction_id.to_bytes()),
block_num: item.block_num.as_u32(),
block_tx_index: item.block_tx_index,
key_epoch: hex::encode(item.key_epoch.as_bytes()),
setup_context_id: hex::encode(item.setup_context_id),
record: item.record.map(Into::into),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub(super) struct PrivateRecordPayload {
pub(super) final_ciphertext: String,
pub(super) cipher_nonce: String,
pub(super) encrypted_record_key: String,
pub(super) decryption_context: String,
}
impl From<StoredPrivateRecord> for PrivateRecordPayload {
fn from(record: StoredPrivateRecord) -> Self {
Self {
final_ciphertext: hex::encode(record.encrypted_record()),
cipher_nonce: hex::encode(record.nonce()),
encrypted_record_key: hex::encode(record.encrypted_record_key()),
decryption_context: hex::encode(record.context().to_bytes()),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub(super) struct ListValidatedPrivateTransactionsResponse {
pub(super) transactions: Vec<ListedValidatedTransaction>,
pub(super) pagination: PaginationInfo,
}
#[derive(Debug, Deserialize, Serialize)]
pub(super) struct PaginationInfo {
pub(super) chain_tip: u32,
pub(super) block_num: Option<u32>,
pub(super) block_tx_index: Option<u32>,
}
pub(super) async fn list_validated_private_transactions(
State(service): State<ValidatorAdminService>,
Query(query): Query<ListTransactionsQuery>,
) -> Result<Json<ListValidatedPrivateTransactionsResponse>, ApiError> {
let max_limit = if query.include_records {
MAX_RECORD_PAGE_LIMIT
} else {
MAX_PAGE_LIMIT
};
let limit = query.limit.unwrap_or(DEFAULT_PAGE_LIMIT);
if limit == 0 || limit > max_limit {
return Err(ApiError::bad_request(format!("limit must be between 1 and {max_limit}")));
}
if query.tx_index_from.is_some() && query.block_from.is_none() {
return Err(ApiError::bad_request("tx_index_from requires block_from"));
}
if let (Some(from), Some(to)) = (query.block_from, query.block_to)
&& from > to
{
return Err(ApiError::bad_request("block_from must not exceed block_to"));
}
let start = query
.block_from
.map(|from| (BlockNumber::from(from), query.tx_index_from.unwrap_or(0)));
let page = service
.reader
.list_validated_transactions(ListTransactionsParams {
start,
block_to: query.block_to.map(BlockNumber::from),
limit,
include_records: query.include_records,
})
.await
.map_err(|error| {
ApiError::internal("failed to list validated private transactions", &error)
})?;
let chain_tip = page.chain_tip.as_u32();
let block_num = page.transactions.last().map(|item| item.block_num.as_u32());
let block_tx_index = page.transactions.last().map(|item| item.block_tx_index);
Ok(Json(ListValidatedPrivateTransactionsResponse {
transactions: page.transactions.into_iter().map(Into::into).collect(),
pagination: PaginationInfo { chain_tip, block_num, block_tx_index },
}))
}