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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use routes::{
order::{API_ORDER, API_ORDERS},
rfq::{API_RFQ, API_RFQ_QUOTE},
user::API_USER_2FA,
vault::API_VAULT_PENDING_REDEEMS,
vault::{API_VAULT_MINT, API_VAULT_PENDING_REDEEMS, API_VAULT_REDEEM},
};
use serde::Serialize;
use serde_json::Value;
Expand Down Expand Up @@ -181,31 +181,32 @@ impl BpxClient {
pub async fn get<U: IntoUrl>(&self, url: U) -> Result<Response> {
let req = self.build_and_maybe_sign_request::<(), _>(url, Method::GET, None)?;
tracing::debug!(?req, "GET request");
let res = self.client.execute(req).await?;
Self::process_response(res).await
self.execute(req).await
}

/// Sends a POST request with a JSON payload to the specified URL and signs it.
pub async fn post<P: Serialize, U: IntoUrl>(&self, url: U, payload: P) -> Result<Response> {
let req = self.build_and_maybe_sign_request(url, Method::POST, Some(&payload))?;
tracing::debug!(?req, "POST request");
let res = self.client.execute(req).await?;
Self::process_response(res).await
self.execute(req).await
}

/// Sends a DELETE request with a JSON payload to the specified URL and signs it.
pub async fn delete<P: Serialize, U: IntoUrl>(&self, url: U, payload: P) -> Result<Response> {
let req = self.build_and_maybe_sign_request(url, Method::DELETE, Some(&payload))?;
tracing::debug!(?req, "DELETE request");
let res = self.client.execute(req).await?;
Self::process_response(res).await
self.execute(req).await
}

/// Sends a PATCH request with a JSON payload to the specified URL and signs it.
pub async fn patch<P: Serialize, U: IntoUrl>(&self, url: U, payload: P) -> Result<Response> {
let req = self.build_and_maybe_sign_request(url, Method::PATCH, Some(&payload))?;
tracing::debug!(?req, "PATCH request");
let res = self.client.execute(req).await?;
self.execute(req).await
}

pub async fn execute(&self, request: Request) -> Result<Response> {
let res = self.client.execute(request).await?;
Self::process_response(res).await
}

Expand All @@ -219,6 +220,10 @@ impl BpxClient {
pub const fn client(&self) -> &reqwest::Client {
&self.client
}

pub fn base_url(&self) -> &Url {
&self.base_url
}
}

// Private functions.
Expand Down Expand Up @@ -264,6 +269,9 @@ impl BpxClient {
API_ACCOUNT_CONVERT_DUST if method == Method::POST => "convertDust",
API_FILLS_HISTORY if method == Method::GET => "fillHistoryQueryAll",
API_VAULT_PENDING_REDEEMS if method == Method::GET => "vaultPendingRedeemsQuery",
API_VAULT_MINT if method == Method::POST => "vaultMint",
API_VAULT_REDEEM if method == Method::POST => "vaultRedeemRequest",
API_VAULT_REDEEM if method == Method::DELETE => "vaultRedeemCancel",
_ => {
let req = self.client().request(method, url);
if let Some(payload) = payload {
Expand All @@ -274,9 +282,23 @@ impl BpxClient {
}
};

let Some(signing_key) = &self.signing_key else {
return Err(Error::NotAuthenticated);
};
self.build_signed_request(url, method, instruction, payload)
}

/// Builds an authenticated request with signing headers.
///
/// Use this to create signed requests for custom endpoints. The `instruction`
/// must match the Backpack API's expected instruction string for the endpoint.
pub fn build_signed_request<P: Serialize, U: IntoUrl>(
&self,
url: U,
method: Method,
instruction: &str,
payload: Option<&P>,
) -> Result<Request> {
let url = url.into_url()?;

let signing_key = self.signing_key.as_ref().ok_or(Error::NotAuthenticated)?;

let query_params = url
.query_pairs()
Expand Down
54 changes: 53 additions & 1 deletion client/src/routes/vault.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,55 @@
use crate::error::Result;

use bpx_api_types::vault::VaultRedeem;
use bpx_api_types::vault::{
Vault, VaultHistory, VaultHistoryParams, VaultMintRequest, VaultRedeem,
VaultRedeemCancelRequest, VaultRedeemRequest,
};

use crate::BpxClient;

#[doc(hidden)]
pub const API_VAULTS: &str = "/api/v1/vaults";
#[doc(hidden)]
pub const API_VAULT_MINT: &str = "/api/v1/vault/mint";
#[doc(hidden)]
pub const API_VAULT_REDEEM: &str = "/api/v1/vault/redeem";
#[doc(hidden)]
pub const API_VAULT_PENDING_REDEEMS: &str = "/api/v1/vault/redeems/pending";
#[doc(hidden)]
pub const API_VAULTS_HISTORY: &str = "/api/v1/vaults/history";

impl BpxClient {
/// Fetches information about all available vaults on the exchange.
pub async fn get_vaults(&self) -> Result<Vec<Vault>> {
let url = self.base_url.join(API_VAULTS)?;
let res = self.get(url).await?;
res.json().await.map_err(Into::into)
}

/// Mints vault tokens by depositing an asset into a vault.
pub async fn vault_mint(&self, request: VaultMintRequest) -> Result<()> {
let url = self.base_url.join(API_VAULT_MINT)?;
let res = self.post(url, request).await?;
let _ = res.bytes().await?;
Ok(())
}

/// Submits a request to redeem vault tokens for USDC.
pub async fn vault_redeem(&self, request: VaultRedeemRequest) -> Result<()> {
let url = self.base_url.join(API_VAULT_REDEEM)?;
let res = self.post(url, request).await?;
let _ = res.bytes().await?;
Ok(())
}

/// Cancels a pending redeem request for a vault.
pub async fn vault_redeem_cancel(&self, request: VaultRedeemCancelRequest) -> Result<()> {
let url = self.base_url.join(API_VAULT_REDEEM)?;
let res = self.delete(url, request).await?;
let _ = res.bytes().await?;
Ok(())
}

/// Fetches pending redeem requests for a vault.
pub async fn get_vault_pending_redeems(&self, vault_id: u32) -> Result<Vec<VaultRedeem>> {
let mut url = self.base_url.join(API_VAULT_PENDING_REDEEMS)?;
Expand All @@ -16,4 +58,14 @@ impl BpxClient {
let res = self.get(url).await?;
res.json().await.map_err(Into::into)
}

/// Fetches historical vault data (NAV, equity, circulating supply).
pub async fn get_vault_history(&self, params: VaultHistoryParams) -> Result<Vec<VaultHistory>> {
let query_string = serde_qs::to_string(&params)
.map_err(|e| crate::error::Error::UrlParseError(e.to_string().into_boxed_str()))?;
let mut url = self.base_url.join(API_VAULTS_HISTORY)?;
url.set_query(Some(&query_string));
let res = self.get(url).await?;
res.json().await.map_err(Into::into)
}
}
9 changes: 9 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ bpx-api-types = { path = "../types", version = "0.17.4" }
clap = { workspace = true, features = ["derive"] }
dotenv = { workspace = true }
rust_decimal = { workspace = true, features = ["serde"] }
rust_decimal_macros = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tokio-tungstenite = { workspace = true }
Expand All @@ -40,3 +41,11 @@ path = "src/bin/orders.rs"
[[bin]]
name = "rfq"
path = "src/bin/rfq.rs"

[[bin]]
name = "vault"
path = "src/bin/vault.rs"

[[bin]]
name = "vault_operations"
path = "src/bin/vault_operations.rs"
38 changes: 38 additions & 0 deletions examples/src/bin/vault.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use bpx_api_client::{
BACKPACK_API_BASE_URL, BpxClient,
types::vault::{VaultHistoryInterval, VaultHistoryParams},
};
use std::env;

#[tokio::main]
async fn main() {
dotenv::dotenv().ok();
let base_url = env::var("BASE_URL").unwrap_or_else(|_| BACKPACK_API_BASE_URL.to_string());

// Public endpoints (get_vaults, get_vault_history) work without authentication
let client = BpxClient::builder()
.base_url(base_url)
.build()
.expect("Failed to initialize Backpack API client");

match client.get_vaults().await {
Ok(vaults) => {
let vaults_json = serde_json::to_string_pretty(&vaults).unwrap();
println!("Vaults:\n{vaults_json}");
}
Err(err) => println!("Error fetching vaults: {err:?}"),
}

let params = VaultHistoryParams {
interval: VaultHistoryInterval::OneDay,
vault_id: None,
};

match client.get_vault_history(params).await {
Ok(history) => {
let history_json = serde_json::to_string_pretty(&history).unwrap();
println!("\nVault history (1d interval):\n{history_json}");
}
Err(err) => println!("Error fetching vault history: {err:?}"),
}
}
57 changes: 57 additions & 0 deletions examples/src/bin/vault_operations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Example of authenticated vault operations (mint, redeem, cancel).
//!
//! Requires SECRET environment variable. These operations will fail without
//! sufficient balance and proper vault setup.

use bpx_api_client::{
BACKPACK_API_BASE_URL, BpxClient,
types::vault::{VaultMintRequest, VaultRedeemCancelRequest, VaultRedeemRequest},
};
use rust_decimal_macros::dec;
use std::env;

#[tokio::main]
async fn main() {
dotenv::dotenv().ok();
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::builder()
.base_url(base_url)
.secret(&secret)
.build()
.expect("Failed to initialize Backpack API client");

// Example: mint vault tokens (deposit USDC, receive vault tokens)
let mint_request = VaultMintRequest {
vault_id: 1,
symbol: "USDC".to_string(),
quantity: dec!(50),
auto_borrow: Some(false),
auto_lend_redeem: Some(false),
};

match client.vault_mint(mint_request).await {
Ok(()) => println!("Vault mint successful"),
Err(err) => println!("Vault mint error: {err:?}"),
}

// // Example: request vault redeem (redeem vault tokens for USDC)
let redeem_request = VaultRedeemRequest {
vault_id: 1,
vault_token_quantity: Some(dec!(20)),
};

match client.vault_redeem(redeem_request).await {
Ok(()) => println!("Vault redeem request successful"),
Err(err) => println!("Vault redeem error: {err:?}"),
}

// // Example: cancel pending vault redeem
let cancel_request = VaultRedeemCancelRequest { vault_id: 1 };

match client.vault_redeem_cancel(cancel_request).await {
Ok(()) => println!("Vault redeem cancel successful"),
Err(err) => println!("Vault redeem cancel error: {err:?}"),
}
}
101 changes: 101 additions & 0 deletions types/src/vault.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,107 @@
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

/// Public vault information.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Vault {
/// Unique identifier for the vault.
pub id: u32,
/// The asset that represents shares in this vault.
pub vault_token: String,
/// The symbol used for minting and redeeming vault tokens.
pub symbol: String,
/// Whether the vault is currently accepting mints.
pub mints_enabled: bool,
/// Whether the vault is currently allowing redeems.
pub redeems_enabled: bool,
/// Minimum quantity required to mint vault tokens.
pub min_mint_quantity: Decimal,
/// Minimum vault token amount required to redeem.
pub min_redeem_tokens: Decimal,
/// Minimum delay (in milliseconds) between redeem request and execution.
pub redeem_delay_ms: i64,
/// Step size for vault token quantities.
pub token_step_size: Decimal,
}

/// Request payload for minting vault tokens.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VaultMintRequest {
/// The vault ID to mint tokens from.
pub vault_id: u32,
/// The symbol of the asset to deposit.
pub symbol: String,
/// Amount to deposit.
pub quantity: Decimal,
/// Whether to allow auto borrowing when depositing into vault.
pub auto_borrow: Option<bool>,
/// Whether to allow auto redeem lent assets when depositing into vault.
pub auto_lend_redeem: Option<bool>,
}

/// Request payload for redeeming vault tokens.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VaultRedeemRequest {
/// The vault ID to redeem from.
pub vault_id: u32,
/// Amount of vault tokens to deposit to redeem USDC.
/// If not specified, uses all available vault tokens for the redemptions.
pub vault_token_quantity: Option<Decimal>,
}

/// Request payload for canceling a vault redeem request.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VaultRedeemCancelRequest {
/// The vault ID to cancel the redeem request for.
pub vault_id: u32,
}

/// Historical vault data.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VaultHistory {
/// The vault ID.
pub vault_id: u32,
/// Timestamp of the snapshot.
pub timestamp: DateTime<Utc>,
/// Net asset value per token.
pub nav: Option<Decimal>,
/// Total vault equity in USDC.
pub vault_equity: Option<Decimal>,
/// Total circulating vault tokens.
pub token_circulating_supply: Option<Decimal>,
}

/// Time interval for vault history data.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VaultHistoryInterval {
#[serde(rename = "1d")]
OneDay,
#[serde(rename = "1w")]
OneWeek,
#[serde(rename = "1month")]
OneMonth,
#[serde(rename = "1year")]
OneYear,
}

/// Parameters for fetching vault history.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VaultHistoryParams {
/// Time interval for historical data.
pub interval: VaultHistoryInterval,
/// Optional vault ID to filter by.
#[serde(skip_serializing_if = "Option::is_none")]
pub vault_id: Option<u32>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VaultRedeemStatus {
Requested,
Expand Down
Loading