bitvavo_api/
lib.rs

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/// Error type returned by the API.
13#[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
77/// A client for the Bitvavo API.
78pub struct Client {
79    client: reqwest::Client,
80}
81
82impl Client {
83    /// Create a new client for the Bitvavo API.
84    pub fn new() -> Self {
85        Self {
86            client: reqwest::Client::new(),
87        }
88    }
89
90    /// Get the current time.
91    ///
92    /// ```no_run
93    /// # tokio_test::block_on(async {
94    /// use bitvavo_api as bitvavo;
95    ///
96    /// let c = bitvavo::Client::new();
97    /// let t = c.time().await.unwrap();
98    ///
99    /// println!("{t}");
100    /// # })
101    /// ```
102    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    /// Get all the assets.
117    ///
118    /// ```no_run
119    /// # tokio_test::block_on(async {
120    /// use bitvavo_api as bitvavo;
121    ///
122    /// let c = bitvavo::Client::new();
123    /// let assets = c.assets().await.unwrap();
124    ///
125    /// println!("Number of assets: {}", assets.len());
126    /// # })
127    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    /// Get the info of a particular asset.
137    ///
138    /// ```no_run
139    /// # tokio_test::block_on(async {
140    /// use bitvavo_api as bitvavo;
141    ///
142    /// let c = bitvavo::Client::new();
143    /// let asset = c.asset("BTC").await.unwrap();
144    ///
145    /// println!("Number of decimals used for BTC: {}", asset.decimals);
146    /// # })
147    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    /// Get all the markets.
159    ///
160    /// ```no_run
161    /// # tokio_test::block_on(async {
162    /// use bitvavo_api as bitvavo;
163    ///
164    /// let c = bitvavo::Client::new();
165    /// let markets = c.markets().await.unwrap();
166    ///
167    /// println!("Number of markets: {}", markets.len());
168    /// # })
169    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    /// Get market information for a specific market.
179    ///
180    /// ```no_run
181    /// # tokio_test::block_on(async {
182    /// use bitvavo_api as bitvavo;
183    ///
184    /// let c = bitvavo::Client::new();
185    /// let market = c.market("BTC-EUR").await.unwrap();
186    ///
187    /// println!("Price precision of BTC-EUR: {}", market.price_precision);
188    /// # })
189    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    /// Get the order book for a particular market.
201    ///
202    /// ```no_run
203    /// # tokio_test::block_on(async {
204    /// use bitvavo_api as bitvavo;
205    ///
206    /// let c = bitvavo::Client::new();
207    /// let ob = c.order_book("BTC-EUR", Some(2)).await.unwrap();
208    ///
209    /// println!("Number of bids: {}", ob.bids.len());
210    /// # })
211    /// ```
212    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    /// Get the trades for a particular market.
228    ///
229    /// ```no_run
230    /// # tokio_test::block_on(async {
231    /// use bitvavo_api as bitvavo;
232    ///
233    /// let c = bitvavo::Client::new();
234    /// let trades = c.trades("BTC-EUR", None, None, None, None, None).await.unwrap();
235    ///
236    /// println!("Number of trades: {}", trades.len());
237    /// # })
238    /// ```
239    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    /// Get candles for a particular market.
275    ///
276    /// ```no_run
277    /// # tokio_test::block_on(async {
278    /// use bitvavo_api as bitvavo;
279    /// use bitvavo::types::CandleInterval;
280    ///
281    /// let c = bitvavo::Client::new();
282    /// let cs = c.candles("BTC-EUR", CandleInterval::OneDay, Some(1), None, None).await.unwrap();
283    ///
284    /// println!("High for BTC-EUR at {} was {}", cs[0].time, cs[0].high);
285    /// # })
286    /// ```
287    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    /// Get all the tickers.
316    ///
317    /// ```no_run
318    /// # tokio_test::block_on(async {
319    /// use bitvavo_api as bitvavo;
320    ///
321    /// let c = bitvavo::Client::new();
322    /// let ms = c.ticker_prices().await.unwrap();
323    ///
324    /// println!("Number of markets: {}", ms.len());
325    /// # })
326    /// ```
327    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    /// Get the ticker for a particular market.
337    ///
338    /// ```no_run
339    /// # tokio_test::block_on(async {
340    /// use bitvavo_api as bitvavo;
341    ///
342    /// let c = bitvavo::Client::new();
343    /// let m = c.ticker_price("BTC-EUR").await.unwrap();
344    ///
345    /// println!("Price for BTC-EUR: {}", m.price.unwrap_or_default());
346    /// # })
347    /// ```
348    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    /// Retrieve the highest buy and lowest sell prices currently available for all markets.
360    ///
361    /// ```no_run
362    /// # tokio_test::block_on(async {
363    /// use bitvavo_api as bitvavo;
364    ///
365    /// let c = bitvavo::Client::new();
366    /// let tb = c.ticker_books().await.unwrap();
367    ///
368    /// println!("Number of tickers: {}", tb.len());
369    /// # })
370    /// ```
371    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    /// Retrieve the highest buy and lowest sell prices currently available for a given market.
381    ///
382    /// ```no_run
383    /// # tokio_test::block_on(async {
384    /// use bitvavo_api as bitvavo;
385    ///
386    /// let c = bitvavo::Client::new();
387    /// let tb = c.ticker_book("BTC-EUR").await.unwrap();
388    ///
389    /// println!("Highest buy price for BTC-EUR: {}", tb.ask.unwrap());
390    /// # })
391    /// ```
392    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    /// Retrieve high, low, open, last, and volume information for trades for all markets over the previous 24h.
404    ///
405    /// ```no_run
406    /// # tokio_test::block_on(async {
407    /// use bitvavo_api as bitvavo;
408    ///
409    /// let c = bitvavo::Client::new();
410    /// let t24h = c.tickers_24h().await.unwrap();
411    ///
412    /// println!("Number of tickers: {}", t24h.len());
413    /// # })
414    /// ```
415    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    /// Retrieve high, low, open, last, and volume information for trades for a given market over the previous 24h.
425    ///
426    /// ```no_run
427    /// # tokio_test::block_on(async {
428    /// use bitvavo_api as bitvavo;
429    ///
430    /// let c = bitvavo::Client::new();
431    /// let t24h = c.ticker_24h("BTC-EUR").await.unwrap();
432    ///
433    /// println!("24h ask for BTC-EUR: {}", t24h.ask.unwrap());
434    /// # })
435    /// ```
436    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}