Skip to content

Commit b4ffcd0

Browse files
committed
feat(hypercore): add HIP-3 builder dex perps support
- Fan out perpetual API calls across active builder dexes via `for_each_dex` helper - Add `PerpDex` model and `perpDexs` RPC endpoint - Map HIP-3 asset indices with offset encoding (100k + dex_index * 10k + meta_index) - Format display names as `SYMBOL (dex)` for builder dex assets - Add `only_isolated` to `Perpetual` and `margin_type` to `PerpetualConfirmData` - Use margin type from confirm data in signer instead of hardcoded cross margin - Merge positions, portfolios, and account summaries across dexes - Extract HIP-3 specific logic into `hip3_mapper` module - Extract `with_dex` helper to deduplicate RPC client dex injection - Add `UniverseAsset::mock()` and `AssetMetadata::mock()` to testkit - Make `ReferrerState.data` optional for `needToCreateCode` stage
1 parent 822e82c commit b4ffcd0

19 files changed

Lines changed: 517 additions & 107 deletions

File tree

crates/gem_hypercore/src/models/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod balance;
33
pub mod candlestick;
44
pub mod metadata;
55
pub mod order;
6+
pub mod perp_dex;
67
pub mod portfolio;
78
pub mod position;
89
pub mod referral;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Clone, Serialize, Deserialize)]
4+
#[serde(rename_all = "camelCase")]
5+
pub struct PerpDex {
6+
pub name: String,
7+
pub is_active: Option<bool>,
8+
}

crates/gem_hypercore/src/provider/fee_calculator.rs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,7 @@ pub fn calculate_perpetual_fee_amount(fiat_value: f64, fee_rate: i64) -> BigInt
1616
BigInt::from(result as i64)
1717
}
1818

19-
pub fn calculate_spot_fee_amount(
20-
swap_data: &SwapData,
21-
from_asset: &Asset,
22-
to_asset: &Asset,
23-
fee_rate: i64,
24-
builder_fee_bps: u32,
25-
) -> Result<BigInt, Box<dyn Error + Send + Sync>> {
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>> {
2620
let fiat_value = calculate_spot_usdc_value(swap_data, from_asset, to_asset, builder_fee_bps)?;
2721
let usdc_decimals = spot_usdc_decimals(from_asset, to_asset)?;
2822
let trade_fee = calculate_perpetual_fee_amount(fiat_value * decimal_scale(usdc_decimals - HYPERCORE_PERPETUAL_USDC_DECIMALS), fee_rate);
@@ -31,12 +25,7 @@ pub fn calculate_spot_fee_amount(
3125
Ok(trade_fee + builder_fee)
3226
}
3327

34-
fn calculate_spot_usdc_value(
35-
swap_data: &SwapData,
36-
from_asset: &Asset,
37-
to_asset: &Asset,
38-
builder_fee_bps: u32,
39-
) -> Result<f64, Box<dyn Error + Send + Sync>> {
28+
fn calculate_spot_usdc_value(swap_data: &SwapData, from_asset: &Asset, to_asset: &Asset, builder_fee_bps: u32) -> Result<f64, Box<dyn Error + Send + Sync>> {
4029
let usdc_from = from_asset.id == *HYPERCORE_SPOT_USDC_ASSET_ID;
4130
let usdc_to = to_asset.id == *HYPERCORE_SPOT_USDC_ASSET_ID;
4231

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
use std::collections::BTreeMap;
2+
3+
use primitives::{
4+
chart::ChartDateValue,
5+
perpetual::{PerpetualBalance, PerpetualPositionsSummary},
6+
portfolio::{PerpetualAccountSummary, PerpetualPortfolio, PerpetualPortfolioTimeframeData},
7+
};
8+
9+
use crate::models::position::AssetPositions;
10+
11+
const HIP3_PERP_ASSET_OFFSET: u32 = 100_000;
12+
const HIP3_PERP_ASSET_STRIDE: u32 = 10_000;
13+
14+
pub fn perp_asset_index(perp_dex_index: u32, meta_index: u32) -> u32 {
15+
if perp_dex_index == 0 {
16+
meta_index
17+
} else {
18+
HIP3_PERP_ASSET_OFFSET + perp_dex_index * HIP3_PERP_ASSET_STRIDE + meta_index
19+
}
20+
}
21+
22+
pub fn format_display_name(name: &str) -> String {
23+
match name.split_once(':') {
24+
Some((dex, symbol)) => format!("{symbol} ({dex})"),
25+
None => name.to_string(),
26+
}
27+
}
28+
29+
pub fn map_account_summary_aggregate(positions: &[AssetPositions]) -> PerpetualAccountSummary {
30+
let account_value: f64 = positions.iter().map(|p| p.margin_summary.account_value.parse().unwrap_or(0.0)).sum();
31+
let total_ntl_pos: f64 = positions.iter().map(|p| p.margin_summary.total_ntl_pos.parse().unwrap_or(0.0)).sum();
32+
let total_margin_used: f64 = positions.iter().map(|p| p.margin_summary.total_margin_used.parse().unwrap_or(0.0)).sum();
33+
let unrealized_pnl: f64 = positions
34+
.iter()
35+
.flat_map(|p| &p.asset_positions)
36+
.map(|p| p.position.unrealized_pnl.parse().unwrap_or(0.0))
37+
.sum();
38+
39+
let account_leverage = if account_value > 0.0 { total_ntl_pos / account_value } else { 0.0 };
40+
let margin_usage = if account_value > 0.0 { total_margin_used / account_value } else { 0.0 };
41+
42+
PerpetualAccountSummary {
43+
account_value,
44+
account_leverage,
45+
margin_usage,
46+
unrealized_pnl,
47+
}
48+
}
49+
50+
pub fn merge_positions_summaries(summaries: Vec<PerpetualPositionsSummary>) -> PerpetualPositionsSummary {
51+
let (positions, balance) = summaries.into_iter().fold(
52+
(Vec::new(), PerpetualBalance { available: 0.0, reserved: 0.0, withdrawable: 0.0 }),
53+
|(mut acc_pos, mut acc_bal), summary| {
54+
acc_pos.extend(summary.positions);
55+
acc_bal.available += summary.balance.available;
56+
acc_bal.reserved += summary.balance.reserved;
57+
acc_bal.withdrawable += summary.balance.withdrawable;
58+
(acc_pos, acc_bal)
59+
},
60+
);
61+
PerpetualPositionsSummary { positions, balance }
62+
}
63+
64+
pub fn merge_perpetual_portfolios(portfolios: Vec<PerpetualPortfolio>, account_summary: Option<PerpetualAccountSummary>) -> PerpetualPortfolio {
65+
let mut day = Vec::new();
66+
let mut week = Vec::new();
67+
let mut month = Vec::new();
68+
let mut all_time = Vec::new();
69+
70+
for portfolio in portfolios {
71+
day.extend(portfolio.day);
72+
week.extend(portfolio.week);
73+
month.extend(portfolio.month);
74+
all_time.extend(portfolio.all_time);
75+
}
76+
77+
PerpetualPortfolio {
78+
day: merge_portfolio_timeframes(day),
79+
week: merge_portfolio_timeframes(week),
80+
month: merge_portfolio_timeframes(month),
81+
all_time: merge_portfolio_timeframes(all_time),
82+
account_summary,
83+
}
84+
}
85+
86+
fn merge_portfolio_timeframes(values: Vec<PerpetualPortfolioTimeframeData>) -> Option<PerpetualPortfolioTimeframeData> {
87+
if values.is_empty() {
88+
return None;
89+
}
90+
91+
let mut account_value_histories = Vec::new();
92+
let mut pnl_histories = Vec::new();
93+
let mut volume = 0.0;
94+
95+
for value in values {
96+
account_value_histories.push(value.account_value_history);
97+
pnl_histories.push(value.pnl_history);
98+
volume += value.volume;
99+
}
100+
101+
Some(PerpetualPortfolioTimeframeData {
102+
account_value_history: merge_chart_histories(account_value_histories),
103+
pnl_history: merge_chart_histories(pnl_histories),
104+
volume,
105+
})
106+
}
107+
108+
fn merge_chart_histories(values: Vec<Vec<ChartDateValue>>) -> Vec<ChartDateValue> {
109+
let mut grouped = BTreeMap::new();
110+
for history in values {
111+
for point in history {
112+
let entry = grouped.entry(point.date).or_insert(0.0);
113+
*entry += point.value;
114+
}
115+
}
116+
117+
grouped.into_iter().map(|(date, value)| ChartDateValue { date, value }).collect()
118+
}
119+
120+
#[cfg(test)]
121+
mod tests {
122+
use super::*;
123+
use primitives::portfolio::PerpetualAccountSummary;
124+
125+
#[test]
126+
fn test_perp_asset_index() {
127+
assert_eq!(perp_asset_index(0, 0), 0);
128+
assert_eq!(perp_asset_index(0, 5), 5);
129+
assert_eq!(perp_asset_index(1, 0), 110_000);
130+
assert_eq!(perp_asset_index(1, 3), 110_003);
131+
assert_eq!(perp_asset_index(2, 0), 120_000);
132+
assert_eq!(perp_asset_index(2, 7), 120_007);
133+
}
134+
135+
#[test]
136+
fn test_format_display_name() {
137+
assert_eq!(format_display_name("xyz:GOLD"), "GOLD (xyz)");
138+
assert_eq!(format_display_name("BTC"), "BTC");
139+
}
140+
141+
#[test]
142+
fn test_map_account_summary_aggregate() {
143+
let positions = vec![AssetPositions::mock(), AssetPositions::mock()];
144+
let summary = map_account_summary_aggregate(&positions);
145+
146+
assert_eq!(summary.account_value, 20000.0);
147+
assert_eq!(summary.account_leverage, 0.5);
148+
assert_eq!(summary.margin_usage, 0.2);
149+
assert_eq!(summary.unrealized_pnl, 0.0);
150+
}
151+
152+
#[test]
153+
fn test_merge_chart_histories() {
154+
use chrono::{TimeZone, Utc};
155+
156+
let d1 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
157+
let d2 = Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap();
158+
let d3 = Utc.with_ymd_and_hms(2024, 1, 3, 0, 0, 0).unwrap();
159+
160+
let histories = vec![
161+
vec![ChartDateValue { date: d1, value: 100.0 }, ChartDateValue { date: d2, value: 200.0 }],
162+
vec![ChartDateValue { date: d1, value: 50.0 }, ChartDateValue { date: d3, value: 300.0 }],
163+
];
164+
165+
let merged = merge_chart_histories(histories);
166+
assert_eq!(merged.len(), 3);
167+
assert_eq!(merged[0].value, 150.0);
168+
assert_eq!(merged[1].value, 200.0);
169+
assert_eq!(merged[2].value, 300.0);
170+
}
171+
172+
#[test]
173+
fn test_merge_perpetual_portfolios() {
174+
use chrono::{TimeZone, Utc};
175+
176+
let d1 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
177+
let portfolios = vec![
178+
PerpetualPortfolio::mock_with_day(d1, 100.0, 10.0, 500.0),
179+
PerpetualPortfolio::mock_with_day(d1, 200.0, 20.0, 300.0),
180+
];
181+
let summary = PerpetualAccountSummary { account_value: 1000.0, account_leverage: 2.0, margin_usage: 0.5, unrealized_pnl: 30.0 };
182+
183+
let merged = merge_perpetual_portfolios(portfolios, Some(summary));
184+
185+
let day = merged.day.unwrap();
186+
assert_eq!(day.volume, 800.0);
187+
assert_eq!(day.account_value_history.len(), 1);
188+
assert_eq!(day.account_value_history[0].value, 300.0);
189+
assert_eq!(day.pnl_history[0].value, 30.0);
190+
assert!(merged.week.is_none());
191+
assert_eq!(merged.account_summary.unwrap().account_value, 1000.0);
192+
}
193+
194+
#[test]
195+
fn test_merge_portfolio_timeframes_empty() {
196+
let result = merge_portfolio_timeframes(vec![]);
197+
assert!(result.is_none());
198+
}
199+
}

crates/gem_hypercore/src/provider/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use gem_client::Client;
55
pub mod balances;
66
pub mod balances_mapper;
77
pub mod fee_calculator;
8+
pub mod hip3_mapper;
89
pub mod perpetual;
910
pub mod perpetual_mapper;
1011
pub mod preload;

0 commit comments

Comments
 (0)