Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -371,6 +371,26 @@
};
}

function enrichTokenFromApi(
token: TokenAmount | undefined,
dataByAssetId: Record<string, { symbol?: string; decimals?: number }>,
): TokenAmount | undefined {
if (!token?.assetId) {
return token;
}
const listToken = dataByAssetId[token.assetId.toLowerCase()];
if (!listToken) {
return token;
}
const symbol = token.symbol ?? listToken.symbol;
const decimals = token.decimals ?? listToken.decimals;
return {
...token,
...(symbol ? { symbol } : {}),
...(decimals === undefined ? {} : { decimals }),
};
}

function areSameDisplayToken(
sourceToken: TokenAmount | undefined,
destinationToken: TokenAmount | undefined,
Expand Down Expand Up @@ -1131,17 +1151,37 @@
)
: 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);
const primaryToken = enrichStablecoinTokenMetadata(
isSpendingCap
? spendingCapToken?.amount
? spendingCapToken
: undefined
: content.primaryToken,
: isLending
? enrichTokenFromApi(content.primaryToken, lendingTokenData)
: content.primaryToken,

Check warning on line 1178 in app/components/UI/ActivityListItemRow/useActivityListItemRowContent.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-mobile&issues=AZ9OccPVCQbpmkJVObFq&open=AZ9OccPVCQbpmkJVObFq&pullRequest=33157
networkChainId,
);
const secondaryToken = enrichStablecoinTokenMetadata(
content.secondaryToken,
isLending
? enrichTokenFromApi(content.secondaryToken, lendingTokenData)
: content.secondaryToken,
networkChainId,
);
const isPerpsFunding = isPerpsFundingKind(item.type);
Expand Down Expand Up @@ -1210,7 +1250,9 @@
avatarTokens:
isSpendingCap && spendingCapToken
? [spendingCapToken]
: resolveAvatarTokens(item, bridgeHistoryItem),
: isLending && primaryToken
? [primaryToken]
: resolveAvatarTokens(item, bridgeHistoryItem),

Check warning on line 1255 in app/components/UI/ActivityListItemRow/useActivityListItemRowContent.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-mobile&issues=AZ9OccPVCQbpmkJVObFr&open=AZ9OccPVCQbpmkJVObFr&pullRequest=33157
avatarIconUrl: predictIconUrl,
perpsMarketSymbol,
primaryToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import { ScrollView, View } from 'react-native';
import { useSelector } from 'react-redux';
import { strings } from '../../../../../../locales/i18n';
import Routes from '../../../../../constants/navigation/Routes';

Check warning on line 13 in app/components/UI/Earn/Views/EarnLendingDepositConfirmationView/index.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'Routes'.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-mobile&issues=AZ9Ocb_TCQbpmkJVObFo&open=AZ9Ocb_TCQbpmkJVObFo&pullRequest=33157
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 +416,7 @@
});
// 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,8 @@
import Text, {
TextVariant,
} from '../../../../../component-library/components/Texts/Text';
import Routes from '../../../../../constants/navigation/Routes';

Check warning on line 23 in app/components/UI/Earn/Views/EarnLendingWithdrawalConfirmationView/index.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'Routes'.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-mobile&issues=AZ9OccO2CQbpmkJVObFp&open=AZ9OccO2CQbpmkJVObFp&pullRequest=33157
import { navigateToActivityAfterConfirmation } from '../../../../../util/navigation/navigateToActivityAfterConfirmation';
import {
IMetaMetricsEvent,
MetaMetricsEvents,
Expand Down Expand Up @@ -316,7 +317,7 @@
});
// 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
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
140 changes: 140 additions & 0 deletions app/util/activity-adapters/adapters/local-transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,146 @@ describe('mapLocalTransaction', () => {
});
});

it('maps a typed lendingDeposit to the underlying token from simulation data', () => {
// Mobile tags lending deposits with TransactionType.lendingDeposit, whose tx
// `to` is the pool — the deposited token must come from the balance change,
// not the pool address (which resolves no symbol/icon).
const transaction = {
chainId: base,
id: 'typed-lending-deposit-id',
hash: '0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809',
status: TransactionStatus.confirmed,
time: 1779892154611,
type: TransactionType.lendingDeposit,
txParams: {
from,
to: baseAavePool,
value: '0x0',
},
simulationData: {
tokenBalanceChanges: [
{
address: baseUsdc,
difference: '0x186a0',
isDecrease: true,
standard: 'erc20',
},
],
},
} as unknown as Partial<TransactionMeta>;

expect(
withoutRaw(mapLocalTransaction(makeGroup(transaction))),
).toStrictEqual({
type: 'lendingDeposit',
chainId: 'eip155:8453',
status: 'success',
timestamp: 1779892154611,
hash: '0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809',
data: {
sourceToken: {
amount: '100000',
assetId: toAssetId(baseUsdc, 'eip155:8453'),
decimals: 6,
direction: 'out',
symbol: 'USDC',
},
},
});
});

it('maps a typed lendingDeposit to the underlying token from the outgoing transfer log when no simulation data', () => {
const transaction = {
chainId: base,
hash: '0x2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809a1',
status: TransactionStatus.confirmed,
time: 1779892154611,
type: TransactionType.lendingDeposit,
txParams: {
from,
to: baseAavePool,
value: '0x0',
},
txReceipt: {
logs: [
// Transfer FROM the user (topics[1]) TO the pool (topics[2]) — the
// deposited token.
{
address: baseUsdc,
data: '0x00000000000000000000000000000000000000000000000000000000000186a0',
topics: [
erc20TransferTopic,
addressTopic(from),
addressTopic(baseAavePool),
],
},
],
},
} as unknown as Partial<TransactionMeta>;

expect(
withoutRaw(mapLocalTransaction(makeGroup(transaction))),
).toStrictEqual({
type: 'lendingDeposit',
chainId: 'eip155:8453',
status: 'success',
timestamp: 1779892154611,
hash: '0x2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809a1',
data: {
sourceToken: {
amount: '100000',
assetId: toAssetId(baseUsdc, 'eip155:8453'),
decimals: 6,
direction: 'out',
symbol: 'USDC',
},
},
});
});

it('maps a typed lendingDeposit with neither simulation data nor a matching transfer log to the pool-address token (prior behavior)', () => {
const otherSender =
'0x0000000000000000000000001111111111111111111111111111111111111111';
const transaction = {
chainId: base,
hash: '0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809a1b2',
status: TransactionStatus.confirmed,
time: 1779892154611,
type: TransactionType.lendingDeposit,
txParams: {
from,
to: baseAavePool,
value: '0x0',
},
txReceipt: {
logs: [
// A Transfer whose sender isn't the user — not the deposited token.
{
address: baseUsdc,
data: '0x00000000000000000000000000000000000000000000000000000000000186a0',
topics: [erc20TransferTopic, otherSender, addressTopic(from)],
},
],
},
} as unknown as Partial<TransactionMeta>;

expect(
withoutRaw(mapLocalTransaction(makeGroup(transaction))),
).toStrictEqual({
type: 'lendingDeposit',
chainId: 'eip155:8453',
status: 'success',
timestamp: 1779892154611,
hash: '0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809a1b2',
data: {
sourceToken: {
assetId: toAssetId(baseAavePool, 'eip155:8453'),
direction: 'out',
},
},
});
});

it('maps a withdraw contract interaction from the received token transfer', () => {
const transaction = {
chainId: base,
Expand Down
Loading
Loading