scope/web/api/
discover.rs1use crate::chains::DexClient;
4use crate::web::AppState;
5use axum::Json;
6use axum::extract::{Query, State};
7use axum::http::StatusCode;
8use axum::response::IntoResponse;
9use serde::Deserialize;
10use std::sync::Arc;
11
12#[derive(Debug, Deserialize)]
14pub struct DiscoverQuery {
15 #[serde(default = "default_source")]
17 pub source: String,
18 pub chain: Option<String>,
20 #[serde(default = "default_limit")]
22 pub limit: u32,
23}
24
25fn default_source() -> String {
26 "profiles".to_string()
27}
28
29fn default_limit() -> u32 {
30 15
31}
32
33pub async fn handle(
35 State(_state): State<Arc<AppState>>,
36 Query(params): Query<DiscoverQuery>,
37) -> impl IntoResponse {
38 let client = DexClient::new();
39
40 let tokens = match params.source.as_str() {
41 "boosts" => client.get_token_boosts().await,
42 "top-boosts" => client.get_token_boosts_top().await,
43 _ => client.get_token_profiles().await,
44 };
45
46 match tokens {
47 Ok(tokens) => {
48 let filtered: Vec<_> = if let Some(ref chain) = params.chain {
49 let c = chain.to_lowercase();
50 tokens
51 .into_iter()
52 .filter(|t| t.chain_id.to_lowercase() == c)
53 .take(params.limit as usize)
54 .collect()
55 } else {
56 tokens.into_iter().take(params.limit as usize).collect()
57 };
58 Json(serde_json::json!(filtered)).into_response()
59 }
60 Err(e) => (
61 StatusCode::INTERNAL_SERVER_ERROR,
62 Json(serde_json::json!({ "error": e.to_string() })),
63 )
64 .into_response(),
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn test_deserialize_full() {
74 let json = serde_json::json!({
75 "source": "boosts",
76 "chain": "ethereum",
77 "limit": 25
78 });
79 let req: DiscoverQuery = serde_json::from_value(json).unwrap();
80 assert_eq!(req.source, "boosts");
81 assert_eq!(req.chain, Some("ethereum".to_string()));
82 assert_eq!(req.limit, 25);
83 }
84
85 #[test]
86 fn test_deserialize_minimal() {
87 let json = serde_json::json!({});
88 let req: DiscoverQuery = serde_json::from_value(json).unwrap();
89 assert_eq!(req.source, "profiles");
90 assert_eq!(req.chain, None);
91 assert_eq!(req.limit, 15);
92 }
93
94 #[test]
95 fn test_defaults() {
96 assert_eq!(default_source(), "profiles");
97 assert_eq!(default_limit(), 15);
98 }
99
100 #[test]
101 fn test_with_chain_filter() {
102 let json = serde_json::json!({
103 "chain": "polygon",
104 "limit": 10
105 });
106 let req: DiscoverQuery = serde_json::from_value(json).unwrap();
107 assert_eq!(req.source, "profiles");
108 assert_eq!(req.chain, Some("polygon".to_string()));
109 assert_eq!(req.limit, 10);
110 }
111
112 #[tokio::test]
113 async fn test_handle_discover_profiles() {
114 use crate::chains::DefaultClientFactory;
115 use crate::config::Config;
116 use crate::web::AppState;
117 use axum::extract::{Query, State};
118 use axum::response::IntoResponse;
119
120 let config = Config::default();
121 let http: std::sync::Arc<dyn crate::http::HttpClient> =
122 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
123 let factory = DefaultClientFactory {
124 chains_config: config.chains.clone(),
125 http,
126 };
127 let state = std::sync::Arc::new(AppState { config, factory });
128
129 let params = DiscoverQuery {
130 source: "profiles".to_string(),
131 chain: None,
132 limit: 5,
133 };
134 let response = handle(State(state), Query(params)).await.into_response();
135 let status = response.status();
136 assert!(status.is_success() || status.is_server_error());
137 }
138
139 #[tokio::test]
140 async fn test_handle_discover_boosts() {
141 use crate::chains::DefaultClientFactory;
142 use crate::config::Config;
143 use crate::web::AppState;
144 use axum::extract::{Query, State};
145 use axum::response::IntoResponse;
146
147 let config = Config::default();
148 let http: std::sync::Arc<dyn crate::http::HttpClient> =
149 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
150 let factory = DefaultClientFactory {
151 chains_config: config.chains.clone(),
152 http,
153 };
154 let state = std::sync::Arc::new(AppState { config, factory });
155
156 let params = DiscoverQuery {
157 source: "boosts".to_string(),
158 chain: None,
159 limit: 5,
160 };
161 let response = handle(State(state), Query(params)).await.into_response();
162 let status = response.status();
163 assert!(status.is_success() || status.is_server_error());
164 }
165
166 #[tokio::test]
167 async fn test_handle_discover_top_boosts() {
168 use crate::chains::DefaultClientFactory;
169 use crate::config::Config;
170 use crate::web::AppState;
171 use axum::extract::{Query, State};
172 use axum::response::IntoResponse;
173
174 let config = Config::default();
175 let http: std::sync::Arc<dyn crate::http::HttpClient> =
176 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
177 let factory = DefaultClientFactory {
178 chains_config: config.chains.clone(),
179 http,
180 };
181 let state = std::sync::Arc::new(AppState { config, factory });
182
183 let params = DiscoverQuery {
184 source: "top-boosts".to_string(),
185 chain: None,
186 limit: 5,
187 };
188 let response = handle(State(state), Query(params)).await.into_response();
189 let status = response.status();
190 assert!(status.is_success() || status.is_server_error());
191 }
192
193 #[tokio::test]
194 async fn test_handle_discover_with_chain_filter() {
195 use crate::chains::DefaultClientFactory;
196 use crate::config::Config;
197 use crate::web::AppState;
198 use axum::extract::{Query, State};
199 use axum::response::IntoResponse;
200
201 let config = Config::default();
202 let http: std::sync::Arc<dyn crate::http::HttpClient> =
203 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
204 let factory = DefaultClientFactory {
205 chains_config: config.chains.clone(),
206 http,
207 };
208 let state = std::sync::Arc::new(AppState { config, factory });
209
210 let params = DiscoverQuery {
211 source: "profiles".to_string(),
212 chain: Some("ethereum".to_string()),
213 limit: 10,
214 };
215 let response = handle(State(state), Query(params)).await.into_response();
216 let status = response.status();
217 assert!(status.is_success() || status.is_server_error());
218 }
219
220 #[tokio::test]
221 async fn test_handle_discover_error_path() {
222 use crate::chains::DefaultClientFactory;
223 use crate::config::Config;
224 use crate::web::AppState;
225 use axum::extract::{Query, State};
226 use axum::http::StatusCode;
227 use axum::response::IntoResponse;
228
229 let config = Config::default();
230 let http: std::sync::Arc<dyn crate::http::HttpClient> =
231 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
232 let factory = DefaultClientFactory {
233 chains_config: config.chains.clone(),
234 http,
235 };
236 let state = std::sync::Arc::new(AppState { config, factory });
237
238 let params = DiscoverQuery {
239 source: "unknown-source".to_string(),
240 chain: None,
241 limit: 5,
242 };
243 let response = handle(State(state), Query(params)).await.into_response();
244 let status = response.status();
245 if status == StatusCode::INTERNAL_SERVER_ERROR {
246 let body = axum::body::to_bytes(response.into_body(), 1_000_000)
247 .await
248 .unwrap();
249 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
250 assert!(json.get("error").is_some());
251 }
252 }
253}