1use axum::extract::{Path, State};
8use hypercall_runtime_api::QuoteProviderConfig;
9use hypercall_types::{
10 QpStatus, QpTier, QuoteProviderResponse, RegisterQuoteProviderRequest, WalletAddress,
11};
12use rust_decimal::Decimal;
13use std::str::FromStr;
14
15use crate::state::AdminState;
16use hypercall_runtime_api::error::ApiError;
17use hypercall_runtime_api::sonic_json::SonicJson;
18
19pub async fn register_quote_provider(
21 State(state): State<AdminState>,
22 SonicJson(request): SonicJson<RegisterQuoteProviderRequest>,
23) -> Result<SonicJson<QuoteProviderResponse>, ApiError> {
24 let tier = match request.tier.as_str() {
25 "qp1" => QpTier::Qp1,
26 "qp2" => QpTier::Qp2,
27 "qp3_internal" => QpTier::Qp3Internal,
28 _ => return Err(ApiError::bad_request("Invalid tier")),
29 };
30
31 let max_notional_per_quote = request
32 .max_notional_per_quote
33 .parse::<Decimal>()
34 .map_err(|_| ApiError::bad_request("Invalid max_notional_per_quote"))?;
35 let max_open_notional = request
36 .max_open_notional
37 .parse::<Decimal>()
38 .map_err(|_| ApiError::bad_request("Invalid max_open_notional"))?;
39
40 let config = QuoteProviderConfig {
41 wallet: request.wallet_address,
42 tier,
43 status: QpStatus::Active,
44 allowed_underlyings: request.allowed_underlyings.clone(),
45 max_notional_per_quote,
46 max_open_notional,
47 };
48
49 state
50 .qp_cache
51 .register(config)
52 .await
53 .map_err(|e| ApiError::internal_error(format!("Failed to register QP: {}", e)))?;
54
55 Ok(SonicJson(QuoteProviderResponse {
56 wallet_address: request.wallet_address,
57 tier: request.tier,
58 status: "active".to_string(),
59 allowed_underlyings: request.allowed_underlyings,
60 max_notional_per_quote,
61 max_open_notional,
62 }))
63}
64
65pub async fn list_quote_providers(
67 State(state): State<AdminState>,
68) -> Result<SonicJson<Vec<QuoteProviderResponse>>, ApiError> {
69 let providers = state.qp_cache.get_all().await;
70 let responses = providers
71 .iter()
72 .map(|p| QuoteProviderResponse {
73 wallet_address: p.wallet,
74 tier: p.tier.as_str().to_string(),
75 status: match p.status {
76 QpStatus::Active => "active".to_string(),
77 QpStatus::Suspended => "suspended".to_string(),
78 },
79 allowed_underlyings: p.allowed_underlyings.clone(),
80 max_notional_per_quote: p.max_notional_per_quote,
81 max_open_notional: p.max_open_notional,
82 })
83 .collect();
84
85 Ok(SonicJson(responses))
86}
87
88pub async fn suspend_quote_provider(
90 State(state): State<AdminState>,
91 Path(wallet_hex): Path<String>,
92) -> Result<SonicJson<serde_json::Value>, ApiError> {
93 let wallet = WalletAddress::from_str(&wallet_hex)
94 .map_err(|_| ApiError::bad_request("Invalid wallet address"))?;
95
96 state
97 .qp_cache
98 .update_status(&wallet, QpStatus::Suspended)
99 .await
100 .map_err(|e| ApiError::internal_error(format!("Failed to suspend QP: {}", e)))?;
101
102 Ok(SonicJson(serde_json::json!({
103 "success": true,
104 "wallet": wallet_hex,
105 "status": "suspended"
106 })))
107}