1use std::time::Duration;
2
3use reqwest::{Client, Method, Response, StatusCode};
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6
7use crate::config::Config;
8use crate::error::KobeApiError;
9use crate::types::*;
10
11#[derive(Debug, Clone)]
13pub struct KobeClient {
14 client: Client,
15 config: Config,
16}
17
18impl KobeClient {
19 pub fn new(config: Config) -> Self {
21 let client = Client::builder()
22 .timeout(config.timeout)
23 .user_agent(&config.user_agent)
24 .build()
25 .expect("Failed to build HTTP client");
26
27 Self { client, config }
28 }
29
30 pub fn mainnet() -> Self {
32 Self::new(Config::mainnet())
33 }
34
35 pub fn base_url(&self) -> &str {
37 &self.config.base_url
38 }
39
40 async fn get<T: DeserializeOwned>(
46 &self,
47 endpoint: &str,
48 query: &str,
49 ) -> Result<T, KobeApiError> {
50 let url = format!(
51 "{}/api/{}{}{}",
52 self.config.base_url,
53 crate::API_VERSION,
54 endpoint,
55 query
56 );
57 self.request(Method::GET, &url, None::<&()>).await
58 }
59
60 async fn post<B: Serialize, T: DeserializeOwned>(
62 &self,
63 endpoint: &str,
64 body: Option<&B>,
65 ) -> Result<T, KobeApiError> {
66 let url = format!(
67 "{}/api/{}{}",
68 self.config.base_url,
69 crate::API_VERSION,
70 endpoint
71 );
72 self.request(Method::POST, &url, body).await
73 }
74
75 async fn request<B: Serialize, T: DeserializeOwned>(
77 &self,
78 method: Method,
79 url: &str,
80 body: Option<&B>,
81 ) -> Result<T, KobeApiError> {
82 let mut retries = 0;
83 let max_retries = if self.config.retry_enabled {
84 self.config.max_retries
85 } else {
86 0
87 };
88
89 loop {
90 let mut request = self.client.request(method.clone(), url);
91
92 if let Some(body) = body {
93 request = request.json(body);
94 }
95
96 let response = request.send().await?;
97
98 match self.handle_response(response).await {
99 Ok(data) => return Ok(data),
100 Err(e) => {
101 if retries >= max_retries || !self.should_retry(&e) {
102 return Err(e);
103 }
104 retries += 1;
105 let delay = Duration::from_millis(100 * 2u64.pow(retries));
107 tokio::time::sleep(delay).await;
108 }
109 }
110 }
111 }
112
113 async fn handle_response<T: DeserializeOwned>(
115 &self,
116 response: Response,
117 ) -> Result<T, KobeApiError> {
118 let status = response.status();
119
120 if status.is_success() {
121 response.json::<T>().await.map_err(Into::into)
122 } else {
123 let status_code = status.as_u16();
124 let error_text = response
125 .text()
126 .await
127 .unwrap_or_else(|_| "Unknown error".to_string());
128
129 match status {
130 StatusCode::NOT_FOUND => Err(KobeApiError::NotFound(error_text)),
131 StatusCode::TOO_MANY_REQUESTS => Err(KobeApiError::RateLimitExceeded),
132 StatusCode::REQUEST_TIMEOUT => Err(KobeApiError::Timeout),
133 _ => Err(KobeApiError::api_error(status_code, error_text)),
134 }
135 }
136 }
137
138 fn should_retry(&self, error: &KobeApiError) -> bool {
140 matches!(error, KobeApiError::Timeout | KobeApiError::HttpError(_))
141 }
142
143 pub async fn get_staker_rewards(
167 &self,
168 limit: Option<u32>,
169 ) -> Result<StakerRewardsResponse, KobeApiError> {
170 let query = if let Some(limit) = limit {
171 format!("?limit={}", limit)
172 } else {
173 String::new()
174 };
175 self.get("/staker_rewards", &query).await
176 }
177
178 pub async fn get_staker_rewards_with_params(
180 &self,
181 params: &QueryParams,
182 ) -> Result<StakerRewardsResponse, KobeApiError> {
183 self.get("/staker_rewards", ¶ms.to_query_string()).await
184 }
185
186 pub async fn get_validator_rewards(
195 &self,
196 epoch: Option<u64>,
197 limit: Option<u32>,
198 ) -> Result<ValidatorRewardsResponse, KobeApiError> {
199 let mut params = Vec::new();
200
201 if let Some(epoch) = epoch {
202 params.push(format!("epoch={}", epoch));
203 }
204 if let Some(limit) = limit {
205 params.push(format!("limit={}", limit));
206 }
207
208 let query = if params.is_empty() {
209 String::new()
210 } else {
211 format!("?{}", params.join("&"))
212 };
213
214 self.get("/validator_rewards", &query).await
215 }
216
217 pub async fn get_validators(
229 &self,
230 epoch: Option<u64>,
231 ) -> Result<ValidatorsResponse, KobeApiError> {
232 if let Some(epoch) = epoch {
233 self.post("/validators", Some(&EpochRequest { epoch }))
234 .await
235 } else {
236 self.post::<EpochRequest, _>("/validators", None).await
237 }
238 }
239
240 pub async fn get_jitosol_validators(
244 &self,
245 epoch: Option<u64>,
246 ) -> Result<ValidatorsResponse, KobeApiError> {
247 if let Some(epoch) = epoch {
248 self.post("/jitosol_validators", Some(&EpochRequest { epoch }))
249 .await
250 } else {
251 self.post::<EpochRequest, _>("/jitosol_validators", None)
252 .await
253 }
254 }
255
256 pub async fn get_validator_history(
264 &self,
265 vote_account: &str,
266 ) -> Result<Vec<ValidatorHistory>, KobeApiError> {
267 self.get(&format!("/validators/{}", vote_account), "").await
268 }
269
270 pub async fn get_mev_rewards(&self, epoch: Option<u64>) -> Result<MevRewards, KobeApiError> {
278 if let Some(epoch) = epoch {
279 self.post("/mev_rewards", Some(&EpochRequest { epoch }))
280 .await
281 } else {
282 self.get("/mev_rewards", "").await
284 }
285 }
286
287 pub async fn get_daily_mev_tips(&self) -> Result<Vec<DailyMevTips>, KobeApiError> {
291 self.get("/daily_mev_tips", "").await
292 }
293
294 pub async fn get_jito_stake_over_time(&self) -> Result<JitoStakeOverTime, KobeApiError> {
298 self.get("/jito_stake_over_time", "").await
299 }
300
301 pub async fn get_mev_commission_average_over_time(
305 &self,
306 ) -> Result<MevCommissionAverageOverTime, KobeApiError> {
307 self.get("/mev_commission_average_over_time", "").await
308 }
309
310 pub async fn get_jitosol_sol_ratio(
317 &self,
318 start: chrono::DateTime<chrono::Utc>,
319 end: chrono::DateTime<chrono::Utc>,
320 ) -> Result<JitoSolRatio, KobeApiError> {
321 let request = RangeRequest {
322 range_filter: RangeFilter { start, end },
323 };
324 self.post("/jitosol_sol_ratio", Some(&request)).await
325 }
326
327 pub async fn get_stake_pool_stats(
333 &self,
334 epoch: Option<u64>,
335 ) -> Result<StakePoolStats, KobeApiError> {
336 if let Some(epoch) = epoch {
337 self.post("/stake_pool_stats", Some(&EpochRequest { epoch }))
338 .await
339 } else {
340 self.get("/stake_pool_stats", "").await
341 }
342 }
343
344 pub async fn get_current_epoch(&self) -> Result<u64, KobeApiError> {
350 let mev_rewards = self.get_mev_rewards(None).await?;
351 Ok(mev_rewards.epoch)
352 }
353
354 pub async fn get_jito_validators(&self) -> Result<Vec<ValidatorInfo>, KobeApiError> {
356 let response: ValidatorsResponse = self.get("/validators", "").await?;
357 Ok(response
358 .validators
359 .into_iter()
360 .filter(|v| v.running_jito)
361 .collect())
362 }
363
364 pub async fn get_validators_by_mev_rewards(
366 &self,
367 epoch: Option<u64>,
368 limit: usize,
369 ) -> Result<Vec<ValidatorInfo>, KobeApiError> {
370 let mut response = self.get_validators(epoch).await?;
371 response
372 .validators
373 .sort_by(|a, b| b.mev_rewards.cmp(&a.mev_rewards));
374 Ok(response.validators.into_iter().take(limit).collect())
375 }
376
377 pub async fn get_validators_by_stake(
379 &self,
380 epoch: Option<u64>,
381 limit: usize,
382 ) -> Result<Vec<ValidatorInfo>, KobeApiError> {
383 let mut response = self.get_validators(epoch).await?;
384 response
385 .validators
386 .sort_by(|a, b| b.active_stake.cmp(&a.active_stake));
387 Ok(response.validators.into_iter().take(limit).collect())
388 }
389
390 pub async fn is_validator_running_jito(
392 &self,
393 vote_account: &str,
394 ) -> Result<bool, KobeApiError> {
395 let response = self.get_validators(None).await?;
396 Ok(response
397 .validators
398 .iter()
399 .find(|v| v.vote_account == vote_account)
400 .map(|v| v.running_jito)
401 .unwrap_or(false))
402 }
403
404 pub async fn get_validator_mev_commission(
406 &self,
407 vote_account: &str,
408 ) -> Result<Option<u16>, KobeApiError> {
409 let response = self.get_validators(None).await?;
410 Ok(response
411 .validators
412 .iter()
413 .find(|v| v.vote_account == vote_account)
414 .map(|v| v.mev_commission_bps))
415 }
416
417 pub async fn calculate_total_mev_rewards(
419 &self,
420 start_epoch: u64,
421 end_epoch: u64,
422 ) -> Result<u64, KobeApiError> {
423 let mut total = 0u64;
424
425 for epoch in start_epoch..=end_epoch {
426 if let Ok(mev_rewards) = self.get_mev_rewards(Some(epoch)).await {
427 total = total.saturating_add(mev_rewards.total_network_mev_lamports);
428 }
429 }
430
431 Ok(total)
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use std::time::Duration;
438
439 use crate::{client_builder::KobeApiClientBuilder, types::QueryParams};
440
441 use super::Config;
442
443 #[test]
444 fn test_config_builder() {
445 let config = Config::mainnet()
446 .with_timeout(Duration::from_secs(60))
447 .with_user_agent("test-agent")
448 .with_retry(false);
449
450 assert_eq!(config.timeout, Duration::from_secs(60));
451 assert_eq!(config.user_agent, "test-agent");
452 assert!(!config.retry_enabled);
453 }
454
455 #[test]
456 fn test_query_params() {
457 let params = QueryParams::default().limit(10).offset(20).epoch(600);
458
459 let query = params.to_query_string();
460 assert!(query.contains("limit=10"));
461 assert!(query.contains("offset=20"));
462 assert!(query.contains("epoch=600"));
463 }
464
465 #[test]
466 fn test_client_builder() {
467 let client = KobeApiClientBuilder::new()
468 .timeout(Duration::from_secs(45))
469 .retry(true)
470 .max_retries(5)
471 .build();
472
473 assert_eq!(client.config.timeout, Duration::from_secs(45));
474 assert!(client.config.retry_enabled);
475 assert_eq!(client.config.max_retries, 5);
476 }
477}