Skip to main content

hypercall_api/handlers/
directives.rs

1//! Directive status REST API handler.
2
3use axum::{
4    extract::{Path, Query, State},
5    http::StatusCode,
6};
7use serde::Deserialize;
8use tracing::error;
9
10use super::AppState;
11use crate::error::ApiError;
12use crate::sonic_json::SonicJson;
13use hypercall_db::AsyncDirectiveOutboxReader;
14use hypercall_types::api_models::{DirectiveStatusResponse, WithdrawalHistoryResponse};
15use hypercall_types::WalletAddress;
16
17/// Look up the delivery status of a directive by its ID.
18///
19/// Public endpoint, no authentication required.
20pub async fn get_directive_status(
21    State(state): State<AppState>,
22    Path(directive_id): Path<String>,
23) -> Result<(StatusCode, SonicJson<DirectiveStatusResponse>), ApiError> {
24    let directive_reader: &dyn AsyncDirectiveOutboxReader = state.db.as_ref();
25    let row = directive_reader
26        .get_directive_status(&directive_id)
27        .await
28        .map_err(|e| {
29            error!("directive status query failed: {e}");
30            ApiError::internal_error("directive status lookup failed")
31        })?;
32
33    match row {
34        Some(row) => {
35            let created_at = row.created_ts_ms.map(|ms| {
36                chrono::DateTime::from_timestamp_millis(ms)
37                    .map(|dt| dt.to_rfc3339())
38                    .unwrap_or_else(|| ms.to_string())
39            });
40
41            Ok((
42                StatusCode::OK,
43                SonicJson(DirectiveStatusResponse {
44                    directive_id: row.directive_id,
45                    action_key: row.action_key,
46                    domain_status: row.domain_status,
47                    delivery_status: row.delivery_status,
48                    tx_hash: row.tx_hash,
49                    created_at,
50                }),
51            ))
52        }
53        None => Err(ApiError::not_found(format!(
54            "directive {} not found",
55            directive_id
56        ))),
57    }
58}
59
60/// Query parameters for withdrawal history lookup.
61#[derive(Deserialize)]
62pub struct WithdrawalHistoryQuery {
63    pub wallet: WalletAddress,
64    #[serde(default)]
65    pub limit: Option<i64>,
66}
67
68fn default_withdrawal_limit() -> i64 {
69    50
70}
71
72const MAX_WITHDRAWAL_LIMIT: i64 = 100;
73
74/// Return withdrawal history for a wallet.
75///
76/// Public endpoint, no authentication required.
77pub async fn get_withdrawal_history(
78    State(state): State<AppState>,
79    Query(params): Query<WithdrawalHistoryQuery>,
80) -> Result<(StatusCode, SonicJson<WithdrawalHistoryResponse>), ApiError> {
81    let limit = params
82        .limit
83        .unwrap_or_else(default_withdrawal_limit)
84        .min(MAX_WITHDRAWAL_LIMIT)
85        .max(1);
86    let wallet = params.wallet;
87
88    let directive_reader: &dyn AsyncDirectiveOutboxReader = state.db.as_ref();
89    let rows = directive_reader
90        .get_withdrawal_history(&wallet, limit)
91        .await
92        .map_err(|e| {
93            error!("withdrawal history query failed: {e}");
94            ApiError::internal_error("withdrawal history lookup failed")
95        })?;
96
97    let withdrawals = rows
98        .into_iter()
99        .map(|row| {
100            let created_at = row.created_ts_ms.map(|ms| {
101                chrono::DateTime::from_timestamp_millis(ms)
102                    .map(|dt| dt.to_rfc3339())
103                    .unwrap_or_else(|| ms.to_string())
104            });
105
106            DirectiveStatusResponse {
107                directive_id: row.directive_id,
108                action_key: row.action_key,
109                domain_status: row.domain_status,
110                delivery_status: row.delivery_status,
111                tx_hash: row.tx_hash,
112                created_at,
113            }
114        })
115        .collect();
116
117    Ok((
118        StatusCode::OK,
119        SonicJson(WithdrawalHistoryResponse { withdrawals }),
120    ))
121}