Skip to content

Commit 47ee978

Browse files
authored
fix: fix block explorer redirection cp-7.74.0 (#28966)
## **Description** Fixes the block explorer URL generation for Solana SPL tokens in the Security Trust screen. The params.address contains a full CAIP asset type ID (e.g., solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB) but the block explorer URL template expects just the raw token address. This change extracts the assetReference from the CAIP asset type before passing it to getBlockExplorerTokenUrl. ## **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: Fix block explorer redirection for solana. ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3076?focusedCommentId=412599 ## **Manual testing steps** ```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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/a84d3264-4545-444f-961d-a3bf8e10a5a8 ## **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. #### 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** - [ ] 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** > Low risk: a small, localized change to external link URL generation plus targeted unit tests; main risk is incorrect CAIP parsing leading to broken explorer links for some non-EVM assets. > > **Overview** > Fixes block explorer redirection from `SecurityTrustScreen` by detecting CAIP asset type addresses and passing only the parsed `assetReference` (e.g., Solana mint) into `getBlockExplorerTokenUrl` instead of the full CAIP identifier. > > Updates `SecurityTrustScreen.test.tsx` to make route params configurable and adds assertions that EVM addresses are passed through unchanged while Solana CAIP addresses are correctly parsed before URL generation (with `useBlockExplorer` now mocked to verify calls). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a5464e1. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent cc6197a commit 47ee978

2 files changed

Lines changed: 109 additions & 68 deletions

File tree

app/components/UI/SecurityTrust/Views/SecurityTrustScreen.test.tsx

Lines changed: 104 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,68 @@ import { strings } from '../../../../../locales/i18n';
77
const mockNavigate = jest.fn();
88
const mockGoBack = jest.fn();
99

10+
const createDefaultRouteParams = () => ({
11+
address: '0x1234567890abcdef',
12+
chainId: '0x1',
13+
symbol: 'TEST',
14+
decimals: 18,
15+
name: 'Test Token',
16+
isNative: false,
17+
securityData: {
18+
resultType: 'Verified',
19+
maliciousScore: '0',
20+
features: [
21+
{
22+
featureId: 'VERIFIED_CONTRACT',
23+
type: 'Info',
24+
description: 'Contract is verified',
25+
},
26+
{
27+
featureId: 'HIGH_REPUTATION_TOKEN',
28+
type: 'Benign',
29+
description: 'Token has high reputation',
30+
},
31+
],
32+
fees: {
33+
buy: 1,
34+
sell: 2,
35+
transfer: 0,
36+
transferFeeMaxAmount: null,
37+
},
38+
financialStats: {
39+
supply: 1000000000000000000000000,
40+
holdersCount: 5000,
41+
topHolders: [
42+
{
43+
label: 'Holder 1',
44+
name: null,
45+
address: '0xholder1',
46+
holdingPercentage: 15,
47+
},
48+
{
49+
label: 'Holder 2',
50+
name: null,
51+
address: '0xholder2',
52+
holdingPercentage: 10,
53+
},
54+
],
55+
tradeVolume24h: 1000000,
56+
lockedLiquidityPct: 80,
57+
markets: [],
58+
},
59+
metadata: {
60+
externalLinks: {
61+
homepage: 'https://example.com',
62+
twitterPage: 'testtoken',
63+
telegramChannelId: 'testtoken',
64+
},
65+
},
66+
created: '2023-01-01T00:00:00Z',
67+
},
68+
});
69+
70+
let mockRouteParams = createDefaultRouteParams();
71+
1072
jest.mock('react-native', () => {
1173
const actual = jest.requireActual('react-native');
1274
return {
@@ -25,65 +87,7 @@ jest.mock('@react-navigation/native', () => ({
2587
goBack: mockGoBack,
2688
}),
2789
useRoute: () => ({
28-
params: {
29-
address: '0x1234567890abcdef',
30-
chainId: '0x1',
31-
symbol: 'TEST',
32-
decimals: 18,
33-
name: 'Test Token',
34-
isNative: false,
35-
securityData: {
36-
resultType: 'Verified',
37-
maliciousScore: '0',
38-
features: [
39-
{
40-
featureId: 'VERIFIED_CONTRACT',
41-
type: 'Info',
42-
description: 'Contract is verified',
43-
},
44-
{
45-
featureId: 'HIGH_REPUTATION_TOKEN',
46-
type: 'Benign',
47-
description: 'Token has high reputation',
48-
},
49-
],
50-
fees: {
51-
buy: 1,
52-
sell: 2,
53-
transfer: 0,
54-
transferFeeMaxAmount: null,
55-
},
56-
financialStats: {
57-
supply: 1000000000000000000000000,
58-
holdersCount: 5000,
59-
topHolders: [
60-
{
61-
label: 'Holder 1',
62-
name: null,
63-
address: '0xholder1',
64-
holdingPercentage: 15,
65-
},
66-
{
67-
label: 'Holder 2',
68-
name: null,
69-
address: '0xholder2',
70-
holdingPercentage: 10,
71-
},
72-
],
73-
tradeVolume24h: 1000000,
74-
lockedLiquidityPct: 80,
75-
markets: [],
76-
},
77-
metadata: {
78-
externalLinks: {
79-
homepage: 'https://example.com',
80-
twitterPage: 'testtoken',
81-
telegramChannelId: 'testtoken',
82-
},
83-
},
84-
created: '2023-01-01T00:00:00Z',
85-
},
86-
},
90+
params: mockRouteParams,
8791
}),
8892
}));
8993

@@ -109,14 +113,23 @@ jest.mock('react-redux', () => ({
109113
}),
110114
}));
111115

112-
jest.mock('../../../hooks/useBlockExplorer', () => ({
113-
__esModule: true,
114-
default: () => ({
115-
getBlockExplorerTokenUrl: (address: string) =>
116-
`https://etherscan.io/address/${address}`,
117-
getBlockExplorerName: () => 'Etherscan',
118-
}),
119-
}));
116+
jest.mock('../../../hooks/useBlockExplorer', () => {
117+
const mockGetBlockExplorerTokenUrl = jest.fn(
118+
(address: string) => `https://etherscan.io/address/${address}`,
119+
);
120+
return {
121+
__esModule: true,
122+
default: () => ({
123+
getBlockExplorerTokenUrl: mockGetBlockExplorerTokenUrl,
124+
getBlockExplorerName: () => 'Etherscan',
125+
}),
126+
mockGetBlockExplorerTokenUrl,
127+
};
128+
});
129+
130+
const { mockGetBlockExplorerTokenUrl } = jest.requireMock(
131+
'../../../hooks/useBlockExplorer',
132+
) as { mockGetBlockExplorerTokenUrl: jest.Mock };
120133

121134
jest.mock('../../TokenDetails/components/TokenDetailsStickyFooter', () => ({
122135
__esModule: true,
@@ -134,6 +147,7 @@ jest.mock('../../TokenDetails/hooks/useTokenActions', () => ({
134147

135148
describe('SecurityTrustScreen', () => {
136149
beforeEach(() => {
150+
mockRouteParams = createDefaultRouteParams();
137151
jest.clearAllMocks();
138152
});
139153

@@ -257,4 +271,27 @@ describe('SecurityTrustScreen', () => {
257271
expect(getByText('Published contract')).toBeTruthy();
258272
expect(getByText('Established reputation')).toBeTruthy();
259273
});
274+
275+
it('passes EVM token address unchanged to getBlockExplorerTokenUrl', () => {
276+
render(<SecurityTrustScreen />);
277+
278+
expect(mockGetBlockExplorerTokenUrl).toHaveBeenCalledWith(
279+
'0x1234567890abcdef',
280+
'0x1',
281+
);
282+
});
283+
284+
it('extracts asset reference from Solana CAIP asset type for block explorer URL', () => {
285+
const solanaMint = 'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB';
286+
const solanaChainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
287+
mockRouteParams.address = `${solanaChainId}/token:${solanaMint}`;
288+
mockRouteParams.chainId = solanaChainId;
289+
290+
render(<SecurityTrustScreen />);
291+
292+
expect(mockGetBlockExplorerTokenUrl).toHaveBeenCalledWith(
293+
solanaMint,
294+
solanaChainId,
295+
);
296+
});
260297
});

app/components/UI/SecurityTrust/Views/SecurityTrustScreen.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
import TokenDetailsStickyFooter from '../../TokenDetails/components/TokenDetailsStickyFooter';
4242
import useBlockExplorer from '../../../hooks/useBlockExplorer';
4343
import { useTokenActions } from '../../TokenDetails/hooks/useTokenActions';
44+
import { isCaipAssetType, parseCaipAssetType } from '@metamask/utils';
4445

4546
const SectionHeader: React.FC<{ title: string }> = ({ title }) => (
4647
<Text
@@ -584,8 +585,11 @@ const SecurityTrustScreen: React.FC = () => {
584585
)}
585586
{Boolean(params?.address && !params.isNative) &&
586587
(() => {
588+
const tokenAddress = isCaipAssetType(params.address)
589+
? parseCaipAssetType(params.address).assetReference
590+
: params.address;
587591
const blockExplorerUrl = explorer.getBlockExplorerTokenUrl(
588-
params.address,
592+
tokenAddress,
589593
params.chainId,
590594
);
591595
const blockExplorerName = explorer.getBlockExplorerName(

0 commit comments

Comments
 (0)