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
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,11 @@ const renderComponent = (overrides?: Partial<PredictActivityItem>) => {
};

describe('PredictActivity', () => {
it('renders BUY activity with title, market, detail, amount and percent', () => {
it('renders BUY activity with title, market, amount and percent', () => {
renderComponent();

expect(screen.getByText('Buy')).toBeOnTheScreen();
expect(screen.getByText(baseItem.marketTitle)).toBeOnTheScreen();
expect(screen.getByText(baseItem.detail)).toBeOnTheScreen();
expect(screen.getByText('-$1,234.50')).toBeOnTheScreen();
expect(screen.getByText('+1.50%')).toBeOnTheScreen();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const PredictActivity: React.FC<PredictActivityProps> = ({ item }) => {
const tw = useTailwind();
const navigation = useNavigation();
const isDebit = item.type === PredictActivityType.BUY;
const isCredit = !isDebit;
const signedAmount = `${isDebit ? '-' : '+'}${formatPrice(
Math.abs(item.amountUsd),
{
Expand All @@ -41,7 +40,6 @@ const PredictActivity: React.FC<PredictActivityProps> = ({ item }) => {
},
)}`;

const amountColor = isCredit ? 'text-success-default' : 'text-error-default';
const percentColor =
(item.percentChange ?? 0) >= 0
? 'text-success-default'
Expand All @@ -64,42 +62,31 @@ const PredictActivity: React.FC<PredictActivityProps> = ({ item }) => {
justifyContent={BoxJustifyContent.Between}
twClassName="w-full p-2"
>
<Box twClassName="h-12 w-12 items-center justify-center rounded-full bg-muted mr-3 overflow-hidden">
{item.icon ? (
<Image
source={{ uri: item.icon }}
style={tw.style('w-full h-full')}
accessibilityLabel="activity icon"
/>
) : (
<Icon name={IconName.Activity} />
)}
<Box twClassName="pt-1">
<Box twClassName="h-10 w-10 items-center justify-center rounded-full bg-muted mr-3 overflow-hidden">
{item.icon ? (
<Image
source={{ uri: item.icon }}
style={tw.style('w-full h-full')}
accessibilityLabel="activity icon"
/>
) : (
<Icon name={IconName.Activity} />
)}
</Box>
</Box>

<Box twClassName="flex-1">
<Text variant={TextVariant.BodyMd} numberOfLines={1}>
{activityTitleByType[item.type]}
</Text>
<Text
variant={TextVariant.BodySm}
twClassName="text-alternative"
numberOfLines={1}
>
<Text variant={TextVariant.BodySm} twClassName="text-alternative">
{item.marketTitle}
</Text>
{item.type !== PredictActivityType.CLAIM ? (
<Text
variant={TextVariant.BodySm}
twClassName="text-alternative"
numberOfLines={1}
>
{item.detail}
</Text>
) : null}
</Box>

<Box twClassName="items-end ml-3">
<Text variant={TextVariant.BodyMd} twClassName={amountColor}>
<Text variant={TextVariant.BodyMd} twClassName="text-alternative">
{signedAmount}
</Text>
{item.percentChange !== undefined ? (
Expand Down
25 changes: 23 additions & 2 deletions app/components/UI/Predict/providers/polymarket/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2085,13 +2085,13 @@ describe('polymarket utils', () => {
}
});

it('maps non-TRADE to claimWinnings entries and handles defaults', () => {
it('maps REDEEM with payout to claimWinnings entries', () => {
const input = [
{
type: 'REDEEM' as const,
side: '' as const,
timestamp: 3000,
usdcSize: 1.23,
usdcSize: 1.23, // Winning claim with actual payout
price: 0,
conditionId: '',
outcomeIndex: 0,
Expand All @@ -2102,11 +2102,32 @@ describe('polymarket utils', () => {
},
];
const result = parsePolymarketActivity(input);
expect(result).toHaveLength(1);
expect(result[0].entry.type).toBe('claimWinnings');
expect(result[0].entry.amount).toBe(1.23);
expect(result[0].id).toBe('0xhash3');
});

it('filters out REDEEM activities with no payout (lost positions)', () => {
const input = [
{
type: 'REDEEM' as const,
side: '' as const,
timestamp: 3000,
usdcSize: 0, // No payout - lost position
price: 0,
conditionId: '',
outcomeIndex: 0,
title: 'Lost Market',
outcome: '' as const,
icon: '',
transactionHash: '0xhash3',
},
];
const result = parsePolymarketActivity(input);
expect(result).toHaveLength(0);
});

it('generates fallback id and timestamp when missing', () => {
const input = [
{
Expand Down
109 changes: 62 additions & 47 deletions app/components/UI/Predict/providers/polymarket/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ export const parsePolymarketEvents = (
/**
* Normalizes Polymarket /activity entries to PredictActivity[]
* Keeps essential metadata used by UI (title/outcome/icon)
* Filters out claim activities with no payout (lost positions - technical clearing only)
*/
export const parsePolymarketActivity = (
activities: PolymarketApiActivity[],
Expand All @@ -458,53 +459,67 @@ export const parsePolymarketActivity = (
return [];
}

const parsedActivities: PredictActivity[] = activities.map((activity) => {
// Normalize entry type: TRADE with explicit side => buy/sell, otherwise claimWinnings
const entryType: 'buy' | 'sell' | 'claimWinnings' =
activity.type === 'TRADE'
? activity.side === 'BUY'
? 'buy'
: activity.side === 'SELL'
? 'sell'
: 'claimWinnings'
: 'claimWinnings';

const id =
activity.transactionHash ?? String(activity.timestamp ?? Math.random());
const timestamp = Number(activity.timestamp ?? Date.now());

const price = Number(activity.price ?? 0);
const amount = Number(activity.usdcSize ?? 0);

const outcomeId = String(activity.conditionId ?? '');
const marketId = String(activity.conditionId ?? '');
const outcomeTokenId = Number(activity.outcomeIndex ?? 0);
const title = String(activity.title ?? 'Market');
const outcome = activity.outcome ? String(activity.outcome) : undefined;
const icon = activity.icon as string | undefined;

const parsedActivity: PredictActivity = {
id,
providerId: 'polymarket',
entry:
entryType === 'claimWinnings'
? { type: 'claimWinnings', timestamp, amount }
: {
type: entryType,
timestamp,
marketId,
outcomeId,
outcomeTokenId,
amount,
price,
},
title,
outcome,
icon,
} as PredictActivity & { title?: string; outcome?: string; icon?: string };

return parsedActivity;
});
const parsedActivities: PredictActivity[] = activities
.map((activity) => {
// Normalize entry type: TRADE with explicit side => buy/sell, otherwise claimWinnings
const entryType: 'buy' | 'sell' | 'claimWinnings' =
activity.type === 'TRADE'
? activity.side === 'BUY'
? 'buy'
: activity.side === 'SELL'
? 'sell'
: 'claimWinnings'
: 'claimWinnings';

const id =
activity.transactionHash ?? String(activity.timestamp ?? Math.random());
const timestamp = Number(activity.timestamp ?? Date.now());

const price = Number(activity.price ?? 0);
const amount = Number(activity.usdcSize ?? 0);

const outcomeId = String(activity.conditionId ?? '');
const marketId = String(activity.conditionId ?? '');
const outcomeTokenId = Number(activity.outcomeIndex ?? 0);
const title = String(activity.title ?? 'Market');
const outcome = activity.outcome ? String(activity.outcome) : undefined;
const icon = activity.icon as string | undefined;

const parsedActivity: PredictActivity = {
id,
providerId: 'polymarket',
entry:
entryType === 'claimWinnings'
? { type: 'claimWinnings', timestamp, amount }
: {
type: entryType,
timestamp,
marketId,
outcomeId,
outcomeTokenId,
amount,
price,
},
title,
outcome,
icon,
} as PredictActivity & {
title?: string;
outcome?: string;
icon?: string;
};

return parsedActivity;
})
.filter((activity) => {
// Filter out claim activities with no actual payout
// These are lost positions being cleared - just technical operations with no transaction value
if (activity.entry.type === 'claimWinnings') {
return activity.entry.amount > 0;
}
return true;
});

return parsedActivities;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ describe('PredictTransactionsView', () => {
});

it('renders list items mapped from activity entries', () => {
const mockTimestamp = Math.floor(Date.now() / 1000); // Current time in seconds

(usePredictActivity as jest.Mock).mockReturnValueOnce({
isLoading: false,
activity: [
Expand All @@ -115,28 +117,52 @@ describe('PredictTransactionsView', () => {
title: 'Market A',
outcome: 'Yes',
icon: 'https://example.com/a.png',
entry: { type: 'buy', amount: 50, price: 0.34 },
entry: {
type: 'buy',
amount: 50,
price: 0.34,
timestamp: mockTimestamp,
marketId: 'market-a',
outcomeId: 'outcome-yes',
outcomeTokenId: 1,
},
},
{
id: 'b2',
title: 'Market B',
outcome: 'No',
icon: 'https://example.com/b.png',
entry: { type: 'sell', amount: 12.3, price: 0.7 },
entry: {
type: 'sell',
amount: 12.3,
price: 0.7,
timestamp: mockTimestamp - 100,
marketId: 'market-b',
outcomeId: 'outcome-no',
outcomeTokenId: 2,
},
},
{
id: 'c3',
title: 'Market C',
outcome: 'Yes',
icon: 'https://example.com/c.png',
entry: { type: 'claimWinnings', amount: 99.99 },
entry: {
type: 'claimWinnings',
amount: 99.99,
timestamp: mockTimestamp - 200,
},
},
{
id: 'd4',
title: 'Market D',
outcome: 'Yes',
icon: 'https://example.com/d.png',
entry: { type: 'unknown', amount: 1.23 },
entry: {
type: 'unknown',
amount: 1.23,
timestamp: mockTimestamp - 300,
},
},
],
});
Expand Down
Loading
Loading