use std::fmt;
use crate::account::signers::ActionKind;
use crate::account::state::{PerpState, SpotState};
use crate::client::{ApiError, Transport, info, user_request};
use crate::utils::sign::{L1Action, Map};
use crate::utils::{Address, AssetKind, BadUnits, Dec, exact_units};
use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AccountRef {
pub owner: Address,
pub sub: Option<Address>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
pub enum SubRefusal {
#[error("a subaccount places no spot orders here")]
Spot,
#[error("a subaccount places no HIP-4 orders and splits or merges nothing here")]
Outcome,
#[error("a subaccount's order names its market, since its spot and HIP-4 orders are refused")]
MarketUnknown,
#[error("a subaccount moves nothing between its own ledgers here")]
LedgerMove,
#[error("a subaccount approves nothing: its master's agent signs for it")]
Approval,
#[error("a subaccount schedules no cancel here")]
ScheduleCancel,
#[error("only the master opens subaccounts and moves money in and out of them")]
MastersOwn,
#[error("a subaccount has no key of its own to sign this")]
NoKey,
}
impl AccountRef {
#[must_use]
pub const fn own(owner: Address) -> AccountRef {
AccountRef { owner, sub: None }
}
#[must_use]
pub const fn sub(owner: Address, sub: Address) -> AccountRef {
AccountRef { owner, sub: Some(sub) }
}
#[must_use]
pub const fn is_sub(&self) -> bool {
self.sub.is_some()
}
#[must_use]
pub fn read_at(&self) -> Address {
self.sub.unwrap_or(self.owner)
}
#[must_use]
pub const fn vault_address(&self) -> Option<Address> {
self.sub
}
#[must_use]
pub const fn signs_for(&self) -> Address {
self.owner
}
#[must_use]
pub fn refuses(&self, kind: ActionKind, market: Option<AssetKind>) -> Option<SubRefusal> {
self.sub?;
match kind {
ActionKind::Order | ActionKind::Modify | ActionKind::BatchModify | ActionKind::TwapOrder => match market {
Some(AssetKind::Perp | AssetKind::Hip3Perp) => None,
Some(AssetKind::Spot) => Some(SubRefusal::Spot),
Some(AssetKind::Outcome) => Some(SubRefusal::Outcome),
None => Some(SubRefusal::MarketUnknown),
},
ActionKind::UserOutcome => Some(SubRefusal::Outcome),
ActionKind::UsdClassTransfer | ActionKind::AgentSendAsset => Some(SubRefusal::LedgerMove),
ActionKind::ApproveAgent | ActionKind::ApproveBuilderFee => Some(SubRefusal::Approval),
ActionKind::ScheduleCancel => Some(SubRefusal::ScheduleCancel),
ActionKind::CreateSubAccount | ActionKind::SubAccountTransfer | ActionKind::SubAccountSpotTransfer => {
Some(SubRefusal::MastersOwn)
}
ActionKind::UserSetAbstraction => None,
user_signed if user_signed.is_user_signed() => Some(SubRefusal::NoKey),
_ => None,
}
}
}
impl fmt::Display for AccountRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.sub {
Some(sub) => write!(f, "{sub} (a subaccount of {})", self.owner),
None => write!(f, "{}", self.owner),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateSubAccount {
name: String,
}
impl CreateSubAccount {
#[must_use]
pub fn new(name: &str) -> CreateSubAccount {
CreateSubAccount { name: name.to_owned() }
}
}
impl L1Action for CreateSubAccount {
fn action(&self) -> Map {
Map::new().put("type", "createSubAccount").put("name", self.name.clone())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubAccountTransfer {
sub_account: Address,
is_deposit: bool,
usd: u64,
}
impl SubAccountTransfer {
pub fn deposit(sub_account: Address, usdc: Dec) -> Result<SubAccountTransfer, BadUnits> {
Ok(SubAccountTransfer { sub_account, is_deposit: true, usd: exact_units(usdc, 6)? })
}
pub fn withdraw(sub_account: Address, usdc: Dec) -> Result<SubAccountTransfer, BadUnits> {
Ok(SubAccountTransfer { sub_account, is_deposit: false, usd: exact_units(usdc, 6)? })
}
#[must_use]
pub const fn from_micros(sub_account: Address, is_deposit: bool, usd: u64) -> SubAccountTransfer {
SubAccountTransfer { sub_account, is_deposit, usd }
}
}
impl L1Action for SubAccountTransfer {
fn action(&self) -> Map {
Map::new()
.put("type", "subAccountTransfer")
.put("subAccountUser", self.sub_account.to_hex())
.put("isDeposit", self.is_deposit)
.put("usd", self.usd)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubAccountSpotTransfer {
sub_account: Address,
is_deposit: bool,
token: String,
amount: Dec,
}
impl SubAccountSpotTransfer {
pub fn new(
sub_account: Address,
is_deposit: bool,
token: &str,
amount: Dec,
) -> Result<SubAccountSpotTransfer, BadUnits> {
if !amount.is_positive() {
return Err(BadUnits::NotPositive(amount));
}
Ok(SubAccountSpotTransfer { sub_account, is_deposit, token: token.to_owned(), amount })
}
}
impl L1Action for SubAccountSpotTransfer {
fn action(&self) -> Map {
Map::new()
.put("type", "subAccountSpotTransfer")
.put("subAccountUser", self.sub_account.to_hex())
.put("isDeposit", self.is_deposit)
.put("token", self.token.clone())
.put("amount", self.amount.to_wire())
}
}
#[must_use]
pub fn sub_accounts_request(master: Address) -> String {
user_request("subAccounts", master)
}
pub async fn sub_accounts(transport: &impl Transport, master: Address) -> Result<Vec<SubAccount>, ApiError> {
let subs: Option<Vec<SubAccount>> = info(transport, sub_accounts_request(master)).await?;
Ok(subs.unwrap_or_default())
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubAccount {
#[serde(default)]
pub name: String,
pub sub_account_user: Address,
pub master: Address,
#[serde(default)]
pub clearinghouse_state: Option<PerpState>,
#[serde(default)]
pub spot_state: Option<SpotState>,
}
impl SubAccount {
#[must_use]
pub const fn account(&self) -> AccountRef {
AccountRef::sub(self.master, self.sub_account_user)
}
#[must_use]
pub fn equity(&self) -> Dec {
self.clearinghouse_state.as_ref().map_or(Dec::ZERO, |state| state.margin_summary.account_value)
}
#[must_use]
pub fn notional(&self) -> Dec {
self.clearinghouse_state.as_ref().map_or(Dec::ZERO, |state| state.margin_summary.total_ntl_pos)
}
#[must_use]
pub fn withdrawable(&self) -> Dec {
self.clearinghouse_state.as_ref().map_or(Dec::ZERO, |state| state.withdrawable)
}
#[must_use]
pub fn positions(&self) -> usize {
self.clearinghouse_state.as_ref().map_or(0, |state| state.positions().count())
}
#[must_use]
pub fn spot_usdc(&self) -> Dec {
self.spot_state.as_ref().and_then(SpotState::usdc).map_or(Dec::ZERO, |usdc| usdc.total)
}
}
#[cfg(test)]
#[path = "tests/sub_account.rs"]
mod tests;