Skip to main content

df_hyperliquid_core/trade/
builder.rs

1//! Builder fees as a setting: what each market kind pays the caller's builder, held to that kind's
2//! cap, and whether the account has approved that much. Whether the builder can be paid at all is
3//! [`crate::account::builder_readiness`].
4
5use crate::trade::builder_fee::BuilderFee;
6use crate::trade::order::{Builder, OverCap, PlaceOrder};
7use crate::utils::{Address, Asset, AssetKind};
8
9/// What each order pays the builder.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FeeRule {
12    /// This many percent of each market kind's cap, cut down to a whole tenth of a basis point: 88 is
13    /// 88 of a perp's 100 and 880 of spot's 1000. At most 100.
14    ShareOfCap(u64),
15    /// One fee on every market; an order whose market allows less is refused,
16    /// never lowered.
17    Flat(BuilderFee),
18}
19
20/// The builder orders are routed through, and what they pay it.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct BuilderSetting {
23    address: Address,
24    rule: FeeRule,
25}
26
27impl BuilderSetting {
28    /// `percent` of each market's cap; `None` over 100.
29    #[must_use]
30    pub const fn share_of_cap(address: Address, percent: u64) -> Option<BuilderSetting> {
31        match percent <= 100 {
32            true => Some(BuilderSetting { address, rule: FeeRule::ShareOfCap(percent) }),
33            false => None,
34        }
35    }
36
37    /// `fee` on every market.
38    #[must_use]
39    pub const fn flat(address: Address, fee: BuilderFee) -> BuilderSetting {
40        BuilderSetting { address, rule: FeeRule::Flat(fee) }
41    }
42
43    #[must_use]
44    pub const fn address(&self) -> Address {
45        self.address
46    }
47
48    #[must_use]
49    pub const fn rule(&self) -> FeeRule {
50        self.rule
51    }
52
53    /// What an order on `kind` pays, whether the market allows it or not.
54    #[must_use]
55    pub const fn fee_on(&self, kind: AssetKind) -> BuilderFee {
56        match self.rule {
57            FeeRule::ShareOfCap(percent) => {
58                BuilderFee::from_tenths(kind.builder_cap().tenths().saturating_mul(percent) / 100)
59            }
60            FeeRule::Flat(fee) => fee,
61        }
62    }
63
64    /// The builder an order on `kind` names, or why it cannot.
65    pub fn on(&self, kind: AssetKind) -> Result<Builder, OverCap> {
66        let fee = self.fee_on(kind);
67        let cap = kind.builder_cap();
68        match fee > cap {
69            true => Err(OverCap { fee, cap, kind }),
70            false => Ok(Builder::new(self.address, fee)),
71        }
72    }
73
74    /// The builder one action over these assets names: held to the
75    /// strictest kind among them, so one perp order holds the whole action
76    /// to the perp cap. `None` for no assets.
77    pub fn on_assets(&self, assets: impl IntoIterator<Item = Asset>) -> Result<Option<Builder>, OverCap> {
78        AssetKind::strictest(assets).map(|kind| self.on(kind)).transpose()
79    }
80
81    /// The most an order on any market it may trade pays, which is what one
82    /// approval has to cover: 880 at 88% of each cap.
83    #[must_use]
84    pub fn most(&self) -> BuilderFee {
85        AssetKind::ALL
86            .into_iter()
87            .filter_map(|kind| self.on(kind).ok())
88            .map(|builder| builder.fee())
89            .max()
90            .unwrap_or_default()
91    }
92
93    /// What `order` needs before it is sent, by what the account has approved this builder to charge
94    /// and by who signs.
95    pub fn approval(&self, order: &PlaceOrder, approved: &Approved, by: SignedBy) -> Result<Approval, ApprovalRefused> {
96        let Some(builder) = order.builder().filter(|builder| builder.fee() > BuilderFee::ZERO) else {
97            return Ok(Approval::Goes);
98        };
99        if builder.address() != self.address {
100            return Err(ApprovalRefused::OtherBuilder { named: builder.address(), set: self.address });
101        }
102        let fee = builder.fee();
103        let approved = match approved {
104            Approved::Upto(approved) if *approved >= fee => return Ok(Approval::Goes),
105            Approved::Upto(approved) => *approved,
106            Approved::Unread => return Err(ApprovalRefused::NotRead),
107            Approved::Failed(why) => return Err(ApprovalRefused::Unreadable(why.clone())),
108        };
109        let rate = self.most().max(fee);
110        match by {
111            SignedBy::Account => Ok(Approval::ApproveFirst { approved, rate }),
112            SignedBy::Agent => Err(ApprovalRefused::AgentCannotApprove { approved, rate }),
113        }
114    }
115}
116
117/// What the account has approved its builder to charge, as `maxBuilderFee`
118/// last answered or as the last approval the venue took.
119#[derive(Debug, Clone, PartialEq, Eq, Default)]
120pub enum Approved {
121    /// Nothing read yet.
122    #[default]
123    Unread,
124    /// Up to this.
125    Upto(BuilderFee),
126    /// The read failed, and why.
127    Failed(String),
128}
129
130impl Approved {
131    /// What the account has approved, in words.
132    #[must_use]
133    pub fn words(&self) -> String {
134        match self {
135            Approved::Unread => "not read yet".to_owned(),
136            Approved::Upto(fee) if *fee == BuilderFee::ZERO => "nothing yet".to_owned(),
137            Approved::Upto(fee) => format!("up to {fee}"),
138            Approved::Failed(why) => format!("would not read: {why}"),
139        }
140    }
141}
142
143/// Whose key signs the order. Only the account's own may sign
144/// `approveBuilderFee`; an agent's may not.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum SignedBy {
147    Account,
148    Agent,
149}
150
151/// What an order paying a builder needs before it is sent.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum Approval {
154    /// Nothing: no builder, a fee of 0, or a fee already approved.
155    Goes,
156    /// `approveBuilderFee` at `rate` first, signed by the account: it has
157    /// approved `approved`, under the order's fee. `rate` is the most any
158    /// market pays, so one approval covers them all.
159    ApproveFirst { approved: BuilderFee, rate: BuilderFee },
160}
161
162impl Approval {
163    /// The `maxFeeRate` `approveBuilderFee` names: `0.88%` for 880.
164    #[must_use]
165    pub fn max_fee_rate(&self) -> Option<String> {
166        match self {
167            Approval::Goes => None,
168            Approval::ApproveFirst { rate, .. } => Some(rate.percent()),
169        }
170    }
171}
172
173/// Why an order cannot go for its builder fee: what is not known is refused
174/// rather than guessed at.
175#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
176pub enum ApprovalRefused {
177    #[error("the builder fee this account has approved is not read yet; try again in a moment")]
178    NotRead,
179    #[error("the builder fee this account has approved would not read, so it is not guessed at: {0}")]
180    Unreadable(String),
181    #[error(
182        "the account's main key has to approve the builder fee up to {rate} once (approved now: {approved}); an agent key cannot sign approveBuilderFee"
183    )]
184    AgentCannotApprove { approved: BuilderFee, rate: BuilderFee },
185    #[error("the order names the builder {named}, and what the account approved was read for {set}")]
186    OtherBuilder { named: Address, set: Address },
187}
188
189#[cfg(test)]
190#[path = "tests/builder.rs"]
191mod tests;