Skip to main content

scope_web/api/
venues.rs

1//! Venue listing API handler.
2//!
3//! GET /api/venues — Returns every market venue (`MarketVenue::listings`):
4//! registry venues with their capabilities, then DEX venues (`kind: "dex"`).
5
6use axum::Json;
7use axum::response::IntoResponse;
8use scope::market::{MarketVenue, VenueRegistry};
9
10use super::{error_response, status_for};
11
12/// GET /api/venues — List available exchange venues.
13pub async fn handle() -> impl IntoResponse {
14    let registry = match VenueRegistry::load() {
15        Ok(r) => r,
16        Err(e) => {
17            return error_response(
18                status_for(&e),
19                format!("Failed to load venue registry: {e}"),
20            );
21        }
22    };
23
24    // One list: registry venues, then DEX venues, each with its `kind`.
25    let venues = MarketVenue::listings(&registry);
26    let output = serde_json::json!({
27        "total": venues.len(),
28        "venues": venues,
29        "user_venues_dir": VenueRegistry::user_venues_dir().display().to_string(),
30    });
31
32    Json(output).into_response()
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use axum::response::IntoResponse;
39
40    #[tokio::test]
41    async fn test_handle_venues() {
42        let response = handle().await.into_response();
43        let status = response.status();
44        assert!(status.is_success());
45    }
46
47    #[tokio::test]
48    async fn test_handle_venues_returns_all_built_in() {
49        let response = handle().await.into_response();
50        let status = response.status();
51        assert!(status.is_success());
52
53        // Extract body and verify structure
54        let body = axum::body::to_bytes(response.into_body(), 1024 * 1024)
55            .await
56            .unwrap();
57        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
58        assert!(json["total"].as_u64().unwrap() >= 11);
59        assert!(json["venues"].is_array());
60        let venues = json["venues"].as_array().unwrap();
61        assert!(venues.iter().any(|v| v["id"] == "binance"));
62        assert!(venues.iter().any(|v| v["id"] == "kraken"));
63        // Verify each venue has expected fields
64        for venue in venues {
65            assert!(venue["id"].is_string());
66            assert!(venue["name"].is_string());
67            // Only a registry (CEX) venue has an API base URL.
68            assert_eq!(venue["base_url"].is_string(), venue["kind"] == "cex");
69            assert!(venue["capabilities"].is_array());
70        }
71    }
72
73    #[tokio::test]
74    async fn test_handle_venues_response_structure() {
75        let response = handle().await.into_response();
76        assert!(response.status().is_success());
77        let body = axum::body::to_bytes(response.into_body(), 1024 * 1024)
78            .await
79            .unwrap();
80        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
81        assert!(json["total"].as_u64().unwrap() >= 11);
82        let venues = json["venues"].as_array().unwrap();
83        for v in venues {
84            assert!(v["id"].is_string());
85            assert!(v["name"].is_string());
86            assert!(v["capabilities"].is_array());
87        }
88        assert!(json["user_venues_dir"].is_string());
89    }
90
91    /// The UI reads DEX venues from here instead of hard-coding them.
92    #[tokio::test]
93    async fn test_handle_venues_lists_dex_venues() {
94        let response = handle().await.into_response();
95        let body = axum::body::to_bytes(response.into_body(), 1024 * 1024)
96            .await
97            .unwrap();
98        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
99        let dex: Vec<&serde_json::Value> = json["venues"]
100            .as_array()
101            .unwrap()
102            .iter()
103            .filter(|v| v["kind"] == "dex")
104            .collect();
105        let ids: Vec<&str> = dex.iter().map(|v| v["id"].as_str().unwrap()).collect();
106        assert_eq!(ids, ["ethereum", "solana"]);
107        assert_eq!(dex[1]["name"], "Solana DEX");
108        assert_eq!(dex[1]["chain"], "solana");
109    }
110}