Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Commit 8a173f8

Browse files
committed
Improve fee calculation for hypercore
1 parent 7e9b6fd commit 8a173f8

8 files changed

Lines changed: 139 additions & 58 deletions

File tree

‎crates/gem_hypercore/src/models/user.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ pub struct UserFee {
3232
#[serde(deserialize_with = "deserialize_f64_from_str")]
3333
pub user_cross_rate: f64,
3434
#[serde(deserialize_with = "deserialize_f64_from_str")]
35+
pub user_spot_cross_rate: f64,
36+
#[serde(deserialize_with = "deserialize_f64_from_str")]
3537
pub active_referral_discount: f64,
3638
}
3739

‎crates/gem_hypercore/src/provider/fee_calculator.rs‎

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,15 @@ use std::error::Error;
55

66
use crate::perpetual_formatter::USDC_DECIMALS_MULTIPLIER;
77

8-
const HYPERCORE_USER_FEE_RATE_SCALE: f64 = 1_000_000.0;
9-
const HYPERCORE_TRADE_FEE_MULTIPLIER: f64 = 20.0;
108
const HYPERCORE_BUILDER_FEE_RATE_SCALE: f64 = 100_000.0;
119
const HYPERCORE_PERPETUAL_USDC_DECIMALS: i32 = 6;
1210

13-
pub fn calculate_perpetual_fee_amount(fiat_value: f64, fee_rate: i64) -> BigInt {
14-
let fee_rate_f64 = fee_rate as f64;
15-
let result = fiat_value * (fee_rate_f64 / HYPERCORE_USER_FEE_RATE_SCALE) * HYPERCORE_TRADE_FEE_MULTIPLIER * USDC_DECIMALS_MULTIPLIER;
11+
pub fn calculate_perpetual_fee_amount(fiat_value: f64, fee_rate: f64) -> BigInt {
12+
let result = fiat_value * fee_rate * USDC_DECIMALS_MULTIPLIER;
1613
BigInt::from(result as i64)
1714
}
1815

19-
pub fn calculate_spot_fee_amount(swap_data: &SwapData, from_asset: &Asset, to_asset: &Asset, fee_rate: i64, builder_fee_bps: u32) -> Result<BigInt, Box<dyn Error + Send + Sync>> {
16+
pub fn calculate_spot_fee_amount(swap_data: &SwapData, from_asset: &Asset, to_asset: &Asset, fee_rate: f64, builder_fee_bps: u32) -> Result<BigInt, Box<dyn Error + Send + Sync>> {
2017
let fiat_value = calculate_spot_usdc_value(swap_data, from_asset, to_asset, builder_fee_bps)?;
2118
let usdc_decimals = spot_usdc_decimals(from_asset, to_asset)?;
2219
let trade_fee = calculate_perpetual_fee_amount(fiat_value * decimal_scale(usdc_decimals - HYPERCORE_PERPETUAL_USDC_DECIMALS), fee_rate);
@@ -69,14 +66,12 @@ mod tests {
6966
#[test]
7067
fn calculate_perpetual_fee_amount_cases() {
7168
for (fiat_value, fee_rate, expected) in [
72-
(100.0, 50000, 100_000_000_i64),
73-
(1000.0, 0, 0),
74-
(0.0, 25000, 0),
75-
(1.0, 1000, 20_000),
76-
(10000.0, 100000, 20_000_000_000),
77-
(500.0, 30000, 300_000_000),
78-
(150.0, 43, 129_000),
79-
(1000.0, 43, 860_000),
69+
(100.0, 0.00045, 45_000_i64),
70+
(1000.0, 0.000315, 315_000),
71+
(1000.0, 0.0, 0),
72+
(0.0, 0.000025, 0),
73+
(1.0, 0.000043, 43),
74+
(10000.0, 0.001, 10_000_000),
8075
] {
8176
assert_eq!(calculate_perpetual_fee_amount(fiat_value, fee_rate), BigInt::from(expected));
8277
}
@@ -89,16 +84,16 @@ mod tests {
8984
SwapData::mock_with_values(SwapProvider::Hyperliquid, "30000000", "1181917897"),
9085
&HYPERCORE_SPOT_HYPE,
9186
&HYPERCORE_SPOT_USDC,
92-
1_856_445_u64,
87+
1_194_273_u64,
9388
),
9489
(
9590
SwapData::mock_with_values(SwapProvider::Hyperliquid, "1197900000", "29986500"),
9691
&HYPERCORE_SPOT_USDC,
9792
&HYPERCORE_SPOT_HYPE,
98-
1_880_702_u64,
93+
1_209_878_u64,
9994
),
10095
] {
101-
let result = calculate_spot_fee_amount(&swap_data, from_asset, to_asset, 56, 45).unwrap();
96+
let result = calculate_spot_fee_amount(&swap_data, from_asset, to_asset, 0.00056, 45).unwrap();
10297
assert_eq!(result, BigInt::from(expected));
10398
}
10499
}

‎crates/gem_hypercore/src/provider/preload.rs‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ use primitives::{
1111

1212
use crate::is_spot_swap;
1313
use crate::provider::fee_calculator::{calculate_perpetual_fee_amount, calculate_spot_fee_amount};
14-
use crate::provider::preload_cache::HyperCoreCache;
14+
use crate::provider::preload_cache::{HyperCoreCache, UserFeeRates};
1515
use crate::provider::preload_mapper::get_approvals_and_credentials;
1616
use crate::rpc::client::HyperCoreClient;
1717

1818
impl<C: Client> HyperCoreClient<C> {
19-
async fn get_order(&self, sender_address: &str) -> Result<(HyperliquidOrder, i64), Box<dyn Error + Sync + Send>> {
19+
async fn get_order(&self, sender_address: &str) -> Result<(HyperliquidOrder, UserFeeRates), Box<dyn Error + Sync + Send>> {
2020
let cache = HyperCoreCache::new(self.preferences.clone(), self.config.clone());
21-
let (agent_required, referral_required, builder_required, fee_rate, agent_address, agent_private_key) = get_approvals_and_credentials(
21+
let (agent_required, referral_required, builder_required, fee_rates, agent_address, agent_private_key) = get_approvals_and_credentials(
2222
&cache,
2323
sender_address,
2424
self.secure_preferences.clone(),
@@ -38,7 +38,7 @@ impl<C: Client> HyperCoreClient<C> {
3838
agent_address,
3939
agent_private_key,
4040
},
41-
fee_rate,
41+
fee_rates,
4242
))
4343
}
4444
}
@@ -60,9 +60,9 @@ impl<C: Client> ChainTransactionLoad for HyperCoreClient<C> {
6060
}
6161
TransactionInputType::Swap(from_asset, to_asset, _) => {
6262
let (fee_amount, order) = if is_spot_swap(from_asset.chain(), to_asset.chain()) {
63-
let (order, fee_rate) = self.get_order(&input.sender_address).await?;
63+
let (order, fee_rates) = self.get_order(&input.sender_address).await?;
6464
let swap_data = input.input_type.get_swap_data().map_err(|err| err.to_string())?;
65-
let fee_amount = calculate_spot_fee_amount(swap_data, from_asset, to_asset, fee_rate, self.config.max_builder_fee_bps)?;
65+
let fee_amount = calculate_spot_fee_amount(swap_data, from_asset, to_asset, fee_rates.spot_cross, self.config.max_builder_fee_bps)?;
6666

6767
(fee_amount, Some(order))
6868
} else {
@@ -82,8 +82,8 @@ impl<C: Client> ChainTransactionLoad for HyperCoreClient<C> {
8282
PerpetualType::Close(data) => data.fiat_value,
8383
PerpetualType::Modify(_) => 0.0,
8484
};
85-
let (order, fee_rate) = self.get_order(&input.sender_address).await?;
86-
let fee_amount = calculate_perpetual_fee_amount(fiat_value, fee_rate);
85+
let (order, fee_rates) = self.get_order(&input.sender_address).await?;
86+
let fee_amount = calculate_perpetual_fee_amount(fiat_value, fee_rates.perpetual_cross);
8787

8888
Ok(TransactionLoadData {
8989
fee: TransactionFee::new_from_fee(fee_amount),

‎crates/gem_hypercore/src/provider/preload_cache.rs‎

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ use std::future::Future;
88
use std::sync::Arc;
99
use std::time::{SystemTime, UNIX_EPOCH};
1010

11-
pub struct HyperCoreCache {
11+
pub(crate) struct UserFeeRates {
12+
pub(crate) perpetual_cross: f64,
13+
pub(crate) spot_cross: f64,
14+
}
15+
16+
pub(crate) struct HyperCoreCache {
1217
preferences: Arc<dyn Preferences>,
1318
config: HypercoreConfig,
1419
}
@@ -17,10 +22,12 @@ impl HyperCoreCache {
1722
const REFERRAL_APPROVED_KEY: &'static str = "referral_approved";
1823
const BUILDER_FEE_APPROVED_KEY: &'static str = "builder_fee_approved";
1924
const AGENT_VALID_UNTIL_KEY: &'static str = "agent_valid_until";
20-
const USER_FEES_KEY: &'static str = "user_fees";
25+
const USER_PERPETUAL_FEE_RATE_KEY: &'static str = "user_perpetual_fee_rate";
26+
const USER_SPOT_FEE_RATE_KEY: &'static str = "user_spot_fee_rate";
2127
const USER_FEES_TTL: u64 = 86_400 * 7;
28+
const USER_FEE_RATE_SCALE: f64 = 1_000_000_000.0;
2229

23-
pub fn new(preferences: Arc<dyn Preferences>, config: HypercoreConfig) -> Self {
30+
pub(crate) fn new(preferences: Arc<dyn Preferences>, config: HypercoreConfig) -> Self {
2431
Self { preferences, config }
2532
}
2633

@@ -32,7 +39,7 @@ impl HyperCoreCache {
3239
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64)
3340
}
3441

35-
pub async fn needs_referral_approval<F>(&self, address: &str, checker: F) -> Result<bool, Box<dyn Error + Send + Sync>>
42+
pub(crate) async fn needs_referral_approval<F>(&self, address: &str, checker: F) -> Result<bool, Box<dyn Error + Send + Sync>>
3643
where
3744
F: Future<Output = Result<Referral, Box<dyn Error + Send + Sync>>>,
3845
{
@@ -52,7 +59,7 @@ impl HyperCoreCache {
5259
Ok(needs_approval)
5360
}
5461

55-
pub async fn needs_builder_fee_approval<F>(&self, address: &str, checker: F) -> Result<bool, Box<dyn Error + Send + Sync>>
62+
pub(crate) async fn needs_builder_fee_approval<F>(&self, address: &str, checker: F) -> Result<bool, Box<dyn Error + Send + Sync>>
5663
where
5764
F: Future<Output = Result<u32, Box<dyn Error + Send + Sync>>>,
5865
{
@@ -72,24 +79,37 @@ impl HyperCoreCache {
7279
Ok(needs_approval)
7380
}
7481

75-
pub async fn get_user_fee_rate<F>(&self, address: &str, fetcher: F) -> Result<i64, Box<dyn Error + Send + Sync>>
82+
pub(crate) async fn get_user_fee_rates<F>(&self, address: &str, fetcher: F) -> Result<UserFeeRates, Box<dyn Error + Send + Sync>>
7683
where
7784
F: Future<Output = Result<UserFee, Box<dyn Error + Send + Sync>>>,
7885
{
79-
let cache_key = self.cache_key(address, Self::USER_FEES_KEY);
80-
81-
if let Some(cached_rate) = self.preferences.get_i64_with_ttl(&cache_key, Self::USER_FEES_TTL)? {
82-
return Ok(cached_rate);
86+
let perpetual_cache_key = self.cache_key(address, Self::USER_PERPETUAL_FEE_RATE_KEY);
87+
let spot_cache_key = self.cache_key(address, Self::USER_SPOT_FEE_RATE_KEY);
88+
89+
if let (Some(perpetual_cross), Some(spot_cross)) = (
90+
self.preferences.get_i64_with_ttl(&perpetual_cache_key, Self::USER_FEES_TTL)?,
91+
self.preferences.get_i64_with_ttl(&spot_cache_key, Self::USER_FEES_TTL)?,
92+
) {
93+
return Ok(UserFeeRates {
94+
perpetual_cross: perpetual_cross as f64 / Self::USER_FEE_RATE_SCALE,
95+
spot_cross: spot_cross as f64 / Self::USER_FEE_RATE_SCALE,
96+
});
8397
}
8498

8599
let user_fees = fetcher.await?;
86-
let rate = (user_fees.user_cross_rate * (1.0 - user_fees.active_referral_discount) * 100000.0) as i64;
87-
88-
self.preferences.set_i64_with_ttl(&cache_key, rate, Self::USER_FEES_TTL)?;
89-
Ok(rate)
100+
let discount = 1.0 - user_fees.active_referral_discount;
101+
let perpetual_cross = (user_fees.user_cross_rate * discount * Self::USER_FEE_RATE_SCALE).round() as i64;
102+
let spot_cross = (user_fees.user_spot_cross_rate * discount * Self::USER_FEE_RATE_SCALE).round() as i64;
103+
104+
self.preferences.set_i64_with_ttl(&perpetual_cache_key, perpetual_cross, Self::USER_FEES_TTL)?;
105+
self.preferences.set_i64_with_ttl(&spot_cache_key, spot_cross, Self::USER_FEES_TTL)?;
106+
Ok(UserFeeRates {
107+
perpetual_cross: perpetual_cross as f64 / Self::USER_FEE_RATE_SCALE,
108+
spot_cross: spot_cross as f64 / Self::USER_FEE_RATE_SCALE,
109+
})
90110
}
91111

92-
pub async fn manage_agent<F>(
112+
pub(crate) async fn manage_agent<F>(
93113
&self,
94114
sender_address: &str,
95115
secure_preferences: Arc<dyn primitives::Preferences>,
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
use crate::models::referral::Referral;
22
use crate::models::user::{AgentSession, UserFee};
3-
use crate::provider::preload_cache::HyperCoreCache;
3+
use crate::provider::preload_cache::{HyperCoreCache, UserFeeRates};
44
use std::error::Error;
55
use std::future::Future;
66
use std::sync::Arc;
77

8-
pub async fn get_approvals_and_credentials(
8+
pub(crate) async fn get_approvals_and_credentials(
99
cache: &HyperCoreCache,
1010
sender_address: &str,
1111
secure_preferences: Arc<dyn primitives::Preferences>,
1212
get_agents: impl Future<Output = Result<Vec<AgentSession>, Box<dyn Error + Send + Sync>>>,
1313
get_referral: impl Future<Output = Result<Referral, Box<dyn Error + Send + Sync>>>,
1414
get_builder_fee: impl Future<Output = Result<u32, Box<dyn Error + Send + Sync>>>,
1515
get_user_fees: impl Future<Output = Result<UserFee, Box<dyn Error + Send + Sync>>>,
16-
) -> Result<(bool, bool, bool, i64, String, String), Box<dyn Error + Send + Sync>> {
17-
let ((agent_required, agent_address, agent_private_key), referral_required, builder_required, fee_rate) = futures::try_join!(
16+
) -> Result<(bool, bool, bool, UserFeeRates, String, String), Box<dyn Error + Send + Sync>> {
17+
let ((agent_required, agent_address, agent_private_key), referral_required, builder_required, fee_rates) = futures::try_join!(
1818
cache.manage_agent(sender_address, secure_preferences.clone(), get_agents),
1919
cache.needs_referral_approval(sender_address, get_referral),
2020
cache.needs_builder_fee_approval(sender_address, get_builder_fee),
21-
cache.get_user_fee_rate(sender_address, get_user_fees),
21+
cache.get_user_fee_rates(sender_address, get_user_fees),
2222
)?;
2323

24-
Ok((agent_required, referral_required, builder_required, fee_rate, agent_address, agent_private_key))
24+
Ok((agent_required, referral_required, builder_required, fee_rates, agent_address, agent_private_key))
2525
}

‎crates/gem_hypercore/src/provider/transaction_state_mapper.rs‎

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
use crate::models::{
2-
order::{FillDirection, UserFill},
3-
transaction_id::HyperCoreActionId,
4-
user::{DelegatorHistoryDelta, DelegatorHistoryUpdate, LedgerDelta, LedgerUpdate},
5-
};
61
use number_formatter::BigNumberFormatter;
72
use primitives::{
83
PerpetualDirection, PerpetualProvider, TransactionChange, TransactionMetadata, TransactionPerpetualMetadata, TransactionState, TransactionType, TransactionUpdate,
94
known_assets::HYPERCORE_HYPE,
105
};
116

7+
use crate::models::{
8+
order::{FillDirection, UserFill},
9+
transaction_id::HyperCoreActionId,
10+
user::{DelegatorHistoryDelta, DelegatorHistoryUpdate, LedgerDelta, LedgerUpdate},
11+
};
12+
use crate::perpetual_formatter::usdc_value;
13+
1214
pub const ACTION_HISTORY_QUERY_LOOKBACK_MS: u64 = 5_000;
1315
const ACTION_HISTORY_MATCH_WINDOW_MS: u64 = 5 * 60 * 1_000;
1416
const DELEGATOR_WITHDRAWAL_INITIATED: &str = "initiated";
@@ -53,8 +55,9 @@ pub fn map_transaction_state_order(fills: Vec<UserFill>, oid: u64, request_id: S
5355
match &last_fill.dir {
5456
FillDirection::Buy | FillDirection::Sell => {}
5557
FillDirection::OpenLong | FillDirection::OpenShort | FillDirection::CloseLong | FillDirection::CloseShort => {
56-
let (_, metadata) = prepare_perpetual_fill(&matching_fills, last_fill).unwrap();
57-
update.changes.push(TransactionChange::Metadata(TransactionMetadata::Perpetual(metadata)));
58+
if let Some(changes) = perpetual_fill_changes(&matching_fills, last_fill) {
59+
update.changes.extend(changes);
60+
}
5861
}
5962
FillDirection::Other(_) => return TransactionUpdate::new_state(TransactionState::Pending),
6063
}
@@ -70,15 +73,29 @@ pub fn map_transaction_state_order(fills: Vec<UserFill>, oid: u64, request_id: S
7073
}
7174

7275
pub fn map_transaction_state_order_action(fills: Vec<UserFill>, nonce: u64, request_id: String) -> TransactionUpdate {
73-
transaction_update_from_hash(order_action_hash(&fills, nonce), request_id)
76+
let Some(fill) = order_action_fill(&fills, nonce) else {
77+
return TransactionUpdate::new_state(TransactionState::Pending);
78+
};
79+
80+
let hash = fill.hash.clone();
81+
let matching_fills: Vec<_> = fills.iter().filter(|item| item.oid == fill.oid).collect();
82+
let mut update = TransactionUpdate::new(TransactionState::Confirmed, vec![TransactionChange::HashChange { old: request_id, new: hash }]);
83+
84+
if let Some(last_fill) = matching_fills.iter().max_by_key(|fill| fill.time)
85+
&& let Some(changes) = perpetual_fill_changes(&matching_fills, last_fill)
86+
{
87+
update.changes.extend(changes);
88+
}
89+
90+
update
7491
}
7592

76-
pub fn order_action_hash(fills: &[UserFill], nonce: u64) -> Option<String> {
93+
pub(crate) fn order_action_fill(fills: &[UserFill], nonce: u64) -> Option<&UserFill> {
7794
fills
7895
.iter()
7996
.filter_map(|fill| action_history_time_delta(fill.time, nonce).filter(|_| !fill.hash.is_empty()).map(|delta| (delta, fill)))
8097
.min_by_key(|(delta, _)| *delta)
81-
.map(|(_, fill)| fill.hash.clone())
98+
.map(|(_, fill)| fill)
8299
}
83100

84101
pub fn map_transaction_state_action(updates: Vec<LedgerUpdate>, action_id: HyperCoreActionId, request_id: String) -> TransactionUpdate {
@@ -162,10 +179,22 @@ fn transaction_update_from_hash(hash: Option<String>, request_id: String) -> Tra
162179
}
163180
}
164181

182+
fn perpetual_fill_changes(matching_fills: &[&UserFill], last_fill: &UserFill) -> Option<Vec<TransactionChange>> {
183+
let (_, metadata) = prepare_perpetual_fill(matching_fills, last_fill)?;
184+
let fee: f64 = matching_fills.iter().map(|fill| fill.fee + fill.builder_fee.unwrap_or(0.0)).sum();
185+
let network_fee = usdc_value(fee).parse().ok()?;
186+
187+
Some(vec![
188+
TransactionChange::Metadata(TransactionMetadata::Perpetual(metadata)),
189+
TransactionChange::NetworkFee(network_fee),
190+
])
191+
}
192+
165193
#[cfg(test)]
166194
mod tests {
167195
use super::*;
168196
use crate::models::order::{FillDirection, UserFill};
197+
use num_bigint::BigInt;
169198

170199
#[test]
171200
fn test_map_transaction_state_order() {
@@ -176,7 +205,7 @@ mod tests {
176205
let update = map_transaction_state_order(fills, oid, request_id.clone());
177206

178207
assert_eq!(update.state, TransactionState::Confirmed);
179-
assert_eq!(update.changes.len(), 2);
208+
assert_eq!(update.changes.len(), 3);
180209

181210
let metadata_change = update.changes.iter().find_map(|change| {
182211
if let TransactionChange::Metadata(TransactionMetadata::Perpetual(metadata)) = change {
@@ -192,6 +221,12 @@ mod tests {
192221
assert_eq!(metadata.is_liquidation, Some(false));
193222
assert_eq!(metadata.provider, Some(PerpetualProvider::Hypercore));
194223

224+
let network_fee_change = update
225+
.changes
226+
.iter()
227+
.find_map(|change| if let TransactionChange::NetworkFee(fee) = change { Some(fee) } else { None });
228+
assert_eq!(network_fee_change, Some(&BigInt::from(666786)));
229+
195230
let hash_change = update.changes.iter().find_map(|change| {
196231
if let TransactionChange::HashChange { old, new } = change {
197232
Some((old, new))
@@ -254,6 +289,27 @@ mod tests {
254289
);
255290
}
256291

292+
#[test]
293+
fn test_map_transaction_state_order_action_includes_perpetual_fee() {
294+
let fills: Vec<UserFill> = serde_json::from_str(include_str!("../../testdata/user_fills_multiple.json")).unwrap();
295+
let request_id = "action:order:1759700579000".to_string();
296+
let update = map_transaction_state_order_action(fills, 1759700579000, request_id.clone());
297+
298+
assert_eq!(update.state, TransactionState::Confirmed);
299+
assert_eq!(
300+
update.changes[0],
301+
TransactionChange::HashChange {
302+
old: request_id,
303+
new: "0x9b4d63110c57f2e19cc7042ce90e300202f500f6a75b11b33f160e63cb5bcccc".to_string(),
304+
}
305+
);
306+
let network_fee_change = update
307+
.changes
308+
.iter()
309+
.find_map(|change| if let TransactionChange::NetworkFee(fee) = change { Some(fee) } else { None });
310+
assert_eq!(network_fee_change, Some(&BigInt::from(666786)));
311+
}
312+
257313
#[test]
258314
fn test_map_transaction_state_order_action_confirms_closest_fill() {
259315
let fills: Vec<UserFill> = serde_json::from_str(include_str!("../../testdata/user_fills_spot_swap.json")).unwrap();

0 commit comments

Comments
 (0)