diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 10dbcce23c98..6a5d6fe93113 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -128,6 +128,12 @@ app/selectors/earnController @MetaMask/metamask-earn
**/Earn/** @MetaMask/metamask-earn
**/earn/** @MetaMask/metamask-earn
+# Perps Team
+app/components/UI/Perps/ @MetaMask/perps
+app/components/UI/WalletAction/*perps* @MetaMask/perps
+**/Perps/** @MetaMask/perps
+**/perps/** @MetaMask/perps
+
# Assets Team
app/components/hooks/useIsOriginalNativeTokenSymbol @MetaMask/metamask-assets
app/components/hooks/useTokenBalancesController @MetaMask/metamask-assets
diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index 2577a637fa05..99f94b73263c 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -97,6 +97,7 @@ import { AssetLoader } from '../../Views/AssetLoader';
import { EarnScreenStack, EarnModalStack } from '../../UI/Earn/routes';
import { BridgeTransactionDetails } from '../../UI/Bridge/components/TransactionDetails/TransactionDetails';
import { BridgeModalStack, BridgeScreenStack } from '../../UI/Bridge/routes';
+import { PerpsScreenStack } from '../../UI/Perps';
import TurnOnBackupAndSync from '../../Views/Identity/TurnOnBackupAndSync/TurnOnBackupAndSync';
import DeFiProtocolPositionDetails from '../../UI/DeFiPositions/DeFiProtocolPositionDetails';
import UnmountOnBlur from '../../Views/UnmountOnBlur';
@@ -863,6 +864,7 @@ const MainNavigator = () => (
component={StakeModalStack}
options={clearStackNavigatorOptions}
/>
+
{
+ const { theme } = params;
+ const { colors } = theme;
+
+ return StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background.default,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ marginVertical: 16,
+ },
+ headerSpacer: {
+ width: 24,
+ },
+ headerTitle: {
+ flex: 1,
+ textAlign: 'center',
+ },
+ closeButton: {
+ padding: 4,
+ },
+ listContainer: {
+ flex: 1,
+ },
+ listHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ marginBottom: 8,
+ marginTop: 30,
+ },
+ listHeaderLeft: {
+ flex: 1,
+ },
+ listHeaderRight: {
+ flex: 1,
+ alignItems: 'flex-end',
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 24,
+ },
+ errorText: {
+ textAlign: 'center',
+ marginBottom: 16,
+ },
+ flashListContent: {
+ paddingBottom: 16,
+ },
+ skeletonContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ minHeight: 88,
+ },
+ skeletonLeftSection: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ skeletonAvatar: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ marginRight: 16,
+ },
+ skeletonTokenInfo: {
+ flex: 1,
+ },
+ skeletonTokenHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 6,
+ },
+ skeletonTokenSymbol: {
+ width: 60,
+ height: 16,
+ borderRadius: 4,
+ marginRight: 8,
+ },
+ skeletonLeverage: {
+ width: 30,
+ height: 14,
+ borderRadius: 4,
+ },
+ skeletonVolume: {
+ width: 80,
+ height: 12,
+ borderRadius: 4,
+ },
+ skeletonRightSection: {
+ alignItems: 'flex-end',
+ flex: 1,
+ },
+ skeletonPrice: {
+ width: 90,
+ height: 16,
+ borderRadius: 4,
+ marginBottom: 6,
+ },
+ skeletonChange: {
+ width: 70,
+ height: 14,
+ borderRadius: 4,
+ },
+ animatedListContainer: {
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ searchContainer: {
+ paddingHorizontal: 16,
+ },
+ searchInputContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: colors.background.muted,
+ borderRadius: 12,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ searchIcon: {
+ marginRight: 10,
+ color: colors.icon.muted,
+ },
+ searchInput: {
+ flex: 1,
+ fontSize: 16,
+ color: colors.text.default,
+ },
+ clearButton: {
+ padding: 4,
+ marginLeft: 8,
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx b/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx
new file mode 100644
index 000000000000..48e3eb0fa003
--- /dev/null
+++ b/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx
@@ -0,0 +1,233 @@
+import React, { useEffect, useRef, useState, useMemo } from 'react';
+import {
+ View,
+ TouchableOpacity,
+ SafeAreaView,
+ Animated,
+ TextInput,
+} from 'react-native';
+import { FlashList } from '@shopify/flash-list';
+import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
+import { useStyles } from '../../../../../component-library/hooks';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../../component-library/components/Texts/Text';
+import Icon, {
+ IconName,
+ IconSize,
+} from '../../../../../component-library/components/Icons/Icon';
+import { strings } from '../../../../../../locales/i18n';
+import PerpsMarketRowItem from '../../components/PerpsMarketRowItem';
+import {
+ PerpsMarketListViewProps,
+ PerpsMarketData,
+} from './PerpsMarketListView.types';
+import { usePerpsMarkets } from '../../hooks/usePerpsMarkets';
+import styleSheet from './PerpsMarketListView.styles';
+import { useNavigation } from '@react-navigation/native';
+
+const PerpsMarketRowItemSkeleton = () => {
+ const { styles, theme } = useStyles(styleSheet, {});
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const PerpsMarketListHeader = () => {
+ const { styles } = useStyles(styleSheet, {});
+
+ return (
+
+
+
+ {strings('perps.token_volume')}
+
+
+
+
+ {strings('perps.last_price_24h_change')}
+
+
+
+ );
+};
+
+const PerpsMarketListView = ({ onMarketSelect }: PerpsMarketListViewProps) => {
+ const { styles, theme } = useStyles(styleSheet, {});
+ const fadeAnimation = useRef(new Animated.Value(0)).current;
+ const [searchQuery, setSearchQuery] = useState('');
+
+ const navigation = useNavigation();
+
+ const { markets, isLoading, error, refresh, isRefreshing } = usePerpsMarkets({
+ enablePolling: false,
+ });
+
+ useEffect(() => {
+ if (markets.length > 0) {
+ Animated.timing(fadeAnimation, {
+ toValue: 1,
+ duration: 300,
+ useNativeDriver: true,
+ }).start();
+ }
+ }, [markets.length, fadeAnimation]);
+
+ const handleMarketPress = (market: PerpsMarketData) => {
+ onMarketSelect?.(market);
+ };
+
+ const handleRefresh = () => {
+ if (!isRefreshing) {
+ refresh();
+ }
+ };
+
+ const handleClose = () => {
+ if (navigation.canGoBack()) {
+ navigation.goBack();
+ }
+ };
+
+ const filteredMarkets = useMemo(() => {
+ if (!searchQuery.trim()) {
+ return markets;
+ }
+ const query = searchQuery.toLowerCase().trim();
+ return markets.filter(
+ (market) =>
+ market.symbol.toLowerCase().includes(query) ||
+ market.name.toLowerCase().includes(query),
+ );
+ }, [markets, searchQuery]);
+
+ const renderMarketList = () => {
+ // Skeleton List
+ if (filteredMarkets.length === 0 && isLoading) {
+ return (
+
+
+ {Array.from({ length: 8 }).map((_, index) => (
+ //Using index as key is fine here because the list is static
+ // eslint-disable-next-line react/no-array-index-key
+
+
+
+ ))}
+
+ );
+ }
+
+ // Error (Failed to load markets)
+ if (error && filteredMarkets.length === 0) {
+ return (
+
+
+ {strings('perps.failed_to_load_market_data')}
+
+
+
+ {strings('perps.tap_to_retry')}
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+ (
+
+ )}
+ keyExtractor={(item) => item.symbol}
+ contentContainerStyle={styles.flashListContent}
+ estimatedItemSize={80}
+ refreshing={isRefreshing}
+ onRefresh={handleRefresh}
+ />
+
+ >
+ );
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ {strings('perps.perpetual_markets')}
+
+
+
+
+
+ {/* Search Bar */}
+
+
+
+
+ {searchQuery.length > 0 && (
+ setSearchQuery('')}
+ style={styles.clearButton}
+ >
+
+
+ )}
+
+
+ {renderMarketList()}
+
+
+ );
+};
+
+export default PerpsMarketListView;
diff --git a/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.types.ts b/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.types.ts
new file mode 100644
index 000000000000..a3eb8228a082
--- /dev/null
+++ b/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.types.ts
@@ -0,0 +1,59 @@
+// Import HyperLiquid SDK types
+import type {
+ PerpsUniverse,
+ PerpsAssetCtx,
+ AllMids,
+} from '@deeeed/hyperliquid-node20';
+
+/**
+ * Market data for perps trading (UI-friendly format)
+ */
+export interface PerpsMarketData {
+ /**
+ * Token symbol (e.g., 'BTC', 'ETH')
+ */
+ symbol: string;
+ /**
+ * Full token name
+ */
+ name: string;
+ /**
+ * Maximum leverage available (e.g., '40x', '25x')
+ */
+ maxLeverage: string;
+ /**
+ * Current price as formatted string
+ */
+ price: string;
+ /**
+ * 24h price change as formatted string
+ */
+ change24h: string;
+ /**
+ * 24h price change percentage
+ */
+ change24hPercent: string;
+ /**
+ * Trading volume as formatted string
+ */
+ volume: string;
+}
+
+/**
+ * Raw market data from HyperLiquid SDK
+ */
+export interface HyperLiquidMarketData {
+ universe: PerpsUniverse[];
+ assetCtxs: PerpsAssetCtx[];
+ allMids: AllMids;
+}
+
+/**
+ * Props for PerpsMarketListView component
+ */
+export interface PerpsMarketListViewProps {
+ /**
+ * Callback when a market row is selected
+ */
+ onMarketSelect?: (market: PerpsMarketData) => void;
+}
diff --git a/app/components/UI/Perps/Views/PerpsView.test.tsx b/app/components/UI/Perps/Views/PerpsView.test.tsx
new file mode 100644
index 000000000000..a8369a4629f1
--- /dev/null
+++ b/app/components/UI/Perps/Views/PerpsView.test.tsx
@@ -0,0 +1,282 @@
+import React from 'react';
+import { render, fireEvent, waitFor } from '@testing-library/react-native';
+import PerpsView from './PerpsView';
+
+// Mock Hyperliquid SDK
+const mockAllMids = jest.fn();
+const mockMeta = jest.fn();
+const mockUnsubscribe = jest.fn();
+const mockAllMidsSubscription = jest.fn();
+
+jest.mock('@deeeed/hyperliquid-node20', () => ({
+ HttpTransport: jest.fn().mockImplementation(() => ({})),
+ InfoClient: jest.fn().mockImplementation(() => ({
+ allMids: mockAllMids,
+ meta: mockMeta,
+ })),
+ WebSocketTransport: jest.fn().mockImplementation(() => ({})),
+ SubscriptionClient: jest.fn().mockImplementation(() => ({
+ allMids: mockAllMidsSubscription,
+ })),
+}));
+
+describe('PerpsView', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.clearAllTimers();
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('should render correctly', () => {
+ const { toJSON } = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should render header and buttons correctly', () => {
+ const { getByText } = render();
+
+ expect(getByText('Perps Trading (Development)')).toBeTruthy();
+ expect(getByText('Test @deeeed/hyperliquid-node20 SDK')).toBeTruthy();
+ expect(getByText('Test SDK Connection')).toBeTruthy();
+ expect(getByText('Test Asset Listing')).toBeTruthy();
+ expect(getByText('Test WebSocket Connection')).toBeTruthy();
+ });
+
+ describe('SDK Connection Test', () => {
+ it('should handle successful SDK connection test', async () => {
+ const mockMarketData = { BTC: 50000, ETH: 3000 };
+ mockAllMids.mockResolvedValueOnce(mockMarketData);
+
+ const { getByText } = render();
+ const testButton = getByText('Test SDK Connection');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(mockAllMids).toHaveBeenCalled();
+ expect(getByText(/✅ SDK connection successful!/)).toBeTruthy();
+ });
+ });
+
+ it('should handle SDK connection test with no data', async () => {
+ mockAllMids.mockResolvedValueOnce(null);
+
+ const { getByText } = render();
+ const testButton = getByText('Test SDK Connection');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(mockAllMids).toHaveBeenCalled();
+ expect(getByText(/❌ SDK connected but no data received/)).toBeTruthy();
+ });
+ });
+
+ it('should handle SDK connection test failure', async () => {
+ const error = new Error('Network error');
+ mockAllMids.mockRejectedValueOnce(error);
+
+ const { getByText } = render();
+ const testButton = getByText('Test SDK Connection');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(mockAllMids).toHaveBeenCalled();
+ expect(getByText(/❌ SDK test failed: Network error/)).toBeTruthy();
+ });
+ });
+ });
+
+ describe('Asset Listing Test', () => {
+ it('should handle successful asset listing test', async () => {
+ const mockPerpsMeta = {
+ universe: [{ name: 'BTC' }, { name: 'ETH' }, { name: 'SOL' }],
+ };
+ mockMeta.mockResolvedValueOnce(mockPerpsMeta);
+
+ const { getByText } = render();
+ const testButton = getByText('Test Asset Listing');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(mockMeta).toHaveBeenCalled();
+ expect(getByText(/✅ Found 3 tradeable assets:/)).toBeTruthy();
+ expect(getByText(/BTC, ETH, SOL/)).toBeTruthy();
+ });
+ });
+
+ it('should handle asset listing test with no assets', async () => {
+ const mockPerpsMeta = { universe: [] };
+ mockMeta.mockResolvedValueOnce(mockPerpsMeta);
+
+ const { getByText } = render();
+ const testButton = getByText('Test Asset Listing');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(mockMeta).toHaveBeenCalled();
+ expect(getByText(/❌ No assets found/)).toBeTruthy();
+ });
+ });
+
+ it('should handle asset listing test failure', async () => {
+ const error = new Error('API error');
+ mockMeta.mockRejectedValueOnce(error);
+
+ const { getByText } = render();
+ const testButton = getByText('Test Asset Listing');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(mockMeta).toHaveBeenCalled();
+ expect(getByText(/❌ Asset listing failed: API error/)).toBeTruthy();
+ });
+ });
+
+ it('should truncate asset list when more than 5 assets', async () => {
+ const mockPerpsMeta = {
+ universe: [
+ { name: 'BTC' },
+ { name: 'ETH' },
+ { name: 'SOL' },
+ { name: 'ADA' },
+ { name: 'DOT' },
+ { name: 'LINK' },
+ { name: 'UNI' },
+ ],
+ };
+ mockMeta.mockResolvedValueOnce(mockPerpsMeta);
+
+ const { getByText } = render();
+ const testButton = getByText('Test Asset Listing');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(getByText(/✅ Found 7 tradeable assets:/)).toBeTruthy();
+ expect(getByText(/\.\.\./)).toBeTruthy();
+ });
+ });
+ });
+
+ describe('WebSocket Connection Test', () => {
+ it('should handle successful WebSocket connection test', async () => {
+ const mockData = { BTC: 50000, ETH: 3000 };
+ const mockSubscription = { unsubscribe: mockUnsubscribe };
+
+ mockAllMidsSubscription.mockImplementation((callback) => {
+ setTimeout(() => callback(mockData), 50);
+ return Promise.resolve(mockSubscription);
+ });
+ mockUnsubscribe.mockResolvedValueOnce(undefined);
+
+ const { getByText } = render();
+ const testButton = getByText('Test WebSocket Connection');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(mockAllMidsSubscription).toHaveBeenCalled();
+ });
+
+ jest.advanceTimersByTime(60);
+
+ await waitFor(() => {
+ expect(getByText(/✅ WebSocket connection successful!/)).toBeTruthy();
+ expect(
+ getByText(/Received real-time market data with 2 assets/),
+ ).toBeTruthy();
+ });
+
+ jest.advanceTimersByTime(150);
+
+ await waitFor(() => {
+ expect(mockUnsubscribe).toHaveBeenCalled();
+ });
+ });
+
+ it('should handle WebSocket connection timeout', async () => {
+ const mockSubscription = { unsubscribe: mockUnsubscribe };
+
+ mockAllMidsSubscription.mockImplementation(() =>
+ Promise.resolve(mockSubscription),
+ );
+ mockUnsubscribe.mockResolvedValueOnce(undefined);
+
+ const { getByText } = render();
+ const testButton = getByText('Test WebSocket Connection');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(mockAllMidsSubscription).toHaveBeenCalled();
+ });
+
+ jest.advanceTimersByTime(5000);
+
+ await waitFor(() => {
+ expect(getByText(/⚠️ WebSocket connection timeout/)).toBeTruthy();
+ expect(mockUnsubscribe).toHaveBeenCalled();
+ });
+ });
+
+ it('should handle WebSocket connection test failure', async () => {
+ const error = new Error('WebSocket error');
+ mockAllMidsSubscription.mockRejectedValueOnce(error);
+
+ const { getByText } = render();
+ const testButton = getByText('Test WebSocket Connection');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(mockAllMidsSubscription).toHaveBeenCalled();
+ expect(
+ getByText(/❌ WebSocket test failed: WebSocket error/),
+ ).toBeTruthy();
+ });
+ });
+ });
+
+ it('should show loading state during tests', async () => {
+ mockAllMids.mockImplementation(
+ () => new Promise((resolve) => setTimeout(resolve, 1000)),
+ );
+
+ const { getByText } = render();
+ const testButton = getByText('Test SDK Connection');
+
+ fireEvent.press(testButton);
+
+ // Note: In a real test environment, you would check for loading indicators
+ // For this component, the loading state is handled by the Button component itself
+ expect(mockAllMids).toHaveBeenCalled();
+ });
+
+ it('should not display result container when no test result', () => {
+ const { queryByText } = render();
+
+ expect(queryByText('Test Result:')).toBeNull();
+ });
+
+ it('should handle unknown errors gracefully', async () => {
+ mockAllMids.mockRejectedValueOnce('Unknown error string');
+
+ const { getByText } = render();
+ const testButton = getByText('Test SDK Connection');
+
+ fireEvent.press(testButton);
+
+ await waitFor(() => {
+ expect(getByText(/❌ SDK test failed: Unknown error/)).toBeTruthy();
+ });
+ });
+});
diff --git a/app/components/UI/Perps/Views/PerpsView.tsx b/app/components/UI/Perps/Views/PerpsView.tsx
new file mode 100644
index 000000000000..87cc1e397d54
--- /dev/null
+++ b/app/components/UI/Perps/Views/PerpsView.tsx
@@ -0,0 +1,272 @@
+import React, { useState } from 'react';
+import { View } from 'react-native';
+import { useStyles } from '../../../../component-library/hooks';
+import Button, {
+ ButtonVariants,
+ ButtonSize,
+ ButtonWidthTypes,
+} from '../../../../component-library/components/Buttons/Button';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../component-library/components/Texts/Text';
+import ScreenView from '../../../Base/ScreenView';
+import Logger from '../../../../util/Logger';
+
+// Import Hyperliquid SDK components
+import {
+ HttpTransport,
+ InfoClient,
+ WebSocketTransport,
+ SubscriptionClient,
+} from '@deeeed/hyperliquid-node20';
+import { PERPS_CONSTANTS } from '../constants';
+import Routes from '../../../../constants/navigation/Routes';
+import { useNavigation } from '@react-navigation/native';
+
+interface PerpsViewProps {}
+
+const styleSheet = () => ({
+ content: {
+ flex: 1,
+ paddingHorizontal: 24,
+ paddingTop: 24,
+ paddingBottom: 32,
+ },
+ headerContainer: {
+ alignItems: 'center' as const,
+ marginBottom: 32,
+ },
+ buttonContainer: {
+ marginBottom: 32,
+ gap: 16,
+ },
+ resultContainer: {
+ padding: 16,
+ borderRadius: 8,
+ marginTop: 16,
+ },
+ resultText: {
+ marginTop: 8,
+ lineHeight: 20,
+ },
+});
+
+const PerpsView: React.FC = () => {
+ const { styles } = useStyles(styleSheet, {});
+ const [isLoading, setIsLoading] = useState(false);
+ const [testResult, setTestResult] = useState('');
+
+ const navigation = useNavigation();
+
+ const testSDKConnection = async () => {
+ setIsLoading(true);
+ setTestResult('');
+
+ try {
+ Logger.log('Perps: Testing SDK connection...');
+ // Create HTTP transport and InfoClient
+ const transport = new HttpTransport();
+ const infoClient = new InfoClient({ transport });
+
+ // Test basic functionality - get all mids (prices)
+ const allMids = await infoClient.allMids();
+
+ if (allMids) {
+ const successMessage =
+ '✅ SDK connection successful!\nRetrieved market data from Hyperliquid';
+ setTestResult(successMessage);
+ Logger.log('Perps: SDK test successful', {
+ dataCount: Object.keys(allMids).length,
+ });
+ } else {
+ const warningMessage = '❌ SDK connected but no data received';
+ setTestResult(warningMessage);
+ Logger.log('Perps: SDK connected but no data received');
+ }
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Unknown error';
+ const fullErrorMessage = `❌ SDK test failed: ${errorMessage}`;
+ setTestResult(fullErrorMessage);
+ Logger.log('Perps: SDK test failed', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const testAssetListing = async () => {
+ setIsLoading(true);
+ setTestResult('');
+
+ try {
+ Logger.log('Perps: Testing asset listing...');
+ const transport = new HttpTransport();
+ const infoClient = new InfoClient({ transport });
+
+ // Test asset listing functionality - get perps meta data
+ const perpsMeta = await infoClient.meta();
+
+ if (perpsMeta?.universe && perpsMeta.universe.length > 0) {
+ const assets = perpsMeta.universe;
+ const successMessage = `✅ Found ${
+ assets.length
+ } tradeable assets:\n${assets
+ .slice(0, 5)
+ .map((asset: { name: string }) => asset.name)
+ .join(', ')}${assets.length > 5 ? '...' : ''}`;
+ setTestResult(successMessage);
+ Logger.log('Perps: Asset listing successful', { count: assets.length });
+ } else {
+ const warningMessage = '❌ No assets found';
+ setTestResult(warningMessage);
+ Logger.log('Perps: No assets found');
+ }
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Unknown error';
+ const fullErrorMessage = `❌ Asset listing failed: ${errorMessage}`;
+ setTestResult(fullErrorMessage);
+ Logger.log('Perps: Asset listing failed', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const testWebSocketConnection = async () => {
+ setIsLoading(true);
+ setTestResult('');
+ let hasReceivedData = false;
+
+ try {
+ Logger.log('Perps: Testing WebSocket connection...');
+ const transport = new WebSocketTransport();
+ const subsClient = new SubscriptionClient({ transport });
+
+ // Test WebSocket connection with a simple subscription
+ const subscription = await subsClient.allMids((data) => {
+ if (!hasReceivedData) {
+ hasReceivedData = true;
+ Logger.log('Perps: WebSocket data received', {
+ dataKeys: Object.keys(data).length,
+ });
+
+ const successMessage = `✅ WebSocket connection successful!\nReceived real-time market data with ${
+ Object.keys(data).length
+ } assets`;
+ setTestResult(successMessage);
+
+ // Unsubscribe after receiving first data
+ setTimeout(async () => {
+ try {
+ await subscription.unsubscribe();
+ Logger.log('Perps: WebSocket subscription cleaned up');
+ setIsLoading(false);
+ } catch (error) {
+ Logger.log('Perps: Error unsubscribing', error);
+ setIsLoading(false);
+ }
+ }, PERPS_CONSTANTS.WEBSOCKET_CLEANUP_DELAY);
+ }
+ });
+
+ // Reduce timeout to 5 seconds and check connection status
+ setTimeout(async () => {
+ if (!hasReceivedData) {
+ try {
+ await subscription.unsubscribe();
+ const timeoutMessage =
+ '⚠️ WebSocket connection timeout - no data received within 5 seconds\nThis might be normal if the market is closed';
+ setTestResult(timeoutMessage);
+ Logger.log(
+ 'Perps: WebSocket connection timeout - may be market hours related',
+ );
+ } catch (error) {
+ Logger.log('Perps: Error during timeout cleanup', error);
+ }
+ setIsLoading(false);
+ }
+ }, 5000);
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Unknown error';
+ const fullErrorMessage = `❌ WebSocket test failed: ${errorMessage}`;
+ setTestResult(fullErrorMessage);
+ Logger.log('Perps: WebSocket test failed', error);
+ setIsLoading(false);
+ }
+ };
+
+ const openPerpsMarketList = () => {
+ navigation.navigate(Routes.PERPS.MARKETS_LIST);
+ };
+
+ return (
+
+
+
+
+ Perps Trading (Development)
+
+
+ Test @deeeed/hyperliquid-node20 SDK
+
+
+
+
+
+
+
+
+
+
+
+
+ {testResult ? (
+
+
+ Test Result:
+
+
+ {testResult}
+
+
+ ) : null}
+
+
+ );
+};
+
+export default PerpsView;
diff --git a/app/components/UI/Perps/Views/__snapshots__/PerpsView.test.tsx.snap b/app/components/UI/Perps/Views/__snapshots__/PerpsView.test.tsx.snap
new file mode 100644
index 000000000000..4c1ed790c933
--- /dev/null
+++ b/app/components/UI/Perps/Views/__snapshots__/PerpsView.test.tsx.snap
@@ -0,0 +1,187 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PerpsView should render correctly 1`] = `
+
+
+
+
+
+
+ Perps Trading (Development)
+
+
+ Test @deeeed/hyperliquid-node20 SDK
+
+
+
+
+
+ Test SDK Connection
+
+
+
+
+ Test Asset Listing
+
+
+
+
+ Test WebSocket Connection
+
+
+
+
+
+
+
+`;
diff --git a/app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.styles.ts b/app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.styles.ts
new file mode 100644
index 000000000000..ea6dcc928ce4
--- /dev/null
+++ b/app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.styles.ts
@@ -0,0 +1,57 @@
+import { StyleSheet } from 'react-native';
+import { Theme } from '../../../../../util/theme/models';
+
+const styleSheet = (params: { theme: Theme }) => {
+ const { theme } = params;
+ const { colors } = theme;
+
+ return StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: 12,
+ backgroundColor: colors.background.default,
+ },
+ leftSection: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ tokenInfo: {
+ flex: 1,
+ },
+ tokenHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ tokenVolume: {
+ marginTop: 2,
+ },
+ rightSection: {
+ alignItems: 'flex-end',
+ flex: 1,
+ },
+ priceInfo: {
+ alignItems: 'flex-end',
+ },
+ price: {
+ marginBottom: 2,
+ },
+ priceChange: {
+ marginTop: 2,
+ },
+ leverageContainer: {
+ backgroundColor: colors.background.muted,
+ paddingVertical: 2,
+ paddingHorizontal: 4,
+ borderRadius: 2,
+ },
+ networkAvatar: {
+ marginRight: 16,
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.tsx b/app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.tsx
new file mode 100644
index 000000000000..f0d5c816daca
--- /dev/null
+++ b/app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { View, TouchableOpacity } from 'react-native';
+import { useStyles } from '../../../../../component-library/hooks';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../../component-library/components/Texts/Text';
+import { PerpsMarketRowItemProps } from './PerpsMarketRowItem.types';
+import styleSheet from './PerpsMarketRowItem.styles';
+import Avatar, {
+ AvatarSize,
+ AvatarVariant,
+} from '../../../../../component-library/components/Avatars/Avatar';
+
+const PerpsMarketRowItem = ({ market, onPress }: PerpsMarketRowItemProps) => {
+ const { styles } = useStyles(styleSheet, {});
+
+ const handlePress = () => {
+ onPress?.(market);
+ };
+
+ const isPositiveChange = !market.change24h.startsWith('-');
+
+ return (
+
+
+
+
+
+
+ {market.symbol}
+
+
+
+ {market.maxLeverage}
+
+
+
+
+ {market.volume}
+
+
+
+
+
+
+
+ {market.price}
+
+
+ {market.change24h} ({market.change24hPercent})
+
+
+
+
+ );
+};
+
+export default PerpsMarketRowItem;
diff --git a/app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.types.ts b/app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.types.ts
new file mode 100644
index 000000000000..334232a16693
--- /dev/null
+++ b/app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.types.ts
@@ -0,0 +1,15 @@
+import { PerpsMarketData } from '../../Views/PerpsMarketListView/PerpsMarketListView.types';
+
+/**
+ * Props for PerpsMarketRowItem component
+ */
+export interface PerpsMarketRowItemProps {
+ /**
+ * Market data to display in the row
+ */
+ market: PerpsMarketData;
+ /**
+ * Callback when the row is pressed
+ */
+ onPress?: (market: PerpsMarketData) => void;
+}
diff --git a/app/components/UI/Perps/components/PerpsMarketRowItem/index.ts b/app/components/UI/Perps/components/PerpsMarketRowItem/index.ts
new file mode 100644
index 000000000000..ed634787007c
--- /dev/null
+++ b/app/components/UI/Perps/components/PerpsMarketRowItem/index.ts
@@ -0,0 +1,2 @@
+export { default } from './PerpsMarketRowItem';
+export type { PerpsMarketRowItemProps } from './PerpsMarketRowItem.types';
\ No newline at end of file
diff --git a/app/components/UI/Perps/constants/index.ts b/app/components/UI/Perps/constants/index.ts
new file mode 100644
index 000000000000..1e309a38d522
--- /dev/null
+++ b/app/components/UI/Perps/constants/index.ts
@@ -0,0 +1,9 @@
+/**
+ * Perps feature constants
+ */
+export const PERPS_CONSTANTS = {
+ FEATURE_FLAG_KEY: 'perpsEnabled',
+ WEBSOCKET_TIMEOUT: 5000, // 5 seconds
+ WEBSOCKET_CLEANUP_DELAY: 1000, // 1 second
+ DEFAULT_ASSET_PREVIEW_LIMIT: 5,
+} as const;
diff --git a/app/components/UI/Perps/constants/marketIcons.ts b/app/components/UI/Perps/constants/marketIcons.ts
new file mode 100644
index 000000000000..d55fb039717c
--- /dev/null
+++ b/app/components/UI/Perps/constants/marketIcons.ts
@@ -0,0 +1,1132 @@
+import { PerpsMarketIconData } from '../types';
+
+// Complete icon mapping for all Hyperliquid markets
+export const HYPERLIQUID_ICONS: Record = {
+ // Major Cryptocurrencies
+ BTC: {
+ symbol: 'BTC',
+ iconUrl: 'https://assets.coingecko.com/coins/images/1/large/bitcoin.png',
+ coinGeckoId: 'bitcoin',
+ category: 'major',
+ },
+ ETH: {
+ symbol: 'ETH',
+ iconUrl: 'https://assets.coingecko.com/coins/images/279/large/ethereum.png',
+ coinGeckoId: 'ethereum',
+ category: 'major',
+ },
+ SOL: {
+ symbol: 'SOL',
+ iconUrl: 'https://assets.coingecko.com/coins/images/4128/large/solana.png',
+ coinGeckoId: 'solana',
+ category: 'layer1',
+ },
+ AVAX: {
+ symbol: 'AVAX',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12559/large/avalanche-avax-logo.png',
+ coinGeckoId: 'avalanche-2',
+ category: 'layer1',
+ },
+ DOT: {
+ symbol: 'DOT',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12171/large/polkadot.png',
+ coinGeckoId: 'polkadot',
+ category: 'layer1',
+ },
+ ADA: {
+ symbol: 'ADA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/975/large/cardano.png',
+ coinGeckoId: 'cardano',
+ category: 'layer1',
+ },
+ LTC: {
+ symbol: 'LTC',
+ iconUrl: 'https://assets.coingecko.com/coins/images/2/large/litecoin.png',
+ coinGeckoId: 'litecoin',
+ category: 'major',
+ },
+ BCH: {
+ symbol: 'BCH',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/780/large/bitcoin-cash-circle.png',
+ coinGeckoId: 'bitcoin-cash',
+ category: 'major',
+ },
+ XRP: {
+ symbol: 'XRP',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/44/large/xrp-symbol-white-128.png',
+ coinGeckoId: 'ripple',
+ category: 'major',
+ },
+
+ // Stablecoins & Gold
+ PAXG: {
+ symbol: 'PAXG',
+ iconUrl: 'https://assets.coingecko.com/coins/images/9519/large/paxgold.png',
+ coinGeckoId: 'pax-gold',
+ category: 'stablecoin',
+ },
+
+ // DeFi Tokens
+ AAVE: {
+ symbol: 'AAVE',
+ iconUrl: 'https://assets.coingecko.com/coins/images/12645/large/AAVE.png',
+ coinGeckoId: 'aave',
+ category: 'defi',
+ },
+ CRV: {
+ symbol: 'CRV',
+ iconUrl: 'https://assets.coingecko.com/coins/images/12124/large/Curve.png',
+ coinGeckoId: 'curve-dao-token',
+ category: 'defi',
+ },
+ ONDO: {
+ symbol: 'ONDO',
+ iconUrl: 'https://assets.coingecko.com/coins/images/26580/large/ONDO.png',
+ coinGeckoId: 'ondo-finance',
+ category: 'defi',
+ },
+ LINK: {
+ symbol: 'LINK',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/877/large/chainlink-new-logo.png',
+ coinGeckoId: 'chainlink',
+ category: 'defi',
+ },
+ UNI: {
+ symbol: 'UNI',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12504/large/uniswap-uni.png',
+ coinGeckoId: 'uniswap',
+ category: 'defi',
+ },
+ COMP: {
+ symbol: 'COMP',
+ iconUrl: 'https://assets.coingecko.com/coins/images/10775/large/COMP.png',
+ coinGeckoId: 'compound',
+ category: 'defi',
+ },
+ MKR: {
+ symbol: 'MKR',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/1364/large/Mark_Maker.png',
+ coinGeckoId: 'maker',
+ category: 'defi',
+ },
+ LDO: {
+ symbol: 'LDO',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/13573/large/Lido_DAO.png',
+ coinGeckoId: 'lido-dao',
+ category: 'defi',
+ },
+ SNX: {
+ symbol: 'SNX',
+ iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png',
+ coinGeckoId: 'havven',
+ category: 'defi',
+ },
+ SUSHI: {
+ symbol: 'SUSHI',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12271/large/512x512_Logo_no_chop.png',
+ coinGeckoId: 'sushi',
+ category: 'defi',
+ },
+ MORPHO: {
+ symbol: 'MORPHO',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34129/large/morpho.png',
+ coinGeckoId: 'morpho-token',
+ category: 'defi',
+ },
+ PENDLE: {
+ symbol: 'PENDLE',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/15069/large/Pendle_Logo_Normal-03.png',
+ coinGeckoId: 'pendle',
+ category: 'defi',
+ },
+ BLUR: {
+ symbol: 'BLUR',
+ iconUrl: 'https://assets.coingecko.com/coins/images/28453/large/blur.png',
+ coinGeckoId: 'blur',
+ category: 'defi',
+ },
+ ENA: {
+ symbol: 'ENA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36530/large/ethena.png',
+ coinGeckoId: 'ethena',
+ category: 'defi',
+ },
+ EIGEN: {
+ symbol: 'EIGEN',
+ iconUrl: 'https://assets.coingecko.com/coins/images/37024/large/eigen.png',
+ coinGeckoId: 'eigenlayer',
+ category: 'defi',
+ },
+ ETHFI: {
+ symbol: 'ETHFI',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/35958/large/etherfi.png',
+ coinGeckoId: 'ether-fi',
+ category: 'defi',
+ },
+
+ // Meme Coins
+ DOGE: {
+ symbol: 'DOGE',
+ iconUrl: 'https://assets.coingecko.com/coins/images/5/large/dogecoin.png',
+ coinGeckoId: 'dogecoin',
+ category: 'meme',
+ },
+ kPEPE: {
+ symbol: 'kPEPE',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/29850/large/pepe-token.jpeg',
+ coinGeckoId: 'pepe',
+ category: 'meme',
+ },
+ kBONK: {
+ symbol: 'kBONK',
+ iconUrl: 'https://assets.coingecko.com/coins/images/28600/large/bonk.jpg',
+ coinGeckoId: 'bonk',
+ category: 'meme',
+ },
+ kFLOKI: {
+ symbol: 'kFLOKI',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/16746/large/PNG_image.png',
+ coinGeckoId: 'floki',
+ category: 'meme',
+ },
+ kSHIB: {
+ symbol: 'kSHIB',
+ iconUrl: 'https://assets.coingecko.com/coins/images/11939/large/shiba.png',
+ coinGeckoId: 'shiba-inu',
+ category: 'meme',
+ },
+ WIF: {
+ symbol: 'WIF',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/33767/large/dogwifhat.jpg',
+ coinGeckoId: 'dogwifcoin',
+ category: 'meme',
+ },
+ GOAT: {
+ symbol: 'GOAT',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/33696/large/Goatseus_Maximus.jpg',
+ coinGeckoId: 'goatseus-maximus',
+ category: 'meme',
+ },
+ CHILLGUY: {
+ symbol: 'CHILLGUY',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/33687/large/photo_2024-11-15_17-01-30.jpg',
+ coinGeckoId: 'just-a-chill-guy',
+ category: 'meme',
+ },
+ POPCAT: {
+ symbol: 'POPCAT',
+ iconUrl: 'https://assets.coingecko.com/coins/images/30323/large/popcat.png',
+ coinGeckoId: 'popcat',
+ category: 'meme',
+ },
+ MOODENG: {
+ symbol: 'MOODENG',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/32473/large/moodeng.webp',
+ coinGeckoId: 'moo-deng',
+ category: 'meme',
+ },
+ PNUT: {
+ symbol: 'PNUT',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/33439/large/peanut-the-squirrel.png',
+ coinGeckoId: 'peanut-the-squirrel',
+ category: 'meme',
+ },
+ MEW: {
+ symbol: 'MEW',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36478/large/mew.png',
+ coinGeckoId: 'cat-in-a-dogs-world',
+ category: 'meme',
+ },
+ TURBO: {
+ symbol: 'TURBO',
+ iconUrl: 'https://assets.coingecko.com/coins/images/30059/large/turbos.png',
+ coinGeckoId: 'turbo',
+ category: 'meme',
+ },
+ BOME: {
+ symbol: 'BOME',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35547/large/book.png',
+ coinGeckoId: 'book-of-meme',
+ category: 'meme',
+ },
+ FARTCOIN: {
+ symbol: 'FARTCOIN',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/34629/large/fartcoin.png',
+ coinGeckoId: 'fartcoin',
+ category: 'meme',
+ },
+ NOT: {
+ symbol: 'NOT',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35119/large/not.png',
+ coinGeckoId: 'notcoin',
+ category: 'meme',
+ },
+ BRETT: {
+ symbol: 'BRETT',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35464/large/brett.png',
+ coinGeckoId: 'based-brett',
+ category: 'meme',
+ },
+ GALA: {
+ symbol: 'GALA',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12493/large/GALA-v2.png',
+ coinGeckoId: 'gala',
+ category: 'gaming',
+ },
+ PENGU: {
+ symbol: 'PENGU',
+ iconUrl: 'https://assets.coingecko.com/coins/images/37430/large/pengu.png',
+ coinGeckoId: 'pudgy-penguins',
+ category: 'meme',
+ },
+ MEME: {
+ symbol: 'MEME',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/32537/large/memecoin.png',
+ coinGeckoId: 'memecoin',
+ category: 'meme',
+ },
+ ANIME: {
+ symbol: 'ANIME',
+ iconUrl: 'https://assets.coingecko.com/coins/images/33399/large/anime.png',
+ coinGeckoId: 'anime',
+ category: 'meme',
+ },
+ BABY: {
+ symbol: 'BABY',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34127/large/baby.png',
+ coinGeckoId: 'baby-doge-coin',
+ category: 'meme',
+ },
+ kNEIRO: {
+ symbol: 'kNEIRO',
+ iconUrl: 'https://assets.coingecko.com/coins/images/32799/large/neiro.png',
+ coinGeckoId: 'neiro-ethereum',
+ category: 'meme',
+ },
+ NEIROETH: {
+ symbol: 'NEIROETH',
+ iconUrl: 'https://assets.coingecko.com/coins/images/32799/large/neiro.png',
+ coinGeckoId: 'neiro-ethereum',
+ category: 'meme',
+ },
+ kLUNC: {
+ symbol: 'kLUNC',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/8284/large/luna1557227471663.png',
+ coinGeckoId: 'terra-luna',
+ category: 'other',
+ },
+ kDOGS: {
+ symbol: 'kDOGS',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34105/large/dogs.png',
+ coinGeckoId: 'dogs',
+ category: 'meme',
+ },
+ HMSTR: {
+ symbol: 'HMSTR',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/34079/large/hamster.png',
+ coinGeckoId: 'hamster-kombat',
+ category: 'gaming',
+ },
+
+ // Layer 1 & Layer 2
+ TRX: {
+ symbol: 'TRX',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/1094/large/tron-logo.png',
+ coinGeckoId: 'tron',
+ category: 'layer1',
+ },
+ ARB: {
+ symbol: 'ARB',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/16547/large/photo_2023-03-29_21.47.00.jpeg',
+ coinGeckoId: 'arbitrum',
+ category: 'layer2',
+ },
+ OP: {
+ symbol: 'OP',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/25244/large/Optimism.png',
+ coinGeckoId: 'optimism',
+ category: 'layer2',
+ },
+ POL: {
+ symbol: 'POL',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/4713/large/matic-token-icon.png',
+ coinGeckoId: 'matic-network',
+ category: 'layer2',
+ },
+ BNB: {
+ symbol: 'BNB',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/825/large/bnb-icon2_2x.png',
+ coinGeckoId: 'binancecoin',
+ category: 'layer1',
+ },
+ SUI: {
+ symbol: 'SUI',
+ iconUrl: 'https://assets.coingecko.com/coins/images/26375/large/sui.png',
+ coinGeckoId: 'sui',
+ category: 'layer1',
+ },
+ NEAR: {
+ symbol: 'NEAR',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/10365/large/near_icon.png',
+ coinGeckoId: 'near',
+ category: 'layer1',
+ },
+ APT: {
+ symbol: 'APT',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/26455/large/aptos_round.png',
+ coinGeckoId: 'aptos',
+ category: 'layer1',
+ },
+ SEI: {
+ symbol: 'SEI',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/28205/large/Sei_Logo_-_Transparent.png',
+ coinGeckoId: 'sei-network',
+ category: 'layer1',
+ },
+ TON: {
+ symbol: 'TON',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/17980/large/ton_symbol.png',
+ coinGeckoId: 'the-open-network',
+ category: 'layer1',
+ },
+ ATOM: {
+ symbol: 'ATOM',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/1481/large/cosmos_hub.png',
+ coinGeckoId: 'cosmos',
+ category: 'layer1',
+ },
+ TIA: {
+ symbol: 'TIA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/31967/large/tia.jpg',
+ coinGeckoId: 'celestia',
+ category: 'layer1',
+ },
+ INJ: {
+ symbol: 'INJ',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12882/large/Secondary_Symbol.png',
+ coinGeckoId: 'injective-protocol',
+ category: 'layer1',
+ },
+ STRK: {
+ symbol: 'STRK',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/35432/large/starknet.png',
+ coinGeckoId: 'starknet',
+ category: 'layer2',
+ },
+ STX: {
+ symbol: 'STX',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/2069/large/Stacks_logo_full.png',
+ coinGeckoId: 'blockstack',
+ category: 'layer1',
+ },
+ MANTA: {
+ symbol: 'MANTA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/33613/large/manta.png',
+ coinGeckoId: 'manta-network',
+ category: 'layer2',
+ },
+ BLAST: {
+ symbol: 'BLAST',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35494/large/blast.png',
+ coinGeckoId: 'blast',
+ category: 'layer2',
+ },
+ ZK: {
+ symbol: 'ZK',
+ iconUrl: 'https://assets.coingecko.com/coins/images/37007/large/zksync.png',
+ coinGeckoId: 'zksync',
+ category: 'layer2',
+ },
+
+ // AI & Tech
+ RENDER: {
+ symbol: 'RENDER',
+ iconUrl: 'https://assets.coingecko.com/coins/images/11636/large/rndr.png',
+ coinGeckoId: 'render-token',
+ category: 'ai',
+ },
+ FET: {
+ symbol: 'FET',
+ iconUrl: 'https://assets.coingecko.com/coins/images/5681/large/Fetch.jpg',
+ coinGeckoId: 'fetch-ai',
+ category: 'ai',
+ },
+ TAO: {
+ symbol: 'TAO',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/33180/large/bittensor.png',
+ coinGeckoId: 'bittensor',
+ category: 'ai',
+ },
+ AIXBT: {
+ symbol: 'AIXBT',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34092/large/aixbt.png',
+ coinGeckoId: 'aixbt-by-virtuals',
+ category: 'ai',
+ },
+ VIRTUAL: {
+ symbol: 'VIRTUAL',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/34014/large/virtuals.png',
+ coinGeckoId: 'virtuals-protocol',
+ category: 'ai',
+ },
+ AI16Z: {
+ symbol: 'AI16Z',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34118/large/ai16z.png',
+ coinGeckoId: 'ai16z',
+ category: 'ai',
+ },
+ ZEREBRO: {
+ symbol: 'ZEREBRO',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/34043/large/zerebro.png',
+ coinGeckoId: 'zerebro',
+ category: 'ai',
+ },
+
+ // Oracles & Data
+ PYTH: {
+ symbol: 'PYTH',
+ iconUrl: 'https://assets.coingecko.com/coins/images/31833/large/pyth.png',
+ coinGeckoId: 'pyth-network',
+ category: 'defi',
+ },
+ TRB: {
+ symbol: 'TRB',
+ iconUrl: 'https://assets.coingecko.com/coins/images/9644/large/Tellor.png',
+ coinGeckoId: 'tellor',
+ category: 'defi',
+ },
+
+ // Gaming & NFT
+ IMX: {
+ symbol: 'IMX',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/17233/large/immutableX-symbol-BLK-RGB.png',
+ coinGeckoId: 'immutable-x',
+ category: 'gaming',
+ },
+ SAND: {
+ symbol: 'SAND',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12129/large/sandbox_logo.jpg',
+ coinGeckoId: 'the-sandbox',
+ category: 'gaming',
+ },
+ APE: {
+ symbol: 'APE',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/24383/large/apecoin.jpg',
+ coinGeckoId: 'apecoin',
+ category: 'gaming',
+ },
+ ENS: {
+ symbol: 'ENS',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/19785/large/acatxTm8_400x400.jpg',
+ coinGeckoId: 'ethereum-name-service',
+ category: 'defi',
+ },
+ YGG: {
+ symbol: 'YGG',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/17358/large/icon_YGG.png',
+ coinGeckoId: 'yield-guild-games',
+ category: 'gaming',
+ },
+
+ // Specialized/Other
+ HYPE: {
+ symbol: 'HYPE',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/34503/large/hyperliquid.jpg',
+ coinGeckoId: 'hyperliquid',
+ category: 'layer1',
+ },
+ WLD: {
+ symbol: 'WLD',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/31069/large/worldcoin.jpeg',
+ coinGeckoId: 'worldcoin-wld',
+ category: 'other',
+ },
+ FIL: {
+ symbol: 'FIL',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12817/large/filecoin.png',
+ coinGeckoId: 'filecoin',
+ category: 'other',
+ },
+ AR: {
+ symbol: 'AR',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/4343/large/oRt6SiEN_400x400.jpg',
+ coinGeckoId: 'arweave',
+ category: 'other',
+ },
+ ALGO: {
+ symbol: 'ALGO',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/4380/large/download.png',
+ coinGeckoId: 'algorand',
+ category: 'layer1',
+ },
+ IOTA: {
+ symbol: 'IOTA',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/692/large/IOTA_Swirl.png',
+ coinGeckoId: 'iota',
+ category: 'layer1',
+ },
+ HBAR: {
+ symbol: 'HBAR',
+ iconUrl: 'https://assets.coingecko.com/coins/images/3688/large/hbar.png',
+ coinGeckoId: 'hedera-hashgraph',
+ category: 'layer1',
+ },
+ ETC: {
+ symbol: 'ETC',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/453/large/ethereum-classic-logo.png',
+ coinGeckoId: 'ethereum-classic',
+ category: 'layer1',
+ },
+ BSV: {
+ symbol: 'BSV',
+ iconUrl: 'https://assets.coingecko.com/coins/images/6799/large/BSV.png',
+ coinGeckoId: 'bitcoin-cash-sv',
+ category: 'major',
+ },
+ XLM: {
+ symbol: 'XLM',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/100/large/Stellar_symbol_black_RGB.png',
+ coinGeckoId: 'stellar',
+ category: 'layer1',
+ },
+ MINA: {
+ symbol: 'MINA',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/15628/large/JM4_vQ34_400x400.png',
+ coinGeckoId: 'mina-protocol',
+ category: 'layer1',
+ },
+ ZEN: {
+ symbol: 'ZEN',
+ iconUrl: 'https://assets.coingecko.com/coins/images/691/large/horizen.png',
+ coinGeckoId: 'horizen',
+ category: 'layer1',
+ },
+ NEO: {
+ symbol: 'NEO',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/480/large/NEO_512_512.png',
+ coinGeckoId: 'neo',
+ category: 'layer1',
+ },
+ KAS: {
+ symbol: 'KAS',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/25751/large/kaspa-icon-exchanges.png',
+ coinGeckoId: 'kaspa',
+ category: 'layer1',
+ },
+ CELO: {
+ symbol: 'CELO',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/11090/large/icon-celo-CELO-color-500.png',
+ coinGeckoId: 'celo',
+ category: 'layer1',
+ },
+
+ // DeFi/Exchange Tokens
+ JUP: {
+ symbol: 'JUP',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35220/large/jup.png',
+ coinGeckoId: 'jupiter-exchange-solana',
+ category: 'defi',
+ },
+ JTO: {
+ symbol: 'JTO',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34341/large/jito.png',
+ coinGeckoId: 'jito-governance-token',
+ category: 'defi',
+ },
+ DYDX: {
+ symbol: 'DYDX',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/17500/large/hjnIm9bV.jpg',
+ coinGeckoId: 'dydx',
+ category: 'defi',
+ },
+ GMX: {
+ symbol: 'GMX',
+ iconUrl: 'https://assets.coingecko.com/coins/images/18323/large/arbit.png',
+ coinGeckoId: 'gmx',
+ category: 'defi',
+ },
+ RSR: {
+ symbol: 'RSR',
+ iconUrl: 'https://assets.coingecko.com/coins/images/8365/large/reserve.jpg',
+ coinGeckoId: 'reserve-rights-token',
+ category: 'defi',
+ },
+ UMA: {
+ symbol: 'UMA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/10951/large/UMA.png',
+ coinGeckoId: 'uma',
+ category: 'defi',
+ },
+ FXS: {
+ symbol: 'FXS',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/13423/large/frax_share.png',
+ coinGeckoId: 'frax-share',
+ category: 'defi',
+ },
+ RUNE: {
+ symbol: 'RUNE',
+ iconUrl: 'https://assets.coingecko.com/coins/images/6595/large/RUNE.png',
+ coinGeckoId: 'thorchain',
+ category: 'defi',
+ },
+ CAKE: {
+ symbol: 'CAKE',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12632/large/pancakeswap-cake-logo_.png',
+ coinGeckoId: 'pancakeswap-token',
+ category: 'defi',
+ },
+ STG: {
+ symbol: 'STG',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/24413/large/STG_LOGO.png',
+ coinGeckoId: 'stargate-finance',
+ category: 'defi',
+ },
+ ZRO: {
+ symbol: 'ZRO',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/28206/large/ftxG9_TJ_400x400.jpeg',
+ coinGeckoId: 'layerzero',
+ category: 'defi',
+ },
+
+ // Specialized Project Tokens
+ INIT: {
+ symbol: 'INIT',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34678/large/init.png',
+ coinGeckoId: 'initial-coin',
+ category: 'other',
+ },
+ HYPER: {
+ symbol: 'HYPER',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34702/large/hyper.png',
+ coinGeckoId: 'hyper',
+ category: 'other',
+ },
+ MNT: {
+ symbol: 'MNT',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/30980/large/token-logo.png',
+ coinGeckoId: 'mantle',
+ category: 'layer2',
+ },
+ SYRUP: {
+ symbol: 'SYRUP',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34801/large/syrup.png',
+ coinGeckoId: 'syrup',
+ category: 'other',
+ },
+ FTT: {
+ symbol: 'FTT',
+ iconUrl: 'https://assets.coingecko.com/coins/images/9026/large/F.png',
+ coinGeckoId: 'ftx-token',
+ category: 'other',
+ },
+ VINE: {
+ symbol: 'VINE',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34856/large/vine.png',
+ coinGeckoId: 'vine',
+ category: 'other',
+ },
+ REZ: {
+ symbol: 'REZ',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35508/large/rez.png',
+ coinGeckoId: 'renzo',
+ category: 'defi',
+ },
+ VVV: {
+ symbol: 'VVV',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34902/large/vvv.png',
+ coinGeckoId: 'vvv',
+ category: 'other',
+ },
+ BERA: {
+ symbol: 'BERA',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/34939/large/berachain.png',
+ coinGeckoId: 'berachain-bera',
+ category: 'layer1',
+ },
+ W: {
+ symbol: 'W',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/35730/large/wormhole.png',
+ coinGeckoId: 'wormhole',
+ category: 'defi',
+ },
+ USUAL: {
+ symbol: 'USUAL',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36715/large/usual.png',
+ coinGeckoId: 'usual',
+ category: 'defi',
+ },
+ DOOD: {
+ symbol: 'DOOD',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34999/large/dood.png',
+ coinGeckoId: 'dood',
+ category: 'meme',
+ },
+ BIO: {
+ symbol: 'BIO',
+ iconUrl: 'https://assets.coingecko.com/coins/images/37191/large/bio.png',
+ coinGeckoId: 'biopassport',
+ category: 'other',
+ },
+ TST: {
+ symbol: 'TST',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35104/large/tst.png',
+ coinGeckoId: 'threshold-network-token',
+ category: 'other',
+ },
+ SCR: {
+ symbol: 'SCR',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35394/large/scr.png',
+ coinGeckoId: 'scroll',
+ category: 'layer2',
+ },
+ BANANA: {
+ symbol: 'BANANA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35263/large/banana.png',
+ coinGeckoId: 'banana-gun',
+ category: 'other',
+ },
+ TNSR: {
+ symbol: 'TNSR',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35943/large/tensor.png',
+ coinGeckoId: 'tensor',
+ category: 'defi',
+ },
+ MELANIA: {
+ symbol: 'MELANIA',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/37525/large/melania.png',
+ coinGeckoId: 'melania',
+ category: 'meme',
+ },
+ ME: {
+ symbol: 'ME',
+ iconUrl: 'https://assets.coingecko.com/coins/images/37079/large/me.png',
+ coinGeckoId: 'magic-eden',
+ category: 'defi',
+ },
+ LAUNCHCOIN: {
+ symbol: 'LAUNCHCOIN',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/35208/large/launchcoin.png',
+ coinGeckoId: 'launchcoin',
+ category: 'other',
+ },
+ RESOLV: {
+ symbol: 'RESOLV',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35409/large/resolv.png',
+ coinGeckoId: 'resolv',
+ category: 'other',
+ },
+ LAYER: {
+ symbol: 'LAYER',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35504/large/layer.png',
+ coinGeckoId: 'layer-bank',
+ category: 'defi',
+ },
+ ZORA: {
+ symbol: 'ZORA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35751/large/zora.png',
+ coinGeckoId: 'zora',
+ category: 'other',
+ },
+ MOVE: {
+ symbol: 'MOVE',
+ iconUrl: 'https://assets.coingecko.com/coins/images/37076/large/move.png',
+ coinGeckoId: 'movement',
+ category: 'layer1',
+ },
+ IP: {
+ symbol: 'IP',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35707/large/ip.png',
+ coinGeckoId: 'intellectual-property',
+ category: 'other',
+ },
+ NXPC: {
+ symbol: 'NXPC',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35801/large/nxpc.png',
+ coinGeckoId: 'nxpc',
+ category: 'other',
+ },
+ OGN: {
+ symbol: 'OGN',
+ iconUrl: 'https://assets.coingecko.com/coins/images/3296/large/op.jpg',
+ coinGeckoId: 'origin-protocol',
+ category: 'defi',
+ },
+ TRUMP: {
+ symbol: 'TRUMP',
+ iconUrl: 'https://assets.coingecko.com/coins/images/37527/large/trump.png',
+ coinGeckoId: 'maga',
+ category: 'meme',
+ },
+ CFX: {
+ symbol: 'CFX',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/13079/large/3vuYMbjN.png',
+ coinGeckoId: 'conflux-token',
+ category: 'layer1',
+ },
+ GRASS: {
+ symbol: 'GRASS',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34404/large/grass.png',
+ coinGeckoId: 'grass',
+ category: 'other',
+ },
+ OM: {
+ symbol: 'OM',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/12220/large/mantra-om.png',
+ coinGeckoId: 'mantra-dao',
+ category: 'defi',
+ },
+ PEOPLE: {
+ symbol: 'PEOPLE',
+ iconUrl: 'https://assets.coingecko.com/coins/images/16727/large/people.jpg',
+ coinGeckoId: 'constitutiondao',
+ category: 'other',
+ },
+ MAV: {
+ symbol: 'MAV',
+ iconUrl: 'https://assets.coingecko.com/coins/images/30745/large/mav.png',
+ coinGeckoId: 'maverick-protocol',
+ category: 'defi',
+ },
+ NIL: {
+ symbol: 'NIL',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35905/large/nil.png',
+ coinGeckoId: 'nil',
+ category: 'other',
+ },
+ PROMPT: {
+ symbol: 'PROMPT',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36001/large/prompt.png',
+ coinGeckoId: 'prompt',
+ category: 'other',
+ },
+ ACE: {
+ symbol: 'ACE',
+ iconUrl: 'https://assets.coingecko.com/coins/images/33413/large/ace.png',
+ coinGeckoId: 'fusionist',
+ category: 'gaming',
+ },
+ SOPH: {
+ symbol: 'SOPH',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36108/large/soph.png',
+ coinGeckoId: 'soph',
+ category: 'other',
+ },
+ OMNI: {
+ symbol: 'OMNI',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35925/large/omni.png',
+ coinGeckoId: 'omni-network',
+ category: 'layer2',
+ },
+ ZETA: {
+ symbol: 'ZETA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/33617/large/zeta.png',
+ coinGeckoId: 'zetachain',
+ category: 'layer1',
+ },
+ XAI: {
+ symbol: 'XAI',
+ iconUrl: 'https://assets.coingecko.com/coins/images/33503/large/xai.png',
+ coinGeckoId: 'xai-games',
+ category: 'gaming',
+ },
+ MERL: {
+ symbol: 'MERL',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36202/large/merl.png',
+ coinGeckoId: 'merlin-chain',
+ category: 'layer2',
+ },
+ ALT: {
+ symbol: 'ALT',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34942/large/alt.png',
+ coinGeckoId: 'altlayer',
+ category: 'layer2',
+ },
+ PURR: {
+ symbol: 'PURR',
+ iconUrl: 'https://assets.coingecko.com/coins/images/34710/large/purr.png',
+ coinGeckoId: 'purr',
+ category: 'other',
+ },
+ SUPER: {
+ symbol: 'SUPER',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/14040/large/6YPdWn6.png',
+ coinGeckoId: 'superfarm',
+ category: 'gaming',
+ },
+ KAITO: {
+ symbol: 'KAITO',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36303/large/kaito.png',
+ coinGeckoId: 'kaito',
+ category: 'other',
+ },
+ IO: {
+ symbol: 'IO',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35490/large/io.png',
+ coinGeckoId: 'io',
+ category: 'ai',
+ },
+ DYM: {
+ symbol: 'DYM',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35052/large/dym.png',
+ coinGeckoId: 'dymension',
+ category: 'layer1',
+ },
+ SPX: {
+ symbol: 'SPX',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36407/large/spx.png',
+ coinGeckoId: 'spx6900',
+ category: 'meme',
+ },
+ USTC: {
+ symbol: 'USTC',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/8284/large/luna1557227471663.png',
+ coinGeckoId: 'terrausd',
+ category: 'stablecoin',
+ },
+ ORDI: {
+ symbol: 'ORDI',
+ iconUrl: 'https://assets.coingecko.com/coins/images/30162/large/ordi.png',
+ coinGeckoId: 'ordi',
+ category: 'other',
+ },
+ GMT: {
+ symbol: 'GMT',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/18085/large/gmtoken.png',
+ coinGeckoId: 'stepn',
+ category: 'other',
+ },
+ SAGA: {
+ symbol: 'SAGA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35912/large/saga.png',
+ coinGeckoId: 'saga',
+ category: 'layer1',
+ },
+ GAS: {
+ symbol: 'GAS',
+ iconUrl: 'https://assets.coingecko.com/coins/images/4480/large/GAS.png',
+ coinGeckoId: 'gas',
+ category: 'other',
+ },
+ S: {
+ symbol: 'S',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36501/large/s.png',
+ coinGeckoId: 's',
+ category: 'other',
+ },
+ WCT: {
+ symbol: 'WCT',
+ iconUrl: 'https://assets.coingecko.com/coins/images/36601/large/wct.png',
+ coinGeckoId: 'wct',
+ category: 'other',
+ },
+ ARK: {
+ symbol: 'ARK',
+ iconUrl: 'https://assets.coingecko.com/coins/images/484/large/ark.png',
+ coinGeckoId: 'ark',
+ category: 'layer1',
+ },
+ REQ: {
+ symbol: 'REQ',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/1031/large/Request_icon_green.png',
+ coinGeckoId: 'request-network',
+ category: 'defi',
+ },
+ BIGTIME: {
+ symbol: 'BIGTIME',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/32404/large/big-time.png',
+ coinGeckoId: 'big-time',
+ category: 'gaming',
+ },
+ POLYX: {
+ symbol: 'POLYX',
+ iconUrl: 'https://assets.coingecko.com/coins/images/17312/large/POLYX.png',
+ coinGeckoId: 'polymesh',
+ category: 'other',
+ },
+ MAVIA: {
+ symbol: 'MAVIA',
+ iconUrl: 'https://assets.coingecko.com/coins/images/35004/large/mavia.png',
+ coinGeckoId: 'heroes-of-mavia',
+ category: 'gaming',
+ },
+ GRIFFAIN: {
+ symbol: 'GRIFFAIN',
+ iconUrl:
+ 'https://assets.coingecko.com/coins/images/36701/large/griffain.png',
+ coinGeckoId: 'griffain',
+ category: 'other',
+ },
+};
diff --git a/app/components/UI/Perps/hooks/useHyperliquidSdk.ts b/app/components/UI/Perps/hooks/useHyperliquidSdk.ts
new file mode 100644
index 000000000000..3ece7146e81f
--- /dev/null
+++ b/app/components/UI/Perps/hooks/useHyperliquidSdk.ts
@@ -0,0 +1,101 @@
+import { useMemo } from 'react';
+import {
+ HttpTransport,
+ InfoClient,
+ WebSocketTransport,
+ SubscriptionClient,
+} from '@deeeed/hyperliquid-node20';
+
+export interface HyperliquidSdkClients {
+ /**
+ * InfoClient for HTTP-based data fetching
+ */
+ infoClient: InfoClient;
+ /**
+ * SubscriptionClient for WebSocket-based real-time data
+ */
+ subscriptionClient: SubscriptionClient;
+ /**
+ * Raw HTTP transport instance
+ */
+ httpTransport: HttpTransport;
+ /**
+ * Raw WebSocket transport instance
+ */
+ webSocketTransport: WebSocketTransport;
+}
+
+/**
+ * Singleton class to manage HyperLiquid SDK clients
+ * Ensures only one instance of each client exists across the entire app
+ */
+class HyperliquidSdkManager {
+ private static instance: HyperliquidSdkManager;
+ private _httpTransport: HttpTransport | null = null;
+ private _webSocketTransport: WebSocketTransport | null = null;
+ private _infoClient: InfoClient | null = null;
+ private _subscriptionClient: SubscriptionClient | null = null;
+
+ private constructor() {}
+
+ static getInstance(): HyperliquidSdkManager {
+ if (!HyperliquidSdkManager.instance) {
+ HyperliquidSdkManager.instance = new HyperliquidSdkManager();
+ }
+ return HyperliquidSdkManager.instance;
+ }
+
+ get httpTransport(): HttpTransport {
+ if (!this._httpTransport) {
+ this._httpTransport = new HttpTransport();
+ }
+ return this._httpTransport;
+ }
+
+ get webSocketTransport(): WebSocketTransport {
+ if (!this._webSocketTransport) {
+ this._webSocketTransport = new WebSocketTransport();
+ }
+ return this._webSocketTransport;
+ }
+
+ get infoClient(): InfoClient {
+ if (!this._infoClient) {
+ this._infoClient = new InfoClient({ transport: this.httpTransport });
+ }
+ return this._infoClient;
+ }
+
+ get subscriptionClient(): SubscriptionClient {
+ if (!this._subscriptionClient) {
+ this._subscriptionClient = new SubscriptionClient({ transport: this.webSocketTransport });
+ }
+ return this._subscriptionClient;
+ }
+
+ /**
+ * Clean up all clients and transports
+ * Call this when the app is shutting down or when you need to reset connections
+ */
+ cleanup(): void {
+ this._httpTransport = null;
+ this._webSocketTransport = null;
+ this._infoClient = null;
+ this._subscriptionClient = null;
+ }
+}
+
+/**
+ * Hook to access HyperLiquid SDK clients
+ * Returns the same singleton instances across all components that use this hook
+ */
+export const useHyperliquidSdk = (): HyperliquidSdkClients => {
+ const sdkManager = useMemo(() => HyperliquidSdkManager.getInstance(), []);
+
+ return useMemo(() => ({
+ infoClient: sdkManager.infoClient,
+ subscriptionClient: sdkManager.subscriptionClient,
+ httpTransport: sdkManager.httpTransport,
+ webSocketTransport: sdkManager.webSocketTransport,
+ }), [sdkManager]);
+};
\ No newline at end of file
diff --git a/app/components/UI/Perps/hooks/usePerpsMarkets.ts b/app/components/UI/Perps/hooks/usePerpsMarkets.ts
new file mode 100644
index 000000000000..7bceea22bb57
--- /dev/null
+++ b/app/components/UI/Perps/hooks/usePerpsMarkets.ts
@@ -0,0 +1,158 @@
+import { useState, useEffect, useCallback } from 'react';
+import {
+ PerpsMarketData,
+ HyperLiquidMarketData,
+} from '../Views/PerpsMarketListView/PerpsMarketListView.types';
+import { transformMarketData } from '../utils/marketDataTransform';
+import { useHyperliquidSdk } from './useHyperliquidSdk';
+import Logger from '../../../../util/Logger';
+
+export interface UsePerpsMarketsResult {
+ /**
+ * Transformed market data ready for UI consumption
+ */
+ markets: PerpsMarketData[];
+ /**
+ * Loading state for initial data fetch
+ */
+ isLoading: boolean;
+ /**
+ * Error state with error message
+ */
+ error: string | null;
+ /**
+ * Refresh function to manually refetch data
+ */
+ refresh: () => Promise;
+ /**
+ * Indicates if data is being refreshed
+ */
+ isRefreshing: boolean;
+}
+
+export interface UsePerpsMarketsOptions {
+ /**
+ * Enable automatic polling for live updates
+ * @default false
+ */
+ enablePolling?: boolean;
+ /**
+ * Polling interval in milliseconds
+ * @default 60000 (1 minute)
+ */
+ pollingInterval?: number;
+ /**
+ * Skip initial data fetch on mount
+ * @default false
+ */
+ skipInitialFetch?: boolean;
+}
+
+/**
+ * Custom hook to fetch and manage Perps market data from HyperLiquid
+ * Uses the singleton SDK clients for efficient resource management
+ */
+export const usePerpsMarkets = (
+ options: UsePerpsMarketsOptions = {},
+): UsePerpsMarketsResult => {
+ const {
+ enablePolling = false,
+ pollingInterval = 60000, // 1 minute default
+ skipInitialFetch = false,
+ } = options;
+
+ const [markets, setMarkets] = useState([]);
+ const [isLoading, setIsLoading] = useState(!skipInitialFetch);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Get singleton SDK clients
+ const { infoClient } = useHyperliquidSdk();
+
+ const fetchMarketData = useCallback(
+ async (isRefresh = false): Promise => {
+ if (isRefresh) {
+ setIsRefreshing(true);
+ } else {
+ setIsLoading(true);
+ }
+ setError(null);
+
+ try {
+ Logger.log('Perps: Fetching market data from HyperLiquid...');
+
+ // Fetch all required data in parallel for better performance
+ const [perpsMeta, allMids] = await Promise.all([
+ infoClient.meta(),
+ infoClient.allMids(),
+ ]);
+
+ if (!perpsMeta?.universe || !allMids) {
+ throw new Error('Failed to fetch market data - no data received');
+ }
+
+ // Also fetch asset contexts for additional data like volume and previous day prices
+ const metaAndCtxs = await infoClient.metaAndAssetCtxs();
+ const assetCtxs = metaAndCtxs?.[1] || [];
+
+ const hyperLiquidData: HyperLiquidMarketData = {
+ universe: perpsMeta.universe,
+ assetCtxs,
+ allMids,
+ };
+
+ const transformedMarkets = transformMarketData(hyperLiquidData);
+ setMarkets(transformedMarkets);
+
+ Logger.log('Perps: Successfully fetched and transformed market data', {
+ marketCount: transformedMarkets.length,
+ });
+ } catch (err) {
+ const errorMessage =
+ err instanceof Error ? err.message : 'Unknown error occurred';
+ setError(errorMessage);
+ Logger.log('Perps: Failed to fetch market data', err);
+
+ // Keep existing data on error to prevent UI flash
+ if (markets.length === 0) {
+ setMarkets([]);
+ }
+ } finally {
+ setIsLoading(false);
+ setIsRefreshing(false);
+ }
+ },
+ [infoClient, markets.length],
+ );
+
+ const refresh = useCallback(
+ (): Promise => fetchMarketData(true),
+ [fetchMarketData],
+ );
+
+ // Initial data fetch
+ useEffect(() => {
+ if (!skipInitialFetch) {
+ fetchMarketData();
+ }
+ }, [fetchMarketData, skipInitialFetch]);
+
+ // Polling effect
+ useEffect(() => {
+ if (!enablePolling) return;
+
+ const intervalId = setInterval(() => {
+ fetchMarketData(true);
+ }, pollingInterval);
+
+ return () => clearInterval(intervalId);
+ }, [enablePolling, pollingInterval, fetchMarketData]);
+
+ return {
+ markets,
+ isLoading,
+ error,
+ refresh,
+ isRefreshing,
+ };
+};
diff --git a/app/components/UI/Perps/index.ts b/app/components/UI/Perps/index.ts
new file mode 100644
index 000000000000..3d90afc00c6b
--- /dev/null
+++ b/app/components/UI/Perps/index.ts
@@ -0,0 +1,6 @@
+// Main exports for Perps module
+export { default as PerpsView } from './Views/PerpsView';
+export { PerpsScreenStack } from './routes';
+export { selectPerpsEnabledFlag } from './selectors';
+export { PERPS_CONSTANTS } from './constants';
+export * from './types';
diff --git a/app/components/UI/Perps/routes/index.tsx b/app/components/UI/Perps/routes/index.tsx
new file mode 100644
index 000000000000..568378545456
--- /dev/null
+++ b/app/components/UI/Perps/routes/index.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { createStackNavigator } from '@react-navigation/stack';
+import Routes from '../../../../constants/navigation/Routes';
+import PerpsView from '../Views/PerpsView';
+import PerpsMarketListView from '../Views/PerpsMarketListView/PerpsMarketListView';
+
+const Stack = createStackNavigator();
+
+const PerpsScreenStack = () => (
+
+
+
+
+);
+
+export { PerpsScreenStack };
diff --git a/app/components/UI/Perps/selectors/index.ts b/app/components/UI/Perps/selectors/index.ts
new file mode 100644
index 000000000000..b25c7d41dc6c
--- /dev/null
+++ b/app/components/UI/Perps/selectors/index.ts
@@ -0,0 +1,11 @@
+import { createSelector } from 'reselect';
+import { selectRemoteFeatureFlags } from '../../../../selectors/featureFlagController';
+
+/**
+ * Selector for perps enabled feature flag
+ * Simple boolean flag from remote feature flags
+ */
+export const selectPerpsEnabledFlag = createSelector(
+ selectRemoteFeatureFlags,
+ (remoteFeatureFlags) => Boolean(remoteFeatureFlags?.perpsEnabled),
+);
diff --git a/app/components/UI/Perps/types/index.ts b/app/components/UI/Perps/types/index.ts
new file mode 100644
index 000000000000..39163b79056c
--- /dev/null
+++ b/app/components/UI/Perps/types/index.ts
@@ -0,0 +1,54 @@
+/**
+ * Perps view component props
+ */
+export interface PerpsViewProps {}
+
+/**
+ * Test result states for SDK validation
+ */
+export type TestResultStatus =
+ | 'idle'
+ | 'loading'
+ | 'success'
+ | 'warning'
+ | 'error';
+
+/**
+ * Test result data structure
+ */
+export interface TestResult {
+ status: TestResultStatus;
+ message: string;
+ data?: Record;
+}
+
+/**
+ * SDK test types
+ */
+export type SDKTestType = 'connection' | 'asset-listing' | 'websocket';
+
+/**
+ * Hyperliquid asset interface (basic structure)
+ */
+export interface HyperliquidAsset {
+ name: string;
+ [key: string]: unknown;
+}
+
+// Icons are sourced from CoinGecko
+export interface PerpsMarketIconData {
+ symbol: string;
+ iconUrl: string;
+ fallbackUrl?: string;
+ coinGeckoId?: string;
+ category:
+ | 'major'
+ | 'defi'
+ | 'meme'
+ | 'ai'
+ | 'gaming'
+ | 'layer1'
+ | 'layer2'
+ | 'stablecoin'
+ | 'other';
+}
diff --git a/app/components/UI/Perps/utils/marketDataTransform.ts b/app/components/UI/Perps/utils/marketDataTransform.ts
new file mode 100644
index 000000000000..b67fdaa0ff41
--- /dev/null
+++ b/app/components/UI/Perps/utils/marketDataTransform.ts
@@ -0,0 +1,209 @@
+import type {
+ PerpsUniverse,
+ PerpsAssetCtx,
+ AllMids,
+} from '@deeeed/hyperliquid-node20';
+import {
+ PerpsMarketData,
+ HyperLiquidMarketData,
+} from '../Views/PerpsMarketListView/PerpsMarketListView.types';
+
+/**
+ * Formats a number as a price string with appropriate decimal places
+ */
+const formatPrice = (price: string): string => {
+ const num = parseFloat(price);
+ if (num >= 0.01) {
+ // For prices >= $0.01, use exactly 2 decimal places
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ })
+ .format(num)
+ .replace('$', '$');
+ } else {
+ // For very small prices < $0.01, keep more precision
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 4,
+ maximumFractionDigits: 6,
+ })
+ .format(num)
+ .replace('$', '$');
+ }
+};
+
+/**
+ * Formats a large number (volume) with appropriate suffixes
+ */
+const formatVolume = (volume: string): string => {
+ const num = parseFloat(volume);
+ if (num >= 1e9) {
+ return `$${(num / 1e9).toFixed(2)}B`;
+ } else if (num >= 1e6) {
+ return `$${(num / 1e6).toFixed(2)}M`;
+ } else if (num >= 1e3) {
+ return `$${(num / 1e3).toFixed(2)}K`;
+ } else {
+ return `$${num.toFixed(2)}`;
+ }
+};
+
+/**
+ * Calculates 24h price change and percentage
+ */
+const calculatePriceChange = (currentPrice: string, prevDayPrice: string) => {
+ const current = parseFloat(currentPrice);
+ const previous = parseFloat(prevDayPrice);
+ const change = current - previous;
+ const changePercent = (change / previous) * 100;
+
+ const formattedChange =
+ change >= 0
+ ? `+$${Math.abs(change).toFixed(2)}`
+ : `-$${Math.abs(change).toFixed(2)}`;
+ const formattedPercent =
+ change >= 0
+ ? `+${changePercent.toFixed(2)}%`
+ : `${changePercent.toFixed(2)}%`;
+
+ return {
+ change24h: formattedChange,
+ change24hPercent: formattedPercent,
+ };
+};
+
+/**
+ * Transforms HyperLiquid SDK data into UI-friendly market data
+ */
+export const transformMarketData = (
+ data: HyperLiquidMarketData,
+): PerpsMarketData[] => {
+ const { universe, assetCtxs, allMids } = data;
+
+ return universe
+ .reduce((accumulator, asset, originalIndex) => {
+ if (asset.isDelisted) return accumulator; // Skip delisted assets
+
+ const assetContext = assetCtxs[originalIndex];
+ const currentPrice = allMids[asset.name] || assetContext?.markPx || '0';
+ const prevDayPrice = assetContext?.prevDayPx || '0';
+ const volume = assetContext?.dayNtlVlm || '0';
+
+ const { change24h, change24hPercent } = calculatePriceChange(
+ currentPrice,
+ prevDayPrice,
+ );
+
+ accumulator.push({
+ symbol: asset.name,
+ name: asset.name,
+ maxLeverage: `${asset.maxLeverage}x`,
+ price: formatPrice(currentPrice),
+ change24h,
+ change24hPercent,
+ volume: formatVolume(volume),
+ });
+
+ return accumulator;
+ }, [] as PerpsMarketData[])
+ .sort((a, b) => {
+ // Sort by 24h trading volume (descending) - largest to smallest
+ const getVolumeNumber = (volumeStr: string): number => {
+ const cleanStr = volumeStr.replace(/[$,]/g, '');
+ if (cleanStr.includes('B')) {
+ return parseFloat(cleanStr.replace('B', '')) * 1e9;
+ } else if (cleanStr.includes('M')) {
+ return parseFloat(cleanStr.replace('M', '')) * 1e6;
+ } else if (cleanStr.includes('K')) {
+ return parseFloat(cleanStr.replace('K', '')) * 1e3;
+ }
+ return parseFloat(cleanStr);
+ };
+
+ const volumeA = getVolumeNumber(a.volume);
+ const volumeB = getVolumeNumber(b.volume);
+ return volumeB - volumeA;
+ });
+};
+
+/**
+ * Creates mock market data for testing purposes
+ */
+export const createMockMarketData = (): PerpsMarketData[] => [
+ {
+ symbol: 'BTC',
+ name: 'Bitcoin',
+ maxLeverage: '40x',
+ price: '$108,844',
+ change24h: '-$777',
+ change24hPercent: '-0.71%',
+ volume: '$2,796,497,269',
+ },
+ {
+ symbol: 'ETH',
+ name: 'Ethereum',
+ maxLeverage: '25x',
+ price: '$2,552.4',
+ change24h: '-$46.1',
+ change24hPercent: '-1.77%',
+ volume: '$1,644,450,691',
+ },
+ {
+ symbol: 'SOL',
+ name: 'Solana',
+ maxLeverage: '20x',
+ price: '$150.34',
+ change24h: '-$5.55',
+ change24hPercent: '-3.56%',
+ volume: '$598,874,983',
+ },
+ {
+ symbol: 'HYPE',
+ name: 'Hyperliquid',
+ maxLeverage: '10x',
+ price: '$38.8',
+ change24h: '-$1.90',
+ change24hPercent: '-4.67%',
+ volume: '$482,432,565',
+ },
+ {
+ symbol: 'FARTCOIN',
+ name: 'Fartcoin',
+ maxLeverage: '10x',
+ price: '$1.18',
+ change24h: '-$0.06',
+ change24hPercent: '-5.21%',
+ volume: '$165,505,993',
+ },
+ {
+ symbol: 'XRP',
+ name: 'Ripple',
+ maxLeverage: '20x',
+ price: '$2.23',
+ change24h: '-$1.92',
+ change24hPercent: '-4.67%',
+ volume: '$160,190,261',
+ },
+ {
+ symbol: 'SUI',
+ name: 'Sui',
+ maxLeverage: '10x',
+ price: '$2.93',
+ change24h: '-$0.11',
+ change24hPercent: '-3.93%',
+ volume: '$71,968,105',
+ },
+ {
+ symbol: 'kPEPE',
+ name: 'kPepe',
+ maxLeverage: '10x',
+ price: '$0.009812',
+ change24h: '-$0.000877',
+ change24hPercent: '-8.21%',
+ volume: '$68,805,011',
+ },
+];
diff --git a/app/components/UI/Perps/utils/perpsMarketIcon.ts b/app/components/UI/Perps/utils/perpsMarketIcon.ts
new file mode 100644
index 000000000000..3f8f8a60127c
--- /dev/null
+++ b/app/components/UI/Perps/utils/perpsMarketIcon.ts
@@ -0,0 +1,4 @@
+import { HYPERLIQUID_ICONS } from '../constants/marketIcons';
+
+export const getPerpsMarketIcon = (symbol: string): string =>
+ HYPERLIQUID_ICONS?.[symbol]?.iconUrl;
diff --git a/app/components/UI/WalletAction/WalletAction.tsx b/app/components/UI/WalletAction/WalletAction.tsx
index 30d999cdb55e..a3b79addc04e 100644
--- a/app/components/UI/WalletAction/WalletAction.tsx
+++ b/app/components/UI/WalletAction/WalletAction.tsx
@@ -74,6 +74,11 @@ const WalletAction = ({
description: strings('asset_overview.earn_description'),
disabledDescription: strings('asset_overview.disabled_button.earn'),
},
+ [WalletActionType.Perps]: {
+ title: strings('asset_overview.perps_button'),
+ description: strings('asset_overview.perps_description'),
+ disabledDescription: strings('asset_overview.disabled_button.perps'),
+ },
};
const actionStrings = actionType ? walletActionDetails[actionType] : null;
diff --git a/app/components/UI/WalletAction/WalletAction.types.ts b/app/components/UI/WalletAction/WalletAction.types.ts
index e999447b4394..aabdeebed08a 100644
--- a/app/components/UI/WalletAction/WalletAction.types.ts
+++ b/app/components/UI/WalletAction/WalletAction.types.ts
@@ -11,6 +11,7 @@ export enum WalletActionType {
Send = 'Send',
Receive = 'Receive',
Earn = 'Earn',
+ Perps = 'Perps',
}
export interface WalletActionDetail {
diff --git a/app/components/Views/WalletActions/WalletActions.test.tsx b/app/components/Views/WalletActions/WalletActions.test.tsx
index 168670e55066..4335b34ad695 100644
--- a/app/components/Views/WalletActions/WalletActions.test.tsx
+++ b/app/components/Views/WalletActions/WalletActions.test.tsx
@@ -30,6 +30,11 @@ import {
import { EarnTokenDetails } from '../../UI/Earn/types/lending.types';
import WalletActions from './WalletActions';
import Routes from '../../../constants/navigation/Routes';
+import { selectPerpsEnabledFlag } from '../../UI/Perps';
+
+jest.mock('../../UI/Perps', () => ({
+ selectPerpsEnabledFlag: jest.fn(),
+}));
jest.mock('../../UI/Earn/selectors/featureFlags', () => ({
selectStablecoinLendingEnabledFlag: jest.fn(),
@@ -346,6 +351,10 @@ describe('WalletActions', () => {
expect(
queryByTestId(WalletActionsBottomSheetSelectorsIDs.EARN_BUTTON),
).toBeNull();
+ // Feature flag is disabled by default
+ expect(
+ queryByTestId(WalletActionsBottomSheetSelectorsIDs.PERPS_BUTTON),
+ ).toBeNull();
});
it('should render earn button if the stablecoin lending feature is enabled', () => {
(
@@ -609,6 +618,42 @@ describe('WalletActions', () => {
).toBeNull();
});
+ it('should render the Perpetuals button if the Perps feature flag is enabled', () => {
+ (
+ selectPerpsEnabledFlag as jest.MockedFunction<
+ typeof selectPerpsEnabledFlag
+ >
+ ).mockReturnValue(true);
+
+ const { getByTestId } = renderWithProvider(, {
+ state: mockInitialState,
+ });
+
+ expect(
+ getByTestId(WalletActionsBottomSheetSelectorsIDs.PERPS_BUTTON),
+ ).toBeDefined();
+ });
+
+ it('should call the onPerps function when the Perpetuals button is pressed', () => {
+ (
+ selectPerpsEnabledFlag as jest.MockedFunction<
+ typeof selectPerpsEnabledFlag
+ >
+ ).mockReturnValue(true);
+
+ const { getByTestId } = renderWithProvider(, {
+ state: mockInitialState,
+ });
+
+ fireEvent.press(
+ getByTestId(WalletActionsBottomSheetSelectorsIDs.PERPS_BUTTON),
+ );
+
+ expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, {
+ screen: Routes.PERPS.TRADING_VIEW,
+ });
+ });
+
it('disables action buttons when the account cannot sign transactions', () => {
(
selectStablecoinLendingEnabledFlag as jest.MockedFunction<
diff --git a/app/components/Views/WalletActions/WalletActions.tsx b/app/components/Views/WalletActions/WalletActions.tsx
index 3a5f52569523..c6c8415df291 100644
--- a/app/components/Views/WalletActions/WalletActions.tsx
+++ b/app/components/Views/WalletActions/WalletActions.tsx
@@ -56,6 +56,7 @@ import {
} from '../../UI/Earn/selectors/featureFlags';
import { isBridgeAllowed } from '../../UI/Bridge/utils';
import { selectDepositEntrypointWalletActions } from '../../../selectors/featureFlagController/deposit';
+import { selectPerpsEnabledFlag } from '../../UI/Perps';
import { EARN_INPUT_VIEW_ACTIONS } from '../../UI/Earn/Views/EarnInputView/EarnInputView.types';
import Engine from '../../../core/Engine';
import { selectMultichainTokenListForAccountId } from '../../../selectors/multichain/multichain';
@@ -80,6 +81,7 @@ const WalletActions = () => {
const isDepositWalletActionEnabled = useSelector(
selectDepositEntrypointWalletActions,
);
+ const isPerpsEnabled = useSelector(selectPerpsEnabledFlag);
const { trackEvent, createEventBuilder } = useMetrics();
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
const selectedAccount = useSelector(selectSelectedInternalAccount);
@@ -349,6 +351,12 @@ const WalletActions = () => {
});
}, [closeBottomSheetAndNavigate, goToBridgeBase]);
+ const onPerps = useCallback(() => {
+ closeBottomSheetAndNavigate(() => {
+ navigate(Routes.PERPS.ROOT, { screen: Routes.PERPS.TRADING_VIEW });
+ });
+ }, [closeBottomSheetAndNavigate, navigate]);
+
const sendIconStyle = useMemo(
() => ({
transform: [{ rotate: '-45deg' }],
@@ -424,6 +432,17 @@ const WalletActions = () => {
disabled={!canSignTransactions}
/>
)}
+ {isPerpsEnabled && (
+
+ )}
/app/util/test/testSetup.js'],
testEnvironment: 'jest-environment-node',
transformIgnorePatterns: [
- 'node_modules/(?!((@metamask/)?(@react-native|react-native|redux-persist-filesystem|@react-navigation|@react-native-community|@react-native-masked-view|react-navigation|react-navigation-redux-helpers|@sentry|d3-color|@notifee|expo-file-system)))',
+ 'node_modules/(?!((@metamask/)?(@react-native|react-native|redux-persist-filesystem|@react-navigation|@react-native-community|@react-native-masked-view|react-navigation|react-navigation-redux-helpers|@sentry|d3-color|@notifee|expo-file-system)|@noble/secp256k1|@deeeed/hyperliquid-node20))',
],
transform: {
'^.+\\.[jt]sx?$': ['babel-jest', { configFile: './babel.config.tests.js' }],
diff --git a/locales/languages/en.json b/locales/languages/en.json
index fd464ca7558b..b2bb694a35ba 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -1653,6 +1653,7 @@
"receive_button": "Receive",
"portfolio_button": "Portfolio",
"earn_button": "Earn",
+ "perps_button": "Perpetuals",
"add_collectible_button": "Add",
"info": "Info",
"swap": "Swap",
@@ -1665,7 +1666,8 @@
"bridge": "Bridging not supported for this account",
"send": "Sending not supported for this account",
"action": "This action is not supported for this account",
- "earn": "Earning not supported for this account"
+ "earn": "Earning not supported for this account",
+ "perps": "Perpetuals trading not supported for this account"
},
"description": "Description",
"totalSupply": "Total Supply",
@@ -1682,6 +1684,7 @@
"send_description": "Send crypto to any account",
"receive_description": "Receive crypto",
"earn_description": "Earn rewards on your tokens",
+ "perps_description": "Trade perpetual contracts",
"chart_time_period": {
"1d": "Today",
"7d": "Past 7 days",
@@ -4550,5 +4553,13 @@
},
"back_button": "Back",
"continue_button": "Continue"
+ },
+ "perps": {
+ "perpetual_markets": "Perpetual Markets",
+ "search": "Search",
+ "token_volume": "Token Volume",
+ "last_price_24h_change": "Last Price / 24h Change",
+ "failed_to_load_market_data": "Failed to load market data",
+ "tap_to_retry": "Tap to retry"
}
}
diff --git a/package.json b/package.json
index 362038c59a75..95db327918b1 100644
--- a/package.json
+++ b/package.json
@@ -163,6 +163,7 @@
"@config-plugins/detox": "^9.0.0",
"@consensys/native-ramps-sdk": "^1.0.9",
"@consensys/on-ramp-sdk": "2.1.10",
+ "@deeeed/hyperliquid-node20": "^0.23.1-node20.1",
"@ethersproject/abi": "^5.7.0",
"@keystonehq/bc-ur-registry-eth": "^0.21.0",
"@keystonehq/metamask-airgapped-keyring": "^0.15.2",
@@ -219,6 +220,7 @@
"@metamask/ppom-validator": "0.36.0",
"@metamask/preferences-controller": "^18.4.0",
"@metamask/profile-sync-controller": "^18.0.0",
+ "@metamask/react-native-acm": "git+https://github.com/MetaMask/react-native-google-acm.git#7a9c4ce54c97b786a5f2cd869b3b3c99f5079bdd",
"@metamask/react-native-actionsheet": "2.4.2",
"@metamask/react-native-button": "^3.0.0",
"@metamask/react-native-payments": "^2.0.0",
@@ -315,6 +317,7 @@
"ethereumjs-util": "^7.0.10",
"ethers": "^5.0.14",
"ethjs-ens": "2.0.1",
+ "event-target-shim": "^6.0.2",
"eventemitter2": "^6.4.9",
"events": "3.0.0",
"expo": "52.0.27",
@@ -370,7 +373,6 @@
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.25.0",
"react-native-get-random-values": "^1.8.0",
- "@metamask/react-native-acm": "git+https://github.com/MetaMask/react-native-google-acm.git#7a9c4ce54c97b786a5f2cd869b3b3c99f5079bdd",
"react-native-gzip": "^1.1.0",
"react-native-i18n": "2.0.15",
"react-native-in-app-review": "^4.3.3",
diff --git a/shim.js b/shim.js
index ca5b153b1103..5fb23b96f321 100644
--- a/shim.js
+++ b/shim.js
@@ -49,6 +49,44 @@ if (typeof process === 'undefined') {
process.browser = false;
if (typeof Buffer === 'undefined') global.Buffer = require('buffer').Buffer;
+// EventTarget polyfills for Hyperliquid SDK WebSocket support
+if (
+ typeof global.EventTarget === 'undefined' ||
+ typeof global.Event === 'undefined'
+) {
+ const { Event, EventTarget } = require('event-target-shim');
+ global.EventTarget = EventTarget;
+ global.Event = Event;
+}
+
+if (typeof global.CustomEvent === 'undefined') {
+ global.CustomEvent = function (type, params) {
+ params = params || {};
+ const event = new global.Event(type, params);
+ event.detail = params.detail || null;
+ return event;
+ };
+}
+
+if (global.AbortSignal && typeof global.AbortSignal.timeout === 'undefined') {
+ global.AbortSignal.timeout = function (delay) {
+ const controller = new AbortController();
+ setTimeout(() => controller.abort(), delay);
+ return controller.signal;
+ };
+}
+
+if (typeof global.Promise.withResolvers === 'undefined') {
+ global.Promise.withResolvers = function () {
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ return { promise, resolve, reject };
+ };
+}
+
// global.location = global.location || { port: 80 }
const isDev = typeof __DEV__ === 'boolean' && __DEV__;
Object.assign(process.env, { NODE_ENV: isDev ? 'development' : 'production' });
@@ -81,4 +119,4 @@ if (enableApiCallLogs || isTest) {
).catch(() => originalFetch(url, options))
: originalFetch(url, options);
})();
-}
\ No newline at end of file
+}
diff --git a/yarn.lock b/yarn.lock
index 3690a0fdeb66..4455f219622c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1683,6 +1683,16 @@
enabled "2.0.x"
kuler "^2.0.0"
+"@deeeed/hyperliquid-node20@^0.23.1-node20.1":
+ version "0.23.1-node20.1"
+ resolved "https://registry.yarnpkg.com/@deeeed/hyperliquid-node20/-/hyperliquid-node20-0.23.1-node20.1.tgz#281625d360942546509cfb704acfecb8f2c55142"
+ integrity sha512-Zt89khmm3mBCkzleSTUXzkNzZZrpUNdB4BVfVzROAcFQLhZUN5w+T4u+xsHH99TcioifRR41rhtO4s/jf2oVkQ==
+ dependencies:
+ "@msgpack/msgpack" "^3.1.2"
+ "@noble/hashes" "^1.8.0"
+ "@noble/secp256k1" "^2.3.0"
+ typescript-event-target "1.1.1"
+
"@discoveryjs/json-ext@^0.5.0":
version "0.5.7"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
@@ -6257,6 +6267,11 @@
resolved "https://registry.yarnpkg.com/@mobily/ts-belt/-/ts-belt-3.13.1.tgz#8f8ce2a2eca41d88c2ca70c84d0f47d0f7f5cd5f"
integrity sha512-K5KqIhPI/EoCTbA6CGbrenM9s41OouyK8A03fGJJcla/zKucsgLbz8HNbeseoLarRPgyWJsUyCYqFhI7t3Ra9Q==
+"@msgpack/msgpack@^3.1.2":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.1.2.tgz#fdd25cc2202297519798bbaf4689152ad9609e19"
+ integrity sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==
+
"@native-html/css-processor@1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@native-html/css-processor/-/css-processor-1.11.0.tgz#27d02e5123b0849f4986d44060ba3f235a15f552"
@@ -6386,6 +6401,11 @@
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
+"@noble/secp256k1@^2.3.0":
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-2.3.0.tgz#ddfe6e853472fb88cba4d5e59b7067adc1e64adf"
+ integrity sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -19355,6 +19375,11 @@ event-target-shim@^5.0.0, event-target-shim@^5.0.1:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+event-target-shim@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-6.0.2.tgz#ea5348c3618ee8b62ff1d344f01908ee2b8a2b71"
+ integrity sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==
+
eventemitter2@^6.4.9:
version "6.4.9"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125"
@@ -30845,6 +30870,11 @@ typescript-compare@^0.0.2:
dependencies:
typescript-logic "^0.0.0"
+typescript-event-target@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/typescript-event-target/-/typescript-event-target-1.1.1.tgz#20a6d491b77d2e37dc432c5394ab74c0d7065539"
+ integrity sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==
+
typescript-logic@^0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/typescript-logic/-/typescript-logic-0.0.0.tgz#66ebd82a2548f2b444a43667bec120b496890196"