diff --git a/app/components/UI/NftGrid/NftGrid.tsx b/app/components/UI/NftGrid/NftGrid.tsx
index a2a1c2645f8..5ab8946d327 100644
--- a/app/components/UI/NftGrid/NftGrid.tsx
+++ b/app/components/UI/NftGrid/NftGrid.tsx
@@ -54,11 +54,13 @@ const NftGridContent = ({
nftRowList,
goToAddCollectible,
isAddNFTEnabled,
+ isFullView = false,
}: {
allFilteredCollectibles: Nft[];
nftRowList: React.ReactNode;
goToAddCollectible: () => void;
isAddNFTEnabled: boolean;
+ isFullView?: boolean;
}) => {
const isNftFetchingProgress = useSelector(isNftFetchingProgressSelector);
@@ -67,7 +69,7 @@ const NftGridContent = ({
}
if (isNftFetchingProgress) {
- return ;
+ return ;
}
return (
@@ -300,6 +302,7 @@ const NftGrid = forwardRef(
nftRowList={nftRowList}
goToAddCollectible={goToAddCollectible}
isAddNFTEnabled={isAddNFTEnabled}
+ isFullView={isFullView}
/>
{/* View all NFTs button - shown when there are more items than maxItems */}
diff --git a/app/components/UI/NftGrid/NftGridSkeleton.tsx b/app/components/UI/NftGrid/NftGridSkeleton.tsx
index ebc5fa372a3..faba279c9c4 100644
--- a/app/components/UI/NftGrid/NftGridSkeleton.tsx
+++ b/app/components/UI/NftGrid/NftGridSkeleton.tsx
@@ -4,12 +4,12 @@ import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
import { useTheme } from '../../../util/theme';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
-const NftGridSkeleton = () => {
+const NftGridSkeleton = ({ isFullView = false }: { isFullView?: boolean }) => {
const { colors } = useTheme();
const tw = useTailwind();
return (
-
+
{
});
it('handles hex wei values correctly', () => {
- // 1 ETH in wei (0xde0b6b3a7640000)
- expect(convertPerpsAmountToUSD('0xde0b6b3a7640000')).toBe('$20,000');
+ // Hex wei is not supported in Perps flow; all inputs return '$0'
+ expect(convertPerpsAmountToUSD('0xde0b6b3a7640000')).toBe('$0');
});
it('handles numeric strings correctly', () => {
diff --git a/app/components/UI/Perps/utils/amountConversion.ts b/app/components/UI/Perps/utils/amountConversion.ts
index d54aec01868..9751ae6f38a 100644
--- a/app/components/UI/Perps/utils/amountConversion.ts
+++ b/app/components/UI/Perps/utils/amountConversion.ts
@@ -1,5 +1,4 @@
import { formatPerpsFiat } from '../utils/formatUtils';
-import BN from 'bnjs4';
import { ensureError } from '../../../../util/errorUtils';
import { PERPS_CONSTANTS, type PerpsLogger } from '@metamask/perps-controller';
@@ -13,7 +12,7 @@ export type AmountConversionLogger = PerpsLogger | undefined;
* Converts various amount formats to USD display string for Perps
* Uses existing Perps formatting utilities for consistency
*
- * @param amount - Amount in various formats (USD string, hex wei, or numeric string)
+ * @param amount - Amount in various formats (USD string or numeric string)
* @param logger - Optional logger for error reporting
* @returns Formatted USD string using Perps formatting standards
*/
@@ -33,18 +32,9 @@ export const convertPerpsAmountToUSD = (
return formatPerpsFiat(numericValue);
}
- // Check if it's a hex value (starts with 0x) - treat as wei
+ // Hex wei input is not supported in the Perps flow (all deposits use ERC-20 USDC)
if (amount.startsWith('0x')) {
- const weiBN = new BN(amount, 16);
- const ethBN = weiBN.div(new BN(10).pow(new BN(18)));
- const ethValue = ethBN.toNumber();
-
- // For now, use a placeholder ETH price since we don't have real-time price data
- // In a real implementation, this should come from a price feed
- const ethPriceUSD = 2000; // TODO: Replace with actual ETH price from price feed
- const usdValue = ethValue * ethPriceUSD;
- // Preserve decimals for converted amounts
- return formatPerpsFiat(usdValue);
+ return '$0';
}
// Otherwise, treat as a direct USD amount (e.g., "1.30" = $1.30)
diff --git a/app/components/Views/Homepage/Sections/Perpetuals/PerpsSection.test.tsx b/app/components/Views/Homepage/Sections/Perpetuals/PerpsSection.test.tsx
index 5b339468646..9c5ee0a1197 100644
--- a/app/components/Views/Homepage/Sections/Perpetuals/PerpsSection.test.tsx
+++ b/app/components/Views/Homepage/Sections/Perpetuals/PerpsSection.test.tsx
@@ -811,7 +811,7 @@ describe('PerpsSection', () => {
fireEvent.press(screen.getByTestId('perps-view-more-card'));
expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, {
- screen: Routes.PERPS.PERPS_HOME,
+ screen: Routes.PERPS.MARKET_LIST,
params: { source: 'home_section' },
});
});
diff --git a/app/components/Views/Homepage/Sections/Perpetuals/PerpsSection.tsx b/app/components/Views/Homepage/Sections/Perpetuals/PerpsSection.tsx
index 791c1a73caa..680f000973c 100644
--- a/app/components/Views/Homepage/Sections/Perpetuals/PerpsSection.tsx
+++ b/app/components/Views/Homepage/Sections/Perpetuals/PerpsSection.tsx
@@ -212,6 +212,13 @@ const PerpsSection = forwardRef(
});
}, [navigation]);
+ const handleViewMorePerps = useCallback(() => {
+ navigation.navigate(Routes.PERPS.ROOT, {
+ screen: Routes.PERPS.MARKET_LIST,
+ params: { source: PERPS_EVENT_VALUE.SOURCE.HOME_SECTION },
+ });
+ }, [navigation]);
+
const handlePositionPress = useCallback(
(position: Position) => {
track(MetaMetricsEvents.PERPS_UI_INTERACTION, {
@@ -330,7 +337,7 @@ const PerpsSection = forwardRef(
/>
))}
diff --git a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx
index ae8ae3b378b..f13926e0516 100644
--- a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx
+++ b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx
@@ -8,7 +8,7 @@ import { ScrollView, View } from 'react-native';
import { useNavigation, NavigationProp } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
-import { Box, TextVariant } from '@metamask/design-system-react-native';
+import { Box } from '@metamask/design-system-react-native';
import SectionTitle from '../../components/SectionTitle';
import ErrorState from '../../components/ErrorState';
import FadingScrollContainer from '../../components/FadingScrollContainer';
@@ -276,8 +276,7 @@ const PredictionsSection = forwardRef<
))}
>
)}
diff --git a/app/components/Views/Homepage/components/ErrorState/ErrorState.tsx b/app/components/Views/Homepage/components/ErrorState/ErrorState.tsx
index cf996c68751..a9c31fdd975 100644
--- a/app/components/Views/Homepage/components/ErrorState/ErrorState.tsx
+++ b/app/components/Views/Homepage/components/ErrorState/ErrorState.tsx
@@ -1,11 +1,8 @@
import React from 'react';
+import { Image } from 'react-native';
import {
Box,
Text,
- Icon,
- IconName,
- IconSize,
- IconColor,
TextVariant,
TextColor,
BoxAlignItems,
@@ -13,7 +10,11 @@ import {
ButtonVariant,
ButtonSize,
} from '@metamask/design-system-react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+import { useAssetFromTheme } from '../../../../../util/theme';
import { strings } from '../../../../../../locales/i18n';
+import errorStateLight from '../../../../../images/error-state-no-connection-light.png';
+import errorStateDark from '../../../../../images/error-state-no-connection-dark.png';
interface ErrorStateProps {
/** Text describing what failed to load (e.g., "Unable to load predictions") */
@@ -24,9 +25,12 @@ interface ErrorStateProps {
/**
* Generic error state for homepage sections.
- * Shows a wifi-off icon, error message, and a retry button.
+ * Shows a no-connection illustration, error message, and a retry button.
*/
const ErrorState: React.FC = ({ title, onRetry }) => {
+ const tw = useTailwind();
+ const noConnectionImage = useAssetFromTheme(errorStateLight, errorStateDark);
+
const handleRetry = () => {
try {
const result = onRetry();
@@ -42,10 +46,10 @@ const ErrorState: React.FC = ({ title, onRetry }) => {
return (
-
= ({
onPress,
@@ -41,22 +41,16 @@ const ViewMoreCard: React.FC = ({
testID={testID}
>
-
-
-
+
- | {
- type: 'PerpsController:placeOrder';
- handler: PerpsController['placeOrder'];
- }
- | {
- type: 'PerpsController:editOrder';
- handler: PerpsController['editOrder'];
- }
- | {
- type: 'PerpsController:cancelOrder';
- handler: PerpsController['cancelOrder'];
- }
- | {
- type: 'PerpsController:cancelOrders';
- handler: PerpsController['cancelOrders'];
- }
- | {
- type: 'PerpsController:closePosition';
- handler: PerpsController['closePosition'];
- }
- | {
- type: 'PerpsController:closePositions';
- handler: PerpsController['closePositions'];
- }
- | {
- type: 'PerpsController:withdraw';
- handler: PerpsController['withdraw'];
- }
- | {
- type: 'PerpsController:getPositions';
- handler: PerpsController['getPositions'];
- }
- | {
- type: 'PerpsController:getOrderFills';
- handler: PerpsController['getOrderFills'];
- }
- | {
- type: 'PerpsController:getOrders';
- handler: PerpsController['getOrders'];
- }
- | {
- type: 'PerpsController:getOpenOrders';
- handler: PerpsController['getOpenOrders'];
- }
- | {
- type: 'PerpsController:getFunding';
- handler: PerpsController['getFunding'];
- }
- | {
- type: 'PerpsController:getAccountState';
- handler: PerpsController['getAccountState'];
- }
- | {
- type: 'PerpsController:getMarkets';
- handler: PerpsController['getMarkets'];
- }
- | {
- type: 'PerpsController:refreshEligibility';
- handler: PerpsController['refreshEligibility'];
- }
- | {
- type: 'PerpsController:toggleTestnet';
- handler: PerpsController['toggleTestnet'];
- }
- | {
- type: 'PerpsController:disconnect';
- handler: PerpsController['disconnect'];
- }
- | {
- type: 'PerpsController:calculateFees';
- handler: PerpsController['calculateFees'];
- }
- | {
- type: 'PerpsController:markTutorialCompleted';
- handler: PerpsController['markTutorialCompleted'];
- }
- | {
- type: 'PerpsController:markFirstOrderCompleted';
- handler: PerpsController['markFirstOrderCompleted'];
- }
- | {
- type: 'PerpsController:getHistoricalPortfolio';
- handler: PerpsController['getHistoricalPortfolio'];
- }
- | {
- type: 'PerpsController:resetFirstTimeUserState';
- handler: PerpsController['resetFirstTimeUserState'];
- }
- | {
- type: 'PerpsController:clearPendingTransactionRequests';
- handler: PerpsController['clearPendingTransactionRequests'];
- }
- | {
- type: 'PerpsController:saveTradeConfiguration';
- handler: PerpsController['saveTradeConfiguration'];
- }
- | {
- type: 'PerpsController:getTradeConfiguration';
- handler: PerpsController['getTradeConfiguration'];
- }
- | {
- type: 'PerpsController:saveMarketFilterPreferences';
- handler: PerpsController['saveMarketFilterPreferences'];
- }
- | {
- type: 'PerpsController:getMarketFilterPreferences';
- handler: PerpsController['getMarketFilterPreferences'];
- }
- | {
- type: 'PerpsController:savePendingTradeConfiguration';
- handler: PerpsController['savePendingTradeConfiguration'];
- }
- | {
- type: 'PerpsController:getPendingTradeConfiguration';
- handler: PerpsController['getPendingTradeConfiguration'];
- }
- | {
- type: 'PerpsController:clearPendingTradeConfiguration';
- handler: PerpsController['clearPendingTradeConfiguration'];
- }
- | {
- type: 'PerpsController:getOrderBookGrouping';
- handler: PerpsController['getOrderBookGrouping'];
- }
- | {
- type: 'PerpsController:saveOrderBookGrouping';
- handler: PerpsController['saveOrderBookGrouping'];
- }
- | {
- type: 'PerpsController:setSelectedPaymentToken';
- handler: PerpsController['setSelectedPaymentToken'];
- }
- | {
- type: 'PerpsController:resetSelectedPaymentToken';
- handler: PerpsController['resetSelectedPaymentToken'];
- };
+ | PerpsControllerMethodActions;
/**
* PerpsController messenger constraints.
@@ -874,6 +740,8 @@ export class PerpsController extends BaseController<
*/
#standaloneProvider: HyperLiquidProvider | null = null;
+ #handlersRegistered = false;
+
#standaloneProviderIsTestnet: boolean | null = null;
#standaloneProviderHip3Version: number | null = null;
@@ -987,11 +855,6 @@ export class PerpsController extends BaseController<
// Migrate old persisted data without accountAddress
this.#migrateRequestsIfNeeded();
-
- this.messenger.registerMethodActionHandlers(
- this,
- MESSENGER_EXPOSED_METHODS,
- );
}
// ============================================================================
@@ -1506,6 +1369,14 @@ export class PerpsController extends BaseController<
* @returns A promise that resolves when the operation completes.
*/
async init(): Promise {
+ if (!this.#handlersRegistered) {
+ this.messenger.registerMethodActionHandlers(
+ this,
+ MESSENGER_EXPOSED_METHODS,
+ );
+ this.#handlersRegistered = true;
+ }
+
if (this.isInitialized) {
return undefined;
}
@@ -1559,123 +1430,7 @@ export class PerpsController extends BaseController<
this.providers.clear();
await this.#cleanupStandaloneProvider();
- const { activeProvider } = this.state;
-
- this.#debugLog(
- 'PerpsController: Creating provider with HIP-3 configuration',
- {
- hip3Enabled: this.#hip3Enabled,
- hip3AllowlistMarkets: this.#hip3AllowlistMarkets,
- hip3BlocklistMarkets: this.#hip3BlocklistMarkets,
- hip3ConfigSource: this.#hip3ConfigSource,
- isTestnet: this.state.isTestnet,
- activeProvider,
- },
- );
-
- // Always create HyperLiquid provider as the base provider
- const hyperLiquidProvider = new HyperLiquidProvider({
- isTestnet: this.state.isTestnet,
- hip3Enabled: this.#hip3Enabled,
- allowlistMarkets: this.#hip3AllowlistMarkets,
- blocklistMarkets: this.#hip3BlocklistMarkets,
- platformDependencies: this.#options.infrastructure,
- messenger: this.messenger,
- });
- this.providers.set('hyperliquid', hyperLiquidProvider);
-
- // Register MYX provider if enabled via feature flag
- const isMYXEnabled = this.#isMYXProviderEnabled();
- if (isMYXEnabled) {
- const myxIsTestnet =
- PROVIDER_CONFIG.MYX_TESTNET_ONLY || this.state.isTestnet;
- const config = this.#options.clientConfig ?? {};
- // When on mainnet, fall back to testnet credentials if mainnet ones are empty.
- // Uses firstNonEmpty because env vars default to '' (not null/undefined),
- // so ?? would not fall through on empty strings.
- const firstNonEmpty = (...vals: (string | undefined)[]): string =>
- vals.find(
- (val) => val !== null && val !== undefined && val !== '',
- ) ?? '';
- const myxAppId = myxIsTestnet
- ? (config.myxAppIdTestnet ?? '')
- : firstNonEmpty(config.myxAppIdMainnet, config.myxAppIdTestnet);
- const myxApiSecret = myxIsTestnet
- ? (config.myxApiSecretTestnet ?? '')
- : firstNonEmpty(
- config.myxApiSecretMainnet,
- config.myxApiSecretTestnet,
- );
- const myxBrokerAddress = myxIsTestnet
- ? (config.myxBrokerAddressTestnet ?? '')
- : firstNonEmpty(
- config.myxBrokerAddressMainnet,
- config.myxBrokerAddressTestnet,
- );
- const myxProvider = new MYXProvider({
- isTestnet: myxIsTestnet,
- platformDependencies: this.#options.infrastructure,
- messenger: this.messenger,
- myxAuthConfig: {
- appId: myxAppId,
- apiSecret: myxApiSecret,
- brokerAddress: myxBrokerAddress,
- },
- });
- this.providers.set('myx', myxProvider);
- this.#debugLog('PerpsController: MYX provider registered', {
- isTestnet: myxIsTestnet,
- });
- }
-
- // Set up active provider based on activeProvider value in state
- // 'aggregated' is treated as just another provider that wraps others
- if (activeProvider === 'aggregated') {
- // Aggregated mode: wrap in AggregatedPerpsProvider for multi-provider support
- this.activeProviderInstance = new AggregatedPerpsProvider({
- providers: this.providers,
- defaultProvider: 'hyperliquid',
- infrastructure: this.#options.infrastructure,
- });
- this.#debugLog(
- 'PerpsController: Using aggregated provider (multi-provider)',
- { registeredProviders: Array.from(this.providers.keys()) },
- );
- } else if (activeProvider === 'hyperliquid') {
- // Direct provider mode: use HyperLiquid provider directly
- this.activeProviderInstance = hyperLiquidProvider;
- this.#debugLog(
- `PerpsController: Using direct provider (${activeProvider})`,
- );
- } else if (activeProvider === 'myx') {
- // MYX provider mode
- const myxProvider = this.providers.get('myx');
- if (myxProvider) {
- this.activeProviderInstance = myxProvider;
- } else {
- // MYX feature flag is disabled — fall back to HyperLiquid
- this.#debugLog(
- 'PerpsController: MYX provider not available (feature flag disabled), falling back to hyperliquid',
- );
- this.activeProviderInstance = hyperLiquidProvider;
- this.update((state) => {
- state.activeProvider = 'hyperliquid';
- });
- }
- this.#debugLog(
- `PerpsController: Using direct provider (${this.activeProviderInstance === hyperLiquidProvider ? 'hyperliquid' : activeProvider})`,
- );
- } else {
- // Unsupported provider - throw error to prevent silent misconfiguration
- throw new Error(
- `Unsupported provider: ${String(activeProvider)}. Currently only 'hyperliquid', 'myx', and 'aggregated' are supported.`,
- );
- }
-
- // Future providers can be added here with their own authentication patterns:
- // - Some might use API keys: new BinanceProvider({ apiKey, apiSecret })
- // - Some might use different wallet patterns: new GMXProvider({ signer })
- // - Some might not need auth at all: new DydxProvider()
+ this.#createProviders();
// Wait for WebSocket transport to be ready before marking as initialized
await wait(PERPS_CONSTANTS.ReconnectionCleanupDelayMs);
@@ -1688,7 +1443,7 @@ export class PerpsController extends BaseController<
this.#debugLog('PerpsController: Providers initialized successfully', {
providerCount: this.providers.size,
- activeProvider,
+ activeProvider: this.state.activeProvider,
timestamp: new Date().toISOString(),
attempts: attempt,
});
@@ -1735,6 +1490,125 @@ export class PerpsController extends BaseController<
});
}
+ /**
+ * Instantiate provider instances based on current state and register them.
+ * Selects and assigns the active provider instance from the registry.
+ * Future providers can be added here with their own authentication patterns:
+ * - Some might use API keys: new BinanceProvider({ apiKey, apiSecret })
+ * - Some might use different wallet patterns: new GMXProvider({ signer })
+ * - Some might not need auth at all: new DydxProvider()
+ */
+ #createProviders(): void {
+ const { activeProvider } = this.state;
+
+ this.#debugLog(
+ 'PerpsController: Creating provider with HIP-3 configuration',
+ {
+ hip3Enabled: this.#hip3Enabled,
+ hip3AllowlistMarkets: this.#hip3AllowlistMarkets,
+ hip3BlocklistMarkets: this.#hip3BlocklistMarkets,
+ hip3ConfigSource: this.#hip3ConfigSource,
+ isTestnet: this.state.isTestnet,
+ activeProvider,
+ },
+ );
+
+ // Always create HyperLiquid provider as the base provider
+ const hyperLiquidProvider = new HyperLiquidProvider({
+ isTestnet: this.state.isTestnet,
+ hip3Enabled: this.#hip3Enabled,
+ allowlistMarkets: this.#hip3AllowlistMarkets,
+ blocklistMarkets: this.#hip3BlocklistMarkets,
+ platformDependencies: this.#options.infrastructure,
+ messenger: this.messenger,
+ });
+ this.providers.set('hyperliquid', hyperLiquidProvider);
+
+ // Register MYX provider if enabled via feature flag
+ const isMYXEnabled = this.#isMYXProviderEnabled();
+ if (isMYXEnabled) {
+ const myxIsTestnet =
+ PROVIDER_CONFIG.MYX_TESTNET_ONLY || this.state.isTestnet;
+ const config = this.#options.clientConfig ?? {};
+ // When on mainnet, fall back to testnet credentials if mainnet ones are empty.
+ // Uses firstNonEmpty because env vars default to '' (not null/undefined),
+ // so ?? would not fall through on empty strings.
+ const firstNonEmpty = (...vals: (string | undefined)[]): string =>
+ vals.find((val) => val !== null && val !== undefined && val !== '') ??
+ '';
+ const myxAppId = myxIsTestnet
+ ? (config.myxAppIdTestnet ?? '')
+ : firstNonEmpty(config.myxAppIdMainnet, config.myxAppIdTestnet);
+ const myxApiSecret = myxIsTestnet
+ ? (config.myxApiSecretTestnet ?? '')
+ : firstNonEmpty(config.myxApiSecretMainnet, config.myxApiSecretTestnet);
+ const myxBrokerAddress = myxIsTestnet
+ ? (config.myxBrokerAddressTestnet ?? '')
+ : firstNonEmpty(
+ config.myxBrokerAddressMainnet,
+ config.myxBrokerAddressTestnet,
+ );
+ const myxProvider = new MYXProvider({
+ isTestnet: myxIsTestnet,
+ platformDependencies: this.#options.infrastructure,
+ messenger: this.messenger,
+ myxAuthConfig: {
+ appId: myxAppId,
+ apiSecret: myxApiSecret,
+ brokerAddress: myxBrokerAddress,
+ },
+ });
+ this.providers.set('myx', myxProvider);
+ this.#debugLog('PerpsController: MYX provider registered', {
+ isTestnet: myxIsTestnet,
+ });
+ }
+
+ // Set up active provider based on activeProvider value in state
+ // 'aggregated' is treated as just another provider that wraps others
+ if (activeProvider === 'aggregated') {
+ // Aggregated mode: wrap in AggregatedPerpsProvider for multi-provider support
+ this.activeProviderInstance = new AggregatedPerpsProvider({
+ providers: this.providers,
+ defaultProvider: 'hyperliquid',
+ infrastructure: this.#options.infrastructure,
+ });
+ this.#debugLog(
+ 'PerpsController: Using aggregated provider (multi-provider)',
+ { registeredProviders: Array.from(this.providers.keys()) },
+ );
+ } else if (activeProvider === 'hyperliquid') {
+ // Direct provider mode: use HyperLiquid provider directly
+ this.activeProviderInstance = hyperLiquidProvider;
+ this.#debugLog(
+ `PerpsController: Using direct provider (${activeProvider})`,
+ );
+ } else if (activeProvider === 'myx') {
+ // MYX provider mode
+ const myxProvider = this.providers.get('myx');
+ if (myxProvider) {
+ this.activeProviderInstance = myxProvider;
+ } else {
+ // MYX feature flag is disabled — fall back to HyperLiquid
+ this.#debugLog(
+ 'PerpsController: MYX provider not available (feature flag disabled), falling back to hyperliquid',
+ );
+ this.activeProviderInstance = hyperLiquidProvider;
+ this.update((state) => {
+ state.activeProvider = 'hyperliquid';
+ });
+ }
+ this.#debugLog(
+ `PerpsController: Using direct provider (${this.activeProviderInstance === hyperLiquidProvider ? 'hyperliquid' : activeProvider})`,
+ );
+ } else {
+ // Unsupported provider - throw error to prevent silent misconfiguration
+ throw new Error(
+ `Unsupported provider: ${String(activeProvider)}. Currently only 'hyperliquid', 'myx', and 'aggregated' are supported.`,
+ );
+ }
+ }
+
/**
* Generate standard error context for Logger.error calls with searchable tags and context.
* Enables Sentry dashboard filtering by feature, provider, and network.
@@ -2302,8 +2176,8 @@ export class PerpsController extends BaseController<
const isCancellation =
errorMessage.includes('User denied') ||
errorMessage.includes('User rejected') ||
- errorMessage.includes('cancelled') ||
- errorMessage.includes('canceled');
+ errorMessage.includes('User cancelled') ||
+ errorMessage.includes('User canceled');
this.update((state) => {
const requestToUpdate = state.depositRequests.find(
(req) => req.id === currentDepositId,
diff --git a/app/controllers/perps/constants/hyperLiquidConfig.ts b/app/controllers/perps/constants/hyperLiquidConfig.ts
index 8188143b2d5..9e56137b13b 100644
--- a/app/controllers/perps/constants/hyperLiquidConfig.ts
+++ b/app/controllers/perps/constants/hyperLiquidConfig.ts
@@ -189,7 +189,7 @@ export const REFERRAL_CONFIG = {
// Deposit constants
export const DEPOSIT_CONFIG = {
- EstimatedGasLimit: 150000, // Estimated gas limit for bridge deposit
+ EstimatedGasLimit: 100000, // Estimated gas limit for bridge deposit
DefaultSlippage: 1, // 1% default slippage for bridge quotes
BridgeQuoteTimeout: 1000, // 1 second timeout for bridge quotes
RefreshRate: 30000, // 30 seconds quote refresh rate
@@ -201,6 +201,7 @@ export const DEPOSIT_CONFIG = {
// Withdrawal constants (HyperLiquid-specific)
export const HYPERLIQUID_WITHDRAWAL_MINUTES = 5; // HyperLiquid withdrawal processing time in minutes
+export const ESTIMATED_FEE_RATE = 0.0009; // 0.09% taker fee estimate for flip operations (close + open)
// Type helpers
export type SupportedAsset = keyof typeof HYPERLIQUID_ASSET_CONFIGS;
@@ -278,6 +279,13 @@ export const HIP3_ASSET_ID_CONFIG = {
*/
export const BASIS_POINTS_DIVISOR = 10000;
+/**
+ * Offset added to spot market pair index to derive the spot asset ID
+ * used in HyperLiquid order routing.
+ * Per HyperLiquid protocol: spotAssetId = SPOT_ASSET_ID_OFFSET + pairIndex
+ */
+export const SPOT_ASSET_ID_OFFSET = 10000;
+
/**
* HIP-3 asset market type classifications (PRODUCTION DEFAULT)
*
diff --git a/app/controllers/perps/index.ts b/app/controllers/perps/index.ts
index 3baaacc2ba3..e77bb525248 100644
--- a/app/controllers/perps/index.ts
+++ b/app/controllers/perps/index.ts
@@ -44,20 +44,430 @@ export type {
// Provider interfaces and implementations
export { HyperLiquidProvider } from './providers/HyperLiquidProvider';
-// All type definitions
-export * from './types';
+// Type definitions (explicit named exports)
+export { WebSocketConnectionState, PerpsAnalyticsEvent } from './types';
+export type {
+ RawLedgerUpdate,
+ UserHistoryItem,
+ GetUserHistoryParams,
+ TradeConfiguration,
+ OrderType,
+ MarketType,
+ MarketTypeFilter,
+ InputMethod,
+ TradeAction,
+ TrackingData,
+ TPSLTrackingData,
+ OrderParams,
+ OrderResult,
+ Position,
+ AccountState,
+ ClosePositionParams,
+ ClosePositionsParams,
+ ClosePositionsResult,
+ UpdateMarginParams,
+ MarginResult,
+ FlipPositionParams,
+ InitializeResult,
+ ReadyToTradeResult,
+ DisconnectResult,
+ MarketInfo,
+ PerpsMarketData,
+ ToggleTestnetResult,
+ AssetRoute,
+ SwitchProviderResult,
+ CancelOrderParams,
+ CancelOrderResult,
+ BatchCancelOrdersParams,
+ CancelOrdersParams,
+ CancelOrdersResult,
+ EditOrderParams,
+ DepositParams,
+ DepositWithConfirmationParams,
+ DepositResult,
+ DepositStatus,
+ DepositFlowType,
+ DepositStepInfo,
+ WithdrawParams,
+ WithdrawResult,
+ TransferBetweenDexsParams,
+ TransferBetweenDexsResult,
+ GetHistoricalPortfolioParams,
+ HistoricalPortfolioResult,
+ LiveDataConfig,
+ PerpsControllerConfig,
+ PriceUpdate,
+ OrderFill,
+ CheckEligibilityParams,
+ GetPositionsParams,
+ GetAccountStateParams,
+ GetOrderFillsParams,
+ GetOrFetchFillsParams,
+ GetOrdersParams,
+ GetFundingParams,
+ GetSupportedPathsParams,
+ GetAvailableDexsParams,
+ GetMarketsParams,
+ SubscribePricesParams,
+ SubscribePositionsParams,
+ SubscribeOrderFillsParams,
+ SubscribeOrdersParams,
+ SubscribeAccountParams,
+ SubscribeOICapsParams,
+ SubscribeCandlesParams,
+ OrderBookLevel,
+ OrderBookData,
+ SubscribeOrderBookParams,
+ LiquidationPriceParams,
+ MaintenanceMarginParams,
+ FeeCalculationParams,
+ FeeCalculationResult,
+ UpdatePositionTPSLParams,
+ Order,
+ Funding,
+ PerpsProvider,
+ PerpsProviderType,
+ PerpsActiveProviderMode,
+ AggregationMode,
+ RoutingStrategy,
+ AggregatedProviderConfig,
+ ProviderError,
+ AggregatedAccountState,
+ PerpsLogger,
+ PerpsTraceName,
+ PerpsTraceValue,
+ PerpsAnalyticsProperties,
+ PerpsMetrics,
+ PerpsDebugLogger,
+ PerpsStreamManager,
+ PerpsPerformance,
+ PerpsTracer,
+ PerpsTypedMessageParams,
+ PerpsTransactionParams,
+ PerpsAddTransactionOptions,
+ PerpsInternalAccount,
+ PerpsRemoteFeatureFlagState,
+ PerpsPlatformDependencies,
+ PerpsCacheType,
+ InvalidateCacheParams,
+ PerpsCacheInvalidator,
+ MarketDataFormatters,
+ PaymentToken,
+ PerpsSelectedPaymentToken,
+ VersionGatedFeatureFlag,
+} from './types';
+export {
+ PerpsTraceNames,
+ PerpsTraceOperations,
+ isVersionGatedFeatureFlag,
+} from './types';
-// All constants
-export * from './constants';
+// Types from sub-modules (re-exported via types/index.ts)
+export type {
+ TestResultStatus,
+ TestResult,
+ SDKTestType,
+ HyperliquidAsset,
+ CandleStick,
+ CandleData,
+ OrderFormState,
+ OrderDirection,
+ ReconnectOptions,
+ ExtendedAssetMeta,
+ ExtendedPerpDex,
+} from './types';
+export type {
+ BaseTransactionResult,
+ LastTransactionResult,
+ TransactionStatus,
+ TransactionRecord,
+} from './types';
+export { isTransactionRecord, isLastTransactionResult } from './types';
+export type {
+ AssetPosition,
+ SpotBalance,
+ PerpsUniverse,
+ PerpsAssetCtx,
+ PredictedFunding,
+ FrontendOrder,
+ SDKOrderParams,
+ ClearinghouseStateResponse,
+ SpotClearinghouseStateResponse,
+ MetaResponse,
+ FrontendOpenOrdersResponse,
+ AllMidsResponse,
+ MetaAndAssetCtxsResponse,
+ PredictedFundingsResponse,
+ SpotMetaResponse,
+} from './types';
+export type {
+ HyperLiquidEndpoints,
+ AssetNetworkConfig,
+ HyperLiquidAssetConfigs,
+ BridgeContractConfig,
+ HyperLiquidBridgeContracts,
+ TransportReconnectConfig,
+ TransportKeepAliveConfig,
+ HyperLiquidTransportConfig,
+ TradingAmountConfig,
+ TradingDefaultsConfig,
+ FeeRatesConfig,
+ HyperLiquidNetwork,
+} from './types';
+export type { PerpsToken } from './types';
-// All utilities
-export * from './utils';
+// Constants (explicit named exports)
+export {
+ CandlePeriod,
+ TimeDuration,
+ ChartInterval,
+ MAX_CANDLE_COUNT,
+ DURATION_CANDLE_PERIODS,
+ CANDLE_PERIODS,
+ DEFAULT_CANDLE_PERIOD,
+ getCandlePeriodsForDuration,
+ getDefaultCandlePeriodForDuration,
+ calculateCandleCount,
+} from './constants';
+export { PERPS_EVENT_PROPERTY, PERPS_EVENT_VALUE } from './constants';
+export { DETAILED_ORDER_TYPES, isTPSLOrder } from './constants';
+export { PERPS_TRANSACTIONS_HISTORY_CONSTANTS } from './constants';
+export {
+ ARBITRUM_MAINNET_CHAIN_ID_HEX,
+ ARBITRUM_MAINNET_CHAIN_ID,
+ ARBITRUM_TESTNET_CHAIN_ID,
+ ARBITRUM_MAINNET_CAIP_CHAIN_ID,
+ ARBITRUM_TESTNET_CAIP_CHAIN_ID,
+ HYPERLIQUID_MAINNET_CHAIN_ID,
+ HYPERLIQUID_TESTNET_CHAIN_ID,
+ HYPERLIQUID_MAINNET_CAIP_CHAIN_ID,
+ HYPERLIQUID_TESTNET_CAIP_CHAIN_ID,
+ HYPERLIQUID_NETWORK_NAME,
+ USDC_SYMBOL,
+ USDC_NAME,
+ USDC_DECIMALS,
+ TOKEN_DECIMALS,
+ ZERO_ADDRESS,
+ ZERO_BALANCE,
+ ARBITRUM_SEPOLIA_CHAIN_ID,
+ USDC_ETHEREUM_MAINNET_ADDRESS,
+ USDC_ARBITRUM_MAINNET_ADDRESS,
+ USDC_ARBITRUM_TESTNET_ADDRESS,
+ USDC_TOKEN_ICON_URL,
+ HYPERLIQUID_ENDPOINTS,
+ HYPERLIQUID_ASSET_ICONS_BASE_URL,
+ METAMASK_PERPS_ICONS_BASE_URL,
+ HYPERLIQUID_ASSET_CONFIGS,
+ HYPERLIQUID_BRIDGE_CONTRACTS,
+ HYPERLIQUID_TRANSPORT_CONFIG,
+ TRADING_DEFAULTS,
+ FEE_RATES,
+ HIP3_FEE_CONFIG,
+ BUILDER_FEE_CONFIG,
+ REFERRAL_CONFIG,
+ DEPOSIT_CONFIG,
+ HYPERLIQUID_WITHDRAWAL_MINUTES,
+ getWebSocketEndpoint,
+ getChainId,
+ getCaipChainId,
+ getBridgeInfo,
+ getSupportedAssets,
+ CAIP_ASSET_NAMESPACES,
+ HYPERLIQUID_CONFIG,
+ HIP3_ASSET_ID_CONFIG,
+ BASIS_POINTS_DIVISOR,
+ SPOT_ASSET_ID_OFFSET,
+ HIP3_ASSET_MARKET_TYPES,
+ TESTNET_HIP3_CONFIG,
+ MAINNET_HIP3_CONFIG,
+ HIP3_MARGIN_CONFIG,
+ USDH_CONFIG,
+ INITIAL_AMOUNT_UI_PROGRESS,
+ WITHDRAWAL_PROGRESS_STAGES,
+ PROGRESS_BAR_COMPLETION_DELAY_MS,
+} from './constants';
+export type { SupportedAsset } from './constants';
+export { PerpsMeasurementName } from './constants';
+export {
+ MYX_MAINNET_CHAIN_ID,
+ MYX_TESTNET_CHAIN_ID,
+ MYX_MAINNET_CAIP_CHAIN_ID,
+ MYX_TESTNET_CAIP_CHAIN_ID,
+ getMYXChainId,
+ MYX_ENDPOINTS,
+ getMYXHttpEndpoint,
+ MYX_PRICE_DECIMALS,
+ MYX_SIZE_DECIMALS,
+ MYX_COLLATERAL_DECIMALS,
+ USDT_BNB_TESTNET,
+ USDT_BNB_MAINNET,
+ MYX_ASSET_CONFIGS,
+ fromMYXPrice,
+ toMYXPrice,
+ fromMYXSize,
+ toMYXSize,
+ fromMYXCollateral,
+ MYX_PRICE_POLLING_INTERVAL_MS,
+ MYX_HTTP_TIMEOUT_MS,
+ MYX_MAX_RETRIES,
+ MYX_MAX_LEVERAGE,
+ MYX_FEE_RATE,
+ MYX_PROTOCOL_FEE_RATE,
+ MYX_DEFAULT_SLIPPAGE_BPS,
+ MYX_MINIMUM_ORDER_SIZE_USD,
+ MYX_EXECUTION_FEE_TOKEN,
+} from './constants';
+export {
+ PERPS_CONSTANTS,
+ WITHDRAWAL_CONSTANTS,
+ VALIDATION_THRESHOLDS,
+ ORDER_SLIPPAGE_CONFIG,
+ PERFORMANCE_CONFIG,
+ TP_SL_CONFIG,
+ HYPERLIQUID_ORDER_LIMITS,
+ CLOSE_POSITION_CONFIG,
+ MARGIN_ADJUSTMENT_CONFIG,
+ DATA_LAKE_API_CONFIG,
+ DECIMAL_PRECISION_CONFIG,
+ MARKET_SORTING_CONFIG,
+ PROVIDER_CONFIG,
+} from './constants';
+export type { SortOptionId } from './constants';
-// Error codes
-export * from './perpsErrorCodes';
+// Utilities (explicit named exports)
+export {
+ findEvmAccount,
+ getEvmAccountFromAccountGroup,
+ getSelectedEvmAccount,
+ calculateWeightedReturnOnEquity,
+ aggregateAccountStates,
+} from './utils';
+export type { ReturnOnEquityInput } from './utils';
+export { ensureError } from './utils';
+export type {
+ OrderBookCacheEntry,
+ ProcessL2BookDataParams,
+ ProcessBboDataParams,
+} from './utils';
+export { processL2BookData, processBboData } from './utils';
+export type { ValidationDebugLogger } from './utils';
+export {
+ createErrorResult,
+ validateWithdrawalParams,
+ validateDepositParams,
+ validateAssetSupport,
+ validateBalance,
+ applyPathFilters,
+ getSupportedPaths,
+ getMaxOrderValue,
+ validateOrderParams,
+ validateCoinExists,
+} from './utils';
+export {
+ generatePerpsId,
+ generateDepositId,
+ generateWithdrawalId,
+ generateOrderId,
+ generateTransactionId,
+} from './utils';
+export {
+ calculateOpenInterestUSD,
+ transformMarketData,
+ formatChange,
+} from './utils';
+export type { HyperLiquidMarketData } from './utils';
+export {
+ adaptMarketFromMYX,
+ adaptPriceFromMYX,
+ adaptMarketDataFromMYX,
+ filterMYXExclusiveMarkets,
+ isOverlappingMarket,
+ buildPoolSymbolMap,
+ buildSymbolPoolsMap,
+ extractSymbolFromPoolId,
+} from './utils';
+export {
+ MAX_MARKET_PATTERN_LENGTH,
+ escapeRegex,
+ validateMarketPattern,
+ compileMarketPattern,
+ matchesMarketPattern,
+ shouldIncludeMarket,
+ getPerpsDisplaySymbol,
+ getPerpsDexFromSymbol,
+ calculateFundingCountdown,
+ calculate24hHighLow,
+ filterMarketsByQuery,
+} from './utils';
+export type { MarketPatternMatcher, CompiledMarketPattern } from './utils';
+export type {
+ OrderCalculationsDebugLogger,
+ CalculateFinalPositionSizeParams,
+ CalculateFinalPositionSizeResult,
+ CalculateOrderPriceAndSizeParams,
+ CalculateOrderPriceAndSizeResult,
+ BuildOrdersArrayParams,
+ BuildOrdersArrayResult,
+} from './utils';
+export {
+ calculatePositionSize,
+ calculateMarginRequired,
+ getMaxAllowedAmount,
+ calculateFinalPositionSize,
+ calculateOrderPriceAndSize,
+ buildOrdersArray,
+} from './utils';
+export {
+ formatAccountToCaipAccountId,
+ isCaipAccountId,
+ handleRewardsError,
+} from './utils';
+export {
+ countSignificantFigures,
+ hasExceededSignificantFigures,
+ roundToSignificantFigures,
+} from './utils';
+export type { SortField, SortDirection, SortMarketsParams } from './utils';
+export { parseVolume, sortMarkets } from './utils';
+export type { StandaloneInfoClientOptions } from './utils';
+export {
+ createStandaloneInfoClient,
+ queryStandaloneClearinghouseStates,
+ queryStandaloneOpenOrders,
+} from './utils';
+export { stripQuotes, parseCommaSeparatedString } from './utils';
+export { generateERC20TransferData } from './utils';
+export { wait } from './utils';
+export {
+ adaptOrderToSDK,
+ adaptPositionFromSDK,
+ adaptOrderFromSDK,
+ adaptMarketFromSDK,
+ adaptAccountStateFromSDK,
+ buildAssetMapping,
+ formatHyperLiquidPrice,
+ formatHyperLiquidSize,
+ calculateHip3AssetId,
+ parseAssetName,
+ adaptHyperLiquidLedgerUpdateToUserHistoryItem,
+} from './utils';
+export { getEnvironment } from './utils';
+
+// Error codes (explicit named exports)
+export { PERPS_ERROR_CODES } from './perpsErrorCodes';
+export type { PerpsErrorCode } from './perpsErrorCodes';
-// Selectors
-export * from './selectors';
+// Selectors (explicit named exports)
+export {
+ selectIsFirstTimeUser,
+ selectHasPlacedFirstOrder,
+ selectWatchlistMarkets,
+ selectIsWatchlistMarket,
+ selectTradeConfiguration,
+ selectPendingTradeConfiguration,
+ selectMarketFilterPreferences,
+ selectOrderBookGrouping,
+} from './selectors';
// Services (only externally consumed items)
export { TradingReadinessCache } from './services/TradingReadinessCache';
diff --git a/app/controllers/perps/providers/HyperLiquidProvider.test.ts b/app/controllers/perps/providers/HyperLiquidProvider.test.ts
index 0a4e43d5460..47f1c14a38c 100644
--- a/app/controllers/perps/providers/HyperLiquidProvider.test.ts
+++ b/app/controllers/perps/providers/HyperLiquidProvider.test.ts
@@ -4,6 +4,7 @@ import {
createMockInfrastructure,
createMockMessenger,
} from '../../../components/UI/Perps/__mocks__/serviceMocks';
+import { CandlePeriod } from '../constants/chartConfig';
import { REFERRAL_CONFIG } from '../constants/hyperLiquidConfig';
import { PERPS_ERROR_CODES } from '../perpsErrorCodes';
import { HyperLiquidClientService } from '../services/HyperLiquidClientService';
@@ -8547,6 +8548,62 @@ describe('HyperLiquidProvider', () => {
});
});
+ describe('fetchHistoricalCandles', () => {
+ const options = {
+ symbol: 'BTC',
+ interval: CandlePeriod.OneHour,
+ limit: 100,
+ };
+
+ it('returns candle data from clientService', async () => {
+ // Arrange
+ const mockCandles = {
+ symbol: 'BTC',
+ interval: CandlePeriod.OneHour,
+ candles: [
+ {
+ open: '50000',
+ close: '51000',
+ high: '51500',
+ low: '49500',
+ volume: '100',
+ time: 1000,
+ },
+ ],
+ };
+ mockClientService.fetchHistoricalCandles = jest
+ .fn()
+ .mockResolvedValue(mockCandles);
+
+ // Act
+ const result = await provider.fetchHistoricalCandles(options);
+
+ // Assert
+ expect(mockClientService.ensureInitialized).toHaveBeenCalled();
+ expect(mockClientService.fetchHistoricalCandles).toHaveBeenCalledWith(
+ options,
+ );
+ expect(result).toStrictEqual(mockCandles);
+ });
+
+ it('returns empty candles when clientService returns null', async () => {
+ // Arrange
+ mockClientService.fetchHistoricalCandles = jest
+ .fn()
+ .mockResolvedValue(null);
+
+ // Act
+ const result = await provider.fetchHistoricalCandles(options);
+
+ // Assert
+ expect(result).toStrictEqual({
+ symbol: options.symbol,
+ interval: options.interval,
+ candles: [],
+ });
+ });
+ });
+
describe('buildAssetMapping with perpDexs network failure', () => {
it('completes asset mapping using fallback when perpDexs throws', async () => {
// Arrange — perpDexs throws, so getValidatedDexs falls back to [null]
diff --git a/app/controllers/perps/providers/HyperLiquidProvider.ts b/app/controllers/perps/providers/HyperLiquidProvider.ts
index 57d45f64b56..5e9c057ffa9 100644
--- a/app/controllers/perps/providers/HyperLiquidProvider.ts
+++ b/app/controllers/perps/providers/HyperLiquidProvider.ts
@@ -2,6 +2,7 @@ import { CaipAccountId } from '@metamask/utils';
import type { Hex } from '@metamask/utils';
import { v4 as uuidv4 } from 'uuid';
+import type { CandlePeriod } from '../constants/chartConfig';
import {
BASIS_POINTS_DIVISOR,
BUILDER_FEE_CONFIG,
@@ -14,6 +15,7 @@ import {
HYPERLIQUID_WITHDRAWAL_MINUTES,
MAINNET_HIP3_CONFIG,
REFERRAL_CONFIG,
+ SPOT_ASSET_ID_OFFSET,
TESTNET_HIP3_CONFIG,
TRADING_DEFAULTS,
USDC_DECIMALS,
@@ -45,6 +47,7 @@ import type {
CancelOrderParams,
CancelOrderResult,
CancelOrdersResult,
+ CandleData,
ClosePositionParams,
ClosePositionsParams,
ClosePositionsResult,
@@ -1700,7 +1703,7 @@ export class HyperLiquidProvider implements PerpsProvider {
return { success: false, error: PERPS_ERROR_CODES.SPOT_PAIR_NOT_FOUND };
}
- const spotAssetId = 10000 + usdhUsdcPair.index;
+ const spotAssetId = SPOT_ASSET_ID_OFFSET + usdhUsdcPair.index;
this.#deps.debugLogger.log(
'HyperLiquidProvider: Found USDH/USDC spot pair',
@@ -7497,6 +7500,23 @@ export class HyperLiquidProvider implements PerpsProvider {
}
}
+ async fetchHistoricalCandles(options: {
+ symbol: string;
+ interval: CandlePeriod;
+ limit?: number;
+ endTime?: number;
+ }): Promise {
+ this.#clientService.ensureInitialized();
+ const result = await this.#clientService.fetchHistoricalCandles(options);
+ return (
+ result ?? {
+ symbol: options.symbol,
+ interval: options.interval,
+ candles: [],
+ }
+ );
+ }
+
/**
* Get block explorer URL for an address or just the base URL
*
diff --git a/app/controllers/perps/selectors.ts b/app/controllers/perps/selectors.ts
index b290cb39c82..a7e0b1cf4ab 100644
--- a/app/controllers/perps/selectors.ts
+++ b/app/controllers/perps/selectors.ts
@@ -142,8 +142,8 @@ export const selectPendingTradeConfiguration = createSelector(
}
// Check if config has expired (5 minutes = 300,000 milliseconds)
- const FIVE_MINUTES_MS = 5 * 60 * 1000;
const now = Date.now();
+ const FIVE_MINUTES_MS = 5 * 60 * 1000;
const age = now - config.timestamp;
if (age > FIVE_MINUTES_MS) {
diff --git a/app/controllers/perps/services/AccountService.ts b/app/controllers/perps/services/AccountService.ts
index f6c121daa1e..d60f8bc9bc4 100644
--- a/app/controllers/perps/services/AccountService.ts
+++ b/app/controllers/perps/services/AccountService.ts
@@ -5,7 +5,10 @@ import {
PERPS_EVENT_VALUE,
} from '../constants/eventNames';
import { USDC_SYMBOL } from '../constants/hyperLiquidConfig';
-import { PERPS_CONSTANTS } from '../constants/perpsConfig';
+import {
+ PERPS_CONSTANTS,
+ WITHDRAWAL_CONSTANTS,
+} from '../constants/perpsConfig';
import { PERPS_ERROR_CODES } from '../perpsErrorCodes';
import {
PerpsAnalyticsEvent,
@@ -117,7 +120,7 @@ export class AccountService {
// Calculate net amount after fees
const grossAmount = parseFloat(params.amount);
- const feeAmount = 1.0; // HyperLiquid withdrawal fee is $1 USDC
+ const feeAmount = WITHDRAWAL_CONSTANTS.DefaultFeeAmount;
const netAmount = Math.max(0, grossAmount - feeAmount);
// Get current account address via messenger
diff --git a/app/controllers/perps/services/DepositService.ts b/app/controllers/perps/services/DepositService.ts
index a4ffe31e519..860964db75d 100644
--- a/app/controllers/perps/services/DepositService.ts
+++ b/app/controllers/perps/services/DepositService.ts
@@ -2,6 +2,7 @@ import { toHex } from '@metamask/controller-utils';
import { parseCaipAssetId } from '@metamask/utils';
import type { Hex } from '@metamask/utils';
+import { DEPOSIT_CONFIG } from '../constants/hyperLiquidConfig';
import type {
PerpsProvider,
PerpsPlatformDependencies,
@@ -13,7 +14,7 @@ import { generateDepositId } from '../utils/idUtils';
import { generateERC20TransferData } from '../utils/transferData';
// Temporary to avoid estimation failures due to insufficient balance
-const DEPOSIT_GAS_LIMIT = toHex(100000);
+const DEPOSIT_GAS_LIMIT = toHex(DEPOSIT_CONFIG.EstimatedGasLimit);
/**
* DepositService
diff --git a/app/controllers/perps/services/MarketDataService.test.ts b/app/controllers/perps/services/MarketDataService.test.ts
index 4727870cc54..da45aa5a025 100644
--- a/app/controllers/perps/services/MarketDataService.test.ts
+++ b/app/controllers/perps/services/MarketDataService.test.ts
@@ -876,14 +876,9 @@ describe('MarketDataService', () => {
};
it('fetches historical candles successfully', async () => {
- const hyperLiquidProvider = mockProvider as unknown as {
- clientService: {
- fetchHistoricalCandles: jest.Mock;
- };
- };
- hyperLiquidProvider.clientService = {
- fetchHistoricalCandles: jest.fn().mockResolvedValue(mockCandleData),
- };
+ mockProvider.fetchHistoricalCandles = jest
+ .fn()
+ .mockResolvedValue(mockCandleData);
const result = await marketDataService.fetchHistoricalCandles({
provider: mockProvider,
@@ -894,9 +889,7 @@ describe('MarketDataService', () => {
});
expect(result).toEqual(mockCandleData);
- expect(
- hyperLiquidProvider.clientService.fetchHistoricalCandles,
- ).toHaveBeenCalledWith({
+ expect(mockProvider.fetchHistoricalCandles).toHaveBeenCalledWith({
symbol: 'BTC',
interval: '1h',
limit: 100,
@@ -934,15 +927,10 @@ describe('MarketDataService', () => {
});
it('updates error state on failure', async () => {
- const hyperLiquidProvider = mockProvider as unknown as {
- clientService: {
- fetchHistoricalCandles: jest.Mock;
- };
- };
const mockError = new Error('Network timeout');
- hyperLiquidProvider.clientService = {
- fetchHistoricalCandles: jest.fn().mockRejectedValue(mockError),
- };
+ mockProvider.fetchHistoricalCandles = jest
+ .fn()
+ .mockRejectedValue(mockError);
await expect(
marketDataService.fetchHistoricalCandles({
diff --git a/app/controllers/perps/services/MarketDataService.ts b/app/controllers/perps/services/MarketDataService.ts
index cdf8b03938d..3da87699504 100644
--- a/app/controllers/perps/services/MarketDataService.ts
+++ b/app/controllers/perps/services/MarketDataService.ts
@@ -760,28 +760,16 @@ export class MarketDataService {
},
});
- // Check if provider supports historical candles via clientService
- const hyperLiquidProvider = provider as {
- clientService?: {
- fetchHistoricalCandles?: (options: {
- symbol: string;
- interval: CandlePeriod;
- limit?: number;
- endTime?: number;
- }) => Promise;
- };
- };
- if (!hyperLiquidProvider.clientService?.fetchHistoricalCandles) {
+ if (!provider.fetchHistoricalCandles) {
throw new Error('Historical candles not supported by provider');
}
- const result =
- await hyperLiquidProvider.clientService.fetchHistoricalCandles({
- symbol,
- interval,
- limit,
- endTime,
- });
+ const result = await provider.fetchHistoricalCandles({
+ symbol,
+ interval,
+ limit,
+ endTime,
+ });
traceData = { success: true };
return result;
diff --git a/app/controllers/perps/services/TradingService.ts b/app/controllers/perps/services/TradingService.ts
index db5dc1b950f..a3e72ef8398 100644
--- a/app/controllers/perps/services/TradingService.ts
+++ b/app/controllers/perps/services/TradingService.ts
@@ -6,6 +6,7 @@ import {
PERPS_EVENT_PROPERTY,
PERPS_EVENT_VALUE,
} from '../constants/eventNames';
+import { ESTIMATED_FEE_RATE } from '../constants/hyperLiquidConfig';
import { isTPSLOrder } from '../constants/orderTypes';
import { PerpsMeasurementName } from '../constants/performanceMetrics';
import { PERPS_CONSTANTS } from '../constants/perpsConfig';
@@ -1894,7 +1895,7 @@ export class TradingService {
const entryPrice = parseFloat(position.entryPrice);
const flipSize = positionSize * 2;
const notionalValue = flipSize * entryPrice;
- const estimatedFees = notionalValue * 0.0009;
+ const estimatedFees = notionalValue * ESTIMATED_FEE_RATE;
if (estimatedFees > availableBalance) {
throw new Error(
diff --git a/app/controllers/perps/types/index.ts b/app/controllers/perps/types/index.ts
index 3fb6e4efe1e..5e53d8584aa 100644
--- a/app/controllers/perps/types/index.ts
+++ b/app/controllers/perps/types/index.ts
@@ -5,7 +5,7 @@ import type {
Hex,
} from '@metamask/utils';
-import type { CandleData } from './perps-types';
+import type { CandleData, OrderType } from './perps-types';
import type { CandlePeriod, TimeDuration } from '../constants/chartConfig';
/**
@@ -72,9 +72,6 @@ export type TradeConfiguration = {
};
};
-// Order type enumeration
-export type OrderType = 'market' | 'limit';
-
// Market asset type classification (reusable across components)
export type MarketType = 'crypto' | 'equity' | 'commodity' | 'forex';
@@ -1066,6 +1063,17 @@ export type PerpsProvider = {
* @returns Array of DEX names (empty string '' represents main DEX)
*/
getAvailableDexs?(params?: GetAvailableDexsParams): Promise;
+
+ /**
+ * Fetch historical OHLCV candle data for a symbol.
+ * Optional: only providers that support historical candles need to implement this.
+ */
+ fetchHistoricalCandles?(options: {
+ symbol: string;
+ interval: CandlePeriod;
+ limit?: number;
+ endTime?: number;
+ }): Promise;
};
// ============================================================================
diff --git a/app/controllers/perps/types/perps-types.ts b/app/controllers/perps/types/perps-types.ts
index 3e0deb1ead4..4b30618788c 100644
--- a/app/controllers/perps/types/perps-types.ts
+++ b/app/controllers/perps/types/perps-types.ts
@@ -1,9 +1,11 @@
/**
* Test result states for SDK validation
*/
-import { OrderType } from '.';
import { CandlePeriod } from '../constants/chartConfig';
+// Order type enumeration
+export type OrderType = 'market' | 'limit';
+
export type TestResultStatus =
| 'idle'
| 'loading'
@@ -55,9 +57,24 @@ export type CandleData = {
candles: CandleStick[];
};
-// Export all configuration types directly
-export type * from './config';
-export type * from './token';
+// Configuration types
+export type {
+ HyperLiquidEndpoints,
+ AssetNetworkConfig,
+ HyperLiquidAssetConfigs,
+ BridgeContractConfig,
+ HyperLiquidBridgeContracts,
+ TransportReconnectConfig,
+ TransportKeepAliveConfig,
+ HyperLiquidTransportConfig,
+ TradingAmountConfig,
+ TradingDefaultsConfig,
+ FeeRatesConfig,
+ HyperLiquidNetwork,
+} from './config';
+
+// Token types
+export type { PerpsToken } from './token';
/**
* Order form state for the Perps order view
diff --git a/app/images/error-state-no-connection-dark.png b/app/images/error-state-no-connection-dark.png
new file mode 100644
index 00000000000..336adbd2e7b
Binary files /dev/null and b/app/images/error-state-no-connection-dark.png differ
diff --git a/app/images/error-state-no-connection-light.png b/app/images/error-state-no-connection-light.png
new file mode 100644
index 00000000000..4b2fa643440
Binary files /dev/null and b/app/images/error-state-no-connection-light.png differ
diff --git a/tests/smoke/ramps/offramp-token-amount.spec.ts b/tests/smoke/ramps/offramp-token-amount.spec.ts
deleted file mode 100644
index 01a0dbf8999..00000000000
--- a/tests/smoke/ramps/offramp-token-amount.spec.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { loginToApp } from '../../flows/wallet.flow';
-import WalletView from '../../page-objects/wallet/WalletView';
-import FundActionMenu from '../../page-objects/UI/FundActionMenu';
-import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
-import { withFixtures } from '../../framework/fixtures/FixtureHelper';
-import { SmokeRamps } from '../../tags';
-import { CustomNetworks } from '../../resources/networks.e2e';
-import BuildQuoteView from '../../page-objects/Ramps/BuildQuoteView';
-import Assertions from '../../framework/Assertions';
-import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
-import { Mockttp } from 'mockttp';
-import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-mocks';
-
-describe(SmokeRamps('Off-ramp token amounts'), () => {
- beforeEach(async () => {
- jest.setTimeout(150000);
- });
- it('should change token amounts directly and by percentage', async () => {
- const selectedRegion = RampsRegions[RampsRegionsEnum.FRANCE];
-
- await withFixtures(
- {
- fixture: new FixtureBuilder()
- .withNetworkController(CustomNetworks.Tenderly.Mainnet)
- .withRampsSelectedRegion(selectedRegion)
- .withRampsSelectedPaymentMethod()
- .build(),
- restartDevice: true,
- testSpecificMock: async (mockServer: Mockttp) => {
- await setupRegionAwareOnRampMocks(mockServer, selectedRegion);
- },
- },
- async () => {
- await loginToApp();
- await WalletView.tapWalletBuyButton();
- await FundActionMenu.tapSellButton();
- await BuildQuoteView.enterAmount('5');
- await Assertions.expectTextDisplayed('5 ETH');
- await BuildQuoteView.tapKeypadDeleteButton(1);
- await BuildQuoteView.tapQuickAmount25();
- await Assertions.expectTextDisplayed('64 ETH');
- await BuildQuoteView.tapQuickAmount50();
- await Assertions.expectTextDisplayed('128 ETH');
- await BuildQuoteView.tapQuickAmount75();
- await Assertions.expectTextDisplayed('192 ETH');
- await BuildQuoteView.tapQuickAmountMax();
- await Assertions.expectTextNotDisplayed('192 ETH');
- },
- );
- });
-});