Skip to content

Commit fc6641a

Browse files
authored
fix: use currency rate fallback for native token fiat when market data missing (#7636)
## Explanation The `selectAssetsBySelectedAccountGroup` selector was returning `undefined` for the `fiat` field on native tokens for certain chains (like Ink chain `0xdef1`) even though the currency conversion rate was available. **Problem:** The `getFiatBalanceForEvmToken` function requires `marketData[chainId][tokenAddress]` to exist in order to calculate fiat values. For native tokens, this market data entry should contain `price: 1` (since 1 ETH = 1 ETH). However, some chains don't have this native token entry in `marketData`, causing the function to return `undefined` even when `currencyRates` has the conversion rate for the native currency symbol (e.g., ETH). **Solution:** Added a fallback mechanism specifically for native tokens: when `marketData` doesn't have an entry for the native token but `currencyRates` has the conversion rate for the native currency symbol, the function now uses `price = 1` and multiplies directly by the currency rate. This ensures native token fiat values are calculated correctly for chains like Ink (`0xdef1`) that use ETH as their native currency but don't have explicit market data entries. UI extension: MetaMask/metamask-extension#39269 ## References - Fixes native token fiat calculation for Ink chain and similar L2s that share the same native currency as mainnet but lack market data entries ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Ensures native token fiat is calculated even when market data is absent by leveraging the native currency conversion rate. > > - Update `getFiatBalanceForEvmToken` to accept `nativeCurrencySymbol` and, when no market data exists for a native token, compute fiat using `currencyRates` (price=1 in native units); return `undefined` if rate also missing > - Pass native currency from `selectAssetsBySelectedAccountGroup` into `getFiatBalanceForEvmToken` > - Add unit tests covering fallback calculation and missing-rate behavior > - Update `CHANGELOG.md` under Unreleased -> Fixed > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7f46dc3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 5972fe0 commit fc6641a

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed

packages/assets-controllers/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Add BOB (0xed88) mapping to eip155:60808/erc20:0x0000000000000000000000000000000000000000 ([#7635](https://github.com/MetaMask/core/pull/7635))
1313

14+
### Fixed
15+
16+
- Fix native token fiat calculation for chains without market data by using currency rate fallback ([#7636](https://github.com/MetaMask/core/pull/7636))
17+
1418
## [95.2.0]
1519

1620
### Added

packages/assets-controllers/src/selectors/token-selectors.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,5 +1024,92 @@ describe('token-selectors', () => {
10241024
expect(result[TrxScope.Nile].length > 1).toBe(true);
10251025
expect(result[TrxScope.Shasta].length > 1).toBe(true);
10261026
});
1027+
1028+
it('calculates fiat for native token using currency rate fallback when market data is missing', () => {
1029+
// Setup: Add a new chain (Ink chain 0xdef1) with native balance but NO market data
1030+
const inkChainId = '0xdef1' as Hex;
1031+
const stateWithInkChain = {
1032+
...mockedMergedState,
1033+
// Add Ink chain to network configuration
1034+
networkConfigurationsByChainId: {
1035+
...mockNetworkControllerState.networkConfigurationsByChainId,
1036+
[inkChainId]: {
1037+
nativeCurrency: 'ETH', // Ink chain uses ETH as native currency
1038+
},
1039+
},
1040+
// Add native balance for the account on Ink chain
1041+
accountsByChainId: {
1042+
...mockAccountsTrackerControllerState.accountsByChainId,
1043+
[inkChainId]: {
1044+
'0x2bd63233fe369b0f13eaf25292af5a9b63d2b7ab': {
1045+
balance: '0xDE0B6B3A7640000', // 1 ETH (1000000000000000000 wei)
1046+
},
1047+
},
1048+
},
1049+
// Market data does NOT include Ink chain native token
1050+
// (using existing mockTokenRatesControllerState which doesn't have 0xdef1)
1051+
};
1052+
1053+
const result = selectAssetsBySelectedAccountGroup(stateWithInkChain);
1054+
1055+
// Find the Ink chain native token
1056+
const inkNativeToken = result[inkChainId]?.find(
1057+
(asset) => asset.isNative,
1058+
);
1059+
1060+
// Should have fiat calculated using the ETH currency rate fallback
1061+
expect(inkNativeToken).toStrictEqual({
1062+
accountType: 'eip155:eoa',
1063+
accountId: 'd7f11451-9d79-4df4-a012-afd253443639',
1064+
chainId: inkChainId,
1065+
assetId: '0x0000000000000000000000000000000000000000',
1066+
address: '0x0000000000000000000000000000000000000000',
1067+
image: '',
1068+
name: 'Ethereum',
1069+
symbol: 'ETH',
1070+
isNative: true,
1071+
decimals: 18,
1072+
rawBalance: '0xDE0B6B3A7640000',
1073+
balance: '1',
1074+
fiat: {
1075+
balance: 2400, // 1 ETH * 2400 USD/ETH
1076+
conversionRate: 2400,
1077+
currency: 'USD',
1078+
},
1079+
});
1080+
});
1081+
1082+
it('returns undefined fiat for native token when both market data and currency rate are missing', () => {
1083+
const inkChainId = '0xdef1' as Hex;
1084+
const stateWithMissingCurrencyRate = {
1085+
...mockedMergedState,
1086+
networkConfigurationsByChainId: {
1087+
...mockNetworkControllerState.networkConfigurationsByChainId,
1088+
[inkChainId]: {
1089+
nativeCurrency: 'INK', // Custom native currency with no currency rate
1090+
},
1091+
},
1092+
accountsByChainId: {
1093+
...mockAccountsTrackerControllerState.accountsByChainId,
1094+
[inkChainId]: {
1095+
'0x2bd63233fe369b0f13eaf25292af5a9b63d2b7ab': {
1096+
balance: '0xDE0B6B3A7640000',
1097+
},
1098+
},
1099+
},
1100+
// currencyRates doesn't have 'INK', only 'ETH'
1101+
};
1102+
1103+
const result = selectAssetsBySelectedAccountGroup(
1104+
stateWithMissingCurrencyRate,
1105+
);
1106+
1107+
const inkNativeToken = result[inkChainId]?.find(
1108+
(asset) => asset.isNative,
1109+
);
1110+
1111+
// Should have undefined fiat since there's no currency rate for 'INK'
1112+
expect(inkNativeToken?.fiat).toBeUndefined();
1113+
});
10271114
});
10281115
});

packages/assets-controllers/src/selectors/token-selectors.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ const selectAllEvmAccountNativeBalances = createAssetListSelector(
208208
currencyRates,
209209
chainId,
210210
nativeToken.address,
211+
nativeCurrency, // Pass native currency symbol for fallback when market data is missing
211212
);
212213

213214
groupChainAssets.push({
@@ -564,6 +565,7 @@ function mergeAssets(
564565
* @param currencyRates - The currency rates for the token
565566
* @param chainId - The chain id of the token
566567
* @param tokenAddress - The address of the token
568+
* @param nativeCurrencySymbol - The native currency symbol (e.g., 'ETH', 'BNB') - used for fallback when market data is missing for native tokens
567569
* @returns The price and currency of the token in the current currency. Returns undefined if the asset is not found in the market data or currency rates.
568570
*/
569571
function getFiatBalanceForEvmToken(
@@ -573,9 +575,29 @@ function getFiatBalanceForEvmToken(
573575
currencyRates: CurrencyRateState['currencyRates'],
574576
chainId: Hex,
575577
tokenAddress: Hex,
578+
nativeCurrencySymbol?: string,
576579
) {
577580
const tokenMarketData = marketData[chainId]?.[tokenAddress];
578581

582+
// For native tokens: if no market data exists, use price=1 and look up currency rate directly
583+
// This is because native tokens are priced in themselves (1 ETH = 1 ETH)
584+
if (!tokenMarketData && nativeCurrencySymbol) {
585+
const currencyRate = currencyRates[nativeCurrencySymbol];
586+
587+
if (!currencyRate?.conversionRate) {
588+
return undefined;
589+
}
590+
591+
const fiatBalance =
592+
(convertHexToDecimal(rawBalance) / 10 ** decimals) *
593+
currencyRate.conversionRate;
594+
595+
return {
596+
balance: fiatBalance,
597+
conversionRate: currencyRate.conversionRate,
598+
};
599+
}
600+
579601
if (!tokenMarketData) {
580602
return undefined;
581603
}

0 commit comments

Comments
 (0)