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