Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ pub mod error;

mod routes;

// Re-export request builders.
pub use routes::markets::GetMarketsRequest;

#[cfg(feature = "ws")]
mod ws;

Expand Down Expand Up @@ -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<Output = Result<Response>>
Copy link
Contributor

@extremeandy extremeandy Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have a method on BpxClient like:

pub async fn send<R>(&self, request: R) -> impl Future<Output = Result<R::Response>> where R: BpxClientRequest

Then users can do:

let req = GetMarketsRequest::new();
let response = client.send(req).await?;

Feels a little more ergonomic than req.send(client).await

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(),
)),
}
}
}
}
90 changes: 85 additions & 5 deletions client/src/routes/markets.rs
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<Vec<Market>> {
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()
Comment on lines +31 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user I would expect that calling BpxClient::get_markets would be equivalent to invoking https://api.bpx.dev/api/v1/markets.

IMO we should not specify defaults here - the exchange backend already has defaults specified if no types are provided in the query:

let market_types = market_type.unwrap_or_else(|| vec![MarketType::SPOT, MarketType::PERP]);

.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<Vec<MarkPrice>> {
let url = self.base_url.join(API_MARK_PRICES)?;
Expand Down Expand Up @@ -96,3 +104,75 @@ impl BpxClient {
res.json().await.map_err(Into::into)
}
}

#[derive(Debug, Default, Clone)]
pub struct GetMarketsRequest(Vec<String>);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that performance really matters here, but could use Cow<'static, str> here since the market types are mostly going to be static str.


impl GetMarketsRequest {
pub fn new() -> Self {
Self::default()
}
Comment on lines +112 to +114
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add an additional overload that allows the user to pass a market type? This way if a new market type becomes available in the API it's still possible to query it without us having to add another method here.


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<Vec<Market>> {
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(())
}
}
36 changes: 34 additions & 2 deletions examples/src/bin/markets.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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<Vec<Market>, Error>) {
match res {
Ok(markets) => {
let markets_json = serde_json::to_string_pretty(&markets).unwrap();
println!("{markets_json}");
Expand Down
Loading