Skip to content

Commit 1123796

Browse files
authored
fix: Format addresses in address list (#23703)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** 1. What is the reason for the change? - EVM addresses should always be checksummed. Non evm addresses should not. - Our addresses were formatted correctly on the list view but not in the QR modal - This fix ensures that all the addresses being rendered on that screen have the proper formatting 2. What is the improvement/solution? - call toFormattedAddress before passing it as a prop. ## **Changelog** <!-- 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: Fixed bug where the EVM addresses were not checksummed ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MUL-1327 ## **Manual testing steps** 1. create or import a wallet 2. con the home page click the receive button 3. it should open the address list 4. then, look at your EVM addresses and ensure that the shortened versions are checksummed 5. copy the address and ensure that the address is checksummed 6. click on the QR modal and ensure that the address is checksummed 7. scan the qr code and verify that the address is checksummed 8. look at your Bitcoin address, it should not be checksummed. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <img width="250" height="600" alt="Simulator Screenshot - iPhone 16 Plus - 2025-12-04 at 16 42 28" src="https://github.com/user-attachments/assets/33e47a72-9f11-42a3-9efd-3a3fea271953" /> <img width="250" height="600" alt="Simulator Screenshot - iPhone 16 Plus - 2025-12-04 at 16 42 12" src="https://github.com/user-attachments/assets/c9162dfe-b619-44e4-9fa4-8dd86f935e70" /> ### **After** <img width="250" height="600" alt="Simulator Screenshot - iPhone 16 Plus - 2025-12-04 at 16 33 52" src="https://github.com/user-attachments/assets/a2feb119-0a07-4010-a220-95b70bcbaf5c" /> <img width="250" height="600" alt="Simulator Screenshot - iPhone 16 Plus - 2025-12-04 at 16 33 48" src="https://github.com/user-attachments/assets/4ea253ae-9953-4f77-a623-7ec739aa286d" /> ## **Pre-merge author checklist** - [ ] 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. ## **Pre-merge reviewer checklist** - [ ] 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] > Checksums EVM addresses (leaves non‑EVM raw) across address list/QR and selectors, and switches PrivateKeyList to store private keys by account id; adds tests. > > - **Address formatting** > - Use `toFormattedAddress` in `MultichainAddressRowsList.utils` when creating `NetworkAddressItem`. > - Format addresses in selector `selectInternalAccountListSpreadByScopesByGroupId` before mapping items. > - Pass formatted address to QR navigation in `AddressList.test.tsx`. > - **Private keys** > - Store exported private keys by `account.id` instead of `account.address` in `PrivateKeyList.tsx` and update copy callback usage. > - **Tests** > - Add assertions for checksummed EVM addresses and unmodified non‑EVM addresses in `MultichainAddressRowsList.utils.test.ts`. > - Update QR expectation to use formatted address in `AddressList.test.tsx`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c35634f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 15146aa commit 1123796

File tree

5 files changed

+33
-6
lines changed

5 files changed

+33
-6
lines changed

app/component-library/components-temp/MultichainAccounts/MultichainAddressRowsList/MultichainAddressRowsList.utils.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,5 +242,26 @@ describe('MultichainAddressRowsList Utils', () => {
242242

243243
expect(result[0].address).toBe(testAddress);
244244
});
245+
246+
it('converts lowercase EVM address to checksummed format', () => {
247+
const lowercaseEvmAddress = '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272';
248+
const expectedChecksummed = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272';
249+
const account = createMockAccount(lowercaseEvmAddress, ['eip155:0x1']);
250+
251+
const result = getCompatibleNetworksForAccount(account, mockNetworks);
252+
253+
expect(result[0].address).toBe(expectedChecksummed);
254+
});
255+
256+
it('returns unmodified address for non-EVM networks', () => {
257+
const solanaAddress = 'DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy';
258+
const account = createMockAccount(solanaAddress, [
259+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
260+
]);
261+
262+
const result = getCompatibleNetworksForAccount(account, mockNetworks);
263+
264+
expect(result[0].address).toBe(solanaAddress);
265+
});
245266
});
246267
});

app/component-library/components-temp/MultichainAccounts/MultichainAddressRowsList/MultichainAddressRowsList.utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { SolScope, BtcScope, TrxScope } from '@metamask/keyring-api';
44
import { CaipChainId } from '@metamask/utils';
55
import { TEST_NETWORK_IDS } from '../../../../constants/network';
66
import { PopularList } from '../../../../util/networks/customNetworks';
7+
import { toFormattedAddress } from '../../../../util/address';
78

89
export interface NetworkAddressItem {
910
chainId: CaipChainId;
@@ -89,13 +90,14 @@ export const sortNetworkAddressItems = (
8990

9091
/**
9192
* Creates a NetworkAddressItem from chain ID, network config, and address
93+
* Ensures addresses are properly formatted: checksummed for EVM, raw for non-EVM
9294
*
9395
* @param chainId - Chain ID
9496
* @param network - Network configuration
9597
* @param network.name - Network name
9698
* @param network.chainId - Network chain ID
9799
* @param address - Address to associate with the network
98-
* @returns NetworkAddressItem object
100+
* @returns NetworkAddressItem object with properly formatted address
99101
*/
100102
const createNetworkAddressItem = (
101103
chainId: CaipChainId,
@@ -104,7 +106,7 @@ const createNetworkAddressItem = (
104106
): NetworkAddressItem => ({
105107
chainId,
106108
networkName: network.name,
107-
address,
109+
address: toFormattedAddress(address),
108110
});
109111

110112
/**

app/components/Views/MultichainAccounts/AddressList/AddressList.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import renderWithProvider from '../../../../util/test/renderWithProvider';
88

99
import { AddressList } from './AddressList';
1010
import { MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID } from '../../../../component-library/components-temp/MultichainAccounts/MultichainAddressRow';
11+
import { toFormattedAddress } from '../../../../util/address';
1112

1213
const ACCOUNT_WALLET_ID = 'entropy:wallet-id-1' as AccountWalletId;
1314
const ACCOUNT_GROUP_ID = 'entropy:wallet-id-1/1' as AccountGroupId;
@@ -208,7 +209,7 @@ describe('AddressList', () => {
208209
{
209210
screen: 'ShareAddressQR',
210211
params: {
211-
address: mockEthEoaAccount.address,
212+
address: toFormattedAddress(mockEthEoaAccount.address),
212213
networkName: 'Ethereum',
213214
chainId: 'eip155:1',
214215
groupId: ACCOUNT_GROUP_ID,

app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export const PrivateKeyList = () => {
139139
password,
140140
account.address,
141141
);
142-
privateKeyMap[account.address] = pk;
142+
privateKeyMap[account.id] = pk;
143143
}),
144144
);
145145

@@ -188,7 +188,7 @@ export const PrivateKeyList = () => {
188188
),
189189
callback: async () => {
190190
await ClipboardManager.setStringExpire(
191-
privateKeys[item.account.address],
191+
privateKeys[item.account.id],
192192
);
193193
},
194194
}}

app/selectors/multichainAccounts/accounts.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { TEST_NETWORK_IDS } from '../../constants/network';
2323
import type { AccountGroupWithInternalAccounts } from './accounts.type';
2424
import { sortNetworkAddressItems } from '../../component-library/components-temp/MultichainAccounts/MultichainAddressRowsList/MultichainAddressRowsList.utils';
25+
import { toFormattedAddress } from '../../util/address';
2526

2627
/**
2728
* Extracts the wallet ID from an account group ID.
@@ -289,12 +290,14 @@ export const selectInternalAccountListSpreadByScopesByGroupId =
289290
account.type === EthAccountType.Eoa
290291
? ethereumNetworkIds
291292
: account.scopes || [];
293+
// Format address once: checksummed for EVM, raw for non-EVM
294+
const formattedAddress = toFormattedAddress(account.address);
292295
// Filter out testnets from scopes and map each scope to an account-scope object
293296
return filterTestnets(
294297
scopes as CaipChainId[],
295298
networkConfigurations,
296299
).map((scope: CaipChainId) => ({
297-
account,
300+
account: { ...account, address: formattedAddress },
298301
scope,
299302
networkName:
300303
networkConfigurations[scope]?.name || 'Unknown Network',

0 commit comments

Comments
 (0)