1pub mod types;
2
3use std::error::Error as StdError;
4use std::fmt;
5
6use reqwest::Response;
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9
10use types::*;
11
12#[derive(Debug)]
14#[non_exhaustive]
15pub enum Error {
16    Reqwest(reqwest::Error),
17    Serde(serde_json::Error),
18    Bitvavo { code: u64, message: String },
19}
20
21impl From<reqwest::Error> for Error {
22    fn from(err: reqwest::Error) -> Self {
23        Self::Reqwest(err)
24    }
25}
26
27impl From<serde_json::Error> for Error {
28    fn from(err: serde_json::Error) -> Self {
29        Self::Serde(err)
30    }
31}
32
33async fn response_from_request<T: DeserializeOwned>(rsp: Response) -> Result<T, Error> {
34    #[derive(Deserialize, Serialize)]
35    #[serde(rename_all = "camelCase")]
36    struct BitvavoError {
37        error_code: u64,
38        error: String,
39    }
40
41    let status = rsp.status();
42    let bytes = rsp.bytes().await?;
43
44    if status.is_success() {
45        Ok(serde_json::from_slice(&bytes)?)
46    } else {
47        let bitvavo_err: BitvavoError = serde_json::from_slice(&bytes)?;
48        Err(Error::Bitvavo {
49            code: bitvavo_err.error_code,
50            message: bitvavo_err.error,
51        })
52    }
53}
54
55impl fmt::Display for Error {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Error::Reqwest(err) => write!(f, "reqwest: {err}"),
59            Error::Serde(err) => write!(f, "serde: {err}"),
60            Error::Bitvavo { code, message } => {
61                write!(f, "bitvavo: {code}: {message}")
62            }
63        }
64    }
65}
66
67impl StdError for Error {}
68
69pub type Result<T, E = Error> = std::result::Result<T, E>;
70
71impl Default for Client {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77pub struct Client {
79    client: reqwest::Client,
80}
81
82impl Client {
83    pub fn new() -> Self {
85        Self {
86            client: reqwest::Client::new(),
87        }
88    }
89
90    pub async fn time(&self) -> Result<u64> {
103        #[derive(Deserialize, Serialize)]
104        struct Response {
105            time: u64,
106        }
107
108        let request = self.client.get("https://api.bitvavo.com/v2/time");
109
110        let http_response = request.send().await?;
111        let response = response_from_request::<Response>(http_response).await?;
112
113        Ok(response.time)
114    }
115
116    pub async fn assets(&self) -> Result<Vec<Asset>> {
128        let request = self.client.get("https://api.bitvavo.com/v2/assets");
129
130        let http_response = request.send().await?;
131        let response = response_from_request(http_response).await?;
132
133        Ok(response)
134    }
135
136    pub async fn asset(&self, symbol: &str) -> Result<Asset> {
148        let request = self
149            .client
150            .get(format!("https://api.bitvavo.com/v2/assets?symbol={symbol}"));
151
152        let http_response = request.send().await?;
153        let response = response_from_request(http_response).await?;
154
155        Ok(response)
156    }
157
158    pub async fn markets(&self) -> Result<Vec<Market>> {
170        let request = self.client.get("https://api.bitvavo.com/v2/markets");
171
172        let http_response = request.send().await?;
173        let response = response_from_request(http_response).await?;
174
175        Ok(response)
176    }
177
178    pub async fn market(&self, pair: &str) -> Result<Market> {
190        let request = self
191            .client
192            .get(format!("https://api.bitvavo.com/v2/markets?market={pair}"));
193
194        let http_response = request.send().await?;
195        let response = response_from_request(http_response).await?;
196
197        Ok(response)
198    }
199
200    pub async fn order_book(&self, market: &str, depth: Option<u64>) -> Result<OrderBook> {
213        let mut url = format!("https://api.bitvavo.com/v2/{market}/book");
214
215        if let Some(depth) = depth {
216            url.push_str(&format!("?depth={depth}"));
217        }
218
219        let request = self.client.get(url);
220
221        let http_response = request.send().await?;
222        let response = response_from_request(http_response).await?;
223
224        Ok(response)
225    }
226
227    pub async fn trades(
240        &self,
241        market: &str,
242        limit: Option<u64>,
243        start: Option<u64>,
244        end: Option<u64>,
245        trade_id_from: Option<String>,
246        trade_id_to: Option<String>,
247    ) -> Result<Vec<Trade>> {
248        let mut url = format!("https://api.bitvavo.com/v2/{market}/trades");
249
250        if let Some(limit) = limit {
251            url.push_str(&format!("?limit={limit}"));
252        }
253        if let Some(start) = start {
254            url.push_str(&format!("&start={start}"));
255        }
256        if let Some(end) = end {
257            url.push_str(&format!("&end={end}"));
258        }
259        if let Some(trade_id_from) = trade_id_from {
260            url.push_str(&format!("&tradeIdFrom={trade_id_from}"));
261        }
262        if let Some(trade_id_to) = trade_id_to {
263            url.push_str(&format!("&tradeIdTo={trade_id_to}"));
264        }
265
266        let request = self.client.get(url);
267
268        let http_response = request.send().await?;
269        let response = response_from_request(http_response).await?;
270
271        Ok(response)
272    }
273
274    pub async fn candles(
288        &self,
289        market: &str,
290        interval: CandleInterval,
291        limit: Option<u16>,
292        start: Option<u64>,
293        end: Option<u64>,
294    ) -> Result<Vec<OHLCV>> {
295        let mut url = format!("https://api.bitvavo.com/v2/{market}/candles?interval={interval}");
296
297        if let Some(limit) = limit {
298            url.push_str(&format!("&limit={limit}"));
299        }
300        if let Some(start) = start {
301            url.push_str(&format!("&start={start}"));
302        }
303        if let Some(end) = end {
304            url.push_str(&format!("&end={end}"));
305        }
306
307        let request = self.client.get(url);
308
309        let http_response = request.send().await?;
310        let response = response_from_request(http_response).await?;
311
312        Ok(response)
313    }
314
315    pub async fn ticker_prices(&self) -> Result<Vec<TickerPrice>> {
328        let request = self.client.get("https://api.bitvavo.com/v2/ticker/price");
329
330        let http_response = request.send().await?;
331        let response = response_from_request(http_response).await?;
332
333        Ok(response)
334    }
335
336    pub async fn ticker_price(&self, pair: &str) -> Result<TickerPrice> {
349        let request = self.client.get(format!(
350            "https://api.bitvavo.com/v2/ticker/price?market={pair}"
351        ));
352
353        let http_response = request.send().await?;
354        let response = response_from_request(http_response).await?;
355
356        Ok(response)
357    }
358
359    pub async fn ticker_books(&self) -> Result<Vec<TickerBook>> {
372        let request = self.client.get("https://api.bitvavo.com/v2/ticker/book");
373
374        let http_response = request.send().await?;
375        let response = response_from_request(http_response).await?;
376
377        Ok(response)
378    }
379
380    pub async fn ticker_book(&self, market: &str) -> Result<TickerBook> {
393        let request = self.client.get(format!(
394            "https://api.bitvavo.com/v2/ticker/book?market={market}"
395        ));
396
397        let http_response = request.send().await?;
398        let response = response_from_request(http_response).await?;
399
400        Ok(response)
401    }
402
403    pub async fn tickers_24h(&self) -> Result<Vec<Ticker24h>> {
416        let request = self.client.get("https://api.bitvavo.com/v2/ticker/24h");
417
418        let http_response = request.send().await?;
419        let response = response_from_request(http_response).await?;
420
421        Ok(response)
422    }
423
424    pub async fn ticker_24h(&self, market: &str) -> Result<Ticker24h> {
437        let request = self.client.get(format!(
438            "https://api.bitvavo.com/v2/ticker/24h?market={market}"
439        ));
440
441        let http_response = request.send().await?;
442        let response = response_from_request(http_response).await?;
443
444        Ok(response)
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[tokio::test]
453    async fn get_time() {
454        let client = Client::new();
455        client
456            .time()
457            .await
458            .expect("Getting the time should succeed");
459    }
460
461    #[tokio::test]
462    async fn get_assets() {
463        let client = Client::new();
464        client
465            .assets()
466            .await
467            .expect("Getting the assets should succeed");
468    }
469
470    #[tokio::test]
471    async fn get_asset() {
472        let client = Client::new();
473        client
474            .asset("BTC")
475            .await
476            .expect("Getting the asset should succeed");
477    }
478
479    #[tokio::test]
480    async fn get_markets() {
481        let client = Client::new();
482        client
483            .markets()
484            .await
485            .expect("Getting the markets should succeed");
486    }
487
488    #[tokio::test]
489    async fn get_market() {
490        let client = Client::new();
491        client
492            .market("BTC-EUR")
493            .await
494            .expect("Getting the market should succeed");
495    }
496
497    #[tokio::test]
498    async fn get_order_book() {
499        let client = Client::new();
500        client
501            .order_book("BTC-EUR", Some(2))
502            .await
503            .expect("Getting the order book should succeed");
504    }
505
506    #[tokio::test]
507    async fn get_trades() {
508        let client = Client::new();
509        client
510            .trades("BTC-EUR", None, None, None, None, None)
511            .await
512            .expect("Getting the order book should succeed");
513    }
514
515    #[tokio::test]
516    async fn get_candles() {
517        let client = Client::new();
518        client
519            .candles("BTC-EUR", CandleInterval::OneDay, Some(1), None, None)
520            .await
521            .expect("Getting the candles should succeed");
522    }
523
524    #[tokio::test]
525    async fn get_ticker_prices() {
526        let client = Client::new();
527        client
528            .ticker_prices()
529            .await
530            .expect("Getting the markets should succeed");
531    }
532
533    #[tokio::test]
534    async fn get_ticker_price() {
535        let client = Client::new();
536        client
537            .ticker_price("BTC-EUR")
538            .await
539            .expect("Getting the market should succeed");
540    }
541
542    #[tokio::test]
543    async fn get_ticker_books() {
544        let client = Client::new();
545        client
546            .ticker_books()
547            .await
548            .expect("Getting the ticker books should succeed");
549    }
550
551    #[tokio::test]
552    async fn get_ticker_book() {
553        let client = Client::new();
554        client
555            .ticker_book("BTC-EUR")
556            .await
557            .expect("Getting the ticker book should succeed");
558    }
559
560    #[tokio::test]
561    async fn get_tickers_24h() {
562        let client = Client::new();
563        client
564            .tickers_24h()
565            .await
566            .expect("Getting the 24h tickers should succeed");
567    }
568
569    #[tokio::test]
570    async fn get_ticker_24h() {
571        let client = Client::new();
572        client
573            .ticker_24h("BTC-EUR")
574            .await
575            .expect("Getting the 24h tickers should succeed");
576    }
577
578    #[tokio::test]
579    async fn error_handling() {
580        let client = Client::new();
581
582        let err = client
583            .ticker_price("BAD-MARKET")
584            .await
585            .expect_err("Getting an invalid market should fail");
586
587        assert!(matches!(err, Error::Bitvavo { .. }));
588    }
589}