diff --git a/client/src/lib.rs b/client/src/lib.rs index 7be81f6..02a32c0 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -61,6 +61,9 @@ pub mod error; mod routes; +// Re-export request builders. +pub use routes::markets::GetMarketsRequest; + #[cfg(feature = "ws")] mod ws; @@ -449,3 +452,53 @@ fn now_millis() -> u64 { .expect("Time went backwards") .as_millis() as u64 } + +pub trait BpxClientRequest { + const PATH: &'static str; + const METHOD: Method; + type Body: Serialize; + + fn query_params(&self) -> Vec<(Cow<'_, str>, Cow<'_, str>)>; + + fn body(&self) -> Option<&Self::Body>; + + fn validate(&self) -> Result<()>; + + fn send(self, client: &BpxClient) -> impl Future> + where + Self: Sized, + { + async move { + self.validate()?; + let mut url = client.base_url.join(Self::PATH)?; + for (key, value) in self.query_params() { + url.query_pairs_mut().append_pair(&key, &value); + } + + match Self::METHOD.as_str() { + "GET" => client.get(url).await, + "POST" => { + let body = self.body().ok_or_else(|| { + Error::InvalidRequest("POST request must have a body".into()) + })?; + client.post(url, body).await + } + "DELETE" => { + let body = self.body().ok_or_else(|| { + Error::InvalidRequest("DELETE request must have a body".into()) + })?; + client.delete(url, body).await + } + "PATCH" => { + let body = self.body().ok_or_else(|| { + Error::InvalidRequest("DELETE request must have a body".into()) + })?; + client.patch(url, body).await + } + _ => Err(Error::InvalidRequest( + format!("unsupported HTTP method: {}", Self::METHOD.as_str()).into(), + )), + } + } + } +} diff --git a/client/src/routes/markets.rs b/client/src/routes/markets.rs index bb7368e..0593639 100644 --- a/client/src/routes/markets.rs +++ b/client/src/routes/markets.rs @@ -1,9 +1,11 @@ use bpx_api_types::markets::{ Asset, FundingRate, Kline, MarkPrice, Market, OrderBookDepth, OrderBookDepthLimit, Ticker, }; +use reqwest::Method; +use std::borrow::Cow; -use crate::BpxClient; use crate::error::Result; +use crate::{BpxClient, BpxClientRequest, Error}; const API_ASSETS: &str = "/api/v1/assets"; const API_MARKETS: &str = "/api/v1/markets"; @@ -22,13 +24,19 @@ impl BpxClient { res.json().await.map_err(Into::into) } - /// Retrieves a list of available markets. + /// Retrieves a list of available spot and perp markets. + /// + /// Note: If you want to retrieve other market types or filter by market type, use `GetMarketsRequest`. pub async fn get_markets(&self) -> Result> { - let url = self.base_url.join(API_MARKETS)?; - let res = self.get(url).await?; - res.json().await.map_err(Into::into) + GetMarketsRequest::new() + .with_spot_markets() + .with_perp_markets() + .send(self) + .await } + // pub async fn get_markets_with_filter + /// Retrieves mark price, index price and the funding rate for the current interval for all symbols, or the symbol specified. pub async fn get_all_mark_prices(&self) -> Result> { let url = self.base_url.join(API_MARK_PRICES)?; @@ -96,3 +104,75 @@ impl BpxClient { res.json().await.map_err(Into::into) } } + +#[derive(Debug, Default, Clone)] +pub struct GetMarketsRequest(Vec); + +impl GetMarketsRequest { + pub fn new() -> Self { + Self::default() + } + + pub fn with_spot_markets(mut self) -> Self { + self.0.push("SPOT".to_string()); + self + } + + pub fn with_perp_markets(mut self) -> Self { + self.0.push("PERP".to_string()); + self + } + + pub fn with_prediction_markets(mut self) -> Self { + self.0.push("PREDICTION".to_string()); + self + } + + pub async fn send(self, client: &BpxClient) -> Result> { + if self.0.is_empty() { + return Err(Error::InvalidRequest( + "at least one market type must be specified".into(), + )); + } + + let mut url = client.base_url.join(API_MARKETS)?; + for market_type in self.0 { + url.query_pairs_mut() + .append_pair("marketType", &market_type); + } + + let res = client.get(url).await?; + res.json().await.map_err(Into::into) + } +} + +impl BpxClientRequest for GetMarketsRequest { + const PATH: &'static str = API_MARKETS; + const METHOD: Method = Method::GET; + type Body = (); + + fn query_params(&self) -> Vec<(Cow<'_, str>, Cow<'_, str>)> { + let mut params = Vec::new(); + for market_type in &self.0 { + params.push(( + Cow::Borrowed("marketType"), + Cow::Borrowed(market_type.as_str()), + )); + } + params + } + + fn body(&self) -> Option<&Self::Body> { + None + } + + fn validate(&self) -> Result<()> { + if self.0.is_empty() { + return Err(Error::InvalidRequest( + "at least one market type must be specified".into(), + )); + } + + Ok(()) + } +} diff --git a/examples/src/bin/markets.rs b/examples/src/bin/markets.rs index d6d71a6..17b93b9 100644 --- a/examples/src/bin/markets.rs +++ b/examples/src/bin/markets.rs @@ -1,4 +1,5 @@ -use bpx_api_client::{BACKPACK_API_BASE_URL, BpxClient}; +use bpx_api_client::{BACKPACK_API_BASE_URL, BpxClient, Error, GetMarketsRequest}; +use bpx_api_types::markets::Market; use std::env; #[tokio::main] @@ -11,7 +12,38 @@ async fn main() { .build() .expect("Failed to initialize Backpack API client"); - match client.get_markets().await { + let spot_only = GetMarketsRequest::new() + .with_spot_markets() + .send(&client) + .await; + + print_result(spot_only); + + let perp_only = GetMarketsRequest::new() + .with_perp_markets() + .send(&client) + .await; + + print_result(perp_only); + + let prediction_only = GetMarketsRequest::new() + .with_prediction_markets() + .send(&client) + .await; + + print_result(prediction_only); + + let spot_and_perp = GetMarketsRequest::new() + .with_spot_markets() + .with_perp_markets() + .send(&client) + .await; + + print_result(spot_and_perp); +} + +fn print_result(res: Result, Error>) { + match res { Ok(markets) => { let markets_json = serde_json::to_string_pretty(&markets).unwrap(); println!("{markets_json}");