1use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5const BASE_URL: &str = "https://pro-api.coingecko.com/api/v3";
6
7#[derive(Debug, Clone)]
8pub struct CoinGeckoProClient {
9 api_key: String,
10 client: reqwest::Client,
11}
12
13#[derive(Debug, Serialize, Deserialize)]
14pub enum OrderType {
15 #[serde(rename = "market_cap_desc")]
16 MarketCapDesc,
17 #[serde(rename = "market_cap_asc")]
18 MarketCapAsc,
19 #[serde(rename = "gecko_desc")]
20 GeckoDesc,
21 #[serde(rename = "gecko_asc")]
22 GeckoAsc,
23 #[serde(rename = "volume_desc")]
24 VolumeDesc,
25 #[serde(rename = "volume_asc")]
26 VolumeAsc,
27}
28
29#[derive(Debug, Serialize, Deserialize)]
30pub enum PriceChangePercentage {
31 #[serde(rename = "1h")]
32 OneHour,
33 #[serde(rename = "24h")]
34 TwentyFourHours,
35 #[serde(rename = "7d")]
36 SevenDays,
37 #[serde(rename = "14d")]
38 FourteenDays,
39 #[serde(rename = "30d")]
40 ThirtyDays,
41 #[serde(rename = "200d")]
42 TwoHundredDays,
43 #[serde(rename = "1y")]
44 OneYear,
45}
46
47impl CoinGeckoProClient {
48 pub fn new(api_key: String) -> Self {
49 let client = reqwest::Client::new();
50 Self { api_key, client }
51 }
52
53 fn get_headers(&self) -> HeaderMap {
54 let mut headers = HeaderMap::new();
55 headers.insert(
56 "X-CG-Pro-API-Key",
57 HeaderValue::from_str(&self.api_key).unwrap(),
58 );
59 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
60 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
61 headers
62 }
63
64 async fn make_request(
65 &self,
66 endpoint: &str,
67 params: Option<HashMap<String, String>>,
68 ) -> Result<String, String> {
69 println!("Making coingecko request to {}", endpoint);
70 let url = format!("{}{}", BASE_URL, endpoint);
71 println!("URL: {} params {:?}", url, params);
72 let response = self
73 .client
74 .get(&url)
75 .headers(self.get_headers())
76 .query(¶ms.unwrap_or_default())
77 .send()
78 .await
79 .map_err(|e| e.to_string())?;
80 println!("Got response from {}", url);
81 if response.status().is_success() {
82 let text = response.text().await.map_err(|e| e.to_string())?;
83 Ok(text)
84 } else {
85 Err(format!("Request failed with status: {}", response.status()))
86 }
87 }
88
89 pub async fn get_network_status(&self) -> Result<String, String> {
91 self.make_request("/ping", None).await
92 }
93
94 pub async fn get_global_data(&self) -> Result<String, String> {
95 self.make_request("/global", None).await
96 }
97
98 pub async fn get_global_defi_data(&self) -> Result<String, String> {
99 self.make_request("/global/decentralized_finance_defi", None)
100 .await
101 }
102
103 pub async fn get_exchanges(
104 &self,
105 per_page: Option<u32>,
106 page: Option<u32>,
107 ) -> Result<String, String> {
108 let mut params = HashMap::new();
109 if let Some(per_page) = per_page {
110 params.insert("per_page".to_string(), per_page.to_string());
111 }
112 if let Some(page) = page {
113 params.insert("page".to_string(), page.to_string());
114 }
115 self.make_request("/exchanges", Some(params)).await
116 }
117
118 pub async fn get_exchange(&self, id: String) -> Result<String, String> {
119 self.make_request(&format!("/exchanges/{}", id), None).await
120 }
121
122 pub async fn get_exchange_tickers(
123 &self,
124 id: String,
125 coin_ids: Option<Vec<String>>,
126 include_exchange_logo: Option<bool>,
127 page: Option<u32>,
128 depth: Option<bool>,
129 order: Option<String>,
130 ) -> Result<String, String> {
131 let mut params = HashMap::new();
132 if let Some(coin_ids) = coin_ids {
133 params.insert("coin_ids".to_string(), coin_ids.join(","));
134 }
135 if let Some(include_exchange_logo) = include_exchange_logo {
136 params.insert(
137 "include_exchange_logo".to_string(),
138 include_exchange_logo.to_string(),
139 );
140 }
141 if let Some(page) = page {
142 params.insert("page".to_string(), page.to_string());
143 }
144 if let Some(depth) = depth {
145 params.insert("depth".to_string(), depth.to_string());
146 }
147 if let Some(order) = order {
148 params.insert("order".to_string(), order);
149 }
150 self.make_request(&format!("/exchanges/{}/tickers", id), Some(params))
151 .await
152 }
153
154 pub async fn get_exchange_volume_chart(&self, id: String, days: u32) -> Result<String, String> {
155 let mut params = HashMap::new();
156 params.insert("days".to_string(), days.to_string());
157 self.make_request(&format!("/exchanges/{}/volume_chart", id), Some(params))
158 .await
159 }
160
161 pub async fn get_coins_list(&self, include_platform: Option<bool>) -> Result<String, String> {
162 let mut params = HashMap::new();
163 if let Some(include_platform) = include_platform {
164 params.insert("include_platform".to_string(), include_platform.to_string());
165 }
166 self.make_request("/coins/list", Some(params)).await
167 }
168
169 pub async fn get_coin_tickers(
170 &self,
171 id: String,
172 exchange_ids: Option<Vec<String>>,
173 include_exchange_logo: Option<bool>,
174 page: Option<u32>,
175 order: Option<String>,
176 depth: Option<bool>,
177 ) -> Result<String, String> {
178 let mut params = HashMap::new();
179 if let Some(exchange_ids) = exchange_ids {
180 params.insert("exchange_ids".to_string(), exchange_ids.join(","));
181 }
182 if let Some(include_exchange_logo) = include_exchange_logo {
183 params.insert(
184 "include_exchange_logo".to_string(),
185 include_exchange_logo.to_string(),
186 );
187 }
188 if let Some(page) = page {
189 params.insert("page".to_string(), page.to_string());
190 }
191 if let Some(order) = order {
192 params.insert("order".to_string(), order);
193 }
194 if let Some(depth) = depth {
195 params.insert("depth".to_string(), depth.to_string());
196 }
197 self.make_request(&format!("/coins/{}/tickers", id), Some(params))
198 .await
199 }
200
201 pub async fn get_coin_history(
202 &self,
203 id: String,
204 date: String,
205 localization: Option<bool>,
206 ) -> Result<String, String> {
207 let mut params = HashMap::new();
208 params.insert("date".to_string(), date);
209 if let Some(localization) = localization {
210 params.insert("localization".to_string(), localization.to_string());
211 }
212 self.make_request(&format!("/coins/{}/history", id), Some(params))
213 .await
214 }
215
216 pub async fn get_coin_market_chart(
217 &self,
218 id: String,
219 vs_currency: String,
220 days: String,
221 interval: Option<String>,
222 ) -> Result<String, String> {
223 let mut params = HashMap::new();
224 params.insert("vs_currency".to_string(), vs_currency);
225 params.insert("days".to_string(), days);
226 if let Some(interval) = interval {
227 params.insert("interval".to_string(), interval);
228 }
229 self.make_request(&format!("/coins/{}/market_chart", id), Some(params))
230 .await
231 }
232
233 pub async fn get_coin_market_chart_range(
234 &self,
235 id: String,
236 vs_currency: String,
237 from: u64,
238 to: u64,
239 ) -> Result<String, String> {
240 let mut params = HashMap::new();
241 params.insert("vs_currency".to_string(), vs_currency);
242 params.insert("from".to_string(), from.to_string());
243 params.insert("to".to_string(), to.to_string());
244 self.make_request(&format!("/coins/{}/market_chart/range", id), Some(params))
245 .await
246 }
247
248 pub async fn get_coin_ohlc(
249 &self,
250 id: String,
251 vs_currency: String,
252 days: String,
253 ) -> Result<String, String> {
254 let mut params = HashMap::new();
255 params.insert("vs_currency".to_string(), vs_currency);
256 params.insert("days".to_string(), days);
257 self.make_request(&format!("/coins/{}/ohlc", id), Some(params))
258 .await
259 }
260
261 pub async fn get_coin_contract(
262 &self,
263 id: String,
264 contract_address: String,
265 ) -> Result<String, String> {
266 self.make_request(
267 &format!("/coins/{}/contract/{}", id, contract_address),
268 None,
269 )
270 .await
271 }
272
273 pub async fn get_coin_contract_market_chart(
274 &self,
275 id: String,
276 contract_address: String,
277 vs_currency: String,
278 days: String,
279 ) -> Result<String, String> {
280 let mut params = HashMap::new();
281 params.insert("vs_currency".to_string(), vs_currency);
282 params.insert("days".to_string(), days);
283 self.make_request(
284 &format!("/coins/{}/contract/{}/market_chart", id, contract_address),
285 Some(params),
286 )
287 .await
288 }
289
290 pub async fn get_coin_contract_market_chart_range(
291 &self,
292 id: String,
293 contract_address: String,
294 vs_currency: String,
295 from: u64,
296 to: u64,
297 ) -> Result<String, String> {
298 let mut params = HashMap::new();
299 params.insert("vs_currency".to_string(), vs_currency);
300 params.insert("from".to_string(), from.to_string());
301 params.insert("to".to_string(), to.to_string());
302 self.make_request(
303 &format!(
304 "/coins/{}/contract/{}/market_chart/range",
305 id, contract_address
306 ),
307 Some(params),
308 )
309 .await
310 }
311
312 pub async fn get_asset_platforms(&self) -> Result<String, String> {
313 self.make_request("/asset_platforms", None).await
314 }
315
316 pub async fn get_coins_categories_list(&self) -> Result<String, String> {
317 self.make_request("/coins/categories/list", None).await
318 }
319
320 pub async fn get_coins_categories(&self, order: Option<String>) -> Result<String, String> {
321 let mut params = HashMap::new();
322 if let Some(order) = order {
323 params.insert("order".to_string(), order);
324 }
325 self.make_request("/coins/categories", Some(params)).await
326 }
327
328 pub async fn get_indexes(&self) -> Result<String, String> {
329 self.make_request("/indexes", None).await
330 }
331
332 pub async fn get_indexes_list(&self) -> Result<String, String> {
333 self.make_request("/indexes/list", None).await
334 }
335
336 pub async fn get_derivatives(&self) -> Result<String, String> {
337 self.make_request("/derivatives", None).await
338 }
339
340 pub async fn get_derivatives_exchanges(
341 &self,
342 order: Option<String>,
343 per_page: Option<u32>,
344 page: Option<u32>,
345 ) -> Result<String, String> {
346 let mut params = HashMap::new();
347 if let Some(order) = order {
348 params.insert("order".to_string(), order);
349 }
350 if let Some(per_page) = per_page {
351 params.insert("per_page".to_string(), per_page.to_string());
352 }
353 if let Some(page) = page {
354 params.insert("page".to_string(), page.to_string());
355 }
356 self.make_request("/derivatives/exchanges", Some(params))
357 .await
358 }
359
360 pub async fn get_derivatives_exchange(
361 &self,
362 id: String,
363 include_tickers: Option<String>,
364 ) -> Result<String, String> {
365 let mut params = HashMap::new();
366 if let Some(include_tickers) = include_tickers {
367 params.insert("include_tickers".to_string(), include_tickers);
368 }
369 self.make_request(&format!("/derivatives/exchanges/{}", id), Some(params))
370 .await
371 }
372
373 pub async fn get_exchange_rates(&self) -> Result<String, String> {
374 self.make_request("/exchange_rates", None).await
375 }
376
377 pub async fn search(&self, query: String) -> Result<String, String> {
378 let mut params = HashMap::new();
379 params.insert("query".to_string(), query);
380 self.make_request("/search", Some(params)).await
381 }
382
383 pub async fn get_trending(&self) -> Result<String, String> {
384 self.make_request("/search/trending", None).await
385 }
386
387 pub async fn get_companies_public_treasury(&self, coin_id: String) -> Result<String, String> {
388 self.make_request(&format!("/companies/public_treasury/{}", coin_id), None)
389 .await
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 fn get_test_client() -> CoinGeckoProClient {
398 let api_key = std::env::var("COINGECKO_PRO_API_KEY")
399 .expect("COINGECKO_PRO_API_KEY must be set for tests");
400 CoinGeckoProClient::new(api_key)
401 }
402
403 #[tokio::test]
404 async fn test_network_status() {
405 let client = get_test_client();
406 let result = client.get_network_status().await;
407 assert!(result.is_ok());
408 }
409
410 #[tokio::test]
411 async fn test_global_data() {
412 let client = get_test_client();
413 let result = client.get_global_data().await;
414 assert!(result.is_ok());
415 }
416
417 #[tokio::test]
418 async fn test_global_defi_data() {
419 let client = get_test_client();
420 let result = client.get_global_defi_data().await;
421 assert!(result.is_ok());
422 }
423
424 #[tokio::test]
425 async fn test_exchanges() {
426 let client = get_test_client();
427 let result = client.get_exchanges(Some(10), Some(1)).await;
428 assert!(result.is_ok());
429 }
430
431 #[tokio::test]
432 async fn test_exchange() {
433 let client = get_test_client();
434 let result = client.get_exchange("binance".to_string()).await;
435 assert!(result.is_ok());
436 }
437
438 #[tokio::test]
439 async fn test_exchange_tickers() {
440 let client = get_test_client();
441 let result = client
442 .get_exchange_tickers(
443 "binance".to_string(),
444 Some(vec!["bitcoin".to_string()]),
445 Some(true),
446 Some(1),
447 Some(true),
448 Some("volume_desc".to_string()),
449 )
450 .await;
451 assert!(result.is_ok());
452 }
453
454 #[tokio::test]
455 async fn test_exchange_volume_chart() {
456 let client = get_test_client();
457 let result = client
458 .get_exchange_volume_chart("binance".to_string(), 1)
459 .await;
460 assert!(result.is_ok());
461 }
462
463 #[tokio::test]
464 async fn test_coins_list() {
465 let client = get_test_client();
466 let result = client.get_coins_list(Some(true)).await;
467 assert!(result.is_ok());
468 }
469
470 #[tokio::test]
471 async fn test_coin_tickers() {
472 let client = get_test_client();
473 let result = client
474 .get_coin_tickers(
475 "bitcoin".to_string(),
476 Some(vec!["binance".to_string()]),
477 Some(true),
478 Some(1),
479 Some("volume_desc".to_string()),
480 Some(true),
481 )
482 .await;
483 assert!(result.is_ok());
484 }
485
486 #[tokio::test]
487 async fn test_coin_history() {
488 let client = get_test_client();
489 let result = client
490 .get_coin_history("bitcoin".to_string(), "30-12-2023".to_string(), Some(true))
491 .await;
492 assert!(result.is_ok());
493 }
494
495 #[tokio::test]
496 async fn test_coin_market_chart() {
497 let client = get_test_client();
498 let result = client
499 .get_coin_market_chart(
500 "bitcoin".to_string(),
501 "usd".to_string(),
502 "1".to_string(),
503 Some("daily".to_string()),
504 )
505 .await;
506 assert!(result.is_ok());
507 }
508
509 #[tokio::test]
510 async fn test_coin_market_chart_range() {
511 let client = get_test_client();
512 let now = std::time::SystemTime::now()
513 .duration_since(std::time::UNIX_EPOCH)
514 .unwrap()
515 .as_secs();
516 let result = client
517 .get_coin_market_chart_range("bitcoin".to_string(), "usd".to_string(), now - 86400, now)
518 .await;
519 assert!(result.is_ok());
520 }
521
522 #[tokio::test]
523 async fn test_coin_ohlc() {
524 let client = get_test_client();
525 let result = client
526 .get_coin_ohlc("bitcoin".to_string(), "usd".to_string(), "1".to_string())
527 .await;
528 assert!(result.is_ok());
529 }
530
531 #[tokio::test]
532 async fn test_coin_contract() {
533 let client = get_test_client();
534 let result = client
535 .get_coin_contract(
536 "ethereum".to_string(),
537 "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984".to_string(), )
539 .await;
540 assert!(result.is_ok());
541 }
542
543 #[tokio::test]
544 async fn test_coin_contract_market_chart() {
545 let client = get_test_client();
546 let result = client
547 .get_coin_contract_market_chart(
548 "ethereum".to_string(),
549 "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984".to_string(),
550 "usd".to_string(),
551 "1".to_string(),
552 )
553 .await;
554 assert!(result.is_ok());
555 }
556
557 #[tokio::test]
558 async fn test_coin_contract_market_chart_range() {
559 let client = get_test_client();
560 let now = std::time::SystemTime::now()
561 .duration_since(std::time::UNIX_EPOCH)
562 .unwrap()
563 .as_secs();
564 let result = client
565 .get_coin_contract_market_chart_range(
566 "ethereum".to_string(),
567 "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984".to_string(),
568 "usd".to_string(),
569 now - 86400,
570 now,
571 )
572 .await;
573 assert!(result.is_ok());
574 }
575
576 #[tokio::test]
577 async fn test_asset_platforms() {
578 let client = get_test_client();
579 let result = client.get_asset_platforms().await;
580 assert!(result.is_ok());
581 }
582
583 #[tokio::test]
584 async fn test_coins_categories_list() {
585 let client = get_test_client();
586 let result = client.get_coins_categories_list().await;
587 assert!(result.is_ok());
588 }
589
590 #[tokio::test]
591 async fn test_coins_categories() {
592 let client = get_test_client();
593 let result = client
594 .get_coins_categories(Some("market_cap_desc".to_string()))
595 .await;
596 assert!(result.is_ok());
597 }
598
599 #[tokio::test]
600 async fn test_indexes() {
601 let client = get_test_client();
602 let result = client.get_indexes().await;
603 assert!(result.is_ok());
604 }
605
606 #[tokio::test]
607 async fn test_indexes_list() {
608 let client = get_test_client();
609 let result = client.get_indexes_list().await;
610 assert!(result.is_ok());
611 }
612
613 #[tokio::test]
614 async fn test_derivatives() {
615 let client = get_test_client();
616 let result = client.get_derivatives().await;
617 assert!(result.is_ok());
618 }
619
620 #[tokio::test]
621 async fn test_derivatives_exchanges() {
622 let client = get_test_client();
623 let result = client
624 .get_derivatives_exchanges(Some("name_desc".to_string()), Some(10), Some(1))
625 .await;
626 assert!(result.is_ok());
627 }
628
629 #[tokio::test]
630 async fn test_derivatives_exchange() {
631 let client = get_test_client();
632 let result = client
633 .get_derivatives_exchange("binance_futures".to_string(), Some("all".to_string()))
634 .await;
635 assert!(result.is_ok());
636 }
637
638 #[tokio::test]
639 async fn test_exchange_rates() {
640 let client = get_test_client();
641 let result = client.get_exchange_rates().await;
642 assert!(result.is_ok());
643 }
644
645 #[tokio::test]
646 async fn test_search() {
647 let client = get_test_client();
648 let result = client.search("bitcoin".to_string()).await;
649 assert!(result.is_ok());
650 }
651
652 #[tokio::test]
653 async fn test_trending() {
654 let client = get_test_client();
655 let result = client.get_trending().await;
656 assert!(result.is_ok());
657 }
658
659 #[tokio::test]
660 async fn test_companies_public_treasury() {
661 let client = get_test_client();
662 let result = client
663 .get_companies_public_treasury("bitcoin".to_string())
664 .await;
665 assert!(result.is_ok());
666 }
667}