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 factory = DefaultClientFactory {
116 chains_config: config.chains.clone(),
117 };
118 let state = Arc::new(AppState { config, factory });
119 let _router = routes(state);
120 }
122
123 #[test]
124 fn test_resolve_address_book_label_found() {
125 let tmp = tempfile::tempdir().unwrap();
126 let mut ab = AddressBook::default();
127 ab.add_address(WatchedAddress {
128 address: "0xABCDEF1234567890abcdef1234567890ABCDEF12".to_string(),
129 label: Some("hot-wallet".to_string()),
130 chain: "polygon".to_string(),
131 tags: vec![],
132 added_at: 0,
133 })
134 .unwrap();
135 ab.save(&tmp.path().to_path_buf()).unwrap();
136
137 let config = Config {
138 address_book: crate::config::AddressBookConfig {
139 data_dir: Some(tmp.path().to_path_buf()),
140 },
141 ..Default::default()
142 };
143 let result = resolve_address_book("@hot-wallet", &config);
144 assert!(result.is_ok());
145 let r = result.unwrap();
146 assert_eq!(r.value, "0xABCDEF1234567890abcdef1234567890ABCDEF12");
147 assert_eq!(r.chain, Some("polygon".to_string()));
148 }
149
150 #[test]
151 fn test_resolve_address_book_label_not_found() {
152 let tmp = tempfile::tempdir().unwrap();
153 let config = Config {
154 address_book: crate::config::AddressBookConfig {
155 data_dir: Some(tmp.path().to_path_buf()),
156 },
157 ..Default::default()
158 };
159 let result = resolve_address_book("@nonexistent", &config);
160 assert!(result.is_err());
161 assert!(result.unwrap_err().contains("@nonexistent"));
162 }
163
164 #[test]
165 fn test_resolve_address_book_raw_address_passthrough() {
166 let tmp = tempfile::tempdir().unwrap();
167 let config = Config {
168 address_book: crate::config::AddressBookConfig {
169 data_dir: Some(tmp.path().to_path_buf()),
170 },
171 ..Default::default()
172 };
173 let result = resolve_address_book("0xSomeRawAddress", &config);
174 assert!(result.is_ok());
175 let r = result.unwrap();
176 assert_eq!(r.value, "0xSomeRawAddress");
177 assert_eq!(r.chain, None);
178 }
179
180 #[test]
181 fn test_resolve_address_book_address_match_returns_chain() {
182 let tmp = tempfile::tempdir().unwrap();
183 let mut ab = AddressBook::default();
184 ab.add_address(WatchedAddress {
185 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
186 label: Some("main".to_string()),
187 chain: "arbitrum".to_string(),
188 tags: vec![],
189 added_at: 0,
190 })
191 .unwrap();
192 ab.save(&tmp.path().to_path_buf()).unwrap();
193
194 let config = Config {
195 address_book: crate::config::AddressBookConfig {
196 data_dir: Some(tmp.path().to_path_buf()),
197 },
198 ..Default::default()
199 };
200 let result = resolve_address_book("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", &config);
202 assert!(result.is_ok());
203 let r = result.unwrap();
204 assert_eq!(r.value, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
205 assert_eq!(r.chain, Some("arbitrum".to_string()));
206 }
207
208 #[test]
209 fn test_resolve_address_book_token_symbol_passthrough() {
210 let tmp = tempfile::tempdir().unwrap();
211 let config = Config {
212 address_book: crate::config::AddressBookConfig {
213 data_dir: Some(tmp.path().to_path_buf()),
214 },
215 ..Default::default()
216 };
217 let result = resolve_address_book("USDC", &config);
219 assert!(result.is_ok());
220 let r = result.unwrap();
221 assert_eq!(r.value, "USDC");
222 assert_eq!(r.chain, None);
223 }
224}