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#[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#[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#[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}