1use crate::chains::ChainClientFactory;
4use crate::web::AppState;
5use axum::Json;
6use axum::extract::State;
7use axum::http::StatusCode;
8use axum::response::IntoResponse;
9use serde::Deserialize;
10use std::sync::Arc;
11
12#[derive(Debug, Deserialize)]
14pub struct ExportRequest {
15 pub address: String,
17 #[serde(default = "default_chain")]
19 pub chain: String,
20 #[serde(default = "default_format")]
22 pub format: String,
23 pub start_date: Option<String>,
25 pub end_date: Option<String>,
27}
28
29fn default_chain() -> String {
30 "ethereum".to_string()
31}
32fn default_format() -> String {
33 "json".to_string()
34}
35
36pub async fn handle(
42 State(state): State<Arc<AppState>>,
43 Json(req): Json<ExportRequest>,
44) -> impl IntoResponse {
45 let client: Box<dyn crate::chains::ChainClient> =
46 match state.factory.create_chain_client(&req.chain) {
47 Ok(c) => c,
48 Err(e) => {
49 return (
50 StatusCode::BAD_REQUEST,
51 Json(serde_json::json!({ "error": e.to_string() })),
52 )
53 .into_response();
54 }
55 };
56
57 let mut balance = match client.get_balance(&req.address).await {
59 Ok(b) => b,
60 Err(e) => {
61 return (
62 StatusCode::INTERNAL_SERVER_ERROR,
63 Json(serde_json::json!({ "error": e.to_string() })),
64 )
65 .into_response();
66 }
67 };
68 client.enrich_balance_usd(&mut balance).await;
69
70 let txs = client.get_transactions(&req.address, 100).await.ok();
72
73 let tokens = client.get_token_balances(&req.address).await.ok();
75
76 Json(serde_json::json!({
77 "address": req.address,
78 "chain": req.chain,
79 "format": req.format,
80 "balance": {
81 "raw": balance.raw,
82 "formatted": balance.formatted,
83 "usd_value": balance.usd_value,
84 },
85 "transactions": txs,
86 "tokens": tokens,
87 }))
88 .into_response()
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn test_deserialize_full() {
97 let json = serde_json::json!({
98 "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
99 "chain": "polygon",
100 "format": "csv",
101 "start_date": "2024-01-01T00:00:00Z",
102 "end_date": "2024-12-31T23:59:59Z"
103 });
104 let req: ExportRequest = serde_json::from_value(json).unwrap();
105 assert_eq!(req.address, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
106 assert_eq!(req.chain, "polygon");
107 assert_eq!(req.format, "csv");
108 assert_eq!(req.start_date, Some("2024-01-01T00:00:00Z".to_string()));
109 assert_eq!(req.end_date, Some("2024-12-31T23:59:59Z".to_string()));
110 }
111
112 #[test]
113 fn test_deserialize_minimal() {
114 let json = serde_json::json!({
115 "address": "0x1234567890123456789012345678901234567890"
116 });
117 let req: ExportRequest = serde_json::from_value(json).unwrap();
118 assert_eq!(req.address, "0x1234567890123456789012345678901234567890");
119 assert_eq!(req.chain, "ethereum");
120 assert_eq!(req.format, "json");
121 assert_eq!(req.start_date, None);
122 assert_eq!(req.end_date, None);
123 }
124
125 #[test]
126 fn test_defaults() {
127 assert_eq!(default_chain(), "ethereum");
128 assert_eq!(default_format(), "json");
129 }
130
131 #[test]
132 fn test_with_date_filters() {
133 let json = serde_json::json!({
134 "address": "0xabcdef1234567890abcdef1234567890abcdef1234",
135 "start_date": "2024-06-01T00:00:00Z",
136 "end_date": "2024-06-30T23:59:59Z"
137 });
138 let req: ExportRequest = serde_json::from_value(json).unwrap();
139 assert_eq!(req.address, "0xabcdef1234567890abcdef1234567890abcdef1234");
140 assert_eq!(req.chain, "ethereum");
141 assert_eq!(req.format, "json");
142 assert_eq!(req.start_date, Some("2024-06-01T00:00:00Z".to_string()));
143 assert_eq!(req.end_date, Some("2024-06-30T23:59:59Z".to_string()));
144 }
145
146 #[tokio::test]
147 async fn test_handle_export_direct() {
148 use crate::chains::DefaultClientFactory;
149 use crate::config::Config;
150 use crate::web::AppState;
151 use axum::extract::State;
152 use axum::response::IntoResponse;
153
154 let config = Config::default();
155 let factory = DefaultClientFactory {
156 chains_config: config.chains.clone(),
157 };
158 let state = std::sync::Arc::new(AppState { config, factory });
159 let req = ExportRequest {
160 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
161 chain: "ethereum".to_string(),
162 format: "json".to_string(),
163 start_date: None,
164 end_date: None,
165 };
166 let response = handle(State(state), axum::Json(req)).await.into_response();
167 let status = response.status();
168 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
169 }
170}