Skip to main content

hypercall_admin/
testnet.rs

1//! Testnet-only admin endpoints (feature `test-endpoints`).
2//!
3//! `POST /monitoring/test/agent-authorization` applies an agent authorization
4//! directly through the engine channel, bypassing the EIP-712 signature flow.
5//! It refuses to run unless the runtime is in testnet mode.
6
7use axum::extract::State;
8use axum::http::StatusCode;
9use serde::{Deserialize, Serialize};
10use utoipa::ToSchema;
11
12use hypercall_runtime_api::error::ApiError;
13use hypercall_runtime_api::sonic_json::SonicJson;
14use hypercall_types::api_models::ApiResponse;
15use hypercall_types::WalletAddress;
16
17use crate::state::{AdminState, ENGINE_RESPONSE_TIMEOUT};
18
19#[derive(Debug, Deserialize, ToSchema)]
20pub struct TestAgentAuthorizationRequest {
21    #[schema(value_type = String)]
22    pub wallet: WalletAddress,
23    #[schema(value_type = String)]
24    pub agent: WalletAddress,
25    pub approve: bool,
26    pub nonce: u64,
27}
28
29#[derive(Debug, Serialize, ToSchema)]
30pub struct TestAgentAuthorizationResponse {
31    #[schema(value_type = String)]
32    pub wallet: WalletAddress,
33    #[schema(value_type = String)]
34    pub agent: WalletAddress,
35    pub approved: bool,
36}
37
38/// API envelope for testnet agent authorization updates.
39#[derive(Debug, Serialize, ToSchema)]
40pub struct TestAgentAuthorizationApiResponse {
41    /// Whether the request succeeded.
42    pub success: bool,
43    pub data: Option<TestAgentAuthorizationResponse>,
44    /// Error message, present on failure.
45    pub error: Option<String>,
46}
47
48/// Apply testnet agent authorization.
49#[utoipa::path(
50    post,
51    path = "/monitoring/test/agent-authorization",
52    request_body = TestAgentAuthorizationRequest,
53    responses(
54        (status = 200, description = "Agent authorization applied", body = TestAgentAuthorizationApiResponse),
55        (status = 403, description = "Not enabled (production mode)"),
56        (status = 503, description = "Engine communication failure"),
57        (status = 504, description = "Engine response timeout")
58    ),
59    tag = "Monitoring",
60    security(("admin_key" = []))
61)]
62pub async fn apply_agent_authorization(
63    State(state): State<AdminState>,
64    SonicJson(request): SonicJson<TestAgentAuthorizationRequest>,
65) -> Result<SonicJson<ApiResponse<TestAgentAuthorizationResponse>>, ApiError> {
66    if !state.runtime_config.testnet_mode {
67        return Err(ApiError::forbidden(
68            "Agent authorization test endpoint requires testnet mode",
69        ));
70    }
71
72    let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
73    state
74        .agent_auth_sender
75        .send(hypercall_runtime_api::AgentAuthRequest {
76            wallet: request.wallet,
77            agent: request.agent,
78            approve: request.approve,
79            expires_at_ms: None,
80            nonce: Some(request.nonce),
81            applied_tx,
82        })
83        .await
84        .map_err(|_| {
85            ApiError::new(
86                StatusCode::SERVICE_UNAVAILABLE,
87                "service_unavailable",
88                "agent authorization engine path closed",
89            )
90        })?;
91
92    match tokio::time::timeout(ENGINE_RESPONSE_TIMEOUT, applied_rx).await {
93        Ok(Ok(Ok(()))) => {}
94        Ok(Ok(Err(error))) => return Ok(SonicJson(ApiResponse::error(error))),
95        Ok(Err(_)) => {
96            return Err(ApiError::new(
97                StatusCode::SERVICE_UNAVAILABLE,
98                "service_unavailable",
99                "agent authorization apply dropped",
100            ))
101        }
102        Err(_) => {
103            return Err(ApiError::gateway_timeout(
104                "agent authorization apply timed out",
105            ))
106        }
107    }
108
109    Ok(SonicJson(ApiResponse::success(
110        TestAgentAuthorizationResponse {
111            wallet: request.wallet,
112            agent: request.agent,
113            approved: request.approve,
114        },
115    )))
116}