1use 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
54pub use bpx_api_types as types;
56
57pub 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
71pub const BACKPACK_API_BASE_URL: &str = "https://api.backpack.exchange";
73
74pub const BACKPACK_WS_URL: &str = "wss://ws.backpack.exchange";
76
77pub type BpxHeaders = reqwest::header::HeaderMap;
79
80#[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
110impl BpxClient {
112 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 #[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 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 pub fn create_headers() -> BpxHeaders {
161 reqwest::header::HeaderMap::new()
162 }
163
164 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 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 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 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 pub fn verifier(&self) -> &VerifyingKey {
209 &self.verifier
210 }
211
212 pub fn client(&self) -> &reqwest::Client {
214 &self.client
215 }
216}
217
218impl BpxClient {
220 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(()), };
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!("×tamp={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
285fn now_millis() -> u64 {
287 SystemTime::now()
288 .duration_since(UNIX_EPOCH)
289 .expect("Time went backwards")
290 .as_millis() as u64
291}