Skip to main content

df_hyperliquid_core/account/
onboarding.rs

1//! What is left before an account can trade, computed from live state every time: a deposit, an
2//! agent approval, a builder fee approval and the account mode. The venue refuses every other
3//! action before a deposit.
4
5use crate::account::agent::{Agent, Role};
6use crate::account::mode::AbstractionMode;
7use crate::account::signers::ActionKind;
8use crate::account::state::{Abstraction, PerpState, SpotState};
9use crate::account::sub_account::AccountRef;
10use crate::client::Network;
11use crate::trade::builder_fee::BuilderFee;
12use crate::utils::{Address, Dec};
13
14/// The bridge credits nothing under 5 USDC.
15pub const MIN_DEPOSIT: Dec = Dec::from_int(5);
16
17/// What the caller wants of the account. Each `None` leaves its step out.
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
19pub struct Wants {
20    /// The agent that will sign, for the owner or for the subaccount.
21    pub agent: Option<Address>,
22    /// A builder and the fee it charges, in tenths of a basis point.
23    pub builder: Option<(Address, BuilderFee)>,
24    pub mode: Option<AbstractionMode>,
25}
26
27/// What the venue said, read at the account's own address, except `agents`
28/// and the agent's role, which are the signing account's.
29#[derive(Debug, Clone, Copy)]
30pub struct Seen<'a> {
31    /// The main dex.
32    pub perp: &'a PerpState,
33    pub spot: &'a SpotState,
34    pub mode: &'a Abstraction,
35    /// `extraAgents` of the account that signs.
36    pub agents: &'a [Agent],
37    /// `userRole` of the wanted agent, where it was read.
38    pub agent_role: Option<&'a Role>,
39    /// `maxBuilderFee` for the wanted builder, where it was read.
40    pub approved: Option<BuilderFee>,
41}
42
43/// One thing to do.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum StepKind {
46    /// USDC to the bridge on `chain_id`, at least `min`.
47    Deposit {
48        chain_id: u64,
49        min: Dec,
50    },
51    /// `subAccountTransfer` from the master.
52    FundFromMaster,
53    ApproveAgent {
54        agent: Address,
55    },
56    ApproveBuilderFee {
57        builder: Address,
58        fee: BuilderFee,
59    },
60    SetMode {
61        mode: AbstractionMode,
62    },
63}
64
65impl StepKind {
66    /// A name that stays the same: `deposit`, `approve-agent`, `builder-fee`,
67    /// `account-mode`.
68    #[must_use]
69    pub const fn id(&self) -> &'static str {
70        match self {
71            StepKind::Deposit { .. } | StepKind::FundFromMaster => "deposit",
72            StepKind::ApproveAgent { .. } => "approve-agent",
73            StepKind::ApproveBuilderFee { .. } => "builder-fee",
74            StepKind::SetMode { .. } => "account-mode",
75        }
76    }
77
78    /// The venue's action that does it; none for a deposit, which goes to
79    /// the bridge. Setting the mode is `userSetAbstraction` with the main
80    /// key, or `agentSetAbstraction` with an agent's.
81    #[must_use]
82    pub const fn action(&self) -> Option<ActionKind> {
83        match self {
84            StepKind::Deposit { .. } => None,
85            StepKind::FundFromMaster => Some(ActionKind::SubAccountTransfer),
86            StepKind::ApproveAgent { .. } => Some(ActionKind::ApproveAgent),
87            StepKind::ApproveBuilderFee { .. } => Some(ActionKind::ApproveBuilderFee),
88            StepKind::SetMode { .. } => Some(ActionKind::UserSetAbstraction),
89        }
90    }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum Status {
95    Done,
96    Needed,
97    Blocked(Blocked),
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum Blocked {
102    /// The venue refuses every action before a deposit.
103    DepositFirst,
104    /// The venue refuses it, in words.
105    Refused(String),
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct Step {
110    pub kind: StepKind,
111    pub status: Status,
112}
113
114/// Whether the account holds anything, perps or spot.
115#[must_use]
116pub fn deposited(perp: &PerpState, spot: &SpotState) -> bool {
117    perp.margin_summary.account_value.is_positive() || spot.balances.iter().any(|row| row.total.is_positive())
118}
119
120/// The steps for `account` on `network` at `now` milliseconds, in the order they are done. An
121/// agent counts as approved while `extraAgents` lists it unexpired or, for the unnamed agent, while
122/// `userRole` says it signs for the account.
123#[must_use]
124pub fn steps(account: &AccountRef, network: Network, seen: &Seen<'_>, wants: &Wants, now: u64) -> Vec<Step> {
125    let funded = deposited(seen.perp, seen.spot);
126    let after = |done: bool| match (done, funded) {
127        (true, _) => Status::Done,
128        (false, true) => Status::Needed,
129        (false, false) => Status::Blocked(Blocked::DepositFirst),
130    };
131    let deposit = match account.is_sub() {
132        true => StepKind::FundFromMaster,
133        false => StepKind::Deposit { chain_id: network.arbitrum_chain_id(), min: MIN_DEPOSIT },
134    };
135    let mut steps = vec![Step { kind: deposit, status: if funded { Status::Done } else { Status::Needed } }];
136    if let Some(agent) = wants.agent {
137        let listed = seen.agents.iter().any(|known| known.address == agent && known.is_live(now));
138        let signs = seen.agent_role.and_then(Role::agent_of) == Some(account.signs_for());
139        steps.push(Step { kind: StepKind::ApproveAgent { agent }, status: after(listed || signs) });
140    }
141    if let Some((builder, fee)) = wants.builder.filter(|_| !account.is_sub()) {
142        let covered = seen.approved.is_some_and(|approved| approved >= fee);
143        steps.push(Step { kind: StepKind::ApproveBuilderFee { builder, fee }, status: after(covered) });
144    }
145    if let Some(mode) = wants.mode {
146        let status = match (seen.mode.wire() == mode.wire(), mode.is_refused_from(seen.mode)) {
147            (true, _) => Status::Done,
148            (false, true) => {
149                Status::Blocked(Blocked::Refused("the venue refuses a unified account back to standard".to_owned()))
150            }
151            (false, false) => after(false),
152        };
153        steps.push(Step { kind: StepKind::SetMode { mode }, status });
154    }
155    steps
156}
157
158/// The steps not done yet.
159pub fn left(steps: &[Step]) -> impl Iterator<Item = &Step> {
160    steps.iter().filter(|step| step.status != Status::Done)
161}
162
163#[cfg(test)]
164#[path = "tests/onboarding.rs"]
165mod tests;