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'); - }, - ); - }); -});