Skip to main content

hypercall_engine/
fee.rs

1//! Fee calculation service for trading fees.
2//!
3//! This module provides fee calculation functionality including:
4//! - Maker/taker fee calculation based on configurable BPS rates
5//! - Builder code fee calculation (share of taker fee)
6//!
7//! All calculations are pure math with no external dependencies.
8
9use hypercall_types::{WalletAddress, CONTRACT_UNIT_MULTIPLIER};
10use rust_decimal::Decimal;
11
12/// Configuration for fee calculation.
13#[derive(Debug, Clone)]
14pub struct FeeConfig {
15    /// Maker fee in basis points (e.g., 0.5 = 0.005%)
16    pub maker_fee_bps: f64,
17    /// Taker fee in basis points (e.g., 2.0 = 0.02%)
18    pub taker_fee_bps: f64,
19    /// Percentage of taker fee that goes to builder code (e.g., 0.10 = 10%)
20    pub builder_code_share_pct: f64,
21}
22
23impl Default for FeeConfig {
24    fn default() -> Self {
25        Self {
26            maker_fee_bps: 0.0,
27            taker_fee_bps: 0.0,
28            builder_code_share_pct: 0.0,
29        }
30    }
31}
32
33/// Result of fee calculation.
34#[derive(Debug, Clone)]
35pub struct FeeCalculation {
36    /// Fee charged to the maker
37    pub maker_fee: f64,
38    /// Fee charged to the taker
39    pub taker_fee: f64,
40    /// Fee paid to builder code (deducted from platform revenue, not additional to taker)
41    pub builder_code_fee: Option<Decimal>,
42}
43
44/// Service for calculating trading fees.
45#[derive(Debug, Clone)]
46pub struct FeeService {
47    config: FeeConfig,
48}
49
50impl FeeService {
51    /// Create a new FeeService with the given configuration.
52    pub fn new(config: FeeConfig) -> Self {
53        Self { config }
54    }
55
56    /// Calculate fees for a trade.
57    ///
58    /// # Arguments
59    /// * `_maker_account` - Maker's account address (reserved for future tier-based fees)
60    /// * `_taker_account` - Taker's account address (reserved for future tier-based fees)
61    /// * `price` - Trade price
62    /// * `size` - Trade size in contract units
63    /// * `builder_code_address` - Optional builder code address for fee sharing
64    ///
65    /// # Returns
66    /// FeeCalculation containing maker_fee, taker_fee, and optional builder_code_fee
67    pub fn get_fees(
68        &self,
69        _maker_account: &str,
70        _taker_account: &str,
71        price: f64,
72        size: u64,
73        builder_code_address: Option<&WalletAddress>,
74    ) -> FeeCalculation {
75        // Convert size from contract units to human-readable
76        let size_human = size as f64 / CONTRACT_UNIT_MULTIPLIER;
77
78        // Calculate notional value
79        let notional = price * size_human;
80
81        // Calculate fees in basis points (1 bps = 0.01% = 0.0001)
82        let maker_fee = notional * self.config.maker_fee_bps / 10_000.0;
83        let taker_fee = notional * self.config.taker_fee_bps / 10_000.0;
84
85        // Calculate builder code fee if builder code exists
86        // Builder code fee is a share of taker fee (deducted from platform revenue)
87        let builder_code_fee = if builder_code_address.is_some()
88            && taker_fee > 0.0
89            && self.config.builder_code_share_pct > 0.0
90        {
91            Decimal::from_f64_retain(taker_fee * self.config.builder_code_share_pct)
92        } else {
93            None
94        };
95
96        FeeCalculation {
97            maker_fee,
98            taker_fee,
99            builder_code_fee,
100        }
101    }
102
103    /// Get the current fee configuration.
104    pub fn config(&self) -> &FeeConfig {
105        &self.config
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use std::str::FromStr;
113
114    #[test]
115    fn test_fee_calculation_basic() {
116        let service = FeeService::new(FeeConfig {
117            maker_fee_bps: 0.5,
118            taker_fee_bps: 2.0,
119            builder_code_share_pct: 0.10,
120        });
121
122        // Trade: price = 100.0, size = 1_000_000 (1.0 contract)
123        // Notional = 100.0 * 1.0 = 100.0
124        // Maker fee = 100.0 * 0.5 / 10000 = 0.005
125        // Taker fee = 100.0 * 2.0 / 10000 = 0.02
126        let result = service.get_fees("maker", "taker", 100.0, 1_000_000, None);
127
128        assert!((result.maker_fee - 0.005).abs() < 1e-10);
129        assert!((result.taker_fee - 0.02).abs() < 1e-10);
130        assert!(result.builder_code_fee.is_none());
131    }
132
133    #[test]
134    fn test_fee_calculation_with_builder_code() {
135        let service = FeeService::new(FeeConfig {
136            maker_fee_bps: 0.5,
137            taker_fee_bps: 2.0,
138            builder_code_share_pct: 0.10,
139        });
140
141        // Trade: price = 100.0, size = 1_000_000 (1.0 contract)
142        // Taker fee = 0.02
143        // Builder code fee = 0.02 * 0.10 = 0.002
144        let builder_code =
145            WalletAddress::from_str("0x1234567890123456789012345678901234567890").unwrap();
146        let result = service.get_fees("maker", "taker", 100.0, 1_000_000, Some(&builder_code));
147
148        assert!((result.maker_fee - 0.005).abs() < 1e-10);
149        assert!((result.taker_fee - 0.02).abs() < 1e-10);
150        assert!(result.builder_code_fee.is_some());
151        assert_eq!(
152            result.builder_code_fee.unwrap(),
153            Decimal::from_f64_retain(0.002).unwrap()
154        );
155    }
156
157    #[test]
158    fn test_fee_calculation_zero_size() {
159        let service = FeeService::new(FeeConfig {
160            maker_fee_bps: 0.5,
161            taker_fee_bps: 2.0,
162            builder_code_share_pct: 0.10,
163        });
164
165        let result = service.get_fees("maker", "taker", 100.0, 0, None);
166
167        assert_eq!(result.maker_fee, 0.0);
168        assert_eq!(result.taker_fee, 0.0);
169        assert!(result.builder_code_fee.is_none());
170    }
171
172    #[test]
173    fn test_fee_calculation_large_notional() {
174        let service = FeeService::new(FeeConfig {
175            maker_fee_bps: 0.5,
176            taker_fee_bps: 2.0,
177            builder_code_share_pct: 0.10,
178        });
179
180        // Trade: price = 50000.0 (BTC), size = 10_000_000 (10.0 BTC)
181        // Notional = 50000.0 * 10.0 = 500000.0
182        // Maker fee = 500000.0 * 0.5 / 10000 = 25.0
183        // Taker fee = 500000.0 * 2.0 / 10000 = 100.0
184        let result = service.get_fees("maker", "taker", 50000.0, 10_000_000, None);
185
186        assert!((result.maker_fee - 25.0).abs() < 1e-10);
187        assert!((result.taker_fee - 100.0).abs() < 1e-10);
188    }
189
190    #[test]
191    fn test_default_config() {
192        let config = FeeConfig::default();
193
194        assert_eq!(config.maker_fee_bps, 0.0);
195        assert_eq!(config.taker_fee_bps, 0.0);
196        assert_eq!(config.builder_code_share_pct, 0.0);
197    }
198
199    #[test]
200    fn test_default_config_disables_builder_code_fee() {
201        let service = FeeService::new(FeeConfig::default());
202        let builder_code =
203            WalletAddress::from_str("0x1234567890123456789012345678901234567890").unwrap();
204
205        let result = service.get_fees("maker", "taker", 100.0, 1_000_000, Some(&builder_code));
206
207        assert_eq!(result.maker_fee, 0.0);
208        assert_eq!(result.taker_fee, 0.0);
209        assert!(result.builder_code_fee.is_none());
210    }
211}