1use base64::{engine::general_purpose::STANDARD, Engine};
37use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
38use reqwest::{header::CONTENT_TYPE, IntoUrl, Method, Request, Response, StatusCode};
39use routes::{
40 account::{API_ACCOUNT, API_ACCOUNT_CONVERT_DUST, API_ACCOUNT_MAX_BORROW, API_ACCOUNT_MAX_WITHDRAWAL},
41 borrow_lend::API_BORROW_LEND_POSITIONS,
42 capital::{API_CAPITAL, API_COLLATERAL, API_DEPOSITS, API_DEPOSIT_ADDRESS, API_WITHDRAWALS},
43 futures::API_FUTURES_POSITION,
44 history::API_FILLS_HISTORY,
45 order::{API_ORDER, API_ORDERS},
46 rfq::{API_RFQ, API_RFQ_QUOTE},
47 user::API_USER_2FA,
48};
49use serde::Serialize;
50use serde_json::Value;
51use std::{
52 borrow::Cow,
53 collections::BTreeMap,
54 time::{SystemTime, UNIX_EPOCH},
55};
56
57pub mod error;
58
59mod routes;
60
61#[cfg(feature = "ws")]
62mod ws;
63
64pub use bpx_api_types as types;
66
67pub use error::{Error, Result};
69
70const API_USER_AGENT: &str = "bpx-rust-client";
71const API_KEY_HEADER: &str = "X-API-Key";
72
73const DEFAULT_WINDOW: u32 = 5000;
74
75const SIGNATURE_HEADER: &str = "X-Signature";
76const TIMESTAMP_HEADER: &str = "X-Timestamp";
77const WINDOW_HEADER: &str = "X-Window";
78
79const JSON_CONTENT: &str = "application/json; charset=utf-8";
80
81pub const BACKPACK_API_BASE_URL: &str = "https://api.backpack.exchange";
83
84pub const BACKPACK_WS_URL: &str = "wss://ws.backpack.exchange";
86
87pub type BpxHeaders = reqwest::header::HeaderMap;
89
90#[derive(Debug, Clone)]
92pub struct BpxClient {
93 signer: SigningKey,
94 verifier: VerifyingKey,
95 base_url: String,
96 #[allow(dead_code)]
97 ws_url: Option<String>,
98 client: reqwest::Client,
99}
100
101impl std::ops::Deref for BpxClient {
102 type Target = reqwest::Client;
103
104 fn deref(&self) -> &Self::Target {
105 &self.client
106 }
107}
108
109impl std::ops::DerefMut for BpxClient {
110 fn deref_mut(&mut self) -> &mut Self::Target {
111 &mut self.client
112 }
113}
114
115impl AsRef<reqwest::Client> for BpxClient {
116 fn as_ref(&self) -> &reqwest::Client {
117 &self.client
118 }
119}
120
121impl BpxClient {
123 pub fn init(base_url: String, secret: &str, headers: Option<BpxHeaders>) -> Result<Self> {
128 Self::init_internal(base_url, None, secret, headers)
129 }
130
131 #[cfg(feature = "ws")]
133 pub fn init_with_ws(base_url: String, ws_url: String, secret: &str, headers: Option<BpxHeaders>) -> Result<Self> {
134 Self::init_internal(base_url, Some(ws_url), secret, headers)
135 }
136
137 fn init_internal(
139 base_url: String,
140 ws_url: Option<String>,
141 secret: &str,
142 headers: Option<BpxHeaders>,
143 ) -> Result<Self> {
144 let signer = STANDARD
145 .decode(secret)?
146 .try_into()
147 .map(|s| SigningKey::from_bytes(&s))
148 .map_err(|_| Error::SecretKey)?;
149
150 let verifier = signer.verifying_key();
151
152 let mut headers = headers.unwrap_or_default();
153 headers.insert(API_KEY_HEADER, STANDARD.encode(verifier).parse()?);
154 headers.insert(CONTENT_TYPE, JSON_CONTENT.parse()?);
155
156 let client = reqwest::Client::builder()
157 .user_agent(API_USER_AGENT)
158 .default_headers(headers)
159 .build()?;
160
161 Ok(BpxClient {
162 signer,
163 verifier,
164 base_url,
165 ws_url,
166 client,
167 })
168 }
169
170 pub fn create_headers() -> BpxHeaders {
172 reqwest::header::HeaderMap::new()
173 }
174
175 async fn process_response(res: Response) -> Result<Response> {
180 if let Err(e) = res.error_for_status_ref() {
181 let err_text = res.text().await?;
182 let err = Error::BpxApiError {
183 status_code: e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
184 message: err_text.into(),
185 };
186 return Err(err);
187 }
188 Ok(res)
189 }
190
191 pub async fn get<U: IntoUrl>(&self, url: U) -> Result<Response> {
193 let req = self.build_and_maybe_sign_request::<(), _>(url, Method::GET, None)?;
194 tracing::debug!("req: {:?}", req);
195 let res = self.client.execute(req).await?;
196 Self::process_response(res).await
197 }
198
199 pub async fn post<P: Serialize, U: IntoUrl>(&self, url: U, payload: P) -> Result<Response> {
201 let req = self.build_and_maybe_sign_request(url, Method::POST, Some(&payload))?;
202 tracing::debug!("req: {:?}", req);
203 let res = self.client.execute(req).await?;
204 Self::process_response(res).await
205 }
206
207 pub async fn delete<P: Serialize, U: IntoUrl>(&self, url: U, payload: P) -> Result<Response> {
209 let req = self.build_and_maybe_sign_request(url, Method::DELETE, Some(&payload))?;
210 tracing::debug!("req: {:?}", req);
211 let res = self.client.execute(req).await?;
212 Self::process_response(res).await
213 }
214
215 pub async fn patch<P: Serialize, U: IntoUrl>(&self, url: U, payload: P) -> Result<Response> {
217 let req = self.build_and_maybe_sign_request(url, Method::PATCH, Some(&payload))?;
218 tracing::debug!("req: {:?}", req);
219 let res = self.client.execute(req).await?;
220 Self::process_response(res).await
221 }
222
223 pub const fn verifier(&self) -> &VerifyingKey {
225 &self.verifier
226 }
227
228 pub const fn client(&self) -> &reqwest::Client {
230 &self.client
231 }
232}
233
234impl BpxClient {
236 fn build_and_maybe_sign_request<P: Serialize, U: IntoUrl>(
242 &self,
243 url: U,
244 method: Method,
245 payload: Option<&P>,
246 ) -> Result<Request> {
247 let url = url.into_url()?;
248 let instruction = match url.path() {
249 API_CAPITAL if method == Method::GET => "balanceQuery",
250 API_DEPOSITS if method == Method::GET => "depositQueryAll",
251 API_DEPOSIT_ADDRESS if method == Method::GET => "depositAddressQuery",
252 API_WITHDRAWALS if method == Method::GET => "withdrawalQueryAll",
253 API_WITHDRAWALS if method == Method::POST => "withdraw",
254 API_USER_2FA if method == Method::POST => "issueTwoFactorToken",
255 API_ORDER if method == Method::GET => "orderQuery",
256 API_ORDER if method == Method::POST => "orderExecute",
257 API_ORDER if method == Method::DELETE => "orderCancel",
258 API_ORDERS if method == Method::GET => "orderQueryAll",
259 API_ORDERS if method == Method::DELETE => "orderCancelAll",
260 API_RFQ if method == Method::POST => "rfqSubmit",
261 API_RFQ_QUOTE if method == Method::POST => "quoteSubmit",
262 API_FUTURES_POSITION if method == Method::GET => "positionQuery",
263 API_BORROW_LEND_POSITIONS if method == Method::GET => "borrowLendPositionQuery",
264 API_COLLATERAL if method == Method::GET => "collateralQuery",
265 API_ACCOUNT if method == Method::GET => "accountQuery",
266 API_ACCOUNT_MAX_BORROW if method == Method::GET => "maxBorrowQuantity",
267 API_ACCOUNT_MAX_WITHDRAWAL if method == Method::GET => "maxWithdrawalQuantity",
268 API_ACCOUNT if method == Method::PATCH => "accountUpdate",
269 API_ACCOUNT_CONVERT_DUST if method == Method::POST => "convertDust",
270 API_FILLS_HISTORY if method == Method::GET => "fillHistoryQueryAll",
271 _ => {
272 let req = self.client().request(method, url);
273 if let Some(payload) = payload {
274 return Ok(req.json(payload).build()?);
275 } else {
276 return Ok(req.build()?);
277 }
278 }
279 };
280
281 let query_params = url.query_pairs().collect::<BTreeMap<Cow<'_, str>, Cow<'_, str>>>();
282 let body_params = if let Some(payload) = payload {
283 let s = serde_json::to_value(payload)?;
284 match s {
285 Value::Object(map) => map
286 .into_iter()
287 .map(|(k, v)| (k, v.to_string()))
288 .collect::<BTreeMap<_, _>>(),
289 _ => return Err(Error::InvalidRequest("payload must be a JSON object".into())),
290 }
291 } else {
292 BTreeMap::new()
293 };
294
295 let timestamp = now_millis();
296 let mut signee = format!("instruction={instruction}");
297 for (k, v) in query_params {
298 signee.push_str(&format!("&{k}={v}"));
299 }
300 for (k, v) in body_params {
301 let v = v.trim_start_matches('"').trim_end_matches('"');
302 signee.push_str(&format!("&{k}={v}"));
303 }
304 signee.push_str(&format!("×tamp={timestamp}&window={DEFAULT_WINDOW}"));
305 tracing::debug!("signee: {}", signee);
306
307 let signature: Signature = self.signer.sign(signee.as_bytes());
308 let signature = STANDARD.encode(signature.to_bytes());
309
310 let mut req = self.client().request(method, url);
311 if let Some(payload) = payload {
312 req = req.json(payload);
313 }
314 let mut req = req.build()?;
315 req.headers_mut().insert(SIGNATURE_HEADER, signature.parse()?);
316 req.headers_mut()
317 .insert(TIMESTAMP_HEADER, timestamp.to_string().parse()?);
318 req.headers_mut()
319 .insert(WINDOW_HEADER, DEFAULT_WINDOW.to_string().parse()?);
320 if matches!(req.method(), &Method::POST | &Method::DELETE) {
321 req.headers_mut().insert(CONTENT_TYPE, JSON_CONTENT.parse()?);
322 }
323 Ok(req)
324 }
325}
326
327fn now_millis() -> u64 {
329 SystemTime::now()
330 .duration_since(UNIX_EPOCH)
331 .expect("Time went backwards")
332 .as_millis() as u64
333}