@@ -125,6 +125,10 @@ import type {
125125 UserHistoryItem ,
126126} from '../types' ;
127127import { PERPS_ERROR_CODES } from '../PerpsController' ;
128+ import type {
129+ ExtendedAssetMeta ,
130+ ExtendedPerpDex ,
131+ } from '../../types/perps-types' ;
128132
129133// Helper method parameter interfaces (module-level for class-dependent methods only)
130134interface GetAssetInfoParams {
@@ -224,6 +228,13 @@ export class HyperLiquidProvider implements IPerpsProvider {
224228 // Filtering is applied on-demand (cheap array operations) - no need for separate processed cache
225229 private cachedMetaByDex = new Map < string , MetaResponse > ( ) ;
226230
231+ // Cache for perpDexs data (deployerFeeScale for dynamic fee calculation)
232+ // TTL-based cache - fee scales rarely change
233+ private perpDexsCache : {
234+ data : ExtendedPerpDex [ ] | null ;
235+ timestamp : number ;
236+ } = { data : null , timestamp : 0 } ;
237+
227238 // Session cache for referral state (cleared on disconnect/reconnect)
228239 // Key: `network:userAddress`, Value: true if referral is set
229240 private referralCheckCache = new Map < string , boolean > ( ) ;
@@ -644,6 +655,116 @@ export class HyperLiquidProvider implements IPerpsProvider {
644655 return meta ;
645656 }
646657
658+ /**
659+ * Fetch perpDexs data with TTL-based caching
660+ * Returns deployerFeeScale info needed for dynamic fee calculation
661+ * @returns Array of ExtendedPerpDex objects (null entries represent main DEX)
662+ */
663+ private async getCachedPerpDexs ( ) : Promise < ExtendedPerpDex [ ] > {
664+ const now = Date . now ( ) ;
665+
666+ // Return cached data if still valid
667+ if (
668+ this . perpDexsCache . data &&
669+ now - this . perpDexsCache . timestamp <
670+ HIP3_FEE_CONFIG . PERP_DEXS_CACHE_TTL_MS
671+ ) {
672+ DevLogger . log ( '[getCachedPerpDexs] Using cached perpDexs data' , {
673+ age : `${ Math . round ( ( now - this . perpDexsCache . timestamp ) / 1000 ) } s` ,
674+ count : this . perpDexsCache . data . length ,
675+ } ) ;
676+ return this . perpDexsCache . data ;
677+ }
678+
679+ // Fetch fresh data from API
680+ // Note: SDK types are incomplete, but API returns deployerFeeScale
681+ this . ensureClientsInitialized ( ) ;
682+ const infoClient = this . clientService . getInfoClient ( ) ;
683+ const perpDexs =
684+ ( await infoClient . perpDexs ( ) ) as unknown as ExtendedPerpDex [ ] ;
685+
686+ // Cache the result
687+ this . perpDexsCache = { data : perpDexs , timestamp : now } ;
688+
689+ DevLogger . log ( '[getCachedPerpDexs] Fetched and cached perpDexs data' , {
690+ count : perpDexs . length ,
691+ dexes : perpDexs
692+ . filter ( ( d ) => d !== null )
693+ . map ( ( d ) => ( {
694+ name : d . name ,
695+ deployerFeeScale : d . deployerFeeScale ,
696+ } ) ) ,
697+ } ) ;
698+
699+ return perpDexs ;
700+ }
701+
702+ /**
703+ * Calculate HIP-3 fee multiplier using HyperLiquid's official formula
704+ * Fetches deployerFeeScale from perpDexs API and growthMode from meta API
705+ *
706+ * Formula from HyperLiquid docs:
707+ * - scaleIfHip3 = deployerFeeScale < 1 ? deployerFeeScale + 1 : deployerFeeScale * 2
708+ * - growthModeScale = growthMode ? 0.1 : 1
709+ * - finalMultiplier = scaleIfHip3 * growthModeScale
710+ *
711+ * @see https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees#fee-formula-for-developers
712+ */
713+ private async calculateHip3FeeMultiplier ( params : {
714+ dexName : string ;
715+ assetSymbol : string ;
716+ } ) : Promise < number > {
717+ const { dexName, assetSymbol } = params ;
718+
719+ try {
720+ // Get deployerFeeScale from perpDexs
721+ const perpDexs = await this . getCachedPerpDexs ( ) ;
722+ const dexInfo = perpDexs . find ( ( d ) => d ?. name === dexName ) ;
723+ const parsedScale = parseFloat ( dexInfo ?. deployerFeeScale ?? '' ) ;
724+ const deployerFeeScale = Number . isNaN ( parsedScale )
725+ ? HIP3_FEE_CONFIG . DEFAULT_DEPLOYER_FEE_SCALE
726+ : parsedScale ;
727+
728+ // Get growthMode from meta for this specific asset
729+ const meta = await this . getCachedMeta ( { dexName } ) ;
730+ const fullAssetName = `${ dexName } :${ assetSymbol } ` ;
731+ const assetMeta = meta . universe . find (
732+ ( u ) => ( u as ExtendedAssetMeta ) . name === fullAssetName ,
733+ ) as ExtendedAssetMeta | undefined ;
734+ const isGrowthMode = assetMeta ?. growthMode === 'enabled' ;
735+
736+ // Apply official formula
737+ const scaleIfHip3 =
738+ deployerFeeScale < 1 ? deployerFeeScale + 1 : deployerFeeScale * 2 ;
739+ const growthModeScale = isGrowthMode
740+ ? HIP3_FEE_CONFIG . GROWTH_MODE_SCALE
741+ : 1 ;
742+
743+ const finalMultiplier = scaleIfHip3 * growthModeScale ;
744+
745+ DevLogger . log ( 'HIP-3 Dynamic Fee Calculation' , {
746+ dexName,
747+ assetSymbol,
748+ fullAssetName,
749+ deployerFeeScale,
750+ isGrowthMode,
751+ scaleIfHip3,
752+ growthModeScale,
753+ finalMultiplier,
754+ } ) ;
755+
756+ return finalMultiplier ;
757+ } catch ( error ) {
758+ DevLogger . log ( 'HIP-3 Fee Calculation Failed, using fallback' , {
759+ dexName,
760+ assetSymbol,
761+ error : ensureError ( error ) . message ,
762+ } ) ;
763+ // Safe fallback: standard HIP-3 2x multiplier (no Growth Mode discount)
764+ return HIP3_FEE_CONFIG . DEFAULT_DEPLOYER_FEE_SCALE * 2 ;
765+ }
766+ }
767+
647768 /**
648769 * Generate session cache key for user-specific caches
649770 * Format: "network:userAddress" (address normalized to lowercase)
@@ -5071,21 +5192,27 @@ export class HyperLiquidProvider implements IPerpsProvider {
50715192 let feeRate =
50725193 orderType === 'market' || ! isMaker ? FEE_RATES . taker : FEE_RATES . maker ;
50735194
5074- // HIP-3 assets have 2× base fees (per fees.md line 9)
5075- // Parse coin to detect HIP-3 DEX (e.g., "xyz:TSLA" → dex="xyz")
5076- const { dex } = parseAssetName ( coin ) ;
5195+ // Parse coin to detect HIP-3 DEX (e.g., "xyz:TSLA" → dex="xyz", symbol="TSLA")
5196+ const { dex, symbol } = parseAssetName ( coin ) ;
50775197 const isHip3Asset = dex !== null ;
50785198
5079- if ( isHip3Asset ) {
5199+ // Calculate HIP-3 fee multiplier dynamically (handles Growth Mode)
5200+ let hip3Multiplier = 1 ;
5201+ if ( isHip3Asset && dex && symbol ) {
5202+ hip3Multiplier = await this . calculateHip3FeeMultiplier ( {
5203+ dexName : dex ,
5204+ assetSymbol : symbol ,
5205+ } ) ;
50805206 const originalRate = feeRate ;
5081- feeRate *= HIP3_FEE_CONFIG . FEE_MULTIPLIER ;
5207+ feeRate *= hip3Multiplier ;
50825208
5083- DevLogger . log ( 'HIP-3 Fee Multiplier Applied' , {
5209+ DevLogger . log ( 'HIP-3 Dynamic Fee Multiplier Applied' , {
50845210 coin,
50855211 dex,
5212+ symbol,
50865213 originalBaseRate : originalRate ,
50875214 hip3BaseRate : feeRate ,
5088- multiplier : HIP3_FEE_CONFIG . FEE_MULTIPLIER ,
5215+ hip3Multiplier ,
50895216 } ) ;
50905217 }
50915218
@@ -5095,6 +5222,7 @@ export class HyperLiquidProvider implements IPerpsProvider {
50955222 amount,
50965223 coin,
50975224 isHip3Asset,
5225+ hip3Multiplier,
50985226 baseFeeRate : feeRate ,
50995227 baseTakerRate : FEE_RATES . taker ,
51005228 baseMakerRate : FEE_RATES . maker ,
@@ -5119,9 +5247,9 @@ export class HyperLiquidProvider implements IPerpsProvider {
51195247 ? cached . perpsTakerRate
51205248 : cached . perpsMakerRate ;
51215249
5122- // Apply HIP-3 multiplier to user-specific rates
5123- if ( isHip3Asset ) {
5124- userFeeRate *= HIP3_FEE_CONFIG . FEE_MULTIPLIER ;
5250+ // Apply HIP-3 dynamic multiplier to user-specific rates (includes Growth Mode)
5251+ if ( isHip3Asset && hip3Multiplier > 0 ) {
5252+ userFeeRate *= hip3Multiplier ;
51255253 }
51265254
51275255 feeRate = userFeeRate ;
@@ -5134,6 +5262,7 @@ export class HyperLiquidProvider implements IPerpsProvider {
51345262 spotMakerRate : cached . spotMakerRate ,
51355263 selectedRate : feeRate ,
51365264 isHip3Asset,
5265+ hip3Multiplier,
51375266 cacheExpiry : new Date ( cached . timestamp + cached . ttl ) . toISOString ( ) ,
51385267 cacheAge : `${ Math . round ( ( Date . now ( ) - cached . timestamp ) / 1000 ) } s` ,
51395268 } ) ;
@@ -5245,9 +5374,9 @@ export class HyperLiquidProvider implements IPerpsProvider {
52455374 ? rates . perpsTakerRate
52465375 : rates . perpsMakerRate ;
52475376
5248- // Apply HIP-3 multiplier to API-fetched rates
5249- if ( isHip3Asset ) {
5250- userFeeRate *= HIP3_FEE_CONFIG . FEE_MULTIPLIER ;
5377+ // Apply HIP-3 dynamic multiplier to API-fetched rates (includes Growth Mode)
5378+ if ( isHip3Asset && hip3Multiplier > 0 ) {
5379+ userFeeRate *= hip3Multiplier ;
52515380 }
52525381
52535382 feeRate = userFeeRate ;
@@ -5257,6 +5386,7 @@ export class HyperLiquidProvider implements IPerpsProvider {
52575386 selectedRatePercentage : `${ ( feeRate * 100 ) . toFixed ( 4 ) } %` ,
52585387 discountApplied : perpsTakerRate < FEE_RATES . taker ,
52595388 isHip3Asset,
5389+ hip3Multiplier,
52605390 cacheExpiry : new Date ( rates . timestamp + rates . ttl ) . toISOString ( ) ,
52615391 } ) ;
52625392 }
@@ -5389,6 +5519,7 @@ export class HyperLiquidProvider implements IPerpsProvider {
53895519 this . referralCheckCache . clear ( ) ;
53905520 this . builderFeeCheckCache . clear ( ) ;
53915521 this . cachedMetaByDex . clear ( ) ;
5522+ this . perpDexsCache = { data : null , timestamp : 0 } ;
53925523
53935524 // Clear pending promise trackers to prevent memory leaks and ensure clean state
53945525 this . ensureReadyPromise = null ;
0 commit comments