Skip to content
Open
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
58 changes: 58 additions & 0 deletions app/components/UI/ActivityListItemRow/ActivityListItemRow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,64 @@ describe('ActivityListItemRow — row content', () => {
jest.mocked(useTokensData).mockReturnValue({});
});

it('resolves a lending-deposit token symbol/decimals from the tokens API and scales the amount', () => {
const assetId =
'eip155:42161/erc20:0x0000000000000000000000000000000000000002';
jest.mocked(useTokensData).mockReturnValue({
[assetId]: {
assetId,
symbol: 'USDT',
decimals: 6,
name: 'Tether USD',
iconUrl: '',
},
});

// The adapter carries only the atomic amount + asset id (the tx targets the
// pool, so symbol/decimals aren't in local metadata). Without decimals the
// amount would render unscaled (10,000 instead of 0.01).
const item = makeItem({
type: 'lendingDeposit',
status: 'success',
chainId: 'eip155:42161',
sourceToken: { amount: '10000', direction: 'out', assetId },
});

const { getByTestId } = render(
<ActivityListItemRow item={item} index={0} />,
);

expect(getByTestId('activity-primary-amount-0xabc').props.children).toBe(
'-0.01 USDT',
);
expect(getByTestId('avatar-token-USDT')).toBeOnTheScreen();

jest.mocked(useTokensData).mockReturnValue({});
});

it('renders a lending-deposit amount from adapter-provided decimals without an API lookup', () => {
// When the adapter already resolved symbol/decimals, the row scales the
// amount without depending on the tokens API (which returns nothing here).
const item = makeItem({
type: 'lendingDeposit',
status: 'success',
sourceToken: {
amount: '10000',
decimals: 6,
symbol: 'USDC',
direction: 'out',
},
});

const { getByTestId } = render(
<ActivityListItemRow item={item} index={0} />,
);

expect(getByTestId('activity-primary-amount-0xabc').props.children).toBe(
'-0.01 USDC',
);
});

it('renders cross-token bridge as swapped with token pair subtitle', () => {
const item = makeItem({
type: 'bridge',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
applyDisplaySign,
type ActivityKind,
type ActivityListItem,
enrichTokenFromApi,
getDisplaySignPrefix,
getHumanReadableTokenAmount,
isUnlimitedApprovalAmount,
Expand Down Expand Up @@ -1131,17 +1132,45 @@ export function useActivityListItemRowContent(
)
: undefined;

const isLending =
item.type === 'lendingDeposit' || item.type === 'lendingWithdrawal';
const lendingAssetIds: string[] = [];
if (isLending) {
if (
'destinationToken' in item.data &&
item.data.destinationToken?.assetId
) {
lendingAssetIds.push(item.data.destinationToken.assetId);
}
if ('sourceToken' in item.data && item.data.sourceToken?.assetId) {
lendingAssetIds.push(item.data.sourceToken.assetId);
}
}
const lendingTokenData = useTokensData(lendingAssetIds);

const content = resolveCoreContent(item, bridgeHistoryItem);

let basePrimaryToken: TokenAmount | undefined;
if (isSpendingCap) {
basePrimaryToken = spendingCapToken?.amount ? spendingCapToken : undefined;
} else if (isLending) {
basePrimaryToken = enrichTokenFromApi(
content.primaryToken,
lendingTokenData,
);
} else {
basePrimaryToken = content.primaryToken;
}
const primaryToken = enrichStablecoinTokenMetadata(
isSpendingCap
? spendingCapToken?.amount
? spendingCapToken
: undefined
: content.primaryToken,
basePrimaryToken,
networkChainId,
);

const baseSecondaryToken = isLending
? enrichTokenFromApi(content.secondaryToken, lendingTokenData)
: content.secondaryToken;
const secondaryToken = enrichStablecoinTokenMetadata(
content.secondaryToken,
baseSecondaryToken,
networkChainId,
);
const isPerpsFunding = isPerpsFundingKind(item.type);
Expand Down Expand Up @@ -1205,12 +1234,18 @@ export function useActivityListItemRowContent(
? getPredictActivity(item)?.icon
: undefined;

let avatarTokens: TokenAmount[];
if (isSpendingCap && spendingCapToken) {
avatarTokens = [spendingCapToken];
} else if (isLending && primaryToken) {
avatarTokens = [primaryToken];
} else {
avatarTokens = resolveAvatarTokens(item, bridgeHistoryItem);
}

return {
...content,
avatarTokens:
isSpendingCap && spendingCapToken
? [spendingCapToken]
: resolveAvatarTokens(item, bridgeHistoryItem),
avatarTokens,
avatarIconUrl: predictIconUrl,
perpsMarketSymbol,
primaryToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ScrollView, View } from 'react-native';
import { useSelector } from 'react-redux';
import { strings } from '../../../../../../locales/i18n';
import Routes from '../../../../../constants/navigation/Routes';
import { navigateToActivityAfterConfirmation } from '../../../../../util/navigation/navigateToActivityAfterConfirmation';
import Engine from '../../../../../core/Engine';
import { selectSelectedInternalAccountByScope } from '../../../../../selectors/multichainAccounts/accounts';
import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController';
Expand Down Expand Up @@ -415,7 +415,7 @@ const EarnLendingDepositConfirmationView = () => {
});
// There is variance in when navigation can be called across chains
setTimeout(() => {
navigation.navigate(Routes.TRANSACTIONS_VIEW);
navigateToActivityAfterConfirmation(navigation);
}, 0);
},
({ transactionMeta }) => transactionMeta.id === transactionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Badge, {
import Text, {
TextVariant,
} from '../../../../../component-library/components/Texts/Text';
import Routes from '../../../../../constants/navigation/Routes';
import { navigateToActivityAfterConfirmation } from '../../../../../util/navigation/navigateToActivityAfterConfirmation';
import {
IMetaMetricsEvent,
MetaMetricsEvents,
Expand Down Expand Up @@ -316,7 +316,7 @@ const EarnLendingWithdrawalConfirmationView = () => {
});
// There is variance in when navigation can be called across chains
setTimeout(() => {
navigation.navigate(Routes.TRANSACTIONS_VIEW);
navigateToActivityAfterConfirmation(navigation);
}, 0);
},
({ transactionMeta }) => transactionMeta.id === transactionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
FooterButtonGroupActions,
FooterButtonGroupProps,
} from './FooterButtonGroup.types';
import Routes from '../../../../../../../constants/navigation/Routes';
import { navigateToActivityAfterConfirmation } from '../../../../../../../util/navigation/navigateToActivityAfterConfirmation';
import usePoolStakedUnstake from '../../../../hooks/usePoolStakedUnstake';
import { useAnalytics } from '../../../../../../hooks/useAnalytics/useAnalytics';
import {
Expand Down Expand Up @@ -59,7 +59,6 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => {
const { styles } = useStyles(styleSheet, {});

const navigation = useNavigation();
const { navigate } = navigation;

const { trackEvent, createEventBuilder } = useAnalytics();

Expand Down Expand Up @@ -117,7 +116,7 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => {
() => {
submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].SUBMITTED);
setDidSubmitTransaction(false);
navigate(Routes.TRANSACTIONS_VIEW);
navigateToActivityAfterConfirmation(navigation);
},
({ transactionMeta }) => transactionMeta.id === transactionId,
);
Expand Down Expand Up @@ -148,7 +147,7 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => {
(transactionMeta) => transactionMeta.id === transactionId,
);
},
[action, navigate, submitTxMetaMetric],
[action, navigation, submitTxMetaMetric],
);

const handleConfirmation = async () => {
Expand Down
108 changes: 108 additions & 0 deletions app/components/Views/ActivityDetails/templates/SwapDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import type {
ActivityListItem,
TokenAmount,
} from '../../../../util/activity-adapters';
import { useTokensData } from '../../../hooks/useTokensData/useTokensData';
import { SwapDetails } from './SwapDetails';

// Capture the token the amount header receives so we can assert it was enriched
// with decimals before formatting.
let capturedSentToken: TokenAmount | undefined;

jest.mock('../../../hooks/useTokensData/useTokensData', () => ({
useTokensData: jest.fn(() => ({})),
}));

jest.mock('../components', () => ({
ActivityDetailsBlockExplorerButton: () => null,
ActivityDetailsDoItAgainButton: () => null,
ActivityDetailsFooter: () => null,
ActivityDetailsMetadata: () => null,
ActivityDetailsFeesAndTotal: () => null,
ActivityDetailsDualAmountHeader: ({
sentToken,
}: {
sentToken?: TokenAmount;
}) => {
capturedSentToken = sentToken;
return null;
},
}));

jest.mock('../hooks/useActivityDetailsDoItAgain', () => ({
useActivityDetailsDoItAgain: () => jest.fn(),
canRenderActivityDetailsDoItAgain: () => false,
}));

const USDT_ASSET_ID =
'eip155:42161/erc20:0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9';

const makeLendingDepositItem = (
sourceToken: Partial<TokenAmount>,
): ActivityListItem =>
({
type: 'lendingDeposit',
chainId: 'eip155:42161',
status: 'success',
timestamp: 1,
hash: '0xabc',
data: { sourceToken },
}) as unknown as ActivityListItem;

describe('SwapDetails', () => {
beforeEach(() => {
capturedSentToken = undefined;
jest.mocked(useTokensData).mockReturnValue({});
});

it('enriches the deposited token decimals from the tokens API so the amount is not rendered as raw base units', () => {
// The adapter left `decimals` off the lending sourceToken; without
// enrichment the amount header would format 10000 base units as "10,000"
// instead of 0.01 USDT.
jest.mocked(useTokensData).mockReturnValue({
[USDT_ASSET_ID]: {
assetId: USDT_ASSET_ID,
symbol: 'USDT',
decimals: 6,
name: 'Tether USD',
iconUrl: '',
},
});

render(
<SwapDetails
item={
makeLendingDepositItem({
direction: 'out',
amount: '10000',
assetId: USDT_ASSET_ID,
}) as never
}
/>,
);

expect(capturedSentToken?.decimals).toBe(6);
expect(capturedSentToken?.symbol).toBe('USDT');
expect(capturedSentToken?.amount).toBe('10000');
});

it('leaves an already-populated token unchanged (no-op when decimals are present)', () => {
render(
<SwapDetails
item={
makeLendingDepositItem({
direction: 'out',
amount: '10000',
assetId: USDT_ASSET_ID,
decimals: 6,
symbol: 'USDT',
}) as never
}
/>,
);

expect(capturedSentToken?.decimals).toBe(6);
});
});
18 changes: 15 additions & 3 deletions app/components/Views/ActivityDetails/templates/SwapDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';
import { Box, SectionDivider } from '@metamask/design-system-react-native';
import type { ActivityListItem } from '../../../../util/activity-adapters';
import {
type ActivityListItem,
enrichTokenFromApi,
} from '../../../../util/activity-adapters';
import { useTokensData } from '../../../hooks/useTokensData/useTokensData';
import {
ActivityDetailsBlockExplorerButton,
ActivityDetailsDoItAgainButton,
Expand Down Expand Up @@ -30,9 +34,17 @@ type SwapDetailsItem = Extract<
>;

export function SwapDetails({ item }: { item: SwapDetailsItem }) {
const { sourceToken } = item.data;
const destinationToken =
const rawSourceToken = item.data.sourceToken;
const rawDestinationToken =
'destinationToken' in item.data ? item.data.destinationToken : undefined;

const tokenData = useTokensData(
[rawSourceToken?.assetId, rawDestinationToken?.assetId].filter(
(assetId): assetId is string => Boolean(assetId),
),
);
const sourceToken = enrichTokenFromApi(rawSourceToken, tokenData);
const destinationToken = enrichTokenFromApi(rawDestinationToken, tokenData);
const totalToken = sourceToken?.amount ? sourceToken : destinationToken;
const handleDoItAgain = useActivityDetailsDoItAgain({
sourceToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useGaslessSupportedSmartTransactions } from '../gas/useGaslessSupported
import { cloneDeep } from 'lodash';
import { useTransactionPayQuotes } from '../pay/useTransactionPayData';
import { useMusdConfirmNavigation } from '../../../../UI/Earn/hooks/useMusdConfirmNavigation';
import { navigateToActivityAfterConfirmation } from '../../../../../util/navigation/navigateToActivityAfterConfirmation';
import { useFiatConfirm } from '../pay/useFiatConfirm';
import { useHandleHwSend } from '../../../../UI/HardwareWallet/Swaps/useHandleHwSend';

Expand Down Expand Up @@ -200,7 +201,7 @@ export function useTransactionConfirm() {
isFullScreenConfirmation &&
!hasTransactionType(transactionMetadata, GO_BACK_TYPES)
) {
navigation.navigate(Routes.TRANSACTIONS_VIEW);
navigateToActivityAfterConfirmation(navigation);
} else {
navigation.goBack();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TransactionType } from '@metamask/transaction-controller';

import PPOMUtil from '../../../../lib/ppom/ppom-util';
import Routes from '../../../../constants/navigation/Routes';
import { navigateToActivityAfterConfirmation } from '../../../../util/navigation/navigateToActivityAfterConfirmation';
import { MetaMetricsEvents } from '../../../../core/Analytics';

import { isSignatureRequest } from '../utils/confirm';
Expand Down Expand Up @@ -76,7 +77,7 @@ export const useConfirmActions = () => {
});

if (approvalType === ApprovalType.TransactionBatch) {
navigation.navigate(Routes.TRANSACTIONS_VIEW);
navigateToActivityAfterConfirmation(navigation);
} else {
navigation.goBack();
}
Expand Down
Loading
Loading