Skip to content

Commit ff2a6df

Browse files
authored
fix: raw token amounts shown in asset details activity list (#33211)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> <!-- mms-check directive vocabulary — read by .github/scripts/shared/pr-template-checks.ts at module load to build the validation plan. Directives are invisible in rendered markdown and must NOT be removed or edited without updating the validator registry. type=text Section must contain non-placeholder prose. type=changelog Section must have a valid CHANGELOG entry: line. type=issue-link Section must have a Fixes:/Closes:/Refs: line with a value. type=manual-testing Section must have real testing steps or an explicit N/A. type=screenshot Section must have evidence (image/URL) or an explicit N/A. type=checklist Section must have all checkboxes consciously checked. required=true|false Whether a missing/invalid section runs the validator at all. blocking=true|false Whether a failure of this check fails the CI workflow. Default: false — failures are shown as warnings in the sticky comment but do not block the PR. Sections without a directive are checked for structural presence only. --> ## **Description** <!-- mms-check: type=text required=true --> <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ### Context Token amounts on the asset details activity list rendered in raw base units (e.g. `500 USDG` instead of `0.0005 USDG`) for any token absent from the static registries (`STATIC_MAINNET_TOKEN_LIST`, `BRIDGE_CHAINID_COMMON_TOKEN_PAIR`). The main activity tab was unaffected: `useLocalActivityItems` enriches groups with `contractTokenMetadata` from TokensController, but the asset details path called `mapLocalTransaction` bare. It also passed the asset's symbol as `nativeAssetSymbol`, mislabeling native/fee legs. ### Changes - `AssetDetailsActivityListItem`: resolve `symbol`/`decimals` from TokensController for the tx target contract (same pattern as the activity tab), and pass the chain's actual native currency. - `mapTransactionToActivityItem`: new optional params feed `contractTokenMetadata`, gated on `txParams.to === assetAddress`. Legacy calls are byte-identical (`nativeAssetSymbol ?? assetSymbol` fallback). ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: fix raw token amounts shown in asset details activity list ## **Related issues** <!-- mms-check: type=issue-link required=true --> Fixes: https://consensyssoftware.atlassian.net/browse/WPN-1589 ## **Manual testing steps** - Send some token that don't have 18 decimals, such as USDG on Robinhood Chain or USDC on Polygon -> Then Activity row from the Token Page should display with correct decimal places. - Send some token with 18 decimals as well as some native tokens (ex: ETH on Base) to verify there is no regression with other tokens. - Check the "network activity tab" as well for each case to check that no regression is introduced there either. <!-- mms-check: type=manual-testing required=true --> ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> <img width="356" alt="image" src="https://github.com/user-attachments/assets/1ef3bed4-9e97-4fca-9d2e-f37925cfdc27" /> ### **After** <!-- [screenshots/recordings] --> <img width="356" alt="image" src="https://github.com/user-attachments/assets/40d31759-eb34-48c4-8922-39e4f58cd820" /> ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Localized UI mapping and display logic with backward-compatible optional parameters and broad unit test coverage; no auth, signing, or persistence changes. > > **Overview** > Fixes **raw base-unit token amounts** (e.g. `500 USDG` instead of `0.0005 USDG`) on the asset details activity list for tokens not in static registries, by aligning enrichment with the main activity tab. > > **`AssetDetailsActivityListItem`** now reads **TokensController** (`selectAllTokens`), the **selected account group EVM account**, and **network native currency** before mapping each row. It resolves symbol/decimals for the transaction’s `txParams.to` contract and passes the chain’s real native symbol instead of reusing the page asset symbol for fees. > > **`mapTransactionToActivityItem`** accepts optional `assetDecimals`, `assetAddress`, and `nativeAssetSymbol`. It sets **`contractTokenMetadata`** only when `txParams.to` matches `assetAddress` (transfers/approves), avoiding wrong decimals on router/swap txs. Callers without the new args keep prior behavior via `nativeAssetSymbol ?? assetSymbol`. > > Tests cover metadata gating, case-insensitive address matching, native symbol resolution, and updated component selector mocks. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3d1815f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 4290439 commit ff2a6df

4 files changed

Lines changed: 302 additions & 11 deletions

File tree

app/components/UI/Transactions/AssetDetailsActivityListItem.test.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import AssetDetailsActivityListItem from './AssetDetailsActivityListItem';
1010
import Routes from '../../../constants/navigation/Routes';
1111
import { selectSelectedInternalAccount } from '../../../selectors/accountsController';
1212
import { selectIsTransactionsRedesignEnabled } from '../../../selectors/featureFlagController/activityRedesign';
13+
import { selectSelectedAccountGroupEvmInternalAccount } from '../../../selectors/multichainAccounts/accountTreeController';
14+
import { selectEvmNetworkConfigurationsByChainId } from '../../../selectors/networkController';
15+
import { selectAllTokens } from '../../../selectors/tokensController';
1316
import type { TransactionWithImportTime } from './AssetDetailsActivityListItem.utils';
1417
import { resolveActivityListItemTitle } from '../ActivityListItemRow/ActivityListItemRow';
1518

@@ -70,10 +73,38 @@ const createTransaction = (
7073
describe('AssetDetailsActivityListItem', () => {
7174
beforeEach(() => {
7275
jest.clearAllMocks();
76+
7377
mockUseSelector.mockImplementation((selector) => {
7478
if (selector === selectSelectedInternalAccount) {
7579
return { metadata: { importTime: 2000 } };
7680
}
81+
82+
if (selector === selectSelectedAccountGroupEvmInternalAccount) {
83+
return { address: '0x123' };
84+
}
85+
86+
if (selector === selectEvmNetworkConfigurationsByChainId) {
87+
return {
88+
'0x1': {
89+
nativeCurrency: 'ETH',
90+
},
91+
};
92+
}
93+
94+
if (selector === selectAllTokens) {
95+
return {
96+
'0x1': {
97+
'0x123': [
98+
{
99+
address: '0x456',
100+
symbol: 'USDC',
101+
decimals: 6,
102+
},
103+
],
104+
},
105+
};
106+
}
107+
77108
return undefined;
78109
});
79110
});
@@ -109,8 +140,26 @@ describe('AssetDetailsActivityListItem', () => {
109140
if (selector === selectSelectedInternalAccount) {
110141
return { metadata: { importTime: null } };
111142
}
143+
144+
if (selector === selectSelectedAccountGroupEvmInternalAccount) {
145+
return { address: '0x123' };
146+
}
147+
148+
if (selector === selectEvmNetworkConfigurationsByChainId) {
149+
return {
150+
'0x1': {
151+
nativeCurrency: 'ETH',
152+
},
153+
};
154+
}
155+
156+
if (selector === selectAllTokens) {
157+
return {};
158+
}
159+
112160
return undefined;
113161
});
162+
114163
const navigation = createNavigation();
115164
const transaction = createTransaction({ insertImportTime: true });
116165

@@ -136,11 +185,30 @@ describe('AssetDetailsActivityListItem', () => {
136185
if (selector === selectSelectedInternalAccount) {
137186
return { metadata: { importTime: 2000 } };
138187
}
188+
189+
if (selector === selectSelectedAccountGroupEvmInternalAccount) {
190+
return { address: '0x123' };
191+
}
192+
193+
if (selector === selectEvmNetworkConfigurationsByChainId) {
194+
return {
195+
'0x1': {
196+
nativeCurrency: 'ETH',
197+
},
198+
};
199+
}
200+
201+
if (selector === selectAllTokens) {
202+
return {};
203+
}
204+
139205
if (selector === selectIsTransactionsRedesignEnabled) {
140206
return true;
141207
}
208+
142209
return undefined;
143210
});
211+
144212
const navigation = createNavigation();
145213
const transaction = createTransaction();
146214

@@ -207,4 +275,21 @@ describe('AssetDetailsActivityListItem', () => {
207275
}),
208276
);
209277
});
278+
279+
it('renders successfully when token metadata is resolved from the selected account group EVM account', () => {
280+
const navigation = createNavigation();
281+
expect(() =>
282+
render(
283+
<AssetDetailsActivityListItem
284+
transaction={createTransaction()}
285+
index={0}
286+
assetSymbol="ETH"
287+
chainId="0x1"
288+
navigation={navigation}
289+
onSpeedUpAction={jest.fn()}
290+
onCancelAction={jest.fn()}
291+
/>,
292+
),
293+
).not.toThrow();
294+
});
210295
});

app/components/UI/Transactions/AssetDetailsActivityListItem.tsx

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import {
2222
mapTransactionToActivityItem,
2323
type TransactionWithImportTime,
2424
} from './AssetDetailsActivityListItem.utils';
25+
import { selectEvmNetworkConfigurationsByChainId } from '../../../selectors/networkController';
26+
import { selectAllTokens } from '../../../selectors/tokensController';
27+
import { selectSelectedAccountGroupEvmInternalAccount } from '../../../selectors/multichainAccounts/accountTreeController';
2528

2629
interface AssetDetailsActivityListItemProps {
2730
transaction: TransactionWithImportTime;
@@ -44,21 +47,63 @@ export const AssetDetailsActivityListItem = ({
4447
onSpeedUpAction,
4548
onCancelAction,
4649
}: AssetDetailsActivityListItemProps) => {
50+
// Kept for importTime metadata
4751
const selectedInternalAccount = useSelector(selectSelectedInternalAccount);
52+
// Used for TokensController lookups (matches useLocalActivityItems)
53+
const groupEvmAccount = useSelector(
54+
selectSelectedAccountGroupEvmInternalAccount,
55+
);
4856
const isTransactionsRedesignEnabled = useSelector(
4957
selectIsTransactionsRedesignEnabled,
5058
);
51-
const accountImportTime = selectedInternalAccount?.metadata.importTime;
52-
const activityItem = useMemo(
53-
() =>
54-
mapTransactionToActivityItem({
55-
transaction: tx,
56-
assetSymbol,
57-
currentChainId,
58-
tokenChainId,
59-
}),
60-
[assetSymbol, currentChainId, tokenChainId, tx],
59+
60+
const networkConfigurations = useSelector(
61+
selectEvmNetworkConfigurationsByChainId,
6162
);
63+
// allTokens: Record<chainId, Record<accountAddress, Token[]>>
64+
const allTokens = useSelector(selectAllTokens) as unknown as Record<
65+
string,
66+
Record<string, { address: string; symbol?: string; decimals?: number }[]>
67+
>;
68+
const accountImportTime = selectedInternalAccount?.metadata.importTime;
69+
const activityItem = useMemo(() => {
70+
const resolvedChainId = (tx.chainId ?? tokenChainId ?? currentChainId) as
71+
| Hex
72+
| undefined;
73+
74+
const nativeAssetSymbol = resolvedChainId
75+
? networkConfigurations?.[resolvedChainId]?.nativeCurrency
76+
: undefined;
77+
78+
// Token metadata for the tx's target contract, from TokensController —
79+
// same enrichment as useLocalActivityItems.
80+
const accountAddress = groupEvmAccount?.address?.toLowerCase();
81+
const contractAddress = tx.txParams?.to?.toLowerCase();
82+
const matchingToken =
83+
resolvedChainId && accountAddress && contractAddress
84+
? (allTokens[resolvedChainId]?.[accountAddress] ?? []).find(
85+
(t) => t.address?.toLowerCase() === contractAddress,
86+
)
87+
: undefined;
88+
89+
return mapTransactionToActivityItem({
90+
transaction: tx,
91+
assetSymbol: matchingToken?.symbol ?? assetSymbol,
92+
assetDecimals: matchingToken?.decimals,
93+
assetAddress: matchingToken ? contractAddress : undefined,
94+
nativeAssetSymbol,
95+
currentChainId,
96+
tokenChainId,
97+
});
98+
}, [
99+
allTokens,
100+
assetSymbol,
101+
currentChainId,
102+
groupEvmAccount?.address,
103+
networkConfigurations,
104+
tokenChainId,
105+
tx,
106+
]);
62107

63108
const handlePress = useCallback(
64109
(item: ActivityListItem) => {

app/components/UI/Transactions/AssetDetailsActivityListItem.utils.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,140 @@ describe('AssetDetailsActivityListItem utils', () => {
9595
showCancelModal,
9696
});
9797
});
98+
99+
describe('contractTokenMetadata enrichment', () => {
100+
const USDG_ADDRESS = '0x5fc5360d0400a0fd4f2af552add042d716f1d168';
101+
const USDG_ADDRESS_CHECKSUMMED =
102+
'0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168';
103+
104+
const createTokenTransfer = (
105+
overrides: Partial<TransactionWithImportTime> = {},
106+
) =>
107+
createTransaction({
108+
type: TransactionType.tokenMethodTransfer,
109+
txParams: {
110+
from: '0x123',
111+
to: USDG_ADDRESS,
112+
value: '0x0',
113+
data: '0xa9059cbb0000000000000000000000000000000000000000000000000000000000000456',
114+
},
115+
...overrides,
116+
});
117+
118+
const expectLocalTransaction = (
119+
item: ReturnType<typeof mapTransactionToActivityItem>,
120+
) => {
121+
if (item.raw?.type !== 'localTransaction') {
122+
throw new Error('Expected local transaction activity item');
123+
}
124+
return item.raw.data;
125+
};
126+
127+
it('attaches contractTokenMetadata when the tx targets the asset contract', () => {
128+
const item = mapTransactionToActivityItem({
129+
transaction: createTokenTransfer(),
130+
assetSymbol: 'USDG',
131+
assetDecimals: 6,
132+
assetAddress: USDG_ADDRESS,
133+
nativeAssetSymbol: 'ETH',
134+
currentChainId: '0x1237',
135+
});
136+
137+
const data = expectLocalTransaction(item);
138+
expect(data.contractTokenMetadata).toStrictEqual({
139+
symbol: 'USDG',
140+
decimals: 6,
141+
});
142+
});
143+
144+
it('matches assetAddress case-insensitively (checksummed vs lowercase)', () => {
145+
const item = mapTransactionToActivityItem({
146+
transaction: createTokenTransfer(),
147+
assetSymbol: 'USDG',
148+
assetDecimals: 6,
149+
assetAddress: USDG_ADDRESS_CHECKSUMMED,
150+
nativeAssetSymbol: 'ETH',
151+
currentChainId: '0x1237',
152+
});
153+
154+
expect(expectLocalTransaction(item).contractTokenMetadata).toStrictEqual({
155+
symbol: 'USDG',
156+
decimals: 6,
157+
});
158+
});
159+
160+
it('does not attach contractTokenMetadata when the tx targets another contract', () => {
161+
const item = mapTransactionToActivityItem({
162+
// to: 0x456 — e.g. a router/swap call on the asset page
163+
transaction: createTransaction(),
164+
assetSymbol: 'USDG',
165+
assetDecimals: 6,
166+
assetAddress: USDG_ADDRESS,
167+
nativeAssetSymbol: 'ETH',
168+
currentChainId: '0x1237',
169+
});
170+
171+
expect(
172+
expectLocalTransaction(item).contractTokenMetadata,
173+
).toBeUndefined();
174+
});
175+
176+
it('does not attach contractTokenMetadata when assetAddress is not provided (legacy call)', () => {
177+
const item = mapTransactionToActivityItem({
178+
transaction: createTokenTransfer(),
179+
assetSymbol: 'USDG',
180+
currentChainId: '0x1237',
181+
});
182+
183+
expect(
184+
expectLocalTransaction(item).contractTokenMetadata,
185+
).toBeUndefined();
186+
});
187+
188+
it('does not attach contractTokenMetadata when txParams.to is undefined (contract deployment)', () => {
189+
const item = mapTransactionToActivityItem({
190+
transaction: createTransaction({
191+
txParams: { from: '0x123', value: '0x0' },
192+
}),
193+
assetSymbol: 'USDG',
194+
assetDecimals: 6,
195+
assetAddress: USDG_ADDRESS,
196+
nativeAssetSymbol: 'ETH',
197+
currentChainId: '0x1237',
198+
});
199+
200+
expect(
201+
expectLocalTransaction(item).contractTokenMetadata,
202+
).toBeUndefined();
203+
});
204+
});
205+
206+
describe('nativeAssetSymbol resolution', () => {
207+
it('uses the explicit nativeAssetSymbol when provided', () => {
208+
const item = mapTransactionToActivityItem({
209+
transaction: createTransaction(),
210+
assetSymbol: 'USDG',
211+
nativeAssetSymbol: 'ETH',
212+
currentChainId: '0x1237',
213+
});
214+
215+
if (item.raw?.type !== 'localTransaction') {
216+
throw new Error('Expected local transaction activity item');
217+
}
218+
expect(item.raw.data.nativeAssetSymbol).toBe('ETH');
219+
});
220+
221+
it('falls back to assetSymbol when nativeAssetSymbol is absent (legacy behavior)', () => {
222+
const item = mapTransactionToActivityItem({
223+
transaction: createTransaction(),
224+
assetSymbol: 'USDG',
225+
currentChainId: '0x1237',
226+
});
227+
228+
if (item.raw?.type !== 'localTransaction') {
229+
throw new Error('Expected local transaction activity item');
230+
}
231+
expect(item.raw.data.nativeAssetSymbol).toBe('USDG');
232+
});
233+
});
98234
});

0 commit comments

Comments
 (0)