Skip to content

Commit e0a8b91

Browse files
fix: cp-12.18.0 graceful fallbacks for no rates (#32731)
## **Description** During beta testing, we noticed an intermittent bug where asset rates were not propagating through the app. This was causing all fiat balances to render as `$0.00` as we were falling back to a zero value in our selector. This is potentially very scary for a user, and a pretty bad look for us. This falls back to a "no conversion rate available" value instead during this scenario to account for the situation where rates are undefined at the application layer. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/32731?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Hardcode `const rate = assetRates?.[assetId]?.rate;` to `const rate = undefined` in `ui/selectors/assets.ts` 2. Notice that `$0.00` is not rendered, but instead, an empty space 3. Hardcode return value of `getAssetsRates` to an empty object {} 4. Notice that Main aggregated fiat balance changes to native token balance 5. Also validate that there are no regressions with EVM ## **Screenshots/Recordings** ### **Before** <img width="1032" alt="Screenshot 2025-05-08 at 1 18 16 PM" src="https://github.com/user-attachments/assets/90ee609e-464c-4600-8385-95c6cb29cd58" /> ### **After** All rates undefined: Fallback text in token list item, fallback to native token balance in main view: <img width="645" alt="Screenshot 2025-05-09 at 10 15 49 AM" src="https://github.com/user-attachments/assets/77d445ff-0a47-4041-8b04-db740133117b" /> Some rates undefined: Fallback text in token list item, aggregated fiat balance remains <img width="641" alt="Screenshot 2025-05-09 at 10 22 12 AM" src="https://github.com/user-attachments/assets/03674da0-1e59-484c-9965-d32f34ef7904" /> Token Details: <img width="648" alt="Screenshot 2025-05-09 at 10 20 54 AM" src="https://github.com/user-attachments/assets/b21ff8e9-df7e-4959-aaa6-0b48c28fdb0c" /> Solana devnet <img width="399" alt="Screenshot 2025-05-09 at 10 25 06 AM" src="https://github.com/user-attachments/assets/e49781c5-fb35-48ab-84c8-d974aca8f6bf" /> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/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. --------- Co-authored-by: sahar-fehri <[email protected]>
1 parent 74c72ac commit e0a8b91

File tree

10 files changed

+80
-25
lines changed

10 files changed

+80
-25
lines changed

ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ exports[`Token Cell should match snapshot 1`] = `
5151
TEST
5252
</p>
5353
<p
54-
class="mm-box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--text-align-end mm-box--color-text-default"
54+
class="mm-box mm-text mm-text--body-sm mm-text--font-weight-normal mm-text--text-align-end mm-box--color-text-default"
5555
data-testid="multichain-token-list-item-secondary-value"
56-
/>
56+
>
57+
No conversion rate available
58+
</p>
5759
</div>
5860
<div
5961
class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between"

ui/components/app/assets/token-cell/cells/token-cell-secondary-display.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { getCurrencyRates } from '../../../../../selectors';
1818
import { getMultichainIsEvm } from '../../../../../selectors/multichain';
1919
import { TokenFiatDisplayInfo } from '../../types';
20+
import { useI18nContext } from '../../../../../hooks/useI18nContext';
2021

2122
type TokenCellSecondaryDisplayProps = {
2223
token: TokenFiatDisplayInfo;
@@ -30,6 +31,7 @@ export const TokenCellSecondaryDisplay = React.memo(
3031
handleScamWarningModal,
3132
privacyMode,
3233
}: TokenCellSecondaryDisplayProps) => {
34+
const t = useI18nContext();
3335
const isEvm = useSelector(getMultichainIsEvm);
3436
const currencyRates = useSelector(getCurrencyRates);
3537

@@ -59,15 +61,15 @@ export const TokenCellSecondaryDisplay = React.memo(
5961
// secondary display text
6062
return (
6163
<SensitiveText
62-
fontWeight={FontWeight.Medium}
63-
variant={TextVariant.bodyMd}
64+
fontWeight={token.secondary ? FontWeight.Medium : FontWeight.Normal}
65+
variant={token.secondary ? TextVariant.bodyMd : TextVariant.bodySm}
6466
textAlign={TextAlign.End}
6567
data-testid="multichain-token-list-item-secondary-value"
6668
ellipsis={token.isStakeable}
6769
isHidden={privacyMode}
6870
length={SensitiveTextLength.Medium}
6971
>
70-
{token.secondary}
72+
{token.secondary || t('noConversionRateAvailable')}
7173
</SensitiveText>
7274
);
7375
},

ui/components/app/assets/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CaipAssetType, CaipChainId, Hex } from '@metamask/utils';
33
// Common mixin for primary and secondary display values
44
export type TokenDisplayValues = {
55
primary: string;
6-
secondary: number;
6+
secondary: number | null;
77
string?: string;
88
};
99

ui/components/ui/aggregated-balance/aggregated-balance.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from '../../../ducks/metamask/metamask';
2323
import {
2424
getAccountAssets,
25+
getAssetsRates,
2526
getMultichainAggregatedBalance,
2627
getMultichainNativeTokenBalance,
2728
} from '../../../selectors/assets';
@@ -54,6 +55,8 @@ export const AggregatedBalance = ({
5455
const multichainNativeTokenBalance = useSelector((state) =>
5556
getMultichainNativeTokenBalance(state, selectedAccount),
5657
);
58+
const multichainAssetsRates = useSelector(getAssetsRates);
59+
const isNonEvmRatesAvailable = Object.keys(multichainAssetsRates).length > 0;
5760

5861
const formattedFiatDisplay = formatWithThreshold(
5962
multichainAggregatedBalance,
@@ -96,7 +99,7 @@ export const AggregatedBalance = ({
9699
isHidden={privacyMode}
97100
data-testid="account-value-and-suffix"
98101
>
99-
{showNativeTokenAsMainBalance
102+
{showNativeTokenAsMainBalance || !isNonEvmRatesAvailable
100103
? formattedTokenDisplay
101104
: formattedFiatDisplay}
102105
</SensitiveText>
@@ -105,7 +108,7 @@ export const AggregatedBalance = ({
105108
variant={TextVariant.inherit}
106109
isHidden={privacyMode}
107110
>
108-
{showNativeTokenAsMainBalance
111+
{showNativeTokenAsMainBalance || !isNonEvmRatesAvailable
109112
? currentNetwork.network.ticker
110113
: currentCurrency.toUpperCase()}
111114
</SensitiveText>

ui/components/ui/aggregated-balance/index.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,28 @@ describe('AggregatedBalance Component', () => {
212212
);
213213
expect(screen.getByText('SOL')).toBeInTheDocument();
214214
});
215+
216+
it('renders token balance when non evm rates are not available', () => {
217+
renderWithProvider(
218+
<AggregatedBalance
219+
classPrefix="test"
220+
balanceIsCached={false}
221+
handleSensitiveToggle={jest.fn()}
222+
/>,
223+
getStore({
224+
metamask: {
225+
...mockMetamaskStore,
226+
preferences: {
227+
showNativeTokenAsMainBalance: false,
228+
},
229+
conversionRates: {},
230+
},
231+
}),
232+
);
233+
234+
expect(screen.getByTestId('account-value-and-suffix')).toHaveTextContent(
235+
'1',
236+
);
237+
expect(screen.getByText('SOL')).toBeInTheDocument();
238+
});
215239
});

ui/pages/defi/components/__snapshots__/defi-details-list.test.tsx.snap

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,11 @@ exports[`DeFiDetailsPage renders defi details list 1`] = `
6363
Ethereum
6464
</p>
6565
<p
66-
class="mm-box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--text-align-end mm-box--color-text-default"
66+
class="mm-box mm-text mm-text--body-sm mm-text--font-weight-normal mm-text--text-align-end mm-box--color-text-default"
6767
data-testid="multichain-token-list-item-secondary-value"
68-
/>
68+
>
69+
No conversion rate available
70+
</p>
6971
</div>
7072
<div
7173
class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between"
@@ -149,9 +151,11 @@ exports[`DeFiDetailsPage renders defi details list 1`] = `
149151
Ethereum
150152
</p>
151153
<p
152-
class="mm-box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--text-align-end mm-box--color-text-default"
154+
class="mm-box mm-text mm-text--body-sm mm-text--font-weight-normal mm-text--text-align-end mm-box--color-text-default"
153155
data-testid="multichain-token-list-item-secondary-value"
154-
/>
156+
>
157+
No conversion rate available
158+
</p>
155159
</div>
156160
<div
157161
class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between"

ui/pages/defi/components/__snapshots__/defi-details-page.test.tsx.snap

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,11 @@ exports[`DeFiDetailsPage renders defi asset page 1`] = `
121121
</div>
122122
</div>
123123
<p
124-
class="mm-box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--text-align-end mm-box--color-text-default"
124+
class="mm-box mm-text mm-text--body-sm mm-text--font-weight-normal mm-text--text-align-end mm-box--color-text-default"
125125
data-testid="multichain-token-list-item-secondary-value"
126-
/>
126+
>
127+
No conversion rate available
128+
</p>
127129
</div>
128130
<div
129131
class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between"

ui/pages/routes/routes.component.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,21 @@ describe('toast display', () => {
308308
},
309309
},
310310
},
311+
conversionRates: {
312+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:105': {
313+
conversionTime: 1745405595549,
314+
currency: 'swift:0/iso4217:USD',
315+
expirationTime: 1745409195549,
316+
rate: '151.36',
317+
},
318+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v':
319+
{
320+
conversionTime: 1745405595549,
321+
currency: 'swift:0/iso4217:USD',
322+
expirationTime: 1745409195549,
323+
rate: '1.00',
324+
},
325+
},
311326
},
312327
activeTab: {
313328
id: 2143026027,

ui/selectors/assets.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ describe('getMultiChainAssets', () => {
160160
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
161161
isNative: true,
162162
primary: '0.051724127',
163-
secondary: 0,
163+
secondary: null,
164164
}),
165165
expect.objectContaining({
166166
title: 'USDC',
@@ -172,7 +172,7 @@ describe('getMultiChainAssets', () => {
172172
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
173173
isNative: false,
174174
primary: '0',
175-
secondary: 0,
175+
secondary: null,
176176
}),
177177
]),
178178
);
@@ -204,7 +204,7 @@ describe('getMultiChainAssets', () => {
204204
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
205205
isNative: true,
206206
primary: '0.051724127',
207-
secondary: 0,
207+
secondary: null,
208208
}),
209209
]),
210210
);
@@ -347,11 +347,11 @@ describe('getTokenByAccountAndAddressAndChainId', () => {
347347
isNative: true,
348348
isStakeable: false,
349349
primary: '0',
350-
secondary: 0,
350+
secondary: null,
351351
string: '',
352352
symbol: 'TKN1',
353353
title: 'Token 1',
354-
tokenFiatAmount: 0,
354+
tokenFiatAmount: null,
355355
});
356356
});
357357
});
@@ -378,11 +378,11 @@ describe('getTokenByAccountAndAddressAndChainId', () => {
378378
isNative: true,
379379
isStakeable: false,
380380
primary: '0',
381-
secondary: 0,
381+
secondary: null,
382382
string: '',
383383
symbol: 'TKN1',
384384
title: 'Token 1',
385-
tokenFiatAmount: 0,
385+
tokenFiatAmount: null,
386386
});
387387
});
388388
});

ui/selectors/assets.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,11 @@ export const getMultiChainAssets = createDeepEqualSelector(
210210
const { chainId, assetNamespace } = parseCaipAssetType(assetId);
211211
const isNative = assetNamespace === 'slip44';
212212
const balance = balances?.[assetId] || { amount: '0', unit: '' };
213-
const rate = assetRates?.[assetId]?.rate || '0';
214-
const balanceInFiat = new BigNumber(balance.amount).times(rate);
213+
const rate = assetRates?.[assetId]?.rate;
214+
215+
const balanceInFiat = rate
216+
? new BigNumber(balance.amount).times(rate).toNumber()
217+
: null;
215218

216219
const assetMetadataFallback = {
217220
name: balance.unit,
@@ -232,9 +235,9 @@ export const getMultiChainAssets = createDeepEqualSelector(
232235
chainId,
233236
isNative,
234237
primary: balance.amount,
235-
secondary: balanceInFiat.toNumber(),
238+
secondary: balanceInFiat,
236239
string: '',
237-
tokenFiatAmount: balanceInFiat.toNumber(), // for now we are keeping this is to satisfy sort, this should be fiat amount
240+
tokenFiatAmount: balanceInFiat,
238241
isStakeable: false,
239242
});
240243
}

0 commit comments

Comments
 (0)