@metamask-previews/perps-controller 3.2.0-preview-6ce5d58fa → 4.0.0-preview-1e2fe74a0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/dist/PerpsController-method-action-types.cjs.map +1 -1
  3. package/dist/PerpsController-method-action-types.d.cts +9 -0
  4. package/dist/PerpsController-method-action-types.d.cts.map +1 -1
  5. package/dist/PerpsController-method-action-types.d.mts +9 -0
  6. package/dist/PerpsController-method-action-types.d.mts.map +1 -1
  7. package/dist/PerpsController-method-action-types.mjs.map +1 -1
  8. package/dist/PerpsController.cjs +15 -3
  9. package/dist/PerpsController.cjs.map +1 -1
  10. package/dist/PerpsController.d.cts +18 -3
  11. package/dist/PerpsController.d.cts.map +1 -1
  12. package/dist/PerpsController.d.mts +18 -3
  13. package/dist/PerpsController.d.mts.map +1 -1
  14. package/dist/PerpsController.mjs +15 -3
  15. package/dist/PerpsController.mjs.map +1 -1
  16. package/dist/constants/perpsConfig.cjs +28 -0
  17. package/dist/constants/perpsConfig.cjs.map +1 -1
  18. package/dist/constants/perpsConfig.d.cts +3 -0
  19. package/dist/constants/perpsConfig.d.cts.map +1 -1
  20. package/dist/constants/perpsConfig.d.mts +3 -0
  21. package/dist/constants/perpsConfig.d.mts.map +1 -1
  22. package/dist/constants/perpsConfig.mjs +28 -0
  23. package/dist/constants/perpsConfig.mjs.map +1 -1
  24. package/dist/providers/AggregatedPerpsProvider.cjs +16 -6
  25. package/dist/providers/AggregatedPerpsProvider.cjs.map +1 -1
  26. package/dist/providers/AggregatedPerpsProvider.d.cts +12 -4
  27. package/dist/providers/AggregatedPerpsProvider.d.cts.map +1 -1
  28. package/dist/providers/AggregatedPerpsProvider.d.mts +12 -4
  29. package/dist/providers/AggregatedPerpsProvider.d.mts.map +1 -1
  30. package/dist/providers/AggregatedPerpsProvider.mjs +16 -6
  31. package/dist/providers/AggregatedPerpsProvider.mjs.map +1 -1
  32. package/dist/providers/HyperLiquidProvider.cjs +56 -20
  33. package/dist/providers/HyperLiquidProvider.cjs.map +1 -1
  34. package/dist/providers/HyperLiquidProvider.d.cts +29 -4
  35. package/dist/providers/HyperLiquidProvider.d.cts.map +1 -1
  36. package/dist/providers/HyperLiquidProvider.d.mts +29 -4
  37. package/dist/providers/HyperLiquidProvider.d.mts.map +1 -1
  38. package/dist/providers/HyperLiquidProvider.mjs +57 -21
  39. package/dist/providers/HyperLiquidProvider.mjs.map +1 -1
  40. package/dist/services/HyperLiquidClientService.cjs +131 -60
  41. package/dist/services/HyperLiquidClientService.cjs.map +1 -1
  42. package/dist/services/HyperLiquidClientService.d.cts +23 -0
  43. package/dist/services/HyperLiquidClientService.d.cts.map +1 -1
  44. package/dist/services/HyperLiquidClientService.d.mts +23 -0
  45. package/dist/services/HyperLiquidClientService.d.mts.map +1 -1
  46. package/dist/services/HyperLiquidClientService.mjs +132 -61
  47. package/dist/services/HyperLiquidClientService.mjs.map +1 -1
  48. package/dist/services/HyperLiquidSubscriptionService.cjs +193 -11
  49. package/dist/services/HyperLiquidSubscriptionService.cjs.map +1 -1
  50. package/dist/services/HyperLiquidSubscriptionService.d.cts.map +1 -1
  51. package/dist/services/HyperLiquidSubscriptionService.d.mts.map +1 -1
  52. package/dist/services/HyperLiquidSubscriptionService.mjs +194 -12
  53. package/dist/services/HyperLiquidSubscriptionService.mjs.map +1 -1
  54. package/dist/services/MarketDataService.cjs +89 -6
  55. package/dist/services/MarketDataService.cjs.map +1 -1
  56. package/dist/services/MarketDataService.d.cts +19 -0
  57. package/dist/services/MarketDataService.d.cts.map +1 -1
  58. package/dist/services/MarketDataService.d.mts +19 -0
  59. package/dist/services/MarketDataService.d.mts.map +1 -1
  60. package/dist/services/MarketDataService.mjs +89 -6
  61. package/dist/services/MarketDataService.mjs.map +1 -1
  62. package/dist/types/index.cjs.map +1 -1
  63. package/dist/types/index.d.cts +21 -3
  64. package/dist/types/index.d.cts.map +1 -1
  65. package/dist/types/index.d.mts +21 -3
  66. package/dist/types/index.d.mts.map +1 -1
  67. package/dist/types/index.mjs.map +1 -1
  68. package/dist/utils/accountUtils.cjs +74 -1
  69. package/dist/utils/accountUtils.cjs.map +1 -1
  70. package/dist/utils/accountUtils.d.cts +4 -0
  71. package/dist/utils/accountUtils.d.cts.map +1 -1
  72. package/dist/utils/accountUtils.d.mts +4 -0
  73. package/dist/utils/accountUtils.d.mts.map +1 -1
  74. package/dist/utils/accountUtils.mjs +70 -0
  75. package/dist/utils/accountUtils.mjs.map +1 -1
  76. package/dist/utils/coalescePerpsRestRequest.cjs +71 -0
  77. package/dist/utils/coalescePerpsRestRequest.cjs.map +1 -0
  78. package/dist/utils/coalescePerpsRestRequest.d.cts +32 -0
  79. package/dist/utils/coalescePerpsRestRequest.d.cts.map +1 -0
  80. package/dist/utils/coalescePerpsRestRequest.d.mts +32 -0
  81. package/dist/utils/coalescePerpsRestRequest.d.mts.map +1 -0
  82. package/dist/utils/coalescePerpsRestRequest.mjs +66 -0
  83. package/dist/utils/coalescePerpsRestRequest.mjs.map +1 -0
  84. package/dist/utils/hyperLiquidAdapter.cjs +7 -7
  85. package/dist/utils/hyperLiquidAdapter.cjs.map +1 -1
  86. package/dist/utils/hyperLiquidAdapter.d.cts +2 -2
  87. package/dist/utils/hyperLiquidAdapter.d.cts.map +1 -1
  88. package/dist/utils/hyperLiquidAdapter.d.mts +2 -2
  89. package/dist/utils/hyperLiquidAdapter.d.mts.map +1 -1
  90. package/dist/utils/hyperLiquidAdapter.mjs +7 -7
  91. package/dist/utils/hyperLiquidAdapter.mjs.map +1 -1
  92. package/dist/utils/perpsFormatters.cjs +5 -1
  93. package/dist/utils/perpsFormatters.cjs.map +1 -1
  94. package/dist/utils/perpsFormatters.d.cts.map +1 -1
  95. package/dist/utils/perpsFormatters.d.mts.map +1 -1
  96. package/dist/utils/perpsFormatters.mjs +5 -1
  97. package/dist/utils/perpsFormatters.mjs.map +1 -1
  98. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.aggregateAccountStates = exports.calculateWeightedReturnOnEquity = exports.getSelectedEvmAccount = exports.getEvmAccountFromAccountGroup = exports.findEvmAccount = void 0;
3
+ exports.aggregateAccountStates = exports.addSpotBalanceToAccountState = exports.getSpotHold = exports.getSpotBalance = exports.calculateWeightedReturnOnEquity = exports.getSelectedEvmAccount = exports.getEvmAccountFromAccountGroup = exports.findEvmAccount = void 0;
4
4
  const perpsConfig_1 = require("../constants/perpsConfig.cjs");
5
5
  const EVM_ACCOUNT_TYPES = new Set(['eip155:eoa', 'eip155:erc4337']);
6
6
  function isEvmAccountType(type) {
@@ -54,6 +54,70 @@ function calculateWeightedReturnOnEquity(accounts) {
54
54
  return weightedROE.toString();
55
55
  }
56
56
  exports.calculateWeightedReturnOnEquity = calculateWeightedReturnOnEquity;
57
+ // Spot coins counted toward currently supported funded-state gating.
58
+ // Today the in-app HyperLiquid market surface is USDC-collateralized only,
59
+ // so USDH must not inflate the shared funded-state path that hides Add Funds.
60
+ // Non-stablecoin spot assets (HYPE, PURR, …) also remain excluded.
61
+ const SPOT_COLLATERAL_COINS = new Set(['USDC']);
62
+ function getSpotBalance(spotState) {
63
+ if (!spotState?.balances || !Array.isArray(spotState.balances)) {
64
+ return 0;
65
+ }
66
+ return spotState.balances.reduce((sum, balance) => {
67
+ if (!balance.coin || !SPOT_COLLATERAL_COINS.has(balance.coin)) {
68
+ return sum;
69
+ }
70
+ const value = parseFloat(balance.total ?? '0');
71
+ return Number.isFinite(value) ? sum + value : sum;
72
+ }, 0);
73
+ }
74
+ exports.getSpotBalance = getSpotBalance;
75
+ function getSpotHold(spotState) {
76
+ if (!spotState?.balances || !Array.isArray(spotState.balances)) {
77
+ return 0;
78
+ }
79
+ return spotState.balances.reduce((sum, balance) => {
80
+ if (!balance.coin || !SPOT_COLLATERAL_COINS.has(balance.coin)) {
81
+ return sum;
82
+ }
83
+ const value = parseFloat(balance.hold ?? '0');
84
+ return Number.isFinite(value) ? sum + value : sum;
85
+ }, 0);
86
+ }
87
+ exports.getSpotHold = getSpotHold;
88
+ function addSpotBalanceToAccountState(accountState, spotState) {
89
+ const spotBalance = getSpotBalance(spotState);
90
+ const spotHold = getSpotHold(spotState);
91
+ const freeSpot = Math.max(0, spotBalance - spotHold);
92
+ const currentTotal = parseFloat(accountState.totalBalance);
93
+ const currentAvailable = parseFloat(accountState.availableBalance);
94
+ // Preserve sentinel totals (e.g. PERPS_CONSTANTS.FallbackDataDisplay '--')
95
+ // rather than coercing them to NaN.
96
+ if (!Number.isFinite(currentTotal)) {
97
+ return accountState;
98
+ }
99
+ if (spotBalance === 0) {
100
+ return {
101
+ ...accountState,
102
+ availableToTradeBalance: Number.isFinite(currentAvailable)
103
+ ? currentAvailable.toString()
104
+ : accountState.availableBalance,
105
+ };
106
+ }
107
+ const availableToTrade = Number.isFinite(currentAvailable)
108
+ ? (currentAvailable + freeSpot).toString()
109
+ : freeSpot.toString();
110
+ // Subtract spotHold to avoid double-counting on Unified/PM accounts:
111
+ // marginSummary.accountValue already includes the margin that HL
112
+ // surfaces via spot.hold. Standard mode has spotHold = 0, no-op.
113
+ const nextTotal = currentTotal + spotBalance - spotHold;
114
+ return {
115
+ ...accountState,
116
+ totalBalance: nextTotal.toString(),
117
+ availableToTradeBalance: availableToTrade,
118
+ };
119
+ }
120
+ exports.addSpotBalanceToAccountState = addSpotBalanceToAccountState;
57
121
  /**
58
122
  * Aggregate multiple per-DEX AccountState objects into one by summing numeric fields.
59
123
  * ROE is recalculated as (totalUnrealizedPnl / totalMarginUsed) * 100.
@@ -76,8 +140,17 @@ function aggregateAccountStates(states) {
76
140
  if (index === 0) {
77
141
  return { ...state };
78
142
  }
143
+ const accAvailableToTrade = parseFloat(acc.availableToTradeBalance ?? acc.availableBalance);
144
+ const stateAvailableToTrade = parseFloat(state.availableToTradeBalance ?? state.availableBalance);
145
+ const availableToTradeSum = Number.isFinite(accAvailableToTrade) &&
146
+ Number.isFinite(stateAvailableToTrade)
147
+ ? (accAvailableToTrade + stateAvailableToTrade).toString()
148
+ : undefined;
79
149
  return {
80
150
  availableBalance: (parseFloat(acc.availableBalance) + parseFloat(state.availableBalance)).toString(),
151
+ ...(availableToTradeSum !== undefined && {
152
+ availableToTradeBalance: availableToTradeSum,
153
+ }),
81
154
  totalBalance: (parseFloat(acc.totalBalance) + parseFloat(state.totalBalance)).toString(),
82
155
  marginUsed: (parseFloat(acc.marginUsed) + parseFloat(state.marginUsed)).toString(),
83
156
  unrealizedPnl: (parseFloat(acc.unrealizedPnl) + parseFloat(state.unrealizedPnl)).toString(),
@@ -1 +1 @@
1
- {"version":3,"file":"accountUtils.cjs","sourceRoot":"","sources":["../../src/utils/accountUtils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Account utilities for Perps components\n * Handles account selection and EVM account filtering\n */\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\n\nimport { PERPS_CONSTANTS } from '../constants/perpsConfig';\nimport type { AccountState, PerpsInternalAccount } from '../types';\n\nconst EVM_ACCOUNT_TYPES = new Set(['eip155:eoa', 'eip155:erc4337']);\n\nfunction isEvmAccountType(type: string): boolean {\n return EVM_ACCOUNT_TYPES.has(type);\n}\n\nexport function findEvmAccount(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string; type: string } | null {\n const evmAccount = accounts.find(\n (account) =>\n account && isEvmAccountType(account.type as InternalAccount['type']),\n );\n return evmAccount ?? null;\n}\n\nexport function getEvmAccountFromAccountGroup(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string } | undefined {\n const evmAccount = findEvmAccount(accounts);\n return evmAccount ? { address: evmAccount.address } : undefined;\n}\n\nexport function getSelectedEvmAccount(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string } | undefined {\n return getEvmAccountFromAccountGroup(accounts);\n}\n\nexport type ReturnOnEquityInput = {\n unrealizedPnl: string | number;\n returnOnEquity: string | number;\n};\n\nexport function calculateWeightedReturnOnEquity(\n accounts: ReturnOnEquityInput[],\n): string {\n if (accounts.length === 0) {\n return '0';\n }\n\n let totalWeightedROE = 0;\n let totalMarginUsed = 0;\n\n for (const account of accounts) {\n const unrealizedPnl =\n typeof account.unrealizedPnl === 'string'\n ? Number.parseFloat(account.unrealizedPnl)\n : account.unrealizedPnl;\n const returnOnEquity =\n typeof account.returnOnEquity === 'string'\n ? Number.parseFloat(account.returnOnEquity)\n : account.returnOnEquity;\n\n if (Number.isNaN(unrealizedPnl) || Number.isNaN(returnOnEquity)) {\n continue;\n }\n\n if (returnOnEquity === 0) {\n continue;\n }\n\n const marginUsed = (unrealizedPnl / returnOnEquity) * 100;\n\n if (Number.isNaN(marginUsed) || marginUsed <= 0) {\n continue;\n }\n\n const roeDecimal = returnOnEquity / 100;\n\n totalWeightedROE += roeDecimal * marginUsed;\n totalMarginUsed += marginUsed;\n }\n\n if (totalMarginUsed <= 0) {\n return '0';\n }\n\n const weightedROE = (totalWeightedROE / totalMarginUsed) * 100;\n return weightedROE.toString();\n}\n\n/**\n * Aggregate multiple per-DEX AccountState objects into one by summing numeric fields.\n * ROE is recalculated as (totalUnrealizedPnl / totalMarginUsed) * 100.\n *\n * @param states - The array of per-DEX account states to aggregate.\n * @returns The combined account state with summed balances and recalculated ROE.\n */\nexport function aggregateAccountStates(states: AccountState[]): AccountState {\n const fallback: AccountState = {\n availableBalance: PERPS_CONSTANTS.FallbackDataDisplay,\n totalBalance: PERPS_CONSTANTS.FallbackDataDisplay,\n marginUsed: PERPS_CONSTANTS.FallbackDataDisplay,\n unrealizedPnl: PERPS_CONSTANTS.FallbackDataDisplay,\n returnOnEquity: PERPS_CONSTANTS.FallbackDataDisplay,\n };\n\n if (states.length === 0) {\n return fallback;\n }\n\n const aggregated = states.reduce<AccountState>((acc, state, index) => {\n if (index === 0) {\n return { ...state };\n }\n return {\n availableBalance: (\n parseFloat(acc.availableBalance) + parseFloat(state.availableBalance)\n ).toString(),\n totalBalance: (\n parseFloat(acc.totalBalance) + parseFloat(state.totalBalance)\n ).toString(),\n marginUsed: (\n parseFloat(acc.marginUsed) + parseFloat(state.marginUsed)\n ).toString(),\n unrealizedPnl: (\n parseFloat(acc.unrealizedPnl) + parseFloat(state.unrealizedPnl)\n ).toString(),\n returnOnEquity: '0',\n };\n }, fallback);\n\n // Recalculate ROE across all DEXs\n const totalMarginUsed = parseFloat(aggregated.marginUsed);\n const totalUnrealizedPnl = parseFloat(aggregated.unrealizedPnl);\n if (totalMarginUsed > 0) {\n aggregated.returnOnEquity = (\n (totalUnrealizedPnl / totalMarginUsed) *\n 100\n ).toString();\n } else {\n aggregated.returnOnEquity = '0';\n }\n\n return aggregated;\n}\n"]}
1
+ {"version":3,"file":"accountUtils.cjs","sourceRoot":"","sources":["../../src/utils/accountUtils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Account utilities for Perps components\n * Handles account selection and EVM account filtering\n */\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\n\nimport { PERPS_CONSTANTS } from '../constants/perpsConfig';\nimport type { AccountState, PerpsInternalAccount } from '../types';\nimport type { SpotClearinghouseStateResponse } from '../types/hyperliquid-types';\n\nconst EVM_ACCOUNT_TYPES = new Set(['eip155:eoa', 'eip155:erc4337']);\n\nfunction isEvmAccountType(type: string): boolean {\n return EVM_ACCOUNT_TYPES.has(type);\n}\n\nexport function findEvmAccount(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string; type: string } | null {\n const evmAccount = accounts.find(\n (account) =>\n account && isEvmAccountType(account.type as InternalAccount['type']),\n );\n return evmAccount ?? null;\n}\n\nexport function getEvmAccountFromAccountGroup(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string } | undefined {\n const evmAccount = findEvmAccount(accounts);\n return evmAccount ? { address: evmAccount.address } : undefined;\n}\n\nexport function getSelectedEvmAccount(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string } | undefined {\n return getEvmAccountFromAccountGroup(accounts);\n}\n\nexport type ReturnOnEquityInput = {\n unrealizedPnl: string | number;\n returnOnEquity: string | number;\n};\n\nexport function calculateWeightedReturnOnEquity(\n accounts: ReturnOnEquityInput[],\n): string {\n if (accounts.length === 0) {\n return '0';\n }\n\n let totalWeightedROE = 0;\n let totalMarginUsed = 0;\n\n for (const account of accounts) {\n const unrealizedPnl =\n typeof account.unrealizedPnl === 'string'\n ? Number.parseFloat(account.unrealizedPnl)\n : account.unrealizedPnl;\n const returnOnEquity =\n typeof account.returnOnEquity === 'string'\n ? Number.parseFloat(account.returnOnEquity)\n : account.returnOnEquity;\n\n if (Number.isNaN(unrealizedPnl) || Number.isNaN(returnOnEquity)) {\n continue;\n }\n\n if (returnOnEquity === 0) {\n continue;\n }\n\n const marginUsed = (unrealizedPnl / returnOnEquity) * 100;\n\n if (Number.isNaN(marginUsed) || marginUsed <= 0) {\n continue;\n }\n\n const roeDecimal = returnOnEquity / 100;\n\n totalWeightedROE += roeDecimal * marginUsed;\n totalMarginUsed += marginUsed;\n }\n\n if (totalMarginUsed <= 0) {\n return '0';\n }\n\n const weightedROE = (totalWeightedROE / totalMarginUsed) * 100;\n return weightedROE.toString();\n}\n\n// Spot coins counted toward currently supported funded-state gating.\n// Today the in-app HyperLiquid market surface is USDC-collateralized only,\n// so USDH must not inflate the shared funded-state path that hides Add Funds.\n// Non-stablecoin spot assets (HYPE, PURR, …) also remain excluded.\nconst SPOT_COLLATERAL_COINS = new Set<string>(['USDC']);\n\nexport function getSpotBalance(\n spotState?: SpotClearinghouseStateResponse | null,\n): number {\n if (!spotState?.balances || !Array.isArray(spotState.balances)) {\n return 0;\n }\n\n return spotState.balances.reduce(\n (sum: number, balance: { coin?: string; total?: string }) => {\n if (!balance.coin || !SPOT_COLLATERAL_COINS.has(balance.coin)) {\n return sum;\n }\n const value = parseFloat(balance.total ?? '0');\n return Number.isFinite(value) ? sum + value : sum;\n },\n 0,\n );\n}\n\nexport function getSpotHold(\n spotState?: SpotClearinghouseStateResponse | null,\n): number {\n if (!spotState?.balances || !Array.isArray(spotState.balances)) {\n return 0;\n }\n\n return spotState.balances.reduce(\n (sum: number, balance: { coin?: string; hold?: string }) => {\n if (!balance.coin || !SPOT_COLLATERAL_COINS.has(balance.coin)) {\n return sum;\n }\n const value = parseFloat(balance.hold ?? '0');\n return Number.isFinite(value) ? sum + value : sum;\n },\n 0,\n );\n}\n\nexport function addSpotBalanceToAccountState(\n accountState: AccountState,\n spotState?: SpotClearinghouseStateResponse | null,\n): AccountState {\n const spotBalance = getSpotBalance(spotState);\n const spotHold = getSpotHold(spotState);\n const freeSpot = Math.max(0, spotBalance - spotHold);\n\n const currentTotal = parseFloat(accountState.totalBalance);\n const currentAvailable = parseFloat(accountState.availableBalance);\n\n // Preserve sentinel totals (e.g. PERPS_CONSTANTS.FallbackDataDisplay '--')\n // rather than coercing them to NaN.\n if (!Number.isFinite(currentTotal)) {\n return accountState;\n }\n\n if (spotBalance === 0) {\n return {\n ...accountState,\n availableToTradeBalance: Number.isFinite(currentAvailable)\n ? currentAvailable.toString()\n : accountState.availableBalance,\n };\n }\n\n const availableToTrade = Number.isFinite(currentAvailable)\n ? (currentAvailable + freeSpot).toString()\n : freeSpot.toString();\n\n // Subtract spotHold to avoid double-counting on Unified/PM accounts:\n // marginSummary.accountValue already includes the margin that HL\n // surfaces via spot.hold. Standard mode has spotHold = 0, no-op.\n const nextTotal = currentTotal + spotBalance - spotHold;\n\n return {\n ...accountState,\n totalBalance: nextTotal.toString(),\n availableToTradeBalance: availableToTrade,\n };\n}\n\n/**\n * Aggregate multiple per-DEX AccountState objects into one by summing numeric fields.\n * ROE is recalculated as (totalUnrealizedPnl / totalMarginUsed) * 100.\n *\n * @param states - The array of per-DEX account states to aggregate.\n * @returns The combined account state with summed balances and recalculated ROE.\n */\nexport function aggregateAccountStates(states: AccountState[]): AccountState {\n const fallback: AccountState = {\n availableBalance: PERPS_CONSTANTS.FallbackDataDisplay,\n totalBalance: PERPS_CONSTANTS.FallbackDataDisplay,\n marginUsed: PERPS_CONSTANTS.FallbackDataDisplay,\n unrealizedPnl: PERPS_CONSTANTS.FallbackDataDisplay,\n returnOnEquity: PERPS_CONSTANTS.FallbackDataDisplay,\n };\n\n if (states.length === 0) {\n return fallback;\n }\n\n const aggregated = states.reduce<AccountState>((acc, state, index) => {\n if (index === 0) {\n return { ...state };\n }\n const accAvailableToTrade = parseFloat(\n acc.availableToTradeBalance ?? acc.availableBalance,\n );\n const stateAvailableToTrade = parseFloat(\n state.availableToTradeBalance ?? state.availableBalance,\n );\n const availableToTradeSum =\n Number.isFinite(accAvailableToTrade) &&\n Number.isFinite(stateAvailableToTrade)\n ? (accAvailableToTrade + stateAvailableToTrade).toString()\n : undefined;\n\n return {\n availableBalance: (\n parseFloat(acc.availableBalance) + parseFloat(state.availableBalance)\n ).toString(),\n ...(availableToTradeSum !== undefined && {\n availableToTradeBalance: availableToTradeSum,\n }),\n totalBalance: (\n parseFloat(acc.totalBalance) + parseFloat(state.totalBalance)\n ).toString(),\n marginUsed: (\n parseFloat(acc.marginUsed) + parseFloat(state.marginUsed)\n ).toString(),\n unrealizedPnl: (\n parseFloat(acc.unrealizedPnl) + parseFloat(state.unrealizedPnl)\n ).toString(),\n returnOnEquity: '0',\n };\n }, fallback);\n\n // Recalculate ROE across all DEXs\n const totalMarginUsed = parseFloat(aggregated.marginUsed);\n const totalUnrealizedPnl = parseFloat(aggregated.unrealizedPnl);\n if (totalMarginUsed > 0) {\n aggregated.returnOnEquity = (\n (totalUnrealizedPnl / totalMarginUsed) *\n 100\n ).toString();\n } else {\n aggregated.returnOnEquity = '0';\n }\n\n return aggregated;\n}\n"]}
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import type { InternalAccount } from "@metamask/keyring-internal-api";
6
6
  import type { AccountState, PerpsInternalAccount } from "../types/index.cjs";
7
+ import type { SpotClearinghouseStateResponse } from "../types/hyperliquid-types.cjs";
7
8
  export declare function findEvmAccount(accounts: (InternalAccount | PerpsInternalAccount)[]): {
8
9
  address: string;
9
10
  type: string;
@@ -19,6 +20,9 @@ export type ReturnOnEquityInput = {
19
20
  returnOnEquity: string | number;
20
21
  };
21
22
  export declare function calculateWeightedReturnOnEquity(accounts: ReturnOnEquityInput[]): string;
23
+ export declare function getSpotBalance(spotState?: SpotClearinghouseStateResponse | null): number;
24
+ export declare function getSpotHold(spotState?: SpotClearinghouseStateResponse | null): number;
25
+ export declare function addSpotBalanceToAccountState(accountState: AccountState, spotState?: SpotClearinghouseStateResponse | null): AccountState;
22
26
  /**
23
27
  * Aggregate multiple per-DEX AccountState objects into one by summing numeric fields.
24
28
  * ROE is recalculated as (totalUnrealizedPnl / totalMarginUsed) * 100.
@@ -1 +1 @@
1
- {"version":3,"file":"accountUtils.d.cts","sourceRoot":"","sources":["../../src/utils/accountUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,uCAAuC;AAGtE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,2BAAiB;AAQnE,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAM1C;AAED,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAGjC;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAEjC;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC;CACjC,CAAC;AAEF,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,MAAM,CA4CR;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CA+C3E"}
1
+ {"version":3,"file":"accountUtils.d.cts","sourceRoot":"","sources":["../../src/utils/accountUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,uCAAuC;AAGtE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,2BAAiB;AACnE,OAAO,KAAK,EAAE,8BAA8B,EAAE,uCAAmC;AAQjF,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAM1C;AAED,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAGjC;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAEjC;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC;CACjC,CAAC;AAEF,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,MAAM,CA4CR;AAQD,wBAAgB,cAAc,CAC5B,SAAS,CAAC,EAAE,8BAA8B,GAAG,IAAI,GAChD,MAAM,CAeR;AAED,wBAAgB,WAAW,CACzB,SAAS,CAAC,EAAE,8BAA8B,GAAG,IAAI,GAChD,MAAM,CAeR;AAED,wBAAgB,4BAA4B,CAC1C,YAAY,EAAE,YAAY,EAC1B,SAAS,CAAC,EAAE,8BAA8B,GAAG,IAAI,GAChD,YAAY,CAqCd;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CA8D3E"}
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import type { InternalAccount } from "@metamask/keyring-internal-api";
6
6
  import type { AccountState, PerpsInternalAccount } from "../types/index.mjs";
7
+ import type { SpotClearinghouseStateResponse } from "../types/hyperliquid-types.mjs";
7
8
  export declare function findEvmAccount(accounts: (InternalAccount | PerpsInternalAccount)[]): {
8
9
  address: string;
9
10
  type: string;
@@ -19,6 +20,9 @@ export type ReturnOnEquityInput = {
19
20
  returnOnEquity: string | number;
20
21
  };
21
22
  export declare function calculateWeightedReturnOnEquity(accounts: ReturnOnEquityInput[]): string;
23
+ export declare function getSpotBalance(spotState?: SpotClearinghouseStateResponse | null): number;
24
+ export declare function getSpotHold(spotState?: SpotClearinghouseStateResponse | null): number;
25
+ export declare function addSpotBalanceToAccountState(accountState: AccountState, spotState?: SpotClearinghouseStateResponse | null): AccountState;
22
26
  /**
23
27
  * Aggregate multiple per-DEX AccountState objects into one by summing numeric fields.
24
28
  * ROE is recalculated as (totalUnrealizedPnl / totalMarginUsed) * 100.
@@ -1 +1 @@
1
- {"version":3,"file":"accountUtils.d.mts","sourceRoot":"","sources":["../../src/utils/accountUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,uCAAuC;AAGtE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,2BAAiB;AAQnE,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAM1C;AAED,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAGjC;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAEjC;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC;CACjC,CAAC;AAEF,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,MAAM,CA4CR;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CA+C3E"}
1
+ {"version":3,"file":"accountUtils.d.mts","sourceRoot":"","sources":["../../src/utils/accountUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,uCAAuC;AAGtE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,2BAAiB;AACnE,OAAO,KAAK,EAAE,8BAA8B,EAAE,uCAAmC;AAQjF,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAM1C;AAED,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAGjC;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,CAAC,eAAe,GAAG,oBAAoB,CAAC,EAAE,GACnD;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAEjC;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC;CACjC,CAAC;AAEF,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,MAAM,CA4CR;AAQD,wBAAgB,cAAc,CAC5B,SAAS,CAAC,EAAE,8BAA8B,GAAG,IAAI,GAChD,MAAM,CAeR;AAED,wBAAgB,WAAW,CACzB,SAAS,CAAC,EAAE,8BAA8B,GAAG,IAAI,GAChD,MAAM,CAeR;AAED,wBAAgB,4BAA4B,CAC1C,YAAY,EAAE,YAAY,EAC1B,SAAS,CAAC,EAAE,8BAA8B,GAAG,IAAI,GAChD,YAAY,CAqCd;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CA8D3E"}
@@ -47,6 +47,67 @@ export function calculateWeightedReturnOnEquity(accounts) {
47
47
  const weightedROE = (totalWeightedROE / totalMarginUsed) * 100;
48
48
  return weightedROE.toString();
49
49
  }
50
+ // Spot coins counted toward currently supported funded-state gating.
51
+ // Today the in-app HyperLiquid market surface is USDC-collateralized only,
52
+ // so USDH must not inflate the shared funded-state path that hides Add Funds.
53
+ // Non-stablecoin spot assets (HYPE, PURR, …) also remain excluded.
54
+ const SPOT_COLLATERAL_COINS = new Set(['USDC']);
55
+ export function getSpotBalance(spotState) {
56
+ if (!spotState?.balances || !Array.isArray(spotState.balances)) {
57
+ return 0;
58
+ }
59
+ return spotState.balances.reduce((sum, balance) => {
60
+ if (!balance.coin || !SPOT_COLLATERAL_COINS.has(balance.coin)) {
61
+ return sum;
62
+ }
63
+ const value = parseFloat(balance.total ?? '0');
64
+ return Number.isFinite(value) ? sum + value : sum;
65
+ }, 0);
66
+ }
67
+ export function getSpotHold(spotState) {
68
+ if (!spotState?.balances || !Array.isArray(spotState.balances)) {
69
+ return 0;
70
+ }
71
+ return spotState.balances.reduce((sum, balance) => {
72
+ if (!balance.coin || !SPOT_COLLATERAL_COINS.has(balance.coin)) {
73
+ return sum;
74
+ }
75
+ const value = parseFloat(balance.hold ?? '0');
76
+ return Number.isFinite(value) ? sum + value : sum;
77
+ }, 0);
78
+ }
79
+ export function addSpotBalanceToAccountState(accountState, spotState) {
80
+ const spotBalance = getSpotBalance(spotState);
81
+ const spotHold = getSpotHold(spotState);
82
+ const freeSpot = Math.max(0, spotBalance - spotHold);
83
+ const currentTotal = parseFloat(accountState.totalBalance);
84
+ const currentAvailable = parseFloat(accountState.availableBalance);
85
+ // Preserve sentinel totals (e.g. PERPS_CONSTANTS.FallbackDataDisplay '--')
86
+ // rather than coercing them to NaN.
87
+ if (!Number.isFinite(currentTotal)) {
88
+ return accountState;
89
+ }
90
+ if (spotBalance === 0) {
91
+ return {
92
+ ...accountState,
93
+ availableToTradeBalance: Number.isFinite(currentAvailable)
94
+ ? currentAvailable.toString()
95
+ : accountState.availableBalance,
96
+ };
97
+ }
98
+ const availableToTrade = Number.isFinite(currentAvailable)
99
+ ? (currentAvailable + freeSpot).toString()
100
+ : freeSpot.toString();
101
+ // Subtract spotHold to avoid double-counting on Unified/PM accounts:
102
+ // marginSummary.accountValue already includes the margin that HL
103
+ // surfaces via spot.hold. Standard mode has spotHold = 0, no-op.
104
+ const nextTotal = currentTotal + spotBalance - spotHold;
105
+ return {
106
+ ...accountState,
107
+ totalBalance: nextTotal.toString(),
108
+ availableToTradeBalance: availableToTrade,
109
+ };
110
+ }
50
111
  /**
51
112
  * Aggregate multiple per-DEX AccountState objects into one by summing numeric fields.
52
113
  * ROE is recalculated as (totalUnrealizedPnl / totalMarginUsed) * 100.
@@ -69,8 +130,17 @@ export function aggregateAccountStates(states) {
69
130
  if (index === 0) {
70
131
  return { ...state };
71
132
  }
133
+ const accAvailableToTrade = parseFloat(acc.availableToTradeBalance ?? acc.availableBalance);
134
+ const stateAvailableToTrade = parseFloat(state.availableToTradeBalance ?? state.availableBalance);
135
+ const availableToTradeSum = Number.isFinite(accAvailableToTrade) &&
136
+ Number.isFinite(stateAvailableToTrade)
137
+ ? (accAvailableToTrade + stateAvailableToTrade).toString()
138
+ : undefined;
72
139
  return {
73
140
  availableBalance: (parseFloat(acc.availableBalance) + parseFloat(state.availableBalance)).toString(),
141
+ ...(availableToTradeSum !== undefined && {
142
+ availableToTradeBalance: availableToTradeSum,
143
+ }),
74
144
  totalBalance: (parseFloat(acc.totalBalance) + parseFloat(state.totalBalance)).toString(),
75
145
  marginUsed: (parseFloat(acc.marginUsed) + parseFloat(state.marginUsed)).toString(),
76
146
  unrealizedPnl: (parseFloat(acc.unrealizedPnl) + parseFloat(state.unrealizedPnl)).toString(),
@@ -1 +1 @@
1
- {"version":3,"file":"accountUtils.mjs","sourceRoot":"","sources":["../../src/utils/accountUtils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Account utilities for Perps components\n * Handles account selection and EVM account filtering\n */\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\n\nimport { PERPS_CONSTANTS } from '../constants/perpsConfig';\nimport type { AccountState, PerpsInternalAccount } from '../types';\n\nconst EVM_ACCOUNT_TYPES = new Set(['eip155:eoa', 'eip155:erc4337']);\n\nfunction isEvmAccountType(type: string): boolean {\n return EVM_ACCOUNT_TYPES.has(type);\n}\n\nexport function findEvmAccount(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string; type: string } | null {\n const evmAccount = accounts.find(\n (account) =>\n account && isEvmAccountType(account.type as InternalAccount['type']),\n );\n return evmAccount ?? null;\n}\n\nexport function getEvmAccountFromAccountGroup(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string } | undefined {\n const evmAccount = findEvmAccount(accounts);\n return evmAccount ? { address: evmAccount.address } : undefined;\n}\n\nexport function getSelectedEvmAccount(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string } | undefined {\n return getEvmAccountFromAccountGroup(accounts);\n}\n\nexport type ReturnOnEquityInput = {\n unrealizedPnl: string | number;\n returnOnEquity: string | number;\n};\n\nexport function calculateWeightedReturnOnEquity(\n accounts: ReturnOnEquityInput[],\n): string {\n if (accounts.length === 0) {\n return '0';\n }\n\n let totalWeightedROE = 0;\n let totalMarginUsed = 0;\n\n for (const account of accounts) {\n const unrealizedPnl =\n typeof account.unrealizedPnl === 'string'\n ? Number.parseFloat(account.unrealizedPnl)\n : account.unrealizedPnl;\n const returnOnEquity =\n typeof account.returnOnEquity === 'string'\n ? Number.parseFloat(account.returnOnEquity)\n : account.returnOnEquity;\n\n if (Number.isNaN(unrealizedPnl) || Number.isNaN(returnOnEquity)) {\n continue;\n }\n\n if (returnOnEquity === 0) {\n continue;\n }\n\n const marginUsed = (unrealizedPnl / returnOnEquity) * 100;\n\n if (Number.isNaN(marginUsed) || marginUsed <= 0) {\n continue;\n }\n\n const roeDecimal = returnOnEquity / 100;\n\n totalWeightedROE += roeDecimal * marginUsed;\n totalMarginUsed += marginUsed;\n }\n\n if (totalMarginUsed <= 0) {\n return '0';\n }\n\n const weightedROE = (totalWeightedROE / totalMarginUsed) * 100;\n return weightedROE.toString();\n}\n\n/**\n * Aggregate multiple per-DEX AccountState objects into one by summing numeric fields.\n * ROE is recalculated as (totalUnrealizedPnl / totalMarginUsed) * 100.\n *\n * @param states - The array of per-DEX account states to aggregate.\n * @returns The combined account state with summed balances and recalculated ROE.\n */\nexport function aggregateAccountStates(states: AccountState[]): AccountState {\n const fallback: AccountState = {\n availableBalance: PERPS_CONSTANTS.FallbackDataDisplay,\n totalBalance: PERPS_CONSTANTS.FallbackDataDisplay,\n marginUsed: PERPS_CONSTANTS.FallbackDataDisplay,\n unrealizedPnl: PERPS_CONSTANTS.FallbackDataDisplay,\n returnOnEquity: PERPS_CONSTANTS.FallbackDataDisplay,\n };\n\n if (states.length === 0) {\n return fallback;\n }\n\n const aggregated = states.reduce<AccountState>((acc, state, index) => {\n if (index === 0) {\n return { ...state };\n }\n return {\n availableBalance: (\n parseFloat(acc.availableBalance) + parseFloat(state.availableBalance)\n ).toString(),\n totalBalance: (\n parseFloat(acc.totalBalance) + parseFloat(state.totalBalance)\n ).toString(),\n marginUsed: (\n parseFloat(acc.marginUsed) + parseFloat(state.marginUsed)\n ).toString(),\n unrealizedPnl: (\n parseFloat(acc.unrealizedPnl) + parseFloat(state.unrealizedPnl)\n ).toString(),\n returnOnEquity: '0',\n };\n }, fallback);\n\n // Recalculate ROE across all DEXs\n const totalMarginUsed = parseFloat(aggregated.marginUsed);\n const totalUnrealizedPnl = parseFloat(aggregated.unrealizedPnl);\n if (totalMarginUsed > 0) {\n aggregated.returnOnEquity = (\n (totalUnrealizedPnl / totalMarginUsed) *\n 100\n ).toString();\n } else {\n aggregated.returnOnEquity = '0';\n }\n\n return aggregated;\n}\n"]}
1
+ {"version":3,"file":"accountUtils.mjs","sourceRoot":"","sources":["../../src/utils/accountUtils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Account utilities for Perps components\n * Handles account selection and EVM account filtering\n */\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\n\nimport { PERPS_CONSTANTS } from '../constants/perpsConfig';\nimport type { AccountState, PerpsInternalAccount } from '../types';\nimport type { SpotClearinghouseStateResponse } from '../types/hyperliquid-types';\n\nconst EVM_ACCOUNT_TYPES = new Set(['eip155:eoa', 'eip155:erc4337']);\n\nfunction isEvmAccountType(type: string): boolean {\n return EVM_ACCOUNT_TYPES.has(type);\n}\n\nexport function findEvmAccount(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string; type: string } | null {\n const evmAccount = accounts.find(\n (account) =>\n account && isEvmAccountType(account.type as InternalAccount['type']),\n );\n return evmAccount ?? null;\n}\n\nexport function getEvmAccountFromAccountGroup(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string } | undefined {\n const evmAccount = findEvmAccount(accounts);\n return evmAccount ? { address: evmAccount.address } : undefined;\n}\n\nexport function getSelectedEvmAccount(\n accounts: (InternalAccount | PerpsInternalAccount)[],\n): { address: string } | undefined {\n return getEvmAccountFromAccountGroup(accounts);\n}\n\nexport type ReturnOnEquityInput = {\n unrealizedPnl: string | number;\n returnOnEquity: string | number;\n};\n\nexport function calculateWeightedReturnOnEquity(\n accounts: ReturnOnEquityInput[],\n): string {\n if (accounts.length === 0) {\n return '0';\n }\n\n let totalWeightedROE = 0;\n let totalMarginUsed = 0;\n\n for (const account of accounts) {\n const unrealizedPnl =\n typeof account.unrealizedPnl === 'string'\n ? Number.parseFloat(account.unrealizedPnl)\n : account.unrealizedPnl;\n const returnOnEquity =\n typeof account.returnOnEquity === 'string'\n ? Number.parseFloat(account.returnOnEquity)\n : account.returnOnEquity;\n\n if (Number.isNaN(unrealizedPnl) || Number.isNaN(returnOnEquity)) {\n continue;\n }\n\n if (returnOnEquity === 0) {\n continue;\n }\n\n const marginUsed = (unrealizedPnl / returnOnEquity) * 100;\n\n if (Number.isNaN(marginUsed) || marginUsed <= 0) {\n continue;\n }\n\n const roeDecimal = returnOnEquity / 100;\n\n totalWeightedROE += roeDecimal * marginUsed;\n totalMarginUsed += marginUsed;\n }\n\n if (totalMarginUsed <= 0) {\n return '0';\n }\n\n const weightedROE = (totalWeightedROE / totalMarginUsed) * 100;\n return weightedROE.toString();\n}\n\n// Spot coins counted toward currently supported funded-state gating.\n// Today the in-app HyperLiquid market surface is USDC-collateralized only,\n// so USDH must not inflate the shared funded-state path that hides Add Funds.\n// Non-stablecoin spot assets (HYPE, PURR, …) also remain excluded.\nconst SPOT_COLLATERAL_COINS = new Set<string>(['USDC']);\n\nexport function getSpotBalance(\n spotState?: SpotClearinghouseStateResponse | null,\n): number {\n if (!spotState?.balances || !Array.isArray(spotState.balances)) {\n return 0;\n }\n\n return spotState.balances.reduce(\n (sum: number, balance: { coin?: string; total?: string }) => {\n if (!balance.coin || !SPOT_COLLATERAL_COINS.has(balance.coin)) {\n return sum;\n }\n const value = parseFloat(balance.total ?? '0');\n return Number.isFinite(value) ? sum + value : sum;\n },\n 0,\n );\n}\n\nexport function getSpotHold(\n spotState?: SpotClearinghouseStateResponse | null,\n): number {\n if (!spotState?.balances || !Array.isArray(spotState.balances)) {\n return 0;\n }\n\n return spotState.balances.reduce(\n (sum: number, balance: { coin?: string; hold?: string }) => {\n if (!balance.coin || !SPOT_COLLATERAL_COINS.has(balance.coin)) {\n return sum;\n }\n const value = parseFloat(balance.hold ?? '0');\n return Number.isFinite(value) ? sum + value : sum;\n },\n 0,\n );\n}\n\nexport function addSpotBalanceToAccountState(\n accountState: AccountState,\n spotState?: SpotClearinghouseStateResponse | null,\n): AccountState {\n const spotBalance = getSpotBalance(spotState);\n const spotHold = getSpotHold(spotState);\n const freeSpot = Math.max(0, spotBalance - spotHold);\n\n const currentTotal = parseFloat(accountState.totalBalance);\n const currentAvailable = parseFloat(accountState.availableBalance);\n\n // Preserve sentinel totals (e.g. PERPS_CONSTANTS.FallbackDataDisplay '--')\n // rather than coercing them to NaN.\n if (!Number.isFinite(currentTotal)) {\n return accountState;\n }\n\n if (spotBalance === 0) {\n return {\n ...accountState,\n availableToTradeBalance: Number.isFinite(currentAvailable)\n ? currentAvailable.toString()\n : accountState.availableBalance,\n };\n }\n\n const availableToTrade = Number.isFinite(currentAvailable)\n ? (currentAvailable + freeSpot).toString()\n : freeSpot.toString();\n\n // Subtract spotHold to avoid double-counting on Unified/PM accounts:\n // marginSummary.accountValue already includes the margin that HL\n // surfaces via spot.hold. Standard mode has spotHold = 0, no-op.\n const nextTotal = currentTotal + spotBalance - spotHold;\n\n return {\n ...accountState,\n totalBalance: nextTotal.toString(),\n availableToTradeBalance: availableToTrade,\n };\n}\n\n/**\n * Aggregate multiple per-DEX AccountState objects into one by summing numeric fields.\n * ROE is recalculated as (totalUnrealizedPnl / totalMarginUsed) * 100.\n *\n * @param states - The array of per-DEX account states to aggregate.\n * @returns The combined account state with summed balances and recalculated ROE.\n */\nexport function aggregateAccountStates(states: AccountState[]): AccountState {\n const fallback: AccountState = {\n availableBalance: PERPS_CONSTANTS.FallbackDataDisplay,\n totalBalance: PERPS_CONSTANTS.FallbackDataDisplay,\n marginUsed: PERPS_CONSTANTS.FallbackDataDisplay,\n unrealizedPnl: PERPS_CONSTANTS.FallbackDataDisplay,\n returnOnEquity: PERPS_CONSTANTS.FallbackDataDisplay,\n };\n\n if (states.length === 0) {\n return fallback;\n }\n\n const aggregated = states.reduce<AccountState>((acc, state, index) => {\n if (index === 0) {\n return { ...state };\n }\n const accAvailableToTrade = parseFloat(\n acc.availableToTradeBalance ?? acc.availableBalance,\n );\n const stateAvailableToTrade = parseFloat(\n state.availableToTradeBalance ?? state.availableBalance,\n );\n const availableToTradeSum =\n Number.isFinite(accAvailableToTrade) &&\n Number.isFinite(stateAvailableToTrade)\n ? (accAvailableToTrade + stateAvailableToTrade).toString()\n : undefined;\n\n return {\n availableBalance: (\n parseFloat(acc.availableBalance) + parseFloat(state.availableBalance)\n ).toString(),\n ...(availableToTradeSum !== undefined && {\n availableToTradeBalance: availableToTradeSum,\n }),\n totalBalance: (\n parseFloat(acc.totalBalance) + parseFloat(state.totalBalance)\n ).toString(),\n marginUsed: (\n parseFloat(acc.marginUsed) + parseFloat(state.marginUsed)\n ).toString(),\n unrealizedPnl: (\n parseFloat(acc.unrealizedPnl) + parseFloat(state.unrealizedPnl)\n ).toString(),\n returnOnEquity: '0',\n };\n }, fallback);\n\n // Recalculate ROE across all DEXs\n const totalMarginUsed = parseFloat(aggregated.marginUsed);\n const totalUnrealizedPnl = parseFloat(aggregated.unrealizedPnl);\n if (totalMarginUsed > 0) {\n aggregated.returnOnEquity = (\n (totalUnrealizedPnl / totalMarginUsed) *\n 100\n ).toString();\n } else {\n aggregated.returnOnEquity = '0';\n }\n\n return aggregated;\n}\n"]}
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resetPerpsRestCacheForTests = exports.coalescePerpsRestRequest = void 0;
4
+ const perpsConfig_1 = require("../constants/perpsConfig.cjs");
5
+ const inflight = new Map();
6
+ const cache = new Map();
7
+ /**
8
+ * Coalesce an idempotent perps REST call.
9
+ *
10
+ * - If a fresh cached value exists for `key`, it is returned immediately.
11
+ * - If an in-flight promise exists for `key`, it is shared with the caller.
12
+ * - Otherwise, `fetcher` runs once; the result populates the cache for `ttlMs`.
13
+ *
14
+ * `forceRefresh` skips both cache and in-flight dedup.
15
+ *
16
+ * @param key - Stable cache key identifying this logical REST call.
17
+ * @param fetcher - Thunk that performs the REST call when no cache/inflight hit.
18
+ * @param options - Optional overrides for TTL and forceRefresh behavior.
19
+ * @returns The fetched (or cached) value.
20
+ */
21
+ function coalescePerpsRestRequest(key, fetcher, options = {}) {
22
+ const ttlMs = options.ttlMs ?? perpsConfig_1.PERFORMANCE_CONFIG.PerpsRestCoalesceTtlMs;
23
+ const forceRefresh = options.forceRefresh ?? false;
24
+ if (forceRefresh) {
25
+ cache.delete(key);
26
+ }
27
+ else {
28
+ const now = Date.now();
29
+ const cached = cache.get(key);
30
+ if (cached) {
31
+ if (cached.expiresAt > now) {
32
+ return Promise.resolve(cached.value);
33
+ }
34
+ // Evict expired entry so callers with per-call-unique keys (e.g.
35
+ // CandleStreamChannel historical paging with per-page endTime) do
36
+ // not accumulate dead blobs for the life of the process.
37
+ cache.delete(key);
38
+ }
39
+ const existing = inflight.get(key);
40
+ if (existing !== undefined) {
41
+ return existing;
42
+ }
43
+ }
44
+ const run = fetcher().then((value) => {
45
+ // Only the currently-tracked in-flight promise writes to cache. A stale
46
+ // in-flight (e.g. one that was racing a later forceRefresh=true caller)
47
+ // must not clobber the fresh value once it finally resolves.
48
+ if (inflight.get(key) === run) {
49
+ cache.set(key, { value, expiresAt: Date.now() + ttlMs });
50
+ inflight.delete(key);
51
+ }
52
+ return value;
53
+ }, (error) => {
54
+ if (inflight.get(key) === run) {
55
+ inflight.delete(key);
56
+ }
57
+ throw error;
58
+ });
59
+ inflight.set(key, run);
60
+ return run;
61
+ }
62
+ exports.coalescePerpsRestRequest = coalescePerpsRestRequest;
63
+ /**
64
+ * Test-only: wipe all cached entries and in-flight promises.
65
+ */
66
+ function resetPerpsRestCacheForTests() {
67
+ cache.clear();
68
+ inflight.clear();
69
+ }
70
+ exports.resetPerpsRestCacheForTests = resetPerpsRestCacheForTests;
71
+ //# sourceMappingURL=coalescePerpsRestRequest.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalescePerpsRestRequest.cjs","sourceRoot":"","sources":["../../src/utils/coalescePerpsRestRequest.ts"],"names":[],"mappings":";;;AAAA,8DAA8D;AAkB9D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAC;AACrD,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+B,CAAC;AAerD;;;;;;;;;;;;;GAaG;AACH,SAAgB,wBAAwB,CACtC,GAAW,EACX,OAA8B,EAC9B,UAA2B,EAAE;IAE7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,gCAAkB,CAAC,sBAAsB,CAAC;IACzE,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,KAAK,CAAC;IAEnD,IAAI,YAAY,EAAE,CAAC;QACjB,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAmC,CAAC;QAChE,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,MAAM,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;gBAC3B,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvC,CAAC;YACD,iEAAiE;YACjE,kEAAkE;YAClE,yDAAyD;YACzD,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;QACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAgC,CAAC;QAClE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC,IAAI,CACxB,CAAC,KAAK,EAAE,EAAE;QACR,wEAAwE;QACxE,wEAAwE;QACxE,6DAA6D;QAC7D,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC;YAC9B,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;YACzD,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,EACD,CAAC,KAAK,EAAE,EAAE;QACR,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC;YAC9B,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC,CACF,CAAC;IAEF,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACvB,OAAO,GAAG,CAAC;AACb,CAAC;AAjDD,4DAiDC;AAED;;GAEG;AACH,SAAgB,2BAA2B;IACzC,KAAK,CAAC,KAAK,EAAE,CAAC;IACd,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AAHD,kEAGC","sourcesContent":["import { PERFORMANCE_CONFIG } from '../constants/perpsConfig';\n\n// Rapid perps market switching (candle bridge + activity tab burst) drives\n// duplicate REST calls against api.hyperliquid.xyz and occasionally trips 429.\n// This helper collapses concurrent identical calls into one in-flight promise\n// and serves a short TTL cache to absorb the burst. Explicit refresh bypasses\n// the cache so user-initiated refetch still re-runs.\n//\n// Lives at the service layer (MarketDataService) rather than per-hook so every\n// current and future caller — hooks, controller, aggregated provider — is\n// deduped automatically. Centralized at MarketDataService because it is\n// already a single choke point.\n\ntype CacheEntry<TValue> = {\n value: TValue;\n expiresAt: number;\n};\n\nconst inflight = new Map<string, Promise<unknown>>();\nconst cache = new Map<string, CacheEntry<unknown>>();\n\nexport type CoalesceOptions = {\n /**\n * Cache TTL in milliseconds. Defaults to\n * {@link PERFORMANCE_CONFIG.PerpsRestCoalesceTtlMs}.\n */\n ttlMs?: number;\n /**\n * Bypass the cache and any in-flight promise. The fresh result will be\n * written back to the cache under the same key.\n */\n forceRefresh?: boolean;\n};\n\n/**\n * Coalesce an idempotent perps REST call.\n *\n * - If a fresh cached value exists for `key`, it is returned immediately.\n * - If an in-flight promise exists for `key`, it is shared with the caller.\n * - Otherwise, `fetcher` runs once; the result populates the cache for `ttlMs`.\n *\n * `forceRefresh` skips both cache and in-flight dedup.\n *\n * @param key - Stable cache key identifying this logical REST call.\n * @param fetcher - Thunk that performs the REST call when no cache/inflight hit.\n * @param options - Optional overrides for TTL and forceRefresh behavior.\n * @returns The fetched (or cached) value.\n */\nexport function coalescePerpsRestRequest<TValue>(\n key: string,\n fetcher: () => Promise<TValue>,\n options: CoalesceOptions = {},\n): Promise<TValue> {\n const ttlMs = options.ttlMs ?? PERFORMANCE_CONFIG.PerpsRestCoalesceTtlMs;\n const forceRefresh = options.forceRefresh ?? false;\n\n if (forceRefresh) {\n cache.delete(key);\n } else {\n const now = Date.now();\n const cached = cache.get(key) as CacheEntry<TValue> | undefined;\n if (cached) {\n if (cached.expiresAt > now) {\n return Promise.resolve(cached.value);\n }\n // Evict expired entry so callers with per-call-unique keys (e.g.\n // CandleStreamChannel historical paging with per-page endTime) do\n // not accumulate dead blobs for the life of the process.\n cache.delete(key);\n }\n const existing = inflight.get(key) as Promise<TValue> | undefined;\n if (existing !== undefined) {\n return existing;\n }\n }\n\n const run = fetcher().then(\n (value) => {\n // Only the currently-tracked in-flight promise writes to cache. A stale\n // in-flight (e.g. one that was racing a later forceRefresh=true caller)\n // must not clobber the fresh value once it finally resolves.\n if (inflight.get(key) === run) {\n cache.set(key, { value, expiresAt: Date.now() + ttlMs });\n inflight.delete(key);\n }\n return value;\n },\n (error) => {\n if (inflight.get(key) === run) {\n inflight.delete(key);\n }\n throw error;\n },\n );\n\n inflight.set(key, run);\n return run;\n}\n\n/**\n * Test-only: wipe all cached entries and in-flight promises.\n */\nexport function resetPerpsRestCacheForTests(): void {\n cache.clear();\n inflight.clear();\n}\n"]}
@@ -0,0 +1,32 @@
1
+ export type CoalesceOptions = {
2
+ /**
3
+ * Cache TTL in milliseconds. Defaults to
4
+ * {@link PERFORMANCE_CONFIG.PerpsRestCoalesceTtlMs}.
5
+ */
6
+ ttlMs?: number;
7
+ /**
8
+ * Bypass the cache and any in-flight promise. The fresh result will be
9
+ * written back to the cache under the same key.
10
+ */
11
+ forceRefresh?: boolean;
12
+ };
13
+ /**
14
+ * Coalesce an idempotent perps REST call.
15
+ *
16
+ * - If a fresh cached value exists for `key`, it is returned immediately.
17
+ * - If an in-flight promise exists for `key`, it is shared with the caller.
18
+ * - Otherwise, `fetcher` runs once; the result populates the cache for `ttlMs`.
19
+ *
20
+ * `forceRefresh` skips both cache and in-flight dedup.
21
+ *
22
+ * @param key - Stable cache key identifying this logical REST call.
23
+ * @param fetcher - Thunk that performs the REST call when no cache/inflight hit.
24
+ * @param options - Optional overrides for TTL and forceRefresh behavior.
25
+ * @returns The fetched (or cached) value.
26
+ */
27
+ export declare function coalescePerpsRestRequest<TValue>(key: string, fetcher: () => Promise<TValue>, options?: CoalesceOptions): Promise<TValue>;
28
+ /**
29
+ * Test-only: wipe all cached entries and in-flight promises.
30
+ */
31
+ export declare function resetPerpsRestCacheForTests(): void;
32
+ //# sourceMappingURL=coalescePerpsRestRequest.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalescePerpsRestRequest.d.cts","sourceRoot":"","sources":["../../src/utils/coalescePerpsRestRequest.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAC7C,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAC9B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CA6CjB;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAGlD"}
@@ -0,0 +1,32 @@
1
+ export type CoalesceOptions = {
2
+ /**
3
+ * Cache TTL in milliseconds. Defaults to
4
+ * {@link PERFORMANCE_CONFIG.PerpsRestCoalesceTtlMs}.
5
+ */
6
+ ttlMs?: number;
7
+ /**
8
+ * Bypass the cache and any in-flight promise. The fresh result will be
9
+ * written back to the cache under the same key.
10
+ */
11
+ forceRefresh?: boolean;
12
+ };
13
+ /**
14
+ * Coalesce an idempotent perps REST call.
15
+ *
16
+ * - If a fresh cached value exists for `key`, it is returned immediately.
17
+ * - If an in-flight promise exists for `key`, it is shared with the caller.
18
+ * - Otherwise, `fetcher` runs once; the result populates the cache for `ttlMs`.
19
+ *
20
+ * `forceRefresh` skips both cache and in-flight dedup.
21
+ *
22
+ * @param key - Stable cache key identifying this logical REST call.
23
+ * @param fetcher - Thunk that performs the REST call when no cache/inflight hit.
24
+ * @param options - Optional overrides for TTL and forceRefresh behavior.
25
+ * @returns The fetched (or cached) value.
26
+ */
27
+ export declare function coalescePerpsRestRequest<TValue>(key: string, fetcher: () => Promise<TValue>, options?: CoalesceOptions): Promise<TValue>;
28
+ /**
29
+ * Test-only: wipe all cached entries and in-flight promises.
30
+ */
31
+ export declare function resetPerpsRestCacheForTests(): void;
32
+ //# sourceMappingURL=coalescePerpsRestRequest.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalescePerpsRestRequest.d.mts","sourceRoot":"","sources":["../../src/utils/coalescePerpsRestRequest.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAC7C,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAC9B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CA6CjB;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAGlD"}
@@ -0,0 +1,66 @@
1
+ import { PERFORMANCE_CONFIG } from "../constants/perpsConfig.mjs";
2
+ const inflight = new Map();
3
+ const cache = new Map();
4
+ /**
5
+ * Coalesce an idempotent perps REST call.
6
+ *
7
+ * - If a fresh cached value exists for `key`, it is returned immediately.
8
+ * - If an in-flight promise exists for `key`, it is shared with the caller.
9
+ * - Otherwise, `fetcher` runs once; the result populates the cache for `ttlMs`.
10
+ *
11
+ * `forceRefresh` skips both cache and in-flight dedup.
12
+ *
13
+ * @param key - Stable cache key identifying this logical REST call.
14
+ * @param fetcher - Thunk that performs the REST call when no cache/inflight hit.
15
+ * @param options - Optional overrides for TTL and forceRefresh behavior.
16
+ * @returns The fetched (or cached) value.
17
+ */
18
+ export function coalescePerpsRestRequest(key, fetcher, options = {}) {
19
+ const ttlMs = options.ttlMs ?? PERFORMANCE_CONFIG.PerpsRestCoalesceTtlMs;
20
+ const forceRefresh = options.forceRefresh ?? false;
21
+ if (forceRefresh) {
22
+ cache.delete(key);
23
+ }
24
+ else {
25
+ const now = Date.now();
26
+ const cached = cache.get(key);
27
+ if (cached) {
28
+ if (cached.expiresAt > now) {
29
+ return Promise.resolve(cached.value);
30
+ }
31
+ // Evict expired entry so callers with per-call-unique keys (e.g.
32
+ // CandleStreamChannel historical paging with per-page endTime) do
33
+ // not accumulate dead blobs for the life of the process.
34
+ cache.delete(key);
35
+ }
36
+ const existing = inflight.get(key);
37
+ if (existing !== undefined) {
38
+ return existing;
39
+ }
40
+ }
41
+ const run = fetcher().then((value) => {
42
+ // Only the currently-tracked in-flight promise writes to cache. A stale
43
+ // in-flight (e.g. one that was racing a later forceRefresh=true caller)
44
+ // must not clobber the fresh value once it finally resolves.
45
+ if (inflight.get(key) === run) {
46
+ cache.set(key, { value, expiresAt: Date.now() + ttlMs });
47
+ inflight.delete(key);
48
+ }
49
+ return value;
50
+ }, (error) => {
51
+ if (inflight.get(key) === run) {
52
+ inflight.delete(key);
53
+ }
54
+ throw error;
55
+ });
56
+ inflight.set(key, run);
57
+ return run;
58
+ }
59
+ /**
60
+ * Test-only: wipe all cached entries and in-flight promises.
61
+ */
62
+ export function resetPerpsRestCacheForTests() {
63
+ cache.clear();
64
+ inflight.clear();
65
+ }
66
+ //# sourceMappingURL=coalescePerpsRestRequest.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalescePerpsRestRequest.mjs","sourceRoot":"","sources":["../../src/utils/coalescePerpsRestRequest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,qCAAiC;AAkB9D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAC;AACrD,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+B,CAAC;AAerD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,wBAAwB,CACtC,GAAW,EACX,OAA8B,EAC9B,UAA2B,EAAE;IAE7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,kBAAkB,CAAC,sBAAsB,CAAC;IACzE,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,KAAK,CAAC;IAEnD,IAAI,YAAY,EAAE,CAAC;QACjB,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAmC,CAAC;QAChE,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,MAAM,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;gBAC3B,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvC,CAAC;YACD,iEAAiE;YACjE,kEAAkE;YAClE,yDAAyD;YACzD,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;QACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAgC,CAAC;QAClE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC,IAAI,CACxB,CAAC,KAAK,EAAE,EAAE;QACR,wEAAwE;QACxE,wEAAwE;QACxE,6DAA6D;QAC7D,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC;YAC9B,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;YACzD,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,EACD,CAAC,KAAK,EAAE,EAAE;QACR,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC;YAC9B,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC,CACF,CAAC;IAEF,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACvB,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,2BAA2B;IACzC,KAAK,CAAC,KAAK,EAAE,CAAC;IACd,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC","sourcesContent":["import { PERFORMANCE_CONFIG } from '../constants/perpsConfig';\n\n// Rapid perps market switching (candle bridge + activity tab burst) drives\n// duplicate REST calls against api.hyperliquid.xyz and occasionally trips 429.\n// This helper collapses concurrent identical calls into one in-flight promise\n// and serves a short TTL cache to absorb the burst. Explicit refresh bypasses\n// the cache so user-initiated refetch still re-runs.\n//\n// Lives at the service layer (MarketDataService) rather than per-hook so every\n// current and future caller — hooks, controller, aggregated provider — is\n// deduped automatically. Centralized at MarketDataService because it is\n// already a single choke point.\n\ntype CacheEntry<TValue> = {\n value: TValue;\n expiresAt: number;\n};\n\nconst inflight = new Map<string, Promise<unknown>>();\nconst cache = new Map<string, CacheEntry<unknown>>();\n\nexport type CoalesceOptions = {\n /**\n * Cache TTL in milliseconds. Defaults to\n * {@link PERFORMANCE_CONFIG.PerpsRestCoalesceTtlMs}.\n */\n ttlMs?: number;\n /**\n * Bypass the cache and any in-flight promise. The fresh result will be\n * written back to the cache under the same key.\n */\n forceRefresh?: boolean;\n};\n\n/**\n * Coalesce an idempotent perps REST call.\n *\n * - If a fresh cached value exists for `key`, it is returned immediately.\n * - If an in-flight promise exists for `key`, it is shared with the caller.\n * - Otherwise, `fetcher` runs once; the result populates the cache for `ttlMs`.\n *\n * `forceRefresh` skips both cache and in-flight dedup.\n *\n * @param key - Stable cache key identifying this logical REST call.\n * @param fetcher - Thunk that performs the REST call when no cache/inflight hit.\n * @param options - Optional overrides for TTL and forceRefresh behavior.\n * @returns The fetched (or cached) value.\n */\nexport function coalescePerpsRestRequest<TValue>(\n key: string,\n fetcher: () => Promise<TValue>,\n options: CoalesceOptions = {},\n): Promise<TValue> {\n const ttlMs = options.ttlMs ?? PERFORMANCE_CONFIG.PerpsRestCoalesceTtlMs;\n const forceRefresh = options.forceRefresh ?? false;\n\n if (forceRefresh) {\n cache.delete(key);\n } else {\n const now = Date.now();\n const cached = cache.get(key) as CacheEntry<TValue> | undefined;\n if (cached) {\n if (cached.expiresAt > now) {\n return Promise.resolve(cached.value);\n }\n // Evict expired entry so callers with per-call-unique keys (e.g.\n // CandleStreamChannel historical paging with per-page endTime) do\n // not accumulate dead blobs for the life of the process.\n cache.delete(key);\n }\n const existing = inflight.get(key) as Promise<TValue> | undefined;\n if (existing !== undefined) {\n return existing;\n }\n }\n\n const run = fetcher().then(\n (value) => {\n // Only the currently-tracked in-flight promise writes to cache. A stale\n // in-flight (e.g. one that was racing a later forceRefresh=true caller)\n // must not clobber the fresh value once it finally resolves.\n if (inflight.get(key) === run) {\n cache.set(key, { value, expiresAt: Date.now() + ttlMs });\n inflight.delete(key);\n }\n return value;\n },\n (error) => {\n if (inflight.get(key) === run) {\n inflight.delete(key);\n }\n throw error;\n },\n );\n\n inflight.set(key, run);\n return run;\n}\n\n/**\n * Test-only: wipe all cached entries and in-flight promises.\n */\nexport function resetPerpsRestCacheForTests(): void {\n cache.clear();\n inflight.clear();\n}\n"]}
@@ -183,7 +183,11 @@ function adaptMarketFromSDK(sdkMarket) {
183
183
  };
184
184
  }
185
185
  exports.adaptMarketFromSDK = adaptMarketFromSDK;
186
- function adaptAccountStateFromSDK(perpsState, spotState) {
186
+ // Perps-only account adapter. Spot balances are layered on afterwards by
187
+ // addSpotBalanceToAccountState, which enforces the USDC-only policy via
188
+ // SPOT_COLLATERAL_COINS. Keeping spot logic out of here preserves a single
189
+ // source of truth for spot balance math.
190
+ function adaptAccountStateFromSDK(perpsState) {
187
191
  const { totalUnrealizedPnl, weightedReturnOnEquity } = perpsState.assetPositions.reduce((acc, assetPos) => {
188
192
  const unrealizedPnl = parseFloat(assetPos.position.unrealizedPnl || '0');
189
193
  const marginUsed = parseFloat(assetPos.position.marginUsed || '0');
@@ -200,14 +204,10 @@ function adaptAccountStateFromSDK(perpsState, spotState) {
200
204
  ? ((weightedReturnOnEquity / totalMarginUsed) * 100).toString()
201
205
  : '0';
202
206
  const perpsBalance = parseFloat(perpsState.marginSummary.accountValue);
203
- let spotBalance = 0;
204
- if (spotState?.balances && Array.isArray(spotState.balances)) {
205
- spotBalance = spotState.balances.reduce((sum, balance) => sum + parseFloat(balance.total ?? '0'), 0);
206
- }
207
- const totalBalance = (spotBalance + perpsBalance).toString();
208
207
  const accountState = {
209
208
  availableBalance: perpsState.withdrawable || '0',
210
- totalBalance: totalBalance || '0',
209
+ availableToTradeBalance: perpsState.withdrawable || '0',
210
+ totalBalance: perpsBalance.toString() || '0',
211
211
  marginUsed: perpsState.marginSummary.totalMarginUsed || '0',
212
212
  unrealizedPnl: totalUnrealizedPnl.toString() || '0',
213
213
  returnOnEquity: totalReturnOnEquityPercentage || '0',