Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,46 @@ describe('TraderPositionView', () => {
).toBeOnTheScreen();
});

describe('perp positions', () => {
beforeEach(() => {
mockRouteParams.position = {
...makeDefaultPosition(),
tokenSymbol: 'ETH',
chain: 'hyperliquid',
perpPositionType: 'short',
perpLeverage: 10,
};
});

it('renders the Trade button instead of the Buy button', () => {
renderWithProvider(<TraderPositionView />, { state: mockState });

expect(
screen.getByTestId(TraderPositionViewSelectorsIDs.TRADE_BUTTON),
).toBeOnTheScreen();
expect(
screen.queryByTestId(TraderPositionViewSelectorsIDs.BUY_BUTTON),
).not.toBeOnTheScreen();
});

it('does not open QuickBuy when the Trade button is pressed', () => {
renderWithProvider(<TraderPositionView />, { state: mockState });

fireEvent.press(
screen.getByTestId(TraderPositionViewSelectorsIDs.TRADE_BUTTON),
);

expect(mockPlayImpact).not.toHaveBeenCalled();
});

it('renders the perp leverage and direction badges in the header', () => {
renderWithProvider(<TraderPositionView />, { state: mockState });

expect(screen.getByText('10x')).toBeOnTheScreen();
expect(screen.getByText('SHORT')).toBeOnTheScreen();
});
});

it('forwards the filtered trades to the chart component', async () => {
renderWithProvider(<TraderPositionView />, { state: mockState });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const TraderPositionViewSelectorsIDs = {
BACK_BUTTON: 'trader-position-view-back-button',
TRADER_NAME_LINK: 'trader-position-view-trader-name-link',
BUY_BUTTON: 'trader-position-view-buy-button',
TRADE_BUTTON: 'trader-position-view-trade-button',
COPY_TOKEN_ADDRESS_BUTTON: 'trader-position-view-copy-token-address-button',
SKELETON: 'trader-position-skeleton',
FALLBACK: 'trader-position-fallback',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
} from '../analytics';
import { MetaMetricsEvents } from '../../../../core/Analytics';
import { chainNameToId } from '../utils/chainMapping';
import { isPerpPosition } from '../utils/perp';
import { toAssetId } from '../../../UI/Bridge/hooks/useAssetMetadata/utils';

const TraderPositionView = () => {
Expand Down Expand Up @@ -287,6 +288,12 @@ const TraderPositionView = () => {
// TODO: update displayed price on scrub.
}, []);

// Perp positions surface Long/Short CTAs instead of Buy. Perp trade entry
// isn't wired up yet, so these are intentional placeholders that don't
// navigate anywhere (a future ticket will hook them up).
const isPerp = resolvedPosition ? isPerpPosition(resolvedPosition) : false;
const handlePerpActionPress = useCallback(() => undefined, []);

const isInitialLoading =
!resolvedPosition && (isPositionLoading || isProfileLoading);
const hasFailed =
Expand Down Expand Up @@ -364,25 +371,42 @@ const TraderPositionView = () => {
/>
</ScrollView>

<Box twClassName="px-4 py-3">
<ButtonHero
size={ButtonHeroSize.Lg}
isFullWidth
onPress={handleBuyPress}
testID={TraderPositionViewSelectorsIDs.BUY_BUTTON}
>
{strings('social_leaderboard.trader_position.buy')}
</ButtonHero>
</Box>

<TraderPositionQuickBuy
isVisible={isQuickBuyVisible}
position={resolvedPosition ?? null}
onClose={handleQuickBuyClose}
traderAddress={traderAddress}
marketCap={typeof marketCap === 'number' ? marketCap : undefined}
source={quickBuySource}
/>
{isPerp ? (
<Box twClassName="px-4 py-3">
<ButtonHero
size={ButtonHeroSize.Lg}
isFullWidth
onPress={handlePerpActionPress}
testID={TraderPositionViewSelectorsIDs.TRADE_BUTTON}
>
{strings('social_leaderboard.trader_position.trade')}
</ButtonHero>
</Box>
) : (
<>
<Box twClassName="px-4 py-3">
<ButtonHero
size={ButtonHeroSize.Lg}
isFullWidth
onPress={handleBuyPress}
testID={TraderPositionViewSelectorsIDs.BUY_BUTTON}
>
{strings('social_leaderboard.trader_position.buy')}
</ButtonHero>
</Box>

<TraderPositionQuickBuy
isVisible={isQuickBuyVisible}
position={resolvedPosition ?? null}
onClose={handleQuickBuyClose}
traderAddress={traderAddress}
marketCap={
typeof marketCap === 'number' ? marketCap : undefined
}
source={quickBuySource}
/>
</>
)}
</>
)}
</SafeAreaView>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
import type { Trade } from '@metamask/social-controllers';
import { strings } from '../../../../../../locales/i18n';
import { formatUsd, formatTradeDate } from '../../utils/formatters';
import PerpBadges from '../../components/PerpBadges';
import { getPerpTradeDirection, isPerpTrade } from '../../utils/perp';

export interface TradeRowProps {
trade: Trade;
Expand All @@ -29,6 +31,26 @@ const TradeRow: React.FC<TradeRowProps> = ({
}) => {
const tw = useTailwind();
const isEntry = trade.intent === 'enter';
const isPerp = isPerpTrade(trade);
const perpDirection = getPerpTradeDirection(trade);

// Perp fills read as "opened"/"closed" (vs spot "bought"/"sold").
const actionLabel = isPerp
? isEntry
? strings('social_leaderboard.trader_position.opened', {
name: traderName,
})
: strings('social_leaderboard.trader_position.closed_action', {
name: traderName,
})
: isEntry
? strings('social_leaderboard.trader_position.bought', {
name: traderName,
})
: strings('social_leaderboard.trader_position.sold', {
name: traderName,
});

return (
<Box
flexDirection={BoxFlexDirection.Row}
Expand All @@ -55,20 +77,28 @@ const TradeRow: React.FC<TradeRowProps> = ({
/>
)}
<Box twClassName="flex-1 min-w-0">
<Text
variant={TextVariant.BodyMd}
fontWeight={FontWeight.Medium}
color={TextColor.TextDefault}
numberOfLines={1}
<Box
flexDirection={BoxFlexDirection.Row}
alignItems={BoxAlignItems.Center}
gap={2}
>
{isEntry
? strings('social_leaderboard.trader_position.bought', {
name: traderName,
})
: strings('social_leaderboard.trader_position.sold', {
name: traderName,
})}
</Text>
<Text
variant={TextVariant.BodyMd}
fontWeight={FontWeight.Medium}
color={TextColor.TextDefault}
numberOfLines={1}
twClassName="shrink"
>
{actionLabel}
</Text>
{perpDirection ? (
<PerpBadges
direction={perpDirection}
leverage={trade.perpLeverage}
testID={`trade-row-perp-badges-${trade.transactionHash}`}
/>
) : null}
</Box>
<Text
variant={TextVariant.BodySm}
color={TextColor.TextAlternative}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import type { Position } from '@metamask/social-controllers';
import { strings } from '../../../../../../locales/i18n';
import { formatCompactUsd } from '../../../../UI/Rewards/utils/formatUtils';
import PositionTokenAvatar from '../../components/PositionTokenAvatar';
import PerpBadges from '../../components/PerpBadges';
import { getPerpPositionDirection } from '../../utils/perp';

export interface TraderTokenInfoRowProps {
symbol: string;
Expand Down Expand Up @@ -52,6 +54,7 @@ const TraderTokenIdentity: React.FC<TraderTokenIdentityProps> = ({
const canCopyTokenAddress = Boolean(
position?.tokenAddress && onCopyTokenAddress,
);
const perpDirection = position ? getPerpPositionDirection(position) : null;

const content = (
<Box
Expand All @@ -68,16 +71,24 @@ const TraderTokenIdentity: React.FC<TraderTokenIdentityProps> = ({
<Box
flexDirection={BoxFlexDirection.Row}
alignItems={BoxAlignItems.Center}
gap={1}
gap={2}
>
<Text
variant={TextVariant.BodyMd}
fontWeight={FontWeight.Medium}
color={TextColor.TextDefault}
numberOfLines={1}
twClassName="shrink"
>
{symbol}
</Text>
{perpDirection ? (
<PerpBadges
direction={perpDirection}
leverage={position?.perpLeverage}
testID="trader-position-perp-badges"
/>
) : null}
{canCopyTokenAddress ? (
<Icon
name={IconName.Copy}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,46 @@ describe('PositionRow', () => {
expect(screen.getByText('+25%')).toBeOnTheScreen();
});
});

describe('perp positions', () => {
const perpPosition: Position = {
...basePosition,
tokenSymbol: 'ETH',
chain: 'hyperliquid',
perpPositionType: 'long',
perpLeverage: 5,
positionAmountWithLeverage: 25,
};

it('renders the leverage and LONG direction badges for a long perp', () => {
renderWithProvider(<PositionRow position={perpPosition} />);

expect(screen.getByText('5x')).toBeOnTheScreen();
expect(screen.getByText('LONG')).toBeOnTheScreen();
});

it('renders a SHORT badge for a short perp', () => {
const position = { ...perpPosition, perpPositionType: 'short' as const };

renderWithProvider(<PositionRow position={position} />);

expect(screen.getByText('SHORT')).toBeOnTheScreen();
});

it('omits the leverage badge when perpLeverage is null', () => {
const position = { ...perpPosition, perpLeverage: null };

renderWithProvider(<PositionRow position={position} />);

expect(screen.queryByText('5x')).not.toBeOnTheScreen();
expect(screen.getByText('LONG')).toBeOnTheScreen();
});

it('does not render perp badges for a spot position', () => {
renderWithProvider(<PositionRow position={basePosition} />);

expect(screen.queryByText('LONG')).not.toBeOnTheScreen();
expect(screen.queryByText('SHORT')).not.toBeOnTheScreen();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
} from '@metamask/design-system-react-native';
import type { Position } from '@metamask/social-controllers';
import PositionTokenAvatar from '../../components/PositionTokenAvatar';
import PerpBadges from '../../components/PerpBadges';
import { getPerpPositionDirection } from '../../utils/perp';
import {
formatUsd,
formatTokenAmount,
Expand Down Expand Up @@ -41,6 +43,8 @@ const PositionRow: React.FC<PositionRowProps> = ({ position, onPress }) => {
const isPnlPositive = hasPnl && (displayPnlPercent ?? 0) >= 0;
const testID = `position-row-${position.tokenSymbol}`;

const perpDirection = getPerpPositionDirection(position);

const content = (
<Box
flexDirection={BoxFlexDirection.Row}
Expand All @@ -58,14 +62,28 @@ const PositionRow: React.FC<PositionRowProps> = ({ position, onPress }) => {
<PositionTokenAvatar position={position} showChainBadge />

<Box twClassName="flex-1 min-w-0">
<Text
variant={TextVariant.BodyMd}
fontWeight={FontWeight.Medium}
color={TextColor.TextDefault}
numberOfLines={1}
<Box
flexDirection={BoxFlexDirection.Row}
alignItems={BoxAlignItems.Center}
gap={2}
>
{position.tokenSymbol}
</Text>
<Text
variant={TextVariant.BodyMd}
fontWeight={FontWeight.Medium}
color={TextColor.TextDefault}
numberOfLines={1}
twClassName="shrink"
>
{position.tokenSymbol}
</Text>
{perpDirection ? (
<PerpBadges
direction={perpDirection}
leverage={position.perpLeverage}
testID={`position-row-perp-badges-${position.tokenSymbol}`}
/>
) : null}
</Box>
<Text
variant={TextVariant.BodySm}
color={TextColor.TextAlternative}
Expand Down
Loading
Loading