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 @@ -608,18 +608,8 @@ describe('OndoCampaignRwaSelectorView', () => {
});
});

describe('token name sanitization', () => {
it('strips "Ondo Tokenized " prefix from token names in list rows', () => {
const token = { ...buildToken('AAPL'), name: 'Ondo Tokenized Apple' };
mockUseRwaTokens.mockReturnValue({ data: [token], isLoading: false });
const { getByText, queryByText } = render(
<OndoCampaignRwaSelectorView />,
);
expect(getByText('Apple')).toBeDefined();
expect(queryByText('Ondo Tokenized Apple')).toBeNull();
});

it('strips "(Ondo Tokenized)" suffix from token names in list rows', () => {
describe('Ondo token name display', () => {
it('preserves backend-provided suffix names in list rows', () => {
const token = {
...buildToken('AAPL'),
name: 'Apple (Ondo Tokenized)',
Expand All @@ -628,25 +618,21 @@ describe('OndoCampaignRwaSelectorView', () => {
const { getByText, queryByText } = render(
<OndoCampaignRwaSelectorView />,
);
expect(getByText('Apple')).toBeDefined();
expect(queryByText('Apple (Ondo Tokenized)')).toBeNull();
});

it('leaves unrelated token names unchanged', () => {
const token = { ...buildToken('USDY'), name: 'Ondo USD Yield' };
mockUseRwaTokens.mockReturnValue({ data: [token], isLoading: false });
const { getByText } = render(<OndoCampaignRwaSelectorView />);
expect(getByText('Ondo USD Yield')).toBeDefined();
expect(getByText('Apple (Ondo Tokenized)')).toBeDefined();
expect(queryByText('Apple')).toBeNull();
});

it('passes original unsanitized name to goToSwaps when token has Ondo prefix', () => {
const token = { ...buildToken('AAPL'), name: 'Ondo Tokenized Apple' };
it('passes the backend-provided name to goToSwaps', () => {
const token = {
...buildToken('AAPL'),
name: 'Apple (Ondo Tokenized)',
};
mockUseRwaTokens.mockReturnValue({ data: [token], isLoading: false });
const { getByTestId } = render(<OndoCampaignRwaSelectorView />);
fireEvent.press(getByTestId('token-row-AAPL'));
expect(mockGoToSwaps).toHaveBeenCalledWith(
undefined,
expect.objectContaining({ name: 'Ondo Tokenized Apple' }),
expect.objectContaining({ name: 'Apple (Ondo Tokenized)' }),
);
});
});
Expand Down
26 changes: 9 additions & 17 deletions app/components/UI/Rewards/Views/OndoCampaignRwaSelectorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ import ErrorBoundary from '../../../Views/ErrorBoundary';
import { useRwaTokens } from '../../Trending/hooks/useRwaTokens/useRwaTokens';
import TrendingTokenRowItem from '../../Trending/components/TrendingTokenRowItem/TrendingTokenRowItem';
import { getTrendingTokenImageUrl } from '../../Trending/utils/getTrendingTokenImageUrl';
import {
parseCaip19,
caipChainIdToHex,
sanitizeOndoTokenName,
} from '../utils/formatUtils';
import { parseCaip19, caipChainIdToHex } from '../utils/formatUtils';
import { RWA_NETWORKS_LIST } from '../../Trending/utils/trendingNetworksList';
import {
useSwapBridgeNavigation,
Expand Down Expand Up @@ -262,30 +258,27 @@ const OndoCampaignRwaSelectorView: React.FC = () => {
sourceToken: srcBridgeToken,
});

// Deduplicate by assetId and sanitize display names.
// Deduplicate by assetId while preserving backend-provided display names.
// Use CAIP-19 assetId (not symbol) for deduplication — symbol comparison
// is fragile when casing differs between chains.
const tokens = useMemo((): TrendingAsset[] => {
const seen = new Set<string>();
return rwaTokens
.filter((token) => {
if (srcTokenAsset && token.assetId === srcTokenAsset) return false;
if (seen.has(token.assetId)) return false;
seen.add(token.assetId);
return true;
})
.map((token) => ({ ...token, name: sanitizeOndoTokenName(token.name) }));
return rwaTokens.filter((token) => {
if (srcTokenAsset && token.assetId === srcTokenAsset) return false;
if (seen.has(token.assetId)) return false;
seen.add(token.assetId);
return true;
});
}, [rwaTokens, srcTokenAsset]);

const handleAssetSelect = useCallback(
(asset: TrendingAsset) => {
const parsed = parseCaip19(asset.assetId);
if (!parsed) return;
const rawToken = rwaTokens.find((t) => t.assetId === asset.assetId);
const destToken: BridgeToken = {
address: parsed.assetReference,
symbol: asset.symbol,
name: rawToken?.name ?? asset.name,
name: asset.name,
decimals: asset.decimals,
chainId: `${parsed.namespace}:${parsed.chainId}` as CaipChainId,
image: getTrendingTokenImageUrl(asset.assetId),
Expand Down Expand Up @@ -318,7 +311,6 @@ const OndoCampaignRwaSelectorView: React.FC = () => {
trackEvent,
createEventBuilder,
ondoUsdSrcToken,
rwaTokens,
],
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ jest.mock('../../../../../../locales/i18n', () => ({
'Please try again',
'rewards.ondo_campaign_portfolio.retry': 'Retry',
'rewards.ondo_campaign_portfolio.updated_at': `Updated: ${params?.time ?? ''}`,
'rewards.ondo_campaign_portfolio.position_units': `${params?.units ?? ''} units`,
};
return translations[key] ?? key;
},
Expand Down Expand Up @@ -217,10 +216,11 @@ jest.mock(
);

const mockRefetch = jest.fn();
const MOCK_POSITION_DISPLAY_NAME = 'Apple Inc. (Ondo Tokenized)';

const MOCK_POSITION: OndoGmPortfolioPositionDto = {
tokenSymbol: 'AAPLon',
tokenName: 'Apple Inc.',
tokenName: MOCK_POSITION_DISPLAY_NAME,
tokenAsset: 'eip155:1/erc20:0x14c3abf95cb9c93a8b82c1cdcb76d72cb87b2d4c',
units: '45.2',
bookPrice: '200.000000',
Expand Down Expand Up @@ -369,7 +369,7 @@ describe('OndoPortfolio', () => {
it('renders the token name', () => {
const { getByText } = render(<OndoPortfolio {...loadedProps} />);

expect(getByText('Apple Inc.')).toBeDefined();
expect(getByText(MOCK_POSITION_DISPLAY_NAME)).toBeDefined();
});
});

Expand All @@ -395,8 +395,8 @@ describe('OndoPortfolio', () => {

it('pressing a position row does not throw', () => {
const { getByText } = render(<OndoPortfolio {...loadedProps} />);
fireEvent.press(getByText('Apple Inc.'));
expect(getByText('Apple Inc.')).toBeDefined();
fireEvent.press(getByText(MOCK_POSITION_DISPLAY_NAME));
expect(getByText(MOCK_POSITION_DISPLAY_NAME)).toBeDefined();
});

it('renders empty banner when portfolio has no positions', () => {
Expand Down Expand Up @@ -480,7 +480,7 @@ describe('OndoPortfolio', () => {
const props = buildPropsWithBalance(rawHexBalance);
const { getByText } = render(<OndoPortfolio {...props} />);

fireEvent.press(getByText('Apple Inc.'));
fireEvent.press(getByText(MOCK_POSITION_DISPLAY_NAME));

expect(props.onOpenAccountPicker).not.toHaveBeenCalled();
},
Expand All @@ -493,7 +493,9 @@ describe('OndoPortfolio', () => {
// empty and the component navigates directly (length === 0 branch).
// Picker not opened either way — we just confirm no throw.
const { getByText } = render(<OndoPortfolio {...props} />);
expect(() => fireEvent.press(getByText('Apple Inc.'))).not.toThrow();
expect(() =>
fireEvent.press(getByText(MOCK_POSITION_DISPLAY_NAME)),
).not.toThrow();
});
});

Expand All @@ -503,9 +505,9 @@ describe('OndoPortfolio', () => {
portfolio: MOCK_PORTFOLIO,
};

it('renders the units text', () => {
it('renders units with the uppercased ticker', () => {
const { getByText } = render(<OndoPortfolio {...loadedProps} />);
expect(getByText('45.2 units')).toBeDefined();
expect(getByText('45.2 AAPLON')).toBeDefined();
});

it('renders positive PnL percent in green', () => {
Expand Down Expand Up @@ -572,7 +574,7 @@ describe('OndoPortfolio', () => {
onOpenAccountPicker={onOpenAccountPicker}
/>,
);
fireEvent.press(getByText('Apple Inc.'));
fireEvent.press(getByText(MOCK_POSITION_DISPLAY_NAME));
expect(onOpenAccountPicker).not.toHaveBeenCalled();
});
});
Expand Down Expand Up @@ -673,7 +675,7 @@ describe('OndoPortfolio', () => {
/>,
);

fireEvent.press(getByText('Apple Inc.'));
fireEvent.press(getByText(MOCK_POSITION_DISPLAY_NAME));

expect(onOpenAccountPicker).toHaveBeenCalledTimes(1);
const config = (onOpenAccountPicker as jest.Mock).mock.calls[0][0];
Expand Down Expand Up @@ -726,7 +728,7 @@ describe('OndoPortfolio', () => {
/>,
);

fireEvent.press(getByText('Apple Inc.'));
fireEvent.press(getByText(MOCK_POSITION_DISPLAY_NAME));

expect(onOpenAccountPicker).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -788,7 +790,7 @@ describe('OndoPortfolio', () => {
/>,
);

fireEvent.press(getByText('Apple Inc.'));
fireEvent.press(getByText(MOCK_POSITION_DISPLAY_NAME));

// Account must be found despite key case mismatch → picker opened
expect(onOpenAccountPicker).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -850,7 +852,7 @@ describe('OndoPortfolio', () => {
/>,
);

fireEvent.press(getByText('Apple Inc.'));
fireEvent.press(getByText(MOCK_POSITION_DISPLAY_NAME));

expect(onOpenAccountPicker).toHaveBeenCalledTimes(1);
const config = (onOpenAccountPicker as jest.Mock).mock.calls[0][0];
Expand Down Expand Up @@ -908,7 +910,7 @@ describe('OndoPortfolio', () => {
/>,
);

fireEvent.press(getByText('Apple Inc.'));
fireEvent.press(getByText(MOCK_POSITION_DISPLAY_NAME));

// No subscribed account has balance → navigate directly, picker not opened
expect(onOpenAccountPicker).not.toHaveBeenCalled();
Expand Down
25 changes: 12 additions & 13 deletions app/components/UI/Rewards/components/Campaigns/OndoPortfolio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import {
groupPortfolioPositionsByAsset,
formatPnlPercent,
isPnlNonNegative,
sanitizeOndoTokenName,
} from './OndoPortfolio.utils';
import { selectAllTokenBalances } from '../../../../../selectors/tokenBalancesController';
import { selectAllTokens } from '../../../../../selectors/tokensController';
Expand Down Expand Up @@ -458,15 +457,20 @@ const OndoPortfolio: React.FC<OndoPortfolioProps> = ({
justifyContent={BoxJustifyContent.Between}
alignItems={BoxAlignItems.Center}
>
<Box twClassName="flex-1 min-w-0 pr-2">
<Text
variant={TextVariant.BodyMd}
fontWeight={FontWeight.Medium}
numberOfLines={1}
ellipsizeMode="tail"
>
{row.tokenName}
</Text>
</Box>
<Text
variant={TextVariant.BodyMd}
fontWeight={FontWeight.Medium}
>
{sanitizeOndoTokenName(row.tokenName)}
</Text>
<Text
variant={TextVariant.BodyMd}
fontWeight={FontWeight.Medium}
numberOfLines={1}
>
{formatUsd(row.currentValue)}
</Text>
Expand All @@ -480,12 +484,7 @@ const OndoPortfolio: React.FC<OndoPortfolioProps> = ({
variant={TextVariant.BodySm}
color={TextColor.TextAlternative}
>
{strings(
'rewards.ondo_campaign_portfolio.position_units',
{
units: row.units,
},
)}
{`${row.units} ${row.tokenSymbol.toUpperCase()}`}
</Text>
{rowPnlPercent ? (
<Text variant={TextVariant.BodySm} color={rowPnlColor}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
groupPortfolioPositionsByAsset,
formatPnlPercent,
isPnlNonNegative,
sanitizeOndoTokenName,
} from './OndoPortfolio.utils';

describe('groupPortfolioPositionsByAsset', () => {
Expand Down Expand Up @@ -112,45 +111,3 @@ describe('isPnlNonNegative', () => {
expect(isPnlNonNegative('—')).toBe(false);
});
});

describe('sanitizeOndoTokenName', () => {
it('strips "(Ondo Tokenized)" suffix and trims', () => {
expect(sanitizeOndoTokenName('US Dollar (Ondo Tokenized)')).toBe(
'US Dollar',
);
});

it('strips "Ondo Tokenized " prefix (trending token API format)', () => {
expect(sanitizeOndoTokenName('Ondo Tokenized Apple')).toBe('Apple');
});

it('is case-insensitive', () => {
expect(sanitizeOndoTokenName('Token (ondo tokenized)')).toBe('Token');
});

it('truncates to 28 characters with ellipsis', () => {
expect(sanitizeOndoTokenName('A Very Long Token Name That Exceeds')).toBe(
'A Very Long Token Name That...',
);
});

it('strips then truncates with ellipsis', () => {
const long = 'Extremely Long Name Here That Keeps Going (Ondo Tokenized)';
const result = sanitizeOndoTokenName(long);
expect(result).toBe('Extremely Long Name Here Tha...');
});

it('does not add ellipsis when exactly 28 characters', () => {
expect(sanitizeOndoTokenName('1234567890123456789012345678')).toBe(
'1234567890123456789012345678',
);
});

it('returns the name unchanged when no stripping or truncation is needed', () => {
expect(sanitizeOndoTokenName('OUSG')).toBe('OUSG');
});

it('handles empty string', () => {
expect(sanitizeOndoTokenName('')).toBe('');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export {
getChainHex,
shortenAddress,
getAssetReference,
sanitizeOndoTokenName,
} from '../../utils/formatUtils';

/**
Expand Down
Loading
Loading