Skip to main content

hypercall_admin/
rfq.rs

1//! RFQ quote-provider admin endpoints.
2//!
3//! These operate on the quote-provider cache exposed through [`AdminState`].
4//! The shared RFQ business logic (matching, fan-out) remains in
5//! `handlers::rfq`; only the admin register/list/suspend surface lives here.
6
7use 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
19/// POST /admin/rfq/quote-providers - Register or update a quote provider.
20pub 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
65/// GET /admin/rfq/quote-providers - List all quote providers
66pub 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
88/// DELETE /admin/rfq/quote-providers/:wallet - Suspend a quote provider
89pub 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}