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}