ferrox_actions/coingecko/
pro.rs

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(&params.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    // Pro API specific endpoints
90    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(), // UNI contract
538            )
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}