use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use strum::{Display, EnumString};
use crate::margin::MarginFunction;
#[derive(
Debug, Display, Clone, EnumString, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr,
)]
#[non_exhaustive]
pub enum BorrowLendBookState {
Open,
Closed,
RepayOnly,
#[strum(default)]
Unknown(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BorrowLendMarket {
pub state: BorrowLendBookState,
pub asset_mark_price: Decimal,
pub borrow_interest_rate: Decimal,
pub borrowed_quantity: Decimal,
pub fee: Decimal,
pub lend_interest_rate: Decimal,
pub lent_quantity: Decimal,
pub max_utilization: Decimal,
pub open_borrow_lend_limit: Decimal,
pub optimal_utilization: Decimal,
pub symbol: String,
pub timestamp: DateTime<Utc>,
pub throttle_utilization_threshold: Decimal,
pub throttle_utilization_bound: Decimal,
pub throttle_update_fraction: Decimal,
pub utilization: Decimal,
pub step_size: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BorrowLendHistory {
pub borrow_interest_rate: Decimal,
pub borrowed_quantity: Decimal,
pub lend_interest_rate: Decimal,
pub lent_quantity: Decimal,
pub timestamp: DateTime<Utc>,
pub utilization: Decimal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BorrowLendMarketHistoryInterval {
#[serde(rename = "1d")]
OneDay,
#[serde(rename = "1w")]
OneWeek,
#[serde(rename = "1month")]
OneMonth,
#[serde(rename = "1year")]
OneYear,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BorrowLendMarketHistoryParams {
pub interval: BorrowLendMarketHistoryInterval,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BorrowLendApyRate {
pub symbol: String,
pub borrow_rate: Decimal,
pub lend_rate: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StakingApyRate {
pub symbol: String,
pub dilution_factor: Decimal,
pub staking_rate: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApyRates {
pub borrow_lend: Vec<BorrowLendApyRate>,
pub staking: Vec<StakingApyRate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BorrowLendPosition {
pub cumulative_interest: Decimal,
pub id: String,
pub symbol: String,
pub imf: Decimal,
pub imf_function: MarginFunction,
pub mark_price: Decimal,
pub mmf: Decimal,
pub mmf_function: MarginFunction,
pub net_exposure_notional: Decimal,
pub net_exposure_quantity: Decimal,
pub net_quantity: Decimal,
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_support::assert_wire;
use rust_decimal_macros::dec;
#[test]
fn borrow_lend_book_state_wire() {
assert_wire(&BorrowLendBookState::Open, "Open");
assert_wire(&BorrowLendBookState::RepayOnly, "RepayOnly");
assert_wire(
&BorrowLendBookState::Unknown("SomeFutureState".into()),
"SomeFutureState",
);
}
#[test]
fn borrow_lend_market_history_interval_wire() {
assert_wire(&BorrowLendMarketHistoryInterval::OneDay, "1d");
assert_wire(&BorrowLendMarketHistoryInterval::OneWeek, "1w");
assert_wire(&BorrowLendMarketHistoryInterval::OneMonth, "1month");
assert_wire(&BorrowLendMarketHistoryInterval::OneYear, "1year");
}
#[test]
fn test_borrow_lend_market_parse() {
let data = r#"
{
"state": "Open",
"symbol": "USDC",
"timestamp": "2026-07-30T00:53:00.000Z",
"assetMarkPrice": "1.0001",
"borrowInterestRate": "0.0523",
"lendInterestRate": "0.0470",
"borrowedQuantity": "1500000.50",
"lentQuantity": "5000000.00",
"utilization": "0.30",
"fee": "0.001",
"maxUtilization": "1",
"openBorrowLendLimit": "10000000",
"optimalUtilization": "0.9",
"stepSize": "0.0000000001",
"throttleUtilizationThreshold": "0.92",
"throttleUtilizationBound": "0.95",
"throttleUpdateFraction": "0.1"
}
"#;
let market: BorrowLendMarket = serde_json::from_str(data).unwrap();
assert_eq!(market.state, BorrowLendBookState::Open);
assert_eq!(market.symbol, "USDC");
assert_eq!(market.borrow_interest_rate, dec!(0.0523));
assert_eq!(market.lend_interest_rate, dec!(0.0470));
assert_eq!(market.utilization, dec!(0.30));
}
#[test]
fn test_borrow_lend_market_unknown_state() {
let data = r#"
{
"state": "SomeFutureState",
"symbol": "USDC",
"timestamp": "2026-07-30T00:53:00.000Z",
"assetMarkPrice": "1",
"borrowInterestRate": "0",
"lendInterestRate": "0",
"borrowedQuantity": "0",
"lentQuantity": "0",
"utilization": "0",
"fee": "0",
"maxUtilization": "1",
"openBorrowLendLimit": "0",
"optimalUtilization": "0.9",
"stepSize": "0.0000000001",
"throttleUtilizationThreshold": "0.92",
"throttleUtilizationBound": "0.95",
"throttleUpdateFraction": "0.1"
}
"#;
let market: BorrowLendMarket = serde_json::from_str(data).unwrap();
assert_eq!(
market.state,
BorrowLendBookState::Unknown("SomeFutureState".into())
);
}
#[test]
fn test_borrow_lend_history_parse() {
let data = r#"
{
"borrowInterestRate": "0.051",
"lendInterestRate": "0.046",
"borrowedQuantity": "1400000",
"lentQuantity": "4800000",
"utilization": "0.29",
"timestamp": "2026-07-29T00:00:00.000Z"
}
"#;
let history: BorrowLendHistory = serde_json::from_str(data).unwrap();
assert_eq!(history.borrow_interest_rate, dec!(0.051));
assert_eq!(history.utilization, dec!(0.29));
}
#[test]
fn test_apy_rates_parse() {
let data = r#"
{
"borrowLend": [
{ "symbol": "USDC", "borrowRate": "0.0523", "lendRate": "0.0470" },
{ "symbol": "BTC", "borrowRate": "0.0312", "lendRate": "0.0265" }
],
"staking": [
{ "symbol": "SOL", "dilutionFactor": "0.85", "stakingRate": "0.0612" },
{ "symbol": "USDC", "dilutionFactor": "0.90", "stakingRate": "0.0480" }
]
}
"#;
let rates: ApyRates = serde_json::from_str(data).unwrap();
assert_eq!(rates.borrow_lend.len(), 2);
assert_eq!(rates.borrow_lend[0].symbol, "USDC");
assert_eq!(rates.staking.len(), 2);
assert_eq!(rates.staking[0].symbol, "SOL");
}
#[test]
fn test_borrow_lend_market_history_interval_query() {
let params = BorrowLendMarketHistoryParams {
interval: BorrowLendMarketHistoryInterval::OneDay,
symbol: Some("USDC".to_string()),
};
let query = serde_qs::to_string(¶ms).unwrap();
assert_eq!(query, "interval=1d&symbol=USDC");
}
}