use serde::{Deserialize, Deserializer};
use crate::account::holdings::positions::{Asset, PoolShare, Position};
use crate::account::settings::integrators::ApprovedIntegrator;
use crate::account::settings::metadata::SubAccountMetadata;
use crate::account::settings::mode::{TradingMode, known_mode};
use crate::client::http::codes;
use crate::client::{ApiError, Call, Transport, read};
use crate::utils::ids::whole;
use crate::utils::lenient::{blank_dec, list, text, unsigned, zero_dec};
use crate::utils::{AccountIndex, Address, AssetId, Dec, MarketId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Lookup {
Index(AccountIndex),
L1Address(Address),
}
impl Lookup {
#[must_use]
pub const fn by(&self) -> &'static str {
match self {
Lookup::Index(_) => "index",
Lookup::L1Address(_) => "l1_address",
}
}
#[must_use]
pub fn value(&self) -> String {
match self {
Lookup::Index(index) => index.to_string(),
Lookup::L1Address(address) => address.to_hex(),
}
}
}
impl From<AccountIndex> for Lookup {
fn from(index: AccountIndex) -> Lookup {
Lookup::Index(index)
}
}
impl From<Address> for Lookup {
fn from(address: Address) -> Lookup {
Lookup::L1Address(address)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AccountKind {
Master,
SubAccount,
Pool(u8),
}
impl AccountKind {
#[must_use]
pub const fn code(self) -> u8 {
match self {
AccountKind::Master => 0,
AccountKind::SubAccount => 1,
AccountKind::Pool(code) => code,
}
}
#[must_use]
pub const fn from_code(code: u8) -> AccountKind {
match code {
0 => AccountKind::Master,
1 => AccountKind::SubAccount,
code => AccountKind::Pool(code),
}
}
}
impl<'de> Deserialize<'de> for AccountKind {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let code = whole(deserializer)?;
Ok(AccountKind::from_code(u8::try_from(code).unwrap_or(u8::MAX)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct BoPosition {
pub market_id: MarketId,
pub sign: i8,
pub size: Dec,
#[serde(default, deserialize_with = "zero_dec")]
pub entry_quote: Dec,
#[serde(default, deserialize_with = "zero_dec")]
pub avg_entry_price: Dec,
}
impl BoPosition {
#[must_use]
pub const fn is_up(&self) -> bool {
self.sign >= 0
}
}
pub use crate::earn::pools::PoolInfo;
pub use crate::earn::staking::PendingUnlock;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Account {
pub index: AccountIndex,
#[serde(rename = "account_type")]
pub kind: AccountKind,
pub l1_address: Address,
#[serde(rename = "account_trading_mode", default, deserialize_with = "known_mode")]
pub trading_mode: Option<TradingMode>,
#[serde(default)]
pub status: u8,
#[serde(deserialize_with = "zero_dec")]
pub collateral: Dec,
#[serde(deserialize_with = "zero_dec")]
pub available_balance: Dec,
#[serde(default, deserialize_with = "zero_dec")]
pub total_asset_value: Dec,
#[serde(default, deserialize_with = "zero_dec")]
pub cross_asset_value: Dec,
#[serde(default, deserialize_with = "zero_dec")]
pub cross_initial_margin_requirement: Dec,
#[serde(default, deserialize_with = "zero_dec")]
pub cross_maintenance_margin_requirement: Dec,
#[serde(default, deserialize_with = "unsigned")]
pub cancel_all_time: u64,
#[serde(default, deserialize_with = "unsigned")]
pub total_order_count: u64,
#[serde(default, deserialize_with = "unsigned")]
pub total_isolated_order_count: u64,
#[serde(default, deserialize_with = "unsigned")]
pub pending_order_count: u64,
#[serde(default, deserialize_with = "list")]
pub positions: Vec<Position>,
#[serde(default, deserialize_with = "list")]
pub bo_positions: Vec<BoPosition>,
#[serde(default, deserialize_with = "list")]
pub assets: Vec<Asset>,
#[serde(default, deserialize_with = "list")]
pub shares: Vec<PoolShare>,
#[serde(default, deserialize_with = "list")]
pub pending_unlocks: Vec<PendingUnlock>,
#[serde(default, deserialize_with = "list")]
pub approved_integrators: Vec<ApprovedIntegrator>,
#[serde(default, deserialize_with = "text")]
pub name: String,
#[serde(default, deserialize_with = "text")]
pub description: String,
#[serde(default)]
pub can_invite: bool,
#[serde(default, deserialize_with = "blank_dec")]
pub referral_points_percentage: Option<Dec>,
#[serde(default)]
pub can_rfq: bool,
#[serde(default, deserialize_with = "market_ids")]
pub can_rfq_market_ids: Vec<MarketId>,
#[serde(default)]
pub agent_enabled: bool,
#[serde(default, deserialize_with = "unsigned")]
pub created_at: u64,
#[serde(default, deserialize_with = "unsigned")]
pub transaction_time: u64,
#[serde(default)]
pub metadata: SubAccountMetadata,
#[serde(default)]
pub pool_info: Option<PoolInfo>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PerpsBalance {
pub collateral: Dec,
pub total: Dec,
pub available: Dec,
pub unrealized_pnl: Dec,
pub margin_used: Dec,
}
impl Account {
#[must_use]
pub const fn is_master(&self) -> bool {
matches!(self.kind, AccountKind::Master)
}
#[must_use]
pub fn is_unified(&self) -> bool {
self.trading_mode == Some(TradingMode::Unified)
}
pub fn open_positions(&self) -> impl Iterator<Item = &Position> {
self.positions.iter().filter(|position| position.is_open())
}
#[must_use]
pub fn position(&self, market: MarketId) -> Option<&Position> {
self.positions.iter().find(|position| position.market_id == market)
}
#[must_use]
pub fn open_position(&self, market: MarketId) -> Option<&Position> {
self.position(market).filter(|position| position.is_open())
}
#[must_use]
pub const fn free_collateral(&self) -> Dec {
self.available_balance
}
#[must_use]
pub fn perps_balance(&self) -> PerpsBalance {
let total = self.total_asset_value;
let unrealized_pnl = total.checked_sub(self.collateral).unwrap_or(Dec::ZERO);
let margin_used = total.checked_sub(self.available_balance).filter(|used| used.is_positive());
PerpsBalance {
collateral: self.collateral,
total,
available: self.available_balance,
unrealized_pnl,
margin_used: margin_used.unwrap_or(Dec::ZERO),
}
}
#[must_use]
pub fn asset(&self, asset: AssetId) -> Option<&Asset> {
self.assets.iter().find(|held| held.asset_id == asset)
}
pub fn spot_assets(&self) -> impl Iterator<Item = &Asset> {
self.assets.iter().filter(|held| held.balance.is_positive())
}
#[must_use]
pub fn pool_share(&self, pool: AccountIndex) -> Option<&PoolShare> {
self.shares.iter().find(|share| share.pool == pool)
}
#[must_use]
pub fn integrator(&self, integrator: AccountIndex) -> Option<&ApprovedIntegrator> {
self.approved_integrators.iter().find(|approved| approved.integrator == integrator)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct AccountPage {
#[serde(default, deserialize_with = "list")]
pub accounts: Vec<Account>,
#[serde(default, deserialize_with = "unsigned")]
pub total: u64,
#[serde(default)]
pub next_cursor: Option<String>,
}
#[must_use]
pub fn account_call(lookup: Lookup, active_only: bool, cursor: Option<&str>) -> Call {
Call::get("account")
.field("by", lookup.by())
.field("value", lookup.value())
.maybe("active_only", active_only.then_some(true))
.maybe("cursor", cursor)
}
pub async fn accounts(
transport: &impl Transport,
lookup: Lookup,
active_only: bool,
cursor: Option<&str>,
) -> Result<AccountPage, ApiError> {
match read::<AccountPage>(transport, account_call(lookup, active_only, cursor)).await {
Err(error) if error.code() == Some(codes::ACCOUNT_NOT_FOUND) => {
Ok(AccountPage { accounts: Vec::new(), total: 0, next_cursor: None })
}
answer => answer,
}
}
pub async fn account(transport: &impl Transport, index: AccountIndex) -> Result<Option<Account>, ApiError> {
let page = accounts(transport, Lookup::Index(index), false, None).await?;
Ok(page.accounts.into_iter().find(|account| account.index == index))
}
fn market_ids<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<MarketId>, D::Error> {
let listed = Option::<Vec<serde_json::Value>>::deserialize(deserializer)?.unwrap_or_default();
Ok(listed
.iter()
.filter_map(|id| match id {
serde_json::Value::String(text) => text.parse().ok(),
serde_json::Value::Number(number) => number.as_u64().and_then(|id| MarketId::new(id).ok()),
_ => None,
})
.collect())
}
#[cfg(test)]
#[path = "tests/account.rs"]
mod tests;