1pub mod address;
7pub mod address_book;
8pub mod compliance;
9pub mod config_status;
10pub mod contract;
11pub mod crawl;
12pub mod discover;
13pub mod exchange;
14pub mod export;
15pub mod insights;
16pub mod market;
17pub mod token_health;
18pub mod tx;
19pub mod venues;
20
21use crate::web::AppState;
22use axum::Router;
23use std::sync::Arc;
24
25#[derive(Debug)]
32pub struct ResolvedInput {
33 pub value: String,
35 pub chain: Option<String>,
37}
38
39pub fn resolve_address_book(
49 input: &str,
50 config: &crate::config::Config,
51) -> Result<ResolvedInput, String> {
52 match crate::cli::address_book::resolve_address_book_input(input, config) {
53 Ok(Some((address, chain))) => Ok(ResolvedInput {
54 value: address,
55 chain: Some(chain),
56 }),
57 Ok(None) => Ok(ResolvedInput {
58 value: input.to_string(),
59 chain: None,
60 }),
61 Err(e) => Err(e.to_string()),
62 }
63}
64
65pub fn routes(state: Arc<AppState>) -> Router<Arc<AppState>> {
67 Router::new()
68 .route("/address", axum::routing::post(address::handle))
69 .route("/tx", axum::routing::post(tx::handle))
70 .route("/insights", axum::routing::post(insights::handle))
71 .route("/crawl", axum::routing::post(crawl::handle))
72 .route("/discover", axum::routing::get(discover::handle))
73 .route("/token-health", axum::routing::post(token_health::handle))
74 .route("/market/summary", axum::routing::post(market::handle))
75 .route(
76 "/address-book/list",
77 axum::routing::get(address_book::handle_list),
78 )
79 .route(
80 "/address-book/add",
81 axum::routing::post(address_book::handle_add),
82 )
83 .route(
84 "/address-book/remove",
85 axum::routing::post(address_book::handle_remove),
86 )
87 .route("/contract", axum::routing::post(contract::handle))
88 .route("/export", axum::routing::post(export::handle))
89 .route(
90 "/compliance/risk",
91 axum::routing::post(compliance::handle_risk),
92 )
93 .route("/config/status", axum::routing::get(config_status::handle))
94 .route("/config", axum::routing::post(config_status::handle_save))
95 .route("/venues", axum::routing::get(venues::handle))
96 .route("/exchange/snapshot", axum::routing::post(exchange::handle))
97 .route(
98 "/exchange/trades",
99 axum::routing::post(exchange::handle_trades),
100 )
101 .route("/exchange/ohlc", axum::routing::post(exchange::handle_ohlc))
102 .with_state(state)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::chains::DefaultClientFactory;
109 use crate::cli::address_book::{AddressBook, WatchedAddress};
110 use crate::config::Config;
111
112 #[test]
113 fn test_routes_construction() {
114 let config = Config::default();
115 let http: std::sync::Arc<dyn crate::http::HttpClient> =
116 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
117 let factory = DefaultClientFactory {
118 chains_config: config.chains.clone(),
119 http,
120 };
121 let state = Arc::new(AppState { config, factory });
122 let _router = routes(state);
123 }
125
126 #[test]
127 fn test_resolve_address_book_label_found() {
128 let tmp = tempfile::tempdir().unwrap();
129 let mut ab = AddressBook::default();
130 ab.add_address(WatchedAddress {
131 address: "0xABCDEF1234567890abcdef1234567890ABCDEF12".to_string(),
132 label: Some("hot-wallet".to_string()),
133 chain: "polygon".to_string(),
134 tags: vec![],
135 added_at: 0,
136 })
137 .unwrap();
138 ab.save(&tmp.path().to_path_buf()).unwrap();
139
140 let config = Config {
141 address_book: crate::config::AddressBookConfig {
142 data_dir: Some(tmp.path().to_path_buf()),
143 },
144 ..Default::default()
145 };
146 let result = resolve_address_book("@hot-wallet", &config);
147 assert!(result.is_ok());
148 let r = result.unwrap();
149 assert_eq!(r.value, "0xABCDEF1234567890abcdef1234567890ABCDEF12");
150 assert_eq!(r.chain, Some("polygon".to_string()));
151 }
152
153 #[test]
154 fn test_resolve_address_book_label_not_found() {
155 let tmp = tempfile::tempdir().unwrap();
156 let config = Config {
157 address_book: crate::config::AddressBookConfig {
158 data_dir: Some(tmp.path().to_path_buf()),
159 },
160 ..Default::default()
161 };
162 let result = resolve_address_book("@nonexistent", &config);
163 assert!(result.is_err());
164 assert!(result.unwrap_err().contains("@nonexistent"));
165 }
166
167 #[test]
168 fn test_resolve_address_book_raw_address_passthrough() {
169 let tmp = tempfile::tempdir().unwrap();
170 let config = Config {
171 address_book: crate::config::AddressBookConfig {
172 data_dir: Some(tmp.path().to_path_buf()),
173 },
174 ..Default::default()
175 };
176 let result = resolve_address_book("0xSomeRawAddress", &config);
177 assert!(result.is_ok());
178 let r = result.unwrap();
179 assert_eq!(r.value, "0xSomeRawAddress");
180 assert_eq!(r.chain, None);
181 }
182
183 #[test]
184 fn test_resolve_address_book_address_match_returns_chain() {
185 let tmp = tempfile::tempdir().unwrap();
186 let mut ab = AddressBook::default();
187 ab.add_address(WatchedAddress {
188 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
189 label: Some("main".to_string()),
190 chain: "arbitrum".to_string(),
191 tags: vec![],
192 added_at: 0,
193 })
194 .unwrap();
195 ab.save(&tmp.path().to_path_buf()).unwrap();
196
197 let config = Config {
198 address_book: crate::config::AddressBookConfig {
199 data_dir: Some(tmp.path().to_path_buf()),
200 },
201 ..Default::default()
202 };
203 let result = resolve_address_book("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", &config);
205 assert!(result.is_ok());
206 let r = result.unwrap();
207 assert_eq!(r.value, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
208 assert_eq!(r.chain, Some("arbitrum".to_string()));
209 }
210
211 #[test]
212 fn test_resolve_address_book_token_symbol_passthrough() {
213 let tmp = tempfile::tempdir().unwrap();
214 let config = Config {
215 address_book: crate::config::AddressBookConfig {
216 data_dir: Some(tmp.path().to_path_buf()),
217 },
218 ..Default::default()
219 };
220 let result = resolve_address_book("USDC", &config);
222 assert!(result.is_ok());
223 let r = result.unwrap();
224 assert_eq!(r.value, "USDC");
225 assert_eq!(r.chain, None);
226 }
227}