Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .js.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ export MM_PERPS_MYX_BROKER_ADDRESS_TESTNET=""
export MM_PERPS_MYX_APP_ID_MAINNET=""
export MM_PERPS_MYX_API_SECRET_MAINNET=""
export MM_PERPS_MYX_BROKER_ADDRESS_MAINNET=""
# HyperLiquid builder fee wallet addresses (empty = uses hardcoded defaults)
export MM_PERPS_HL_BUILDER_ADDRESS_TESTNET=""
export MM_PERPS_HL_BUILDER_ADDRESS_MAINNET=""
# HIP-3 Feature Flags (remote override with local fallback)
export MM_PERPS_HIP3_ENABLED="true"
export MM_PERPS_HIP3_ALLOWLIST_MARKETS="" # Allowlist: Empty = enable all markets. Examples: "xyz:XYZ100,xyz:TSLA" or "xyz:*,abc:TSLA"
Expand Down
9 changes: 9 additions & 0 deletions app/components/UI/Perps/Perps.testIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,15 @@ export const PerpsTransactionSelectorsIDs = {
FUNDING_TRANSACTION_VIEW: 'perps-funding-transaction-view',
ORDER_TRANSACTION_VIEW: 'perps-order-transaction-view',

// FlashList
FLASH_LIST: 'perps-transactions-flash-list',

// Fill tags
FILL_TAG_TAKE_PROFIT: 'perps-fill-tag-take-profit',
FILL_TAG_STOP_LOSS: 'perps-fill-tag-stop-loss',
FILL_TAG_LIQUIDATED: 'perps-fill-tag-liquidated',
FILL_TAG_ADL: 'perps-fill-tag-adl',

// Common buttons
BLOCK_EXPLORER_BUTTON: 'block-explorer-button',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export const styleSheet = (params: { theme: Theme }) => {
fontSize: 14,
color: colors.text.alternative,
},
fillTag: {
flexDirection: 'row' as const,
alignItems: 'center' as const,
gap: 8,
},
rightContent: {
alignItems: 'flex-end' as const,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ const PerpsTransactionsView: React.FC = () => {
)}

<FlashList
testID="perps-transactions-flash-list"
ref={flashListRef}
data={flatListData}
renderItem={renderListItem}
Expand Down
64 changes: 63 additions & 1 deletion app/components/UI/Perps/adapters/mobileInfrastructure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { AnalyticsEventBuilder } from '../../../../util/analytics/AnalyticsEvent
import { analytics } from '../../../../util/analytics/analytics';
import Logger from '../../../../util/Logger';
import type { PerpsAnalyticsEvent } from '@metamask/perps-controller';
import { createMobileInfrastructure } from './mobileInfrastructure';
import {
createMobileInfrastructure,
createMobileClientConfig,
} from './mobileInfrastructure';
import Engine from '../../../../core/Engine';

jest.mock('../../../../util/analytics/analytics', () => ({
Expand Down Expand Up @@ -212,3 +215,62 @@ describe('createMobileInfrastructure', () => {
});
});
});

describe('createMobileClientConfig', () => {
it('returns default config with empty strings and arrays when no env vars are set', () => {
// Arrange — ensure relevant env vars are absent
const envVars = [
'MM_PERPS_BLOCKED_REGIONS',
'MM_PERPS_HIP3_ENABLED',
'MM_PERPS_HIP3_ALLOWLIST_MARKETS',
'MM_PERPS_HIP3_BLOCKLIST_MARKETS',
'MM_PERPS_HL_BUILDER_ADDRESS_TESTNET',
'MM_PERPS_HL_BUILDER_ADDRESS_MAINNET',
'MM_PERPS_MYX_PROVIDER_ENABLED',
'MM_PERPS_MYX_APP_ID_TESTNET',
'MM_PERPS_MYX_API_SECRET_TESTNET',
'MM_PERPS_MYX_BROKER_ADDRESS_TESTNET',
'MM_PERPS_MYX_APP_ID_MAINNET',
'MM_PERPS_MYX_API_SECRET_MAINNET',
'MM_PERPS_MYX_BROKER_ADDRESS_MAINNET',
];
const saved: Record<string, string | undefined> = {};
for (const key of envVars) {
saved[key] = process.env[key];
delete process.env[key];
}

// Act
const config = createMobileClientConfig();

// Assert
expect(config).toEqual({
fallbackBlockedRegions: [],
fallbackHip3Enabled: false,
fallbackHip3AllowlistMarkets: [],
fallbackHip3BlocklistMarkets: [],
providerCredentials: {
hyperliquid: {
builderAddressTestnet: '',
builderAddressMainnet: '',
},
myx: {
enabled: false,
appIdTestnet: '',
apiSecretTestnet: '',
brokerAddressTestnet: '',
appIdMainnet: '',
apiSecretMainnet: '',
brokerAddressMainnet: '',
},
},
});

// Restore
for (const key of envVars) {
if (saved[key] !== undefined) {
process.env[key] = saved[key];
}
}
});
});
40 changes: 40 additions & 0 deletions app/components/UI/Perps/adapters/mobileInfrastructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import { getStreamManagerInstance } from '../providers/PerpsStreamManager';
import Engine from '../../../../core/Engine';
import {
PERPS_CONSTANTS,
parseCommaSeparatedString,
type PerpsPlatformDependencies,
type PerpsControllerConfig,
type PerpsMetrics,
type PerpsTraceName,
type PerpsTraceValue,
Expand Down Expand Up @@ -155,6 +157,44 @@ function createCacheInvalidatorAdapter() {
};
}

/**
* Creates mobile-specific client config from environment variables.
* Centralizes all process.env reads so the Engine init file stays pure wiring.
*/
export function createMobileClientConfig(): PerpsControllerConfig {
return {
fallbackBlockedRegions: parseCommaSeparatedString(
process.env.MM_PERPS_BLOCKED_REGIONS ?? '',
),
fallbackHip3Enabled: process.env.MM_PERPS_HIP3_ENABLED === 'true',
fallbackHip3AllowlistMarkets: parseCommaSeparatedString(
process.env.MM_PERPS_HIP3_ALLOWLIST_MARKETS ?? '',
),
fallbackHip3BlocklistMarkets: parseCommaSeparatedString(
process.env.MM_PERPS_HIP3_BLOCKLIST_MARKETS ?? '',
),
providerCredentials: {
hyperliquid: {
builderAddressTestnet:
process.env.MM_PERPS_HL_BUILDER_ADDRESS_TESTNET ?? '',
builderAddressMainnet:
process.env.MM_PERPS_HL_BUILDER_ADDRESS_MAINNET ?? '',
},
myx: {
enabled: process.env.MM_PERPS_MYX_PROVIDER_ENABLED === 'true',
appIdTestnet: process.env.MM_PERPS_MYX_APP_ID_TESTNET ?? '',
apiSecretTestnet: process.env.MM_PERPS_MYX_API_SECRET_TESTNET ?? '',
brokerAddressTestnet:
process.env.MM_PERPS_MYX_BROKER_ADDRESS_TESTNET ?? '',
appIdMainnet: process.env.MM_PERPS_MYX_APP_ID_MAINNET ?? '',
apiSecretMainnet: process.env.MM_PERPS_MYX_API_SECRET_MAINNET ?? '',
brokerAddressMainnet:
process.env.MM_PERPS_MYX_BROKER_ADDRESS_MAINNET ?? '',
},
},
};
}

/**
* Creates mobile-specific platform dependencies for PerpsController.
* Controller access uses messenger pattern (messenger.call()).
Expand Down
27 changes: 17 additions & 10 deletions app/components/UI/Perps/components/PerpsFillTag/PerpsFillTag.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { TouchableOpacity, Linking } from 'react-native';
import { View, TouchableOpacity, Linking } from 'react-native';
import { useSelector } from 'react-redux';
import Text, {
TextColor,
Expand All @@ -20,6 +20,7 @@ import {
PERPS_EVENT_VALUE,
} from '@metamask/perps-controller';
import { FillType, PerpsTransaction } from '../../types/transactionHistory';
import { PerpsTransactionSelectorsIDs } from '../../Perps.testIds';

interface PerpsFillTagProps {
transaction: PerpsTransaction;
Expand Down Expand Up @@ -61,6 +62,7 @@ const PerpsFillTag: React.FC<PerpsFillTagProps> = ({
severity: TagSeverity.Info,
textColor: TextColor.Default,
includesBorder: false,
testID: PerpsTransactionSelectorsIDs.FILL_TAG_ADL,
},
[FillType.Liquidation]: {
// Only show if liquidated user is current user
Expand All @@ -73,18 +75,21 @@ const PerpsFillTag: React.FC<PerpsFillTagProps> = ({
severity: TagSeverity.Danger,
textColor: TextColor.Error,
includesBorder: false,
testID: PerpsTransactionSelectorsIDs.FILL_TAG_LIQUIDATED,
},
[FillType.TakeProfit]: {
label: strings('perps.transactions.order.take_profit'),
severity: TagSeverity.Default,
textColor: TextColor.Alternative,
includesBorder: true,
testID: PerpsTransactionSelectorsIDs.FILL_TAG_TAKE_PROFIT,
},
[FillType.StopLoss]: {
label: strings('perps.transactions.order.stop_loss'),
severity: TagSeverity.Default,
textColor: TextColor.Alternative,
includesBorder: true,
testID: PerpsTransactionSelectorsIDs.FILL_TAG_STOP_LOSS,
},
};

Expand All @@ -95,15 +100,17 @@ const PerpsFillTag: React.FC<PerpsFillTagProps> = ({
}

const tagContent = (
<TagBase
shape={TagShape.Pill}
severity={tagConfig.severity}
includesBorder={tagConfig.includesBorder}
>
<Text variant={TextVariant.BodyXSMedium} color={tagConfig.textColor}>
{tagConfig.label}
</Text>
</TagBase>
<View testID={tagConfig.testID}>
<TagBase
shape={TagShape.Pill}
severity={tagConfig.severity}
includesBorder={tagConfig.includesBorder}
>
<Text variant={TextVariant.BodyXSMedium} color={tagConfig.textColor}>
{tagConfig.label}
</Text>
</TagBase>
</View>
);

// Only wrap in TouchableOpacity for ADL fill type which has an action.
Expand Down
51 changes: 51 additions & 0 deletions app/components/UI/Perps/hooks/usePerpsHomeData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,57 @@ describe('usePerpsHomeData', () => {
});
});

it('preserves detailedOrderType from REST fill when WS fill lacks it', async () => {
// Arrange — REST fill has enriched detailedOrderType
const restFill = createMockOrderFill({
orderId: 'fill-tp-1',
symbol: 'BTC',
timestamp: 1234567800,
detailedOrderType: 'Take Profit Limit',
});
const mockGetOrderFills = jest.fn().mockResolvedValue([restFill]);
(
Engine.context.PerpsController.getActiveProviderOrNull as jest.Mock
).mockReturnValue({
getOrderFills: mockGetOrderFills,
});

// WS fill with same key but no detailedOrderType
const wsFill = createMockOrderFill({
orderId: 'fill-tp-1',
symbol: 'BTC',
timestamp: 1234567800,
});
mockUsePerpsLiveFills.mockReturnValue({
fills: [wsFill],
isInitialLoading: false,
});

mockUsePerpsConnection.mockReturnValue({
isConnected: true,
isInitialized: true,
isConnecting: false,
error: null,
connect: jest.fn(),
disconnect: jest.fn(),
resetError: jest.fn(),
} as never);

// Act
const { result } = renderHook(() => usePerpsHomeData());
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});

// Assert — recentActivity contains the merged fill with preserved detailedOrderType
// The detailedOrderType from REST is preserved during merge, then
// transformFillsToTransactions converts it to FillType.TakeProfit
expect(result.current.recentActivity).toHaveLength(1);
expect(result.current.recentActivity[0].fill?.fillType).toBe(
FillType.TakeProfit,
);
});

it('handles special characters in search query', () => {
const { result } = renderHook(() =>
usePerpsHomeData({ searchQuery: '$BTC*' }),
Expand Down
13 changes: 12 additions & 1 deletion app/components/UI/Perps/hooks/usePerpsHomeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,20 @@ export const usePerpsHomeData = ({
}

// Add live fills (overwrites duplicates from REST - live data is fresher)
// Preserve detailedOrderType from REST fills since WS fills lack it
for (const fill of liveFills) {
const key = `${fill.orderId}-${fill.timestamp}`;
fillsMap.set(key, fill);
const existing = fillsMap.get(key);
if (existing?.detailedOrderType && !fill.detailedOrderType) {
fillsMap.set(key, {
...fill,
detailedOrderType: existing.detailedOrderType,
...(existing.liquidation &&
!fill.liquidation && { liquidation: existing.liquidation }),
});
} else {
fillsMap.set(key, fill);
}
}

// Convert back to array and sort by timestamp descending (newest first)
Expand Down
Loading
Loading