Skip to main content

hypercall_api/handlers/
agents.rs

1use crate::sonic_json::SonicJson;
2use axum::extract::{Query, State};
3use axum::http::StatusCode;
4use hypercall_auth::SignatureRecovery;
5use hypercall_types::{
6    ApproveAgentRequest, ApproveAgentResponse, AuthorizedAgentsResponse, RevokeAgentRequest,
7    RevokeAgentResponse, WalletAddress,
8};
9use serde::Deserialize;
10use tracing::error;
11use utoipa::IntoParams;
12
13use super::AppState;
14
15/// Approve an agent to act on behalf of a wallet
16#[utoipa::path(
17    post,
18    path = "/approve-agent",
19    request_body = ApproveAgentRequest,
20    responses(
21        (status = 200, description = "Agent approved", body = ApproveAgentResponse),
22        (status = 400, description = "Invalid signature")
23    ),
24    tag = "Agents"
25)]
26pub async fn approve_agent(
27    State(app_state): State<AppState>,
28    SonicJson(request): SonicJson<ApproveAgentRequest>,
29) -> SonicJson<ApproveAgentResponse> {
30    if app_state
31        .is_draining
32        .load(std::sync::atomic::Ordering::Relaxed)
33    {
34        return SonicJson(ApproveAgentResponse {
35            success: false,
36            error: Some("Server is draining, not accepting writes".to_string()),
37        });
38    }
39
40    let wallet_address = match SignatureRecovery::recover_approve_agent_signer(
41        &request.agent.as_hex(),
42        request.nonce,
43        &request.signature,
44        app_state.runtime_config.signing_chain_id,
45    ) {
46        Ok(addr) => WalletAddress::from(addr),
47        Err(e) => {
48            error!("Failed to recover signature: {}", e);
49            return SonicJson(ApproveAgentResponse {
50                success: false,
51                error: Some(format!("Invalid signature: {}", e)),
52            });
53        }
54    };
55
56    let (tx, rx) = tokio::sync::oneshot::channel();
57    let auth_request = hypercall_runtime_api::AgentAuthRequest {
58        wallet: wallet_address,
59        agent: request.agent,
60        approve: true,
61        expires_at_ms: None,
62        nonce: Some(request.nonce),
63        applied_tx: tx,
64    };
65    if let Err(e) = app_state.agent_auth_sender.send(auth_request).await {
66        return SonicJson(ApproveAgentResponse {
67            success: false,
68            error: Some(format!("Engine channel closed: {}", e)),
69        });
70    }
71    match tokio::time::timeout(std::time::Duration::from_secs(5), rx).await {
72        Ok(Ok(Ok(()))) => SonicJson(ApproveAgentResponse {
73            success: true,
74            error: None,
75        }),
76        Ok(Ok(Err(e))) => SonicJson(ApproveAgentResponse {
77            success: false,
78            error: Some(e),
79        }),
80        Ok(Err(_)) => SonicJson(ApproveAgentResponse {
81            success: false,
82            error: Some("Engine dropped response channel".to_string()),
83        }),
84        Err(_) => SonicJson(ApproveAgentResponse {
85            success: false,
86            error: Some("Timed out waiting for engine".to_string()),
87        }),
88    }
89}
90
91/// Revoke an agent's authorization
92#[utoipa::path(
93    delete,
94    path = "/revoke-agent",
95    request_body = RevokeAgentRequest,
96    responses(
97        (status = 200, description = "Agent revoked", body = RevokeAgentResponse),
98        (status = 400, description = "Invalid signature")
99    ),
100    tag = "Agents"
101)]
102pub async fn revoke_agent(
103    State(app_state): State<AppState>,
104    SonicJson(request): SonicJson<RevokeAgentRequest>,
105) -> SonicJson<RevokeAgentResponse> {
106    if app_state
107        .is_draining
108        .load(std::sync::atomic::Ordering::Relaxed)
109    {
110        return SonicJson(RevokeAgentResponse {
111            success: false,
112            error: Some("Server is draining, not accepting writes".to_string()),
113        });
114    }
115
116    let wallet_address = match SignatureRecovery::recover_revoke_agent_signer(
117        &request.agent.as_hex(),
118        request.nonce,
119        &request.signature,
120        app_state.runtime_config.signing_chain_id,
121    ) {
122        Ok(addr) => WalletAddress::from(addr),
123        Err(e) => {
124            error!("Failed to recover signature: {}", e);
125            return SonicJson(RevokeAgentResponse {
126                success: false,
127                error: Some(format!("Invalid signature: {}", e)),
128            });
129        }
130    };
131
132    let (tx, rx) = tokio::sync::oneshot::channel();
133    let auth_request = hypercall_runtime_api::AgentAuthRequest {
134        wallet: wallet_address,
135        agent: request.agent,
136        approve: false,
137        expires_at_ms: None,
138        nonce: Some(request.nonce),
139        applied_tx: tx,
140    };
141    if let Err(e) = app_state.agent_auth_sender.send(auth_request).await {
142        return SonicJson(RevokeAgentResponse {
143            success: false,
144            error: Some(format!("Engine channel closed: {}", e)),
145        });
146    }
147    match tokio::time::timeout(std::time::Duration::from_secs(5), rx).await {
148        Ok(Ok(Ok(()))) => SonicJson(RevokeAgentResponse {
149            success: true,
150            error: None,
151        }),
152        Ok(Ok(Err(e))) => SonicJson(RevokeAgentResponse {
153            success: false,
154            error: Some(e),
155        }),
156        Ok(Err(_)) => SonicJson(RevokeAgentResponse {
157            success: false,
158            error: Some("Engine dropped response channel".to_string()),
159        }),
160        Err(_) => SonicJson(RevokeAgentResponse {
161            success: false,
162            error: Some("Timed out waiting for engine".to_string()),
163        }),
164    }
165}
166
167#[derive(Debug, Deserialize, IntoParams)]
168pub struct AuthorizedAgentsQuery {
169    #[param(example = "0x1234567890abcdef1234567890abcdef12345678")]
170    pub wallet: WalletAddress,
171}
172
173/// Get list of authorized agents for a wallet
174#[utoipa::path(
175    get,
176    path = "/authorized-agents",
177    params(AuthorizedAgentsQuery),
178    responses(
179        (status = 200, description = "List of authorized agents", body = AuthorizedAgentsResponse),
180        (status = 500, description = "Internal server error")
181    ),
182    tag = "Agents"
183)]
184pub async fn get_authorized_agents(
185    State(app_state): State<AppState>,
186    Query(query): Query<AuthorizedAgentsQuery>,
187) -> Result<SonicJson<AuthorizedAgentsResponse>, StatusCode> {
188    let agents = app_state.agent_auth.get_authorized_agents(&query.wallet);
189    Ok(SonicJson(AuthorizedAgentsResponse { agents }))
190}