From 264531dd64e4fe714ba06be5d354c99302a86bab Mon Sep 17 00:00:00 2001 From: Francisco Lopez Date: Wed, 13 Nov 2024 09:15:51 +0100 Subject: [PATCH 1/7] docs + websocket + RFQs Enhanced CI, documentation and READMEs; added private WS subscription mechanism; added support for RFQs --- .github/workflows/audit.yml | 20 ++ .github/workflows/rust.yml | 62 ++++++ README.md | 21 +- img/backpack.png | Bin 0 -> 6948 bytes img/book_example.png | Bin 0 -> 81312 bytes rust/.cargo/audit.toml | 2 + rust/.gitignore | 14 ++ rust/Cargo.toml | 14 +- rust/README.md | 32 ++- rust/client/Cargo.toml | 19 +- rust/client/src/error.rs | 53 +++-- rust/client/src/lib.rs | 281 ++++++++++++++++-------- rust/client/src/{ => routes}/capital.rs | 39 ++-- rust/client/src/{ => routes}/markets.rs | 23 +- rust/client/src/routes/mod.rs | 6 + rust/client/src/{ => routes}/order.rs | 43 ++-- rust/client/src/routes/rfq.rs | 33 +++ rust/client/src/{ => routes}/trades.rs | 9 +- rust/client/src/routes/user.rs | 20 ++ rust/client/src/user.rs | 16 -- rust/client/src/ws/mod.rs | 62 ++++++ rust/examples/Cargo.toml | 19 ++ rust/examples/README.md | 15 ++ rust/examples/justfile | 8 + rust/examples/src/bin/rfq.rs | 25 +++ rust/justfile | 26 +++ rust/rust-toolchain.toml | 2 + rust/rustfmt.toml | 3 + rust/types/Cargo.toml | 7 +- rust/types/src/lib.rs | 21 +- rust/types/src/order.rs | 20 +- rust/types/src/rfq.rs | 52 +++++ 32 files changed, 742 insertions(+), 225 deletions(-) create mode 100644 .github/workflows/audit.yml create mode 100644 .github/workflows/rust.yml create mode 100644 img/backpack.png create mode 100644 img/book_example.png create mode 100644 rust/.cargo/audit.toml create mode 100644 rust/.gitignore rename rust/client/src/{ => routes}/capital.rs (51%) rename rust/client/src/{ => routes}/markets.rs (59%) create mode 100644 rust/client/src/routes/mod.rs rename rust/client/src/{ => routes}/order.rs (51%) create mode 100644 rust/client/src/routes/rfq.rs rename rust/client/src/{ => routes}/trades.rs (64%) create mode 100644 rust/client/src/routes/user.rs delete mode 100644 rust/client/src/user.rs create mode 100644 rust/client/src/ws/mod.rs create mode 100644 rust/examples/Cargo.toml create mode 100644 rust/examples/README.md create mode 100644 rust/examples/justfile create mode 100644 rust/examples/src/bin/rfq.rs create mode 100644 rust/justfile create mode 100644 rust/rust-toolchain.toml create mode 100644 rust/rustfmt.toml create mode 100644 rust/types/src/rfq.rs diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000..a0b4122 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,20 @@ +name: audit + +on: + push: + paths: + - "**/Cargo.toml" + - "**/Cargo.lock" + schedule: + - cron: "0 0 * * *" + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: cargo audit + working-directory: rust + env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-Dwarnings" \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..6cb8ac7 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,62 @@ +name: rust + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] + branches: + - master + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-Dwarnings" + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + id: cache-dependencies + with: + path: | + rust/.cargo/registry + rust/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: cargo test + working-directory: rust + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + components: rustfmt + - run: cargo fmt --all -- --check + working-directory: rust + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + components: clippy + - run: cargo clippy --all-targets --all-features + working-directory: rust \ No newline at end of file diff --git a/README.md b/README.md index 6e966d5..e90bde6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,20 @@ -# Backpack Exchange Api Clients +# Backpack Exchange API -This repository contains the Backpack Exchange API clients for various languages. +Backpack -## Clients +Access the official API documentation here: [https://docs.backpack.exchange/](https://docs.backpack.exchange/). -[Rust](./rust) +This repository hosts the Backpack Exchange API clients. Currently, the client is only available in [Rust](./rust). + +Order Book example + +*Example of an Order Book* + +## Contributing + +We welcome contributions from the community! +Feel free to open bug reports, suggest new features, or submit pull requests to improve the matching engine and related components. + +## License + +This project is licensed under the [Apache 2.0 License](LICENSE). \ No newline at end of file diff --git a/img/backpack.png b/img/backpack.png new file mode 100644 index 0000000000000000000000000000000000000000..140953504dcd6bd5268e94f3d72fbb98c2d9a172 GIT binary patch literal 6948 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 IkTDJZA8j~tV*mgE literal 0 HcmV?d00001 diff --git a/img/book_example.png b/img/book_example.png new file mode 100644 index 0000000000000000000000000000000000000000..3ae127371f223c306a3e9677b33fa432f8f5fe77 GIT binary patch literal 81312 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 literal 0 HcmV?d00001 diff --git a/rust/.cargo/audit.toml b/rust/.cargo/audit.toml new file mode 100644 index 0000000..9e54fc7 --- /dev/null +++ b/rust/.cargo/audit.toml @@ -0,0 +1,2 @@ +[advisories] +ignore = ["RUSTSEC-2023-0033"] # issue in rust_decimal due to borsh diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..6985cf1 --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/rust/Cargo.toml b/rust/Cargo.toml index a22d65f..bc418d5 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,18 +1,26 @@ [workspace] -members = ["client", "types"] +members = ["client", "examples", "types"] resolver = "2" +[workspace.package] +keywords = ["backpack", "exchange"] +repository = "https://github.com/backpack-exchange/bpx-api-client" + [workspace.dependencies] +anyhow = "1.0.93" base64 = "0.22.1" chrono = { version = "0.4.38", features = ["serde"] } ed25519-dalek = "2.1.1" +futures-util = "0.3.31" reqwest = { version = "0.12.5", default-features = false, features = [ - "json", - "rustls-tls", + "json", + "rustls-tls", ] } rust_decimal = "1.35.0" serde = { version = "1.0.208", features = ["derive"] } serde_json = "1.0.125" strum = { version = "0.26.3", features = ["derive"] } thiserror = "1.0.63" +tokio = { version = "1.41.1", features = ["full"] } +tokio-tungstenite = "0.24.0" tracing = "0.1.40" diff --git a/rust/README.md b/rust/README.md index 7572080..7d6ec67 100644 --- a/rust/README.md +++ b/rust/README.md @@ -1,14 +1,32 @@ -# Backpack Exchange Rust Client +[![Build Badge]][build] [![Crates Badge]][crates] [![Docs Badge]][docs] [![License Badge]][license] -## Table of Contents +[Build Badge]: https://github.com/backpack-exchange/bpx-api-client/actions/workflows/rust.yml/badge.svg +[build]: https://github.com/backpack-exchange/bpx-api-client/actions -- [Usage](#usage) -- [Contributing](#contributing) +[Crates Badge]: https://img.shields.io/crates/v/bpx_api_client.svg +[crates]: https://crates.io/crates/bpx_api_client + +[Docs Badge]: https://docs.rs/bpx_api_client/badge.svg +[docs]: https://docs.rs/bpx_api_client + +[License Badge]: https://img.shields.io/badge/License-Apache_2.0-blue.svg +[license]: ../LICENSE + +# Backpack Exchange API Crate + +This crate provides both REST and WebSocket APIs for interacting with the Backpack Exchange: + +- **REST API**: Includes public and private (authenticated) endpoints. +- **WebSocket API**: Offers public and private streams. + +The official API documentation is available at [https://docs.backpack.exchange/](https://docs.backpack.exchange/). ## Usage -Instructions on how to use the project. +This project uses [Just](https://github.com/casey/just) to manage various build and development tasks. -## Contributing +To see the available commands, run: -Guidelines on how to contribute to the project. +```shell +just +``` diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index c516b18..7480581 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "bpx-api-client" -authors = ["Backpack "] +version = "0.5.0" license = "Apache-2.0" -version = "0.4.0" edition = "2021" -description = "Rust client for Backpack Exchange" -repository = "https://github.com/backpack-exchange/bpx-api-client/tree/master/rust/client" +description = "Backpack Exchange API client" +repository = "https://github.com/backpack-exchange/bpx-api-client" [dependencies] base64 = { workspace = true } -bpx-api-types = { version = "0.2.0", path = "../types" } +bpx-api-types = { path = "../types" } ed25519-dalek = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } @@ -17,5 +16,11 @@ serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -[dev-dependencies] -tokio = { version = "1.39", features = ["full"] } +# Dependencies for the `ws` feature +tokio = { workspace = true, optional = true } +tokio-tungstenite = { workspace = true, optional = true } +futures-util = { workspace = true, optional = true } + +[features] +default = ["ws"] +ws = ["tokio", "tokio-tungstenite", "futures-util"] diff --git a/rust/client/src/error.rs b/rust/client/src/error.rs index d47a28c..43d47d2 100644 --- a/rust/client/src/error.rs +++ b/rust/client/src/error.rs @@ -1,37 +1,54 @@ +//! Error handling module for the Backpack Exchange API client. +//! +//! Defines a custom `Error` type and a `Result` type alias to encapsulate +//! various errors that can occur during API interactions. + +/// A type alias for `Result` using the custom `Error` type. pub type Result = std::result::Result; +/// Enum representing possible errors in the Backpack Exchange API client. #[derive(Debug, thiserror::Error)] pub enum Error { + /// Error decoding a base64 string. #[error(transparent)] - InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), + Base64Decode(#[from] base64::DecodeError), + + /// Backpack API returned an error with status code and message. + #[error("Backpack API error: {status_code}: {message}")] + BpxApiError { + status_code: reqwest::StatusCode, + message: String, + }, + /// Invalid HTTP header value. #[error(transparent)] - Reqwest(#[from] reqwest::Error), + InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), - #[error("Invalid URL: {0}")] - UrlParseError(String), + /// Represents an invalid request with a custom message. + #[error("Invalid request: {0}")] + InvalidRequest(String), + /// General HTTP client error from `reqwest`. #[error(transparent)] - SystemTime(#[from] std::time::SystemTimeError), + Reqwest(#[from] reqwest::Error), + /// Invalid secret key provided. + #[error("Invalid secret key")] + SecretKey, + + /// Error during JSON serialization or deserialization. #[error(transparent)] SerdeJson(#[from] serde_json::error::Error), + /// Error working with system time. #[error(transparent)] - Utf8(#[from] std::str::Utf8Error), + SystemTime(#[from] std::time::SystemTimeError), + /// UTF-8 decoding error. #[error(transparent)] - Base64Decode(#[from] base64::DecodeError), - - #[error("Invalid secret key")] - SecretKey, - - #[error("Invalid request: {0}")] - InvalidRequest(String), + Utf8(#[from] std::str::Utf8Error), - #[error("Backpack API error: {status_code}: {message}")] - BpxApiError { - status_code: reqwest::StatusCode, - message: String, - }, + /// Invalid URL format. + #[error("Invalid URL: {0}")] + UrlParseError(String), } diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index 02e677d..9786167 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -1,28 +1,90 @@ +//! Backpack Exchange API Client +//! +//! This module provides the `BpxClient` for interacting with the Backpack Exchange API. +//! It includes functionality for authenticated and public endpoints, +//! along with utilities for error handling, request signing, and response processing. +//! +//! ## Features +//! - Request signing and authentication using ED25519 signatures. +//! - Supports both REST and WebSocket endpoints. +//! - Includes modules for managing capital, orders, trades, and user data. +//! +//! ## Example +//! ```no_run +//! use bpx_api_client::BpxClient; +//! +//! #[tokio::main] +//! async fn main() { +//! let base_url = "https://api.backpack.exchange/".to_string(); +//! let secret = "your_api_secret_here"; +//! let headers = None; +//! +//! let client = BpxClient::init(base_url, secret, headers) +//! .expect("Failed to initialize Backpack API client"); +//! +//! match client.get_open_orders(Some("SOL_USDC")).await { +//! Ok(orders) => println!("Open Orders: {:?}", orders), +//! Err(err) => eprintln!("Error: {:?}", err), +//! } +//! } +//! ``` + use base64::{engine::general_purpose::STANDARD, Engine}; use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey}; -pub use error::{Error, Result}; use reqwest::{header::CONTENT_TYPE, IntoUrl, Method, Request, Response, StatusCode}; +use routes::{ + capital::{API_CAPITAL, API_DEPOSITS, API_DEPOSIT_ADDRESS, API_WITHDRAWALS}, + order::{API_ORDER, API_ORDERS}, + rfq::{API_RFQ, API_RFQ_QUOTE}, + user::API_USER_2FA, +}; use serde::Serialize; -use std::collections::BTreeMap; +use std::{ + collections::BTreeMap, + time::{SystemTime, UNIX_EPOCH}, +}; + +pub mod error; +mod routes; + +#[cfg(feature = "ws")] +mod ws; + +/// Re-export of the Backpack Exchange API types. pub use bpx_api_types as types; -pub use reqwest; -pub mod capital; -pub mod error; -pub mod markets; -pub mod order; -pub mod trades; -pub mod user; +/// Re-export of the custom `Error` type and `Result` alias for error handling. +pub use error::{Error, Result}; + +const API_USER_AGENT: &str = "bpx-rust-client"; +const API_KEY_HEADER: &str = "X-API-Key"; + +const DEFAULT_WINDOW: u32 = 5000; + +const SIGNATURE_HEADER: &str = "X-Signature"; +const TIMESTAMP_HEADER: &str = "X-Timestamp"; +const WINDOW_HEADER: &str = "X-Window"; + +const JSON_CONTENT: &str = "application/json; charset=utf-8"; + +/// The official base URL for the Backpack Exchange REST API. +pub const BACKPACK_API_BASE_URL: &str = "https://api.backpack.exchange"; + +/// The official WebSocket URL for real-time data from the Backpack Exchange. +pub const BACKPACK_WS_URL: &str = "wss://ws.backpack.exchange"; -const SIGNING_WINDOW: u32 = 5000; +/// Type alias for custom HTTP headers passed to `BpxClient` during initialization. +pub type BpxHeaders = reqwest::header::HeaderMap; +/// A client for interacting with the Backpack Exchange API. #[derive(Debug, Clone)] pub struct BpxClient { - pub verifier: VerifyingKey, signer: SigningKey, + verifier: VerifyingKey, base_url: String, - pub client: reqwest::Client, + ws_url: Option, + client: reqwest::Client, } impl std::ops::Deref for BpxClient { @@ -45,23 +107,31 @@ impl AsRef for BpxClient { } } +// Public functions. impl BpxClient { - /// Initialize a new client with the given base URL, API key, and API secret. + /// Initializes a new client with the given base URL, API secret, and optional headers. /// - /// # Arguments - /// * `base_url` - The base URL of the API. - /// * `api_secret` - The API secret. - /// * `headers` - Additional headers to include in the request. - /// - /// # Returns - /// A new client instance. - pub fn init( + /// This sets up the signing and verification keys, and creates a `reqwest` client + /// with default headers including the API key and content type. + pub fn init(base_url: String, secret: &str, headers: Option) -> Result { + Self::init_internal(base_url, None, secret, headers) + } + + /// Initializes a new client with WebSocket support. + #[cfg(feature = "ws")] + pub fn init_with_ws(base_url: String, ws_url: String, secret: &str, headers: Option) -> Result { + Self::init_internal(base_url, Some(ws_url), secret, headers) + } + + /// Internal helper function for client initialization. + fn init_internal( base_url: String, - api_secret: &str, - headers: Option, + ws_url: Option, + secret: &str, + headers: Option, ) -> Result { let signer = STANDARD - .decode(api_secret)? + .decode(secret)? .try_into() .map(|s| SigningKey::from_bytes(&s)) .map_err(|_| Error::SecretKey)?; @@ -69,44 +139,107 @@ impl BpxClient { let verifier = signer.verifying_key(); let mut headers = headers.unwrap_or_default(); - headers.insert("X-API-Key", STANDARD.encode(verifier).parse()?); - headers.insert(CONTENT_TYPE, "application/json; charset=utf-8".parse()?); + headers.insert(API_KEY_HEADER, STANDARD.encode(verifier).parse()?); + headers.insert(CONTENT_TYPE, JSON_CONTENT.parse()?); let client = reqwest::Client::builder() - .user_agent("bpx-rust-client") + .user_agent(API_USER_AGENT) .default_headers(headers) .build()?; Ok(BpxClient { - verifier, signer, + verifier, base_url, + ws_url, client, }) } + /// Creates a new, empty `BpxHeaders` instance. + pub fn create_headers() -> BpxHeaders { + reqwest::header::HeaderMap::new() + } + + /// Processes the response to check for HTTP errors and extracts + /// the response content. + /// + /// Returns a custom error if the status code is non-2xx. + async fn process_response(res: Response) -> Result { + if let Err(e) = res.error_for_status_ref() { + let err_text = res.text().await?; + let err = Error::BpxApiError { + status_code: e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + message: err_text, + }; + return Err(err); + } + Ok(res) + } + + /// Sends a GET request to the specified URL and signs it before execution. + pub async fn get(&self, url: U) -> Result { + let mut req = self.client.get(url).build()?; + tracing::debug!("req: {:?}", req); + self.sign(&mut req)?; + let res = self.client.execute(req).await?; + Self::process_response(res).await + } + + /// Sends a POST request with a JSON payload to the specified URL and signs it. + pub async fn post(&self, url: U, payload: P) -> Result { + let mut req = self.client.post(url).json(&payload).build()?; + tracing::debug!("req: {:?}", req); + self.sign(&mut req)?; + let res = self.client.execute(req).await?; + Self::process_response(res).await + } + + /// Sends a DELETE request with a JSON payload to the specified URL and signs it. + pub async fn delete(&self, url: U, payload: P) -> Result { + let mut req = self.client.delete(url).json(&payload).build()?; + tracing::debug!("req: {:?}", req); + self.sign(&mut req)?; + let res = self.client.execute(req).await?; + Self::process_response(res).await + } + + /// Returns a reference to the `VerifyingKey` used for request verification. + pub fn verifier(&self) -> &VerifyingKey { + &self.verifier + } + + /// Returns a reference to the underlying HTTP client. + pub fn client(&self) -> &reqwest::Client { + &self.client + } +} + +// Private functions. +impl BpxClient { + /// Signs a request by generating a signature from the request details + /// and appending necessary headers for authentication. + /// + /// # Arguments + /// * `req` - The mutable reference to the request to be signed. fn sign(&self, req: &mut Request) -> Result<()> { let instruction = match req.url().path() { - "/api/v1/capital" if req.method() == Method::GET => "balanceQuery", - "/wapi/v1/capital/deposits" if req.method() == Method::GET => "depositQueryAll", - "/wapi/v1/capital/deposit/address" if req.method() == Method::GET => { - "depositAddressQuery" - } - "/wapi/v1/capital/withdrawals" if req.method() == Method::GET => "withdrawalQueryAll", - "/wapi/v1/capital/withdrawals" if req.method() == Method::POST => "withdraw", - "/wapi/v1/user/2fa" if req.method() == Method::POST => "issueTwoFactorToken", - "/api/v1/order" if req.method() == Method::GET => "orderQuery", - "/api/v1/order" if req.method() == Method::POST => "orderExecute", - "/api/v1/order" if req.method() == Method::DELETE => "orderCancel", - "/api/v1/orders" if req.method() == Method::GET => "orderQueryAll", - "/api/v1/orders" if req.method() == Method::DELETE => "orderCancelAll", - _ => return Ok(()), // other endpoints don't require signing + API_CAPITAL if req.method() == Method::GET => "balanceQuery", + API_DEPOSITS if req.method() == Method::GET => "depositQueryAll", + API_DEPOSIT_ADDRESS if req.method() == Method::GET => "depositAddressQuery", + API_WITHDRAWALS if req.method() == Method::GET => "withdrawalQueryAll", + API_WITHDRAWALS if req.method() == Method::POST => "withdraw", + API_USER_2FA if req.method() == Method::POST => "issueTwoFactorToken", + API_ORDER if req.method() == Method::GET => "orderQuery", + API_ORDER if req.method() == Method::POST => "orderExecute", + API_ORDER if req.method() == Method::DELETE => "orderCancel", + API_ORDERS if req.method() == Method::GET => "orderQueryAll", + API_ORDERS if req.method() == Method::DELETE => "orderCancelAll", + API_RFQ if req.method() == Method::POST => "rfqSubmit", + API_RFQ_QUOTE if req.method() == Method::POST => "quoteSubmit", + _ => return Ok(()), // Other endpoints don't require signing. }; - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH)? - .as_millis(); - let query_params = req .url() .query_pairs() @@ -120,67 +253,39 @@ impl BpxClient { BTreeMap::new() }; + let timestamp = now_millis(); + let mut signee = format!("instruction={instruction}"); for (k, v) in query_params { signee.push_str(&format!("&{k}={v}")); } - for (k, v) in body_params { signee.push_str(&format!("&{k}={v}")); } - signee.push_str(&format!("×tamp={timestamp}&window={SIGNING_WINDOW}")); + signee.push_str(&format!("×tamp={timestamp}&window={DEFAULT_WINDOW}")); tracing::debug!("signee: {}", signee); let signature: Signature = self.signer.sign(signee.as_bytes()); let signature = STANDARD.encode(signature.to_bytes()); + req.headers_mut().insert(SIGNATURE_HEADER, signature.parse()?); req.headers_mut() - .insert("X-Timestamp", timestamp.to_string().parse()?); + .insert(TIMESTAMP_HEADER, timestamp.to_string().parse()?); req.headers_mut() - .insert("X-Window", SIGNING_WINDOW.to_string().parse()?); - req.headers_mut().insert("X-Signature", signature.parse()?); + .insert(WINDOW_HEADER, DEFAULT_WINDOW.to_string().parse()?); if matches!(req.method(), &Method::POST | &Method::DELETE) { - req.headers_mut() - .insert(CONTENT_TYPE, "application/json; charset=utf-8".parse()?); + req.headers_mut().insert(CONTENT_TYPE, JSON_CONTENT.parse()?); } Ok(()) } +} - async fn process_response(res: Response) -> Result { - if let Err(e) = res.error_for_status_ref() { - let err_text = res.text().await?; - let err = Error::BpxApiError { - status_code: e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), - message: err_text, - }; - return Err(err); - } - Ok(res) - } - - pub async fn get(&self, url: U) -> Result { - let mut req = self.client.get(url).build()?; - tracing::debug!("req: {:?}", req); - self.sign(&mut req)?; - let res = self.client.execute(req).await?; - Self::process_response(res).await - } - - pub async fn post(&self, url: U, payload: P) -> Result { - let mut req = self.client.post(url).json(&payload).build()?; - tracing::debug!("req: {:?}", req); - self.sign(&mut req)?; - let res = self.client.execute(req).await?; - Self::process_response(res).await - } - - pub async fn delete(&self, url: U, payload: P) -> Result { - let mut req = self.client.delete(url).json(&payload).build()?; - tracing::debug!("req: {:?}", req); - self.sign(&mut req)?; - let res = self.client.execute(req).await?; - Self::process_response(res).await - } +/// Returns the current time in milliseconds since UNIX epoch. +fn now_millis() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() as u64 } diff --git a/rust/client/src/capital.rs b/rust/client/src/routes/capital.rs similarity index 51% rename from rust/client/src/capital.rs rename to rust/client/src/routes/capital.rs index 8d0b477..d9e419a 100644 --- a/rust/client/src/capital.rs +++ b/rust/client/src/routes/capital.rs @@ -8,19 +8,26 @@ use bpx_api_types::{ use crate::BpxClient; +#[doc(hidden)] +pub const API_CAPITAL: &str = "/api/v1/capital"; +#[doc(hidden)] +pub const API_DEPOSITS: &str = "/wapi/v1/capital/deposits"; +#[doc(hidden)] +pub const API_DEPOSIT_ADDRESS: &str = "/wapi/v1/capital/deposit/address"; +#[doc(hidden)] +pub const API_WITHDRAWALS: &str = "/wapi/v1/capital/withdrawals"; + impl BpxClient { + /// Fetches the account's current balances. pub async fn get_balances(&self) -> Result> { - let url = format!("{}/api/v1/capital", self.base_url); + let url = format!("{}{}", self.base_url, API_CAPITAL); let res = self.get(url).await?; res.json().await.map_err(Into::into) } - pub async fn get_deposits( - &self, - limit: Option, - offset: Option, - ) -> Result> { - let mut url = format!("{}/wapi/v1/capital/deposits", self.base_url); + /// Retrieves a list of deposits with optional pagination. + pub async fn get_deposits(&self, limit: Option, offset: Option) -> Result> { + let mut url = format!("{}{}", self.base_url, API_DEPOSITS); for (k, v) in [("limit", limit), ("offset", offset)] { if let Some(v) = v { url.push_str(&format!("&{}={}", k, v)); @@ -30,21 +37,16 @@ impl BpxClient { res.json().await.map_err(Into::into) } + /// Fetches the deposit address for a specified blockchain. pub async fn get_deposit_address(&self, blockchain: Blockchain) -> Result { - let url = format!( - "{}/wapi/v1/capital/deposit/address?blockchain={}", - self.base_url, blockchain - ); + let url = format!("{}{}?blockchain={}", self.base_url, API_DEPOSIT_ADDRESS, blockchain); let res = self.get(url).await?; res.json().await.map_err(Into::into) } - pub async fn get_withdrawals( - &self, - limit: Option, - offset: Option, - ) -> Result> { - let mut url = format!("{}/wapi/v1/capital/withdrawals", self.base_url); + /// Retrieves a list of withdrawals with optional pagination. + pub async fn get_withdrawals(&self, limit: Option, offset: Option) -> Result> { + let mut url = format!("{}{}", self.base_url, API_WITHDRAWALS); for (k, v) in [("limit", limit), ("offset", offset)] { if let Some(v) = v { url.push_str(&format!("{}={}&", k, v)); @@ -54,8 +56,9 @@ impl BpxClient { res.json().await.map_err(Into::into) } + /// Submits a withdrawal request for the specified payload. pub async fn request_withdrawal(&self, payload: RequestWithdrawalPayload) -> Result<()> { - let endpoint = format!("{}/wapi/v1/capital/withdrawals", self.base_url); + let endpoint = format!("{}{}", self.base_url, API_WITHDRAWALS); self.post(endpoint, payload).await.map(|_| ()) } } diff --git a/rust/client/src/markets.rs b/rust/client/src/routes/markets.rs similarity index 59% rename from rust/client/src/markets.rs rename to rust/client/src/routes/markets.rs index e242d3a..2e41cd0 100644 --- a/rust/client/src/markets.rs +++ b/rust/client/src/routes/markets.rs @@ -5,31 +5,42 @@ use bpx_api_types::markets::{Kline, Market, OrderBookDepth, Ticker, Token}; use crate::error::Result; use crate::BpxClient; +const API_ASSETS: &str = "/api/v1/assets"; +const API_MARKETS: &str = "/api/v1/markets"; +const API_TICKER: &str = "/api/v1/ticker"; +const API_DEPTH: &str = "/api/v1/depth"; +const API_KLINES: &str = "/api/v1/klines"; + impl BpxClient { + /// Fetches available assets and their associated tokens. pub async fn get_assets(&self) -> Result>> { - let url = format!("{}/api/v1/assets", self.base_url); + let url = format!("{}{}", self.base_url, API_ASSETS); let res = self.get(url).await?; res.json().await.map_err(Into::into) } + /// Retrieves a list of available markets. pub async fn get_markets(&self) -> Result> { - let url = format!("{}/api/v1/markets", self.base_url); + let url = format!("{}{}", self.base_url, API_MARKETS); let res = self.get(url).await?; res.json().await.map_err(Into::into) } + /// Fetches the ticker information for a given symbol. pub async fn get_ticker(&self, symbol: &str) -> Result> { - let url = format!("{}/api/v1/ticker&symbol={}", self.base_url, symbol); + let url = format!("{}{}&symbol={}", self.base_url, API_TICKER, symbol); let res = self.get(url).await?; res.json().await.map_err(Into::into) } + /// Retrieves the order book depth for a given symbol. pub async fn get_order_book_depth(&self, symbol: &str) -> Result { - let url = format!("{}/api/v1/depth&symbol={}", self.base_url, symbol); + let url = format!("{}{}&symbol={}", self.base_url, API_DEPTH, symbol); let res = self.get(url).await?; res.json().await.map_err(Into::into) } + /// Fetches historical K-line (candlestick) data for a given symbol and interval. pub async fn get_k_lines( &self, symbol: &str, @@ -38,8 +49,8 @@ impl BpxClient { end_time: Option, ) -> Result> { let mut url = format!( - "/{}/api/v1/klines?symbol={}&kline_interval={}", - self.base_url, symbol, kline_interval + "/{}{}?symbol={}&kline_interval={}", + self.base_url, API_KLINES, symbol, kline_interval ); for (k, v) in [("start_time", start_time), ("end_time", end_time)] { if let Some(v) = v { diff --git a/rust/client/src/routes/mod.rs b/rust/client/src/routes/mod.rs new file mode 100644 index 0000000..e81cda1 --- /dev/null +++ b/rust/client/src/routes/mod.rs @@ -0,0 +1,6 @@ +pub mod capital; +pub mod markets; +pub mod order; +pub mod rfq; +pub mod trades; +pub mod user; diff --git a/rust/client/src/order.rs b/rust/client/src/routes/order.rs similarity index 51% rename from rust/client/src/order.rs rename to rust/client/src/routes/order.rs index 56d58eb..a37bfca 100644 --- a/rust/client/src/order.rs +++ b/rust/client/src/routes/order.rs @@ -1,45 +1,40 @@ -use bpx_api_types::order::{ - CancelOpenOrdersPayload, CancelOrderPayload, ExecuteOrderPayload, Order, -}; +use bpx_api_types::order::{CancelOpenOrdersPayload, CancelOrderPayload, ExecuteOrderPayload, Order}; use crate::error::{Error, Result}; use crate::BpxClient; +#[doc(hidden)] +pub const API_ORDER: &str = "/api/v1/order"; +#[doc(hidden)] +pub const API_ORDERS: &str = "/api/v1/orders"; + impl BpxClient { - pub async fn get_open_order( - &self, - symbol: &str, - order_id: Option<&str>, - client_id: Option, - ) -> Result { - let mut url = format!("{}/api/v1/order?symbol={}", self.base_url, symbol); + /// Fetches a specific open order by symbol and either order ID or client ID. + pub async fn get_open_order(&self, symbol: &str, order_id: Option<&str>, client_id: Option) -> Result { + let mut url = format!("{}{}?symbol={}", self.base_url, API_ORDER, symbol); if let Some(order_id) = order_id { url.push_str(&format!("&orderId={}", order_id)); } else { url.push_str(&format!( "&clientId={}", - client_id.ok_or_else(|| Error::InvalidRequest( - "either order_id or client_id is required".to_string() - ))? + client_id + .ok_or_else(|| Error::InvalidRequest("either order_id or client_id is required".to_string()))? )); } let res = self.get(url).await?; res.json().await.map_err(Into::into) } + /// Executes a new order with the given payload. pub async fn execute_order(&self, payload: ExecuteOrderPayload) -> Result { - let endpoint = format!("{}/api/v1/order", self.base_url); + let endpoint = format!("{}{}", self.base_url, API_ORDER); let res = self.post(endpoint, payload).await?; res.json().await.map_err(Into::into) } - pub async fn cancel_order( - &self, - symbol: &str, - order_id: Option<&str>, - client_id: Option, - ) -> Result { - let url = format!("{}/api/v1/order", self.base_url); + /// Cancels a specific order by symbol and either order ID or client ID. + pub async fn cancel_order(&self, symbol: &str, order_id: Option<&str>, client_id: Option) -> Result { + let url = format!("{}{}", self.base_url, API_ORDER); let payload = CancelOrderPayload { symbol: symbol.to_string(), order_id: order_id.map(|s| s.to_string()), @@ -50,8 +45,9 @@ impl BpxClient { res.json().await.map_err(Into::into) } + /// Retrieves all open orders, optionally filtered by symbol. pub async fn get_open_orders(&self, symbol: Option<&str>) -> Result> { - let mut url = format!("{}/api/v1/orders", self.base_url); + let mut url = format!("{}{}", self.base_url, API_ORDERS); if let Some(s) = symbol { url.push_str(&format!("?symbol={s}")); } @@ -59,8 +55,9 @@ impl BpxClient { res.json().await.map_err(Into::into) } + /// Cancels all open orders matching the specified payload. pub async fn cancel_open_orders(&self, payload: CancelOpenOrdersPayload) -> Result> { - let url = format!("{}/api/v1/orders", self.base_url); + let url = format!("{}{}", self.base_url, API_ORDERS); let res = self.delete(url, payload).await?; res.json().await.map_err(Into::into) } diff --git a/rust/client/src/routes/rfq.rs b/rust/client/src/routes/rfq.rs new file mode 100644 index 0000000..d587e7e --- /dev/null +++ b/rust/client/src/routes/rfq.rs @@ -0,0 +1,33 @@ +use bpx_api_types::rfq::{Quote, QuotePayload, RequestForQuote, RequestForQuotePayload}; + +#[cfg(feature = "ws")] +use tokio::sync::mpsc::Sender; + +use crate::error::Result; +use crate::BpxClient; + +#[doc(hidden)] +pub const API_RFQ: &str = "/api/v1/rfq"; +#[doc(hidden)] +pub const API_RFQ_QUOTE: &str = "/api/v1/rfq/quote"; + +const API_RFQ_STREAM: &str = "account.rfqUpdate"; + +impl BpxClient { + pub async fn submit_rfq(&self, payload: RequestForQuotePayload) -> Result { + let endpoint = format!("{}{}", self.base_url, API_RFQ); + let res = self.post(endpoint, payload).await?; + res.json().await.map_err(Into::into) + } + + pub async fn submit_quote(&self, payload: QuotePayload) -> Result { + let endpoint = format!("{}{}", self.base_url, API_RFQ_QUOTE); + let res = self.post(endpoint, payload).await?; + res.json().await.map_err(Into::into) + } + + #[cfg(feature = "ws")] + pub async fn subscribe_to_rfqs(&self, tx: Sender) { + self.subscribe(API_RFQ_STREAM, tx).await; + } +} diff --git a/rust/client/src/trades.rs b/rust/client/src/routes/trades.rs similarity index 64% rename from rust/client/src/trades.rs rename to rust/client/src/routes/trades.rs index a0b833a..b72c600 100644 --- a/rust/client/src/trades.rs +++ b/rust/client/src/routes/trades.rs @@ -3,9 +3,13 @@ use bpx_api_types::trade::Trade; use crate::error::Result; use crate::BpxClient; +const API_TRADES: &str = "/api/v1/trades"; +const API_TRADES_HISTORY: &str = "/api/v1/trades/history"; + impl BpxClient { + /// Fetches the most recent trades for a given symbol, with an optional limit. pub async fn get_recent_trades(&self, symbol: &str, limit: Option) -> Result> { - let mut url = format!("{}/api/v1/trades?symbol={}", self.base_url, symbol); + let mut url = format!("{}{}?symbol={}", self.base_url, API_TRADES, symbol); if let Some(limit) = limit { url.push_str(&format!("&limit={}", limit)); } @@ -13,13 +17,14 @@ impl BpxClient { res.json().await.map_err(Into::into) } + /// Fetches historical trades for a given symbol, with optional limit and offset. pub async fn get_historical_trades( &self, symbol: &str, limit: Option, offset: Option, ) -> Result> { - let mut url = format!("{}/api/v1/trades/history?symbol={}", self.base_url, symbol); + let mut url = format!("{}{}?symbol={}", self.base_url, API_TRADES_HISTORY, symbol); for (k, v) in [("limit", limit), ("offset", offset)] { if let Some(v) = v { url.push_str(&format!("&{}={}", k, v)); diff --git a/rust/client/src/routes/user.rs b/rust/client/src/routes/user.rs new file mode 100644 index 0000000..660f353 --- /dev/null +++ b/rust/client/src/routes/user.rs @@ -0,0 +1,20 @@ +use bpx_api_types::user::{RequestTwoFactorPayload, RequestTwoFactorResponse}; + +use crate::{error::Result, BpxClient}; + +#[doc(hidden)] +pub const API_USER_2FA: &str = "/wapi/v1/user/2fa"; + +impl BpxClient { + /// Requests a two-factor authentication token. + /// + /// Sends a request to initiate the two-factor authentication process + /// with the provided payload and returns the response. + pub async fn request_two_factor(&self, payload: RequestTwoFactorPayload) -> Result { + let endpoint = format!("{}{}", self.base_url, API_USER_2FA); + let res = self.post(endpoint, payload).await?; + + let data: RequestTwoFactorResponse = res.json().await?; + Ok(data) + } +} diff --git a/rust/client/src/user.rs b/rust/client/src/user.rs deleted file mode 100644 index 2930175..0000000 --- a/rust/client/src/user.rs +++ /dev/null @@ -1,16 +0,0 @@ -use bpx_api_types::user::{RequestTwoFactorPayload, RequestTwoFactorResponse}; - -use crate::{error::Result, BpxClient}; - -impl BpxClient { - pub async fn request_two_factor( - &self, - payload: RequestTwoFactorPayload, - ) -> Result { - let endpoint = format!("{}/wapi/v1/user/2fa", self.base_url); - let res = self.post(endpoint, payload).await?; - - let data: RequestTwoFactorResponse = res.json().await?; - Ok(data) - } -} diff --git a/rust/client/src/ws/mod.rs b/rust/client/src/ws/mod.rs new file mode 100644 index 0000000..d725ba7 --- /dev/null +++ b/rust/client/src/ws/mod.rs @@ -0,0 +1,62 @@ +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use ed25519_dalek::Signer; +use futures_util::{SinkExt, StreamExt}; +use serde::de::DeserializeOwned; +use serde_json::{json, Value}; +use tokio::sync::mpsc::Sender; +use tokio_tungstenite::connect_async; +use tokio_tungstenite::tungstenite::protocol::Message; + +use crate::{now_millis, BpxClient, BACKPACK_WS_URL, DEFAULT_WINDOW}; + +impl BpxClient { + /// Subscribes to a private WebSocket stream and sends messages of type `T` through a transmitter channel. + pub async fn subscribe(&self, stream: &str, tx: Sender) + where + T: DeserializeOwned + Send + 'static, + { + let timestamp = now_millis(); + let window = DEFAULT_WINDOW; + let message = format!("instruction=subscribe×tamp={}&window={}", timestamp, window); + + let verifying_key = STANDARD.encode(self.verifier.to_bytes()); + let signature = STANDARD.encode(self.signer.sign(message.as_bytes()).to_bytes()); + + let subscribe_message = json!({ + "method": "SUBSCRIBE", + "params": [stream.to_string()], + "signature": [verifying_key, signature, timestamp.to_string(), window.to_string()], + }); + + let ws_url = self.ws_url.as_deref().unwrap_or(BACKPACK_WS_URL); + let (mut ws_stream, _) = connect_async(ws_url).await.expect("Error connecting to WebSocket"); + ws_stream + .send(Message::Text(subscribe_message.to_string())) + .await + .expect("Error subscribing to WebSocket"); + + println!("Subscribed to {stream} stream..."); + + while let Some(message) = ws_stream.next().await { + match message { + Ok(msg) => match msg { + Message::Text(text) => { + if let Ok(value) = serde_json::from_str::(&text) { + if let Some(payload) = value.get("data") { + if let Ok(data) = serde_json::from_value::(payload.clone()) { + if tx.send(data).await.is_err() { + eprintln!("Failed to send message through the channel"); + } + } + } + } + } + Message::Close(_) => break, + _ => {} + }, + Err(error) => eprintln!("WebSocket error: {}", error), + } + } + } +} diff --git a/rust/examples/Cargo.toml b/rust/examples/Cargo.toml new file mode 100644 index 0000000..92ba432 --- /dev/null +++ b/rust/examples/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bpx-api-examples" +version = "0.1.0" +license = "Apache-2.0" +edition = "2021" +description = "Backpack Exchange API examples" +repository = "https://github.com/backpack-exchange/bpx-api-client" + +[dependencies] +bpx-api-client = { path = "../client" } +bpx-api-types = { path = "../types" } + +anyhow = { workspace = true } +tokio = { workspace = true } +tokio-tungstenite = { workspace = true } + +[[bin]] +name = "rfq" +path = "src/bin/rfq.rs" diff --git a/rust/examples/README.md b/rust/examples/README.md new file mode 100644 index 0000000..a2fbbb9 --- /dev/null +++ b/rust/examples/README.md @@ -0,0 +1,15 @@ +# Backpack Exchange API Examples + +This repository demonstrates how to interact with Backpack Exchange’s APIs. + +## Configuration + +Set the following environment variables: + +- `BASE_URL` (optional, default: `https://api.backpack.exchange`) +- `WS_URL` (optional, default: `wss://ws.backpack.exchange`) +- `SECRET` (required): Your API secret key. + +## Running the Examples + +- To subscribe to the RFQ private stream, run: `just rfq` diff --git a/rust/examples/justfile b/rust/examples/justfile new file mode 100644 index 0000000..9303bb3 --- /dev/null +++ b/rust/examples/justfile @@ -0,0 +1,8 @@ +# Print available options +default: + @just --list --unsorted + +# Run the example subscribing to the RFQ WebSocket +rfq: + SECRET=... \ + cargo -q run --bin rfq diff --git a/rust/examples/src/bin/rfq.rs b/rust/examples/src/bin/rfq.rs new file mode 100644 index 0000000..a0c9f94 --- /dev/null +++ b/rust/examples/src/bin/rfq.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use bpx_api_client::{BpxClient, BACKPACK_API_BASE_URL, BACKPACK_WS_URL}; +use bpx_api_types::rfq::RequestForQuote; +use std::env; +use tokio::sync::mpsc; + +#[tokio::main] +async fn main() -> Result<()> { + let base_url = env::var("BASE_URL").unwrap_or_else(|_| BACKPACK_API_BASE_URL.to_string()); + let ws_url = env::var("WS_URL").unwrap_or_else(|_| BACKPACK_WS_URL.to_string()); + let secret = env::var("SECRET").expect("Missing SECRET environment variable"); + + let client = BpxClient::init_with_ws(base_url, ws_url, &secret, None)?; + + let (tx, mut rx) = mpsc::channel::(100); + tokio::spawn(async move { + while let Some(rfq) = rx.recv().await { + println!("Received RFQ: {:?}", rfq); + } + }); + + client.subscribe_to_rfqs(tx).await; + + Ok(()) +} diff --git a/rust/justfile b/rust/justfile new file mode 100644 index 0000000..acf7312 --- /dev/null +++ b/rust/justfile @@ -0,0 +1,26 @@ +# print options +default: + @just --list --unsorted + +# check formatting, clippy and taplo +check: + cargo check + cargo +nightly fmt --all -- --check + cargo clippy --all-targets --all-features + +# automatically fix check warnings +fix: + cargo +nightly fmt --all + cargo clippy --allow-dirty --allow-staged --fix + +# execute the tests +test: + cargo test + +# build project (debug profile) +build: + cargo build --all-targets + +# build project (release profile) +release: + cargo build --all-targets --release diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/rust/rustfmt.toml b/rust/rustfmt.toml new file mode 100644 index 0000000..db9af78 --- /dev/null +++ b/rust/rustfmt.toml @@ -0,0 +1,3 @@ +edition = "2021" +max_width = 120 +reorder_imports = true diff --git a/rust/types/Cargo.toml b/rust/types/Cargo.toml index 52c4952..9b438ac 100644 --- a/rust/types/Cargo.toml +++ b/rust/types/Cargo.toml @@ -1,11 +1,10 @@ [package] name = "bpx-api-types" -authors = ["Backpack "] +version = "0.5.0" license = "Apache-2.0" -version = "0.2.0" edition = "2021" -description = "Backpack Exchange types" -repository = "https://github.com/backpack-exchange/bpx-api-client/tree/master/rust/types" +description = "Backpack Exchange API types" +repository = "https://github.com/backpack-exchange/bpx-api-client" [dependencies] chrono = { workspace = true } diff --git a/rust/types/src/lib.rs b/rust/types/src/lib.rs index 9c2579b..d5d259c 100644 --- a/rust/types/src/lib.rs +++ b/rust/types/src/lib.rs @@ -1,26 +1,19 @@ +//! Types module for the Backpack Exchange API. +//! +//! This module contains various types used across the Backpack Exchange API, +//! including enums and structs for capital, markets, orders, trades, and user data. + use serde::{Deserialize, Serialize}; use strum::{Display, EnumIter, EnumString}; pub mod capital; pub mod markets; pub mod order; +pub mod rfq; pub mod trade; pub mod user; -#[derive( - Debug, - Display, - Clone, - Copy, - Serialize, - Deserialize, - Default, - EnumString, - PartialEq, - Eq, - Hash, - EnumIter, -)] +#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash, EnumIter)] #[strum(serialize_all = "PascalCase")] #[serde(rename_all = "PascalCase")] pub enum Blockchain { diff --git a/rust/types/src/order.rs b/rust/types/src/order.rs index 7b1d50e..70ba35e 100644 --- a/rust/types/src/order.rs +++ b/rust/types/src/order.rs @@ -39,9 +39,7 @@ pub struct LimitOrder { pub created_at: i64, } -#[derive( - Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash, -)] +#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)] #[strum(serialize_all = "PascalCase")] #[serde(rename_all = "PascalCase")] pub enum OrderType { @@ -57,9 +55,7 @@ pub enum Order { Limit(LimitOrder), } -#[derive( - Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash, -)] +#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)] #[strum(serialize_all = "UPPERCASE")] #[serde(rename_all = "UPPERCASE")] pub enum TimeInForce { @@ -69,9 +65,7 @@ pub enum TimeInForce { FOK, } -#[derive( - Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash, -)] +#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)] #[strum(serialize_all = "PascalCase")] #[serde(rename_all = "PascalCase")] pub enum SelfTradePrevention { @@ -82,9 +76,7 @@ pub enum SelfTradePrevention { Allow, } -#[derive( - Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash, -)] +#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)] #[strum(serialize_all = "PascalCase")] #[serde(rename_all = "PascalCase")] pub enum OrderStatus { @@ -97,9 +89,7 @@ pub enum OrderStatus { Triggered, } -#[derive( - Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash, -)] +#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)] #[strum(serialize_all = "PascalCase")] #[serde(rename_all = "PascalCase")] pub enum Side { diff --git a/rust/types/src/rfq.rs b/rust/types/src/rfq.rs new file mode 100644 index 0000000..543cbb7 --- /dev/null +++ b/rust/types/src/rfq.rs @@ -0,0 +1,52 @@ +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; + +use crate::order::OrderStatus; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestForQuotePayload { + pub quantity: Decimal, + pub asset_in: String, + pub asset_out: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct QuotePayload { + rfq_id: u64, + bid_price: Decimal, + ask_price: Decimal, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestForQuote { + #[serde(rename = "R")] + pub rfq_id: u64, + #[serde(rename = "T")] + pub submission_time: i64, + #[serde(rename = "w")] + pub expiry_time: i64, + #[serde(rename = "s")] + pub symbol: String, + #[serde(rename = "q")] + pub quantity: Decimal, + #[serde(rename = "X")] + pub status: OrderStatus, + #[serde(rename = "e")] + pub event_type: String, + #[serde(rename = "W")] + pub received_time: i64, + #[serde(rename = "E")] + pub event_time: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Quote { + pub rfq_id: String, + pub quote_id: String, + pub client_id: Option, + pub status: OrderStatus, +} From 4b519beed76111695a497a9855f5420d81bd932f Mon Sep 17 00:00:00 2001 From: Francisco Lopez Date: Thu, 14 Nov 2024 10:54:51 +0100 Subject: [PATCH 2/7] better docs and added REST example --- rust/README.md | 78 ++++++++++++++++++++++++++++++++- rust/client/Cargo.toml | 2 +- rust/client/src/lib.rs | 4 +- rust/examples/Cargo.toml | 6 ++- rust/examples/README.md | 1 + rust/examples/justfile | 5 +++ rust/examples/src/bin/orders.rs | 15 +++++++ 7 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 rust/examples/src/bin/orders.rs diff --git a/rust/README.md b/rust/README.md index 7d6ec67..429facd 100644 --- a/rust/README.md +++ b/rust/README.md @@ -16,13 +16,84 @@ This crate provides both REST and WebSocket APIs for interacting with the Backpack Exchange: -- **REST API**: Includes public and private (authenticated) endpoints. -- **WebSocket API**: Offers public and private streams. +## Features + +- **REST API**: Access public and private (authenticated) endpoints. +- **WebSocket API**: Subscribe to private streams for real-time updates (requires `ws` feature). The official API documentation is available at [https://docs.backpack.exchange/](https://docs.backpack.exchange/). +## Installation + +Add this crate to your `Cargo.toml`: + +```toml +[dependencies] +bpx_api_client = "x.y.z" # Replace with the latest version +``` + +To enable WebSocket support: + +```toml +[dependencies] +bpx_api_client = { version = "x.y.z", features = ["ws"] } +``` + ## Usage +REST API example: + +```rust +use bpx_api_client::{BpxClient, BACKPACK_API_BASE_URL}; +use std::env; + +#[tokio::main] +async fn main() { + let base_url = env::var("BASE_URL").unwrap_or_else(|_| BACKPACK_API_BASE_URL.to_string()); + let secret = env::var("SECRET").expect("Missing SECRET environment variable"); + + let client = BpxClient::init(base_url, secret, headers) + .expect("Failed to initialize Backpack API client"); + + match client.get_open_orders(Some("SOL_USDC")).await { + Ok(orders) => println!("Open Orders: {:?}", orders), + Err(err) => eprintln!("Error: {:?}", err), + } +} +``` + +WebSocket API example: + +```rust +use anyhow::Result; +use bpx_api_client::{BpxClient, BACKPACK_API_BASE_URL, BACKPACK_WS_URL}; +use bpx_api_types::rfq::RequestForQuote; +use std::env; +use tokio::sync::mpsc; + +#[tokio::main] +async fn main() -> Result<()> { + let base_url = env::var("BASE_URL").unwrap_or_else(|_| BACKPACK_API_BASE_URL.to_string()); + let ws_url = env::var("WS_URL").unwrap_or_else(|_| BACKPACK_WS_URL.to_string()); + let secret = env::var("SECRET").expect("Missing SECRET environment variable"); + + let client = BpxClient::init_with_ws(base_url, ws_url, &secret, None)?; + + let (tx, mut rx) = mpsc::channel::(100); + tokio::spawn(async move { + while let Some(rfq) = rx.recv().await { + println!("Received RFQ: {:?}", rfq); + } + }); + + client.subscribe_to_rfqs(tx).await; + + Ok(()) +} +``` + +## Development + This project uses [Just](https://github.com/casey/just) to manage various build and development tasks. To see the available commands, run: @@ -30,3 +101,6 @@ To see the available commands, run: ```shell just ``` + +## Crate Usage + diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index 7480581..fa51098 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -22,5 +22,5 @@ tokio-tungstenite = { workspace = true, optional = true } futures-util = { workspace = true, optional = true } [features] -default = ["ws"] +default = [] ws = ["tokio", "tokio-tungstenite", "futures-util"] diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index 9786167..adb706f 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -11,11 +11,11 @@ //! //! ## Example //! ```no_run -//! use bpx_api_client::BpxClient; +//! use bpx_api_client::{BACKPACK_API_BASE_URL, BpxClient}; //! //! #[tokio::main] //! async fn main() { -//! let base_url = "https://api.backpack.exchange/".to_string(); +//! let base_url = BACKPACK_API_BASE_URL.to_string(); //! let secret = "your_api_secret_here"; //! let headers = None; //! diff --git a/rust/examples/Cargo.toml b/rust/examples/Cargo.toml index 92ba432..8f53b48 100644 --- a/rust/examples/Cargo.toml +++ b/rust/examples/Cargo.toml @@ -7,13 +7,17 @@ description = "Backpack Exchange API examples" repository = "https://github.com/backpack-exchange/bpx-api-client" [dependencies] -bpx-api-client = { path = "../client" } +bpx-api-client = { path = "../client", features = ["ws"] } bpx-api-types = { path = "../types" } anyhow = { workspace = true } tokio = { workspace = true } tokio-tungstenite = { workspace = true } +[[bin]] +name = "orders" +path = "src/bin/orders.rs" + [[bin]] name = "rfq" path = "src/bin/rfq.rs" diff --git a/rust/examples/README.md b/rust/examples/README.md index a2fbbb9..f7054ee 100644 --- a/rust/examples/README.md +++ b/rust/examples/README.md @@ -12,4 +12,5 @@ Set the following environment variables: ## Running the Examples +- To retrieve all the open orders, run: `just orders` - To subscribe to the RFQ private stream, run: `just rfq` diff --git a/rust/examples/justfile b/rust/examples/justfile index 9303bb3..9f8fe45 100644 --- a/rust/examples/justfile +++ b/rust/examples/justfile @@ -2,6 +2,11 @@ default: @just --list --unsorted +# Run the example retrieving all the open orders +orders: + SECRET=... \ + cargo -q run --bin orders + # Run the example subscribing to the RFQ WebSocket rfq: SECRET=... \ diff --git a/rust/examples/src/bin/orders.rs b/rust/examples/src/bin/orders.rs new file mode 100644 index 0000000..c329553 --- /dev/null +++ b/rust/examples/src/bin/orders.rs @@ -0,0 +1,15 @@ +use bpx_api_client::{BpxClient, BACKPACK_API_BASE_URL}; +use std::env; + +#[tokio::main] +async fn main() { + let base_url = env::var("BASE_URL").unwrap_or_else(|_| BACKPACK_API_BASE_URL.to_string()); + let secret = env::var("SECRET").expect("Missing SECRET environment variable"); + + let client = BpxClient::init(base_url, &secret, None).expect("Failed to initialize Backpack API client"); + + match client.get_open_orders(Some("SOL_USDC")).await { + Ok(orders) => println!("Open Orders: {:?}", orders), + Err(err) => eprintln!("Error: {:?}", err), + } +} From 62ff9ff1fc89e73bcbf269abac24f10f6f35714e Mon Sep 17 00:00:00 2001 From: Francisco Lopez Date: Thu, 14 Nov 2024 13:33:30 +0100 Subject: [PATCH 3/7] deserialize different event types --- rust/README.md | 2 +- rust/types/src/rfq.rs | 84 ++++++++++++++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/rust/README.md b/rust/README.md index 429facd..60dd023 100644 --- a/rust/README.md +++ b/rust/README.md @@ -52,7 +52,7 @@ async fn main() { let base_url = env::var("BASE_URL").unwrap_or_else(|_| BACKPACK_API_BASE_URL.to_string()); let secret = env::var("SECRET").expect("Missing SECRET environment variable"); - let client = BpxClient::init(base_url, secret, headers) + let client = BpxClient::init(base_url, secret, None) .expect("Failed to initialize Backpack API client"); match client.get_open_orders(Some("SOL_USDC")).await { diff --git a/rust/types/src/rfq.rs b/rust/types/src/rfq.rs index 543cbb7..381c005 100644 --- a/rust/types/src/rfq.rs +++ b/rust/types/src/rfq.rs @@ -20,26 +20,70 @@ pub struct QuotePayload { } #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RequestForQuote { - #[serde(rename = "R")] - pub rfq_id: u64, - #[serde(rename = "T")] - pub submission_time: i64, - #[serde(rename = "w")] - pub expiry_time: i64, - #[serde(rename = "s")] - pub symbol: String, - #[serde(rename = "q")] - pub quantity: Decimal, - #[serde(rename = "X")] - pub status: OrderStatus, - #[serde(rename = "e")] - pub event_type: String, - #[serde(rename = "W")] - pub received_time: i64, - #[serde(rename = "E")] - pub event_time: i64, +pub struct RequestForQuoteStream { + pub stream: String, + pub data: RequestForQuote, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "e", rename_all = "camelCase")] // Discriminates based on "e" field +pub enum RequestForQuote { + RfqActive { + #[serde(rename = "R")] + rfq_id: u64, + #[serde(rename = "T")] + submission_time: i64, + #[serde(rename = "w")] + expiry_time: i64, + #[serde(rename = "s")] + symbol: String, + #[serde(rename = "q")] + quantity: Decimal, + #[serde(rename = "X")] + status: OrderStatus, + #[serde(rename = "W")] + received_time: i64, + #[serde(rename = "E")] + event_time: i64, + }, + QuoteAccepted { + #[serde(rename = "R")] + rfq_id: u64, + #[serde(rename = "Q")] + quote_id: u64, + #[serde(rename = "T")] + submission_time: i64, + #[serde(rename = "X")] + status: OrderStatus, + #[serde(rename = "E")] + event_time: i64, + }, + QuoteCancelled { + #[serde(rename = "R")] + rfq_id: u64, + #[serde(rename = "Q")] + quote_id: u64, + #[serde(rename = "X")] + status: OrderStatus, + #[serde(rename = "E")] + event_time: i64, + }, + RfqFilled { + #[serde(rename = "R")] + rfq_id: u64, + #[serde(rename = "Q")] + quote_id: u64, + #[serde(rename = "T")] + submission_time: i64, + #[serde(rename = "X")] + status: OrderStatus, + #[serde(rename = "E")] + event_time: i64, + #[serde(rename = "S")] + side: String, + #[serde(rename = "p")] + price: Decimal, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] From c16e6ff921e0db7712f3d317e89191b32df9ae01 Mon Sep 17 00:00:00 2001 From: Francisco Lopez Date: Thu, 14 Nov 2024 13:53:01 +0100 Subject: [PATCH 4/7] use enum variants to decode WS messages properly --- rust/types/src/rfq.rs | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/rust/types/src/rfq.rs b/rust/types/src/rfq.rs index 381c005..aab5e81 100644 --- a/rust/types/src/rfq.rs +++ b/rust/types/src/rfq.rs @@ -1,7 +1,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use crate::order::OrderStatus; +use crate::order::{OrderStatus, Side}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -29,34 +29,32 @@ pub struct RequestForQuoteStream { #[serde(tag = "e", rename_all = "camelCase")] // Discriminates based on "e" field pub enum RequestForQuote { RfqActive { + #[serde(rename = "E")] + event_time: i64, #[serde(rename = "R")] rfq_id: u64, - #[serde(rename = "T")] - submission_time: i64, - #[serde(rename = "w")] - expiry_time: i64, #[serde(rename = "s")] symbol: String, #[serde(rename = "q")] quantity: Decimal, - #[serde(rename = "X")] - status: OrderStatus, #[serde(rename = "W")] - received_time: i64, - #[serde(rename = "E")] - event_time: i64, + expiry_time: i64, + #[serde(rename = "X")] + order_status: OrderStatus, + #[serde(rename = "T")] + timestamp: i64, }, QuoteAccepted { + #[serde(rename = "E")] + event_time: i64, #[serde(rename = "R")] rfq_id: u64, #[serde(rename = "Q")] quote_id: u64, - #[serde(rename = "T")] - submission_time: i64, #[serde(rename = "X")] - status: OrderStatus, - #[serde(rename = "E")] - event_time: i64, + order_status: OrderStatus, + #[serde(rename = "T")] + timestamp: i64, }, QuoteCancelled { #[serde(rename = "R")] @@ -69,20 +67,20 @@ pub enum RequestForQuote { event_time: i64, }, RfqFilled { + #[serde(rename = "E")] + event_time: i64, #[serde(rename = "R")] rfq_id: u64, #[serde(rename = "Q")] quote_id: u64, - #[serde(rename = "T")] - submission_time: i64, - #[serde(rename = "X")] - status: OrderStatus, - #[serde(rename = "E")] - event_time: i64, #[serde(rename = "S")] - side: String, + side: Side, #[serde(rename = "p")] price: Decimal, + #[serde(rename = "X")] + order_status: OrderStatus, + #[serde(rename = "T")] + timestamp: i64, }, } From 40131215171e36a9db7e1a582f33e60f5d614ded Mon Sep 17 00:00:00 2001 From: Francisco Lopez Date: Thu, 14 Nov 2024 14:42:09 +0100 Subject: [PATCH 5/7] make quote fields public --- README.md | 2 +- rust/types/src/rfq.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e90bde6..0cfd9ba 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This repository hosts the Backpack Exchange API clients. Currently, the client i ## Contributing We welcome contributions from the community! -Feel free to open bug reports, suggest new features, or submit pull requests to improve the matching engine and related components. +Feel free to open bug reports, suggest new features, or submit pull requests to improve the client and related components. ## License diff --git a/rust/types/src/rfq.rs b/rust/types/src/rfq.rs index aab5e81..cce6642 100644 --- a/rust/types/src/rfq.rs +++ b/rust/types/src/rfq.rs @@ -14,9 +14,9 @@ pub struct RequestForQuotePayload { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct QuotePayload { - rfq_id: u64, - bid_price: Decimal, - ask_price: Decimal, + pub rfq_id: u64, + pub bid_price: Decimal, + pub ask_price: Decimal, } #[derive(Debug, Clone, Serialize, Deserialize)] From 0130a66f135a5f34f6388be44d54b156c9b39975 Mon Sep 17 00:00:00 2001 From: Francisco Lopez Date: Thu, 14 Nov 2024 15:59:52 +0100 Subject: [PATCH 6/7] use tracing --- rust/README.md | 2 +- rust/client/src/lib.rs | 2 +- rust/client/src/ws/mod.rs | 6 +++--- rust/examples/Cargo.toml | 1 + rust/examples/src/bin/orders.rs | 2 +- rust/types/src/rfq.rs | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/rust/README.md b/rust/README.md index 60dd023..9235791 100644 --- a/rust/README.md +++ b/rust/README.md @@ -57,7 +57,7 @@ async fn main() { match client.get_open_orders(Some("SOL_USDC")).await { Ok(orders) => println!("Open Orders: {:?}", orders), - Err(err) => eprintln!("Error: {:?}", err), + Err(err) => tracing::error!("Error: {:?}", err), } } ``` diff --git a/rust/client/src/lib.rs b/rust/client/src/lib.rs index adb706f..5be3e29 100644 --- a/rust/client/src/lib.rs +++ b/rust/client/src/lib.rs @@ -24,7 +24,7 @@ //! //! match client.get_open_orders(Some("SOL_USDC")).await { //! Ok(orders) => println!("Open Orders: {:?}", orders), -//! Err(err) => eprintln!("Error: {:?}", err), +//! Err(err) => tracing::error!("Error: {:?}", err), //! } //! } //! ``` diff --git a/rust/client/src/ws/mod.rs b/rust/client/src/ws/mod.rs index d725ba7..7ec58f2 100644 --- a/rust/client/src/ws/mod.rs +++ b/rust/client/src/ws/mod.rs @@ -36,7 +36,7 @@ impl BpxClient { .await .expect("Error subscribing to WebSocket"); - println!("Subscribed to {stream} stream..."); + tracing::debug!("Subscribed to {stream} stream..."); while let Some(message) = ws_stream.next().await { match message { @@ -46,7 +46,7 @@ impl BpxClient { if let Some(payload) = value.get("data") { if let Ok(data) = serde_json::from_value::(payload.clone()) { if tx.send(data).await.is_err() { - eprintln!("Failed to send message through the channel"); + tracing::error!("Failed to send message through the channel"); } } } @@ -55,7 +55,7 @@ impl BpxClient { Message::Close(_) => break, _ => {} }, - Err(error) => eprintln!("WebSocket error: {}", error), + Err(error) => tracing::error!("WebSocket error: {}", error), } } } diff --git a/rust/examples/Cargo.toml b/rust/examples/Cargo.toml index 8f53b48..3a3e1fc 100644 --- a/rust/examples/Cargo.toml +++ b/rust/examples/Cargo.toml @@ -13,6 +13,7 @@ bpx-api-types = { path = "../types" } anyhow = { workspace = true } tokio = { workspace = true } tokio-tungstenite = { workspace = true } +tracing = { workspace = true } [[bin]] name = "orders" diff --git a/rust/examples/src/bin/orders.rs b/rust/examples/src/bin/orders.rs index c329553..79dce0f 100644 --- a/rust/examples/src/bin/orders.rs +++ b/rust/examples/src/bin/orders.rs @@ -10,6 +10,6 @@ async fn main() { match client.get_open_orders(Some("SOL_USDC")).await { Ok(orders) => println!("Open Orders: {:?}", orders), - Err(err) => eprintln!("Error: {:?}", err), + Err(err) => tracing::error!("Error: {:?}", err), } } diff --git a/rust/types/src/rfq.rs b/rust/types/src/rfq.rs index cce6642..7f55209 100644 --- a/rust/types/src/rfq.rs +++ b/rust/types/src/rfq.rs @@ -14,7 +14,7 @@ pub struct RequestForQuotePayload { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct QuotePayload { - pub rfq_id: u64, + pub rfq_id: String, pub bid_price: Decimal, pub ask_price: Decimal, } From 493d204380441fb261fd745c0ee3b296fcc7fbfc Mon Sep 17 00:00:00 2001 From: Francisco Lopez Date: Fri, 15 Nov 2024 07:37:18 +0100 Subject: [PATCH 7/7] apply change from origin --- rust/client/src/routes/markets.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rust/client/src/routes/markets.rs b/rust/client/src/routes/markets.rs index 2e41cd0..e44cce6 100644 --- a/rust/client/src/routes/markets.rs +++ b/rust/client/src/routes/markets.rs @@ -8,6 +8,7 @@ use crate::BpxClient; const API_ASSETS: &str = "/api/v1/assets"; const API_MARKETS: &str = "/api/v1/markets"; const API_TICKER: &str = "/api/v1/ticker"; +const API_TICKERS: &str = "/api/v1/tickers"; const API_DEPTH: &str = "/api/v1/depth"; const API_KLINES: &str = "/api/v1/klines"; @@ -27,12 +28,19 @@ impl BpxClient { } /// Fetches the ticker information for a given symbol. - pub async fn get_ticker(&self, symbol: &str) -> Result> { + pub async fn get_ticker(&self, symbol: &str) -> Result { let url = format!("{}{}&symbol={}", self.base_url, API_TICKER, symbol); let res = self.get(url).await?; res.json().await.map_err(Into::into) } + /// Fetches the ticker information for all symbols. + pub async fn get_tickers(&self) -> Result> { + let url = format!("{}{}", self.base_url, API_TICKERS); + let res = self.get(url).await?; + res.json().await.map_err(Into::into) + } + /// Retrieves the order book depth for a given symbol. pub async fn get_order_book_depth(&self, symbol: &str) -> Result { let url = format!("{}{}&symbol={}", self.base_url, API_DEPTH, symbol);