bpx_api_client/
lib.rs

1//! Backpack Exchange API Client
2//!
3//! This module provides the `BpxClient` for interacting with the Backpack Exchange API.
4//! It includes functionality for authenticated and public endpoints,
5//! along with utilities for error handling, request signing, and response processing.
6//!
7//! ## Features
8//! - Request signing and authentication using ED25519 signatures.
9//! - Supports both REST and WebSocket endpoints.
10//! - Includes modules for managing capital, orders, trades, and user data.
11//!
12//! ## Example
13//! ```no_run
14//! use bpx_api_client::{BACKPACK_API_BASE_URL, BpxClient};
15//!
16//! #[tokio::main]
17//! async fn main() {
18//!     let base_url = BACKPACK_API_BASE_URL.to_string();
19//!     let secret = "your_api_secret_here";
20//!     let headers = None;
21//!
22//!     let client = BpxClient::init(base_url, secret, headers)
23//!         .expect("Failed to initialize Backpack API client");
24//!
25//!     match client.get_open_orders(Some("SOL_USDC")).await {
26//!         Ok(orders) => println!("Open Orders: {:?}", orders),
27//!         Err(err) => tracing::error!("Error: {:?}", err),
28//!     }
29//! }
30//! ```
31
32use base64::{engine::general_purpose::STANDARD, Engine};
33use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
34use reqwest::{header::CONTENT_TYPE, IntoUrl, Method, Request, Response, StatusCode};
35use routes::{
36    capital::{API_CAPITAL, API_DEPOSITS, API_DEPOSIT_ADDRESS, API_WITHDRAWALS},
37    order::{API_ORDER, API_ORDERS},
38    rfq::{API_RFQ, API_RFQ_QUOTE},
39    user::API_USER_2FA,
40};
41use serde::Serialize;
42use std::{
43    collections::BTreeMap,
44    time::{SystemTime, UNIX_EPOCH},
45};
46
47pub mod error;
48
49mod routes;
50
51#[cfg(feature = "ws")]
52mod ws;
53
54/// Re-export of the Backpack Exchange API types.
55pub use bpx_api_types as types;
56
57/// Re-export of the custom `Error` type and `Result` alias for error handling.
58pub use error::{Error, Result};
59
60const API_USER_AGENT: &str = "bpx-rust-client";
61const API_KEY_HEADER: &str = "X-API-Key";
62
63const DEFAULT_WINDOW: u32 = 5000;
64
65const SIGNATURE_HEADER: &str = "X-Signature";
66const TIMESTAMP_HEADER: &str = "X-Timestamp";
67const WINDOW_HEADER: &str = "X-Window";
68
69const JSON_CONTENT: &str = "application/json; charset=utf-8";
70
71/// The official base URL for the Backpack Exchange REST API.
72pub const BACKPACK_API_BASE_URL: &str = "https://api.backpack.exchange";
73
74/// The official WebSocket URL for real-time data from the Backpack Exchange.
75pub const BACKPACK_WS_URL: &str = "wss://ws.backpack.exchange";
76
77/// Type alias for custom HTTP headers passed to `BpxClient` during initialization.
78pub type BpxHeaders = reqwest::header::HeaderMap;
79
80/// A client for interacting with the Backpack Exchange API.
81#[derive(Debug, Clone)]
82pub struct BpxClient {
83    signer: SigningKey,
84    verifier: VerifyingKey,
85    base_url: String,
86    ws_url: Option<String>,
87    client: reqwest::Client,
88}
89
90impl std::ops::Deref for BpxClient {
91    type Target = reqwest::Client;
92
93    fn deref(&self) -> &Self::Target {
94        &self.client
95    }
96}
97
98impl std::ops::DerefMut for BpxClient {
99    fn deref_mut(&mut self) -> &mut Self::Target {
100        &mut self.client
101    }
102}
103
104impl AsRef<reqwest::Client> for BpxClient {
105    fn as_ref(&self) -> &reqwest::Client {
106        &self.client
107    }
108}
109
110// Public functions.
111impl BpxClient {
112    /// Initializes a new client with the given base URL, API secret, and optional headers.
113    ///
114    /// This sets up the signing and verification keys, and creates a `reqwest` client
115    /// with default headers including the API key and content type.
116    pub fn init(base_url: String, secret: &str, headers: Option<BpxHeaders>) -> Result<Self> {
117        Self::init_internal(base_url, None, secret, headers)
118    }
119
120    /// Initializes a new client with WebSocket support.
121    #[cfg(feature = "ws")]
122    pub fn init_with_ws(base_url: String, ws_url: String, secret: &str, headers: Option<BpxHeaders>) -> Result<Self> {
123        Self::init_internal(base_url, Some(ws_url), secret, headers)
124    }
125
126    /// Internal helper function for client initialization.
127    fn init_internal(
128        base_url: String,
129        ws_url: Option<String>,
130        secret: &str,
131        headers: Option<BpxHeaders>,
132    ) -> Result<Self> {
133        let signer = STANDARD
134            .decode(secret)?
135            .try_into()
136            .map(|s| SigningKey::from_bytes(&s))
137            .map_err(|_| Error::SecretKey)?;
138
139        let verifier = signer.verifying_key();
140
141        let mut headers = headers.unwrap_or_default();
142        headers.insert(API_KEY_HEADER, STANDARD.encode(verifier).parse()?);
143        headers.insert(CONTENT_TYPE, JSON_CONTENT.parse()?);
144
145        let client = reqwest::Client::builder()
146            .user_agent(API_USER_AGENT)
147            .default_headers(headers)
148            .build()?;
149
150        Ok(BpxClient {
151            signer,
152            verifier,
153            base_url,
154            ws_url,
155            client,
156        })
157    }
158
159    /// Creates a new, empty `BpxHeaders` instance.
160    pub fn create_headers() -> BpxHeaders {
161        reqwest::header::HeaderMap::new()
162    }
163
164    /// Processes the response to check for HTTP errors and extracts
165    /// the response content.
166    ///
167    /// Returns a custom error if the status code is non-2xx.
168    async fn process_response(res: Response) -> Result<Response> {
169        if let Err(e) = res.error_for_status_ref() {
170            let err_text = res.text().await?;
171            let err = Error::BpxApiError {
172                status_code: e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
173                message: err_text,
174            };
175            return Err(err);
176        }
177        Ok(res)
178    }
179
180    /// Sends a GET request to the specified URL and signs it before execution.
181    pub async fn get<U: IntoUrl>(&self, url: U) -> Result<Response> {
182        let mut req = self.client.get(url).build()?;
183        tracing::debug!("req: {:?}", req);
184        self.sign(&mut req)?;
185        let res = self.client.execute(req).await?;
186        Self::process_response(res).await
187    }
188
189    /// Sends a POST request with a JSON payload to the specified URL and signs it.
190    pub async fn post<P: Serialize, U: IntoUrl>(&self, url: U, payload: P) -> Result<Response> {
191        let mut req = self.client.post(url).json(&payload).build()?;
192        tracing::debug!("req: {:?}", req);
193        self.sign(&mut req)?;
194        let res = self.client.execute(req).await?;
195        Self::process_response(res).await
196    }
197
198    /// Sends a DELETE request with a JSON payload to the specified URL and signs it.
199    pub async fn delete<P: Serialize, U: IntoUrl>(&self, url: U, payload: P) -> Result<Response> {
200        let mut req = self.client.delete(url).json(&payload).build()?;
201        tracing::debug!("req: {:?}", req);
202        self.sign(&mut req)?;
203        let res = self.client.execute(req).await?;
204        Self::process_response(res).await
205    }
206
207    /// Returns a reference to the `VerifyingKey` used for request verification.
208    pub fn verifier(&self) -> &VerifyingKey {
209        &self.verifier
210    }
211
212    /// Returns a reference to the underlying HTTP client.
213    pub fn client(&self) -> &reqwest::Client {
214        &self.client
215    }
216}
217
218// Private functions.
219impl BpxClient {
220    /// Signs a request by generating a signature from the request details
221    /// and appending necessary headers for authentication.
222    ///
223    /// # Arguments
224    /// * `req` - The mutable reference to the request to be signed.
225    fn sign(&self, req: &mut Request) -> Result<()> {
226        let instruction = match req.url().path() {
227            API_CAPITAL if req.method() == Method::GET => "balanceQuery",
228            API_DEPOSITS if req.method() == Method::GET => "depositQueryAll",
229            API_DEPOSIT_ADDRESS if req.method() == Method::GET => "depositAddressQuery",
230            API_WITHDRAWALS if req.method() == Method::GET => "withdrawalQueryAll",
231            API_WITHDRAWALS if req.method() == Method::POST => "withdraw",
232            API_USER_2FA if req.method() == Method::POST => "issueTwoFactorToken",
233            API_ORDER if req.method() == Method::GET => "orderQuery",
234            API_ORDER if req.method() == Method::POST => "orderExecute",
235            API_ORDER if req.method() == Method::DELETE => "orderCancel",
236            API_ORDERS if req.method() == Method::GET => "orderQueryAll",
237            API_ORDERS if req.method() == Method::DELETE => "orderCancelAll",
238            API_RFQ if req.method() == Method::POST => "rfqSubmit",
239            API_RFQ_QUOTE if req.method() == Method::POST => "quoteSubmit",
240            _ => return Ok(()), // Other endpoints don't require signing.
241        };
242
243        let query_params = req
244            .url()
245            .query_pairs()
246            .map(|(x, y)| (x.into_owned(), y.into_owned()))
247            .collect::<BTreeMap<String, String>>();
248
249        let body_params = if let Some(b) = req.body() {
250            let s = std::str::from_utf8(b.as_bytes().unwrap_or_default())?;
251            serde_json::from_str::<BTreeMap<String, String>>(s)?
252        } else {
253            BTreeMap::new()
254        };
255
256        let timestamp = now_millis();
257
258        let mut signee = format!("instruction={instruction}");
259        for (k, v) in query_params {
260            signee.push_str(&format!("&{k}={v}"));
261        }
262        for (k, v) in body_params {
263            signee.push_str(&format!("&{k}={v}"));
264        }
265        signee.push_str(&format!("&timestamp={timestamp}&window={DEFAULT_WINDOW}"));
266        tracing::debug!("signee: {}", signee);
267
268        let signature: Signature = self.signer.sign(signee.as_bytes());
269        let signature = STANDARD.encode(signature.to_bytes());
270
271        req.headers_mut().insert(SIGNATURE_HEADER, signature.parse()?);
272        req.headers_mut()
273            .insert(TIMESTAMP_HEADER, timestamp.to_string().parse()?);
274        req.headers_mut()
275            .insert(WINDOW_HEADER, DEFAULT_WINDOW.to_string().parse()?);
276
277        if matches!(req.method(), &Method::POST | &Method::DELETE) {
278            req.headers_mut().insert(CONTENT_TYPE, JSON_CONTENT.parse()?);
279        }
280
281        Ok(())
282    }
283}
284
285/// Returns the current time in milliseconds since UNIX epoch.
286fn now_millis() -> u64 {
287    SystemTime::now()
288        .duration_since(UNIX_EPOCH)
289        .expect("Time went backwards")
290        .as_millis() as u64
291}