Skip to content

Commit 86633c5

Browse files
authored
fix(bridge): fix: cp-7.47.3 prevent crash when viewing Solana asset details (#16770)
## **Description** Fixes a crash that occurs when viewing asset details for Solana tokens in the Bridge flow. The issue was caused by `getTokenDetails` assuming all non-EVM asset addresses are already in CAIP format, when the Bridge API sometimes returns raw Solana addresses. ## **Related issues** Fixes #16734 Fixes [SWAPS-2525](https://consensyssoftware.atlassian.net/browse/SWAPS-2525) ## **Manual testing steps** 1. Navigate to Bridge feature 2. Select Solana as source or destination chain 3. Select any Solana token from the token list 4. Tap on the asset detail info button 5. Verify the asset details screen loads without crashing 6. Verify contract address and other details display correctly ## **Screenshots/Recordings** <!-- Add screenshots or recordings demonstrating the fix --> **Before:** https://github.com/user-attachments/assets/cf41e9fa-ae3d-4336-aefe-40017e831364 **After:** https://github.com/user-attachments/assets/ccacf57e-1971-42c4-a3d7-a3978e7c954b ## **Pre-merge author checklist** - [x] I've followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md) - [x] I've clearly explained what problem this PR solves - [x] I've linked the issue that this PR fixes - [x] I've included manual testing steps - [x] I've included screenshots/recordings if applicable - [x] I've added unit tests if applicable ## **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 the issue linked above - [ ] I've checked that all unit tests pass ## **Solution Details** ### **Root Cause** The `getTokenDetails` function was calling `parseCaipAssetType` directly on `asset.address` for non-EVM assets, assuming it was already in CAIP format. However, the Bridge API sometimes returns raw Solana addresses instead of CAIP-formatted addresses, causing `parseCaipAssetType` to throw an error. ### **Fix Applied** Implemented the same defensive approach used in `useTokenHistoricalPrices`: ```typescript // Detect if address is already in CAIP format const isCaipAssetType = asset.address.startsWith(`${asset.chainId}`); // Convert to CAIP format if needed const normalizedCaipAssetTypeAddress = isCaipAssetType ? asset.address : `${asset.chainId}/token:${asset.address}`; ``` ### **Benefits** - ✅ **Fixes crash**: Solana asset details now load without errors - ✅ **Backward compatible**: Handles both raw and CAIP format addresses - ✅ **Consistent pattern**: Uses same approach as `useTokenHistoricalPrices` - ✅ **Zero breaking changes**: All existing functionality preserved - ✅ **Future-proof**: Works with any non-EVM chain automatically ### **Testing** - Added comprehensive unit tests covering both scenarios - All 12 tests pass including new test cases - Verified with existing Bridge test suite - Manual testing confirms crash is resolved [SWAPS-2525]: https://consensyssoftware.atlassian.net/browse/SWAPS-2525?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent aa2e03a commit 86633c5

File tree

3 files changed

+120
-21
lines changed

3 files changed

+120
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- feat(bridge): add "hardware wallets not supported" error when attempting Solana swap/bridge ([#15743](https://github.com/MetaMask/metamask-mobile/pull/15743))
1111
- feat(bridge): fetch all tokens for bridge input([#15993](https://github.com/MetaMask/metamask-mobile/pull/15993))
1212
- fix(bridge): don't show currency value unless amount is above zero ([#15798](https://github.com/MetaMask/metamask-mobile/pull/15798))
13+
- fix(bridge): prevent crash when viewing Solana asset details ([#16770](https://github.com/MetaMask/metamask-mobile/pull/16770))
1314

1415
## [7.47.1]
1516

app/components/UI/AssetOverview/utils/getTokenDetails.test.ts

Lines changed: 110 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ describe('getTokenDetails', () => {
3030
aggregators: ['uniswap', '1inch'],
3131
};
3232

33+
beforeEach(() => {
34+
// Clear mock calls before each test for proper isolation
35+
(parseCaipAssetType as jest.Mock).mockClear();
36+
});
37+
3338
describe('Network-specific behavior', () => {
3439
it('should format token details for non-EVM networks for spl token', () => {
3540
(parseCaipAssetType as jest.Mock).mockReturnValue({
@@ -114,6 +119,111 @@ describe('getTokenDetails', () => {
114119
tokenList: 'uniswap, 1inch',
115120
});
116121
});
122+
123+
it('converts raw Solana address to CAIP format for non-EVM networks', () => {
124+
const solanaAsset: TokenI = {
125+
...mockAsset,
126+
address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // Raw Solana address
127+
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana chainId
128+
};
129+
130+
(parseCaipAssetType as jest.Mock).mockReturnValue({
131+
assetNamespace: 'token',
132+
assetReference: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
133+
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
134+
chain: {
135+
namespace: 'solana',
136+
reference: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
137+
},
138+
});
139+
140+
const result = getTokenDetails(
141+
solanaAsset,
142+
true, // isNonEvmAsset
143+
undefined,
144+
mockEvmMetadata,
145+
);
146+
147+
// Verify parseCaipAssetType was called with the converted CAIP format
148+
expect(parseCaipAssetType).toHaveBeenCalledWith(
149+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
150+
);
151+
152+
expect(result).toEqual({
153+
contractAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
154+
tokenDecimal: 18,
155+
tokenList: 'uniswap, 1inch',
156+
});
157+
});
158+
159+
it('handles address already in CAIP format for non-EVM networks', () => {
160+
// Test that addresses already in CAIP format are handled correctly
161+
const solanaAssetWithCaipAddress: TokenI = {
162+
...mockAsset,
163+
address:
164+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // Already CAIP format
165+
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
166+
};
167+
168+
(parseCaipAssetType as jest.Mock).mockReturnValue({
169+
assetNamespace: 'token',
170+
assetReference: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
171+
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
172+
chain: {
173+
namespace: 'solana',
174+
reference: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
175+
},
176+
});
177+
178+
const result = getTokenDetails(
179+
solanaAssetWithCaipAddress,
180+
true, // isNonEvmAsset
181+
undefined,
182+
mockEvmMetadata,
183+
);
184+
185+
// Verify parseCaipAssetType was called with the original CAIP address (no conversion needed)
186+
expect(parseCaipAssetType).toHaveBeenCalledWith(
187+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
188+
);
189+
190+
expect(result).toEqual({
191+
contractAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
192+
tokenDecimal: 18,
193+
tokenList: 'uniswap, 1inch',
194+
});
195+
});
196+
197+
it('should handle empty address in asset', () => {
198+
const assetWithoutAddress: TokenI = {
199+
...mockAsset,
200+
address: '',
201+
};
202+
203+
// Mock for empty address case - should return null/empty assetReference
204+
(parseCaipAssetType as jest.Mock).mockReturnValue({
205+
assetNamespace: 'token',
206+
assetReference: '', // Empty asset reference for empty address
207+
chainId: 'test:test',
208+
chain: {
209+
namespace: 'slip44',
210+
reference: '0x123',
211+
},
212+
});
213+
214+
const result = getTokenDetails(
215+
assetWithoutAddress,
216+
true, // isNonEvmAsset
217+
undefined,
218+
mockEvmMetadata,
219+
);
220+
221+
expect(result).toEqual({
222+
contractAddress: null, // Empty assetReference should result in null
223+
tokenDecimal: 18,
224+
tokenList: 'uniswap, 1inch',
225+
});
226+
});
117227
});
118228

119229
describe('Metadata handling', () => {
@@ -177,26 +287,6 @@ describe('getTokenDetails', () => {
177287
});
178288

179289
describe('Asset property handling', () => {
180-
it('should handle empty address in asset', () => {
181-
const assetWithoutAddress: TokenI = {
182-
...mockAsset,
183-
address: '',
184-
};
185-
186-
const result = getTokenDetails(
187-
assetWithoutAddress,
188-
true, // isNonEvmAsset
189-
undefined,
190-
mockEvmMetadata,
191-
);
192-
193-
expect(result).toEqual({
194-
contractAddress: null,
195-
tokenDecimal: 18,
196-
tokenList: 'uniswap, 1inch',
197-
});
198-
});
199-
200290
it('should handle zero decimals in asset', () => {
201291
(parseCaipAssetType as jest.Mock).mockReturnValue({
202292
assetNamespace: 'token',

app/components/UI/AssetOverview/utils/getTokenDetails.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,16 @@ export const getTokenDetails = (
1010
tokenMetadata: Record<string, string | number | string[]>,
1111
): TokenDetails => {
1212
if (isNonEvmAsset) {
13+
// Use the same approach as useTokenHistoricalPrices
14+
const isCaipAssetType = asset.address.startsWith(`${asset.chainId}`);
15+
16+
// Ensure we have proper CAIP format address for parsing
17+
const normalizedCaipAssetTypeAddress = isCaipAssetType
18+
? asset.address
19+
: `${asset.chainId}/token:${asset.address}`;
20+
1321
const { assetNamespace, assetReference } = parseCaipAssetType(
14-
asset.address as `${string}:${string}/${string}:${string}`,
22+
normalizedCaipAssetTypeAddress as `${string}:${string}/${string}:${string}`,
1523
);
1624
const isNative = assetNamespace === 'slip44';
1725
return {

0 commit comments

Comments
 (0)