1use std::time::Duration;
2
3use reqwest::{Client, Method, Response, StatusCode};
4use serde::{Serialize, de::DeserializeOwned};
5
6use crate::{config::Config, error::KobeApiError, types::*};
7
8#[derive(Debug, Clone)]
10pub struct KobeClient {
11 client: Client,
12 config: Config,
13}
14
15impl KobeClient {
16 pub fn new(config: Config) -> Self {
18 let client = Client::builder()
19 .timeout(config.timeout)
20 .user_agent(&config.user_agent)
21 .build()
22 .expect("Failed to build HTTP client");
23
24 Self { client, config }
25 }
26
27 pub fn mainnet() -> Self {
29 Self::new(Config::mainnet())
30 }
31
32 pub fn base_url(&self) -> &str {
34 &self.config.base_url
35 }
36
37 async fn get<T: DeserializeOwned>(
39 &self,
40 endpoint: &str,
41 query: &str,
42 ) -> Result<T, KobeApiError> {
43 let url = format!(
44 "{}/api/{}{}{}",
45 self.config.base_url,
46 crate::API_VERSION,
47 endpoint,
48 query
49 );
50 self.request(Method::GET, &url, None::<&()>).await
51 }
52
53 async fn post<B: Serialize, T: DeserializeOwned>(
55 &self,
56 endpoint: &str,
57 body: Option<&B>,
58 ) -> Result<T, KobeApiError> {
59 let url = format!(
60 "{}/api/{}{}",
61 self.config.base_url,
62 crate::API_VERSION,
63 endpoint
64 );
65 self.request(Method::POST, &url, body).await
66 }
67
68 async fn request<B: Serialize, T: DeserializeOwned>(
70 &self,
71 method: Method,
72 url: &str,
73 body: Option<&B>,
74 ) -> Result<T, KobeApiError> {
75 let mut retries = 0;
76 let max_retries = if self.config.retry_enabled {
77 self.config.max_retries
78 } else {
79 0
80 };
81
82 loop {
83 let mut request = self.client.request(method.clone(), url);
84
85 if let Some(body) = body {
86 request = request.json(body);
87 }
88
89 let response = request.send().await?;
90
91 match self.handle_response(response).await {
92 Ok(data) => return Ok(data),
93 Err(e) => {
94 if retries >= max_retries || !self.should_retry(&e) {
95 return Err(e);
96 }
97 retries += 1;
98 let delay = Duration::from_millis(100 * 2u64.pow(retries));
100 tokio::time::sleep(delay).await;
101 }
102 }
103 }
104 }
105
106 async fn handle_response<T: DeserializeOwned>(
108 &self,
109 response: Response,
110 ) -> Result<T, KobeApiError> {
111 let status = response.status();
112
113 if status.is_success() {
114 response.json::<T>().await.map_err(Into::into)
115 } else {
116 let status_code = status.as_u16();
117 let error_text = response
118 .text()
119 .await
120 .unwrap_or_else(|_| "Unknown error".to_string());
121
122 match status {
123 StatusCode::NOT_FOUND => Err(KobeApiError::NotFound(error_text)),
124 StatusCode::TOO_MANY_REQUESTS => Err(KobeApiError::RateLimitExceeded),
125 StatusCode::REQUEST_TIMEOUT => Err(KobeApiError::Timeout),
126 _ => Err(KobeApiError::api_error(status_code, error_text)),
127 }
128 }
129 }
130
131 fn should_retry(&self, error: &KobeApiError) -> bool {
133 matches!(error, KobeApiError::Timeout | KobeApiError::HttpError(_))
134 }
135
136 pub async fn get_staker_rewards(
156 &self,
157 limit: Option<u32>,
158 ) -> Result<StakerRewardsResponse, KobeApiError> {
159 let query = if let Some(limit) = limit {
160 format!("?limit={}", limit)
161 } else {
162 String::new()
163 };
164 self.get("/staker_rewards", &query).await
165 }
166
167 pub async fn get_staker_rewards_with_params(
169 &self,
170 params: &QueryParams,
171 ) -> Result<StakerRewardsResponse, KobeApiError> {
172 self.get("/staker_rewards", ¶ms.to_query_string()).await
173 }
174
175 pub async fn get_validator_rewards(
184 &self,
185 epoch: Option<u64>,
186 limit: Option<u32>,
187 ) -> Result<ValidatorRewardsResponse, KobeApiError> {
188 let mut params = Vec::new();
189
190 if let Some(epoch) = epoch {
191 params.push(format!("epoch={}", epoch));
192 }
193 if let Some(limit) = limit {
194 params.push(format!("limit={}", limit));
195 }
196
197 let query = if params.is_empty() {
198 String::new()
199 } else {
200 format!("?{}", params.join("&"))
201 };
202
203 self.get("/validator_rewards", &query).await
204 }
205
206 pub async fn get_validators(
214 &self,
215 epoch: Option<u64>,
216 ) -> Result<ValidatorsResponse, KobeApiError> {
217 if let Some(epoch) = epoch {
218 self.post("/validators", Some(&EpochRequest { epoch }))
219 .await
220 } else {
221 self.post::<EpochRequest, _>("/validators", None).await
222 }
223 }
224
225 pub async fn get_jitosol_validators(
229 &self,
230 epoch: Option<u64>,
231 ) -> Result<ValidatorsResponse, KobeApiError> {
232 if let Some(epoch) = epoch {
233 self.post("/jitosol_validators", Some(&EpochRequest { epoch }))
234 .await
235 } else {
236 self.post::<EpochRequest, _>("/jitosol_validators", None)
237 .await
238 }
239 }
240
241 pub async fn get_validator_info_by_vote_account(
249 &self,
250 vote_account: &str,
251 ) -> Result<Vec<ValidatorByVoteAccount>, KobeApiError> {
252 self.get(&format!("/validators/{}", vote_account), "").await
253 }
254
255 pub async fn get_mev_rewards(&self, epoch: Option<u64>) -> Result<MevRewards, KobeApiError> {
263 if let Some(epoch) = epoch {
264 self.post("/mev_rewards", Some(&EpochRequest { epoch }))
265 .await
266 } else {
267 self.get("/mev_rewards", "").await
269 }
270 }
271
272 pub async fn get_daily_mev_rewards(&self) -> Result<Vec<DailyMevRewards>, KobeApiError> {
276 self.get("/daily_mev_rewards", "").await
277 }
278
279 pub async fn get_jito_stake_over_time(&self) -> Result<JitoStakeOverTime, KobeApiError> {
283 self.get("/jito_stake_over_time", "").await
284 }
285
286 pub async fn get_mev_commission_average_over_time(
290 &self,
291 ) -> Result<MevCommissionAverageOverTime, KobeApiError> {
292 self.get("/mev_commission_average_over_time", "").await
293 }
294
295 pub async fn get_jitosol_sol_ratio(
302 &self,
303 start: chrono::DateTime<chrono::Utc>,
304 end: chrono::DateTime<chrono::Utc>,
305 ) -> Result<JitoSolRatio, KobeApiError> {
306 let request = RangeRequest {
307 range_filter: RangeFilter { start, end },
308 };
309 self.post("/jitosol_sol_ratio", Some(&request)).await
310 }
311
312 pub async fn get_stake_pool_stats(
317 &self,
318 request: Option<&StakePoolStatsRequest>,
319 ) -> Result<StakePoolStats, KobeApiError> {
320 if let Some(req) = request {
321 self.post("/stake_pool_stats", Some(req)).await
322 } else {
323 self.get("/stake_pool_stats", "").await
325 }
326 }
327
328 pub async fn get_current_epoch(&self) -> Result<u64, KobeApiError> {
330 let mev_rewards = self.get_mev_rewards(None).await?;
331 Ok(mev_rewards.epoch)
332 }
333
334 pub async fn get_jito_validators(&self) -> Result<Vec<ValidatorInfo>, KobeApiError> {
336 let response: ValidatorsResponse = self.get("/validators", "").await?;
337 Ok(response
338 .validators
339 .into_iter()
340 .filter(|v| v.running_jito)
341 .collect())
342 }
343
344 pub async fn get_validators_by_stake(
359 &self,
360 epoch: Option<u64>,
361 limit: usize,
362 ) -> Result<Vec<ValidatorInfo>, KobeApiError> {
363 let mut response = self.get_validators(epoch).await?;
364 response
365 .validators
366 .sort_by(|a, b| b.active_stake.cmp(&a.active_stake));
367 Ok(response.validators.into_iter().take(limit).collect())
368 }
369
370 pub async fn is_validator_running_jito(
372 &self,
373 vote_account: &str,
374 ) -> Result<bool, KobeApiError> {
375 let response = self.get_validators(None).await?;
376 Ok(response
377 .validators
378 .iter()
379 .find(|v| v.vote_account == vote_account)
380 .map(|v| v.running_jito)
381 .unwrap_or(false))
382 }
383
384 pub async fn calculate_total_mev_rewards(
399 &self,
400 start_epoch: u64,
401 end_epoch: u64,
402 ) -> Result<u64, KobeApiError> {
403 let mut total = 0u64;
404
405 for epoch in start_epoch..=end_epoch {
406 if let Ok(mev_rewards) = self.get_mev_rewards(Some(epoch)).await {
407 total = total.saturating_add(mev_rewards.total_network_mev_lamports);
408 }
409 }
410
411 Ok(total)
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use std::time::Duration;
418
419 use crate::{client_builder::KobeApiClientBuilder, types::QueryParams};
420
421 use super::Config;
422
423 #[test]
424 fn test_config_builder() {
425 let config = Config::mainnet()
426 .with_timeout(Duration::from_secs(60))
427 .with_user_agent("test-agent")
428 .with_retry(false);
429
430 assert_eq!(config.timeout, Duration::from_secs(60));
431 assert_eq!(config.user_agent, "test-agent");
432 assert!(!config.retry_enabled);
433 }
434
435 #[test]
436 fn test_query_params() {
437 let params = QueryParams::default().limit(10).offset(20).epoch(600);
438
439 let query = params.to_query_string();
440 assert!(query.contains("limit=10"));
441 assert!(query.contains("offset=20"));
442 assert!(query.contains("epoch=600"));
443 }
444
445 #[test]
446 fn test_client_builder() {
447 let client = KobeApiClientBuilder::new()
448 .timeout(Duration::from_secs(45))
449 .retry(true)
450 .max_retries(5)
451 .build();
452
453 assert_eq!(client.config.timeout, Duration::from_secs(45));
454 assert!(client.config.retry_enabled);
455 assert_eq!(client.config.max_retries, 5);
456 }
457}